# m2-GTK4 — GTK4 bindings for GNU Modula-2 Thin `DEFINITION MODULE FOR "C"` bindings of **GTK 4** for `gm2` (GCC 16 / GNU Modula-2), plus a small pure-Modula-2 helper layer. The first slice covers the canonical GTK4 application shape (`GtkApplication` + `GtkWindow` + `GtkLabel` / `GtkButton` / `GtkBox`), a working `"activate"` / `"clicked"` signal path, and the GLib/GObject runtime underneath it. The second slice adds the common input widgets: `GtkEntry`/`GtkEditable`, `GtkCheckButton`, `GtkSwitch`, `GtkScale` (`GtkRange`), plus a shared `GtkEnums` module. The third slice adds the layout containers: `GtkGrid`, `GtkStack` + `GtkStackSwitcher`, `GtkNotebook`, `GtkScrolledWindow`. The fourth slice adds actions, menus and accelerators: GIO's `GAction` / `GActionMap` / `GSimpleAction`, `GMenu` / `GMenuModel` / `GVariant`, plus `GtkApplicationWindow` and `GtkPopoverMenuBar`. The fifth slice switches the project to the ISO dialect (`-fiso`) and adds per-connection signal closures (`GtkClosures`) and window chrome (`GtkHeaderBar`, `GtkMenuButton`, `gtk_window_set_titlebar`). The sixth slice adds more inputs: `GtkSpinButton`, `GtkSearchEntry`, `GtkDropDown` (with `GtkStringList` / `GListModel`) and `GtkTextView` + `GtkTextBuffer`. The seventh slice adds more containers: `GtkFrame`, `GtkExpander`, `GtkPaned`, `GtkOverlay`, `GtkListBox` (+ `GtkListBoxRow`). The eighth slice adds GIO extras: `GListStore` (+ `GtkStringObject`), `GFile`, `GtkFileDialog` and `GSettings` (+ `GSettingsSchema`). The ninth slice adds model-backed views: `GtkListView`, `GtkGridView`, `GtkColumnView` (+ `GtkColumnViewColumn` / `GtkColumnViewCell`), the selection models `GtkSingleSelection` / `GtkNoSelection`, and the `GtkSignalListItemFactory` / `GtkListItem` pair. A [`tools/gir2def.py`](#generator-broad-coverage) generator additionally emits broad `.def` coverage from GObject-Introspection `.gir` XML. A tenth slice adds **libadwaita** (AdwApplication, header/toolbar, preferences rows, toast, about/message dialogs) and **GError** handling plus an ownership helper layer. Verified with gm2 16.0.1 (experimental) and GTK 4.18.6 on Linux. ## Status | Module | C header | Contents | |---|---|---| | `GLib` | `` | base C types, `GMainLoop`, timeouts/idle sources, memory, strings, clock | | `GObject` | `` | `ref`/`unref`, `GType`, `g_object_new`, `g_signal_connect_data`, handler disconnect, properties | | `Gio` | `` | `GApplication` flags, `run`/`quit`/`hold`/`release` | | `Gtk` | `` | `gtk_init`, library version helpers | | `GtkApplication` | `` | create/add/remove windows | | `GtkWidget` | `` | visibility, sensitivity, margins, expand, align, size | | `GtkWindow` | `` | create, title, size, child, present/close/destroy | | `GtkBox` | `` | orientation, append/prepend/remove, spacing | | `GtkLabel` | `` | text, markup | | `GtkButton` | `` | label buttons | | `GtkEnums` | `` | orientation, alignment, position constants | | `GtkEditable` | `` | text get/set, editable, alignment, selection | | `GtkEntry` | `` | placeholder, max length, visibility, progress | | `GtkCheckButton` | `` | active, label, inconsistent | | `GtkSwitch` | `` | active / state | | `GtkRange` | `` | numeric value (base of GtkScale) | | `GtkScale` | `` | slider, digits, value position | | `GtkGrid` | `` | cell attach, spacing, homogeneous | | `GtkStack` | `` | named/titled pages, visible child, transitions | | `GtkStackSwitcher` | `` | button row that drives a stack | | `GtkNotebook` | `` | tabbed pages | | `GtkScrolledWindow` | `` | scrollable viewport, policy | | `GtkApplicationWindow` | `` | app window, window-scoped actions, show-menubar | | `GtkPopoverMenuBar` | `` | in-window menu bar from a model | | `GtkHeaderBar` | `` | title widget, pack start/end, title buttons, decoration layout | | `GtkMenuButton` | `` | button that pops up a menu model | | `GAction` | `` | activate, name, enabled, state | | `GActionMap` | `` | add / lookup / remove named actions | | `GSimpleAction` | `` | parameterless and stateful actions | | `GMenu` | `` | GMenu + GMenuItem builder | | `GMenuModel` | `` | model item count | | `GVariant` | `` | boolean / string / int32 values | | `GtkSpinButton` | `` | numeric entry, range/increments/digits/wrap | | `GtkSearchEntry` | `` | search-styled entry, placeholder | | `GListModel` | `` | indexed model interface | | `GtkStringList` | `` | simple string model | | `GtkDropDown` | `` | choose one item from a model | | `GtkTextIter` | `` | opaque 80-byte iterator (by address) | | `GtkTextBuffer` | `` | text model for a text view | | `GtkTextView` | `` | multi-line text widget | | `GtkFrame` | `` | labelled border around one child | | `GtkExpander` | `` | collapsible section | | `GtkPaned` | `` | two panes with a draggable divider | | `GtkOverlay` | `` | stack overlays on a main child | | `GtkListBox` | `` | vertical row list + `GtkListBoxRow` | | `GListStore` | `` | in-memory list model | | `GtkStringObject` | `` | GObject string wrapper (item type) | | `GtkSignalListItemFactory` | `` | setup/bind/teardown item factory | | `GtkListItem` | `` | per-row object passed to a factory | | `GtkSelectionModel` | `` | selection interface for a view | | `GtkSingleSelection` | `` | zero/one selected item | | `GtkNoSelection` | `` | display-only (no selection) | | `GtkListView` | `` | model-driven list | | `GtkGridView` | `` | model-driven wrapping grid | | `GtkColumnView` | `` | model-driven multi-column table | | `GtkColumnViewColumn` | `` | one column | | `GtkColumnViewCell` | `` | column cell (`GtkListItem` subclass) | | `GFile` | `` | path/URI file handle | | `GtkFileDialog` | `` | async native open/save/folder dialog | | `GSettings` | `` | typed schema keys | | `GSettingsSchema` | `` | schema lookup / key check | | `GError` | `` | error value (message/domain/code) | | `GioError` | `` | GIO error domain and codes | | `AdwApplication` | `` | Adwaita application | | `AdwApplicationWindow` | `` | Adwaita window | | `AdwHeaderBar` | `` | header bar | | `AdwToolbarView` | `` | top/bottom bars + content | | `AdwPreferencesPage` | `` | page of groups | | `AdwPreferencesGroup` | `` | titled group of rows | | `AdwPreferencesRow` | `` | base row (title) | | `AdwActionRow` | `` | row with subtitle/prefix/suffix | | `AdwSwitchRow` | `` | row with a switch | | `AdwEntryRow` | `` | row with an entry | | `AdwComboRow` | `` | row with a drop-down | | `AdwExpanderRow` | `` | row that expands | | `AdwStatusPage` | `` | placeholder page | | `AdwToast` | `` | transient notification | | `AdwToastOverlay` | `` | toast host | | `AdwBanner` | `` | dismissible info bar | | `AdwClamp` | `` | max-size centred container | | `AdwAboutWindow` | `` | about dialog | | `AdwMessageDialog` | `` | message / confirm dialog | | `GtkUtils` (`lib/`) | — | `CStrToM2`, `StrAppend`, `IntToStr`, `Connect`, `SetAccel`, `Unref`, `Free`, `ErrorMessage`, `ClearError` | | `GtkClosures` (`lib/`) | — | per-connection signal contexts (`Connect2`/`Connect3`, owned variants) | ## Layout ```text m2-GTK4/ src/ FOR "C" .def bindings (one per GTK/GIO object) lib/ pure-M2 helpers built on the bindings (GtkUtils, GtkClosures) tests/ gm2 smoke tests + run_tests.sh (+ a private GSettings schema) examples/ runnable GTK4 programs (hello, counter, ...) tools/ gir2def.py (GIR -> .def generator) + run_gir_check.sh gen/ generator output (git-ignored) build/ generated binaries (git-ignored) ``` ## Prerequisites - `gm2` (tested: 16.0.1 experimental, `~/bin/Modula2/Gm2/bin/gm2`) - GTK4 dev files (`pkg-config --cflags --libs gtk4`, tested: 4.18.6) - libadwaita dev files (`pkg-config --cflags --libs libadwaita-1`, tested: 1.7.6) — optional; enables the `Adw*` modules, `test_adw` and the `adw` example. The build detects them automatically. The build uses the **ISO dialect** (`-fiso`). That is required for `SYSTEM.CAST` and for `NEW`/`DISPOSE` (which need `ALLOCATE`/`DEALLOCATE` from the ISO `Storage` module), both used by `GtkClosures`. `make` passes `-fiso` by default. ```sh export PATH=$HOME/bin/Modula2/Gm2/bin:$PATH ``` ## Build & test ```sh make # tests + examples -> build/ ./tests/run_tests.sh ./build/examples/hello ./build/examples/counter ./build/examples/inputs ./build/examples/containers ./build/examples/actions ./build/examples/headerbar ./build/examples/editor ./build/examples/layouts ./build/examples/files ./build/examples/views ./build/examples/adw ``` `make check` is `make tests` + the runner script. The GUI tests are skipped automatically when neither `DISPLAY` nor `WAYLAND_DISPLAY` is set. The runner also compiles `tests/schemas/*.gschema.xml` into `build/schemas` and points `GSETTINGS_SCHEMA_DIR` at it, so `test_gio` can exercise `GSettings` without touching real user settings. ## Generator (broad coverage) The hand-written modules in `src/` are the idiomatic API. For breadth, `tools/gir2def.py` generates `FOR "C"` `.def` files directly from GObject-Introspection `.gir` XML: ```sh make gir # Gtk-4.0 -> gen/ (301 modules, ~3.7k procedures) make gir GIR=Gio-2.0 # any namespace make gir-check # generate + compile/link a sample ``` It emits one module per class/interface/record (named ``, e.g. `GtkButton`), namespace functions in `Functions.def`, and enums/bitfields in `Enums.def`. Procedure names are the exact GIR `c:identifier`, so the output links straight against the C library. All 633 generated modules across Gtk/GLib/Gio/Adw parse under `gm2 -fiso`. `make gir-verify` closes the loop by checking that every generated identifier actually exists as an exported symbol in the installed library (via `nm`); it reports 0 unexpected across Gtk, GLib, Gio and Adw (a short allowlist covers macros/deprecated no-ops). See `tools/README.md` for the type mapping and the known limits (value structs are opaque, signals/properties are not generated, variadics and `GError**` are dropped). Output goes to `gen/` and is meant to be cherry-picked, not to replace `src/`. ## Usage ```modula2 MODULE hello ; FROM Gio IMPORT g_application_run; FROM GtkApplication IMPORT gtk_application_new; FROM GtkWindow IMPORT gtk_application_window_new, gtk_window_set_title, gtk_window_set_default_size, gtk_window_set_child, gtk_window_present; FROM GtkLabel IMPORT gtk_label_new; FROM GtkUtils IMPORT Connect; FROM SYSTEM IMPORT ADDRESS, ADR; PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS); VAR window, label: ADDRESS; BEGIN window := gtk_application_window_new(application); gtk_window_set_title(window, "Hello"); gtk_window_set_default_size(window, 360, 120); label := gtk_label_new("Hello, GNU Modula-2 + GTK4!"); gtk_window_set_child(window, label); gtk_window_present(window) END OnActivate; VAR app: ADDRESS; BEGIN app := gtk_application_new("org.example.Hello", 0); Connect(app, "activate", ADR(OnActivate), NIL); g_application_run(app, 0, NIL) END hello. ``` Compile a program against the bindings: ```sh gm2 -fiso -Isrc -Ilib myprog.mod build/objs/GtkUtils.o \ build/objs/GtkClosures.o -o myprog \ $(pkg-config --cflags --libs gtk4) ``` ## How it was built, step by step **Step 0 — feasibility spike.** Before writing anything, the riskiest interop points were proved against the real library: creating a GTK4 window from Modula-2, running the GLib main loop, and invoking a Modula-2 procedure from C as a `GSourceFunc`. This fixed the design. **Step 1 — discover the GNU Modula-2 ↔ C rules.** See the next section. These rules decide every line of a binding `.def`. **Step 2 — GLib and GObject.** Everything else sits on the base types, the main loop and the signal machinery, so these came first. **Step 3 — application and window.** `GtkApplication` + `GApplication` drive the loop; `GtkWindow` gives something to show. Using `gtk_application_window_new(app)` (not `gtk_window_new()`) is what keeps the application alive while the window is open. **Step 4 — widgets and layout.** `GtkBox`, `GtkLabel`, `GtkButton`, and the per-widget `GtkWidget` layout helpers. **Step 5 — helpers and signal ergonomics.** `GtkUtils.Connect` wraps the `g_signal_connect` macro (which has no C symbol) by calling the real `g_signal_connect_data`; `CStrToM2` and `IntToStr` handle the string/number conversions GTK code always needs. **Step 6 — tests and examples.** A headless GLib loop test, a version/link test, a widget + signal round-trip test, and an application test; plus `hello` and `counter` examples. **Step 7 — input widgets.** `GtkEnums` was extracted as the shared home for enum constants, then `GtkEditable`/`GtkEntry`, `GtkCheckButton`, `GtkSwitch`, `GtkRange`/`GtkScale` were added, with a `test_inputs` round-trip test and the `inputs` example. This step also pinned down the real-number mapping (`REAL` = C `double`). **Step 8 — layout containers.** `GtkGrid`, `GtkStack` + `GtkStackSwitcher`, `GtkNotebook` and `GtkScrolledWindow`, with a `test_containers` test and the `containers` example (a notebook whose pages are a grid, a switcher-driven stack, and a scrolled list). **Step 9 — actions, menus, accelerators.** GIO's action/menu stack (`GAction`, `GActionMap`, `GSimpleAction`, `GMenu`, `GMenuModel`, `GVariant`), `GtkApplicationWindow`, and `GtkPopoverMenuBar`, wired together in the `actions` example (menu bar + app/win actions + Ctrl+N/Q/B accelerators via `GtkUtils.SetAccel`) and checked by `test_actions`. **Step 10 — ISO dialect and per-connection closures.** The project was switched to `-fiso`, which provides `SYSTEM.CAST` and intrinsic `NEW`/`DISPOSE` (with `Storage`'s `ALLOCATE`/`DEALLOCATE`). That made the `GtkClosures` module possible: each signal connection gets its own heap context holding the handler and payload, a trampoline forwards the call, and a GClosure destroy notify frees the context - and optionally the payload - on disconnect or finalize. Covered by `test_closure`. **Step 11 — header bar and window controls.** `GtkHeaderBar`, `GtkMenuButton`, and `gtk_window_set_titlebar()` were added, then combined in the `headerbar` example (title widget, a connected "New" button packed start, a menu button packed end) and checked by `test_headerbar`. **Step 12 — more inputs.** `GtkSpinButton`, `GtkSearchEntry`, `GtkStringList` + `GListModel`, `GtkDropDown`, and `GtkTextView` + `GtkTextBuffer` with an opaque 80-byte `GtkTextIter` marshalled by address. Checked by `test_inputs2` and shown in the `editor` example (search entry, text view, wrap-mode drop-down, padding spin button). **Step 13 — more containers.** `GtkFrame`, `GtkExpander`, `GtkPaned`, `GtkOverlay`, and `GtkListBox` + `GtkListBoxRow` (with `GtkSelectionMode` in `GtkEnums`). Checked by `test_containers2` and shown in the `layouts` example (a paned split, frame/expander on the left, overlay + list box on the right, `row-activated` via `GtkClosures.Connect3`). **Step 14 — GIO extras.** `GListStore` + `GtkStringObject` (a concrete list model), `GFile`, the asynchronous `GtkFileDialog`, and `GSettings` + `GSettingsSchema`. Checked by `test_gio` (headless) and `test_filedialog`; shown in the `files` example (button -> native open dialog -> `open_finish` -> `GFile` basename). The GSettings test uses a private schema compiled by the runner. **Step 15 — model-backed views.** `GtkSignalListItemFactory` + `GtkListItem`, the selection models `GtkSingleSelection` / `GtkNoSelection`, and the views `GtkListView`, `GtkGridView`, `GtkColumnView` (+ `GtkColumnViewColumn`, `GtkColumnViewCell`). Checked by `test_views`, which presents a window and asserts (from the main loop) that the factory "setup"/"bind" callbacks ran, and shown in the `views` example (one store feeding list, grid and column views). **Step 16 — generator verification.** `tools/gir_verify.py` collects every `c:identifier` that `gir2def.py` would emit and checks it against the exported symbols of the libraries `pkg-config` links for the namespace (symbol-version suffixes stripped). `make gir-verify` reports 0 unexpected across Gtk/GLib/Gio/Adw, with a small allowlist for GIR-listed macros and per-module ABI functions. `tools/run_gir_check.sh` also bulk-parses every generated module. **Step 17 — libadwaita.** Curated `Adw*` modules (application, window, header bar, toolbar view, preferences page/group and the action/switch/ entry/combo/expander rows, status page, toast/overlay, banner, clamp, about and message dialogs), checked by `test_adw` and shown in the `adw` example. libadwaita is optional in the build. **Step 18 — GError and ownership.** `GError` (with its public struct layout) and `GioError` (domain + codes), plus `GtkUtils.Unref`/`Free`/ `ErrorMessage`/`ClearError` and a documented ownership discipline. Checked by `test_gerror` (a real `GIO_ERROR_NOT_FOUND` from `g_file_delete`). ## GNU Modula-2 interop rules learned 1. Declare the module `DEFINITION MODULE FOR "C" Name ;` and list every export in `EXPORT UNQUALIFIED`. Without `EXPORT UNQUALIFIED` the compiler prefixes symbols with `Module_`, and the link fails. 2. Formal parameters in a **procedure type** must be unnamed: `GSourceFunc = PROCEDURE (ADDRESS) : INTEGER;` (named parameters are a syntax error in gm2's default dialect). 3. A function result that callers may ignore is declared `: [ T ]` (e.g. `PROCEDURE g_timeout_add (...) : [ guint ];`). Without the brackets, ignoring the result is a hard error. This also works in ordinary modules — `GtkUtils.Connect` uses it. 4. `ARRAY OF CHAR` maps onto `char *`, so string literals and `CHAR` arrays can be passed straight to `const char *` parameters. 5. `VAR` record/string parameters map onto `T *`; opaque handles (`GObject *`, `GtkWidget *`, ...) travel as `ADDRESS` (use `NIL` to test for `NULL`). 6. `BOOLEAN` is not `gboolean`: pass `INTEGER` with `GLib.gTrue` / `GLib.gFalse`, or use `INTEGER` directly. 7. A top-level Modula-2 procedure is a plain C function pointer; pass it directly to a callback-typed parameter, or use `ADR(proc)` where the binding takes an `ADDRESS`/`GCallback`. 8. **Real numbers:** gm2 `REAL` is 8 bytes and matches C `double`; gm2 `SHORTREAL` is 4 bytes and matches C `float`; gm2 `LONGREAL` is 16 bytes and matches nothing in GTK. Use `REAL` for `gdouble` / `double` (e.g. `GtkRange` values, `GtkScale` ranges). 9. Beware `*)` and `(*` inside comments — they close/open a comment early. Write `void ptr`, not `void *`, in prose. 10. gm2 has no pointer arithmetic and its `T(x)` cast requires equal sizes; `CStrToM2` therefore copies with libc `strncpy` instead of walking a `char *`. 11. `GVariant` values from `g_variant_new_*` are *floating*: ownership transfers to the callee (e.g. `g_simple_action_new_stateful`). Values returned by `g_action_get_state()` / `g_variant_get_*` own a reference and must be released with `g_variant_unref()`. 12. **ISO dialect.** Compiled with `-fiso`: `CAST(T, expr)` (ISO `SYSTEM`) and `NEW`/`DISPOSE` (with `Storage`'s `ALLOCATE`/`DEALLOCATE`) are available there but not in PIM. 13. When a parameter is typed as a **procedure type**, pass the procedure directly (`Connect2(b, "clicked", OnClick, ...)`), not `ADR(OnClick)` — gm2 type-checks it. Use `ADR(proc)` only where the parameter is a plain `ADDRESS` / `GCallback`. 14. A large opaque C struct that is always passed by address (e.g. `GtkTextIter`) can be modelled as a record of `ADDRESS` words sized to `sizeof` it: that gives the right size *and* 64-bit alignment. `GtkTextIter` is 80 bytes, hence `ARRAY [0..9] OF ADDRESS`. ## Roadmap The original roadmap is complete: GLib/GObject core, widgets, inputs, containers, actions/menus/accelerators, header bars, GIO extras, model-backed views, and the GIR generator are all in. On top of that: generator verification (`make gir-verify`), curated **libadwaita**, and **GError** + ownership helpers. Possible next steps: 1. Hand-curate more of the generated surface (signals, properties, value-struct records, `GtkCssProvider` styling). 2. Turn the generator's `ADDRESS` placeholders into cross-module type aliases and `[ ]`/`VAR` refinements using the GIR annotations. 3. Grow the test suite around the newly generated modules. 4. Prune/promote useful `gen/` output into `src/` as it proves out.