Quellcode durchsuchen

feat: C shim for GObject subclassing (custom GtkWidget / GdkPaintable)

GNU Modula-2 cannot register a GObject subclass or override vfuncs,
which blocked many gtk-demo widgets.

- lib/m2gtkshim.c: a C support library registering
    * M2GtkWidget  - a GtkWidget subclass whose snapshot vfunc calls a
                     Modula-2 Cairo callback (via gtk_snapshot_append_cairo)
    * M2Paintable  - a GdkPaintable implementation, same idea
  M2 reuses its existing Cairo binding; the GObject/Graphene plumbing
  stays in C.
- lib/M2GtkShim.def: the Modula-2 binding
- GtkPicture: set/get_paintable
- test_shim (custom widget + paintable draw callbacks fire)
- examples/customwidget.mod

The build now compiles the C shim and links it into tests/examples.

28 tests pass; 123 src modules, 20 examples.
Eric Streit vor 2 Tagen
Ursprung
Commit
963a0b6faf
8 geänderte Dateien mit 517 neuen und 10 gelöschten Zeilen
  1. 8 3
      Makefile
  2. 5 3
      README.md
  3. 100 0
      examples/customwidget.mod
  4. 52 0
      lib/M2GtkShim.def
  5. 217 0
      lib/m2gtkshim.c
  6. 8 1
      src/GtkPicture.def
  7. 6 3
      tests/run_tests.sh
  8. 121 0
      tests/test_shim.mod

+ 8 - 3
Makefile

@@ -1,5 +1,6 @@
 # m2-GTK4 - GNU Modula-2 bindings for GTK4
 GM2      ?= gm2
+CC       ?= gcc
 GTKCFG   := $(shell pkg-config --cflags gtk4 2>/dev/null)
 GTKLIBS  := $(shell pkg-config --libs gtk4 2>/dev/null || echo -lgtk-4)
 # libadwaita is optional; added to the flags when present.
@@ -14,12 +15,12 @@ SRC_DIR := src
 LIB_DIR := lib
 BLD     := build
 OBJDIR  := $(BLD)/objs
-OBJS    := $(OBJDIR)/GtkUtils.o $(OBJDIR)/GtkClosures.o
+OBJS    := $(OBJDIR)/GtkUtils.o $(OBJDIR)/GtkClosures.o $(OBJDIR)/m2gtkshim.o
 
 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_models test_cairo test_drawing test_gestures test_models2 test_widgets3 test_application
-EXAMPLES := hello counter inputs containers actions headerbar editor layouts files views builder feedback styling popover dialogs drawing demo gestures
+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_gestures test_models2 test_widgets3 test_shim test_application
+EXAMPLES := hello counter inputs containers actions headerbar editor layouts files views builder feedback styling popover dialogs drawing demo gestures customwidget
 
 # Include the libadwaita test/example only when libadwaita is installed.
 ifneq ($(ADWLIBS),)
@@ -48,6 +49,10 @@ $(OBJDIR)/GtkUtils.o: $(LIB_DIR)/GtkUtils.mod $(LIB_DIR)/GtkUtils.def | $(OBJDIR
 $(OBJDIR)/GtkClosures.o: $(LIB_DIR)/GtkClosures.mod $(LIB_DIR)/GtkClosures.def | $(OBJDIR)
 	$(GM2) $(INCLUDES) $(GM2FLAGS) -c $< -o $@
 
+# C shim for GObject subclassing (custom GtkWidget / GdkPaintable).
+$(OBJDIR)/m2gtkshim.o: $(LIB_DIR)/m2gtkshim.c | $(OBJDIR)
+	$(CC) $(GTKCFG) -c $< -o $@
+
 tests: $(BLD)/tests $(OBJS) $(addprefix $(BLD)/tests/,$(TESTS))
 
 $(BLD)/tests/%: tests/%.mod $(OBJS) | $(BLD)/tests

+ 5 - 3
README.md

@@ -21,7 +21,7 @@ Verified with `gm2` 16.0.1 (experimental), GTK 4.18.6 and libadwaita 1.7.6.
 - **Pure-Modula-2 helpers**: `GtkUtils` (string/number conversion,
   `Connect`, `SetAccel`, ownership and `GError` helpers) and
   `GtkClosures` (per-connection signal contexts with cleanup).
-- **27 tests** and **19 runnable examples**.
+- **28 tests** and **20 runnable examples**.
 - **Generator + verifier**: `tools/gir2def.py` turns a `.gir` file into
   `.def` files; `tools/gir_verify.py` checks every generated identifier
   against the installed library. Verified clean across 8602 identifiers.
@@ -121,6 +121,7 @@ gm2 -fiso -Isrc -Ilib myprog.mod \
 | `drawing` | `GtkDrawingArea` + Cairo + Pango |
 | `demo` | searchable demo browser (filter model + stack of pages) |
 | `gestures` | click/drag gestures on a drawing area |
+| `customwidget` | custom `GtkWidget` and `GdkPaintable` via the C shim |
 | `adw` | libadwaita preferences app (toolbar view, rows, toast, about) |
 
 ## Module reference
@@ -185,12 +186,13 @@ gm2 -fiso -Isrc -Ilib myprog.mod \
 | `AdwToast`, `AdwToastOverlay` | transient notifications |
 | `AdwAboutWindow`, `AdwMessageDialog` | about and message dialogs |
 
-### Helpers (pure Modula-2, in `lib/`)
+### Helpers and the C shim (`lib/`)
 
 | Module | Contents |
 |---|---|
 | `GtkUtils` | `CStrToM2`, `StrAppend`, `IntToStr`, `Connect`, `SetAccel`, `Unref`, `Free`, `ErrorMessage`, `ClearError` |
 | `GtkClosures` | `Connect2`/`Connect3` (and owned variants) for per-connection signal state |
+| `M2GtkShim` (`lib/m2gtkshim.c`) | C shim for GObject subclassing: a custom `GtkWidget` and `GdkPaintable` whose snapshot vfuncs hand a Cairo context to Modula-2 |
 
 ## Signals, ownership and errors
 
@@ -258,7 +260,7 @@ for the type mapping and limits.
 ```text
 m2-GTK4/
   src/        FOR "C" .def bindings (one per GTK/GIO/Adw object)
-  lib/        pure-M2 helpers (GtkUtils, GtkClosures)
+  lib/        helpers (GtkUtils, GtkClosures) + C shim (m2gtkshim.c)
   tests/      gm2 tests + run_tests.sh (+ a private GSettings schema)
   examples/   runnable GTK4 / libadwaita programs
   tools/      gir2def.py, gir_verify.py, run_gir_check.sh

+ 100 - 0
examples/customwidget.mod

@@ -0,0 +1,100 @@
+MODULE customwidget ;
+
+(*
+   m2-GTK4 example - custom GObject subclasses via the C shim.
+
+   A custom GtkWidget (concentric circles) and a custom GdkPaintable
+   shown in a GtkPicture, both drawn from Modula-2 Cairo callbacks.
+
+   Build:  make examples
+   Run:    ./build/examples/customwidget
+*)
+
+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;
+FROM GtkWidget IMPORT gtk_widget_set_size_request;
+FROM GtkPicture IMPORT gtk_picture_new, gtk_picture_set_paintable;
+FROM M2GtkShim IMPORT m2_gtk_widget_new, m2_paintable_new;
+FROM Cairo IMPORT cairo_set_source_rgb, cairo_rectangle, cairo_fill,
+   cairo_arc, cairo_set_line_width, cairo_stroke;
+FROM GtkClosures IMPORT Connect2;
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM libc IMPORT printf;
+
+CONST
+   AppId = "org.example.m2gtk4.CustomWidget";
+
+PROCEDURE DrawCircles (widget: ADDRESS; cr: ADDRESS; width, height: INTEGER;
+                       user: ADDRESS);
+VAR
+   i: INTEGER;
+   cx, cy, radius: REAL;
+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);
+
+   cx := VAL(REAL, width DIV 2);
+   cy := VAL(REAL, height DIV 2);
+   FOR i := 0 TO 4 DO
+      IF i MOD 2 = 0 THEN
+         cairo_set_source_rgb(cr, 0.20, 0.50, 0.90)
+      ELSE
+         cairo_set_source_rgb(cr, 0.90, 0.30, 0.20)
+      END;
+      radius := VAL(REAL, 12 + i * 14);
+      cairo_arc(cr, cx, cy, radius, 0.0, 6.2831853);
+      cairo_fill(cr)
+   END
+END DrawCircles;
+
+PROCEDURE DrawPaintable (cr: ADDRESS; width, height: INTEGER; user: ADDRESS);
+BEGIN
+   cairo_set_source_rgb(cr, 0.14, 0.54, 0.34);
+   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_set_line_width(cr, 3.0);
+   cairo_rectangle(cr, 4.0, 4.0, VAL(REAL, width) - 8.0, VAL(REAL, height) - 8.0);
+   cairo_stroke(cr)
+END DrawPaintable;
+
+PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
+VAR
+   window, box, custom, picture, paintable: ADDRESS;
+BEGIN
+   custom := m2_gtk_widget_new(ADR(DrawCircles), NIL, NIL);
+   gtk_widget_set_size_request(custom, 180, 180);
+
+   paintable := m2_paintable_new(ADR(DrawPaintable), 120, 80, NIL, NIL);
+   picture := gtk_picture_new();
+   gtk_picture_set_paintable(picture, paintable);
+   gtk_widget_set_size_request(picture, 120, 80);
+
+   box := gtk_box_new(GtkVertical, 12);
+   gtk_box_set_spacing(box, 12);
+   gtk_box_append(box, custom);
+   gtk_box_append(box, picture);
+
+   window := gtk_application_window_new(application);
+   gtk_window_set_title(window, "m2-GTK4 Custom Widget");
+   gtk_window_set_default_size(window, 320, 340);
+   gtk_window_set_child(window, box);
+   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 customwidget.

+ 52 - 0
lib/M2GtkShim.def

@@ -0,0 +1,52 @@
+DEFINITION MODULE FOR "C" M2GtkShim ;
+
+(*
+   m2-GTK4 - C shim for GObject subclassing.
+
+   GNU Modula-2 cannot register a GObject subclass or override vfuncs.
+   This module exposes two C-implemented helper types whose snapshot
+   vfunc hands a Cairo context to a Modula-2 callback, so custom drawing
+   is possible without leaving Modula-2 (and reusing the Cairo binding).
+
+   Provided by lib/m2gtkshim.c:
+
+     M2Widget     a GtkWidget subclass.  The draw callback is
+                    PROCEDURE (widget: ADDRESS; cr: ADDRESS;
+                               width, height: INTEGER; user: ADDRESS);
+                  Set its size with GtkWidget.gtk_widget_set_size_request.
+
+     M2Paintable  a GdkPaintable implementation.  The draw callback is
+                    PROCEDURE (cr: ADDRESS; width, height: INTEGER;
+                               user: ADDRESS);
+                  Give it to a GtkPicture or GtkImage.
+
+   user_data is passed through; the optional destroy procedure runs when
+   the object is finalized.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   M2Widget, M2WidgetDrawFunc,
+   M2Paintable, M2PaintableDrawFunc,
+   m2_gtk_widget_new, m2_paintable_new;
+
+
+TYPE
+   M2Widget = ADDRESS;
+   M2WidgetDrawFunc = ADDRESS;
+   M2Paintable = ADDRESS;
+   M2PaintableDrawFunc = ADDRESS;
+
+(* GtkWidget *m2_gtk_widget_new(M2WidgetDrawFunc draw, gpointer user_data,
+                                GDestroyNotify destroy) *)
+PROCEDURE m2_gtk_widget_new (draw: M2WidgetDrawFunc; user_data: ADDRESS;
+                             destroy: ADDRESS) : M2Widget;
+
+(* GdkPaintable *m2_paintable_new(M2PaintableDrawFunc draw, int width,
+                                  int height, gpointer user_data,
+                                  GDestroyNotify destroy) *)
+PROCEDURE m2_paintable_new (draw: M2PaintableDrawFunc; width, height: INTEGER;
+                            user_data: ADDRESS; destroy: ADDRESS) : M2Paintable;
+
+END M2GtkShim.

+ 217 - 0
lib/m2gtkshim.c

@@ -0,0 +1,217 @@
+/*
+ * m2gtkshim.c - small C support library for the m2-GTK4 Modula-2 bindings.
+ *
+ * GNU Modula-2 cannot register GObject subclasses (no class_init /
+ * vfunc overrides), which blocks widgets and paintables that need a
+ * custom snapshot.  This shim provides two such types whose snapshot
+ * vfuncs hand a Cairo context to a Modula-2 callback, so the drawing
+ * itself reuses the existing Cairo binding:
+ *
+ *   M2GtkWidget  - a GtkWidget subclass; the M2 callback is
+ *                  void draw(GtkWidget *w, cairo_t *cr,
+ *                            int width, int height, gpointer user);
+ *   M2Paintable  - a GdkPaintable implementation; the M2 callback is
+ *                  void draw(cairo_t *cr, int width, int height,
+ *                            gpointer user);
+ *
+ * The M2 callback addresses and user data are supplied at construction;
+ * an optional destroy notify frees the user data.
+ */
+
+#include <gtk/gtk.h>
+
+/* ------------------------------------------------------------------ */
+/* M2GtkWidget                                                         */
+/* ------------------------------------------------------------------ */
+
+typedef void (*M2WidgetDrawFunc) (GtkWidget *widget, cairo_t *cr,
+                                  int width, int height, gpointer user_data);
+
+typedef struct _M2GtkWidget M2GtkWidget;
+typedef struct _M2GtkWidgetClass M2GtkWidgetClass;
+
+struct _M2GtkWidget
+{
+  GtkWidget parent_instance;
+  M2WidgetDrawFunc draw;
+  gpointer user_data;
+  GDestroyNotify destroy;
+};
+
+struct _M2GtkWidgetClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DEFINE_TYPE (M2GtkWidget, m2_gtk_widget, GTK_TYPE_WIDGET)
+
+static void
+m2_gtk_widget_snapshot (GtkWidget *widget, GtkSnapshot *snapshot)
+{
+  M2GtkWidget *self = (M2GtkWidget *) widget;
+  int width = gtk_widget_get_width (widget);
+  int height = gtk_widget_get_height (widget);
+
+  if (self->draw == NULL || width <= 0 || height <= 0)
+    return;
+
+  graphene_rect_t bounds = GRAPHENE_RECT_INIT (0.f, 0.f, width, height);
+  cairo_t *cr = gtk_snapshot_append_cairo (snapshot, &bounds);
+  self->draw (widget, cr, width, height, self->user_data);
+  cairo_destroy (cr);
+}
+
+static void
+m2_gtk_widget_finalize (GObject *object)
+{
+  M2GtkWidget *self = (M2GtkWidget *) object;
+
+  if (self->destroy != NULL)
+    self->destroy (self->user_data);
+
+  G_OBJECT_CLASS (m2_gtk_widget_parent_class)->finalize (object);
+}
+
+static void
+m2_gtk_widget_class_init (M2GtkWidgetClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = m2_gtk_widget_finalize;
+  widget_class->snapshot = m2_gtk_widget_snapshot;
+}
+
+static void
+m2_gtk_widget_init (M2GtkWidget *self)
+{
+  self->draw = NULL;
+  self->user_data = NULL;
+  self->destroy = NULL;
+}
+
+GtkWidget *
+m2_gtk_widget_new (M2WidgetDrawFunc draw, gpointer user_data,
+                   GDestroyNotify destroy)
+{
+  M2GtkWidget *self = g_object_new (m2_gtk_widget_get_type (), NULL);
+
+  self->draw = draw;
+  self->user_data = user_data;
+  self->destroy = destroy;
+  return GTK_WIDGET (self);
+}
+
+/* ------------------------------------------------------------------ */
+/* M2Paintable (implements GdkPaintable)                               */
+/* ------------------------------------------------------------------ */
+
+typedef void (*M2PaintableDrawFunc) (cairo_t *cr, int width, int height,
+                                     gpointer user_data);
+
+typedef struct _M2Paintable M2Paintable;
+typedef struct _M2PaintableClass M2PaintableClass;
+
+struct _M2Paintable
+{
+  GObject parent_instance;
+  M2PaintableDrawFunc draw;
+  gpointer user_data;
+  GDestroyNotify destroy;
+  int width;
+  int height;
+};
+
+struct _M2PaintableClass
+{
+  GObjectClass parent_class;
+};
+
+static void
+m2_paintable_snapshot (GdkPaintable *paintable, GtkSnapshot *snapshot,
+                       double width, double height)
+{
+  M2Paintable *self = (M2Paintable *) paintable;
+  graphene_rect_t bounds = GRAPHENE_RECT_INIT (0.f, 0.f, (float) width,
+                                               (float) height);
+  cairo_t *cr = gtk_snapshot_append_cairo (snapshot, &bounds);
+
+  if (self->draw != NULL)
+    self->draw (cr, (int) width, (int) height, self->user_data);
+
+  cairo_destroy (cr);
+}
+
+static int
+m2_paintable_get_intrinsic_width (GdkPaintable *paintable)
+{
+  return ((M2Paintable *) paintable)->width;
+}
+
+static int
+m2_paintable_get_intrinsic_height (GdkPaintable *paintable)
+{
+  return ((M2Paintable *) paintable)->height;
+}
+
+static double
+m2_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
+{
+  M2Paintable *self = (M2Paintable *) paintable;
+
+  return self->height > 0 ? (double) self->width / (double) self->height : 0.0;
+}
+
+static void
+m2_paintable_paintable_init (GdkPaintableInterface *iface)
+{
+  iface->snapshot = m2_paintable_snapshot;
+  iface->get_intrinsic_width = m2_paintable_get_intrinsic_width;
+  iface->get_intrinsic_height = m2_paintable_get_intrinsic_height;
+  iface->get_intrinsic_aspect_ratio = m2_paintable_get_intrinsic_aspect_ratio;
+}
+
+G_DEFINE_TYPE_WITH_CODE (M2Paintable, m2_paintable, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
+                                                m2_paintable_paintable_init))
+
+static void
+m2_paintable_finalize (GObject *object)
+{
+  M2Paintable *self = (M2Paintable *) object;
+
+  if (self->destroy != NULL)
+    self->destroy (self->user_data);
+
+  G_OBJECT_CLASS (m2_paintable_parent_class)->finalize (object);
+}
+
+static void
+m2_paintable_class_init (M2PaintableClass *klass)
+{
+  G_OBJECT_CLASS (klass)->finalize = m2_paintable_finalize;
+}
+
+static void
+m2_paintable_init (M2Paintable *self)
+{
+  self->draw = NULL;
+  self->user_data = NULL;
+  self->destroy = NULL;
+  self->width = 0;
+  self->height = 0;
+}
+
+GdkPaintable *
+m2_paintable_new (M2PaintableDrawFunc draw, int width, int height,
+                  gpointer user_data, GDestroyNotify destroy)
+{
+  M2Paintable *self = g_object_new (m2_paintable_get_type (), NULL);
+
+  self->draw = draw;
+  self->width = width;
+  self->height = height;
+  self->user_data = user_data;
+  self->destroy = destroy;
+  return GDK_PAINTABLE (self);
+}

+ 8 - 1
src/GtkPicture.def

@@ -13,7 +13,8 @@ EXPORT UNQUALIFIED
    GtkContentFitScaleDown,
    gtk_picture_new, gtk_picture_new_for_filename,
    gtk_picture_set_filename, gtk_picture_set_can_shrink,
-   gtk_picture_set_content_fit;
+   gtk_picture_set_content_fit,
+   gtk_picture_set_paintable, gtk_picture_get_paintable;
 
 
 TYPE
@@ -43,4 +44,10 @@ PROCEDURE gtk_picture_set_can_shrink (self: GtkPicture; can_shrink: INTEGER);
 PROCEDURE gtk_picture_set_content_fit (self: GtkPicture;
                                        content_fit: GtkContentFit);
 
+(* void gtk_picture_set_paintable(GtkPicture *self, GdkPaintable *paintable) *)
+PROCEDURE gtk_picture_set_paintable (self: GtkPicture; paintable: ADDRESS);
+
+(* GdkPaintable *gtk_picture_get_paintable(GtkPicture *self) *)
+PROCEDURE gtk_picture_get_paintable (self: GtkPicture) : ADDRESS;
+
 END GtkPicture.

+ 6 - 3
tests/run_tests.sh

@@ -13,7 +13,7 @@ CFLAGS=$(pkg-config --cflags gtk4)
 LIBS=$(pkg-config --libs gtk4)
 
 # 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_models test_cairo test_drawing test_gestures test_models2 test_widgets3 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_gestures test_models2 test_widgets3 test_shim test_application"
 if pkg-config --exists libadwaita-1 2>/dev/null; then
   CFLAGS="$CFLAGS $(pkg-config --cflags libadwaita-1)"
   LIBS="$LIBS $(pkg-config --libs libadwaita-1)"
@@ -28,6 +28,8 @@ mkdir -p "$BIN" "$OBJ"
 $GM2 -I"$ROOT/src" -I"$ROOT/lib" $GM2FLAGS -c "$ROOT/lib/GtkUtils.mod" -o "$OBJ/GtkUtils.o"
 # shellcheck disable=SC2086
 $GM2 -I"$ROOT/src" -I"$ROOT/lib" $GM2FLAGS -c "$ROOT/lib/GtkClosures.mod" -o "$OBJ/GtkClosures.o"
+# C shim for GObject subclassing (compiled with the C compiler).
+${CC:-cc} $CFLAGS -c "$ROOT/lib/m2gtkshim.c" -o "$OBJ/m2gtkshim.o"
 
 # compile the test-only GSettings schema and make it discoverable
 SCHEMA_SRC="$ROOT/tests/schemas"
@@ -43,9 +45,10 @@ for t in $TESTS; do
   echo "== $t =="
   # shellcheck disable=SC2086
   $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" \
+       "$OBJ/m2gtkshim.o" -o "$BIN/$t" $LIBS
   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_models|test_drawing|test_gestures|test_models2|test_widgets3|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_gestures|test_models2|test_widgets3|test_shim|test_adw|test_application) needs_display=1 ;;
     *) needs_display=0 ;;
   esac
   if [ "$needs_display" = 1 ] && [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then

+ 121 - 0
tests/test_shim.mod

@@ -0,0 +1,121 @@
+MODULE test_shim ;
+
+(*
+   m2-GTK4 test - the C subclassing shim (M2GtkShim).
+
+   Creates a custom GtkWidget and a custom GdkPaintable, each drawing
+   through a Modula-2 Cairo callback, presents them and checks (from a
+   timeout) that both draw callbacks 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 GtkBox IMPORT gtk_box_new, gtk_box_append;
+FROM GtkEnums IMPORT GtkVertical;
+FROM GtkWidget IMPORT gtk_widget_set_size_request;
+FROM GtkPicture IMPORT gtk_picture_new, gtk_picture_set_paintable,
+   gtk_picture_get_paintable;
+FROM M2GtkShim IMPORT m2_gtk_widget_new, m2_paintable_new;
+FROM Cairo IMPORT cairo_set_source_rgb, cairo_rectangle, cairo_fill;
+FROM GLib IMPORT g_timeout_add, GSourceRemove;
+FROM GtkUtils IMPORT Connect;
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM libc IMPORT printf;
+
+CONST
+   AppId = "org.example.m2gtk4.TestShim";
+
+VAR
+   app: ADDRESS;
+   passed, widgetDrew, paintableDrew: BOOLEAN;
+
+PROCEDURE DrawWidget (widget: ADDRESS; cr: ADDRESS; width, height: INTEGER;
+                      user: ADDRESS);
+BEGIN
+   widgetDrew := TRUE;
+   cairo_set_source_rgb(cr, 0.20, 0.50, 0.90);
+   cairo_rectangle(cr, 0.0, 0.0, VAL(REAL, width), VAL(REAL, height));
+   cairo_fill(cr)
+END DrawWidget;
+
+PROCEDURE DrawPaintable (cr: ADDRESS; width, height: INTEGER; user: ADDRESS);
+BEGIN
+   paintableDrew := 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)
+END DrawPaintable;
+
+PROCEDURE AfterMap (data: ADDRESS) : INTEGER;
+BEGIN
+   IF NOT widgetDrew THEN
+      printf("custom widget draw not called\n");
+      passed := FALSE
+   END;
+   IF NOT paintableDrew THEN
+      printf("paintable draw not called\n");
+      passed := FALSE
+   END;
+   g_application_quit(app);
+   RETURN GSourceRemove
+END AfterMap;
+
+PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
+VAR
+   window, box, widget, paintable, picture: ADDRESS;
+   ok: BOOLEAN;
+BEGIN
+   ok := TRUE;
+   widgetDrew := FALSE;
+   paintableDrew := FALSE;
+
+   widget := m2_gtk_widget_new(ADR(DrawWidget), NIL, NIL);
+   IF widget = NIL THEN
+      printf("custom widget NIL\n"); ok := FALSE
+   END;
+   gtk_widget_set_size_request(widget, 120, 80);
+
+   paintable := m2_paintable_new(ADR(DrawPaintable), 60, 40, NIL, NIL);
+   IF paintable = NIL THEN
+      printf("paintable NIL\n"); ok := FALSE
+   END;
+   picture := gtk_picture_new();
+   gtk_picture_set_paintable(picture, paintable);
+   IF gtk_picture_get_paintable(picture) # paintable THEN
+      printf("picture paintable wrong\n"); ok := FALSE
+   END;
+   gtk_widget_set_size_request(picture, 80, 60);
+
+   box := gtk_box_new(GtkVertical, 6);
+   gtk_box_append(box, widget);
+   gtk_box_append(box, picture);
+   window := gtk_application_window_new(application);
+   gtk_window_set_child(window, box);
+   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_shim: 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_shim: PASS\n");
+      HALT(0)
+   ELSE
+      printf("test_shim: FAIL\n");
+      HALT(1)
+   END
+END test_shim.