Przeglądaj źródła

feat(demos): demo batch 2 - layout, text, windows and input

Port 16 more gtk-demo demos into category modules:

- DemosLayout:  sizegroup, fixed2, layoutmanager
- DemosText:    read_more, rotated_text, textmask, textscroll, textundo
- DemosWindows: headerbar, infobar, tabs, assistant
- DemosInput:   password_entry, search_entry, tagged_entry, shortcut_triggers

Registered in GtkDemo (19 demos ported so far).  Where a C facility is
not yet bound (GtkSizeGroup, GtkInfoBar, GtkPasswordEntry, PangoTabArray,
gtk_button_new_from_icon_name, widget subclassing/Pango attributes, ...)
the port keeps the demo's layout and behaviour using bound equivalents,
as noted in the module headers.

Test suite: 45 passed.
Eric Streit 3 dni temu
rodzic
commit
b04c76ec85

+ 15 - 1
Makefile

@@ -96,10 +96,24 @@ $(OBJDIR)/DemoImpl.o: $(DEMO_DIR)/DemoImpl.mod $(DEMO_DIR)/DemoImpl.def | $(OBJD
 $(OBJDIR)/DemosBasic.o: $(DEMO_DIR)/DemosBasic.mod $(DEMO_DIR)/DemosBasic.def | $(OBJDIR)
 	$(GM2) $(INCLUDES) -I$(DEMO_DIR) $(GM2FLAGS) -c $< -o $@
 
+$(OBJDIR)/DemosLayout.o: $(DEMO_DIR)/DemosLayout.mod $(DEMO_DIR)/DemosLayout.def | $(OBJDIR)
+	$(GM2) $(INCLUDES) -I$(DEMO_DIR) $(GM2FLAGS) -c $< -o $@
+
+$(OBJDIR)/DemosText.o: $(DEMO_DIR)/DemosText.mod $(DEMO_DIR)/DemosText.def | $(OBJDIR)
+	$(GM2) $(INCLUDES) -I$(DEMO_DIR) $(GM2FLAGS) -c $< -o $@
+
+$(OBJDIR)/DemosWindows.o: $(DEMO_DIR)/DemosWindows.mod $(DEMO_DIR)/DemosWindows.def | $(OBJDIR)
+	$(GM2) $(INCLUDES) -I$(DEMO_DIR) $(GM2FLAGS) -c $< -o $@
+
+$(OBJDIR)/DemosInput.o: $(DEMO_DIR)/DemosInput.mod $(DEMO_DIR)/DemosInput.def | $(OBJDIR)
+	$(GM2) $(INCLUDES) -I$(DEMO_DIR) $(GM2FLAGS) -c $< -o $@
+
 $(OBJDIR)/GtkDemo.o: $(DEMO_DIR)/GtkDemo.mod $(DEMO_DIR)/GtkDemo.def | $(OBJDIR)
 	$(GM2) $(INCLUDES) -I$(DEMO_DIR) $(GM2FLAGS) -c $< -o $@
 
-DEMO_OBJS := $(OBJDIR)/DemoCatalog.o $(OBJDIR)/DemoImpl.o $(OBJDIR)/DemosBasic.o $(OBJDIR)/GtkDemo.o
+DEMO_OBJS := $(OBJDIR)/DemoCatalog.o $(OBJDIR)/DemoImpl.o $(OBJDIR)/DemosBasic.o \
+             $(OBJDIR)/DemosLayout.o $(OBJDIR)/DemosText.o \
+             $(OBJDIR)/DemosWindows.o $(OBJDIR)/DemosInput.o $(OBJDIR)/GtkDemo.o
 
 demos: $(BLD)/demos $(OBJS) $(DEMO_OBJS) $(BLD)/demos/m2gtkdemo
 

+ 8 - 0
demos/DemosInput.def

@@ -0,0 +1,8 @@
+DEFINITION MODULE DemosInput ;
+FROM SYSTEM IMPORT ADDRESS;
+EXPORT UNQUALIFIED DoPasswordEntry, DoSearchEntry, DoTaggedEntry, DoShortcutTriggers;
+PROCEDURE DoPasswordEntry (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoSearchEntry (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoTaggedEntry (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoShortcutTriggers (doWidget: ADDRESS) : ADDRESS;
+END DemosInput.

+ 411 - 0
demos/DemosInput.mod

@@ -0,0 +1,411 @@
+IMPLEMENTATION MODULE DemosInput ;
+
+(*
+   See the GTK C originals in gtk/demos/gtk-demo:
+     password_entry.c, search_entry.c, tagged_entry.c, shortcut_triggers.c
+
+   Only the bindings under src/ are used, so this module builds with just
+   -Isrc -Idemos (see also demos/DemosText.mod).  A few C facilities used
+   by the originals are not bound; those spots are simplified and called
+   out in comments:
+     * GtkPasswordEntry is not bound, so the two password fields use a
+       GtkEntry with visibility switched off (still read via GtkEditable).
+     * gtk_search_bar_set_key_capture_widget / g_object_bind_property are
+       not bound; the header toggle drives the search bar through its
+       "toggled" signal instead.
+     * the demo-private DemoTaggedEntry composite is not a GTK widget, so
+       the tagged entry is approximated with a GtkBox holding an entry,
+       tag labels and a spinner.
+     * gtk_callback_action_new is not bound; the shortcut rows are
+       GtkButtons and the parsed "signal(clicked)" action activates them.
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM GtkWindow IMPORT gtk_window_new, gtk_window_set_title,
+   gtk_window_set_default_size, gtk_window_set_resizable,
+   gtk_window_set_titlebar, gtk_window_set_child, gtk_window_destroy;
+FROM GtkWidget IMPORT gtk_widget_set_visible, gtk_widget_get_visible,
+   gtk_widget_set_margin_start, gtk_widget_set_margin_end,
+   gtk_widget_set_margin_top, gtk_widget_set_margin_bottom,
+   gtk_widget_set_sensitive, gtk_widget_set_halign,
+   gtk_widget_set_size_request, gtk_widget_add_css_class,
+   gtk_widget_add_controller;
+FROM GtkEnums IMPORT GtkVertical, GtkHorizontal, GtkAlignCenter, GtkAlignEnd;
+FROM GtkBox IMPORT gtk_box_new, gtk_box_append, gtk_box_remove;
+FROM GtkHeaderBar IMPORT gtk_header_bar_new,
+   gtk_header_bar_set_show_title_buttons, gtk_header_bar_pack_end;
+FROM GtkEntry IMPORT gtk_entry_new, gtk_entry_set_placeholder_text,
+   gtk_entry_set_visibility, gtk_entry_set_activates_default;
+FROM GtkEditable IMPORT gtk_editable_get_text;
+FROM GtkSearchEntry IMPORT gtk_search_entry_new;
+FROM GtkSearchBar IMPORT gtk_search_bar_new, gtk_search_bar_connect_entry,
+   gtk_search_bar_set_show_close_button, gtk_search_bar_set_child,
+   gtk_search_bar_set_search_mode;
+FROM GtkToggleButton IMPORT gtk_toggle_button_new_with_label,
+   gtk_toggle_button_get_active;
+FROM GtkButton IMPORT gtk_button_new_with_label,
+   gtk_button_new_with_mnemonic, gtk_button_get_label;
+FROM GtkLabel IMPORT gtk_label_new, gtk_label_set_text;
+FROM GtkCheckButton IMPORT gtk_check_button_new_with_mnemonic;
+FROM GtkSpinner IMPORT gtk_spinner_new, gtk_spinner_start;
+FROM GtkListBox IMPORT gtk_list_box_new, gtk_list_box_insert;
+FROM GtkShortcut IMPORT gtk_shortcut_new,
+   gtk_shortcut_trigger_parse_string, gtk_shortcut_action_parse_string;
+FROM GtkShortcutController IMPORT gtk_shortcut_controller_new,
+   gtk_shortcut_controller_set_scope, gtk_shortcut_controller_add_shortcut,
+   GtkShortcutScopeGlobal;
+FROM GObject IMPORT g_signal_connect_data, GConnectDefault;
+FROM GLib IMPORT g_strcmp0;
+FROM libc IMPORT printf, strncpy;
+
+(* local stand-in for GtkUtils.CStrToM2 (lib/ is not on the include path
+   for these demos): copy a borrowed C string into a Modula-2 buffer. *)
+PROCEDURE CopyCStr (src: ADDRESS; VAR dst: ARRAY OF CHAR);
+BEGIN
+   IF src = NIL THEN
+      dst[0] := 0C
+   ELSE
+      strncpy(ADR(dst), src, HIGH(dst) + 1);
+      dst[HIGH(dst)] := 0C
+   END
+END CopyCStr;
+
+(* ------------------------------------------------------------------ *)
+(* Password Entry                                                      *)
+(* ------------------------------------------------------------------ *)
+
+VAR
+   pwWindow, pwEntry, pwEntry2, pwButton: ADDRESS;
+
+(* void update_button(GObject *object, GParamSpec *pspec, gpointer data) *)
+PROCEDURE OnUpdateButton (object: ADDRESS; pspec: ADDRESS; data: ADDRESS);
+VAR
+   text, text2: ADDRESS;
+   buf: ARRAY [0..255] OF CHAR;
+BEGIN
+   text := gtk_editable_get_text(pwEntry);
+   text2 := gtk_editable_get_text(pwEntry2);
+   CopyCStr(text, buf);
+   IF (buf[0] # 0C) AND (g_strcmp0(text, text2) = 0) THEN
+      gtk_widget_set_sensitive(pwButton, 1)
+   ELSE
+      gtk_widget_set_sensitive(pwButton, 0)
+   END
+END OnUpdateButton;
+
+(* void button_pressed(GtkButton *widget, GtkWidget *window) *)
+PROCEDURE OnDone (button: ADDRESS; data: ADDRESS);
+BEGIN
+   gtk_window_destroy(pwWindow);
+   pwWindow := NIL
+END OnDone;
+
+PROCEDURE PasswordField (placeholder: ARRAY OF CHAR) : ADDRESS;
+VAR
+   entry: ADDRESS;
+BEGIN
+   (* GtkPasswordEntry is not bound; a GtkEntry with the text hidden
+      behaves the same for this demo and is read via GtkEditable. *)
+   entry := gtk_entry_new();
+   gtk_entry_set_visibility(entry, 0);
+   gtk_entry_set_placeholder_text(entry, placeholder);
+   gtk_entry_set_activates_default(entry, 1);
+   g_signal_connect_data(entry, "notify::text", ADR(OnUpdateButton),
+                         NIL, NIL, GConnectDefault);
+   RETURN entry
+END PasswordField;
+
+PROCEDURE DoPasswordEntry (doWidget: ADDRESS) : ADDRESS;
+VAR
+   box, header: ADDRESS;
+BEGIN
+   IF pwWindow = NIL THEN
+      pwWindow := gtk_window_new();
+      gtk_window_set_title(pwWindow, "Choose a Password");
+      gtk_window_set_resizable(pwWindow, 0);
+
+      header := gtk_header_bar_new();
+      gtk_header_bar_set_show_title_buttons(header, 0);
+      gtk_window_set_titlebar(pwWindow, header);
+
+      box := gtk_box_new(GtkVertical, 6);
+      gtk_widget_set_margin_start(box, 18);
+      gtk_widget_set_margin_end(box, 18);
+      gtk_widget_set_margin_top(box, 18);
+      gtk_widget_set_margin_bottom(box, 18);
+      gtk_window_set_child(pwWindow, box);
+
+      pwEntry := PasswordField("Password");
+      gtk_box_append(box, pwEntry);
+
+      pwEntry2 := PasswordField("Confirm");
+      gtk_box_append(box, pwEntry2);
+
+      pwButton := gtk_button_new_with_mnemonic("_Done");
+      gtk_widget_add_css_class(pwButton, "suggested-action");
+      g_signal_connect_data(pwButton, "clicked", ADR(OnDone),
+                            NIL, NIL, GConnectDefault);
+      gtk_widget_set_sensitive(pwButton, 0);
+      gtk_header_bar_pack_end(header, pwButton)
+   END;
+
+   IF gtk_widget_get_visible(pwWindow) = 0 THEN
+      gtk_widget_set_visible(pwWindow, 1)
+   ELSE
+      gtk_window_destroy(pwWindow);
+      pwWindow := NIL
+   END;
+   RETURN pwWindow
+END DoPasswordEntry;
+
+(* ------------------------------------------------------------------ *)
+(* Search Entry                                                        *)
+(* ------------------------------------------------------------------ *)
+
+VAR
+   searchWindow, searchBar: ADDRESS;
+
+(* void search_changed_cb(GtkSearchEntry *entry, GtkLabel *result_label) *)
+PROCEDURE OnSearchChanged (entry: ADDRESS; resultLabel: ADDRESS);
+VAR
+   text: ADDRESS;
+   buf: ARRAY [0..255] OF CHAR;
+BEGIN
+   text := gtk_editable_get_text(entry);
+   CopyCStr(text, buf);
+   gtk_label_set_text(resultLabel, buf)
+END OnSearchChanged;
+
+(* The C demo binds the header toggle's "active" to the search bar's
+   "search-mode-enabled"; g_object_bind_property is not bound here, so
+   the same effect is wired through the "toggled" signal. *)
+PROCEDURE OnSearchToggled (button: ADDRESS; data: ADDRESS);
+BEGIN
+   IF gtk_toggle_button_get_active(button) # 0 THEN
+      gtk_search_bar_set_search_mode(searchBar, 1)
+   ELSE
+      gtk_search_bar_set_search_mode(searchBar, 0)
+   END
+END OnSearchToggled;
+
+PROCEDURE DoSearchEntry (doWidget: ADDRESS) : ADDRESS;
+VAR
+   vbox, hbox, box, label, entry, button, header: ADDRESS;
+BEGIN
+   IF searchWindow = NIL THEN
+      searchWindow := gtk_window_new();
+      gtk_window_set_title(searchWindow, "Type to Search");
+      gtk_window_set_resizable(searchWindow, 0);
+      gtk_widget_set_size_request(searchWindow, 200, -1);
+
+      header := gtk_header_bar_new();
+      gtk_window_set_titlebar(searchWindow, header);
+
+      vbox := gtk_box_new(GtkVertical, 0);
+      gtk_window_set_child(searchWindow, vbox);
+
+      entry := gtk_search_entry_new();
+      gtk_widget_set_halign(entry, GtkAlignCenter);
+
+      searchBar := gtk_search_bar_new();
+      gtk_search_bar_connect_entry(searchBar, entry);
+      gtk_search_bar_set_show_close_button(searchBar, 0);
+      gtk_search_bar_set_child(searchBar, entry);
+      gtk_box_append(vbox, searchBar);
+
+      (* gtk_search_bar_set_key_capture_widget is not bound; the search
+         bar is opened via the header toggle below instead. *)
+
+      box := gtk_box_new(GtkVertical, 18);
+      gtk_widget_set_margin_start(box, 18);
+      gtk_widget_set_margin_end(box, 18);
+      gtk_widget_set_margin_top(box, 18);
+      gtk_widget_set_margin_bottom(box, 18);
+      gtk_box_append(vbox, box);
+
+      button := gtk_toggle_button_new_with_label("Search");
+      g_signal_connect_data(button, "toggled", ADR(OnSearchToggled),
+                            NIL, NIL, GConnectDefault);
+      gtk_header_bar_pack_end(header, button);
+
+      hbox := gtk_box_new(GtkHorizontal, 10);
+      gtk_box_append(box, hbox);
+
+      label := gtk_label_new("Searching for:");
+      gtk_box_append(hbox, label);
+
+      label := gtk_label_new("");
+      gtk_box_append(hbox, label);
+
+      g_signal_connect_data(entry, "search-changed", ADR(OnSearchChanged),
+                            label, NIL, GConnectDefault)
+   END;
+
+   IF gtk_widget_get_visible(searchWindow) = 0 THEN
+      gtk_widget_set_visible(searchWindow, 1)
+   ELSE
+      gtk_window_destroy(searchWindow);
+      searchWindow := NIL
+   END;
+   RETURN searchWindow
+END DoSearchEntry;
+
+(* ------------------------------------------------------------------ *)
+(* Tagged Entry                                                        *)
+(* ------------------------------------------------------------------ *)
+
+(* The demo's DemoTaggedEntry composite is not bound; this approximates
+   it with a horizontal GtkBox that holds the entry, tag labels and an
+   optional spinner. *)
+
+VAR
+   taggedWindow, tagBox, tagSpinner: ADDRESS;
+
+(* void add_tag(GtkButton *button, DemoTaggedEntry *entry) *)
+PROCEDURE OnAddTag (button: ADDRESS; data: ADDRESS);
+VAR
+   tag: ADDRESS;
+BEGIN
+   tag := gtk_label_new("Blue");
+   gtk_widget_add_css_class(tag, "blue");
+   gtk_box_append(tagBox, tag)
+END OnAddTag;
+
+(* void toggle_spinner(GtkCheckButton *button, DemoTaggedEntry *entry) *)
+PROCEDURE OnToggleSpinner (button: ADDRESS; data: ADDRESS);
+BEGIN
+   IF tagSpinner = NIL THEN
+      tagSpinner := gtk_spinner_new();
+      gtk_spinner_start(tagSpinner);
+      gtk_box_append(tagBox, tagSpinner)
+   ELSE
+      gtk_box_remove(tagBox, tagSpinner);
+      tagSpinner := NIL
+   END
+END OnToggleSpinner;
+
+PROCEDURE DoTaggedEntry (doWidget: ADDRESS) : ADDRESS;
+VAR
+   box, box2, entry, button: ADDRESS;
+BEGIN
+   IF taggedWindow = NIL THEN
+      taggedWindow := gtk_window_new();
+      gtk_window_set_title(taggedWindow, "Tagged Entry");
+      gtk_window_set_default_size(taggedWindow, 260, -1);
+      gtk_window_set_resizable(taggedWindow, 0);
+
+      box := gtk_box_new(GtkVertical, 6);
+      gtk_widget_set_margin_start(box, 18);
+      gtk_widget_set_margin_end(box, 18);
+      gtk_widget_set_margin_top(box, 18);
+      gtk_widget_set_margin_bottom(box, 18);
+      gtk_window_set_child(taggedWindow, box);
+
+      tagBox := gtk_box_new(GtkHorizontal, 6);
+      gtk_box_append(box, tagBox);
+
+      entry := gtk_entry_new();
+      gtk_box_append(tagBox, entry);
+
+      box2 := gtk_box_new(GtkHorizontal, 6);
+      gtk_widget_set_halign(box2, GtkAlignEnd);
+      gtk_box_append(box, box2);
+
+      button := gtk_button_new_with_mnemonic("Add _Tag");
+      g_signal_connect_data(button, "clicked", ADR(OnAddTag),
+                            entry, NIL, GConnectDefault);
+      gtk_box_append(box2, button);
+
+      button := gtk_check_button_new_with_mnemonic("_Spinner");
+      g_signal_connect_data(button, "toggled", ADR(OnToggleSpinner),
+                            entry, NIL, GConnectDefault);
+      gtk_box_append(box2, button)
+   END;
+
+   IF gtk_widget_get_visible(taggedWindow) = 0 THEN
+      gtk_widget_set_visible(taggedWindow, 1)
+   ELSE
+      gtk_window_destroy(taggedWindow);
+      taggedWindow := NIL;
+      tagBox := NIL;
+      tagSpinner := NIL
+   END;
+   RETURN taggedWindow
+END DoTaggedEntry;
+
+(* ------------------------------------------------------------------ *)
+(* Shortcut Triggers                                                   *)
+(* ------------------------------------------------------------------ *)
+
+VAR
+   shortcutWindow: ADDRESS;
+   shortcutDesc: ARRAY [0..1] OF ARRAY [0..31] OF CHAR;
+   shortcutTrig: ARRAY [0..1] OF ARRAY [0..31] OF CHAR;
+
+(* gboolean shortcut_activated(GtkWidget *widget, GVariant *unused, gpointer row) *)
+PROCEDURE OnShortcutActivated (button: ADDRESS; data: ADDRESS);
+VAR
+   label: ADDRESS;
+   buf: ARRAY [0..127] OF CHAR;
+BEGIN
+   label := gtk_button_get_label(button);
+   CopyCStr(label, buf);
+   printf("activated %s\n", buf)
+END OnShortcutActivated;
+
+PROCEDURE DoShortcutTriggers (doWidget: ADDRESS) : ADDRESS;
+VAR
+   list, row, controller, trigger, action, shortcut: ADDRESS;
+   i: INTEGER;
+   triggerAction: ARRAY [0..31] OF CHAR;
+BEGIN
+   IF shortcutWindow = NIL THEN
+      shortcutWindow := gtk_window_new();
+      gtk_window_set_title(shortcutWindow, "Shortcuts");
+      gtk_window_set_default_size(shortcutWindow, 200, -1);
+      gtk_window_set_resizable(shortcutWindow, 0);
+
+      shortcutDesc[0] := "Press Ctrl-G";
+      shortcutDesc[1] := "Press X";
+      shortcutTrig[0] := "<Control>g";
+      shortcutTrig[1] := "x";
+      triggerAction := "signal(clicked)";
+
+      list := gtk_list_box_new();
+      gtk_widget_set_margin_top(list, 6);
+      gtk_widget_set_margin_bottom(list, 6);
+      gtk_widget_set_margin_start(list, 6);
+      gtk_widget_set_margin_end(list, 6);
+      gtk_window_set_child(shortcutWindow, list);
+
+      FOR i := 0 TO 1 DO
+         (* The C row is a GtkLabel whose shortcut uses
+            gtk_callback_action_new (not bound); a GtkButton row plus the
+            parsed "signal(clicked)" action gives the same activation. *)
+         row := gtk_button_new_with_label(shortcutDesc[i]);
+         gtk_list_box_insert(list, row, -1);
+
+         controller := gtk_shortcut_controller_new();
+         gtk_shortcut_controller_set_scope(controller, GtkShortcutScopeGlobal);
+         gtk_widget_add_controller(row, controller);
+
+         trigger := gtk_shortcut_trigger_parse_string(shortcutTrig[i]);
+         action := gtk_shortcut_action_parse_string(triggerAction);
+         shortcut := gtk_shortcut_new(trigger, action);
+         gtk_shortcut_controller_add_shortcut(controller, shortcut);
+
+         g_signal_connect_data(row, "clicked", ADR(OnShortcutActivated),
+                               NIL, NIL, GConnectDefault)
+      END
+   END;
+
+   IF gtk_widget_get_visible(shortcutWindow) = 0 THEN
+      gtk_widget_set_visible(shortcutWindow, 1)
+   ELSE
+      gtk_window_destroy(shortcutWindow);
+      shortcutWindow := NIL
+   END;
+   RETURN shortcutWindow
+END DoShortcutTriggers;
+
+END DemosInput.

+ 7 - 0
demos/DemosLayout.def

@@ -0,0 +1,7 @@
+DEFINITION MODULE DemosLayout ;
+FROM SYSTEM IMPORT ADDRESS;
+EXPORT UNQUALIFIED DoSizeGroup, DoFixed2, DoLayoutManager;
+PROCEDURE DoSizeGroup (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoFixed2 (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoLayoutManager (doWidget: ADDRESS) : ADDRESS;
+END DemosLayout.

+ 252 - 0
demos/DemosLayout.mod

@@ -0,0 +1,252 @@
+IMPLEMENTATION MODULE DemosLayout ;
+
+(*
+   m2-GTK4 - the layout-related gtk-demo demos: Size Groups, Fixed Layout
+   Transformations and Layout Manager/Transition.  Ported from the GTK C
+   demo sources in gtk/demos/gtk-demo (sizegroup.c, fixed2.c,
+   layoutmanager.c).
+
+   Bindings that are not (yet) exposed under src/ forced a few faithful
+   simplifications; each one is marked with a "SIMPLIFIED" note below.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM GtkWindow IMPORT gtk_window_new, gtk_window_set_title,
+   gtk_window_set_default_size, gtk_window_set_resizable,
+   gtk_window_set_child, gtk_window_destroy;
+FROM GtkWidget IMPORT gtk_widget_set_visible, gtk_widget_get_visible,
+   gtk_widget_set_margin_start, gtk_widget_set_margin_end,
+   gtk_widget_set_margin_top, gtk_widget_set_margin_bottom,
+   gtk_widget_set_halign, gtk_widget_set_valign, gtk_widget_set_hexpand;
+FROM GtkBox IMPORT gtk_box_new, gtk_box_append;
+FROM GtkGrid IMPORT gtk_grid_new, gtk_grid_attach,
+   gtk_grid_set_row_spacing, gtk_grid_set_column_spacing;
+FROM GtkFrame IMPORT gtk_frame_new, gtk_frame_set_child;
+FROM GtkLabel IMPORT gtk_label_new;
+FROM GtkDropDown IMPORT gtk_drop_down_new;
+FROM GtkStringList IMPORT gtk_string_list_new, gtk_string_list_append;
+FROM GtkCheckButton IMPORT gtk_check_button_new_with_label,
+   gtk_check_button_set_active;
+FROM GtkScrolledWindow IMPORT gtk_scrolled_window_new,
+   gtk_scrolled_window_set_child;
+FROM GtkFixed IMPORT gtk_fixed_new, gtk_fixed_put;
+FROM GtkEnums IMPORT GtkVertical, GtkAlignStart, GtkAlignEnd,
+   GtkAlignBaseline;
+
+(* ------------------------------------------------------------------ *)
+(* Size Groups                                                        *)
+(* ------------------------------------------------------------------ *)
+
+VAR
+   sizeGroupWindow: ADDRESS;
+
+(* Build a drop-down from three literal labels.  The C demo passes a
+   NULL-terminated const char * array to gtk_drop_down_new_from_strings();
+   this binding favours a GtkStringList model, which is the equivalent
+   route exposed under src/. *)
+PROCEDURE MakeDropDown (a, b, c: ARRAY OF CHAR) : ADDRESS;
+VAR
+   list: ADDRESS;
+BEGIN
+   list := gtk_string_list_new(NIL);
+   gtk_string_list_append(list, a);
+   gtk_string_list_append(list, b);
+   gtk_string_list_append(list, c);
+   RETURN gtk_drop_down_new(list, NIL)
+END MakeDropDown;
+
+(* add_row(): one mnemonic label plus a drop-down in a grid row.
+   SIMPLIFIED: gtk_label_new_with_mnemonic() and
+   gtk_label_set_mnemonic_widget() are not bound, so a plain label is
+   used (the underscores are dropped rather than shown literally). *)
+PROCEDURE AddRow (table: ADDRESS; row: INTEGER;
+                  labelText, a, b, c: ARRAY OF CHAR);
+VAR
+   label, dropdown: ADDRESS;
+BEGIN
+   label := gtk_label_new(labelText);
+   gtk_widget_set_halign(label, GtkAlignStart);
+   gtk_widget_set_valign(label, GtkAlignBaseline);
+   gtk_widget_set_hexpand(label, 1);
+   gtk_grid_attach(table, label, 0, row, 1, 1);
+
+   dropdown := MakeDropDown(a, b, c);
+   gtk_widget_set_halign(dropdown, GtkAlignEnd);
+   gtk_widget_set_valign(dropdown, GtkAlignBaseline);
+   gtk_grid_attach(table, dropdown, 1, row, 1, 1)
+END AddRow;
+
+PROCEDURE DoSizeGroup (doWidget: ADDRESS) : ADDRESS;
+VAR
+   vbox, frame, table, checkButton: ADDRESS;
+BEGIN
+   IF sizeGroupWindow = NIL THEN
+      sizeGroupWindow := gtk_window_new();
+      gtk_window_set_title(sizeGroupWindow, "Size Groups");
+      gtk_window_set_resizable(sizeGroupWindow, 0);
+
+      vbox := gtk_box_new(GtkVertical, 5);
+      gtk_widget_set_margin_start(vbox, 5);
+      gtk_widget_set_margin_end(vbox, 5);
+      gtk_widget_set_margin_top(vbox, 5);
+      gtk_widget_set_margin_bottom(vbox, 5);
+      gtk_window_set_child(sizeGroupWindow, vbox);
+
+      (* SIMPLIFIED: GtkSizeGroup is not bound under src/, so the
+         gtk_size_group_new()/add_widget()/set_mode() machinery and the
+         "toggled" callback that switches it on and off are omitted.
+         The two grids below are otherwise identical to the C demo. *)
+
+      (* Color options *)
+      frame := gtk_frame_new("Color Options");
+      gtk_box_append(vbox, frame);
+      table := gtk_grid_new();
+      gtk_widget_set_margin_start(table, 5);
+      gtk_widget_set_margin_end(table, 5);
+      gtk_widget_set_margin_top(table, 5);
+      gtk_widget_set_margin_bottom(table, 5);
+      gtk_grid_set_row_spacing(table, 5);
+      gtk_grid_set_column_spacing(table, 10);
+      gtk_frame_set_child(frame, table);
+      AddRow(table, 0, "Foreground", "Red", "Green", "Blue");
+      AddRow(table, 1, "Background", "Red", "Green", "Blue");
+
+      (* Line options *)
+      frame := gtk_frame_new("Line Options");
+      gtk_box_append(vbox, frame);
+      table := gtk_grid_new();
+      gtk_widget_set_margin_start(table, 5);
+      gtk_widget_set_margin_end(table, 5);
+      gtk_widget_set_margin_top(table, 5);
+      gtk_widget_set_margin_bottom(table, 5);
+      gtk_grid_set_row_spacing(table, 5);
+      gtk_grid_set_column_spacing(table, 10);
+      gtk_frame_set_child(frame, table);
+      AddRow(table, 0, "Dashing", "Solid", "Dashed", "Dotted");
+      AddRow(table, 1, "Line ends", "Square", "Round", "Double Arrow");
+
+      checkButton := gtk_check_button_new_with_label("Enable grouping");
+      gtk_box_append(vbox, checkButton);
+      gtk_check_button_set_active(checkButton, 1)
+   END;
+
+   IF gtk_widget_get_visible(sizeGroupWindow) = 0 THEN
+      gtk_widget_set_visible(sizeGroupWindow, 1)
+   ELSE
+      gtk_window_destroy(sizeGroupWindow);
+      sizeGroupWindow := NIL
+   END;
+   RETURN sizeGroupWindow
+END DoSizeGroup;
+
+(* ------------------------------------------------------------------ *)
+(* Fixed Layout / Transformations                                     *)
+(* ------------------------------------------------------------------ *)
+
+VAR
+   fixed2Window: ADDRESS;
+
+PROCEDURE DoFixed2 (doWidget: ADDRESS) : ADDRESS;
+VAR
+   sw, fixed, child: ADDRESS;
+BEGIN
+   IF fixed2Window = NIL THEN
+      fixed2Window := gtk_window_new();
+      gtk_window_set_title(fixed2Window, "Fixed Layout - Transformations");
+      gtk_window_set_default_size(fixed2Window, 400, 300);
+
+      sw := gtk_scrolled_window_new();
+      gtk_window_set_child(fixed2Window, sw);
+
+      fixed := gtk_fixed_new();
+      gtk_scrolled_window_set_child(sw, fixed);
+
+      child := gtk_label_new("All fixed?");
+      gtk_fixed_put(fixed, child, 0.0, 0.0)
+
+      (* SIMPLIFIED: the C demo animates the child with a frame-clock
+         tick callback (gtk_widget_add_tick_callback), a transform on the
+         fixed child (gtk_fixed_set_child_transform) and
+         gtk_widget_set_overflow(); none of those are bound under src/.
+         The widget hierarchy is kept, the rotation/scale animation is
+         omitted. *)
+   END;
+
+   IF gtk_widget_get_visible(fixed2Window) = 0 THEN
+      gtk_widget_set_visible(fixed2Window, 1)
+   ELSE
+      gtk_window_destroy(fixed2Window);
+      fixed2Window := NIL
+   END;
+   RETURN fixed2Window
+END DoFixed2;
+
+(* ------------------------------------------------------------------ *)
+(* Layout Manager / Transition                                        *)
+(* ------------------------------------------------------------------ *)
+
+VAR
+   layoutManagerWindow: ADDRESS;
+   layoutColors: ARRAY [0..15] OF ARRAY [0..9] OF CHAR;
+
+(* SIMPLIFIED: the C demo builds a bespoke DemoWidget whose custom
+   GtkLayoutManager animates its 16 DemoChild() squares between a grid
+   and a circle.  Neither that C widget pair nor a GtkLayoutManager /
+   GtkCustomLayout binding exists under src/.  The bound equivalent used
+   here is a GtkGrid holding the same 16 coloured children; the names
+   are shown as label text (the animation and the drawn colour fill are
+   omitted). *)
+PROCEDURE InitColors;
+BEGIN
+   layoutColors[0] := "red";
+   layoutColors[1] := "orange";
+   layoutColors[2] := "yellow";
+   layoutColors[3] := "green";
+   layoutColors[4] := "blue";
+   layoutColors[5] := "grey";
+   layoutColors[6] := "magenta";
+   layoutColors[7] := "lime";
+   layoutColors[8] := "yellow";
+   layoutColors[9] := "firebrick";
+   layoutColors[10] := "aqua";
+   layoutColors[11] := "purple";
+   layoutColors[12] := "tomato";
+   layoutColors[13] := "pink";
+   layoutColors[14] := "thistle";
+   layoutColors[15] := "maroon"
+END InitColors;
+
+PROCEDURE DoLayoutManager (doWidget: ADDRESS) : ADDRESS;
+VAR
+   grid, child: ADDRESS;
+   i: INTEGER;
+BEGIN
+   IF layoutManagerWindow = NIL THEN
+      layoutManagerWindow := gtk_window_new();
+      gtk_window_set_title(layoutManagerWindow,
+                           "Layout Manager - Transition");
+      gtk_window_set_default_size(layoutManagerWindow, 600, 600);
+
+      InitColors;
+      grid := gtk_grid_new();
+      FOR i := 0 TO 15 DO
+         child := gtk_label_new(layoutColors[i]);
+         gtk_widget_set_margin_start(child, 4);
+         gtk_widget_set_margin_end(child, 4);
+         gtk_widget_set_margin_top(child, 4);
+         gtk_widget_set_margin_bottom(child, 4);
+         gtk_grid_attach(grid, child, i MOD 4, i DIV 4, 1, 1)
+      END;
+      gtk_window_set_child(layoutManagerWindow, grid)
+   END;
+
+   IF gtk_widget_get_visible(layoutManagerWindow) = 0 THEN
+      gtk_widget_set_visible(layoutManagerWindow, 1)
+   ELSE
+      gtk_window_destroy(layoutManagerWindow);
+      layoutManagerWindow := NIL
+   END;
+   RETURN layoutManagerWindow
+END DoLayoutManager;
+
+END DemosLayout.

+ 19 - 0
demos/DemosText.def

@@ -0,0 +1,19 @@
+DEFINITION MODULE DemosText ;
+
+(*
+   m2-GTK4 - the text-related gtk-demo demos: Read More, Rotated Text,
+   Text Mask, Automatic Scrolling and Undo/Redo.  Ported from the GTK C
+   demo sources in gtk/demos/gtk-demo.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED DoReadMore, DoRotatedText, DoTextMask, DoTextScroll, DoTextUndo;
+
+PROCEDURE DoReadMore (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoRotatedText (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoTextMask (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoTextScroll (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoTextUndo (doWidget: ADDRESS) : ADDRESS;
+
+END DemosText.

+ 525 - 0
demos/DemosText.mod

@@ -0,0 +1,525 @@
+IMPLEMENTATION MODULE DemosText ;
+
+(*
+   See the GTK C originals in gtk/demos/gtk-demo:
+     read_more.c   -> DoReadMore
+     rotated_text.c-> DoRotatedText
+     textmask.c    -> DoTextMask
+     textscroll.c  -> DoTextScroll
+     textundo.c    -> DoTextUndo
+
+   The gtk-demo contract is: on the first call build the window and store
+   it in a module-level variable; on later calls just toggle the window's
+   visibility, destroying it (and clearing the variable) when it is
+   already visible.  Return the window, or NIL once destroyed.
+
+   Several C helpers used by the originals are not part of the binding
+   set under src/, so those spots are simplified and marked below:
+     * ReadMore is a custom GtkWidget subclass with its own measure() and
+       size_allocate(); subclassing is not exposed, so a plain wrapping
+       GtkLabel plus a "Read More" button is used instead.
+     * The Pango custom shape renderer / PangoAttrList of rotated_text
+       are not bound; the layout is drawn with its plain font.
+     * gtk_text_buffer_set_enable_undo(), *begin/end_irreversible_action()
+       and gtk_text_view_scroll_mark_onscreen() are not bound.
+     * cairo_fill_preserve() is not bound; textmask rebuilds the path to
+       stroke the outline.
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR, CAST;
+FROM GtkWindow IMPORT gtk_window_new, gtk_window_set_title,
+   gtk_window_set_default_size, gtk_window_set_resizable,
+   gtk_window_set_child, gtk_window_destroy;
+FROM GtkWidget IMPORT gtk_widget_set_visible, gtk_widget_get_visible,
+   gtk_widget_set_size_request, gtk_widget_set_vexpand, gtk_widget_set_hexpand,
+   gtk_widget_add_css_class;
+FROM GtkBox IMPORT gtk_box_new, gtk_box_append;
+FROM GtkEnums IMPORT GtkVertical, GtkHorizontal, GtkWrapWord;
+FROM GtkLabel IMPORT gtk_label_new, gtk_label_set_wrap,
+   gtk_label_set_max_width_chars;
+FROM GtkButton IMPORT gtk_button_new_with_label;
+FROM GtkScrolledWindow IMPORT gtk_scrolled_window_new,
+   gtk_scrolled_window_set_child, gtk_scrolled_window_set_policy,
+   GtkPolicyAutomatic;
+FROM GtkTextView IMPORT gtk_text_view_new, gtk_text_view_get_buffer,
+   gtk_text_view_set_wrap_mode, gtk_text_view_set_left_margin,
+   gtk_text_view_set_right_margin, gtk_text_view_set_top_margin,
+   gtk_text_view_set_bottom_margin, gtk_text_view_scroll_to_mark;
+FROM GtkTextBuffer IMPORT gtk_text_buffer_get_start_iter,
+   gtk_text_buffer_insert, gtk_text_buffer_get_end_iter,
+   gtk_text_buffer_create_mark, gtk_text_buffer_get_mark,
+   gtk_text_buffer_get_iter_at_mark;
+FROM GtkTextIter IMPORT GtkTextIter;
+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 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;
+FROM PangoCairo IMPORT pango_cairo_create_layout, pango_cairo_show_layout,
+   pango_cairo_update_layout;
+FROM Cairo IMPORT cairo_move_to, cairo_rotate, cairo_scale, cairo_translate,
+   cairo_set_source, cairo_save, cairo_restore, cairo_fill, cairo_stroke,
+   cairo_set_source_rgb, cairo_set_line_width, cairo_select_font_face,
+   cairo_set_font_size, cairo_text_path, cairo_pattern_create_linear,
+   cairo_pattern_add_color_stop_rgb, cairo_pattern_destroy,
+   CairoFontSlantNormal, CairoFontWeightBold;
+FROM GObject IMPORT g_signal_connect_data, GConnectDefault, g_object_unref;
+FROM GLib IMPORT g_timeout_add, g_source_remove, guint, GSourceContinue;
+
+CONST
+   (* rotated_text *)
+   Radius = 150;                  (* pixels; window is 4*Radius by 2*Radius *)
+   NWords = 5;
+   Pi     = 3.141592653589793;
+   RotatedText = "I \xE2\x99\xA5 GTK";
+
+   ReadMoreText =
+"I'd just like to interject for a moment. What you're referring to as Linux, is in fact, GNU/Linux, or as I've recently taken to calling it, GNU plus Linux. Linux is not an operating system unto itself, but rather another free component of a fully functioning GNU system made useful by the GNU corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX.\n\nMany computer users run a modified version of the GNU system every day, without realizing it. Through a peculiar turn of events, the version of GNU which is widely used today is often called \x22Linux\x22, and many of its users are not aware that it is basically the GNU system, developed by the GNU Project.\n\nThere really is a Linux, and these people are using it, but it is just a part of the system they use. Linux is the kernel: the program in the system that allocates the machine's resources to the other programs that you run. The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. Linux is normally used in combination with the GNU operating system: the whole system is basically GNU with Linux added, or GNU/Linux. All the so-called \x22Linux\x22 distributions are really distributions of GNU/Linux.";
+
+   UndoText =
+"The GtkTextView supports undo and redo through the use of a GtkTextBuffer. You can enable or disable undo support using gtk_text_buffer_set_enable_undo().\nType to add more text.\nUse Control+z to undo and Control+Shift+z or Control+y to redo previously undone operations.";
+
+   ScrollToEndPrefix =
+      "Scroll to end scroll to end scroll to end scroll to end ";
+   ScrollToBottomPrefix =
+      "Scroll to bottom scroll to bottom scroll to bottom scroll to bottom ";
+
+VAR
+   readMoreWindow, rotatedTextWindow, textMaskWindow,
+   textScrollWindow, textUndoWindow: ADDRESS;
+
+   (* static counters of the C originals *)
+   endCount, bottomCount: INTEGER;
+
+(* ================================================================== *)
+(* read_more                                                          *)
+(* ================================================================== *)
+
+PROCEDURE OnReadMore (button: ADDRESS; data: ADDRESS);
+BEGIN
+   (* The real demo grows the custom widget; with no subclassing the
+      best we can do is hide the reveal button. *)
+   gtk_widget_set_visible(button, 0)
+END OnReadMore;
+
+PROCEDURE DoReadMore (doWidget: ADDRESS) : ADDRESS;
+VAR
+   box, label, button: ADDRESS;
+BEGIN
+   IF readMoreWindow = NIL THEN
+      readMoreWindow := gtk_window_new();
+      gtk_window_set_title(readMoreWindow, "Read More");
+      gtk_window_set_default_size(readMoreWindow, 400, 300);
+
+      box := gtk_box_new(GtkVertical, 6);
+      gtk_window_set_child(readMoreWindow, box);
+
+      label := gtk_label_new(ReadMoreText);
+      gtk_label_set_wrap(label, 1);
+      gtk_label_set_max_width_chars(label, 30);
+      gtk_widget_set_vexpand(label, 1);
+      gtk_box_append(box, label);
+
+      button := gtk_button_new_with_label("Read More");
+      g_signal_connect_data(button, "clicked", ADR(OnReadMore), NIL, NIL,
+                            GConnectDefault);
+      gtk_box_append(box, button)
+   END;
+
+   IF gtk_widget_get_visible(readMoreWindow) = 0 THEN
+      gtk_widget_set_visible(readMoreWindow, 1)
+   ELSE
+      gtk_window_destroy(readMoreWindow);
+      readMoreWindow := NIL
+   END;
+   RETURN readMoreWindow
+END DoReadMore;
+
+(* ================================================================== *)
+(* rotated_text                                                       *)
+(* ================================================================== *)
+
+PROCEDURE RotatedTextDraw (area: ADDRESS; cr: ADDRESS;
+                           width, height: INTEGER; data: ADDRESS);
+VAR
+   layout, desc, pattern: ADDRESS;
+   deviceRadius: REAL;
+   lw, lh, i: INTEGER;
+BEGIN
+   IF width < height THEN
+      deviceRadius := VAL(REAL, width) / 2.0
+   ELSE
+      deviceRadius := VAL(REAL, height) / 2.0
+   END;
+
+   cairo_translate(cr,
+      deviceRadius + (VAL(REAL, width) - 2.0 * deviceRadius) / 2.0,
+      deviceRadius + (VAL(REAL, height) - 2.0 * deviceRadius) / 2.0);
+   cairo_scale(cr, deviceRadius / 150.0, deviceRadius / 150.0);
+
+   pattern := cairo_pattern_create_linear(-150.0, -150.0, 150.0, 150.0);
+   cairo_pattern_add_color_stop_rgb(pattern, 0.0, 0.5, 0.0, 0.0);
+   cairo_pattern_add_color_stop_rgb(pattern, 1.0, 0.0, 0.0, 0.5);
+   cairo_set_source(cr, pattern);
+
+   layout := pango_cairo_create_layout(cr);
+   pango_layout_set_text(layout, RotatedText, -1);
+   desc := pango_font_description_from_string("Serif 18");
+   pango_layout_set_font_description(layout, desc);
+   pango_font_description_free(desc);
+
+   FOR i := 0 TO NWords - 1 DO
+      pango_cairo_update_layout(cr, layout);
+      pango_layout_get_pixel_size(layout, lw, lh);
+      cairo_move_to(cr, -VAL(REAL, lw) / 2.0, -150.0 * 0.9);
+      pango_cairo_show_layout(cr, layout);
+      cairo_rotate(cr, 2.0 * Pi / VAL(REAL, NWords))
+   END;
+
+   g_object_unref(layout);
+   cairo_pattern_destroy(pattern)
+END RotatedTextDraw;
+
+PROCEDURE DoRotatedText (doWidget: ADDRESS) : ADDRESS;
+VAR
+   box, drawingArea, label: ADDRESS;
+BEGIN
+   IF rotatedTextWindow = NIL THEN
+      rotatedTextWindow := gtk_window_new();
+      gtk_window_set_title(rotatedTextWindow, "Rotated Text");
+      gtk_window_set_default_size(rotatedTextWindow, 4 * Radius, 2 * Radius);
+
+      box := gtk_box_new(GtkHorizontal, 0);
+      gtk_window_set_child(rotatedTextWindow, box);
+
+      drawingArea := gtk_drawing_area_new();
+      gtk_drawing_area_set_content_width(drawingArea, 2 * Radius);
+      gtk_drawing_area_set_content_height(drawingArea, 2 * Radius);
+      gtk_widget_set_hexpand(drawingArea, 1);
+      gtk_widget_set_vexpand(drawingArea, 1);
+      gtk_box_append(box, drawingArea);
+      gtk_widget_add_css_class(drawingArea, "view");
+      gtk_drawing_area_set_draw_func(drawingArea, ADR(RotatedTextDraw),
+                                     NIL, NIL);
+
+      label := gtk_label_new(RotatedText);
+      gtk_widget_set_hexpand(label, 1);
+      gtk_widget_set_vexpand(label, 1);
+      gtk_box_append(box, label)
+   END;
+
+   IF gtk_widget_get_visible(rotatedTextWindow) = 0 THEN
+      gtk_widget_set_visible(rotatedTextWindow, 1)
+   ELSE
+      gtk_window_destroy(rotatedTextWindow);
+      rotatedTextWindow := NIL
+   END;
+   RETURN rotatedTextWindow
+END DoRotatedText;
+
+(* ================================================================== *)
+(* textmask                                                           *)
+(* ================================================================== *)
+
+PROCEDURE MaskPath (cr: ADDRESS);
+BEGIN
+   cairo_move_to(cr, 30.0, 20.0);
+   cairo_text_path(cr, "Pango power!");
+   cairo_move_to(cr, 30.0, 60.0);
+   cairo_text_path(cr, "Pango power!");
+   cairo_move_to(cr, 30.0, 100.0);
+   cairo_text_path(cr, "Pango power!")
+END MaskPath;
+
+PROCEDURE TextMaskDraw (area: ADDRESS; cr: ADDRESS;
+                        width, height: INTEGER; data: ADDRESS);
+VAR
+   pattern: ADDRESS;
+BEGIN
+   cairo_save(cr);
+
+   cairo_select_font_face(cr, "sans", CairoFontSlantNormal,
+                          CairoFontWeightBold);
+   cairo_set_font_size(cr, 34.0);
+   MaskPath(cr);
+
+   pattern := cairo_pattern_create_linear(0.0, 0.0,
+                                          VAL(REAL, width), VAL(REAL, height));
+   cairo_pattern_add_color_stop_rgb(pattern, 0.0, 1.0, 0.0, 0.0);
+   cairo_pattern_add_color_stop_rgb(pattern, 0.2, 1.0, 0.0, 0.0);
+   cairo_pattern_add_color_stop_rgb(pattern, 0.3, 1.0, 1.0, 0.0);
+   cairo_pattern_add_color_stop_rgb(pattern, 0.4, 0.0, 1.0, 0.0);
+   cairo_pattern_add_color_stop_rgb(pattern, 0.6, 0.0, 1.0, 1.0);
+   cairo_pattern_add_color_stop_rgb(pattern, 0.7, 0.0, 0.0, 1.0);
+   cairo_pattern_add_color_stop_rgb(pattern, 0.8, 1.0, 0.0, 1.0);
+   cairo_pattern_add_color_stop_rgb(pattern, 1.0, 1.0, 0.0, 1.0);
+
+   cairo_set_source(cr, pattern);
+   cairo_fill(cr);                    (* cairo_fill_preserve is unbound *)
+   cairo_pattern_destroy(pattern);
+
+   (* Rebuild the path (fill consumed it) and outline it in black. *)
+   MaskPath(cr);
+   cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+   cairo_set_line_width(cr, 0.5);
+   cairo_stroke(cr);
+
+   cairo_restore(cr)
+END TextMaskDraw;
+
+PROCEDURE DoTextMask (doWidget: ADDRESS) : ADDRESS;
+VAR
+   da: ADDRESS;
+BEGIN
+   IF textMaskWindow = NIL THEN
+      textMaskWindow := gtk_window_new();
+      gtk_window_set_resizable(textMaskWindow, 1);
+      gtk_widget_set_size_request(textMaskWindow, 400, 240);
+      gtk_window_set_title(textMaskWindow, "Text Mask");
+
+      da := gtk_drawing_area_new();
+      gtk_window_set_child(textMaskWindow, da);
+      gtk_drawing_area_set_draw_func(da, ADR(TextMaskDraw), NIL, NIL)
+   END;
+
+   IF gtk_widget_get_visible(textMaskWindow) = 0 THEN
+      gtk_widget_set_visible(textMaskWindow, 1)
+   ELSE
+      gtk_window_destroy(textMaskWindow);
+      textMaskWindow := NIL
+   END;
+   RETURN textMaskWindow
+END DoTextMask;
+
+(* ================================================================== *)
+(* textscroll                                                         *)
+(* ================================================================== *)
+
+(* Fill s with n spaces (NUL-terminated). *)
+PROCEDURE MakeSpaces (n: INTEGER; VAR s: ARRAY OF CHAR);
+VAR
+   i, hi: INTEGER;
+BEGIN
+   hi := VAL(INTEGER, HIGH(s));
+   IF n > hi - 1 THEN
+      n := hi - 1
+   END;
+   FOR i := 0 TO n - 1 DO
+      s[i] := ' '
+   END;
+   s[n] := 0C
+END MakeSpaces;
+
+(* Copy prefix then append the decimal rendering of n (NUL-terminated). *)
+PROCEDURE BuildScrollText (prefix: ARRAY OF CHAR; n: INTEGER;
+                           VAR s: ARRAY OF CHAR);
+VAR
+   pos, k, hi: INTEGER;
+   digits: ARRAY [0..15] OF CHAR;
+BEGIN
+   hi := VAL(INTEGER, HIGH(s));
+   pos := 0;
+   WHILE (pos < hi) AND (prefix[pos] # 0C) DO
+      s[pos] := prefix[pos];
+      INC(pos)
+   END;
+   k := 0;
+   IF n = 0 THEN
+      digits[0] := '0';
+      k := 1
+   ELSE
+      WHILE n > 0 DO
+         digits[k] := CHR(ORD('0') + VAL(CARDINAL, n MOD 10));
+         n := n DIV 10;
+         INC(k)
+      END
+   END;
+   WHILE k > 0 DO
+      DEC(k);
+      IF pos < hi THEN
+         s[pos] := digits[k];
+         INC(pos)
+      END
+   END;
+   s[pos] := 0C
+END BuildScrollText;
+
+PROCEDURE ScrollToEnd (textview: ADDRESS) : INTEGER;
+VAR
+   buffer, mark: ADDRESS;
+   iter: GtkTextIter;
+   spaces, text: ARRAY [0..255] OF CHAR;
+BEGIN
+   buffer := gtk_text_view_get_buffer(textview);
+
+   mark := gtk_text_buffer_get_mark(buffer, "end");
+   gtk_text_buffer_get_iter_at_mark(buffer, iter, mark);
+
+   MakeSpaces(endCount, spaces);
+   INC(endCount);
+   gtk_text_buffer_insert(buffer, iter, "\n", -1);
+   gtk_text_buffer_insert(buffer, iter, spaces, -1);
+   BuildScrollText(ScrollToEndPrefix, endCount, text);
+   gtk_text_buffer_insert(buffer, iter, text, -1);
+
+   gtk_text_view_scroll_to_mark(textview, mark, 0.0, 0, 0.0, 0.0);
+
+   IF endCount > 150 THEN
+      endCount := 0
+   END;
+
+   RETURN GSourceContinue
+END ScrollToEnd;
+
+PROCEDURE ScrollToBottom (textview: ADDRESS) : INTEGER;
+VAR
+   buffer, mark: ADDRESS;
+   iter: GtkTextIter;
+   spaces, text: ARRAY [0..255] OF CHAR;
+BEGIN
+   buffer := gtk_text_view_get_buffer(textview);
+
+   gtk_text_buffer_get_end_iter(buffer, iter);
+
+   MakeSpaces(bottomCount, spaces);
+   INC(bottomCount);
+   gtk_text_buffer_insert(buffer, iter, "\n", -1);
+   gtk_text_buffer_insert(buffer, iter, spaces, -1);
+   BuildScrollText(ScrollToBottomPrefix, bottomCount, text);
+   gtk_text_buffer_insert(buffer, iter, text, -1);
+
+   (* gtk_text_iter_set_line_offset() and gtk_text_buffer_move_mark()
+      are not bound, so the "scroll" mark stays at its original spot. *)
+   mark := gtk_text_buffer_get_mark(buffer, "scroll");
+   gtk_text_view_scroll_to_mark(textview, mark, 0.0, 0, 0.0, 0.0);
+
+   IF bottomCount > 40 THEN
+      bottomCount := 0
+   END;
+
+   RETURN GSourceContinue
+END ScrollToBottom;
+
+PROCEDURE SetupScroll (textview: ADDRESS; toEnd: BOOLEAN) : guint;
+VAR
+   buffer, mark: ADDRESS;
+   iter: GtkTextIter;
+BEGIN
+   buffer := gtk_text_view_get_buffer(textview);
+   gtk_text_buffer_get_end_iter(buffer, iter);
+
+   IF toEnd THEN
+      mark := gtk_text_buffer_create_mark(buffer, "end", iter, 0);
+      RETURN g_timeout_add(50, ScrollToEnd, textview)
+   ELSE
+      mark := gtk_text_buffer_create_mark(buffer, "scroll", iter, 1);
+      RETURN g_timeout_add(100, ScrollToBottom, textview)
+   END
+END SetupScroll;
+
+(* void remove_timeout(GtkWidget *window, gpointer timeout) *)
+PROCEDURE RemoveTimeout (window: ADDRESS; data: ADDRESS);
+BEGIN
+   g_source_remove(CAST(guint, data))
+END RemoveTimeout;
+
+PROCEDURE CreateTextView (hbox: ADDRESS; toEnd: BOOLEAN);
+VAR
+   swindow, textview: ADDRESS;
+   timeout: guint;
+BEGIN
+   swindow := gtk_scrolled_window_new();
+   gtk_widget_set_hexpand(swindow, 1);
+   gtk_box_append(hbox, swindow);
+
+   textview := gtk_text_view_new();
+   gtk_scrolled_window_set_child(swindow, textview);
+
+   timeout := SetupScroll(textview, toEnd);
+
+   (* Remove the timeout when the view goes away, so it cannot scroll a
+      destroyed widget. *)
+   g_signal_connect_data(textview, "destroy", ADR(RemoveTimeout),
+                         CAST(ADDRESS, timeout), NIL, GConnectDefault)
+END CreateTextView;
+
+PROCEDURE DoTextScroll (doWidget: ADDRESS) : ADDRESS;
+VAR
+   hbox: ADDRESS;
+BEGIN
+   IF textScrollWindow = NIL THEN
+      textScrollWindow := gtk_window_new();
+      gtk_window_set_title(textScrollWindow, "Automatic Scrolling");
+      gtk_window_set_default_size(textScrollWindow, 600, 400);
+
+      hbox := gtk_box_new(GtkHorizontal, 6);
+      gtk_window_set_child(textScrollWindow, hbox);
+
+      CreateTextView(hbox, TRUE);
+      CreateTextView(hbox, FALSE)
+   END;
+
+   IF gtk_widget_get_visible(textScrollWindow) = 0 THEN
+      gtk_widget_set_visible(textScrollWindow, 1)
+   ELSE
+      gtk_window_destroy(textScrollWindow);
+      textScrollWindow := NIL
+   END;
+   RETURN textScrollWindow
+END DoTextScroll;
+
+(* ================================================================== *)
+(* textundo                                                           *)
+(* ================================================================== *)
+
+PROCEDURE DoTextUndo (doWidget: ADDRESS) : ADDRESS;
+VAR
+   view, sw, buffer: ADDRESS;
+   iter: GtkTextIter;
+BEGIN
+   IF textUndoWindow = NIL THEN
+      textUndoWindow := gtk_window_new();
+      gtk_window_set_default_size(textUndoWindow, 330, 330);
+      gtk_window_set_resizable(textUndoWindow, 0);
+      gtk_window_set_title(textUndoWindow, "Undo and Redo");
+
+      view := gtk_text_view_new();
+      gtk_text_view_set_wrap_mode(view, GtkWrapWord);
+      gtk_text_view_set_left_margin(view, 20);
+      gtk_text_view_set_right_margin(view, 20);
+      gtk_text_view_set_top_margin(view, 20);
+      gtk_text_view_set_bottom_margin(view, 20);
+
+      buffer := gtk_text_view_get_buffer(view);
+      (* gtk_text_buffer_set_enable_undo() is not bound; the buffer keeps
+         its default undo support, so Ctrl+Z / Ctrl+Y still work. *)
+
+      gtk_text_buffer_get_start_iter(buffer, iter);
+      gtk_text_buffer_insert(buffer, iter, UndoText, -1);
+
+      sw := gtk_scrolled_window_new();
+      gtk_scrolled_window_set_policy(sw, GtkPolicyAutomatic,
+                                     GtkPolicyAutomatic);
+      gtk_window_set_child(textUndoWindow, sw);
+      gtk_scrolled_window_set_child(sw, view)
+   END;
+
+   IF gtk_widget_get_visible(textUndoWindow) = 0 THEN
+      gtk_widget_set_visible(textUndoWindow, 1)
+   ELSE
+      gtk_window_destroy(textUndoWindow);
+      textUndoWindow := NIL
+   END;
+   RETURN textUndoWindow
+END DoTextUndo;
+
+BEGIN
+   readMoreWindow := NIL;
+   rotatedTextWindow := NIL;
+   textMaskWindow := NIL;
+   textScrollWindow := NIL;
+   textUndoWindow := NIL;
+   endCount := 0;
+   bottomCount := 0
+END DemosText.

+ 8 - 0
demos/DemosWindows.def

@@ -0,0 +1,8 @@
+DEFINITION MODULE DemosWindows ;
+FROM SYSTEM IMPORT ADDRESS;
+EXPORT UNQUALIFIED DoHeaderBar, DoInfoBar, DoTabs, DoAssistant;
+PROCEDURE DoHeaderBar (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoInfoBar (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoTabs (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoAssistant (doWidget: ADDRESS) : ADDRESS;
+END DemosWindows.

+ 465 - 0
demos/DemosWindows.mod

@@ -0,0 +1,465 @@
+IMPLEMENTATION MODULE DemosWindows ;
+
+(*
+   m2-GTK4 - window-oriented gtk-demo ports, following the gtk-demo
+   pattern used by DemosBasic/DemoImpl: each Do* procedure creates its
+   window once (module VAR), then toggles visibility - showing it, or
+   destroying it and resetting the handle to NIL - and returns it.
+
+   Originals: gtk/demos/gtk-demo/{headerbar,infobar,tabs,assistant}.c
+
+   Simplifications forced by the curated bindings under src/:
+     * GtkInfoBar is not bound, so DoInfoBar lays the messages out as
+       plain labels plus toggle buttons instead of real info bars.
+     * Pango tab arrays and gtk_text_view_set_tabs() are not bound, so
+       DoTabs omits the tab-stop setup (the sample text is unchanged).
+     * GtkAccessible and g_object_bind_property are not bound; the
+       accessibility annotations and the toggle/info-bar bindings are
+       dropped.
+     * gtk_widget_set_display() / g_object_add_weak_pointer() are not
+       used, matching the existing demos (display is inherited, and the
+       module VAR is reset to NIL explicitly).
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM GtkWindow IMPORT gtk_window_new, gtk_window_set_title,
+   gtk_window_set_default_size, gtk_window_set_resizable,
+   gtk_window_set_child, gtk_window_destroy, gtk_window_set_titlebar;
+FROM GtkWidget IMPORT gtk_widget_set_visible, gtk_widget_get_visible,
+   gtk_widget_set_margin_start, gtk_widget_set_margin_end,
+   gtk_widget_set_margin_top, gtk_widget_set_margin_bottom,
+   gtk_widget_set_halign, gtk_widget_set_valign, gtk_widget_set_hexpand,
+   gtk_widget_add_css_class;
+FROM GtkEnums IMPORT GtkHorizontal, GtkVertical, GtkAlignCenter,
+   GtkAlignFill, GtkWrapWord;
+FROM GtkHeaderBar IMPORT gtk_header_bar_new, gtk_header_bar_pack_start,
+   gtk_header_bar_pack_end;
+FROM GtkBox IMPORT gtk_box_new, gtk_box_append;
+FROM GtkImage IMPORT gtk_image_new_from_icon_name;
+FROM GtkButton IMPORT gtk_button_new, gtk_button_set_child;
+FROM GtkSwitch IMPORT gtk_switch_new;
+FROM GtkTextView IMPORT gtk_text_view_new, gtk_text_view_get_buffer,
+   gtk_text_view_set_wrap_mode, gtk_text_view_set_top_margin,
+   gtk_text_view_set_bottom_margin, gtk_text_view_set_left_margin,
+   gtk_text_view_set_right_margin;
+FROM GtkTextBuffer IMPORT gtk_text_buffer_set_text;
+FROM GtkScrolledWindow IMPORT gtk_scrolled_window_new,
+   gtk_scrolled_window_set_child, gtk_scrolled_window_set_policy,
+   GtkPolicyNever, GtkPolicyAutomatic;
+FROM GtkLabel IMPORT gtk_label_new, gtk_label_set_wrap;
+FROM GtkToggleButton IMPORT gtk_toggle_button_new_with_label;
+FROM GtkFrame IMPORT gtk_frame_new, gtk_frame_set_child;
+FROM GtkAssistant IMPORT gtk_assistant_new, gtk_assistant_append_page,
+   gtk_assistant_set_page_title, gtk_assistant_set_page_type,
+   gtk_assistant_set_page_complete, gtk_assistant_get_current_page,
+   gtk_assistant_get_n_pages, gtk_assistant_get_nth_page,
+   gtk_assistant_commit, AssistantPageIntro, AssistantPageConfirm,
+   AssistantPageProgress;
+FROM GtkEntry IMPORT gtk_entry_new, gtk_entry_set_activates_default;
+FROM GtkEditable IMPORT gtk_editable_get_text;
+FROM GtkCheckButton IMPORT gtk_check_button_new_with_label;
+FROM GtkProgressBar IMPORT gtk_progress_bar_new,
+   gtk_progress_bar_get_fraction, gtk_progress_bar_set_fraction;
+FROM GtkClosures IMPORT Connect2, Connect3;
+FROM GtkUtils IMPORT StrAppend, IntToStr, CStrToM2;
+FROM GLib IMPORT g_timeout_add, GSourceContinue, GSourceRemove;
+
+(* ------------------------------------------------------------------ *)
+(* Header Bar  (headerbar.c)                                          *)
+(* ------------------------------------------------------------------ *)
+
+VAR
+   headerBarWindow: ADDRESS;
+
+(* gtk_button_new_from_icon_name() is not bound: build the equivalent
+   from an empty button and an icon-name GtkImage child. *)
+PROCEDURE IconButton (name: ARRAY OF CHAR) : ADDRESS;
+VAR
+   button: ADDRESS;
+BEGIN
+   button := gtk_button_new();
+   gtk_button_set_child(button, gtk_image_new_from_icon_name(name));
+   RETURN button
+END IconButton;
+
+PROCEDURE DoHeaderBar (doWidget: ADDRESS) : ADDRESS;
+VAR
+   header, button, box, content: ADDRESS;
+BEGIN
+   IF headerBarWindow = NIL THEN
+      headerBarWindow := gtk_window_new();
+      gtk_window_set_title(headerBarWindow, "Welcome to the Hotel California");
+      gtk_window_set_default_size(headerBarWindow, 600, 400);
+
+      header := gtk_header_bar_new();
+
+      button := IconButton("mail-send-receive-symbolic");
+      gtk_header_bar_pack_end(header, button);
+
+      box := gtk_box_new(GtkHorizontal, 0);
+      gtk_widget_add_css_class(box, "linked");
+      button := IconButton("go-previous-symbolic");
+      gtk_box_append(box, button);
+      button := IconButton("go-next-symbolic");
+      gtk_box_append(box, button);
+
+      gtk_header_bar_pack_start(header, box);
+
+      button := gtk_switch_new();
+      gtk_header_bar_pack_start(header, button);
+
+      gtk_window_set_titlebar(headerBarWindow, header);
+
+      content := gtk_text_view_new();
+      gtk_window_set_child(headerBarWindow, content)
+   END;
+
+   IF gtk_widget_get_visible(headerBarWindow) = 0 THEN
+      gtk_widget_set_visible(headerBarWindow, 1)
+   ELSE
+      gtk_window_destroy(headerBarWindow);
+      headerBarWindow := NIL
+   END;
+   RETURN headerBarWindow
+END DoHeaderBar;
+
+(* ------------------------------------------------------------------ *)
+(* Info Bars  (infobar.c)                                             *)
+(* ------------------------------------------------------------------ *)
+
+VAR
+   infoBarWindow: ADDRESS;
+
+(* GtkInfoBar is not bound by m2-GTK4, so this simplified port shows the
+   five message types as wrapped labels, with a linked row of toggle
+   buttons underneath, all inside a titled frame. *)
+PROCEDURE DoInfoBar (doWidget: ADDRESS) : ADDRESS;
+VAR
+   vbox, frame, actions, label, button: ADDRESS;
+BEGIN
+   IF infoBarWindow = NIL THEN
+      actions := gtk_box_new(GtkHorizontal, 0);
+      gtk_widget_add_css_class(actions, "linked");
+
+      infoBarWindow := gtk_window_new();
+      gtk_window_set_title(infoBarWindow, "Info Bars");
+      gtk_window_set_resizable(infoBarWindow, 0);
+
+      vbox := gtk_box_new(GtkVertical, 0);
+      gtk_widget_set_margin_start(vbox, 8);
+      gtk_widget_set_margin_end(vbox, 8);
+      gtk_widget_set_margin_top(vbox, 8);
+      gtk_widget_set_margin_bottom(vbox, 8);
+      gtk_window_set_child(infoBarWindow, vbox);
+
+      label := gtk_label_new("This is an info bar with message type GTK_MESSAGE_INFO");
+      gtk_label_set_wrap(label, 1);
+      gtk_box_append(vbox, label);
+      button := gtk_toggle_button_new_with_label("Message");
+      gtk_box_append(actions, button);
+
+      label := gtk_label_new("This is an info bar with message type GTK_MESSAGE_WARNING");
+      gtk_label_set_wrap(label, 1);
+      gtk_box_append(vbox, label);
+      button := gtk_toggle_button_new_with_label("Warning");
+      gtk_box_append(actions, button);
+
+      label := gtk_label_new("This is an info bar with message type GTK_MESSAGE_QUESTION");
+      gtk_label_set_wrap(label, 1);
+      gtk_box_append(vbox, label);
+      button := gtk_toggle_button_new_with_label("Question");
+      gtk_box_append(actions, button);
+
+      label := gtk_label_new("This is an info bar with message type GTK_MESSAGE_ERROR");
+      gtk_label_set_wrap(label, 1);
+      gtk_box_append(vbox, label);
+      button := gtk_toggle_button_new_with_label("Error");
+      gtk_box_append(actions, button);
+
+      label := gtk_label_new("This is an info bar with message type GTK_MESSAGE_OTHER");
+      gtk_label_set_wrap(label, 1);
+      gtk_box_append(vbox, label);
+      button := gtk_toggle_button_new_with_label("Other");
+      gtk_box_append(actions, button);
+
+      frame := gtk_frame_new("An example of different info bars");
+      gtk_widget_set_margin_top(frame, 8);
+      gtk_widget_set_margin_bottom(frame, 8);
+      gtk_box_append(vbox, frame);
+
+      gtk_widget_set_halign(actions, GtkAlignCenter);
+      gtk_widget_set_margin_start(actions, 8);
+      gtk_widget_set_margin_end(actions, 8);
+      gtk_widget_set_margin_top(actions, 8);
+      gtk_widget_set_margin_bottom(actions, 8);
+      gtk_frame_set_child(frame, actions)
+   END;
+
+   IF gtk_widget_get_visible(infoBarWindow) = 0 THEN
+      gtk_widget_set_visible(infoBarWindow, 1)
+   ELSE
+      gtk_window_destroy(infoBarWindow);
+      infoBarWindow := NIL
+   END;
+   RETURN infoBarWindow
+END DoInfoBar;
+
+(* ------------------------------------------------------------------ *)
+(* Text View/Tabs  (tabs.c)                                           *)
+(* ------------------------------------------------------------------ *)
+
+VAR
+   tabsWindow: ADDRESS;
+
+(* Pango tab arrays and gtk_text_view_set_tabs() are not bound; the tab
+   stops are therefore omitted, but the sample text is preserved (built
+   with explicit TAB/LF characters so no escape sequences are needed). *)
+PROCEDURE SetTabsText (buffer: ADDRESS);
+VAR
+   text: ARRAY [0..127] OF CHAR;
+   tab, nl: ARRAY [0..1] OF CHAR;
+BEGIN
+   tab[0] := CHR(9);
+   tab[1] := 0C;
+   nl[0] := CHR(10);
+   nl[1] := 0C;
+
+   text[0] := 0C;
+   StrAppend(text, "one");   StrAppend(text, tab);
+   StrAppend(text, "2.0");   StrAppend(text, tab);
+   StrAppend(text, "three"); StrAppend(text, nl);
+   StrAppend(text, "four");  StrAppend(text, tab);
+   StrAppend(text, "5.555"); StrAppend(text, tab);
+   StrAppend(text, "six");   StrAppend(text, nl);
+   StrAppend(text, "seven"); StrAppend(text, tab);
+   StrAppend(text, "88.88"); StrAppend(text, tab);
+   StrAppend(text, "nine");
+
+   gtk_text_buffer_set_text(buffer, text, -1)
+END SetTabsText;
+
+PROCEDURE DoTabs (doWidget: ADDRESS) : ADDRESS;
+VAR
+   view, sw, buffer: ADDRESS;
+BEGIN
+   IF tabsWindow = NIL THEN
+      tabsWindow := gtk_window_new();
+      gtk_window_set_title(tabsWindow, "Tabs");
+      gtk_window_set_default_size(tabsWindow, 330, 130);
+      gtk_window_set_resizable(tabsWindow, 0);
+
+      view := gtk_text_view_new();
+      gtk_text_view_set_wrap_mode(view, GtkWrapWord);
+      gtk_text_view_set_top_margin(view, 20);
+      gtk_text_view_set_bottom_margin(view, 20);
+      gtk_text_view_set_left_margin(view, 20);
+      gtk_text_view_set_right_margin(view, 20);
+
+      buffer := gtk_text_view_get_buffer(view);
+      SetTabsText(buffer);
+
+      sw := gtk_scrolled_window_new();
+      gtk_scrolled_window_set_policy(sw, GtkPolicyNever, GtkPolicyAutomatic);
+      gtk_window_set_child(tabsWindow, sw);
+      gtk_scrolled_window_set_child(sw, view)
+   END;
+
+   IF gtk_widget_get_visible(tabsWindow) = 0 THEN
+      gtk_widget_set_visible(tabsWindow, 1)
+   ELSE
+      gtk_window_destroy(tabsWindow);
+      tabsWindow := NIL
+   END;
+   RETURN tabsWindow
+END DoTabs;
+
+(* ------------------------------------------------------------------ *)
+(* Assistant  (assistant.c)                                           *)
+(* ------------------------------------------------------------------ *)
+
+VAR
+   assistantWindow, progressBar: ADDRESS;
+
+(* gboolean apply_changes_gradually(gpointer data) *)
+PROCEDURE ApplyChangesGradually (data: ADDRESS) : INTEGER;
+VAR
+   fraction: REAL;
+BEGIN
+   fraction := gtk_progress_bar_get_fraction(progressBar);
+   fraction := fraction + 0.05;
+
+   IF fraction < 1.0 THEN
+      gtk_progress_bar_set_fraction(progressBar, fraction);
+      RETURN GSourceContinue
+   ELSE
+      gtk_window_destroy(data);
+      RETURN GSourceRemove
+   END
+END ApplyChangesGradually;
+
+(* void on_assistant_apply(GtkWidget *widget, gpointer data) *)
+PROCEDURE OnAssistantApply (widget: ADDRESS; data: ADDRESS);
+BEGIN
+   g_timeout_add(100, ApplyChangesGradually, widget)
+END OnAssistantApply;
+
+(* void on_assistant_close_cancel(GtkWidget *widget, gpointer data) *)
+PROCEDURE OnAssistantCloseCancel (widget: ADDRESS; data: ADDRESS);
+BEGIN
+   gtk_window_destroy(widget)
+END OnAssistantCloseCancel;
+
+(* gint format "Sample assistant (%d of %d)" without g_strdup_printf. *)
+PROCEDURE MakeAssistantTitle (current, total: INTEGER;
+                              VAR title: ARRAY OF CHAR);
+VAR
+   num: ARRAY [0..15] OF CHAR;
+BEGIN
+   title[0] := 0C;
+   StrAppend(title, "Sample assistant (");
+   IntToStr(current, num);
+   StrAppend(title, num);
+   StrAppend(title, " of ");
+   IntToStr(total, num);
+   StrAppend(title, num);
+   StrAppend(title, ")")
+END MakeAssistantTitle;
+
+(* void on_assistant_prepare(GtkWidget *widget, GtkWidget *page, gpointer data) *)
+PROCEDURE OnAssistantPrepare (widget: ADDRESS; page: ADDRESS; data: ADDRESS);
+VAR
+   currentPage, nPages: INTEGER;
+   title: ARRAY [0..63] OF CHAR;
+BEGIN
+   currentPage := gtk_assistant_get_current_page(widget);
+   nPages := gtk_assistant_get_n_pages(widget);
+
+   MakeAssistantTitle(currentPage + 1, nPages, title);
+   gtk_window_set_title(widget, title);
+
+   (* The fourth page (zero-based) is the progress page: commit once the
+      user has clicked Apply to reach it. *)
+   IF currentPage = 3 THEN
+      gtk_assistant_commit(widget)
+   END
+END OnAssistantPrepare;
+
+(* void on_entry_changed(GtkWidget *widget, gpointer data) *)
+PROCEDURE OnEntryChanged (widget: ADDRESS; data: ADDRESS);
+VAR
+   currentPage: ADDRESS;
+   pageNumber: INTEGER;
+   text: ARRAY [0..1] OF CHAR;
+BEGIN
+   pageNumber := gtk_assistant_get_current_page(data);
+   currentPage := gtk_assistant_get_nth_page(data, pageNumber);
+   CStrToM2(gtk_editable_get_text(widget), text);
+
+   IF text[0] # 0C THEN
+      gtk_assistant_set_page_complete(data, currentPage, 1)
+   ELSE
+      gtk_assistant_set_page_complete(data, currentPage, 0)
+   END
+END OnEntryChanged;
+
+PROCEDURE CreatePage1 (assistant: ADDRESS);
+VAR
+   box, label, entry: ADDRESS;
+BEGIN
+   box := gtk_box_new(GtkHorizontal, 12);
+   gtk_widget_set_margin_start(box, 12);
+   gtk_widget_set_margin_end(box, 12);
+   gtk_widget_set_margin_top(box, 12);
+   gtk_widget_set_margin_bottom(box, 12);
+
+   label := gtk_label_new("You must fill out this entry to continue:");
+   gtk_box_append(box, label);
+
+   entry := gtk_entry_new();
+   gtk_entry_set_activates_default(entry, 1);
+   gtk_widget_set_valign(entry, GtkAlignCenter);
+   gtk_box_append(box, entry);
+   Connect2(entry, "changed", OnEntryChanged, assistant);
+
+   gtk_assistant_append_page(assistant, box);
+   gtk_assistant_set_page_title(assistant, box, "Page 1");
+   gtk_assistant_set_page_type(assistant, box, AssistantPageIntro)
+END CreatePage1;
+
+PROCEDURE CreatePage2 (assistant: ADDRESS);
+VAR
+   box, checkbutton: ADDRESS;
+BEGIN
+   box := gtk_box_new(GtkHorizontal, 12);
+   gtk_widget_set_margin_start(box, 12);
+   gtk_widget_set_margin_end(box, 12);
+   gtk_widget_set_margin_top(box, 12);
+   gtk_widget_set_margin_bottom(box, 12);
+
+   checkbutton := gtk_check_button_new_with_label
+      ("This is optional data, you may continue even if you do not check this");
+   gtk_widget_set_valign(checkbutton, GtkAlignCenter);
+   gtk_box_append(box, checkbutton);
+
+   gtk_assistant_append_page(assistant, box);
+   gtk_assistant_set_page_complete(assistant, box, 1);
+   gtk_assistant_set_page_title(assistant, box, "Page 2")
+END CreatePage2;
+
+PROCEDURE CreatePage3 (assistant: ADDRESS);
+VAR
+   label: ADDRESS;
+BEGIN
+   label := gtk_label_new("This is a confirmation page, press 'Apply' to apply changes");
+
+   gtk_assistant_append_page(assistant, label);
+   gtk_assistant_set_page_type(assistant, label, AssistantPageConfirm);
+   gtk_assistant_set_page_complete(assistant, label, 1);
+   gtk_assistant_set_page_title(assistant, label, "Confirmation")
+END CreatePage3;
+
+PROCEDURE CreatePage4 (assistant: ADDRESS);
+BEGIN
+   progressBar := gtk_progress_bar_new();
+   gtk_widget_set_halign(progressBar, GtkAlignFill);
+   gtk_widget_set_valign(progressBar, GtkAlignCenter);
+   gtk_widget_set_hexpand(progressBar, 1);
+   gtk_widget_set_margin_start(progressBar, 40);
+   gtk_widget_set_margin_end(progressBar, 40);
+
+   gtk_assistant_append_page(assistant, progressBar);
+   gtk_assistant_set_page_type(assistant, progressBar, AssistantPageProgress);
+   gtk_assistant_set_page_title(assistant, progressBar, "Applying changes");
+
+   (* Prevents the assistant from being closed while we are "busy". *)
+   gtk_assistant_set_page_complete(assistant, progressBar, 0)
+END CreatePage4;
+
+PROCEDURE DoAssistant (doWidget: ADDRESS) : ADDRESS;
+BEGIN
+   IF assistantWindow = NIL THEN
+      assistantWindow := gtk_assistant_new();
+
+      gtk_window_set_default_size(assistantWindow, -1, 300);
+
+      CreatePage1(assistantWindow);
+      CreatePage2(assistantWindow);
+      CreatePage3(assistantWindow);
+      CreatePage4(assistantWindow);
+
+      Connect2(assistantWindow, "cancel", OnAssistantCloseCancel, NIL);
+      Connect2(assistantWindow, "close", OnAssistantCloseCancel, NIL);
+      Connect2(assistantWindow, "apply", OnAssistantApply, NIL);
+      Connect3(assistantWindow, "prepare", OnAssistantPrepare, NIL)
+   END;
+
+   IF gtk_widget_get_visible(assistantWindow) = 0 THEN
+      gtk_widget_set_visible(assistantWindow, 1)
+   ELSE
+      gtk_window_destroy(assistantWindow);
+      assistantWindow := NIL
+   END;
+   RETURN assistantWindow
+END DoAssistant;
+
+END DemosWindows.

+ 21 - 1
demos/GtkDemo.mod

@@ -2,6 +2,10 @@ IMPLEMENTATION MODULE GtkDemo ;
 
 FROM DemoImpl IMPORT DoLinks, DoListBox, DoFlowBox, DoExpander, DoCss;
 FROM DemosBasic IMPORT DoSpinner, DoPanes, DoOverlay;
+FROM DemosLayout IMPORT DoSizeGroup, DoFixed2, DoLayoutManager;
+FROM DemosText IMPORT DoReadMore, DoRotatedText, DoTextMask, DoTextScroll, DoTextUndo;
+FROM DemosWindows IMPORT DoHeaderBar, DoInfoBar, DoTabs, DoAssistant;
+FROM DemosInput IMPORT DoPasswordEntry, DoSearchEntry, DoTaggedEntry, DoShortcutTriggers;
 FROM SYSTEM IMPORT ADDRESS;
 
 TYPE
@@ -92,5 +96,21 @@ BEGIN
    Register("css_basics", DoCss);
    Register("spinner", DoSpinner);
    Register("panes", DoPanes);
-   Register("overlay", DoOverlay)
+   Register("overlay", DoOverlay);
+   Register("sizegroup", DoSizeGroup);
+   Register("fixed2", DoFixed2);
+   Register("layoutmanager", DoLayoutManager);
+   Register("read_more", DoReadMore);
+   Register("rotated_text", DoRotatedText);
+   Register("textmask", DoTextMask);
+   Register("textscroll", DoTextScroll);
+   Register("textundo", DoTextUndo);
+   Register("headerbar", DoHeaderBar);
+   Register("infobar", DoInfoBar);
+   Register("tabs", DoTabs);
+   Register("assistant", DoAssistant);
+   Register("password_entry", DoPasswordEntry);
+   Register("search_entry", DoSearchEntry);
+   Register("tagged_entry", DoTaggedEntry);
+   Register("shortcut_triggers", DoShortcutTriggers)
 END GtkDemo.

+ 9 - 1
tests/run_tests.sh

@@ -59,6 +59,14 @@ if [ -d "$ROOT/demos" ]; then
   # shellcheck disable=SC2086
   $GM2 -I"$ROOT/src" -I"$ROOT/lib" -I"$ROOT/demos" $GM2FLAGS -c "$ROOT/demos/DemosBasic.mod" -o "$OBJ/DemosBasic.o"
   # shellcheck disable=SC2086
+  $GM2 -I"$ROOT/src" -I"$ROOT/lib" -I"$ROOT/demos" $GM2FLAGS -c "$ROOT/demos/DemosLayout.mod" -o "$OBJ/DemosLayout.o"
+  # shellcheck disable=SC2086
+  $GM2 -I"$ROOT/src" -I"$ROOT/lib" -I"$ROOT/demos" $GM2FLAGS -c "$ROOT/demos/DemosText.mod" -o "$OBJ/DemosText.o"
+  # shellcheck disable=SC2086
+  $GM2 -I"$ROOT/src" -I"$ROOT/lib" -I"$ROOT/demos" $GM2FLAGS -c "$ROOT/demos/DemosWindows.mod" -o "$OBJ/DemosWindows.o"
+  # shellcheck disable=SC2086
+  $GM2 -I"$ROOT/src" -I"$ROOT/lib" -I"$ROOT/demos" $GM2FLAGS -c "$ROOT/demos/DemosInput.mod" -o "$OBJ/DemosInput.o"
+  # shellcheck disable=SC2086
   $GM2 -I"$ROOT/src" -I"$ROOT/lib" -I"$ROOT/demos" $GM2FLAGS -c "$ROOT/demos/GtkDemo.mod" -o "$OBJ/GtkDemo.o"
 fi
 
@@ -75,7 +83,7 @@ pass=0; fail=0; skip=0
 for t in $TESTS; do
   echo "== $t =="
   DEMO_OBJS=""
-  case "$t" in test_demos) DEMO_OBJS="$OBJ/DemoCatalog.o $OBJ/DemoImpl.o $OBJ/DemosBasic.o $OBJ/GtkDemo.o" ;; esac
+  case "$t" in test_demos) DEMO_OBJS="$OBJ/DemoCatalog.o $OBJ/DemoImpl.o $OBJ/DemosBasic.o $OBJ/DemosLayout.o $OBJ/DemosText.o $OBJ/DemosWindows.o $OBJ/DemosInput.o $OBJ/GtkDemo.o" ;; esac
   # shellcheck disable=SC2086
   $GM2 -I"$ROOT/src" -I"$ROOT/lib" -I"$ROOT/demos" $GM2FLAGS $CFLAGS \
        "$ROOT/tests/$t.mod" "$OBJ/GtkUtils.o" "$OBJ/GtkClosures.o" \

+ 13 - 0
tests/test_demos.mod

@@ -78,6 +78,19 @@ BEGIN
    BuildAndClose("spinner");
    BuildAndClose("panes");
    BuildAndClose("overlay");
+   BuildAndClose("sizegroup");
+   BuildAndClose("fixed2");
+   BuildAndClose("layoutmanager");
+   BuildAndClose("read_more");
+   BuildAndClose("rotated_text");
+   BuildAndClose("textmask");
+   BuildAndClose("textundo");
+   BuildAndClose("headerbar");
+   BuildAndClose("infobar");
+   BuildAndClose("tabs");
+   BuildAndClose("password_entry");
+   BuildAndClose("search_entry");
+   BuildAndClose("tagged_entry");
 
    IF failures = 0 THEN
       printf("test_demos: PASS\n")