Bläddra i källkod

feat: custom layout manager (shim), OpenGL, and printing

Shim (item 1):
- M2LayoutManager: a GtkLayoutManager subclass in lib/m2gtkshim.c whose
  measure/allocate vfuncs call Modula-2 callbacks; m2_layout_allocate_child
  helper positions children via a GskTransform
- GtkWidget: get_first_child/get_next_sibling/set_layout_manager
- test_shim3, examples/customlayout.mod

OpenGL (item 2):
- GtkGLArea, GdkGLContext, and a small GL subset (glClear/glClearColor/...)
- the build links -lGL when available
- test_gl, examples/gl.mod

Printing (item 3):
- GtkPrintOperation, GtkPrintSettings, GtkPageSetup, GtkPrintContext
- test_print

34 tests pass; 139 src modules, 24 examples.
Eric Streit 5 dagar sedan
förälder
incheckning
05645a0caf

+ 4 - 3
Makefile

@@ -5,8 +5,9 @@ GTKCFG   := $(shell pkg-config --cflags gtk4 2>/dev/null)
 GTKLIBS  := $(shell pkg-config --libs gtk4 2>/dev/null || echo -lgtk-4)
 GTKLIBS  := $(shell pkg-config --libs gtk4 2>/dev/null || echo -lgtk-4)
 # libadwaita is optional; added to the flags when present.
 # libadwaita is optional; added to the flags when present.
 ADWLIBS  := $(shell pkg-config --libs libadwaita-1 2>/dev/null)
 ADWLIBS  := $(shell pkg-config --libs libadwaita-1 2>/dev/null)
+GLLIBS   := $(shell pkg-config --libs gl 2>/dev/null)
 GTKCFG   += $(shell pkg-config --cflags libadwaita-1 2>/dev/null)
 GTKCFG   += $(shell pkg-config --cflags libadwaita-1 2>/dev/null)
-GTKLIBS  += $(ADWLIBS)
+GTKLIBS  += $(ADWLIBS) $(GLLIBS)
 GM2FLAGS ?= -fiso
 GM2FLAGS ?= -fiso
 GIR      ?= Gtk-4.0
 GIR      ?= Gtk-4.0
 GIR_DIR  ?= /usr/share/gir-1.0
 GIR_DIR  ?= /usr/share/gir-1.0
@@ -19,8 +20,8 @@ OBJS    := $(OBJDIR)/GtkUtils.o $(OBJDIR)/GtkClosures.o $(OBJDIR)/m2gtkshim.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_models test_cairo test_drawing test_gestures test_models2 test_widgets3 test_shim test_shim2 test_dnd test_media test_application
-EXAMPLES := hello counter inputs containers actions headerbar editor layouts files views builder feedback styling popover dialogs drawing demo gestures customwidget media clipboard
+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_shim2 test_shim3 test_dnd test_media test_gl test_print test_application
+EXAMPLES := hello counter inputs containers actions headerbar editor layouts files views builder feedback styling popover dialogs drawing demo gestures customwidget customlayout media clipboard gl
 
 
 # Include the libadwaita test/example only when libadwaita is installed.
 # Include the libadwaita test/example only when libadwaita is installed.
 ifneq ($(ADWLIBS),)
 ifneq ($(ADWLIBS),)

+ 8 - 4
README.md

@@ -14,14 +14,14 @@ Verified with `gm2` 16.0.1 (experimental), GTK 4.18.6 and libadwaita 1.7.6.
 
 
 ## Features
 ## Features
 
 
-- **132 hand-written binding modules** across GLib, GObject, Gio, GTK4
+- **139 hand-written binding modules** across GLib, GObject, Gio, GTK4
   and libadwaita — windows, widgets, inputs, containers, model-backed
   and libadwaita — windows, widgets, inputs, containers, model-backed
   views, actions/menus/accelerators, dialogs, CSS, drawing (Cairo/Pango),
   views, actions/menus/accelerators, dialogs, CSS, drawing (Cairo/Pango),
-  gestures, drag-and-drop, media, settings, and more.
+  gestures, drag-and-drop, media, OpenGL, printing, 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).
-- **31 tests** and **22 runnable examples**.
+- **34 tests** and **24 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.
@@ -122,6 +122,8 @@ gm2 -fiso -Isrc -Ilib myprog.mod \
 | `demo` | searchable demo browser (filter model + stack of pages) |
 | `demo` | searchable demo browser (filter model + stack of pages) |
 | `gestures` | click/drag gestures on a drawing area |
 | `gestures` | click/drag gestures on a drawing area |
 | `customwidget` | custom `GtkWidget` and `GdkPaintable` via the C shim |
 | `customwidget` | custom `GtkWidget` and `GdkPaintable` via the C shim |
+| `customlayout` | custom `GtkLayoutManager` via the C shim |
+| `gl` | OpenGL clearing inside a `GtkGLArea` |
 | `media` | `GdkTexture` in a `GtkPicture`, plus `GtkVideo` |
 | `media` | `GdkTexture` in a `GtkPicture`, plus `GtkVideo` |
 | `clipboard` | copy/paste text through `GdkClipboard` |
 | `clipboard` | copy/paste text through `GdkClipboard` |
 | `adw` | libadwaita preferences app (toolbar view, rows, toast, about) |
 | `adw` | libadwaita preferences app (toolbar view, rows, toast, about) |
@@ -155,6 +157,8 @@ gm2 -fiso -Isrc -Ilib myprog.mod \
 | `GtkDirectoryList`, `GtkBookmarkList` | directory / bookmark models |
 | `GtkDirectoryList`, `GtkBookmarkList` | directory / bookmark models |
 | `GtkDropTarget`, `GtkDragSource`, `GdkContentProvider`, `GdkClipboard` | drag-and-drop and clipboard |
 | `GtkDropTarget`, `GtkDragSource`, `GdkContentProvider`, `GdkClipboard` | drag-and-drop and clipboard |
 | `GdkTexture`, `GdkPixbuf`, `GtkVideo`, `GtkMediaFile`, `GtkMediaStream` | images and media |
 | `GdkTexture`, `GdkPixbuf`, `GtkVideo`, `GtkMediaFile`, `GtkMediaStream` | images and media |
+| `GtkGLArea`, `GdkGLContext`, `GL` | OpenGL rendering (`-lGL`) |
+| `GtkPrintOperation`, `GtkPrintSettings`, `GtkPageSetup`, `GtkPrintContext` | printing |
 | `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 |
 
 
@@ -196,7 +200,7 @@ gm2 -fiso -Isrc -Ilib myprog.mod \
 |---|---|
 |---|---|
 | `GtkUtils` | `CStrToM2`, `StrAppend`, `IntToStr`, `Connect`, `SetAccel`, `Unref`, `Free`, `ErrorMessage`, `ClearError` |
 | `GtkUtils` | `CStrToM2`, `StrAppend`, `IntToStr`, `Connect`, `SetAccel`, `Unref`, `Free`, `ErrorMessage`, `ClearError` |
 | `GtkClosures` | `Connect2`/`Connect3` (and owned variants) for per-connection signal state |
 | `GtkClosures` | `Connect2`/`Connect3` (and owned variants) for per-connection signal state |
-| `M2GtkShim` (`lib/m2gtkshim.c`) | C shim for GObject subclassing: a custom `GtkWidget`, a `GdkPaintable`, and a generic `GListModel`, all driven by Modula-2 callbacks |
+| `M2GtkShim` (`lib/m2gtkshim.c`) | C shim for GObject subclassing: a custom `GtkWidget`, a `GdkPaintable`, a generic `GListModel`, and a custom `GtkLayoutManager`, all driven by Modula-2 callbacks |
 
 
 ## Signals, ownership and errors
 ## Signals, ownership and errors
 
 

+ 83 - 0
examples/customlayout.mod

@@ -0,0 +1,83 @@
+MODULE customlayout ;
+
+(*
+   m2-GTK4 example - a custom layout manager from Modula-2.
+
+   The shim's M2LayoutManager positions the parent's children in a
+   single column using the Modula-2 measure/allocate callbacks.
+
+   Build:  make examples
+   Run:    ./build/examples/customlayout
+*)
+
+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 GtkWidget IMPORT gtk_widget_set_parent, gtk_widget_get_first_child,
+   gtk_widget_get_next_sibling, gtk_widget_set_layout_manager;
+FROM M2GtkShim IMPORT m2_gtk_widget_new, m2_layout_manager_new,
+   m2_layout_allocate_child;
+FROM GtkClosures IMPORT Connect2;
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM libc IMPORT printf;
+
+CONST
+   AppId = "org.example.m2gtk4.CustomLayout";
+   RowHeight = 36;
+
+PROCEDURE Measure (widget: ADDRESS; orientation: INTEGER; for_size: INTEGER;
+                   VAR minimum, natural: INTEGER; user: ADDRESS);
+BEGIN
+   minimum := 240;
+   natural := 240
+END Measure;
+
+PROCEDURE Allocate (widget: ADDRESS; width, height, baseline: INTEGER;
+                    user: ADDRESS);
+VAR
+   child: ADDRESS;
+   i: INTEGER;
+BEGIN
+   i := 0;
+   child := gtk_widget_get_first_child(widget);
+   WHILE child # NIL DO
+      m2_layout_allocate_child(child, 8, i * RowHeight, width - 16,
+                               RowHeight - 4);
+      INC(i);
+      child := gtk_widget_get_next_sibling(child)
+   END
+END Allocate;
+
+PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
+VAR
+   window, parent, manager: ADDRESS;
+BEGIN
+   parent := m2_gtk_widget_new(NIL, NIL, NIL);
+   manager := m2_layout_manager_new(ADR(Measure), ADR(Allocate), NIL, NIL);
+   gtk_widget_set_layout_manager(parent, manager);
+
+   gtk_widget_set_parent(gtk_label_new("row one"), parent);
+   gtk_widget_set_parent(gtk_label_new("row two"), parent);
+   gtk_widget_set_parent(gtk_label_new("row three"), parent);
+   gtk_widget_set_parent(gtk_label_new("row four"), parent);
+
+   window := gtk_application_window_new(application);
+   gtk_window_set_title(window, "m2-GTK4 Custom Layout");
+   gtk_window_set_default_size(window, 300, 220);
+   gtk_window_set_child(window, parent);
+   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 customlayout.

+ 63 - 0
examples/gl.mod

@@ -0,0 +1,63 @@
+MODULE gl ;
+
+(*
+   m2-GTK4 example - OpenGL in a GtkGLArea.
+
+   A GtkGLArea with auto-render; the "render" callback clears the
+   framebuffer to a colour.
+
+   Build:  make examples
+   Run:    ./build/examples/gl
+*)
+
+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 GtkGLArea IMPORT gtk_gl_area_new, gtk_gl_area_set_auto_render,
+   gtk_gl_area_set_has_depth_buffer;
+FROM GL IMPORT glClear, glClearColor, GLColorBufferBit;
+FROM GObject IMPORT g_signal_connect_data, GConnectDefault;
+FROM GtkClosures IMPORT Connect2;
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM libc IMPORT printf;
+
+CONST
+   AppId = "org.example.m2gtk4.Gl";
+
+(* gboolean render(GtkGLArea *area, GdkGLContext *context, gpointer user) *)
+PROCEDURE OnRender (area: ADDRESS; context: ADDRESS; user: ADDRESS) : INTEGER;
+BEGIN
+   glClearColor(0.16, 0.50, 0.72, 1.0);
+   glClear(GLColorBufferBit);
+   RETURN 1
+END OnRender;
+
+PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
+VAR
+   window, area: ADDRESS;
+BEGIN
+   area := gtk_gl_area_new();
+   gtk_gl_area_set_auto_render(area, 1);
+   gtk_gl_area_set_has_depth_buffer(area, 1);
+   g_signal_connect_data(area, "render", ADR(OnRender), NIL, NIL,
+                         GConnectDefault);
+
+   window := gtk_application_window_new(application);
+   gtk_window_set_title(window, "m2-GTK4 GL");
+   gtk_window_set_default_size(window, 400, 300);
+   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 gl.

+ 23 - 1
lib/M2GtkShim.def

@@ -30,8 +30,10 @@ EXPORT UNQUALIFIED
    M2Widget, M2WidgetDrawFunc,
    M2Widget, M2WidgetDrawFunc,
    M2Paintable, M2PaintableDrawFunc,
    M2Paintable, M2PaintableDrawFunc,
    M2ListModel, M2ListModelGetNItemsFunc, M2ListModelGetItemFunc,
    M2ListModel, M2ListModelGetNItemsFunc, M2ListModelGetItemFunc,
+   M2LayoutManager, M2LayoutMeasureFunc, M2LayoutAllocateFunc,
    m2_gtk_widget_new, m2_paintable_new,
    m2_gtk_widget_new, m2_paintable_new,
-   m2_list_model_new, m2_list_model_items_changed;
+   m2_list_model_new, m2_list_model_items_changed,
+   m2_layout_manager_new, m2_layout_allocate_child;
 
 
 
 
 TYPE
 TYPE
@@ -45,6 +47,13 @@ TYPE
    (* PROCEDURE (position: CARDINAL; user: ADDRESS) : ADDRESS
    (* PROCEDURE (position: CARDINAL; user: ADDRESS) : ADDRESS
       Must return a new reference (transfer full). *)
       Must return a new reference (transfer full). *)
    M2ListModelGetItemFunc = ADDRESS;
    M2ListModelGetItemFunc = ADDRESS;
+   M2LayoutManager = ADDRESS;
+   (* PROCEDURE (widget: ADDRESS; orientation: INTEGER; for_size: INTEGER;
+                   VAR minimum, natural: INTEGER; user: ADDRESS) *)
+   M2LayoutMeasureFunc = ADDRESS;
+   (* PROCEDURE (widget: ADDRESS; width, height, baseline: INTEGER;
+                   user: ADDRESS) *)
+   M2LayoutAllocateFunc = ADDRESS;
 
 
 (* GtkWidget *m2_gtk_widget_new(M2WidgetDrawFunc draw, gpointer user_data,
 (* GtkWidget *m2_gtk_widget_new(M2WidgetDrawFunc draw, gpointer user_data,
                                 GDestroyNotify destroy) *)
                                 GDestroyNotify destroy) *)
@@ -71,4 +80,17 @@ PROCEDURE m2_list_model_new (get_n_items: M2ListModelGetNItemsFunc;
 PROCEDURE m2_list_model_items_changed (model: M2ListModel;
 PROCEDURE m2_list_model_items_changed (model: M2ListModel;
                                        position, removed, added: CARDINAL);
                                        position, removed, added: CARDINAL);
 
 
+(* GtkLayoutManager *m2_layout_manager_new(M2LayoutMeasureFunc measure,
+                                            M2LayoutAllocateFunc allocate,
+                                            gpointer user_data,
+                                            GDestroyNotify destroy) *)
+PROCEDURE m2_layout_manager_new (measure: M2LayoutMeasureFunc;
+                                 allocate: M2LayoutAllocateFunc;
+                                 user_data: ADDRESS;
+                                 destroy: ADDRESS) : M2LayoutManager;
+
+(* void m2_layout_allocate_child(GtkWidget *child, int x, int y,
+                                  int width, int height) *)
+PROCEDURE m2_layout_allocate_child (child: ADDRESS; x, y, width, height: INTEGER);
+
 END M2GtkShim.
 END M2GtkShim.

+ 111 - 0
lib/m2gtkshim.c

@@ -325,3 +325,114 @@ m2_list_model_items_changed (GListModel *model, guint position,
 {
 {
   g_list_model_items_changed (model, position, removed, added);
   g_list_model_items_changed (model, position, removed, added);
 }
 }
+
+/* ------------------------------------------------------------------ */
+/* M2LayoutManager (subclass of GtkLayoutManager)                      */
+/* ------------------------------------------------------------------ */
+
+typedef void (*M2LayoutMeasureFunc) (GtkWidget *widget, int orientation,
+                                     int for_size, int *minimum, int *natural,
+                                     gpointer user_data);
+typedef void (*M2LayoutAllocateFunc) (GtkWidget *widget, int width, int height,
+                                      int baseline, gpointer user_data);
+
+typedef struct _M2LayoutManager M2LayoutManager;
+typedef struct _M2LayoutManagerClass M2LayoutManagerClass;
+
+struct _M2LayoutManager
+{
+  GtkLayoutManager parent_instance;
+  M2LayoutMeasureFunc measure;
+  M2LayoutAllocateFunc allocate;
+  gpointer user_data;
+  GDestroyNotify destroy;
+};
+
+struct _M2LayoutManagerClass
+{
+  GtkLayoutManagerClass parent_class;
+};
+
+static void
+m2_layout_manager_measure (GtkLayoutManager *manager, GtkWidget *widget,
+                           GtkOrientation orientation, int for_size,
+                           int *minimum, int *natural, int *minimum_baseline,
+                           int *natural_baseline)
+{
+  M2LayoutManager *self = (M2LayoutManager *) manager;
+
+  *minimum = 0;
+  *natural = 0;
+  if (self->measure != NULL)
+    self->measure (widget, (int) orientation, for_size, minimum, natural,
+                   self->user_data);
+  *minimum_baseline = -1;
+  *natural_baseline = -1;
+}
+
+static void
+m2_layout_manager_allocate (GtkLayoutManager *manager, GtkWidget *widget,
+                            int width, int height, int baseline)
+{
+  M2LayoutManager *self = (M2LayoutManager *) manager;
+
+  if (self->allocate != NULL)
+    self->allocate (widget, width, height, baseline, self->user_data);
+}
+
+G_DEFINE_TYPE (M2LayoutManager, m2_layout_manager, GTK_TYPE_LAYOUT_MANAGER)
+
+static void
+m2_layout_manager_finalize (GObject *object)
+{
+  M2LayoutManager *self = (M2LayoutManager *) object;
+
+  if (self->destroy != NULL)
+    self->destroy (self->user_data);
+
+  G_OBJECT_CLASS (m2_layout_manager_parent_class)->finalize (object);
+}
+
+static void
+m2_layout_manager_class_init (M2LayoutManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkLayoutManagerClass *manager_class = GTK_LAYOUT_MANAGER_CLASS (klass);
+
+  object_class->finalize = m2_layout_manager_finalize;
+  manager_class->measure = m2_layout_manager_measure;
+  manager_class->allocate = m2_layout_manager_allocate;
+}
+
+static void
+m2_layout_manager_init (M2LayoutManager *self)
+{
+  self->measure = NULL;
+  self->allocate = NULL;
+  self->user_data = NULL;
+  self->destroy = NULL;
+}
+
+GtkLayoutManager *
+m2_layout_manager_new (M2LayoutMeasureFunc measure, M2LayoutAllocateFunc allocate,
+                       gpointer user_data, GDestroyNotify destroy)
+{
+  M2LayoutManager *self = g_object_new (m2_layout_manager_get_type (), NULL);
+
+  self->measure = measure;
+  self->allocate = allocate;
+  self->user_data = user_data;
+  self->destroy = destroy;
+  return GTK_LAYOUT_MANAGER (self);
+}
+
+/* Position a child at (x, y) with the given size (helper for allocate). */
+void
+m2_layout_allocate_child (GtkWidget *child, int x, int y, int width, int height)
+{
+  graphene_point_t point = GRAPHENE_POINT_INIT (x, y);
+  GskTransform *transform = gsk_transform_translate (NULL, &point);
+
+  gtk_widget_allocate (child, width, height, -1, transform);
+  gsk_transform_unref (transform);
+}

+ 50 - 0
src/GL.def

@@ -0,0 +1,50 @@
+DEFINITION MODULE FOR "C" GL ;
+
+(*
+   m2-GTK4 - a tiny OpenGL subset for use inside a GtkGLArea render
+   callback.  Mapped onto <GL/gl.h>.
+
+   Enough for a clear-and-draw demo; link with -lGL (added automatically
+   by the build when pkg-config knows "gl").
+*)
+
+EXPORT UNQUALIFIED
+   GLColorBufferBit, GLDepthBufferBit, GLStencilBufferBit,
+   GLDepthTest, GLBlend,
+   GLSrcAlpha, GLOneMinusSrcAlpha, GLTriangles,
+   glClear, glClearColor, glViewport,
+   glEnable, glDisable, glBlendFunc, glGetError;
+
+
+CONST
+   GLColorBufferBit   = 000004000H;
+   GLDepthBufferBit   = 000000100H;
+   GLStencilBufferBit = 000000400H;
+   GLDepthTest        = 000000B71H;
+   GLBlend            = 000000BE2H;
+   GLSrcAlpha         = 000000302H;
+   GLOneMinusSrcAlpha = 000000303H;
+   GLTriangles        = 000000004H;
+
+(* void glClear(GLbitfield mask) *)
+PROCEDURE glClear (mask: CARDINAL);
+
+(* void glClearColor(GLfloat red, green, blue, alpha) *)
+PROCEDURE glClearColor (red, green, blue, alpha: SHORTREAL);
+
+(* void glViewport(GLint x, GLint y, GLsizei width, GLsizei height) *)
+PROCEDURE glViewport (x, y, width, height: INTEGER);
+
+(* void glEnable(GLenum cap) *)
+PROCEDURE glEnable (cap: CARDINAL);
+
+(* void glDisable(GLenum cap) *)
+PROCEDURE glDisable (cap: CARDINAL);
+
+(* void glBlendFunc(GLenum sfactor, GLenum dfactor) *)
+PROCEDURE glBlendFunc (sfactor, dfactor: CARDINAL);
+
+(* GLenum glGetError(void) *)
+PROCEDURE glGetError () : CARDINAL;
+
+END GL.

+ 40 - 0
src/GdkGLContext.def

@@ -0,0 +1,40 @@
+DEFINITION MODULE FOR "C" GdkGLContext ;
+
+(*
+   m2-GTK4 - GdkGLContext.  Mapped onto <gdk/gdkglcontext.h>.
+
+   Obtained from a GtkGLArea with gtk_gl_area_get_context(); call
+   make_current() before issuing raw OpenGL commands.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM GError IMPORT GError;
+
+EXPORT UNQUALIFIED
+   GdkGLContext,
+   gdk_gl_context_make_current, gdk_gl_context_clear_current,
+   gdk_gl_context_realize, gdk_gl_context_get_display,
+   gdk_gl_context_get_version;
+
+
+TYPE
+   GdkGLContext = ADDRESS;
+
+(* void gdk_gl_context_make_current(GdkGLContext *context) *)
+PROCEDURE gdk_gl_context_make_current (context: GdkGLContext);
+
+(* void gdk_gl_context_clear_current(void) *)
+PROCEDURE gdk_gl_context_clear_current ();
+
+(* gboolean gdk_gl_context_realize(GdkGLContext *context, GError **error) *)
+PROCEDURE gdk_gl_context_realize (context: GdkGLContext;
+                                  VAR error: GError) : [ INTEGER ];
+
+(* GdkDisplay *gdk_gl_context_get_display(GdkGLContext *context) *)
+PROCEDURE gdk_gl_context_get_display (context: GdkGLContext) : ADDRESS;
+
+(* void gdk_gl_context_get_version(GdkGLContext *context, int *major, int *minor) *)
+PROCEDURE gdk_gl_context_get_version (context: GdkGLContext;
+                                      VAR major, minor: INTEGER);
+
+END GdkGLContext.

+ 82 - 0
src/GtkGLArea.def

@@ -0,0 +1,82 @@
+DEFINITION MODULE FOR "C" GtkGLArea ;
+
+(*
+   m2-GTK4 - GtkGLArea: a widget with an OpenGL context.  Mapped onto
+   <gtk/gtkglarea.h>.
+
+   Set auto_render and connect the "render" signal
+   (gboolean (GtkGLArea*, GdkGLContext*, gpointer) - return TRUE when you
+   have drawn).  Call GdkGLContext.gdk_gl_context_make_current() inside
+   the callback if you need to issue GL commands.
+
+   GtkGLArea derives from GtkWidget, so add it to a layout normally.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkGLArea,
+   gtk_gl_area_new,
+   gtk_gl_area_set_auto_render, gtk_gl_area_get_auto_render,
+   gtk_gl_area_set_has_depth_buffer, gtk_gl_area_get_has_depth_buffer,
+   gtk_gl_area_set_has_stencil_buffer,
+   gtk_gl_area_set_use_es, gtk_gl_area_get_use_es,
+   gtk_gl_area_set_required_version, gtk_gl_area_get_required_version,
+   gtk_gl_area_get_error,
+   gtk_gl_area_queue_render, gtk_gl_area_make_current,
+   gtk_gl_area_attach_buffers, gtk_gl_area_get_context;
+
+
+TYPE
+   GtkGLArea = ADDRESS;
+
+(* GtkWidget *gtk_gl_area_new(void) *)
+PROCEDURE gtk_gl_area_new () : GtkGLArea;
+
+(* void gtk_gl_area_set_auto_render(GtkGLArea *area, gboolean auto_render) *)
+PROCEDURE gtk_gl_area_set_auto_render (area: GtkGLArea; auto_render: INTEGER);
+
+(* gboolean gtk_gl_area_get_auto_render(GtkGLArea *area) *)
+PROCEDURE gtk_gl_area_get_auto_render (area: GtkGLArea) : [ INTEGER ];
+
+(* void gtk_gl_area_set_has_depth_buffer(GtkGLArea *area, gboolean has_depth_buffer) *)
+PROCEDURE gtk_gl_area_set_has_depth_buffer (area: GtkGLArea;
+                                            has_depth_buffer: INTEGER);
+
+(* gboolean gtk_gl_area_get_has_depth_buffer(GtkGLArea *area) *)
+PROCEDURE gtk_gl_area_get_has_depth_buffer (area: GtkGLArea) : [ INTEGER ];
+
+(* void gtk_gl_area_set_has_stencil_buffer(GtkGLArea *area, gboolean has_stencil_buffer) *)
+PROCEDURE gtk_gl_area_set_has_stencil_buffer (area: GtkGLArea;
+                                              has_stencil_buffer: INTEGER);
+
+(* void gtk_gl_area_set_use_es(GtkGLArea *area, gboolean use_es) *)
+PROCEDURE gtk_gl_area_set_use_es (area: GtkGLArea; use_es: INTEGER);
+
+(* gboolean gtk_gl_area_get_use_es(GtkGLArea *area) *)
+PROCEDURE gtk_gl_area_get_use_es (area: GtkGLArea) : [ INTEGER ];
+
+(* void gtk_gl_area_set_required_version(GtkGLArea *area, int major, int minor) *)
+PROCEDURE gtk_gl_area_set_required_version (area: GtkGLArea;
+                                            major, minor: INTEGER);
+
+(* void gtk_gl_area_get_required_version(GtkGLArea *area, int *major, int *minor) *)
+PROCEDURE gtk_gl_area_get_required_version (area: GtkGLArea;
+                                            VAR major, minor: INTEGER);
+
+(* GError *gtk_gl_area_get_error(GtkGLArea *area) *)
+PROCEDURE gtk_gl_area_get_error (area: GtkGLArea) : ADDRESS;
+
+(* void gtk_gl_area_queue_render(GtkGLArea *area) *)
+PROCEDURE gtk_gl_area_queue_render (area: GtkGLArea);
+
+(* void gtk_gl_area_make_current(GtkGLArea *area) *)
+PROCEDURE gtk_gl_area_make_current (area: GtkGLArea);
+
+(* void gtk_gl_area_attach_buffers(GtkGLArea *area) *)
+PROCEDURE gtk_gl_area_attach_buffers (area: GtkGLArea);
+
+(* GdkGLContext *gtk_gl_area_get_context(GtkGLArea *area) *)
+PROCEDURE gtk_gl_area_get_context (area: GtkGLArea) : ADDRESS;
+
+END GtkGLArea.

+ 55 - 0
src/GtkPageSetup.def

@@ -0,0 +1,55 @@
+DEFINITION MODULE FOR "C" GtkPageSetup ;
+
+(*
+   m2-GTK4 - GtkPageSetup: paper size and orientation.  Mapped onto
+   <gtk/gtkpagesetup.h>.
+
+   Units: 1 = points, 2 = inches, 3 = millimetres (GtkUnit).
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkPageSetup, GtkPageOrientation, GtkUnit,
+   GtkPagePortrait, GtkPageLandscape,
+   GtkPageReversePortrait, GtkPageReverseLandscape,
+   GtkUnitNone, GtkUnitPoints, GtkUnitInch, GtkUnitMm,
+   gtk_page_setup_new,
+   gtk_page_setup_get_orientation, gtk_page_setup_set_orientation,
+   gtk_page_setup_get_page_width, gtk_page_setup_get_page_height;
+
+
+TYPE
+   GtkPageSetup = ADDRESS;
+   GtkPageOrientation = CARDINAL;
+   GtkUnit = CARDINAL;
+
+CONST
+   GtkPagePortrait         = 0;
+   GtkPageLandscape        = 1;
+   GtkPageReversePortrait  = 2;
+   GtkPageReverseLandscape = 3;
+
+   GtkUnitNone   = 0;
+   GtkUnitPoints = 1;
+   GtkUnitInch   = 2;
+   GtkUnitMm     = 3;
+
+(* GtkPageSetup *gtk_page_setup_new(void) *)
+PROCEDURE gtk_page_setup_new () : GtkPageSetup;
+
+(* GtkPageOrientation gtk_page_setup_get_orientation(GtkPageSetup *setup) *)
+PROCEDURE gtk_page_setup_get_orientation (setup: GtkPageSetup) : GtkPageOrientation;
+
+(* void gtk_page_setup_set_orientation(GtkPageSetup *setup,
+                                       GtkPageOrientation orientation) *)
+PROCEDURE gtk_page_setup_set_orientation (setup: GtkPageSetup;
+                                          orientation: GtkPageOrientation);
+
+(* double gtk_page_setup_get_page_width(GtkPageSetup *setup, GtkUnit unit) *)
+PROCEDURE gtk_page_setup_get_page_width (setup: GtkPageSetup; unit: GtkUnit) : REAL;
+
+(* double gtk_page_setup_get_page_height(GtkPageSetup *setup, GtkUnit unit) *)
+PROCEDURE gtk_page_setup_get_page_height (setup: GtkPageSetup; unit: GtkUnit) : REAL;
+
+END GtkPageSetup.

+ 32 - 0
src/GtkPrintContext.def

@@ -0,0 +1,32 @@
+DEFINITION MODULE FOR "C" GtkPrintContext ;
+
+(*
+   m2-GTK4 - GtkPrintContext: the drawing surface for one printed page.
+   Mapped onto <gtk/gtkprintcontext.h>.
+
+   Passed to the "draw-page" signal of GtkPrintOperation.  Get the Cairo
+   context with gtk_print_context_get_cairo_context() and draw on it;
+   dimensions are in points.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkPrintContext,
+   gtk_print_context_get_cairo_context,
+   gtk_print_context_get_width, gtk_print_context_get_height;
+
+
+TYPE
+   GtkPrintContext = ADDRESS;
+
+(* cairo_t *gtk_print_context_get_cairo_context(GtkPrintContext *context) *)
+PROCEDURE gtk_print_context_get_cairo_context (context: GtkPrintContext) : ADDRESS;
+
+(* double gtk_print_context_get_width(GtkPrintContext *context) *)
+PROCEDURE gtk_print_context_get_width (context: GtkPrintContext) : REAL;
+
+(* double gtk_print_context_get_height(GtkPrintContext *context) *)
+PROCEDURE gtk_print_context_get_height (context: GtkPrintContext) : REAL;
+
+END GtkPrintContext.

+ 94 - 0
src/GtkPrintOperation.def

@@ -0,0 +1,94 @@
+DEFINITION MODULE FOR "C" GtkPrintOperation ;
+
+(*
+   m2-GTK4 - GtkPrintOperation: the print/print-preview machinery.
+   Mapped onto <gtk/gtkprintoperation.h>.
+
+   Create one, set n_pages and the page setup/settings, connect
+   "draw-page" (void (GtkPrintOperation*, GtkPrintContext*, int page_nr,
+   gpointer)), then run it:
+
+      result := gtk_print_operation_run(op, PrintDialog, parent, err);
+
+   In "draw-page", draw with the context's Cairo context (GtkPrintContext)
+   and finish with gtk_print_operation_draw_page_finish().
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM GError IMPORT GError;
+
+EXPORT UNQUALIFIED
+   GtkPrintOperation, GtkPrintOperationAction, GtkPrintOperationResult,
+   PrintDialog, PrintActionPrint, PrintActionPreview, PrintActionExport,
+   PrintResultError, PrintResultApply, PrintResultCancel, PrintResultInProgress,
+   gtk_print_operation_new,
+   gtk_print_operation_set_n_pages, gtk_print_operation_set_current_page,
+   gtk_print_operation_set_unit,
+   gtk_print_operation_set_default_page_setup,
+   gtk_print_operation_get_default_page_setup,
+   gtk_print_operation_set_print_settings, gtk_print_operation_get_print_settings,
+   gtk_print_operation_run, gtk_print_operation_draw_page_finish,
+   gtk_print_operation_get_error;
+
+
+TYPE
+   GtkPrintOperation = ADDRESS;
+   GtkPrintOperationAction = CARDINAL;
+   GtkPrintOperationResult = INTEGER;
+
+CONST
+   PrintDialog    = 0;   (* GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG *)
+   PrintActionPrint   = 1;
+   PrintActionPreview = 2;
+   PrintActionExport  = 3;
+
+   PrintResultError      = -1;   (* GTK_PRINT_OPERATION_RESULT_ERROR *)
+   PrintResultApply      = 0;
+   PrintResultCancel     = 1;
+   PrintResultInProgress = 2;
+
+(* GtkPrintOperation *gtk_print_operation_new(void) *)
+PROCEDURE gtk_print_operation_new () : GtkPrintOperation;
+
+(* void gtk_print_operation_set_n_pages(GtkPrintOperation *op, int n_pages) *)
+PROCEDURE gtk_print_operation_set_n_pages (op: GtkPrintOperation; n_pages: INTEGER);
+
+(* void gtk_print_operation_set_current_page(GtkPrintOperation *op, int current_page) *)
+PROCEDURE gtk_print_operation_set_current_page (op: GtkPrintOperation;
+                                                current_page: INTEGER);
+
+(* void gtk_print_operation_set_unit(GtkPrintOperation *op, GtkUnit unit) *)
+PROCEDURE gtk_print_operation_set_unit (op: GtkPrintOperation; unit: INTEGER);
+
+(* void gtk_print_operation_set_default_page_setup(GtkPrintOperation *op,
+                                                   GtkPageSetup *default_page_setup) *)
+PROCEDURE gtk_print_operation_set_default_page_setup (op: GtkPrintOperation;
+                                                      default_page_setup: ADDRESS);
+
+(* GtkPageSetup *gtk_print_operation_get_default_page_setup(GtkPrintOperation *op) *)
+PROCEDURE gtk_print_operation_get_default_page_setup (op: GtkPrintOperation) : ADDRESS;
+
+(* void gtk_print_operation_set_print_settings(GtkPrintOperation *op,
+                                               GtkPrintSettings *print_settings) *)
+PROCEDURE gtk_print_operation_set_print_settings (op: GtkPrintOperation;
+                                                  print_settings: ADDRESS);
+
+(* GtkPrintSettings *gtk_print_operation_get_print_settings(GtkPrintOperation *op) *)
+PROCEDURE gtk_print_operation_get_print_settings (op: GtkPrintOperation) : ADDRESS;
+
+(* GtkPrintOperationResult gtk_print_operation_run(GtkPrintOperation *op,
+                                                    GtkPrintOperationAction action,
+                                                    GtkWindow *parent,
+                                                    GError **error) *)
+PROCEDURE gtk_print_operation_run (op: GtkPrintOperation;
+                                   action: GtkPrintOperationAction;
+                                   parent: ADDRESS;
+                                   VAR error: GError) : GtkPrintOperationResult;
+
+(* void gtk_print_operation_draw_page_finish(GtkPrintOperation *op) *)
+PROCEDURE gtk_print_operation_draw_page_finish (op: GtkPrintOperation);
+
+(* GError *gtk_print_operation_get_error(GtkPrintOperation *op) *)
+PROCEDURE gtk_print_operation_get_error (op: GtkPrintOperation) : ADDRESS;
+
+END GtkPrintOperation.

+ 40 - 0
src/GtkPrintSettings.def

@@ -0,0 +1,40 @@
+DEFINITION MODULE FOR "C" GtkPrintSettings ;
+
+(*
+   m2-GTK4 - GtkPrintSettings: printer options key/value store.  Mapped
+   onto <gtk/gtkprintsettings.h>.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   GtkPrintSettings,
+   gtk_print_settings_new,
+   gtk_print_settings_set, gtk_print_settings_get,
+   gtk_print_settings_set_printer, gtk_print_settings_get_printer;
+
+
+TYPE
+   GtkPrintSettings = ADDRESS;
+
+(* GtkPrintSettings *gtk_print_settings_new(void) *)
+PROCEDURE gtk_print_settings_new () : GtkPrintSettings;
+
+(* void gtk_print_settings_set(GtkPrintSettings *settings, const char *key,
+                               const char *value) *)
+PROCEDURE gtk_print_settings_set (settings: GtkPrintSettings;
+                                  key, value: ARRAY OF CHAR);
+
+(* const char *gtk_print_settings_get(GtkPrintSettings *settings, const char *key) *)
+PROCEDURE gtk_print_settings_get (settings: GtkPrintSettings;
+                                  key: ARRAY OF CHAR) : ADDRESS;
+
+(* void gtk_print_settings_set_printer(GtkPrintSettings *settings,
+                                       const char *printer) *)
+PROCEDURE gtk_print_settings_set_printer (settings: GtkPrintSettings;
+                                          printer: ARRAY OF CHAR);
+
+(* const char *gtk_print_settings_get_printer(GtkPrintSettings *settings) *)
+PROCEDURE gtk_print_settings_get_printer (settings: GtkPrintSettings) : ADDRESS;
+
+END GtkPrintSettings.

+ 11 - 0
src/GtkWidget.def

@@ -28,6 +28,8 @@ EXPORT UNQUALIFIED
    gtk_widget_add_css_class, gtk_widget_remove_css_class,
    gtk_widget_add_css_class, gtk_widget_remove_css_class,
    gtk_widget_has_css_class,
    gtk_widget_has_css_class,
    gtk_widget_set_parent, gtk_widget_get_parent, gtk_widget_unparent,
    gtk_widget_set_parent, gtk_widget_get_parent, gtk_widget_unparent,
+   gtk_widget_get_first_child, gtk_widget_get_next_sibling,
+   gtk_widget_set_layout_manager,
    gtk_widget_add_controller, gtk_widget_remove_controller;
    gtk_widget_add_controller, gtk_widget_remove_controller;
 
 
 
 
@@ -103,6 +105,15 @@ PROCEDURE gtk_widget_get_parent (widget: GtkWidget) : ADDRESS;
 (* void gtk_widget_unparent(GtkWidget *widget) *)
 (* void gtk_widget_unparent(GtkWidget *widget) *)
 PROCEDURE gtk_widget_unparent (widget: GtkWidget);
 PROCEDURE gtk_widget_unparent (widget: GtkWidget);
 
 
+(* GtkWidget *gtk_widget_get_first_child(GtkWidget *widget) *)
+PROCEDURE gtk_widget_get_first_child (widget: GtkWidget) : ADDRESS;
+
+(* GtkWidget *gtk_widget_get_next_sibling(GtkWidget *widget) *)
+PROCEDURE gtk_widget_get_next_sibling (widget: GtkWidget) : ADDRESS;
+
+(* void gtk_widget_set_layout_manager(GtkWidget *widget, GtkLayoutManager *layout_manager) *)
+PROCEDURE gtk_widget_set_layout_manager (widget: GtkWidget; layout_manager: ADDRESS);
+
 (* void gtk_widget_add_controller(GtkWidget *widget, GtkEventController *controller) *)
 (* void gtk_widget_add_controller(GtkWidget *widget, GtkEventController *controller) *)
 PROCEDURE gtk_widget_add_controller (widget: GtkWidget; controller: ADDRESS);
 PROCEDURE gtk_widget_add_controller (widget: GtkWidget; controller: ADDRESS);
 
 

+ 7 - 2
tests/run_tests.sh

@@ -13,13 +13,18 @@ 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_models test_cairo test_drawing test_gestures test_models2 test_widgets3 test_shim test_shim2 test_dnd test_media 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_shim2 test_shim3 test_dnd test_media test_gl test_print 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)"
   TESTS="$TESTS test_adw"
   TESTS="$TESTS test_adw"
 fi
 fi
 
 
+# OpenGL (GtkGLArea examples/tests).
+if pkg-config --exists gl 2>/dev/null; then
+  LIBS="$LIBS $(pkg-config --libs gl)"
+fi
+
 mkdir -p "$BIN" "$OBJ"
 mkdir -p "$BIN" "$OBJ"
 
 
 # pure-Modula-2 helper must be compiled with -c first (no main), then
 # pure-Modula-2 helper must be compiled with -c first (no main), then
@@ -48,7 +53,7 @@ for t in $TESTS; do
        "$ROOT/tests/$t.mod" "$OBJ/GtkUtils.o" "$OBJ/GtkClosures.o" \
        "$ROOT/tests/$t.mod" "$OBJ/GtkUtils.o" "$OBJ/GtkClosures.o" \
        "$OBJ/m2gtkshim.o" -o "$BIN/$t" $LIBS
        "$OBJ/m2gtkshim.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_models|test_drawing|test_gestures|test_models2|test_widgets3|test_shim|test_shim2|test_dnd|test_media|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_shim2|test_shim3|test_dnd|test_media|test_gl|test_print|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

+ 89 - 0
tests/test_gl.mod

@@ -0,0 +1,89 @@
+MODULE test_gl ;
+
+(*
+   m2-GTK4 test - GtkGLArea property round-trips.  Requires a display
+   (and a GL-capable renderer to actually draw, which this does not do).
+*)
+
+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 GtkGLArea IMPORT gtk_gl_area_new, gtk_gl_area_set_auto_render,
+   gtk_gl_area_get_auto_render, gtk_gl_area_set_has_depth_buffer,
+   gtk_gl_area_get_has_depth_buffer, gtk_gl_area_set_has_stencil_buffer,
+   gtk_gl_area_set_use_es, gtk_gl_area_get_use_es,
+   gtk_gl_area_set_required_version, gtk_gl_area_get_required_version;
+FROM GtkUtils IMPORT Connect;
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM libc IMPORT printf;
+
+CONST
+   AppId = "org.example.m2gtk4.TestGl";
+
+VAR
+   app: ADDRESS;
+   passed: BOOLEAN;
+
+PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
+VAR
+   window, area: ADDRESS;
+   major, minor: INTEGER;
+   ok: BOOLEAN;
+BEGIN
+   ok := TRUE;
+
+   area := gtk_gl_area_new();
+   IF area = NIL THEN printf("gl area NIL\n"); ok := FALSE END;
+
+   gtk_gl_area_set_auto_render(area, 1);
+   IF gtk_gl_area_get_auto_render(area) # 1 THEN
+      printf("auto_render wrong\n"); ok := FALSE
+   END;
+
+   gtk_gl_area_set_has_depth_buffer(area, 1);
+   IF gtk_gl_area_get_has_depth_buffer(area) # 1 THEN
+      printf("has_depth_buffer wrong\n"); ok := FALSE
+   END;
+
+   gtk_gl_area_set_has_stencil_buffer(area, 1);
+
+   gtk_gl_area_set_use_es(area, 0);
+   IF gtk_gl_area_get_use_es(area) # 0 THEN
+      printf("use_es wrong\n"); ok := FALSE
+   END;
+
+   gtk_gl_area_set_required_version(area, 3, 3);
+   gtk_gl_area_get_required_version(area, major, minor);
+   IF (major # 3) OR (minor # 3) THEN
+      printf("required version wrong: %d.%d\n", major, minor); ok := FALSE
+   END;
+
+   window := gtk_application_window_new(application);
+   gtk_window_set_child(window, area);
+   gtk_window_present(window);
+
+   passed := ok;
+   g_application_quit(application)
+END OnActivate;
+
+VAR
+   rc: INTEGER;
+BEGIN
+   passed := FALSE;
+   app := gtk_application_new(AppId, 0);
+   IF app = NIL THEN
+      printf("test_gl: 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_gl: PASS\n");
+      HALT(0)
+   ELSE
+      printf("test_gl: FAIL\n");
+      HALT(1)
+   END
+END test_gl.

+ 95 - 0
tests/test_print.mod

@@ -0,0 +1,95 @@
+MODULE test_print ;
+
+(*
+   m2-GTK4 test - printing objects: GtkPageSetup, GtkPrintSettings and
+   GtkPrintOperation property round-trips (the print dialog is not
+   shown).  Requires a display (GTK initialised).
+*)
+
+FROM Gio IMPORT g_application_run, g_application_quit;
+FROM GtkApplication IMPORT gtk_application_new;
+FROM GtkPageSetup IMPORT gtk_page_setup_new, gtk_page_setup_set_orientation,
+   gtk_page_setup_get_orientation, gtk_page_setup_get_page_width,
+   GtkPageLandscape, GtkUnitPoints;
+FROM GtkPrintSettings IMPORT gtk_print_settings_new, gtk_print_settings_set,
+   gtk_print_settings_get, gtk_print_settings_set_printer;
+FROM GtkPrintOperation IMPORT gtk_print_operation_new,
+   gtk_print_operation_set_n_pages,
+   gtk_print_operation_set_default_page_setup,
+   gtk_print_operation_get_default_page_setup,
+   gtk_print_operation_set_print_settings,
+   gtk_print_operation_get_print_settings;
+FROM GObject IMPORT g_object_unref;
+FROM GtkUtils IMPORT Connect, CStrToM2;
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM libc IMPORT printf;
+
+CONST
+   AppId = "org.example.m2gtk4.TestPrint";
+
+VAR
+   app: ADDRESS;
+   passed: BOOLEAN;
+
+PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
+VAR
+   setup, settings, op: ADDRESS;
+   buf: ARRAY [0..63] OF CHAR;
+   width: REAL;
+   ok: BOOLEAN;
+BEGIN
+   ok := TRUE;
+
+   setup := gtk_page_setup_new();
+   gtk_page_setup_set_orientation(setup, GtkPageLandscape);
+   IF gtk_page_setup_get_orientation(setup) # GtkPageLandscape THEN
+      printf("orientation wrong\n"); ok := FALSE
+   END;
+   width := gtk_page_setup_get_page_width(setup, GtkUnitPoints);
+   IF width <= 0.0 THEN printf("page width wrong\n"); ok := FALSE END;
+
+   settings := gtk_print_settings_new();
+   gtk_print_settings_set(settings, "test-key", "test-value");
+   CStrToM2(gtk_print_settings_get(settings, "test-key"), buf);
+   IF (buf[0] # 't') OR (buf[10] # 0C) THEN
+      printf("settings value wrong: [%s]\n", buf); ok := FALSE
+   END;
+   gtk_print_settings_set_printer(settings, "MyPrinter");
+
+   op := gtk_print_operation_new();
+   gtk_print_operation_set_n_pages(op, 3);
+   gtk_print_operation_set_default_page_setup(op, setup);
+   IF gtk_print_operation_get_default_page_setup(op) # setup THEN
+      printf("page setup round-trip wrong\n"); ok := FALSE
+   END;
+   gtk_print_operation_set_print_settings(op, settings);
+   IF gtk_print_operation_get_print_settings(op) # settings THEN
+      printf("settings round-trip wrong\n"); ok := FALSE
+   END;
+
+   g_object_unref(op);
+
+   passed := ok;
+   g_application_quit(application)
+END OnActivate;
+
+VAR
+   rc: INTEGER;
+BEGIN
+   passed := FALSE;
+   app := gtk_application_new(AppId, 0);
+   IF app = NIL THEN
+      printf("test_print: 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_print: PASS\n");
+      HALT(0)
+   ELSE
+      printf("test_print: FAIL\n");
+      HALT(1)
+   END
+END test_print.

+ 115 - 0
tests/test_shim3.mod

@@ -0,0 +1,115 @@
+MODULE test_shim3 ;
+
+(*
+   m2-GTK4 test - the shim's custom GtkLayoutManager.
+
+   Installs a Modula-2 layout manager on a parent widget, adds two
+   children, and checks (from a timeout) that the allocate callback ran
+   and positioned them.  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 GtkLabel IMPORT gtk_label_new;
+FROM GtkWidget IMPORT gtk_widget_set_parent, gtk_widget_get_first_child,
+   gtk_widget_get_next_sibling, gtk_widget_set_layout_manager,
+   gtk_widget_get_width;
+FROM M2GtkShim IMPORT m2_gtk_widget_new, m2_layout_manager_new,
+   m2_layout_allocate_child;
+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.TestShim3";
+
+VAR
+   app, child1: ADDRESS;
+   passed, allocated: BOOLEAN;
+
+(* PROCEDURE (widget, orientation, for_size; VAR minimum, natural; user) *)
+PROCEDURE Measure (widget: ADDRESS; orientation: INTEGER; for_size: INTEGER;
+                   VAR minimum, natural: INTEGER; user: ADDRESS);
+BEGIN
+   minimum := 200;
+   natural := 200
+END Measure;
+
+(* PROCEDURE (widget, width, height, baseline, user) *)
+PROCEDURE Allocate (widget: ADDRESS; width, height, baseline: INTEGER;
+                    user: ADDRESS);
+VAR
+   child: ADDRESS;
+   i: INTEGER;
+BEGIN
+   allocated := TRUE;
+   i := 0;
+   child := gtk_widget_get_first_child(widget);
+   WHILE child # NIL DO
+      m2_layout_allocate_child(child, 0, i * 30, width, 30);
+      INC(i);
+      child := gtk_widget_get_next_sibling(child)
+   END
+END Allocate;
+
+PROCEDURE AfterMap (data: ADDRESS) : INTEGER;
+BEGIN
+   IF NOT allocated THEN
+      printf("allocate callback did not run\n");
+      passed := FALSE
+   END;
+   IF (child1 # NIL) AND (gtk_widget_get_width(child1) <= 0) THEN
+      printf("child was not allocated\n");
+      passed := FALSE
+   END;
+   g_application_quit(app);
+   RETURN GSourceRemove
+END AfterMap;
+
+PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
+VAR
+   window, parent, manager: ADDRESS;
+   ok: BOOLEAN;
+BEGIN
+   ok := TRUE;
+   allocated := FALSE;
+
+   parent := m2_gtk_widget_new(NIL, NIL, NIL);
+   manager := m2_layout_manager_new(ADR(Measure), ADR(Allocate), NIL, NIL);
+   gtk_widget_set_layout_manager(parent, manager);
+
+   child1 := gtk_label_new("one");
+   gtk_widget_set_parent(child1, parent);
+   gtk_widget_set_parent(gtk_label_new("two"), parent);
+
+   window := gtk_application_window_new(application);
+   gtk_window_set_child(window, parent);
+   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_shim3: 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_shim3: PASS\n");
+      HALT(0)
+   ELSE
+      printf("test_shim3: FAIL\n");
+      HALT(1)
+   END
+END test_shim3.