Parcourir la source

feat(demos): demo batch 3 - choosers and canvas

Port 8 more gtk-demo demos:

- DemosChooser: combobox, entry_completion, entry_undo, dialog
- DemosCanvas:  drawingarea, gestures, mask, snapping

Also add the GtkWidget.queue_draw binding (needed by the interactive
drawing demos) and register the new demos.

Deprecated model-based widgets (GtkComboBox/GtkCellRenderer/GtkTreeModel)
and GtkDialog are not bound; those demos use their modern equivalents
(GtkDropDown, AdwMessageDialog) as noted in the module headers.

Test suite: 45 passed.
Eric Streit il y a 2 jours
Parent
commit
2605deaeb3
9 fichiers modifiés avec 1347 ajouts et 3 suppressions
  1. 8 1
      Makefile
  2. 8 0
      demos/DemosCanvas.def
  3. 756 0
      demos/DemosCanvas.mod
  4. 9 0
      demos/DemosChooser.def
  5. 541 0
      demos/DemosChooser.mod
  6. 11 1
      demos/GtkDemo.mod
  7. 4 0
      src/GtkWidget.def
  8. 5 1
      tests/run_tests.sh
  9. 5 0
      tests/test_demos.mod

+ 8 - 1
Makefile

@@ -108,12 +108,19 @@ $(OBJDIR)/DemosWindows.o: $(DEMO_DIR)/DemosWindows.mod $(DEMO_DIR)/DemosWindows.
 $(OBJDIR)/DemosInput.o: $(DEMO_DIR)/DemosInput.mod $(DEMO_DIR)/DemosInput.def | $(OBJDIR)
 	$(GM2) $(INCLUDES) -I$(DEMO_DIR) $(GM2FLAGS) -c $< -o $@
 
+$(OBJDIR)/DemosChooser.o: $(DEMO_DIR)/DemosChooser.mod $(DEMO_DIR)/DemosChooser.def | $(OBJDIR)
+	$(GM2) $(INCLUDES) -I$(DEMO_DIR) $(GM2FLAGS) -c $< -o $@
+
+$(OBJDIR)/DemosCanvas.o: $(DEMO_DIR)/DemosCanvas.mod $(DEMO_DIR)/DemosCanvas.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)/DemosLayout.o $(OBJDIR)/DemosText.o \
-             $(OBJDIR)/DemosWindows.o $(OBJDIR)/DemosInput.o $(OBJDIR)/GtkDemo.o
+             $(OBJDIR)/DemosWindows.o $(OBJDIR)/DemosInput.o \
+             $(OBJDIR)/DemosChooser.o $(OBJDIR)/DemosCanvas.o $(OBJDIR)/GtkDemo.o
 
 demos: $(BLD)/demos $(OBJS) $(DEMO_OBJS) $(BLD)/demos/m2gtkdemo
 

+ 8 - 0
demos/DemosCanvas.def

@@ -0,0 +1,8 @@
+DEFINITION MODULE DemosCanvas ;
+FROM SYSTEM IMPORT ADDRESS;
+EXPORT UNQUALIFIED DoDrawingArea, DoGestures, DoMask, DoSnapping;
+PROCEDURE DoDrawingArea (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoGestures (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoMask (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoSnapping (doWidget: ADDRESS) : ADDRESS;
+END DemosCanvas.

+ 756 - 0
demos/DemosCanvas.mod

@@ -0,0 +1,756 @@
+IMPLEMENTATION MODULE DemosCanvas ;
+
+(*
+   m2-GTK4 - the drawing/canvas gtk-demo demos:
+     drawingarea.c -> DoDrawingArea
+     gestures.c    -> DoGestures
+     mask.c        -> DoMask
+     snapping.c    -> DoSnapping
+
+   The gtk-demo contract is: on the first call build the window and store
+   it in a module-level variable; on later calls toggle the window's
+   visibility, destroying it (and clearing the variable) when it is
+   already visible.  Return the window, or NIL once destroyed.
+
+   Only bindings under src/ are used, so a few C facilities are missing and
+   those spots are simplified (each marked "SIMPLIFIED" below):
+     * gtk_widget_queue_draw() is not bound.  Gesture/scribble state is
+       still recorded, but the drawing areas cannot be repainted until GTK
+       redraws them for another reason (e.g. an expose or resize).
+     * drawingarea's groups_draw uses cairo_surface_create_similar(),
+       cairo_get_target() and CAIRO_OPERATOR_DEST_OUT/ADD, none of which
+       are bound; the knockout/composite is approximated by a black disc
+       with the three coloured sub-discs drawn on top.
+     * cairo_image_surface_get_width()/get_height() are not bound, so the
+       scribble surface is (re)created on the "resize" signal only.
+     * gtk_window_set_display(), the accessible-role/an accessible-relation
+       setup and g_object_new(drawing area) are not bound; a plain
+       gtk_drawing_area_new() is used.
+     * gestures' rotate/zoom visualisation reads the gesture deltas from
+       the signal arguments instead of gtk_gesture_*_get_*_delta(), and uses
+       the widget centre instead of gtk_gesture_get_bounding_box_center();
+       the touchpad 3-finger swipe and gtk_gesture_is_recognized() are not
+       reproduced.
+     * mask's Demo4Widget is a custom GskMaskNode widget with a Pango
+       layout and a frame-clock tick callback; subclassing, GskMaskNode and
+       gtk_widget_add_tick_callback() are not bound, so the "123" text is
+       filled with a moving 8-stop gradient via Cairo instead.
+     * snapping's GtkTiler is a custom GdkPaintable; a GtkDrawingArea is
+       used instead, drawing the QR code on a pixel-snapped 43x43 grid with
+       linear gradients between differing neighbours (the conic/radial
+       corner cases are left white).
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM GtkWindow IMPORT gtk_window_new, gtk_window_set_title,
+   gtk_window_set_default_size, 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_vexpand, gtk_widget_set_hexpand,
+   gtk_widget_add_css_class, gtk_widget_get_width, gtk_widget_get_height,
+   gtk_widget_add_controller;
+FROM GtkEnums IMPORT GtkVertical, GtkHorizontal;
+FROM GtkBox IMPORT gtk_box_new, gtk_box_append;
+FROM GtkLabel IMPORT gtk_label_new;
+FROM GtkFrame IMPORT gtk_frame_new, gtk_frame_set_child;
+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 GtkGestures IMPORT gtk_gesture_drag_new, gtk_gesture_single_set_button,
+   gtk_gesture_swipe_new, gtk_gesture_long_press_new,
+   gtk_gesture_rotate_new, gtk_gesture_zoom_new;
+FROM GtkEventController IMPORT gtk_event_controller_set_propagation_phase,
+   GtkPhaseBubble;
+FROM GtkScale IMPORT gtk_scale_new_with_range;
+FROM GtkRange IMPORT gtk_range_set_value, gtk_range_get_value;
+FROM GObject IMPORT g_signal_connect_data, GConnectDefault;
+FROM GtkClosures IMPORT Connect2;
+FROM Cairo IMPORT
+   cairo_create, cairo_destroy, cairo_set_source_rgb, cairo_paint,
+   cairo_set_source_surface, cairo_move_to, cairo_line_to,
+   cairo_set_line_width, cairo_set_line_cap, cairo_stroke,
+   cairo_save, cairo_restore, cairo_translate, cairo_scale, cairo_rotate,
+   cairo_arc,
+   cairo_close_path, cairo_rectangle, cairo_fill, cairo_set_source_rgba,
+   cairo_surface_destroy, cairo_image_surface_create,
+   cairo_pattern_create_linear, cairo_pattern_add_color_stop_rgb,
+   cairo_set_source, cairo_pattern_destroy, cairo_select_font_face,
+   cairo_set_font_size, cairo_text_path,
+   CairoFormatARGB32, CairoLineCapRound,
+   CairoFontSlantNormal, CairoFontWeightBold;
+
+CONST
+   Pi = 3.14159265358979;
+
+VAR
+   (* drawingarea *)
+   drawingWindow, drawSurface: ADDRESS;
+   dragStartX, dragStartY: REAL;
+
+   (* gestures *)
+   gesturesWindow: ADDRESS;
+   swipeX, swipeY, rotateDelta, zoomScale: REAL;
+   longPressed, hasRotate, hasZoom: BOOLEAN;
+
+   (* mask *)
+   maskWindow, maskScale: ADDRESS;
+   maskProgress: REAL;
+
+   (* snapping *)
+   snappingWindow: ADDRESS;
+   qrData: ARRAY [0..20] OF ARRAY [0..20] OF CHAR;
+
+(* ------------------------------------------------------------------ *)
+(* small helpers                                                      *)
+(* ------------------------------------------------------------------ *)
+
+PROCEDURE IMin (a, b: INTEGER) : INTEGER;
+BEGIN
+   IF a < b THEN RETURN a ELSE RETURN b END
+END IMin;
+
+(* The 21x21 QR matrix of snapping.c, one row per string. *)
+PROCEDURE InitQrData;
+BEGIN
+   qrData[0]  := "000000010111110000000";
+   qrData[1]  := "011111011100110111110";
+   qrData[2]  := "010001011010010100010";
+   qrData[3]  := "010001010011110100010";
+   qrData[4]  := "010001010001010100010";
+   qrData[5]  := "011111010100110111110";
+   qrData[6]  := "000000010101010000000";
+   qrData[7]  := "111111110000011111111";
+   qrData[8]  := "011101000000100000110";
+   qrData[9]  := "111100100000011100011";
+   qrData[10] := "110010010110110001001";
+   qrData[11] := "111001100101101111100";
+   qrData[12] := "101100000011001100110";
+   qrData[13] := "111111110011011100011";
+   qrData[14] := "000000010011000101001";
+   qrData[15] := "011111011000001000100";
+   qrData[16] := "010001010010111011101";
+   qrData[17] := "010001011001000000100";
+   qrData[18] := "010001011010110100011";
+   qrData[19] := "011111011001001111111";
+   qrData[20] := "000000010100001011110"
+END InitQrData;
+
+(* ------------------------------------------------------------------ *)
+(* drawing area                                                       *)
+(* ------------------------------------------------------------------ *)
+
+PROCEDURE CreateSurface (widget: ADDRESS);
+VAR
+   cr: ADDRESS;
+BEGIN
+   IF drawSurface # NIL THEN
+      cairo_surface_destroy(drawSurface)
+   END;
+   drawSurface := cairo_image_surface_create(CairoFormatARGB32,
+                    gtk_widget_get_width(widget),
+                    gtk_widget_get_height(widget));
+   cr := cairo_create(drawSurface);
+   cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
+   cairo_paint(cr);
+   cairo_destroy(cr)
+END CreateSurface;
+
+(* void scribble_resize(GtkWidget *widget, int width, int height, gpointer) *)
+PROCEDURE ScribbleResize (widget: ADDRESS; width, height: INTEGER;
+                          data: ADDRESS);
+BEGIN
+   CreateSurface(widget)
+END ScribbleResize;
+
+PROCEDURE ScribbleDraw (area: ADDRESS; cr: ADDRESS;
+                        width, height: INTEGER; data: ADDRESS);
+BEGIN
+   IF drawSurface # NIL THEN
+      cairo_set_source_surface(cr, drawSurface, 0.0, 0.0);
+      cairo_paint(cr)
+   END
+END ScribbleDraw;
+
+PROCEDURE DrawBrush (widget: ADDRESS; x, y: REAL);
+VAR
+   cr: ADDRESS;
+BEGIN
+   IF drawSurface = NIL THEN
+      CreateSurface(widget)
+   END;
+
+   cr := cairo_create(drawSurface);
+   cairo_move_to(cr, x, y);
+   cairo_line_to(cr, x, y);
+   cairo_set_line_width(cr, 6.0);
+   cairo_set_line_cap(cr, CairoLineCapRound);
+   cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+   cairo_stroke(cr);
+   cairo_destroy(cr)
+   (* gtk_widget_queue_draw(widget) is not bound (see header). *)
+END DrawBrush;
+
+PROCEDURE DragBegin (gesture: ADDRESS; x, y: REAL; area: ADDRESS);
+BEGIN
+   dragStartX := x;
+   dragStartY := y;
+   DrawBrush(area, x, y)
+END DragBegin;
+
+PROCEDURE DragUpdate (gesture: ADDRESS; x, y: REAL; area: ADDRESS);
+BEGIN
+   DrawBrush(area, dragStartX + x, dragStartY + y)
+END DragUpdate;
+
+PROCEDURE DragEnd (gesture: ADDRESS; x, y: REAL; area: ADDRESS);
+BEGIN
+   DrawBrush(area, dragStartX + x, dragStartY + y)
+END DragEnd;
+
+PROCEDURE OvalPath (cr: ADDRESS; xc, yc, xr, yr: REAL);
+BEGIN
+   cairo_save(cr);
+   cairo_translate(cr, xc, yc);
+   cairo_scale(cr, 1.0, yr / xr);
+   cairo_move_to(cr, xr, 0.0);
+   cairo_arc(cr, 0.0, 0.0, xr, 0.0, 2.0 * Pi);
+   cairo_close_path(cr);
+   cairo_restore(cr)
+END OvalPath;
+
+PROCEDURE FillChecks (cr: ADDRESS; x, y, width, height: INTEGER);
+CONST
+   CheckSize = 16;
+VAR
+   i, j: INTEGER;
+BEGIN
+   cairo_rectangle(cr, VAL(REAL, x), VAL(REAL, y),
+                   VAL(REAL, width), VAL(REAL, height));
+   cairo_set_source_rgb(cr, 0.4, 0.4, 0.4);
+   cairo_fill(cr);
+
+   (* x & (-CHECK_SIZE), valid for CHECK_SIZE a power of two. *)
+   j := x - (x MOD CheckSize);
+   WHILE j < height DO
+      i := y - (y MOD CheckSize);
+      WHILE i < width DO
+         IF ((i DIV CheckSize) + (j DIV CheckSize)) MOD 2 = 0 THEN
+            cairo_rectangle(cr, VAL(REAL, i), VAL(REAL, j),
+                            VAL(REAL, CheckSize), VAL(REAL, CheckSize))
+         END;
+         INC(i, CheckSize)
+      END;
+      INC(j, CheckSize)
+   END;
+
+   cairo_set_source_rgb(cr, 0.7, 0.7, 0.7);
+   cairo_fill(cr)
+END FillChecks;
+
+PROCEDURE Draw3Circles (cr: ADDRESS; xc, yc, radius, alpha: REAL);
+CONST
+   SubRadius = 0.56666667;      (* 2/3 - 0.1 *)
+   Third     = 0.33333333;
+   Cos30     = 0.86602540;
+VAR
+   sr: REAL;
+BEGIN
+   sr := radius * SubRadius;
+
+   cairo_set_source_rgba(cr, 1.0, 0.0, 0.0, alpha);
+   OvalPath(cr, xc, yc - radius * Third, sr, sr);
+   cairo_fill(cr);
+
+   cairo_set_source_rgba(cr, 0.0, 1.0, 0.0, alpha);
+   OvalPath(cr, xc - radius * Third * Cos30,
+                yc + radius * Third * 0.5, sr, sr);
+   cairo_fill(cr);
+
+   cairo_set_source_rgba(cr, 0.0, 0.0, 1.0, alpha);
+   OvalPath(cr, xc + radius * Third * Cos30,
+                yc + radius * Third * 0.5, sr, sr);
+   cairo_fill(cr)
+END Draw3Circles;
+
+PROCEDURE GroupsDraw (area: ADDRESS; cr: ADDRESS;
+                      width, height: INTEGER; data: ADDRESS);
+VAR
+   radius, xc, yc: REAL;
+BEGIN
+   radius := 0.5 * VAL(REAL, IMin(width, height)) - 10.0;
+   xc := VAL(REAL, width) / 2.0;
+   yc := VAL(REAL, height) / 2.0;
+
+   FillChecks(cr, 0, 0, width, height);
+
+   (* SIMPLIFIED: no similar surfaces / DEST_OUT / ADD. *)
+   cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+   OvalPath(cr, xc, yc, radius, radius);
+   cairo_fill(cr);
+
+   Draw3Circles(cr, xc, yc, radius, 1.0)
+END GroupsDraw;
+
+PROCEDURE OnDrawingDestroy (widget: ADDRESS; data: ADDRESS);
+BEGIN
+   IF drawSurface # NIL THEN
+      cairo_surface_destroy(drawSurface);
+      drawSurface := NIL
+   END
+END OnDrawingDestroy;
+
+PROCEDURE DoDrawingArea (doWidget: ADDRESS) : ADDRESS;
+VAR
+   vbox, da, label, frame, drag: ADDRESS;
+BEGIN
+   IF drawingWindow = NIL THEN
+      drawingWindow := gtk_window_new();
+      gtk_window_set_title(drawingWindow, "Drawing Area");
+      gtk_window_set_default_size(drawingWindow, 250, -1);
+      Connect2(drawingWindow, "destroy", OnDrawingDestroy, NIL);
+
+      vbox := gtk_box_new(GtkVertical, 8);
+      gtk_widget_set_margin_start(vbox, 16);
+      gtk_widget_set_margin_end(vbox, 16);
+      gtk_widget_set_margin_top(vbox, 16);
+      gtk_widget_set_margin_bottom(vbox, 16);
+      gtk_window_set_child(drawingWindow, vbox);
+
+      (* groups area *)
+      label := gtk_label_new("Knockout groups");
+      gtk_widget_add_css_class(label, "heading");
+      gtk_box_append(vbox, label);
+
+      frame := gtk_frame_new("");
+      gtk_widget_set_vexpand(frame, 1);
+      gtk_box_append(vbox, frame);
+
+      da := gtk_drawing_area_new();
+      gtk_frame_set_child(frame, da);
+      gtk_drawing_area_set_content_width(da, 100);
+      gtk_drawing_area_set_content_height(da, 100);
+      gtk_drawing_area_set_draw_func(da, ADR(GroupsDraw), NIL, NIL);
+
+      (* scribble area *)
+      label := gtk_label_new("Scribble area");
+      gtk_widget_add_css_class(label, "heading");
+      gtk_box_append(vbox, label);
+
+      frame := gtk_frame_new("");
+      gtk_widget_set_vexpand(frame, 1);
+      gtk_box_append(vbox, frame);
+
+      da := gtk_drawing_area_new();
+      gtk_frame_set_child(frame, da);
+      gtk_drawing_area_set_content_width(da, 100);
+      gtk_drawing_area_set_content_height(da, 100);
+      gtk_drawing_area_set_draw_func(da, ADR(ScribbleDraw), NIL, NIL);
+      g_signal_connect_data(da, "resize", ADR(ScribbleResize), NIL, NIL,
+                            GConnectDefault);
+
+      drag := gtk_gesture_drag_new();
+      gtk_gesture_single_set_button(drag, 0);
+      gtk_widget_add_controller(da, drag);
+      g_signal_connect_data(drag, "drag-begin", ADR(DragBegin), da, NIL,
+                            GConnectDefault);
+      g_signal_connect_data(drag, "drag-update", ADR(DragUpdate), da, NIL,
+                            GConnectDefault);
+      g_signal_connect_data(drag, "drag-end", ADR(DragEnd), da, NIL,
+                            GConnectDefault)
+   END;
+
+   IF gtk_widget_get_visible(drawingWindow) = 0 THEN
+      gtk_widget_set_visible(drawingWindow, 1)
+   ELSE
+      gtk_window_destroy(drawingWindow);
+      drawingWindow := NIL
+   END;
+   RETURN drawingWindow
+END DoDrawingArea;
+
+(* ------------------------------------------------------------------ *)
+(* gestures                                                           *)
+(* ------------------------------------------------------------------ *)
+
+PROCEDURE SwipeSwept (gesture: ADDRESS; vx, vy: REAL; widget: ADDRESS);
+BEGIN
+   swipeX := vx / 10.0;
+   swipeY := vy / 10.0
+END SwipeSwept;
+
+PROCEDURE LongPressPressed (gesture: ADDRESS; x, y: REAL; widget: ADDRESS);
+BEGIN
+   longPressed := TRUE
+END LongPressPressed;
+
+PROCEDURE LongPressEnd (gesture: ADDRESS; sequence: ADDRESS; widget: ADDRESS);
+BEGIN
+   longPressed := FALSE
+END LongPressEnd;
+
+PROCEDURE RotationChanged (gesture: ADDRESS; angle, delta: REAL;
+                           widget: ADDRESS);
+BEGIN
+   rotateDelta := delta;
+   hasRotate := TRUE
+END RotationChanged;
+
+PROCEDURE ZoomChanged (gesture: ADDRESS; scale: REAL; widget: ADDRESS);
+BEGIN
+   zoomScale := scale;
+   hasZoom := TRUE
+END ZoomChanged;
+
+PROCEDURE GesturesDraw (area: ADDRESS; cr: ADDRESS;
+                        width, height: INTEGER; data: ADDRESS);
+VAR
+   pat: ADDRESS;
+   cx, cy: REAL;
+BEGIN
+   cx := VAL(REAL, width) / 2.0;
+   cy := VAL(REAL, height) / 2.0;
+
+   IF (swipeX # 0.0) OR (swipeY # 0.0) THEN
+      cairo_save(cr);
+      cairo_set_line_width(cr, 6.0);
+      cairo_move_to(cr, cx, cy);
+      cairo_line_to(cr, cx + swipeX, cy + swipeY);
+      cairo_set_source_rgba(cr, 1.0, 0.0, 0.0, 0.5);
+      cairo_stroke(cr);
+      cairo_restore(cr)
+   END;
+
+   IF hasRotate OR hasZoom THEN
+      pat := cairo_pattern_create_linear(-100.0, 0.0, 200.0, 0.0);
+      cairo_pattern_add_color_stop_rgb(pat, 0.0, 0.0, 0.0, 1.0);
+      cairo_pattern_add_color_stop_rgb(pat, 1.0, 1.0, 0.0, 0.0);
+
+      cairo_save(cr);
+      cairo_translate(cr, cx, cy);
+      IF hasZoom AND (zoomScale > 0.0) THEN
+         cairo_scale(cr, zoomScale, zoomScale)
+      END;
+      IF hasRotate THEN
+         cairo_rotate(cr, rotateDelta)
+      END;
+      cairo_rectangle(cr, -100.0, -100.0, 200.0, 200.0);
+      cairo_set_source(cr, pat);
+      cairo_fill(cr);
+      cairo_restore(cr);
+
+      cairo_pattern_destroy(pat)
+   END;
+
+   IF longPressed THEN
+      cairo_save(cr);
+      cairo_arc(cr, cx, cy, 50.0, 0.0, 2.0 * Pi);
+      cairo_set_source_rgba(cr, 0.0, 1.0, 0.0, 0.5);
+      cairo_stroke(cr);
+      cairo_restore(cr)
+   END
+END GesturesDraw;
+
+PROCEDURE DoGestures (doWidget: ADDRESS) : ADDRESS;
+VAR
+   da, gesture: ADDRESS;
+BEGIN
+   IF gesturesWindow = NIL THEN
+      gesturesWindow := gtk_window_new();
+      gtk_window_set_title(gesturesWindow, "Gestures");
+      gtk_window_set_default_size(gesturesWindow, 400, 400);
+
+      da := gtk_drawing_area_new();
+      gtk_window_set_child(gesturesWindow, da);
+      gtk_drawing_area_set_draw_func(da, ADR(GesturesDraw), NIL, NIL);
+
+      (* swipe *)
+      gesture := gtk_gesture_swipe_new();
+      g_signal_connect_data(gesture, "swipe", ADR(SwipeSwept), da, NIL,
+                            GConnectDefault);
+      gtk_event_controller_set_propagation_phase(gesture, GtkPhaseBubble);
+      gtk_widget_add_controller(da, gesture);
+
+      (* long press *)
+      gesture := gtk_gesture_long_press_new();
+      g_signal_connect_data(gesture, "pressed", ADR(LongPressPressed), da,
+                            NIL, GConnectDefault);
+      g_signal_connect_data(gesture, "end", ADR(LongPressEnd), da, NIL,
+                            GConnectDefault);
+      gtk_event_controller_set_propagation_phase(gesture, GtkPhaseBubble);
+      gtk_widget_add_controller(da, gesture);
+
+      (* rotate *)
+      gesture := gtk_gesture_rotate_new();
+      g_signal_connect_data(gesture, "angle-changed", ADR(RotationChanged),
+                            da, NIL, GConnectDefault);
+      gtk_event_controller_set_propagation_phase(gesture, GtkPhaseBubble);
+      gtk_widget_add_controller(da, gesture);
+
+      (* zoom *)
+      gesture := gtk_gesture_zoom_new();
+      g_signal_connect_data(gesture, "scale-changed", ADR(ZoomChanged), da,
+                            NIL, GConnectDefault);
+      gtk_event_controller_set_propagation_phase(gesture, GtkPhaseBubble);
+      gtk_widget_add_controller(da, gesture)
+   END;
+
+   IF gtk_widget_get_visible(gesturesWindow) = 0 THEN
+      gtk_widget_set_visible(gesturesWindow, 1)
+   ELSE
+      gtk_window_destroy(gesturesWindow);
+      gesturesWindow := NIL
+   END;
+   RETURN gesturesWindow
+END DoGestures;
+
+(* ------------------------------------------------------------------ *)
+(* masking                                                            *)
+(* ------------------------------------------------------------------ *)
+
+PROCEDURE OnMaskChanged (range: ADDRESS; data: ADDRESS);
+BEGIN
+   maskProgress := gtk_range_get_value(range)
+END OnMaskChanged;
+
+PROCEDURE MaskDraw (area: ADDRESS; cr: ADDRESS;
+                    width, height: INTEGER; data: ADDRESS);
+VAR
+   pat: ADDRESS;
+   w, h, x0, y0: REAL;
+BEGIN
+   w := VAL(REAL, width);
+   h := VAL(REAL, height);
+
+   cairo_set_source_rgb(cr, 0.08, 0.08, 0.10);
+   cairo_rectangle(cr, 0.0, 0.0, w, h);
+   cairo_fill(cr);
+
+   (* SIMPLIFIED: fill the "123" glyphs with a moving gradient in place of
+      the GskMaskNode composition. *)
+   cairo_select_font_face(cr, "sans", CairoFontSlantNormal,
+                          CairoFontWeightBold);
+   cairo_set_font_size(cr, 200.0);
+   x0 := w / 2.0 - 165.0;
+   y0 := h / 2.0 + 70.0;
+   cairo_move_to(cr, x0, y0);
+   cairo_text_path(cr, "123");
+
+   pat := cairo_pattern_create_linear(maskProgress * w - w, 0.0,
+                                      maskProgress * w, h);
+   cairo_pattern_add_color_stop_rgb(pat, 0.0,        1.0, 0.0, 0.0);
+   cairo_pattern_add_color_stop_rgb(pat, 1.0 / 7.0,  1.0, 0.5, 0.0);
+   cairo_pattern_add_color_stop_rgb(pat, 2.0 / 7.0,  1.0, 1.0, 0.0);
+   cairo_pattern_add_color_stop_rgb(pat, 3.0 / 7.0,  0.0, 1.0, 0.0);
+   cairo_pattern_add_color_stop_rgb(pat, 4.0 / 7.0,  0.0, 1.0, 1.0);
+   cairo_pattern_add_color_stop_rgb(pat, 5.0 / 7.0,  0.0, 0.0, 1.0);
+   cairo_pattern_add_color_stop_rgb(pat, 6.0 / 7.0,  0.6, 0.0, 1.0);
+   cairo_pattern_add_color_stop_rgb(pat, 1.0,        1.0, 0.0, 1.0);
+   cairo_set_source(cr, pat);
+   cairo_fill(cr);
+   cairo_pattern_destroy(pat)
+END MaskDraw;
+
+PROCEDURE DoMask (doWidget: ADDRESS) : ADDRESS;
+VAR
+   box, da: ADDRESS;
+BEGIN
+   IF maskWindow = NIL THEN
+      maskWindow := gtk_window_new();
+      gtk_window_set_title(maskWindow, "Mask Nodes");
+      gtk_window_set_default_size(maskWindow, 600, 400);
+
+      box := gtk_box_new(GtkVertical, 0);
+      gtk_window_set_child(maskWindow, box);
+
+      da := gtk_drawing_area_new();
+      gtk_widget_set_hexpand(da, 1);
+      gtk_widget_set_vexpand(da, 1);
+      gtk_drawing_area_set_draw_func(da, ADR(MaskDraw), NIL, NIL);
+      gtk_box_append(box, da);
+
+      maskScale := gtk_scale_new_with_range(GtkHorizontal, 0.0, 1.0, 0.1);
+      gtk_range_set_value(maskScale, 0.5);
+      maskProgress := 0.5;
+      Connect2(maskScale, "value-changed", OnMaskChanged, NIL);
+      gtk_box_append(box, maskScale)
+   END;
+
+   IF gtk_widget_get_visible(maskWindow) = 0 THEN
+      gtk_widget_set_visible(maskWindow, 1)
+   ELSE
+      gtk_window_destroy(maskWindow);
+      maskWindow := NIL
+   END;
+   RETURN maskWindow
+END DoMask;
+
+(* ------------------------------------------------------------------ *)
+(* snapping                                                           *)
+(* ------------------------------------------------------------------ *)
+
+PROCEDURE QrOn (x, y: INTEGER) : BOOLEAN;
+BEGIN
+   RETURN qrData[y][x] = '1'
+END QrOn;
+
+PROCEDURE SetOnColor (cr: ADDRESS; on: BOOLEAN);
+BEGIN
+   IF on THEN
+      cairo_set_source_rgb(cr, 0.2, 0.6, 1.0)      (* ON      *)
+   ELSE
+      cairo_set_source_rgb(cr, 1.0, 0.95, 0.7)     (* OFF     *)
+   END
+END SetOnColor;
+
+(* A 1x1 tile with a horizontal gradient between differing neighbours. *)
+PROCEDURE HBetween (cr: ADDRESS; x, y: REAL; a, b: BOOLEAN);
+VAR
+   pat: ADDRESS;
+BEGIN
+   IF a = b THEN
+      SetOnColor(cr, a)
+   ELSE
+      pat := cairo_pattern_create_linear(x, y, x + 1.0, y);
+      IF a THEN
+         cairo_pattern_add_color_stop_rgb(pat, 0.3, 0.2, 0.6, 1.0);
+         cairo_pattern_add_color_stop_rgb(pat, 0.5, 1.0, 1.0, 1.0);
+         cairo_pattern_add_color_stop_rgb(pat, 0.7, 1.0, 0.95, 0.7)
+      ELSE
+         cairo_pattern_add_color_stop_rgb(pat, 0.3, 1.0, 0.95, 0.7);
+         cairo_pattern_add_color_stop_rgb(pat, 0.5, 1.0, 1.0, 1.0);
+         cairo_pattern_add_color_stop_rgb(pat, 0.7, 0.2, 0.6, 1.0)
+      END;
+      cairo_set_source(cr, pat)
+   END;
+
+   cairo_rectangle(cr, x, y, 1.0, 1.0);
+   cairo_fill(cr);
+
+   IF a # b THEN
+      cairo_pattern_destroy(pat)
+   END
+END HBetween;
+
+(* A 1x1 tile with a vertical gradient between differing neighbours. *)
+PROCEDURE VBetween (cr: ADDRESS; x, y: REAL; a, b: BOOLEAN);
+VAR
+   pat: ADDRESS;
+BEGIN
+   IF a = b THEN
+      SetOnColor(cr, a)
+   ELSE
+      pat := cairo_pattern_create_linear(x, y, x, y + 1.0);
+      IF a THEN
+         cairo_pattern_add_color_stop_rgb(pat, 0.3, 0.2, 0.6, 1.0);
+         cairo_pattern_add_color_stop_rgb(pat, 0.5, 1.0, 1.0, 1.0);
+         cairo_pattern_add_color_stop_rgb(pat, 0.7, 1.0, 0.95, 0.7)
+      ELSE
+         cairo_pattern_add_color_stop_rgb(pat, 0.3, 1.0, 0.95, 0.7);
+         cairo_pattern_add_color_stop_rgb(pat, 0.5, 1.0, 1.0, 1.0);
+         cairo_pattern_add_color_stop_rgb(pat, 0.7, 0.2, 0.6, 1.0)
+      END;
+      cairo_set_source(cr, pat)
+   END;
+
+   cairo_rectangle(cr, x, y, 1.0, 1.0);
+   cairo_fill(cr);
+
+   IF a # b THEN
+      cairo_pattern_destroy(pat)
+   END
+END VBetween;
+
+PROCEDURE SnappingDraw (area: ADDRESS; cr: ADDRESS;
+                        width, height: INTEGER; data: ADDRESS);
+VAR
+   cell, ox, oy, x, y: INTEGER;
+BEGIN
+   (* 21 tiles plus 21 gaps make 43 units; snap to whole device pixels. *)
+   cell := IMin(width, height) DIV 43;
+   IF cell < 1 THEN cell := 1 END;
+   ox := (width - 43 * cell) DIV 2;
+   oy := (height - 43 * cell) DIV 2;
+
+   cairo_save(cr);
+   cairo_translate(cr, VAL(REAL, ox), VAL(REAL, oy));
+   cairo_scale(cr, VAL(REAL, cell), VAL(REAL, cell));
+
+   cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
+   cairo_rectangle(cr, 0.0, 0.0, 43.0, 43.0);
+   cairo_fill(cr);
+
+   (* tile centres at odd coordinates 1, 3, ..., 41 *)
+   FOR y := 0 TO 20 DO
+      FOR x := 0 TO 20 DO
+         SetOnColor(cr, QrOn(x, y));
+         cairo_rectangle(cr, VAL(REAL, 2 * x + 1), VAL(REAL, 2 * y + 1),
+                         1.0, 1.0);
+         cairo_fill(cr)
+      END
+   END;
+
+   (* horizontal gaps between columns *)
+   FOR y := 0 TO 20 DO
+      FOR x := 0 TO 19 DO
+         HBetween(cr, VAL(REAL, 2 * x + 2), VAL(REAL, 2 * y + 1),
+                  QrOn(x, y), QrOn(x + 1, y))
+      END
+   END;
+
+   (* vertical gaps between rows *)
+   FOR y := 0 TO 19 DO
+      FOR x := 0 TO 20 DO
+         VBetween(cr, VAL(REAL, 2 * x + 1), VAL(REAL, 2 * y + 2),
+                  QrOn(x, y), QrOn(x, y + 1))
+      END
+   END;
+
+   cairo_restore(cr)
+END SnappingDraw;
+
+PROCEDURE DoSnapping (doWidget: ADDRESS) : ADDRESS;
+VAR
+   da: ADDRESS;
+BEGIN
+   IF snappingWindow = NIL THEN
+      snappingWindow := gtk_window_new();
+      gtk_window_set_title(snappingWindow, "Snapping");
+      gtk_window_set_default_size(snappingWindow, 600, 400);
+
+      da := gtk_drawing_area_new();
+      gtk_window_set_child(snappingWindow, da);
+      gtk_drawing_area_set_draw_func(da, ADR(SnappingDraw), NIL, NIL)
+   END;
+
+   IF gtk_widget_get_visible(snappingWindow) = 0 THEN
+      gtk_widget_set_visible(snappingWindow, 1)
+   ELSE
+      gtk_window_destroy(snappingWindow);
+      snappingWindow := NIL
+   END;
+   RETURN snappingWindow
+END DoSnapping;
+
+BEGIN
+   drawingWindow := NIL;
+   drawSurface := NIL;
+   dragStartX := 0.0;
+   dragStartY := 0.0;
+
+   gesturesWindow := NIL;
+   swipeX := 0.0;
+   swipeY := 0.0;
+   rotateDelta := 0.0;
+   zoomScale := 1.0;
+   longPressed := FALSE;
+   hasRotate := FALSE;
+   hasZoom := FALSE;
+
+   maskWindow := NIL;
+   maskScale := NIL;
+   maskProgress := 0.5;
+
+   snappingWindow := NIL;
+   InitQrData
+END DemosCanvas.

+ 9 - 0
demos/DemosChooser.def

@@ -0,0 +1,9 @@
+DEFINITION MODULE DemosChooser ;
+
+FROM SYSTEM IMPORT ADDRESS;
+EXPORT UNQUALIFIED DoComboBox, DoEntryCompletion, DoEntryUndo, DoDialog;
+PROCEDURE DoComboBox (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoEntryCompletion (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoEntryUndo (doWidget: ADDRESS) : ADDRESS;
+PROCEDURE DoDialog (doWidget: ADDRESS) : ADDRESS;
+END DemosChooser.

+ 541 - 0
demos/DemosChooser.mod

@@ -0,0 +1,541 @@
+IMPLEMENTATION MODULE DemosChooser ;
+
+(*
+   See the GTK C originals in gtk/demos/gtk-demo:
+     combobox.c         -> DoComboBox
+     entry_completion.c -> DoEntryCompletion
+     entry_undo.c       -> DoEntryUndo
+     dialog.c           -> DoDialog
+
+   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.
+
+   None of the deprecated, model-based widgets these demos use are
+   exposed under src/ (GtkComboBox, GtkComboBoxText, GtkTreeModel,
+   GtkListStore, GtkTreeStore, GtkCellRenderer, GtkCellLayout), and
+   neither are GtkDialog / GtkMessageDialog.  Each affected spot is
+   adapted to a bound equivalent and marked SIMPLIFIED below:
+
+     * combobox: the four combo boxes become GtkDropDown widgets over a
+       GtkStringList (the documented modern replacement).  The icon
+       renderer, the row separator, the insensitive rows and the
+       GtkTreeStore hierarchy cannot be reproduced, so the "Items with
+       icons" frame lists its five labels and the "Where are we ?" frame
+       lists the capitals of its tree flattened.  The editable combo is
+       a drop-down with search enabled; the "String IDs" property
+       binding (g_object_bind_property, unbound) is emulated by wiring
+       the drop-down's "notify::selected" and the entry's "changed"
+       signals together.
+     * entry_completion: GtkEntryCompletion is bound, but the
+       GtkTreeModel/GtkListStore it needs for its items is not, so the
+       completion is created with a text column but no model.  The
+       inline-completion and inline-selection setters are not bound
+       either.
+     * entry_undo: gtk_editable_set_enable_undo() is not bound, so the
+       entry is created without enabling undo/redo.
+     * dialog: GtkDialog/GtkMessageDialog are replaced by
+       AdwMessageDialog (responses plus an extra child); plain labels
+       stand in for the mnemonic labels, whose setters
+       (gtk_label_new_with_mnemonic / gtk_label_set_mnemonic_widget) are
+       not bound.
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM GtkWindow IMPORT gtk_window_new, gtk_window_set_title,
+   gtk_window_set_resizable, gtk_window_set_child, gtk_window_destroy,
+   gtk_window_present;
+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_hexpand, gtk_widget_set_vexpand,
+   gtk_widget_set_halign, gtk_widget_set_valign;
+FROM GtkEnums IMPORT GtkVertical, GtkHorizontal, GtkAlignCenter;
+FROM GtkBox IMPORT gtk_box_new, gtk_box_append;
+FROM GtkFrame IMPORT gtk_frame_new, gtk_frame_set_child;
+FROM GtkLabel IMPORT gtk_label_new, gtk_label_set_markup;
+FROM GtkGrid IMPORT gtk_grid_new, gtk_grid_attach,
+   gtk_grid_set_row_spacing, gtk_grid_set_column_spacing;
+FROM GtkSeparator IMPORT gtk_separator_new;
+FROM GtkButton IMPORT gtk_button_new_with_mnemonic;
+FROM GtkEntry IMPORT gtk_entry_new, gtk_entry_set_completion;
+FROM GtkEntryCompletion IMPORT gtk_entry_completion_new,
+   gtk_entry_completion_set_text_column,
+   gtk_entry_completion_set_popup_completion;
+FROM GtkEditable IMPORT gtk_editable_get_text, gtk_editable_set_text;
+FROM GtkDropDown IMPORT gtk_drop_down_new, gtk_drop_down_set_selected,
+   gtk_drop_down_get_selected, gtk_drop_down_get_model,
+   gtk_drop_down_set_enable_search;
+FROM GtkStringList IMPORT gtk_string_list_new, gtk_string_list_append,
+   gtk_string_list_get_string;
+FROM AdwMessageDialog IMPORT adw_message_dialog_new,
+   adw_message_dialog_add_response, adw_message_dialog_set_default_response,
+   adw_message_dialog_set_extra_child;
+FROM GtkClosures IMPORT Connect2, Connect3;
+FROM GtkUtils IMPORT IntToStr, CStrToM2, StrAppend;
+FROM GLib IMPORT g_strcmp0;
+
+(* ================================================================== *)
+(* combobox                                                           *)
+(* ================================================================== *)
+
+VAR
+   comboWindow: ADDRESS;
+   comboStringEntry: ADDRESS;      (* entry of the "String IDs" frame *)
+   comboIdsDropDown: ADDRESS;      (* drop-down of the "String IDs" frame *)
+
+(* Keep the two sides of the (unbound) active-id <-> text property
+   binding in sync: selecting a drop-down item writes the entry, and
+   typing an exact item writes the drop-down. *)
+PROCEDURE OnComboSelected (dropdown: ADDRESS; pspec: ADDRESS; data: ADDRESS);
+VAR
+   model, item: ADDRESS;
+   buf: ARRAY [0..63] OF CHAR;
+BEGIN
+   model := gtk_drop_down_get_model(dropdown);
+   item := gtk_string_list_get_string(model, gtk_drop_down_get_selected(dropdown));
+   CStrToM2(item, buf);
+   gtk_editable_set_text(comboStringEntry, buf)
+END OnComboSelected;
+
+PROCEDURE OnComboEntryChanged (entry: ADDRESS; data: ADDRESS);
+VAR
+   model, item: ADDRESS;
+   text, buf: ARRAY [0..63] OF CHAR;
+   i: INTEGER;
+BEGIN
+   CStrToM2(gtk_editable_get_text(entry), text);
+   model := gtk_drop_down_get_model(comboIdsDropDown);
+   FOR i := 0 TO 2 DO
+      item := gtk_string_list_get_string(model, VAL(CARDINAL, i));
+      CStrToM2(item, buf);
+      IF g_strcmp0(ADR(text), ADR(buf)) = 0 THEN
+         gtk_drop_down_set_selected(comboIdsDropDown, VAL(CARDINAL, i));
+         RETURN
+      END
+   END
+END OnComboEntryChanged;
+
+(* The capitals of create_capital_store(), flattened (the group rows and
+   the tree shape are lost with GtkTreeStore unbound).  Index 8 is
+   Boston, which is what the C demo selects via path (0, 8). *)
+PROCEDURE AddCapitals (list: ADDRESS);
+BEGIN
+   gtk_string_list_append(list, "Albany");
+   gtk_string_list_append(list, "Annapolis");
+   gtk_string_list_append(list, "Atlanta");
+   gtk_string_list_append(list, "Augusta");
+   gtk_string_list_append(list, "Austin");
+   gtk_string_list_append(list, "Baton Rouge");
+   gtk_string_list_append(list, "Bismarck");
+   gtk_string_list_append(list, "Boise");
+   gtk_string_list_append(list, "Boston");
+   gtk_string_list_append(list, "Carson City");
+   gtk_string_list_append(list, "Charleston");
+   gtk_string_list_append(list, "Cheyenne");
+   gtk_string_list_append(list, "Columbia");
+   gtk_string_list_append(list, "Columbus");
+   gtk_string_list_append(list, "Concord");
+   gtk_string_list_append(list, "Denver");
+   gtk_string_list_append(list, "Des Moines");
+   gtk_string_list_append(list, "Dover");
+   gtk_string_list_append(list, "Frankfort");
+   gtk_string_list_append(list, "Harrisburg");
+   gtk_string_list_append(list, "Hartford");
+   gtk_string_list_append(list, "Helena");
+   gtk_string_list_append(list, "Honolulu");
+   gtk_string_list_append(list, "Indianapolis");
+   gtk_string_list_append(list, "Jackson");
+   gtk_string_list_append(list, "Jefferson City");
+   gtk_string_list_append(list, "Juneau");
+   gtk_string_list_append(list, "Lansing");
+   gtk_string_list_append(list, "Lincoln");
+   gtk_string_list_append(list, "Little Rock");
+   gtk_string_list_append(list, "Madison");
+   gtk_string_list_append(list, "Montgomery");
+   gtk_string_list_append(list, "Montpelier");
+   gtk_string_list_append(list, "Nashville");
+   gtk_string_list_append(list, "Oklahoma City");
+   gtk_string_list_append(list, "Olympia");
+   gtk_string_list_append(list, "Phoenix");
+   gtk_string_list_append(list, "Pierre");
+   gtk_string_list_append(list, "Providence");
+   gtk_string_list_append(list, "Raleigh");
+   gtk_string_list_append(list, "Richmond");
+   gtk_string_list_append(list, "Sacramento");
+   gtk_string_list_append(list, "Salem");
+   gtk_string_list_append(list, "Salt Lake City");
+   gtk_string_list_append(list, "Santa Fe");
+   gtk_string_list_append(list, "Springfield");
+   gtk_string_list_append(list, "St. Paul");
+   gtk_string_list_append(list, "Tallahassee");
+   gtk_string_list_append(list, "Topeka");
+   gtk_string_list_append(list, "Trenton")
+END AddCapitals;
+
+PROCEDURE DoComboBox (doWidget: ADDRESS) : ADDRESS;
+VAR
+   vbox, frame, box, combo, list: ADDRESS;
+BEGIN
+   IF comboWindow = NIL THEN
+      comboWindow := gtk_window_new();
+      gtk_window_set_title(comboWindow, "Combo Boxes");
+
+      vbox := gtk_box_new(GtkVertical, 2);
+      gtk_widget_set_margin_start(vbox, 10);
+      gtk_widget_set_margin_end(vbox, 10);
+      gtk_widget_set_margin_top(vbox, 10);
+      gtk_widget_set_margin_bottom(vbox, 10);
+      gtk_window_set_child(comboWindow, vbox);
+
+      (* Items with icons (icons, separator and insensitivity omitted) *)
+      frame := gtk_frame_new("Items with icons");
+      gtk_box_append(vbox, frame);
+      box := gtk_box_new(GtkVertical, 0);
+      gtk_widget_set_margin_start(box, 5);
+      gtk_widget_set_margin_end(box, 5);
+      gtk_widget_set_margin_top(box, 5);
+      gtk_widget_set_margin_bottom(box, 5);
+      gtk_frame_set_child(frame, box);
+
+      list := gtk_string_list_new(NIL);
+      gtk_string_list_append(list, "Warning");
+      gtk_string_list_append(list, "Stop");
+      gtk_string_list_append(list, "New");
+      gtk_string_list_append(list, "Clear");
+      gtk_string_list_append(list, "Open");
+      combo := gtk_drop_down_new(list, NIL);
+      gtk_drop_down_set_selected(combo, 0);
+      gtk_box_append(box, combo);
+
+      (* Where are we ? (tree flattened to its capitals) *)
+      frame := gtk_frame_new("Where are we ?");
+      gtk_box_append(vbox, frame);
+      box := gtk_box_new(GtkVertical, 0);
+      gtk_widget_set_margin_start(box, 5);
+      gtk_widget_set_margin_end(box, 5);
+      gtk_widget_set_margin_top(box, 5);
+      gtk_widget_set_margin_bottom(box, 5);
+      gtk_frame_set_child(frame, box);
+
+      list := gtk_string_list_new(NIL);
+      AddCapitals(list);
+      combo := gtk_drop_down_new(list, NIL);
+      gtk_drop_down_set_selected(combo, 8);
+      gtk_box_append(box, combo);
+
+      (* Editable (search stands in for the validating entry) *)
+      frame := gtk_frame_new("Editable");
+      gtk_box_append(vbox, frame);
+      box := gtk_box_new(GtkVertical, 0);
+      gtk_widget_set_margin_start(box, 5);
+      gtk_widget_set_margin_end(box, 5);
+      gtk_widget_set_margin_top(box, 5);
+      gtk_widget_set_margin_bottom(box, 5);
+      gtk_frame_set_child(frame, box);
+
+      list := gtk_string_list_new(NIL);
+      gtk_string_list_append(list, "One");
+      gtk_string_list_append(list, "Two");
+      gtk_string_list_append(list, "2\xC2\xBD");
+      gtk_string_list_append(list, "Three");
+      combo := gtk_drop_down_new(list, NIL);
+      gtk_drop_down_set_enable_search(combo, 1);
+      gtk_box_append(box, combo);
+
+      (* String IDs (active-id binding emulated with two signals) *)
+      frame := gtk_frame_new("String IDs");
+      gtk_box_append(vbox, frame);
+      box := gtk_box_new(GtkVertical, 0);
+      gtk_widget_set_margin_start(box, 5);
+      gtk_widget_set_margin_end(box, 5);
+      gtk_widget_set_margin_top(box, 5);
+      gtk_widget_set_margin_bottom(box, 5);
+      gtk_frame_set_child(frame, box);
+
+      list := gtk_string_list_new(NIL);
+      gtk_string_list_append(list, "Not visible");
+      gtk_string_list_append(list, "Visible when active");
+      gtk_string_list_append(list, "Always visible");
+      comboIdsDropDown := gtk_drop_down_new(list, NIL);
+      gtk_box_append(box, comboIdsDropDown);
+
+      comboStringEntry := gtk_entry_new();
+      gtk_box_append(box, comboStringEntry);
+
+      Connect3(comboIdsDropDown, "notify::selected", OnComboSelected, NIL);
+      Connect2(comboStringEntry, "changed", OnComboEntryChanged, NIL)
+   END;
+
+   IF gtk_widget_get_visible(comboWindow) = 0 THEN
+      gtk_widget_set_visible(comboWindow, 1)
+   ELSE
+      gtk_window_destroy(comboWindow);
+      comboWindow := NIL;
+      comboStringEntry := NIL;
+      comboIdsDropDown := NIL
+   END;
+   RETURN comboWindow
+END DoComboBox;
+
+(* ================================================================== *)
+(* entry_completion                                                   *)
+(* ================================================================== *)
+
+VAR
+   entryCompletionWindow: ADDRESS;
+
+PROCEDURE DoEntryCompletion (doWidget: ADDRESS) : ADDRESS;
+VAR
+   vbox, label, entry, completion: ADDRESS;
+BEGIN
+   IF entryCompletionWindow = NIL THEN
+      entryCompletionWindow := gtk_window_new();
+      gtk_window_set_title(entryCompletionWindow, "Completion");
+      gtk_window_set_resizable(entryCompletionWindow, 0);
+
+      vbox := gtk_box_new(GtkVertical, 12);
+      gtk_widget_set_margin_start(vbox, 18);
+      gtk_widget_set_margin_end(vbox, 18);
+      gtk_widget_set_margin_top(vbox, 18);
+      gtk_widget_set_margin_bottom(vbox, 18);
+      gtk_window_set_child(entryCompletionWindow, vbox);
+
+      label := gtk_label_new("");
+      gtk_label_set_markup(label,
+         "Try writing <b>total</b> or <b>gnome</b> for example.");
+      gtk_box_append(vbox, label);
+
+      entry := gtk_entry_new();
+      gtk_box_append(vbox, entry);
+
+      completion := gtk_entry_completion_new();
+      gtk_entry_set_completion(entry, completion);
+
+      (* SIMPLIFIED: GtkTreeModel/GtkListStore (the completion's model)
+         are unbound, so there are no items to complete; only the text
+         column and popup-completion setting survive.  The C demo's
+         inline-completion/inline-selection setters are unbound too. *)
+      gtk_entry_completion_set_text_column(completion, 0);
+      gtk_entry_completion_set_popup_completion(completion, 1)
+   END;
+
+   IF gtk_widget_get_visible(entryCompletionWindow) = 0 THEN
+      gtk_widget_set_visible(entryCompletionWindow, 1)
+   ELSE
+      gtk_window_destroy(entryCompletionWindow);
+      entryCompletionWindow := NIL
+   END;
+   RETURN entryCompletionWindow
+END DoEntryCompletion;
+
+(* ================================================================== *)
+(* entry_undo                                                         *)
+(* ================================================================== *)
+
+VAR
+   entryUndoWindow: ADDRESS;
+
+PROCEDURE DoEntryUndo (doWidget: ADDRESS) : ADDRESS;
+VAR
+   vbox, label, entry: ADDRESS;
+BEGIN
+   IF entryUndoWindow = NIL THEN
+      entryUndoWindow := gtk_window_new();
+      gtk_window_set_title(entryUndoWindow, "Undo and Redo");
+      gtk_window_set_resizable(entryUndoWindow, 0);
+
+      vbox := gtk_box_new(GtkVertical, 12);
+      gtk_widget_set_margin_start(vbox, 18);
+      gtk_widget_set_margin_end(vbox, 18);
+      gtk_widget_set_margin_top(vbox, 18);
+      gtk_widget_set_margin_bottom(vbox, 18);
+      gtk_window_set_child(entryUndoWindow, vbox);
+
+      label := gtk_label_new("");
+      gtk_label_set_markup(label,
+         "Use Control+z or Control+Shift+z to undo or redo changes");
+      gtk_box_append(vbox, label);
+
+      entry := gtk_entry_new();
+      (* SIMPLIFIED: gtk_editable_set_enable_undo() is not bound, so
+         undo/redo support cannot be turned on from here. *)
+      gtk_box_append(vbox, entry)
+   END;
+
+   IF gtk_widget_get_visible(entryUndoWindow) = 0 THEN
+      gtk_widget_set_visible(entryUndoWindow, 1)
+   ELSE
+      gtk_window_destroy(entryUndoWindow);
+      entryUndoWindow := NIL
+   END;
+   RETURN entryUndoWindow
+END DoEntryUndo;
+
+(* ================================================================== *)
+(* dialog                                                             *)
+(* ================================================================== *)
+
+VAR
+   dialogWindow: ADDRESS;
+   dialogEntry1, dialogEntry2: ADDRESS;          (* left-hand entries *)
+   dialogLocalEntry1, dialogLocalEntry2: ADDRESS;
+   dialogCount: INTEGER;
+
+(* void message_dialog_clicked(GtkButton *button, gpointer user_data) *)
+PROCEDURE OnMessageDialog (button: ADDRESS; data: ADDRESS);
+VAR
+   dialog: ADDRESS;
+   body, num: ARRAY [0..63] OF CHAR;
+BEGIN
+   INC(dialogCount);
+
+   body := "Has been shown ";
+   IntToStr(dialogCount, num);
+   StrAppend(body, num);
+   StrAppend(body, " times");
+
+   dialog := adw_message_dialog_new(dialogWindow, "Test message", body);
+   adw_message_dialog_add_response(dialog, "ok", "_OK");
+   adw_message_dialog_add_response(dialog, "cancel", "_Cancel");
+   adw_message_dialog_set_default_response(dialog, "ok");
+   gtk_window_present(dialog)
+END OnMessageDialog;
+
+(* void on_dialog_response(GtkDialog *dialog, int response, gpointer data) *)
+PROCEDURE OnDialogResponse (dialog: ADDRESS; response: ADDRESS; data: ADDRESS);
+VAR
+   ok: ARRAY [0..3] OF CHAR;
+   text: ARRAY [0..255] OF CHAR;
+BEGIN
+   ok := "ok";
+   IF g_strcmp0(response, ADR(ok)) = 0 THEN
+      CStrToM2(gtk_editable_get_text(dialogLocalEntry1), text);
+      gtk_editable_set_text(dialogEntry1, text);
+      CStrToM2(gtk_editable_get_text(dialogLocalEntry2), text);
+      gtk_editable_set_text(dialogEntry2, text)
+   END
+   (* AdwMessageDialog closes itself once a response is activated. *)
+END OnDialogResponse;
+
+(* void interactive_dialog_clicked(GtkButton *button, gpointer user_data) *)
+PROCEDURE OnInteractiveDialog (button: ADDRESS; data: ADDRESS);
+VAR
+   dialog, grid, label: ADDRESS;
+   text: ARRAY [0..255] OF CHAR;
+BEGIN
+   dialog := adw_message_dialog_new(dialogWindow, "Interactive Dialog", "");
+   adw_message_dialog_add_response(dialog, "ok", "_OK");
+   adw_message_dialog_add_response(dialog, "cancel", "_Cancel");
+   adw_message_dialog_set_default_response(dialog, "ok");
+
+   grid := gtk_grid_new();
+   gtk_widget_set_hexpand(grid, 1);
+   gtk_widget_set_vexpand(grid, 1);
+   gtk_widget_set_halign(grid, GtkAlignCenter);
+   gtk_widget_set_valign(grid, GtkAlignCenter);
+   gtk_grid_set_row_spacing(grid, 6);
+   gtk_grid_set_column_spacing(grid, 6);
+
+   (* SIMPLIFIED: plain labels, as the mnemonic variants are unbound. *)
+   label := gtk_label_new("Entry 1");
+   gtk_grid_attach(grid, label, 0, 0, 1, 1);
+   dialogLocalEntry1 := gtk_entry_new();
+   CStrToM2(gtk_editable_get_text(dialogEntry1), text);
+   gtk_editable_set_text(dialogLocalEntry1, text);
+   gtk_grid_attach(grid, dialogLocalEntry1, 1, 0, 1, 1);
+
+   label := gtk_label_new("Entry 2");
+   gtk_grid_attach(grid, label, 0, 1, 1, 1);
+   dialogLocalEntry2 := gtk_entry_new();
+   CStrToM2(gtk_editable_get_text(dialogEntry2), text);
+   gtk_editable_set_text(dialogLocalEntry2, text);
+   gtk_grid_attach(grid, dialogLocalEntry2, 1, 1, 1, 1);
+
+   adw_message_dialog_set_extra_child(dialog, grid);
+
+   Connect3(dialog, "response", OnDialogResponse, NIL);
+
+   gtk_window_present(dialog)
+END OnInteractiveDialog;
+
+PROCEDURE DoDialog (doWidget: ADDRESS) : ADDRESS;
+VAR
+   vbox, vbox2, hbox, button, table, label: ADDRESS;
+BEGIN
+   IF dialogWindow = NIL THEN
+      dialogWindow := gtk_window_new();
+      gtk_window_set_title(dialogWindow, "Dialogs");
+      gtk_window_set_resizable(dialogWindow, 0);
+
+      vbox := gtk_box_new(GtkVertical, 8);
+      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(dialogWindow, vbox);
+
+      (* Standard message dialog *)
+      hbox := gtk_box_new(GtkHorizontal, 8);
+      gtk_box_append(vbox, hbox);
+      button := gtk_button_new_with_mnemonic("_Message Dialog");
+      Connect2(button, "clicked", OnMessageDialog, NIL);
+      gtk_box_append(hbox, button);
+
+      gtk_box_append(vbox, gtk_separator_new(GtkHorizontal));
+
+      (* Interactive dialog *)
+      hbox := gtk_box_new(GtkHorizontal, 8);
+      gtk_box_append(vbox, hbox);
+      vbox2 := gtk_box_new(GtkVertical, 0);
+
+      button := gtk_button_new_with_mnemonic("_Interactive Dialog");
+      Connect2(button, "clicked", OnInteractiveDialog, NIL);
+      gtk_box_append(hbox, vbox2);
+      gtk_box_append(vbox2, button);
+
+      table := gtk_grid_new();
+      gtk_grid_set_row_spacing(table, 4);
+      gtk_grid_set_column_spacing(table, 4);
+      gtk_box_append(hbox, table);
+
+      label := gtk_label_new("Entry 1");
+      gtk_grid_attach(table, label, 0, 0, 1, 1);
+
+      dialogEntry1 := gtk_entry_new();
+      gtk_grid_attach(table, dialogEntry1, 1, 0, 1, 1);
+
+      label := gtk_label_new("Entry 2");
+      gtk_grid_attach(table, label, 0, 1, 1, 1);
+
+      dialogEntry2 := gtk_entry_new();
+      gtk_grid_attach(table, dialogEntry2, 1, 1, 1, 1)
+   END;
+
+   IF gtk_widget_get_visible(dialogWindow) = 0 THEN
+      gtk_widget_set_visible(dialogWindow, 1)
+   ELSE
+      gtk_window_destroy(dialogWindow);
+      dialogWindow := NIL;
+      dialogEntry1 := NIL;
+      dialogEntry2 := NIL;
+      dialogLocalEntry1 := NIL;
+      dialogLocalEntry2 := NIL
+   END;
+   RETURN dialogWindow
+END DoDialog;
+
+BEGIN
+   comboWindow := NIL;
+   comboStringEntry := NIL;
+   comboIdsDropDown := NIL;
+   entryCompletionWindow := NIL;
+   entryUndoWindow := NIL;
+   dialogWindow := NIL;
+   dialogEntry1 := NIL;
+   dialogEntry2 := NIL;
+   dialogLocalEntry1 := NIL;
+   dialogLocalEntry2 := NIL;
+   dialogCount := 0
+END DemosChooser.

+ 11 - 1
demos/GtkDemo.mod

@@ -6,6 +6,8 @@ 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 DemosChooser IMPORT DoComboBox, DoEntryCompletion, DoEntryUndo, DoDialog;
+FROM DemosCanvas IMPORT DoDrawingArea, DoGestures, DoMask, DoSnapping;
 FROM SYSTEM IMPORT ADDRESS;
 
 TYPE
@@ -112,5 +114,13 @@ BEGIN
    Register("password_entry", DoPasswordEntry);
    Register("search_entry", DoSearchEntry);
    Register("tagged_entry", DoTaggedEntry);
-   Register("shortcut_triggers", DoShortcutTriggers)
+   Register("shortcut_triggers", DoShortcutTriggers);
+   Register("combobox", DoComboBox);
+   Register("entry_completion", DoEntryCompletion);
+   Register("entry_undo", DoEntryUndo);
+   Register("dialog", DoDialog);
+   Register("drawingarea", DoDrawingArea);
+   Register("gestures", DoGestures);
+   Register("mask", DoMask);
+   Register("snapping", DoSnapping)
 END GtkDemo.

+ 4 - 0
src/GtkWidget.def

@@ -24,6 +24,7 @@ EXPORT UNQUALIFIED
    gtk_widget_set_hexpand, gtk_widget_set_vexpand,
    gtk_widget_set_halign, gtk_widget_set_valign,
    gtk_widget_set_can_target,
+   gtk_widget_queue_draw,
    gtk_widget_set_size_request, gtk_widget_grab_focus,
    gtk_widget_get_width, gtk_widget_get_height,
    gtk_widget_add_css_class, gtk_widget_remove_css_class,
@@ -75,6 +76,9 @@ PROCEDURE gtk_widget_set_valign (widget: GtkWidget; align: GtkAlign);
 (* void gtk_widget_set_can_target(GtkWidget *widget, gboolean can_target) *)
 PROCEDURE gtk_widget_set_can_target (widget: GtkWidget; can_target: INTEGER);
 
+(* void gtk_widget_queue_draw(GtkWidget *widget) *)
+PROCEDURE gtk_widget_queue_draw (widget: GtkWidget);
+
 (* void gtk_widget_set_size_request(GtkWidget *widget, int width, int height) *)
 PROCEDURE gtk_widget_set_size_request (widget: GtkWidget;
                                        width, height: INTEGER);

+ 5 - 1
tests/run_tests.sh

@@ -67,6 +67,10 @@ if [ -d "$ROOT/demos" ]; then
   # 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/DemosChooser.mod" -o "$OBJ/DemosChooser.o"
+  # shellcheck disable=SC2086
+  $GM2 -I"$ROOT/src" -I"$ROOT/lib" -I"$ROOT/demos" $GM2FLAGS -c "$ROOT/demos/DemosCanvas.mod" -o "$OBJ/DemosCanvas.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
 
@@ -83,7 +87,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/DemosLayout.o $OBJ/DemosText.o $OBJ/DemosWindows.o $OBJ/DemosInput.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/DemosChooser.o $OBJ/DemosCanvas.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" \

+ 5 - 0
tests/test_demos.mod

@@ -91,6 +91,11 @@ BEGIN
    BuildAndClose("password_entry");
    BuildAndClose("search_entry");
    BuildAndClose("tagged_entry");
+   BuildAndClose("combobox");
+   BuildAndClose("entry_completion");
+   BuildAndClose("entry_undo");
+   BuildAndClose("drawingarea");
+   BuildAndClose("gestures");
 
    IF failures = 0 THEN
       printf("test_demos: PASS\n")