Переглянути джерело

feat: demo-browser infrastructure and Cairo/Pango drawing

Filter/sort models + shell widgets (roadmap item 1):
- GtkFilterListModel, GtkCustomFilter, GtkSortListModel, GtkCustomSorter
  (match/compare callbacks are Modula-2 procedures)
- GtkStackSidebar, GtkSearchBar, GtkRevealer, GtkAspectFrame
- test_models, examples/demo.mod (searchable demo browser)

Drawing (roadmap item 2):
- GtkDrawingArea
- curated Cairo subset (context, paths, fill/stroke, patterns, text,
  image surfaces, PNG)
- curated Pango + PangoCairo (layout, font description, extents)
- test_cairo (off-screen image surface + text), test_drawing,
  examples/drawing.mod

24 tests pass; 107 src modules, 18 examples.
Eric Streit 2 днів тому
батько
коміт
896bbc49f5

+ 2 - 2
Makefile

@@ -18,8 +18,8 @@ OBJS    := $(OBJDIR)/GtkUtils.o $(OBJDIR)/GtkClosures.o
 
 
 INCLUDES := -I$(SRC_DIR) -I$(LIB_DIR)
 INCLUDES := -I$(SRC_DIR) -I$(LIB_DIR)
 
 
-TESTS    := test_glib test_version test_widgets test_inputs test_inputs2 test_containers test_containers2 test_actions test_closure test_headerbar test_gio test_gerror test_filedialog test_views test_builder test_utility test_css test_popover test_dialogs test_application
-EXAMPLES := hello counter inputs containers actions headerbar editor layouts files views builder feedback styling popover dialogs
+TESTS    := test_glib test_version test_widgets test_inputs test_inputs2 test_containers test_containers2 test_actions test_closure test_headerbar test_gio test_gerror test_filedialog test_views test_builder test_utility test_css test_popover test_dialogs test_models test_cairo test_drawing test_application
+EXAMPLES := hello counter inputs containers actions headerbar editor layouts files views builder feedback styling popover dialogs drawing demo
 
 
 # Include the libadwaita test/example only when libadwaita is installed.
 # Include the libadwaita test/example only when libadwaita is installed.
 ifneq ($(ADWLIBS),)
 ifneq ($(ADWLIBS),)

+ 10 - 4
README.md

@@ -14,13 +14,14 @@ Verified with `gm2` 16.0.1 (experimental), GTK 4.18.6 and libadwaita 1.7.6.
 
 
 ## Features
 ## Features
 
 
-- **82 hand-written binding modules** across GLib, GObject, Gio, GTK4 and
-  libadwaita — windows, widgets, inputs, containers, model-backed views,
-  actions/menus/accelerators, dialogs, settings, and more.
+- **107 hand-written binding modules** across GLib, GObject, Gio, GTK4
+  and libadwaita — windows, widgets, inputs, containers, model-backed
+  views, actions/menus/accelerators, dialogs, CSS, drawing (Cairo/Pango),
+  settings, and more.
 - **Pure-Modula-2 helpers**: `GtkUtils` (string/number conversion,
 - **Pure-Modula-2 helpers**: `GtkUtils` (string/number conversion,
   `Connect`, `SetAccel`, ownership and `GError` helpers) and
   `Connect`, `SetAccel`, ownership and `GError` helpers) and
   `GtkClosures` (per-connection signal contexts with cleanup).
   `GtkClosures` (per-connection signal contexts with cleanup).
-- **21 tests** and **16 runnable examples**.
+- **24 tests** and **18 runnable examples**.
 - **Generator + verifier**: `tools/gir2def.py` turns a `.gir` file into
 - **Generator + verifier**: `tools/gir2def.py` turns a `.gir` file into
   `.def` files; `tools/gir_verify.py` checks every generated identifier
   `.def` files; `tools/gir_verify.py` checks every generated identifier
   against the installed library. Verified clean across 8602 identifiers.
   against the installed library. Verified clean across 8602 identifiers.
@@ -117,6 +118,8 @@ gm2 -fiso -Isrc -Ilib myprog.mod \
 | `styling` | CSS provider and per-widget CSS classes |
 | `styling` | CSS provider and per-widget CSS classes |
 | `popover` | popover and menu popover |
 | `popover` | popover and menu popover |
 | `dialogs` | `GtkAlertDialog` and `GtkColorDialog` |
 | `dialogs` | `GtkAlertDialog` and `GtkColorDialog` |
+| `drawing` | `GtkDrawingArea` + Cairo + Pango |
+| `demo` | searchable demo browser (filter model + stack of pages) |
 | `adw` | libadwaita preferences app (toolbar view, rows, toast, about) |
 | `adw` | libadwaita preferences app (toolbar view, rows, toast, about) |
 
 
 ## Module reference
 ## Module reference
@@ -142,6 +145,7 @@ gm2 -fiso -Isrc -Ilib myprog.mod \
 | `GListModel`, `GListStore` | indexed models; in-memory store |
 | `GListModel`, `GListStore` | indexed models; in-memory store |
 | `GtkStringList`, `GtkStringObject` | string model / item |
 | `GtkStringList`, `GtkStringObject` | string model / item |
 | `GMenu`, `GMenuModel`, `GSimpleAction`, `GAction`, `GActionMap` | menus and actions |
 | `GMenu`, `GMenuModel`, `GSimpleAction`, `GAction`, `GActionMap` | menus and actions |
+| `GtkFilterListModel`, `GtkCustomFilter`, `GtkSortListModel`, `GtkCustomSorter` | M2-callback filter/sort models |
 | `GFile`, `GtkFileDialog` | file handles and the native dialog |
 | `GFile`, `GtkFileDialog` | file handles and the native dialog |
 | `GSettings`, `GSettingsSchema` | typed settings and schema lookup |
 | `GSettings`, `GSettingsSchema` | typed settings and schema lookup |
 
 
@@ -159,6 +163,8 @@ gm2 -fiso -Isrc -Ilib myprog.mod \
 | `GtkBuilder` | load widget trees from `.ui` XML (with `GError`) |
 | `GtkBuilder` | load widget trees from `.ui` XML (with `GError`) |
 | `GtkAlertDialog`, `GtkColorDialog` | modern alert and colour dialogs |
 | `GtkAlertDialog`, `GtkColorDialog` | modern alert and colour dialogs |
 | `GtkCssProvider`, `GtkStyleContext` | CSS styling for the display |
 | `GtkCssProvider`, `GtkStyleContext` | CSS styling for the display |
+| `GtkStackSidebar`, `GtkSearchBar`, `GtkRevealer`, `GtkAspectFrame` | sidebar/search/animation/layout helpers |
+| `GtkDrawingArea` + `Cairo` + `Pango` + `PangoCairo` | custom drawing with Cairo and text layout |
 | `GtkListBox`, `GtkListView`, `GtkGridView`, `GtkColumnView`, `GtkColumnViewColumn`, `GtkColumnViewCell`, `GtkListItem`, `GtkSignalListItemFactory`, `GtkSelectionModel`, `GtkSingleSelection`, `GtkNoSelection` | lists, grids and tables |
 | `GtkListBox`, `GtkListView`, `GtkGridView`, `GtkColumnView`, `GtkColumnViewColumn`, `GtkColumnViewCell`, `GtkListItem`, `GtkSignalListItemFactory`, `GtkSelectionModel`, `GtkSingleSelection`, `GtkNoSelection` | lists, grids and tables |
 
 
 ### libadwaita
 ### libadwaita

+ 286 - 0
examples/demo.mod

@@ -0,0 +1,286 @@
+MODULE demo ;
+
+(*
+   m2-GTK4 example - a small "gtk-demo"-style browser.
+
+   A searchable sidebar (GtkListView over a GtkFilterListModel with a
+   Modula-2 GtkCustomFilter) drives a GtkStack of demo pages.  Shows the
+   infrastructure a full gtk-demo port would need.
+
+   Build:  make examples
+   Run:    ./build/examples/demo
+*)
+
+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 GtkBox IMPORT gtk_box_new, gtk_box_append, gtk_box_set_spacing;
+FROM GtkEnums IMPORT GtkVertical, GtkHorizontal;
+FROM GtkLabel IMPORT gtk_label_new, gtk_label_set_text;
+FROM GtkButton IMPORT gtk_button_new_with_label;
+FROM GtkListBox IMPORT gtk_list_box_new, gtk_list_box_append,
+   gtk_list_box_row_new, gtk_list_box_row_set_child;
+FROM GtkSearchEntry IMPORT gtk_search_entry_new,
+   gtk_search_entry_set_placeholder_text;
+FROM GtkEditable IMPORT gtk_editable_get_text;
+FROM GtkWidget IMPORT gtk_widget_set_size_request, gtk_widget_set_vexpand,
+   gtk_widget_set_hexpand, gtk_widget_add_css_class,
+   gtk_widget_set_margin_start, gtk_widget_set_margin_end,
+   gtk_widget_set_margin_top, gtk_widget_set_margin_bottom;
+FROM GtkListView IMPORT gtk_list_view_new;
+FROM GtkListItem IMPORT gtk_list_item_get_item, gtk_list_item_get_child,
+   gtk_list_item_set_child;
+FROM GtkSignalListItemFactory IMPORT gtk_signal_list_item_factory_new;
+FROM GtkSingleSelection IMPORT gtk_single_selection_new,
+   gtk_single_selection_get_selected;
+FROM GtkFilterListModel IMPORT gtk_filter_list_model_new;
+FROM GtkCustomFilter IMPORT gtk_custom_filter_new,
+   gtk_custom_filter_set_filter_func;
+FROM GtkStack IMPORT gtk_stack_new, gtk_stack_add_titled,
+   gtk_stack_set_visible_child_name;
+FROM GtkDrawingArea IMPORT gtk_drawing_area_new,
+   gtk_drawing_area_set_content_width, gtk_drawing_area_set_content_height,
+   gtk_drawing_area_set_draw_func;
+FROM GtkCssProvider IMPORT gtk_css_provider_new,
+   gtk_css_provider_load_from_string;
+FROM GtkStyleContext IMPORT gtk_style_context_add_provider_for_display,
+   GtkStyleProviderPriorityApplication;
+FROM Gdk IMPORT gdk_display_get_default;
+FROM GListStore IMPORT g_list_store_new, g_list_store_append;
+FROM GListModel IMPORT g_list_model_get_item;
+FROM GtkStringObject IMPORT gtk_string_object_new,
+   gtk_string_object_get_string, gtk_string_object_get_type;
+FROM Cairo IMPORT cairo_set_source_rgb, cairo_arc, cairo_fill,
+   cairo_set_line_width, cairo_move_to, cairo_line_to, cairo_stroke;
+FROM GObject IMPORT g_object_unref;
+FROM GtkClosures IMPORT Connect2, Connect3;
+FROM GtkUtils IMPORT CStrToM2;
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM libc IMPORT printf;
+
+CONST
+   AppId = "org.example.m2gtk4.Demo";
+   InvalidPosition = 0FFFFFFFFH;
+   Css = ".demo-accent { color: #e01b24; font-weight: bold; }";
+
+TYPE
+   DemoProc = PROCEDURE () : ADDRESS;
+   Demo = RECORD
+      title: ARRAY [0..31] OF CHAR;
+      build: DemoProc;
+   END;
+
+VAR
+   stack, filter, filterModel, selection: ADDRESS;
+   searchText: ARRAY [0..63] OF CHAR;
+
+(* --- a couple of demo pages --- *)
+
+PROCEDURE BuildWelcome () : ADDRESS;
+BEGIN
+   RETURN gtk_label_new("Welcome to the m2-GTK4 demo browser.")
+END BuildWelcome;
+
+PROCEDURE BuildButtons () : ADDRESS;
+VAR box: ADDRESS;
+BEGIN
+   box := gtk_box_new(GtkVertical, 6);
+   gtk_box_append(box, gtk_button_new_with_label("Click me"));
+   gtk_box_append(box, gtk_button_new_with_label("Me too"));
+   RETURN box
+END BuildButtons;
+
+PROCEDURE BuildList () : ADDRESS;
+VAR box, row: ADDRESS; i: INTEGER;
+BEGIN
+   box := gtk_list_box_new();
+   FOR i := 1 TO 3 DO
+      row := gtk_list_box_row_new();
+      gtk_list_box_row_set_child(row, gtk_label_new("A list row"));
+      gtk_list_box_append(box, row)
+   END;
+   RETURN box
+END BuildList;
+
+PROCEDURE BuildCSS () : ADDRESS;
+VAR label: ADDRESS;
+BEGIN
+   label := gtk_label_new("Styled by CSS");
+   gtk_widget_add_css_class(label, "demo-accent");
+   RETURN label
+END BuildCSS;
+
+PROCEDURE DrawDemo (area: ADDRESS; cr: ADDRESS; width, height: INTEGER;
+                    data: ADDRESS);
+BEGIN
+   cairo_set_source_rgb(cr, 0.20, 0.50, 0.90);
+   cairo_arc(cr, 80.0, 80.0, 50.0, 0.0, 6.2831853);
+   cairo_fill(cr);
+   cairo_set_source_rgb(cr, 0.90, 0.20, 0.20);
+   cairo_set_line_width(cr, 6.0);
+   cairo_move_to(cr, 150.0, 40.0);
+   cairo_line_to(cr, 280.0, 120.0);
+   cairo_stroke(cr)
+END DrawDemo;
+
+PROCEDURE BuildDrawing () : ADDRESS;
+VAR area: ADDRESS;
+BEGIN
+   area := gtk_drawing_area_new();
+   gtk_drawing_area_set_content_width(area, 300);
+   gtk_drawing_area_set_content_height(area, 160);
+   gtk_drawing_area_set_draw_func(area, ADR(DrawDemo), NIL, NIL);
+   RETURN area
+END BuildDrawing;
+
+(* --- search filter --- *)
+
+PROCEDURE Contains (hay, needle: ARRAY OF CHAR) : BOOLEAN;
+VAR
+   i, j, hl, nl: CARDINAL;
+   found: BOOLEAN;
+BEGIN
+   hl := 0;
+   WHILE (hl <= HIGH(hay)) AND (hay[hl] # 0C) DO INC(hl) END;
+   nl := 0;
+   WHILE (nl <= HIGH(needle)) AND (needle[nl] # 0C) DO INC(nl) END;
+   IF nl = 0 THEN RETURN TRUE END;
+   IF nl > hl THEN RETURN FALSE END;
+   i := 0;
+   WHILE i + nl <= hl DO
+      found := TRUE;
+      j := 0;
+      WHILE j < nl DO
+         IF hay[i + j] # needle[j] THEN found := FALSE END;
+         INC(j)
+      END;
+      IF found THEN RETURN TRUE END;
+      INC(i)
+   END;
+   RETURN FALSE
+END Contains;
+
+PROCEDURE Match (item: ADDRESS; user: ADDRESS) : INTEGER;
+VAR
+   buf: ARRAY [0..63] OF CHAR;
+BEGIN
+   CStrToM2(gtk_string_object_get_string(item), buf);
+   IF Contains(buf, searchText) THEN RETURN 1 ELSE RETURN 0 END
+END Match;
+
+PROCEDURE OnSearchChanged (entry: ADDRESS; user: ADDRESS);
+BEGIN
+   CStrToM2(gtk_editable_get_text(entry), searchText);
+   gtk_custom_filter_set_filter_func(filter, ADR(Match), NIL, NIL)
+END OnSearchChanged;
+
+PROCEDURE OnSelected (model: ADDRESS; pspec: ADDRESS; user: ADDRESS);
+VAR
+   index: CARDINAL;
+   item: ADDRESS;
+   name: ARRAY [0..31] OF CHAR;
+BEGIN
+   index := gtk_single_selection_get_selected(selection);
+   IF index = InvalidPosition THEN RETURN END;
+   item := g_list_model_get_item(filterModel, index);
+   CStrToM2(gtk_string_object_get_string(item), name);
+   g_object_unref(item);
+   gtk_stack_set_visible_child_name(stack, name)
+END OnSelected;
+
+(* --- list factory --- *)
+
+PROCEDURE RowSetup (factory: ADDRESS; item: ADDRESS; user: ADDRESS);
+BEGIN
+   gtk_list_item_set_child(item, gtk_label_new(""))
+END RowSetup;
+
+PROCEDURE RowBind (factory: ADDRESS; item: ADDRESS; user: ADDRESS);
+VAR
+   obj, child: ADDRESS;
+   buf: ARRAY [0..63] OF CHAR;
+BEGIN
+   obj := gtk_list_item_get_item(item);
+   CStrToM2(gtk_string_object_get_string(obj), buf);
+   child := gtk_list_item_get_child(item);
+   gtk_label_set_text(child, buf)
+END RowBind;
+
+PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
+VAR
+   window, root, content, search, listView, factory, store, provider: ADDRESS;
+   demos: ARRAY [0..4] OF Demo;
+   i: INTEGER;
+BEGIN
+   searchText[0] := 0C;
+
+   provider := gtk_css_provider_new();
+   gtk_css_provider_load_from_string(provider, Css);
+   gtk_style_context_add_provider_for_display(gdk_display_get_default(),
+      provider, GtkStyleProviderPriorityApplication);
+
+   demos[0].title := "Welcome"; demos[0].build := BuildWelcome;
+   demos[1].title := "Buttons"; demos[1].build := BuildButtons;
+   demos[2].title := "List";    demos[2].build := BuildList;
+   demos[3].title := "CSS";     demos[3].build := BuildCSS;
+   demos[4].title := "Drawing"; demos[4].build := BuildDrawing;
+
+   store := g_list_store_new(gtk_string_object_get_type());
+   stack := gtk_stack_new();
+   FOR i := 0 TO 4 DO
+      gtk_stack_add_titled(stack, demos[i].build(),
+                           demos[i].title, demos[i].title);
+      g_list_store_append(store, gtk_string_object_new(demos[i].title))
+   END;
+
+   filter := gtk_custom_filter_new(ADR(Match), NIL, NIL);
+   filterModel := gtk_filter_list_model_new(store, filter);
+   selection := gtk_single_selection_new(filterModel);
+   Connect3(selection, "notify::selected", OnSelected, NIL);
+
+   factory := gtk_signal_list_item_factory_new();
+   Connect3(factory, "setup", RowSetup, NIL);
+   Connect3(factory, "bind", RowBind, NIL);
+   listView := gtk_list_view_new(selection, factory);
+   gtk_widget_set_size_request(listView, 180, -1);
+   gtk_widget_set_vexpand(listView, 1);
+
+   search := gtk_search_entry_new();
+   gtk_search_entry_set_placeholder_text(search, "Filter demos");
+   Connect2(search, "search-changed", OnSearchChanged, NIL);
+
+   content := gtk_box_new(GtkHorizontal, 0);
+   gtk_widget_set_vexpand(content, 1);
+   gtk_box_append(content, listView);
+   gtk_box_append(content, stack);
+   gtk_widget_set_hexpand(stack, 1);
+
+   root := gtk_box_new(GtkVertical, 6);
+   gtk_box_set_spacing(root, 6);
+   gtk_widget_set_margin_top(root, 8);
+   gtk_widget_set_margin_bottom(root, 8);
+   gtk_widget_set_margin_start(root, 8);
+   gtk_widget_set_margin_end(root, 8);
+   gtk_box_append(root, search);
+   gtk_box_append(root, content);
+
+   window := gtk_application_window_new(application);
+   gtk_window_set_title(window, "m2-GTK4 Demo");
+   gtk_window_set_default_size(window, 640, 420);
+   gtk_window_set_child(window, root);
+   gtk_stack_set_visible_child_name(stack, "Welcome");
+   gtk_window_present(window)
+END OnActivate;
+
+VAR
+   app: ADDRESS;
+BEGIN
+   app := gtk_application_new(AppId, 0);
+   IF app = NIL THEN
+      printf("gtk_application_new failed\n");
+      HALT(1)
+   END;
+   Connect2(app, "activate", OnActivate, NIL);
+   g_application_run(app, 0, NIL)
+END demo.

+ 89 - 0
examples/drawing.mod

@@ -0,0 +1,89 @@
+MODULE drawing ;
+
+(*
+   m2-GTK4 example - drawing with GtkDrawingArea, Cairo and Pango.
+
+   Draws a filled circle, a line and a Pango text layout.
+
+   Build:  make examples
+   Run:    ./build/examples/drawing
+*)
+
+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 GtkDrawingArea IMPORT gtk_drawing_area_new,
+   gtk_drawing_area_set_content_width,
+   gtk_drawing_area_set_content_height, gtk_drawing_area_set_draw_func;
+FROM Cairo IMPORT cairo_set_source_rgb, cairo_rectangle, cairo_fill,
+   cairo_arc, cairo_set_line_width, cairo_stroke, cairo_move_to, cairo_line_to;
+FROM Pango IMPORT pango_font_description_from_string,
+   pango_font_description_free, pango_layout_set_text,
+   pango_layout_set_font_description;
+FROM PangoCairo IMPORT pango_cairo_create_layout, pango_cairo_show_layout;
+FROM GObject IMPORT g_object_unref;
+FROM GtkClosures IMPORT Connect2;
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM libc IMPORT printf;
+
+CONST
+   AppId = "org.example.m2gtk4.Drawing";
+
+PROCEDURE Draw (area: ADDRESS; cr: ADDRESS; width, height: INTEGER;
+                data: ADDRESS);
+VAR
+   layout, desc: ADDRESS;
+BEGIN
+   cairo_set_source_rgb(cr, 0.95, 0.95, 0.97);
+   cairo_rectangle(cr, 0.0, 0.0, VAL(REAL, width), VAL(REAL, height));
+   cairo_fill(cr);
+
+   cairo_set_source_rgb(cr, 0.20, 0.50, 0.90);
+   cairo_arc(cr, 80.0, 80.0, 50.0, 0.0, 6.2831853);
+   cairo_fill(cr);
+
+   cairo_set_source_rgb(cr, 0.90, 0.20, 0.20);
+   cairo_set_line_width(cr, 6.0);
+   cairo_move_to(cr, 150.0, 40.0);
+   cairo_line_to(cr, 330.0, 140.0);
+   cairo_stroke(cr);
+
+   layout := pango_cairo_create_layout(cr);
+   desc := pango_font_description_from_string("Sans Bold 18");
+   pango_layout_set_font_description(layout, desc);
+   pango_font_description_free(desc);
+   pango_layout_set_text(layout, "Cairo + Pango from Modula-2", -1);
+   cairo_set_source_rgb(cr, 0.10, 0.10, 0.10);
+   cairo_move_to(cr, 20.0, 160.0);
+   pango_cairo_show_layout(cr, layout);
+   g_object_unref(layout)
+END Draw;
+
+PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
+VAR
+   window, area: ADDRESS;
+BEGIN
+   area := gtk_drawing_area_new();
+   gtk_drawing_area_set_content_width(area, 360);
+   gtk_drawing_area_set_content_height(area, 200);
+   gtk_drawing_area_set_draw_func(area, ADR(Draw), NIL, NIL);
+
+   window := gtk_application_window_new(application);
+   gtk_window_set_title(window, "m2-GTK4 Drawing");
+   gtk_window_set_default_size(window, 400, 240);
+   gtk_window_set_child(window, area);
+   gtk_window_present(window)
+END OnActivate;
+
+VAR
+   app: ADDRESS;
+BEGIN
+   app := gtk_application_new(AppId, 0);
+   IF app = NIL THEN
+      printf("gtk_application_new failed\n");
+      HALT(1)
+   END;
+   Connect2(app, "activate", OnActivate, NIL);
+   g_application_run(app, 0, NIL)
+END drawing.

+ 225 - 0
src/Cairo.def

@@ -0,0 +1,225 @@
+DEFINITION MODULE FOR "C" Cairo ;
+
+(*
+   m2-GTK4 - curated Cairo subset for GtkDrawingArea and GdkPaintable
+   drawing.  Mapped onto <cairo.h>.
+
+   All Cairo objects are opaque and travel as ADDRESS.  Colours and
+   geometry use REAL (C double).  Enumerations are CARDINAL with the
+   usual constants.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   cairo_t, cairo_surface_t, cairo_pattern_t,
+   CairoFormat, CairoFormatARGB32, CairoFormatRGB24, CairoFormatA8,
+   CairoLineCap, CairoLineCapButt, CairoLineCapRound, CairoLineCapSquare,
+   CairoLineJoin, CairoLineJoinMiter, CairoLineJoinRound, CairoLineJoinBevel,
+   CairoFontSlant, CairoFontSlantNormal, CairoFontSlantItalic,
+   CairoFontSlantOblique,
+   CairoFontWeight, CairoFontWeightNormal, CairoFontWeightBold,
+   cairo_create, cairo_destroy, cairo_reference,
+   cairo_status, cairo_surface_status,
+   cairo_save, cairo_restore,
+   cairo_move_to, cairo_line_to, cairo_curve_to,
+   cairo_arc, cairo_arc_negative,
+   cairo_rectangle, cairo_close_path, cairo_new_path, cairo_new_sub_path,
+   cairo_stroke, cairo_fill, cairo_paint, cairo_paint_with_alpha, cairo_clip,
+   cairo_set_source_rgb, cairo_set_source_rgba, cairo_set_source, cairo_mask,
+   cairo_set_source_surface,
+   cairo_set_line_width, cairo_set_line_cap, cairo_set_line_join,
+   cairo_translate, cairo_scale, cairo_rotate,
+   cairo_select_font_face, cairo_set_font_size, cairo_show_text, cairo_text_path,
+   cairo_push_group, cairo_pop_group_to_source,
+   cairo_image_surface_create, cairo_image_surface_create_from_png,
+   cairo_surface_destroy, cairo_surface_flush, cairo_surface_write_to_png,
+   cairo_pattern_create_linear, cairo_pattern_create_radial,
+   cairo_pattern_add_color_stop_rgb, cairo_pattern_destroy;
+
+
+TYPE
+   cairo_t = ADDRESS;
+   cairo_surface_t = ADDRESS;
+   cairo_pattern_t = ADDRESS;
+   CairoFormat = CARDINAL;
+   CairoLineCap = CARDINAL;
+   CairoLineJoin = CARDINAL;
+   CairoFontSlant = CARDINAL;
+   CairoFontWeight = CARDINAL;
+
+CONST
+   CairoFormatARGB32 = 0;
+   CairoFormatRGB24  = 1;
+   CairoFormatA8     = 2;
+
+   CairoLineCapButt   = 0;
+   CairoLineCapRound  = 1;
+   CairoLineCapSquare = 2;
+
+   CairoLineJoinMiter = 0;
+   CairoLineJoinRound = 1;
+   CairoLineJoinBevel = 2;
+
+   CairoFontSlantNormal  = 0;
+   CairoFontSlantItalic  = 1;
+   CairoFontSlantOblique = 2;
+
+   CairoFontWeightNormal = 0;
+   CairoFontWeightBold   = 1;
+
+(* cairo_t *cairo_create(cairo_surface_t *target) *)
+PROCEDURE cairo_create (target: cairo_surface_t) : cairo_t;
+
+(* void cairo_destroy(cairo_t *cr) *)
+PROCEDURE cairo_destroy (cr: cairo_t);
+
+(* cairo_t *cairo_reference(cairo_t *cr) *)
+PROCEDURE cairo_reference (cr: cairo_t) : cairo_t;
+
+(* cairo_status_t cairo_status(cairo_t *cr)  (0 = CAIRO_STATUS_SUCCESS) *)
+PROCEDURE cairo_status (cr: cairo_t) : CARDINAL;
+
+(* cairo_status_t cairo_surface_status(cairo_surface_t *surface) *)
+PROCEDURE cairo_surface_status (surface: cairo_surface_t) : CARDINAL;
+
+(* void cairo_save(cairo_t *cr) / void cairo_restore(cairo_t *cr) *)
+PROCEDURE cairo_save (cr: cairo_t);
+PROCEDURE cairo_restore (cr: cairo_t);
+
+(* void cairo_move_to(cairo_t *cr, double x, double y) *)
+PROCEDURE cairo_move_to (cr: cairo_t; x, y: REAL);
+
+(* void cairo_line_to(cairo_t *cr, double x, double y) *)
+PROCEDURE cairo_line_to (cr: cairo_t; x, y: REAL);
+
+(* void cairo_curve_to(cairo_t *cr, double x1, y1, x2, y2, x3, y3) *)
+PROCEDURE cairo_curve_to (cr: cairo_t; x1, y1, x2, y2, x3, y3: REAL);
+
+(* void cairo_arc(cairo_t *cr, double xc, yc, radius,
+                   double angle1, double angle2) *)
+PROCEDURE cairo_arc (cr: cairo_t; xc, yc, radius, angle1, angle2: REAL);
+
+(* void cairo_arc_negative(cairo_t *cr, double xc, yc, radius,
+                            double angle1, double angle2) *)
+PROCEDURE cairo_arc_negative (cr: cairo_t; xc, yc, radius,
+                              angle1, angle2: REAL);
+
+(* void cairo_rectangle(cairo_t *cr, double x, y, width, height) *)
+PROCEDURE cairo_rectangle (cr: cairo_t; x, y, width, height: REAL);
+
+(* void cairo_close_path(cairo_t *cr) *)
+PROCEDURE cairo_close_path (cr: cairo_t);
+
+(* void cairo_new_path(cairo_t *cr) *)
+PROCEDURE cairo_new_path (cr: cairo_t);
+
+(* void cairo_new_sub_path(cairo_t *cr) *)
+PROCEDURE cairo_new_sub_path (cr: cairo_t);
+
+(* void cairo_stroke(cairo_t *cr) / void cairo_fill(cairo_t *cr) *)
+PROCEDURE cairo_stroke (cr: cairo_t);
+PROCEDURE cairo_fill (cr: cairo_t);
+
+(* void cairo_paint(cairo_t *cr) *)
+PROCEDURE cairo_paint (cr: cairo_t);
+
+(* void cairo_paint_with_alpha(cairo_t *cr, double alpha) *)
+PROCEDURE cairo_paint_with_alpha (cr: cairo_t; alpha: REAL);
+
+(* void cairo_clip(cairo_t *cr) *)
+PROCEDURE cairo_clip (cr: cairo_t);
+
+(* void cairo_set_source_rgb(cairo_t *cr, double red, green, blue) *)
+PROCEDURE cairo_set_source_rgb (cr: cairo_t; red, green, blue: REAL);
+
+(* void cairo_set_source_rgba(cairo_t *cr, double red, green, blue, alpha) *)
+PROCEDURE cairo_set_source_rgba (cr: cairo_t; red, green, blue, alpha: REAL);
+
+(* void cairo_set_source(cairo_t *cr, cairo_pattern_t *source) *)
+PROCEDURE cairo_set_source (cr: cairo_t; source: cairo_pattern_t);
+
+(* void cairo_mask(cairo_t *cr, cairo_pattern_t *pattern) *)
+PROCEDURE cairo_mask (cr: cairo_t; pattern: cairo_pattern_t);
+
+(* void cairo_set_source_surface(cairo_t *cr, cairo_surface_t *surface,
+                                 double x, double y) *)
+PROCEDURE cairo_set_source_surface (cr: cairo_t; surface: cairo_surface_t;
+                                    x, y: REAL);
+
+(* void cairo_set_line_width(cairo_t *cr, double width) *)
+PROCEDURE cairo_set_line_width (cr: cairo_t; width: REAL);
+
+(* void cairo_set_line_cap(cairo_t *cr, cairo_line_cap_t line_cap) *)
+PROCEDURE cairo_set_line_cap (cr: cairo_t; line_cap: CairoLineCap);
+
+(* void cairo_set_line_join(cairo_t *cr, cairo_line_join_t line_join) *)
+PROCEDURE cairo_set_line_join (cr: cairo_t; line_join: CairoLineJoin);
+
+(* void cairo_translate(cairo_t *cr, double tx, double ty) *)
+PROCEDURE cairo_translate (cr: cairo_t; tx, ty: REAL);
+
+(* void cairo_scale(cairo_t *cr, double sx, double sy) *)
+PROCEDURE cairo_scale (cr: cairo_t; sx, sy: REAL);
+
+(* void cairo_rotate(cairo_t *cr, double angle) *)
+PROCEDURE cairo_rotate (cr: cairo_t; angle: REAL);
+
+(* void cairo_select_font_face(cairo_t *cr, const char *family,
+                                cairo_font_slant_t slant,
+                                cairo_font_weight_t weight) *)
+PROCEDURE cairo_select_font_face (cr: cairo_t; family: ARRAY OF CHAR;
+                                  slant: CairoFontSlant;
+                                  weight: CairoFontWeight);
+
+(* void cairo_set_font_size(cairo_t *cr, double size) *)
+PROCEDURE cairo_set_font_size (cr: cairo_t; size: REAL);
+
+(* void cairo_show_text(cairo_t *cr, const char *utf8) *)
+PROCEDURE cairo_show_text (cr: cairo_t; utf8: ARRAY OF CHAR);
+
+(* void cairo_text_path(cairo_t *cr, const char *utf8) *)
+PROCEDURE cairo_text_path (cr: cairo_t; utf8: ARRAY OF CHAR);
+
+(* void cairo_push_group(cairo_t *cr) *)
+PROCEDURE cairo_push_group (cr: cairo_t);
+
+(* void cairo_pop_group_to_source(cairo_t *cr) *)
+PROCEDURE cairo_pop_group_to_source (cr: cairo_t);
+
+(* cairo_surface_t *cairo_image_surface_create(cairo_format_t format,
+                                                int width, int height) *)
+PROCEDURE cairo_image_surface_create (format: CairoFormat;
+                                      width, height: INTEGER) : cairo_surface_t;
+
+(* cairo_surface_t *cairo_image_surface_create_from_png(const char *filename) *)
+PROCEDURE cairo_image_surface_create_from_png (filename: ARRAY OF CHAR) : cairo_surface_t;
+
+(* void cairo_surface_destroy(cairo_surface_t *surface) *)
+PROCEDURE cairo_surface_destroy (surface: cairo_surface_t);
+
+(* void cairo_surface_flush(cairo_surface_t *surface) *)
+PROCEDURE cairo_surface_flush (surface: cairo_surface_t);
+
+(* cairo_status_t cairo_surface_write_to_png(cairo_surface_t *surface,
+                                              const char *filename) *)
+PROCEDURE cairo_surface_write_to_png (surface: cairo_surface_t;
+                                      filename: ARRAY OF CHAR) : [ CARDINAL ];
+
+(* cairo_pattern_t *cairo_pattern_create_linear(double x0, y0, x1, y1) *)
+PROCEDURE cairo_pattern_create_linear (x0, y0, x1, y1: REAL) : cairo_pattern_t;
+
+(* cairo_pattern_t *cairo_pattern_create_radial(double cx0, cy0, radius0,
+                                                 double cx1, cy1, radius1) *)
+PROCEDURE cairo_pattern_create_radial (cx0, cy0, radius0,
+                                       cx1, cy1, radius1: REAL) : cairo_pattern_t;
+
+(* void cairo_pattern_add_color_stop_rgb(cairo_pattern_t *pattern,
+                                          double offset, double red, green, blue) *)
+PROCEDURE cairo_pattern_add_color_stop_rgb (pattern: cairo_pattern_t;
+                                            offset, red, green, blue: REAL);
+
+(* void cairo_pattern_destroy(cairo_pattern_t *pattern) *)
+PROCEDURE cairo_pattern_destroy (pattern: cairo_pattern_t);
+
+END Cairo.

+ 36 - 0
src/GtkAspectFrame.def

@@ -0,0 +1,36 @@
+DEFINITION MODULE FOR "C" GtkAspectFrame ;
+
+(*
+   m2-GTK4 - GtkAspectFrame: constrains its child to an aspect ratio.
+   Mapped onto <gtk/gtkaspectframe.h>.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkAspectFrame,
+   gtk_aspect_frame_new,
+   gtk_aspect_frame_set_child,
+   gtk_aspect_frame_set_obey_child,
+   gtk_aspect_frame_set_ratio;
+
+
+TYPE
+   GtkAspectFrame = ADDRESS;
+
+(* GtkWidget *gtk_aspect_frame_new(float xalign, float yalign,
+                                    float ratio, gboolean obey_child) *)
+PROCEDURE gtk_aspect_frame_new (xalign, yalign, ratio: SHORTREAL;
+                                obey_child: INTEGER) : GtkAspectFrame;
+
+(* void gtk_aspect_frame_set_child(GtkAspectFrame *self, GtkWidget *child) *)
+PROCEDURE gtk_aspect_frame_set_child (self: GtkAspectFrame; child: ADDRESS);
+
+(* void gtk_aspect_frame_set_obey_child(GtkAspectFrame *self, gboolean obey_child) *)
+PROCEDURE gtk_aspect_frame_set_obey_child (self: GtkAspectFrame;
+                                           obey_child: INTEGER);
+
+(* void gtk_aspect_frame_set_ratio(GtkAspectFrame *self, float ratio) *)
+PROCEDURE gtk_aspect_frame_set_ratio (self: GtkAspectFrame; ratio: SHORTREAL);
+
+END GtkAspectFrame.

+ 48 - 0
src/GtkCustomFilter.def

@@ -0,0 +1,48 @@
+DEFINITION MODULE FOR "C" GtkCustomFilter ;
+
+(*
+   m2-GTK4 - GtkCustomFilter: a GtkFilter whose match test is a callback.
+   Mapped onto <gtk/gtkcustomfilter.h>.
+
+   The match function has the C shape
+
+      gboolean match(gpointer item, gpointer user_data);
+
+   Pass a top-level Modula-2 PROCEDURE (item, user: ADDRESS) : INTEGER
+   as its address:
+
+      filter := gtk_custom_filter_new(ADR(Match), NIL, NIL);
+
+   Call gtk_custom_filter_changed-equivalent by re-setting the filter
+   function (or use gtk_filter_changed from GtkFilter) to re-run it.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkCustomFilter, GtkCustomFilterFunc,
+   gtk_custom_filter_new, gtk_custom_filter_set_filter_func;
+
+
+TYPE
+   GtkCustomFilter = ADDRESS;
+   (* gboolean func(gpointer item, gpointer user_data) *)
+   GtkCustomFilterFunc = ADDRESS;
+
+(* GtkCustomFilter *gtk_custom_filter_new(GtkCustomFilterFunc match_func,
+                                           gpointer user_data,
+                                           GDestroyNotify user_destroy) *)
+PROCEDURE gtk_custom_filter_new (match_func: GtkCustomFilterFunc;
+                                 user_data: ADDRESS;
+                                 user_destroy: ADDRESS) : GtkCustomFilter;
+
+(* void gtk_custom_filter_set_filter_func(GtkCustomFilter *self,
+                                           GtkCustomFilterFunc match_func,
+                                           gpointer user_data,
+                                           GDestroyNotify user_destroy) *)
+PROCEDURE gtk_custom_filter_set_filter_func (self: GtkCustomFilter;
+                                             match_func: GtkCustomFilterFunc;
+                                             user_data: ADDRESS;
+                                             user_destroy: ADDRESS);
+
+END GtkCustomFilter.

+ 45 - 0
src/GtkCustomSorter.def

@@ -0,0 +1,45 @@
+DEFINITION MODULE FOR "C" GtkCustomSorter ;
+
+(*
+   m2-GTK4 - GtkCustomSorter: a GtkSorter whose comparison is a callback.
+   Mapped onto <gtk/gtkcustomsorter.h>.
+
+   The compare function has the C shape
+
+      int compare(gconstpointer a, gconstpointer b, gpointer user_data);
+
+   Pass a top-level Modula-2 PROCEDURE (a, b, user: ADDRESS) : INTEGER
+   as its address:
+
+      sorter := gtk_custom_sorter_new(ADR(Compare), NIL, NIL);
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkCustomSorter, GtkCompareDataFunc,
+   gtk_custom_sorter_new, gtk_custom_sorter_set_sort_func;
+
+
+TYPE
+   GtkCustomSorter = ADDRESS;
+   (* int func(gconstpointer a, gconstpointer b, gpointer user_data) *)
+   GtkCompareDataFunc = ADDRESS;
+
+(* GtkCustomSorter *gtk_custom_sorter_new(GtkCompareDataFunc sort_func,
+                                           gpointer user_data,
+                                           GDestroyNotify user_destroy) *)
+PROCEDURE gtk_custom_sorter_new (sort_func: GtkCompareDataFunc;
+                                 user_data: ADDRESS;
+                                 user_destroy: ADDRESS) : GtkCustomSorter;
+
+(* void gtk_custom_sorter_set_sort_func(GtkCustomSorter *self,
+                                         GtkCompareDataFunc sort_func,
+                                         gpointer user_data,
+                                         GDestroyNotify user_destroy) *)
+PROCEDURE gtk_custom_sorter_set_sort_func (self: GtkCustomSorter;
+                                           sort_func: GtkCompareDataFunc;
+                                           user_data: ADDRESS;
+                                           user_destroy: ADDRESS);
+
+END GtkCustomSorter.

+ 60 - 0
src/GtkDrawingArea.def

@@ -0,0 +1,60 @@
+DEFINITION MODULE FOR "C" GtkDrawingArea ;
+
+(*
+   m2-GTK4 - GtkDrawingArea: a widget with a Cairo surface to draw on.
+   Mapped onto <gtk/gtkdrawingarea.h>.
+
+   Give it a draw function and, optionally, a fixed content size:
+
+      PROCEDURE Draw (area: ADDRESS; cr: ADDRESS; w, h: INTEGER; data: ADDRESS);
+      ...
+      area := gtk_drawing_area_new();
+      gtk_drawing_area_set_content_width(area, 200);
+      gtk_drawing_area_set_content_height(area, 200);
+      gtk_drawing_area_set_draw_func(area, ADR(Draw), NIL, NIL);
+
+   The C draw callback has the shape
+      void func(GtkDrawingArea *area, cairo_t *cr, int width, int height,
+                gpointer user_data).
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkDrawingArea,
+   gtk_drawing_area_new,
+   gtk_drawing_area_set_content_width,
+   gtk_drawing_area_set_content_height,
+   gtk_drawing_area_get_content_width,
+   gtk_drawing_area_get_content_height,
+   gtk_drawing_area_set_draw_func;
+
+
+TYPE
+   GtkDrawingArea = ADDRESS;
+
+(* GtkWidget *gtk_drawing_area_new(void) *)
+PROCEDURE gtk_drawing_area_new () : GtkDrawingArea;
+
+(* void gtk_drawing_area_set_content_width(GtkDrawingArea *self, int width) *)
+PROCEDURE gtk_drawing_area_set_content_width (self: GtkDrawingArea; width: INTEGER);
+
+(* void gtk_drawing_area_set_content_height(GtkDrawingArea *self, int height) *)
+PROCEDURE gtk_drawing_area_set_content_height (self: GtkDrawingArea; height: INTEGER);
+
+(* int gtk_drawing_area_get_content_width(GtkDrawingArea *self) *)
+PROCEDURE gtk_drawing_area_get_content_width (self: GtkDrawingArea) : INTEGER;
+
+(* int gtk_drawing_area_get_content_height(GtkDrawingArea *self) *)
+PROCEDURE gtk_drawing_area_get_content_height (self: GtkDrawingArea) : INTEGER;
+
+(* void gtk_drawing_area_set_draw_func(GtkDrawingArea *self,
+                                       GtkDrawingAreaDrawFunc draw_func,
+                                       gpointer user_data,
+                                       GDestroyNotify destroy) *)
+PROCEDURE gtk_drawing_area_set_draw_func (self: GtkDrawingArea;
+                                          draw_func: ADDRESS;
+                                          user_data: ADDRESS;
+                                          destroy: ADDRESS);
+
+END GtkDrawingArea.

+ 54 - 0
src/GtkFilterListModel.def

@@ -0,0 +1,54 @@
+DEFINITION MODULE FOR "C" GtkFilterListModel ;
+
+(*
+   m2-GTK4 - GtkFilterListModel: a GListModel that filters another model.
+   Mapped onto <gtk/gtkfilterlistmodel.h>.
+
+   Wrap a GListModel with a filter (see GtkCustomFilter) to expose only
+   the items the filter matches.  Signal: "items-changed".
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkFilterListModel,
+   gtk_filter_list_model_new,
+   gtk_filter_list_model_set_model, gtk_filter_list_model_get_model,
+   gtk_filter_list_model_set_filter, gtk_filter_list_model_get_filter,
+   gtk_filter_list_model_set_incremental,
+   gtk_filter_list_model_get_pending;
+
+
+TYPE
+   GtkFilterListModel = ADDRESS;
+
+(* GtkFilterListModel *gtk_filter_list_model_new(GListModel *model,
+                                                  GtkFilter *filter) *)
+PROCEDURE gtk_filter_list_model_new (model: ADDRESS;
+                                     filter: ADDRESS) : GtkFilterListModel;
+
+(* void gtk_filter_list_model_set_model(GtkFilterListModel *self,
+                                        GListModel *model) *)
+PROCEDURE gtk_filter_list_model_set_model (self: GtkFilterListModel;
+                                           model: ADDRESS);
+
+(* GListModel *gtk_filter_list_model_get_model(GtkFilterListModel *self) *)
+PROCEDURE gtk_filter_list_model_get_model (self: GtkFilterListModel) : ADDRESS;
+
+(* void gtk_filter_list_model_set_filter(GtkFilterListModel *self,
+                                         GtkFilter *filter) *)
+PROCEDURE gtk_filter_list_model_set_filter (self: GtkFilterListModel;
+                                            filter: ADDRESS);
+
+(* GtkFilter *gtk_filter_list_model_get_filter(GtkFilterListModel *self) *)
+PROCEDURE gtk_filter_list_model_get_filter (self: GtkFilterListModel) : ADDRESS;
+
+(* void gtk_filter_list_model_set_incremental(GtkFilterListModel *self,
+                                              gboolean incremental) *)
+PROCEDURE gtk_filter_list_model_set_incremental (self: GtkFilterListModel;
+                                                 incremental: INTEGER);
+
+(* guint gtk_filter_list_model_get_pending(GtkFilterListModel *self) *)
+PROCEDURE gtk_filter_list_model_get_pending (self: GtkFilterListModel) : CARDINAL;
+
+END GtkFilterListModel.

+ 67 - 0
src/GtkRevealer.def

@@ -0,0 +1,67 @@
+DEFINITION MODULE FOR "C" GtkRevealer ;
+
+(*
+   m2-GTK4 - GtkRevealer: animates a child in and out of view.  Mapped
+   onto <gtk/gtkrevealer.h>.
+
+   Signal: "notify::reveal-child".
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkRevealer, GtkRevealerTransitionType,
+   GtkRevealerNone, GtkRevealerCrossfade,
+   GtkRevealerSlideRight, GtkRevealerSlideLeft,
+   GtkRevealerSlideUp, GtkRevealerSlideDown,
+   GtkRevealerSwingRight, GtkRevealerSwingLeft,
+   GtkRevealerSwingUp, GtkRevealerSwingDown,
+   gtk_revealer_new,
+   gtk_revealer_set_child, gtk_revealer_get_child,
+   gtk_revealer_set_reveal_child, gtk_revealer_get_reveal_child,
+   gtk_revealer_set_transition_type, gtk_revealer_set_transition_duration;
+
+
+TYPE
+   GtkRevealer = ADDRESS;
+   GtkRevealerTransitionType = CARDINAL;
+
+CONST
+   GtkRevealerNone       = 0;
+   GtkRevealerCrossfade  = 1;
+   GtkRevealerSlideRight = 2;
+   GtkRevealerSlideLeft  = 3;
+   GtkRevealerSlideUp    = 4;
+   GtkRevealerSlideDown  = 5;
+   GtkRevealerSwingRight = 6;
+   GtkRevealerSwingLeft  = 7;
+   GtkRevealerSwingUp    = 8;
+   GtkRevealerSwingDown  = 9;
+
+(* GtkWidget *gtk_revealer_new(void) *)
+PROCEDURE gtk_revealer_new () : GtkRevealer;
+
+(* void gtk_revealer_set_child(GtkRevealer *revealer, GtkWidget *child) *)
+PROCEDURE gtk_revealer_set_child (revealer: GtkRevealer; child: ADDRESS);
+
+(* GtkWidget *gtk_revealer_get_child(GtkRevealer *revealer) *)
+PROCEDURE gtk_revealer_get_child (revealer: GtkRevealer) : ADDRESS;
+
+(* void gtk_revealer_set_reveal_child(GtkRevealer *revealer, gboolean reveal_child) *)
+PROCEDURE gtk_revealer_set_reveal_child (revealer: GtkRevealer;
+                                         reveal_child: INTEGER);
+
+(* gboolean gtk_revealer_get_reveal_child(GtkRevealer *revealer) *)
+PROCEDURE gtk_revealer_get_reveal_child (revealer: GtkRevealer) : [ INTEGER ];
+
+(* void gtk_revealer_set_transition_type(GtkRevealer *revealer,
+                                         GtkRevealerTransitionType transition) *)
+PROCEDURE gtk_revealer_set_transition_type (revealer: GtkRevealer;
+                                            transition: GtkRevealerTransitionType);
+
+(* void gtk_revealer_set_transition_duration(GtkRevealer *revealer,
+                                             guint duration) *)
+PROCEDURE gtk_revealer_set_transition_duration (revealer: GtkRevealer;
+                                                duration: CARDINAL);
+
+END GtkRevealer.

+ 44 - 0
src/GtkSearchBar.def

@@ -0,0 +1,44 @@
+DEFINITION MODULE FOR "C" GtkSearchBar ;
+
+(*
+   m2-GTK4 - GtkSearchBar: a container that reveals a search entry.
+   Mapped onto <gtk/gtksearchbar.h>.
+
+   Put the entry in with set_child, wire it with connect_entry so the
+   bar reacts to typing, and toggle search_mode to show/hide it.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkSearchBar,
+   gtk_search_bar_new,
+   gtk_search_bar_set_child, gtk_search_bar_connect_entry,
+   gtk_search_bar_set_search_mode, gtk_search_bar_get_search_mode,
+   gtk_search_bar_set_show_close_button;
+
+
+TYPE
+   GtkSearchBar = ADDRESS;
+
+(* GtkWidget *gtk_search_bar_new(void) *)
+PROCEDURE gtk_search_bar_new () : GtkSearchBar;
+
+(* void gtk_search_bar_set_child(GtkSearchBar *bar, GtkWidget *child) *)
+PROCEDURE gtk_search_bar_set_child (bar: GtkSearchBar; child: ADDRESS);
+
+(* void gtk_search_bar_connect_entry(GtkSearchBar *bar, GtkEditable *entry) *)
+PROCEDURE gtk_search_bar_connect_entry (bar: GtkSearchBar; entry: ADDRESS);
+
+(* void gtk_search_bar_set_search_mode(GtkSearchBar *bar, gboolean search_mode) *)
+PROCEDURE gtk_search_bar_set_search_mode (bar: GtkSearchBar;
+                                          search_mode: INTEGER);
+
+(* gboolean gtk_search_bar_get_search_mode(GtkSearchBar *bar) *)
+PROCEDURE gtk_search_bar_get_search_mode (bar: GtkSearchBar) : [ INTEGER ];
+
+(* void gtk_search_bar_set_show_close_button(GtkSearchBar *bar, gboolean visible) *)
+PROCEDURE gtk_search_bar_set_show_close_button (bar: GtkSearchBar;
+                                                visible: INTEGER);
+
+END GtkSearchBar.

+ 44 - 0
src/GtkSortListModel.def

@@ -0,0 +1,44 @@
+DEFINITION MODULE FOR "C" GtkSortListModel ;
+
+(*
+   m2-GTK4 - GtkSortListModel: a GListModel that sorts another model.
+   Mapped onto <gtk/gtksortlistmodel.h>.
+
+   Wrap a GListModel with a sorter (see GtkCustomSorter or
+   GtkStringSorter).  Signal: "items-changed".
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkSortListModel,
+   gtk_sort_list_model_new,
+   gtk_sort_list_model_set_model, gtk_sort_list_model_get_model,
+   gtk_sort_list_model_set_sorter, gtk_sort_list_model_get_sorter,
+   gtk_sort_list_model_get_pending;
+
+
+TYPE
+   GtkSortListModel = ADDRESS;
+
+(* GtkSortListModel *gtk_sort_list_model_new(GListModel *model,
+                                              GtkSorter *sorter) *)
+PROCEDURE gtk_sort_list_model_new (model: ADDRESS;
+                                   sorter: ADDRESS) : GtkSortListModel;
+
+(* void gtk_sort_list_model_set_model(GtkSortListModel *self, GListModel *model) *)
+PROCEDURE gtk_sort_list_model_set_model (self: GtkSortListModel; model: ADDRESS);
+
+(* GListModel *gtk_sort_list_model_get_model(GtkSortListModel *self) *)
+PROCEDURE gtk_sort_list_model_get_model (self: GtkSortListModel) : ADDRESS;
+
+(* void gtk_sort_list_model_set_sorter(GtkSortListModel *self, GtkSorter *sorter) *)
+PROCEDURE gtk_sort_list_model_set_sorter (self: GtkSortListModel; sorter: ADDRESS);
+
+(* GtkSorter *gtk_sort_list_model_get_sorter(GtkSortListModel *self) *)
+PROCEDURE gtk_sort_list_model_get_sorter (self: GtkSortListModel) : ADDRESS;
+
+(* guint gtk_sort_list_model_get_pending(GtkSortListModel *self) *)
+PROCEDURE gtk_sort_list_model_get_pending (self: GtkSortListModel) : CARDINAL;
+
+END GtkSortListModel.

+ 31 - 0
src/GtkStackSidebar.def

@@ -0,0 +1,31 @@
+DEFINITION MODULE FOR "C" GtkStackSidebar ;
+
+(*
+   m2-GTK4 - GtkStackSidebar: an automatically generated sidebar that
+   switches a GtkStack.  Mapped onto <gtk/gtkstacksidebar.h>.
+
+   It lists the stack's titled children (gtk_stack_add_titled) and shows
+   the selected one.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkStackSidebar,
+   gtk_stack_sidebar_new,
+   gtk_stack_sidebar_set_stack, gtk_stack_sidebar_get_stack;
+
+
+TYPE
+   GtkStackSidebar = ADDRESS;
+
+(* GtkWidget *gtk_stack_sidebar_new(void) *)
+PROCEDURE gtk_stack_sidebar_new () : GtkStackSidebar;
+
+(* void gtk_stack_sidebar_set_stack(GtkStackSidebar *self, GtkStack *stack) *)
+PROCEDURE gtk_stack_sidebar_set_stack (self: GtkStackSidebar; stack: ADDRESS);
+
+(* GtkStack *gtk_stack_sidebar_get_stack(GtkStackSidebar *self) *)
+PROCEDURE gtk_stack_sidebar_get_stack (self: GtkStackSidebar) : ADDRESS;
+
+END GtkStackSidebar.

+ 77 - 0
src/Pango.def

@@ -0,0 +1,77 @@
+DEFINITION MODULE FOR "C" Pango ;
+
+(*
+   m2-GTK4 - curated Pango subset for text layout and drawing.
+   Mapped onto <pango/pango.h>.
+
+   PangoLayout and PangoFontDescription are opaque (ADDRESS).  The
+   PangoRectangle is a plain value record (4 ints), used by
+   pango_layout_get_pixel_extents.
+
+   Typical use with PangoCairo:
+      desc   := pango_font_description_from_string("Sans 14");
+      layout := pango_cairo_create_layout(cr);
+      pango_layout_set_font_description(layout, desc);
+      pango_layout_set_text(layout, "Hello", -1);
+      pango_cairo_show_layout(cr, layout);
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   PangoLayout, PangoFontDescription, PangoRectangle,
+   PangoAlignment, PangoAlignLeft, PangoAlignCenter, PangoAlignRight,
+   pango_layout_set_text, pango_layout_set_font_description,
+   pango_layout_set_width, pango_layout_set_alignment,
+   pango_layout_get_pixel_extents, pango_layout_get_pixel_size,
+   pango_font_description_from_string, pango_font_description_free;
+
+
+TYPE
+   PangoLayout = ADDRESS;
+   PangoFontDescription = ADDRESS;
+   PangoRectangle = RECORD
+      x, y, width, height: INTEGER;
+   END;
+   PangoAlignment = CARDINAL;
+
+CONST
+   PangoAlignLeft   = 0;
+   PangoAlignCenter = 1;
+   PangoAlignRight  = 2;
+
+(* void pango_layout_set_text(PangoLayout *layout, const char *text, int length) *)
+PROCEDURE pango_layout_set_text (layout: PangoLayout; text: ARRAY OF CHAR;
+                                 length: INTEGER);
+
+(* void pango_layout_set_font_description(PangoLayout *layout,
+                                          const PangoFontDescription *desc) *)
+PROCEDURE pango_layout_set_font_description (layout: PangoLayout;
+                                             desc: PangoFontDescription);
+
+(* void pango_layout_set_width(PangoLayout *layout, int width) *)
+PROCEDURE pango_layout_set_width (layout: PangoLayout; width: INTEGER);
+
+(* void pango_layout_set_alignment(PangoLayout *layout, PangoAlignment alignment) *)
+PROCEDURE pango_layout_set_alignment (layout: PangoLayout;
+                                      alignment: PangoAlignment);
+
+(* void pango_layout_get_pixel_extents(PangoLayout *layout,
+                                        PangoRectangle *ink_rect,
+                                        PangoRectangle *logical_rect) *)
+PROCEDURE pango_layout_get_pixel_extents (layout: PangoLayout;
+                                          VAR ink: PangoRectangle;
+                                          VAR logical: PangoRectangle);
+
+(* void pango_layout_get_pixel_size(PangoLayout *layout,
+                                     int *width, int *height) *)
+PROCEDURE pango_layout_get_pixel_size (layout: PangoLayout;
+                                       VAR width, height: INTEGER);
+
+(* PangoFontDescription *pango_font_description_from_string(const char *str) *)
+PROCEDURE pango_font_description_from_string (str: ARRAY OF CHAR) : PangoFontDescription;
+
+(* void pango_font_description_free(PangoFontDescription *desc) *)
+PROCEDURE pango_font_description_free (desc: PangoFontDescription);
+
+END Pango.

+ 28 - 0
src/PangoCairo.def

@@ -0,0 +1,28 @@
+DEFINITION MODULE FOR "C" PangoCairo ;
+
+(*
+   m2-GTK4 - PangoCairo: render Pango layouts onto a Cairo context.
+   Mapped onto <pango/pangocairo.h>.
+
+   Create a layout from a cairo_t (GtkDrawingArea's cr, or an image
+   surface), set its text/font, then draw it with pango_cairo_show_layout.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM Pango IMPORT PangoLayout;
+
+EXPORT UNQUALIFIED
+   pango_cairo_create_layout, pango_cairo_show_layout,
+   pango_cairo_update_layout;
+
+
+(* PangoLayout *pango_cairo_create_layout(cairo_t *cr) *)
+PROCEDURE pango_cairo_create_layout (cr: ADDRESS) : PangoLayout;
+
+(* void pango_cairo_show_layout(cairo_t *cr, PangoLayout *layout) *)
+PROCEDURE pango_cairo_show_layout (cr: ADDRESS; layout: PangoLayout);
+
+(* void pango_cairo_update_layout(cairo_t *cr, PangoLayout *layout) *)
+PROCEDURE pango_cairo_update_layout (cr: ADDRESS; layout: PangoLayout);
+
+END PangoCairo.

+ 2 - 2
tests/run_tests.sh

@@ -13,7 +13,7 @@ CFLAGS=$(pkg-config --cflags gtk4)
 LIBS=$(pkg-config --libs gtk4)
 LIBS=$(pkg-config --libs gtk4)
 
 
 # libadwaita is optional: add its flags and test when available.
 # libadwaita is optional: add its flags and test when available.
-TESTS="test_glib test_version test_widgets test_inputs test_inputs2 test_containers test_containers2 test_actions test_closure test_headerbar test_gio test_gerror test_filedialog test_views test_builder test_utility test_css test_popover test_dialogs test_application"
+TESTS="test_glib test_version test_widgets test_inputs test_inputs2 test_containers test_containers2 test_actions test_closure test_headerbar test_gio test_gerror test_filedialog test_views test_builder test_utility test_css test_popover test_dialogs test_models test_cairo test_drawing test_application"
 if pkg-config --exists libadwaita-1 2>/dev/null; then
 if pkg-config --exists libadwaita-1 2>/dev/null; then
   CFLAGS="$CFLAGS $(pkg-config --cflags libadwaita-1)"
   CFLAGS="$CFLAGS $(pkg-config --cflags libadwaita-1)"
   LIBS="$LIBS $(pkg-config --libs libadwaita-1)"
   LIBS="$LIBS $(pkg-config --libs libadwaita-1)"
@@ -45,7 +45,7 @@ for t in $TESTS; do
   $GM2 -I"$ROOT/src" -I"$ROOT/lib" $GM2FLAGS $CFLAGS \
   $GM2 -I"$ROOT/src" -I"$ROOT/lib" $GM2FLAGS $CFLAGS \
        "$ROOT/tests/$t.mod" "$OBJ/GtkUtils.o" "$OBJ/GtkClosures.o" -o "$BIN/$t" $LIBS
        "$ROOT/tests/$t.mod" "$OBJ/GtkUtils.o" "$OBJ/GtkClosures.o" -o "$BIN/$t" $LIBS
   case "$t" in
   case "$t" in
-    test_widgets|test_inputs|test_inputs2|test_containers|test_containers2|test_actions|test_closure|test_headerbar|test_filedialog|test_views|test_builder|test_utility|test_css|test_popover|test_dialogs|test_adw|test_application) needs_display=1 ;;
+    test_widgets|test_inputs|test_inputs2|test_containers|test_containers2|test_actions|test_closure|test_headerbar|test_filedialog|test_views|test_builder|test_utility|test_css|test_popover|test_dialogs|test_models|test_drawing|test_adw|test_application) needs_display=1 ;;
     *) needs_display=0 ;;
     *) needs_display=0 ;;
   esac
   esac
   if [ "$needs_display" = 1 ] && [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
   if [ "$needs_display" = 1 ] && [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then

+ 79 - 0
tests/test_cairo.mod

@@ -0,0 +1,79 @@
+MODULE test_cairo ;
+
+(*
+   m2-GTK4 test - Cairo + Pango on an off-screen image surface.
+
+   Draws a filled rectangle and lays out text, checks the surface/text
+   extents, and writes a PNG (build/m2cairo.png).  Needs no display.
+*)
+
+FROM Cairo IMPORT cairo_image_surface_create, cairo_create, cairo_destroy,
+   cairo_surface_destroy, cairo_surface_status, cairo_status,
+   cairo_set_source_rgb, cairo_rectangle, cairo_fill,
+   cairo_surface_write_to_png, CairoFormatARGB32;
+FROM Pango IMPORT pango_font_description_from_string,
+   pango_font_description_free, pango_layout_set_text,
+   pango_layout_set_font_description, pango_layout_get_pixel_size,
+   pango_layout_get_pixel_extents, PangoRectangle;
+FROM PangoCairo IMPORT pango_cairo_create_layout, pango_cairo_show_layout;
+FROM GObject IMPORT g_object_unref;
+FROM SYSTEM IMPORT ADDRESS;
+FROM libc IMPORT printf;
+
+VAR
+   failed: BOOLEAN;
+
+PROCEDURE Check (condition: BOOLEAN; what: ARRAY OF CHAR);
+BEGIN
+   IF NOT condition THEN
+      printf("FAIL: %s\n", what);
+      failed := TRUE
+   END
+END Check;
+
+VAR
+   surface, cr, layout, desc: ADDRESS;
+   ink, logical: PangoRectangle;
+   w, h: INTEGER;
+BEGIN
+   failed := FALSE;
+
+   surface := cairo_image_surface_create(CairoFormatARGB32, 200, 100);
+   Check(cairo_surface_status(surface) = 0, "image surface status");
+
+   cr := cairo_create(surface);
+   Check(cairo_status(cr) = 0, "cairo context status");
+
+   cairo_set_source_rgb(cr, 0.20, 0.40, 0.80);
+   cairo_rectangle(cr, 10.0, 10.0, 180.0, 80.0);
+   cairo_fill(cr);
+
+   layout := pango_cairo_create_layout(cr);
+   desc := pango_font_description_from_string("Sans 14");
+   pango_layout_set_font_description(layout, desc);
+   pango_font_description_free(desc);
+   pango_layout_set_text(layout, "Hello Pango", -1);
+
+   pango_layout_get_pixel_size(layout, w, h);
+   Check((w > 0) AND (h > 0), "layout pixel size > 0");
+   pango_layout_get_pixel_extents(layout, ink, logical);
+   Check(logical.width > 0, "logical extent width > 0");
+
+   cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+   pango_cairo_show_layout(cr, layout);
+
+   Check(cairo_surface_write_to_png(surface, "/tmp/opencode/m2cairo.png") = 0,
+         "write PNG");
+
+   g_object_unref(layout);
+   cairo_destroy(cr);
+   cairo_surface_destroy(surface);
+
+   IF failed THEN
+      printf("test_cairo: FAIL\n");
+      HALT(1)
+   ELSE
+      printf("test_cairo: PASS\n");
+      HALT(0)
+   END
+END test_cairo.

+ 100 - 0
tests/test_drawing.mod

@@ -0,0 +1,100 @@
+MODULE test_drawing ;
+
+(*
+   m2-GTK4 test - GtkDrawingArea with a Cairo draw callback.
+
+   Presents a window with a drawing area and checks (from a timeout,
+   once it has been drawn) that the callback ran.  Requires a display.
+*)
+
+FROM Gio IMPORT g_application_run, g_application_quit;
+FROM GtkApplication IMPORT gtk_application_new;
+FROM GtkWindow IMPORT gtk_application_window_new, gtk_window_set_child,
+   gtk_window_present;
+FROM GtkDrawingArea IMPORT gtk_drawing_area_new,
+   gtk_drawing_area_set_content_width, gtk_drawing_area_set_content_height,
+   gtk_drawing_area_get_content_width, gtk_drawing_area_get_content_height,
+   gtk_drawing_area_set_draw_func;
+FROM Cairo IMPORT cairo_set_source_rgb, cairo_rectangle, cairo_fill;
+FROM GtkUtils IMPORT Connect;
+FROM GLib IMPORT g_timeout_add, GSourceRemove;
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM libc IMPORT printf;
+
+CONST
+   AppId = "org.example.m2gtk4.TestDrawing";
+
+VAR
+   app: ADDRESS;
+   passed, drew: BOOLEAN;
+
+(* void draw(GtkDrawingArea *area, cairo_t *cr, int w, int h, gpointer data) *)
+PROCEDURE Draw (area: ADDRESS; cr: ADDRESS; width, height: INTEGER;
+                data: ADDRESS);
+BEGIN
+   drew := TRUE;
+   cairo_set_source_rgb(cr, 0.90, 0.30, 0.20);
+   cairo_rectangle(cr, 0.0, 0.0, VAL(REAL, width), VAL(REAL, height));
+   cairo_fill(cr);
+   cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
+   cairo_rectangle(cr, 20.0, 20.0, 60.0, 40.0);
+   cairo_fill(cr)
+END Draw;
+
+PROCEDURE AfterMap (data: ADDRESS) : INTEGER;
+BEGIN
+   IF NOT drew THEN
+      printf("draw callback did not run\n");
+      passed := FALSE
+   END;
+   g_application_quit(app);
+   RETURN GSourceRemove
+END AfterMap;
+
+PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
+VAR
+   window, area: ADDRESS;
+   ok: BOOLEAN;
+BEGIN
+   ok := TRUE;
+   drew := FALSE;
+
+   area := gtk_drawing_area_new();
+   gtk_drawing_area_set_content_width(area, 200);
+   gtk_drawing_area_set_content_height(area, 120);
+   IF gtk_drawing_area_get_content_width(area) # 200 THEN
+      printf("content width wrong\n"); ok := FALSE
+   END;
+   IF gtk_drawing_area_get_content_height(area) # 120 THEN
+      printf("content height wrong\n"); ok := FALSE
+   END;
+   gtk_drawing_area_set_draw_func(area, ADR(Draw), NIL, NIL);
+
+   window := gtk_application_window_new(application);
+   gtk_window_set_child(window, area);
+   gtk_window_present(window);
+
+   passed := ok;
+   g_timeout_add(300, AfterMap, NIL)
+END OnActivate;
+
+VAR
+   rc: INTEGER;
+BEGIN
+   passed := FALSE;
+   app := gtk_application_new(AppId, 0);
+   IF app = NIL THEN
+      printf("test_drawing: FAIL (no app)\n");
+      HALT(1)
+   END;
+   Connect(app, "activate", ADR(OnActivate), NIL);
+   rc := g_application_run(app, 0, NIL);
+
+   IF passed THEN
+      printf("test_drawing: PASS\n");
+      HALT(0)
+   ELSE
+      printf("test_drawing: FAIL\n");
+      HALT(1)
+   END
+END test_drawing.

+ 114 - 0
tests/test_models.mod

@@ -0,0 +1,114 @@
+MODULE test_models ;
+
+(*
+   m2-GTK4 test - filter and sort models.
+
+   Wraps a GListStore of strings with a GtkCustomFilter (via
+   GtkFilterListModel) and a GtkCustomSorter (via GtkSortListModel).
+   The match/compare callbacks are plain Modula-2 procedures.  Needs no
+   display.
+*)
+
+FROM GListStore IMPORT g_list_store_new, g_list_store_append;
+FROM GtkStringObject IMPORT gtk_string_object_new,
+   gtk_string_object_get_string, gtk_string_object_get_type;
+FROM GListModel IMPORT g_list_model_get_n_items, g_list_model_get_item;
+FROM GtkCustomFilter IMPORT gtk_custom_filter_new;
+FROM GtkFilterListModel IMPORT gtk_filter_list_model_new,
+   gtk_filter_list_model_set_incremental, gtk_filter_list_model_get_filter;
+FROM GtkCustomSorter IMPORT gtk_custom_sorter_new;
+FROM GtkSortListModel IMPORT gtk_sort_list_model_new,
+   gtk_sort_list_model_get_sorter;
+FROM GObject IMPORT g_object_ref, g_object_unref;
+FROM GtkUtils IMPORT CStrToM2;
+FROM Gtk IMPORT gtk_init;
+FROM GLib IMPORT g_strcmp0;
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM libc IMPORT strncpy, printf;
+
+VAR
+   failed: BOOLEAN;
+
+PROCEDURE Check (condition: BOOLEAN; what: ARRAY OF CHAR);
+BEGIN
+   IF NOT condition THEN
+      printf("FAIL: %s\n", what);
+      failed := TRUE
+   END
+END Check;
+
+(* gboolean match(gpointer item, gpointer user_data): keep names with 'l'. *)
+PROCEDURE Match (item: ADDRESS; user: ADDRESS) : INTEGER;
+VAR
+   src: ADDRESS;
+   buf: ARRAY [0..63] OF CHAR;
+   i: CARDINAL;
+   found: BOOLEAN;
+BEGIN
+   src := gtk_string_object_get_string(item);
+   strncpy(ADR(buf), src, HIGH(buf) + 1);
+   buf[HIGH(buf)] := 0C;
+   found := FALSE;
+   i := 0;
+   WHILE (i < HIGH(buf)) AND (buf[i] # 0C) DO
+      IF (buf[i] = 'l') OR (buf[i] = 'L') THEN found := TRUE END;
+      INC(i)
+   END;
+   IF found THEN RETURN 1 ELSE RETURN 0 END
+END Match;
+
+(* int compare(gconstpointer a, gconstpointer b, gpointer user_data) *)
+PROCEDURE Compare (a, b, user: ADDRESS) : INTEGER;
+BEGIN
+   RETURN g_strcmp0(gtk_string_object_get_string(a),
+                    gtk_string_object_get_string(b))
+END Compare;
+
+VAR
+   store, filter, filterModel, sorter, sortModel, item: ADDRESS;
+   buf: ARRAY [0..63] OF CHAR;
+BEGIN
+   failed := FALSE;
+
+   gtk_init();   (* GTK model types need the GTK type system initialised *)
+
+   store := g_list_store_new(gtk_string_object_get_type());
+   g_list_store_append(store, gtk_string_object_new("Alpha"));
+   g_list_store_append(store, gtk_string_object_new("Beta"));
+   g_list_store_append(store, gtk_string_object_new("Gamma"));
+   g_list_store_append(store, gtk_string_object_new("Delta"));
+
+   (* filter: only "Alpha" and "Delta" contain 'l'.
+      gtk_filter_list_model_new takes ownership of model and filter. *)
+   filter := gtk_custom_filter_new(ADR(Match), NIL, NIL);
+   filterModel := gtk_filter_list_model_new(g_object_ref(store), filter);
+   gtk_filter_list_model_set_incremental(filterModel, 0);
+   Check(gtk_filter_list_model_get_filter(filterModel) = filter,
+         "filter list model filter");
+   Check(g_list_model_get_n_items(filterModel) = 2, "filtered count");
+
+   (* sort: ascending by string; first item must be "Alpha".
+      gtk_sort_list_model_new takes ownership of model and sorter. *)
+   sorter := gtk_custom_sorter_new(ADR(Compare), NIL, NIL);
+   sortModel := gtk_sort_list_model_new(g_object_ref(store), sorter);
+   Check(gtk_sort_list_model_get_sorter(sortModel) = sorter,
+         "sort list model sorter");
+   Check(g_list_model_get_n_items(sortModel) = 4, "sorted count");
+   item := g_list_model_get_item(sortModel, 0);
+   CStrToM2(gtk_string_object_get_string(item), buf);
+   Check(buf[0] = 'A', "sorted first item is Alpha");
+   g_object_unref(item);
+
+   (* filter and sorter were consumed by the models above *)
+   g_object_unref(filterModel);
+   g_object_unref(sortModel);
+   g_object_unref(store);
+
+   IF failed THEN
+      printf("test_models: FAIL\n");
+      HALT(1)
+   ELSE
+      printf("test_models: PASS\n");
+      HALT(0)
+   END
+END test_models.