A GNU Modula-2 (gm2 -fiso) binding of the tigr graphic library, plus a few
test programs in modula2/examples. The C library is vendored in
tigr-master/ (v3.2).
From modula2/ (working directory must contain tigr.c, tigr.def,
helper.mod, helper.def):
gcc -c tigr.c
gm2 -fiso -c helper.mod
gm2 -fiso -I. helper.o tigr.o <example>.mod -o <example> -lGL -lX11
The -s flag shown in the README is optional and worked fine in testing.
Verified by building and running on this machine (X11 session):
Essai1, demo, clip, clavier, flags, update, hello, shader
all compile, link and run as windows; headless runs without a window and
writes a correct 320x240 PNG; opengl opens two windows and drives the
raw GL path (magenta glClearColor/glClear visible in Window2).
Essai2 (qualified imports) and Essai3 build. invaders (new, no
assets) is a playable Space Invaders game using only tigrFillRect +
the built-in font: two-frame invader animation, accelerating movement,
aimed multiple bombs, bullet-vs-bomb scoring and autofire. Verified in
live captures: bombs (orange #FFA028) raining on every shot and sprite
frames toggling between frames. snake (new, no assets) is a classic
Snake game on an 8px grid - verified moving, eating/growing and the
wall-collision "GAME OVER" state. pong (new, no assets) is a classic
Pong game in a 640x400 window - verified serves, wall/paddle bounces,
scoring and the match-over restart. essai4.mod is
not a tigr example: it is a standalone PIM-style bit-twiddling test
(SHIFT/AND/OR on CARDINAL) that the ISO dialect does not accept -
left as is.tigrClear, tigrFill, tigrLine, tigrRect,
tigrCircle, tigrFillCircle, tigrClip, tigrBlit, tigrBlitAlpha,
tigrMouse, tigrKeyDown, tigrReadChar, tigrTime, tigrTextWidth,
tigrTextHeight, tigrPrint, tigrLoadImage, tigrReadFile.TPixelType passed/returned by value matches the C ABI (checked with
tigrGet/tigrPlot).Tigr record layout (tigr.def) matches the C struct Tigr
(tigr.h) field-for-field.tfont and the built-in font work; text metrics are correct.Only ~35 of ~110 identifiers declared in tigr.def were in the
EXPORT UNQUALIFIED list, so the rest could not be imported at all
(GM2 rejects them, even with qualified names). Affected:
tigrBeginOpenGL, tigrSetPostShader, tigrGet, tigrPlot,
tigrBlitTint, tigrBlitMode, tigrLoadFont, tigrFreeFont,
tigrTouch, tigrScrollWheel, tigrShowKeyboard, tigrLoadImageMem,
tigrSaveImage, tigrInflate, tigrDecodeUTF8.TigrType, intPtr, shortRealPtr, TigrFontTypePtr,
TigrFontType, TigrGlyphType, TCodepageType, TigrTouchPoint,
TigrTouchPointPtr.TIGR_FIXED, TIGR_KEEP_ALPHA, TIGR_BLEND_ALPHA and almost
all TK_* key codes (only ESCAPE, SPACE, LEFT, RIGHT were exported).All of them are now in the export list (verified: tigrGet, intPtr,
tigrLoadFont, ... now compile in both qualified and unqualified form;
demo, flags, clavier, Essai1, Essai2, Essai3 still build).
TIGR_BLEND_ALPHA has the wrong value (fixed 2026-09-16)tigr.def: TIGR_BLEND_ALPHA = -1tigr.h: TIGR_BLEND_ALPHA = 1 (enum, default blit mode)blitMode is used arithmetically in tigr.c (lines 470, 591, 643), so a
client passing -1 would break alpha blending. Fix: it is now set to 1.
This also made the constant usable (it is exported).
TCodepageType enum values are wrong (fixed 2026-09-16)tigr.def: TCodepageType = (TCP_ASCII, TCP_1252, TCP_UTF32) -> 0, 1, 2tigr.h: TCP_ASCII = 0, TCP_1252 = 1252, TCP_UTF32 = 12001A Modula-2 enumerated type cannot hold non-consecutive ordinals, so the enumeration was replaced by plain constants:
TCP_ASCII = 0;
TCP_1252 = 1252;
TCP_UTF32 = 12001;
tigrLoadFont takes a CARDINAL codepage, so clients can now pass the
correct values. The TCodepageType type and the stray TCodepage variable
were removed from tigr.def.
DynamicStrings.String used as a C const char* (fixed 2026-09-16)A GNU Modula-2 String (from DynamicStrings) is a record whose character
buffer is at offset 0 but is not NUL-terminated and is capped at 127
bytes (MaxBuf). Passing an InitString result to a C function that expects
a const char* only worked while the freshly-allocated memory happened to be
zeroed.
Fix applied in tigr.def (GNU Modula-2 maps ARRAY OF CHAR in a FOR "C"
module onto char*/const char*, including NUL-terminated string literals):
| procedure | before | after |
|---|---|---|
tigrWindow |
title: String |
title: ARRAY OF CHAR |
tigrPrint |
text: String |
text: ARRAY OF CHAR |
tigrReadFile |
returns String (raw char*) |
returns ADDRESS |
tigrDecodeUTF8 |
text: String; ... ): String |
text: ADDRESS; cp: intPtr): ADDRESS |
tigrEncodeUTF8 |
text: String; ... ): String |
text: ADDRESS; cp: INTEGER): ADDRESS |
Consequences for users:
tigrPrint text are now ordinary string literals /
ARRAY OF CHAR (e.g. tigrWindow(320, 240, "Clip", 0)), instead of
requiring InitString.tigrReadFile returns a NUL-terminated char* as ADDRESS. To use the
contents in M2, wrap it: InitStringCharStar(raw) then CopyOut(arr, s)
(used this way in demo.mod).tigrDecodeUTF8/tigrEncodeUTF8 operate on ADDRESS buffers (pass
ADR(buffer)); they return the next position as ADDRESS. Do not use them
with DynamicStrings.String values.The FROM DynamicStrings IMPORT String; import was removed from tigr.def
(no longer needed).
All examples were updated to the new API:
demo.mod, Essai1.mod, clavier.mod, flags.mod (they were passing
InitString(...) results before), and clip.mod now compiles as-is because
its string literal/constant arguments are accepted directly.
examples/clip/clip.mod did not compile (tigrWindow(320, 240, "Clip", 0)
passed a string literal where String was expected). This was the same
String/char* mismatch as BUG 4, so fixing tigr.def fixed
clip.mod and it now compiles and runs unchanged.examples/flags/flags.mod crashed immediately at old line 184: the
toggle (TogglePtr) variable was never initialized (dereferences NIL),
modeChange was never set, the loop ran 0..numToggles (one too many
iterations), keys never un-set checked, and window recreation only ran
when flags > newFlags. Fixed:
toggle := CAST(TogglePtr, ADR(toggles[i])); inside the loop.FOR i := 0 TO numToggles - 1.toggle^.checked := 1 - toggle^.checked.flags # newFlags).modeFlags := TIGR_AUTO + TIGR_RETINA and
modeChange := (flags AND modeFlags) # (newFlags AND modeFlags); since
ISO rejects CARDINAL AND/OR, bit masking is done via
CAST(BITSET, n) * CAST(BITSET, mask) (set intersection).drawToggle's stride parameter is CARDINAL (was INTEGER, receiving
a CARDINAL).AND (window open AND ESC not pressed) so ESC quits,
matching the C example.examples/unicode/update.mod was an unfinished stub (missing : in the
VAR block, empty body). Completed into a working bouncing-ball example that
updates position each frame on the tigr window (keeps the original
x/y/posx/posy variable names).examples/Essai2/Essai2.mod was a stub. Completed into a minimal window
test that exercises the qualified-import style (IMPORT helper, tigr):
builds a TPixelType with helper.tigrRGB(80H, 90H, 0A0H), clears the
window with it, and closes on ESC.The original tigr-master/ tree (sources identical to the bundled copy) also
shipped headless, hello, opengl and shader. modula2/examples/ had
empty placeholders for headless/, opengl/ and shader/; hello/ did not
exist. All four were ported and verified:
headless/headless.mod - off-screen bitmap (tigrBitmap) + tigrPrint +
tigrSaveImage("headless.png", bmp); no window at all. Produces a valid
320x240 RGBA PNG whose corner pixel is exactly tigrRGB(80H, 90H, 0A0H)
= (128, 144, 160) and which contains the rendered white text.
Note: C returns !err, i.e. 1 = success - check result = 0 for
failure, not # 0.hello/hello.mod - the trivial windowed "Hello, world." (blue clear +
white text). Runs fine.opengl/opengl.mod - two windows; Window2 clears the raw GL
framebuffer magenta via tigrBeginOpenGL + glClearColor(1,0,1,1) +
glClear(GL_COLOR_BUFFER_BIT) and then blits tigr text over it. Verified
by screenshot: the window is dominated by #FF00FF. The GL functions are
called through a tiny DEFINITION MODULE FOR "C" gl; (opengl/gl.def).shader/shader.mod - tigrSetPostShader with the GLSL fxShader string
tigrSetPostFX/tigrTime loop ("Shady" window). Needs
size := HIGH(fxShader) (gm2 computes the literal's length - there is no
LENGTH/strlen for ARRAY OF CHAR in ISO M2).snake/snake.mod (new, no assets) - classic Snake on an 8px grid in the
320x240 window. Fixed-size body arrays (whole board = win condition),
per-tick pending turn stored as INTEGER deltas (no 180 degree reversal),
deterministic LCG food placement, tigrTime-deltas to pace movement,
speed increases with score. Verified live: head advances one cell per
step, eating grows it by one cell (forced food in the head's line), and a
straight run ends in "GAME OVER" with the head frozen at the wall.pong/pong.mod (new, no assets) - classic Pong in a 640x400 window.
Two-player capable: W/S move the left paddle, UP/DOWN take over the
right paddle from the CPU opponent. Ball and paddle positions are
SHORTREAL, moved with tigrTime deltas; the ball speeds up (x1.08,
capped) on every paddle hit and bounces off the paddle surface at an
angle derived from the impact offset; first to 5 points wins. Verified
live: serving, wall/paddle bounces, scoring and match restart all work.GM2 gotchas encountered:
DEFINITION MODULE FOR "C" gl; puts FOR "C" before the module name
(the order MODULE gl FOR "C"; only produces syntax warnings).FOR "C" definition without EXPORT UNQUALIFIED is mangled as
<module>_<proc> (e.g. gl_glClear), so it cannot reach a plain C
function like glClear; with EXPORT UNQUALIFIED glClear, ...; GM2
generates the plain C names and links straight against -lGL.CARDINAL/INTEGER bitwise AND/OR/SHIFT; use
CAST(BITSET, n) * ... (intersection) / + (union).DOWNTO in ISO: write FOR k := n - 1 TO 1 BY -1 DO.fxShader).SHORTREAL literals (1.0) pass straight into C float parameters.tigrTime() is not an absolute clock: each call returns the number of
seconds since the previous call (i.e. a per-frame delta; the first call
returns 0). Use it directly as dt := tigrTime(); per frame (as
demo.mod and shader.mod do) - do not subtract two calls to build a
"now" you then difference, or timers effectively never fire (seen in the
first invaders version: dt = now - lastTime collapsed to ~0 and bombs
never spawned).wide_int_to_tree_1 at tree.cc:1900, via fold_negate_const) when
folding constant integer arithmetic: a compile-time a - b or a negated
integer literal (e.g. Serve(-1), winH - 28, winW / 2 - 20) crashes
cc1gm2. Workarounds used in pong.mod: keep CONST entries as plain
literals, use precomputed literals at call sites
(CenterPrint(372, ...)), and pass directions as BOOLEAN (TRUE =
down/right, FALSE = up/left) instead of -1/1 INTEGER literals.
Negating runtime variables (-bvy, -diff) is fine.bvx > 0.0, not bvx > 0
(the latter is an ordinary ISO type error, SHORTREAL vs INTEGER,
not a compiler bug).tigr.def still declares stray global variables (Tigr : TigrType,
TigrGlyph, TigrFont) that do not correspond to anything in the C
library. They are not exported; they could be removed (TCodepage was
removed along with BUG 3).Essai1 uses tigrError to quit once the window is closed; on Linux
tigrError prints to stderr and calls exit(1).microui v2.02 (immediate-mode UI, rxi, MIT) was rewritten from the C in
microui-master/ into ISO Modula-2:
| file | role |
|---|---|
microui.def |
public API: types, constants, widget procedures |
microui.mod |
full port of microui.c (widgets, layout, command list) |
microuiHelpers.def/.mod |
bit ops (ISO has none), string/memory, number formatting |
examples/microui-demo/microui-demo.mod |
interactive demo rendered with tigr |
examples/microui-demo/microui-headless.mod |
off-screen smoke test writing microui-demo.png |
Build:
cd modula2
gcc -c ../tigr-master/tigr.c -o tigr.o
gm2 -fiso -c helper.mod
gm2 -fiso -I. -c microuiHelpers.mod
gm2 -fiso -I. -c microui.mod
cd examples/microui-demo
gm2 -fiso -I ../../ ../../tigr.o ../../helper.o \
../../microuiHelpers.o ../../microui.o \
microui-demo.mod -o microui-demo -lGL -lX11
gm2 -fiso -I ../../ ../../tigr.o ../../helper.o \
../../microuiHelpers.o ../../microui.o \
microui-headless.mod -o microui-headless -lGL -lX11
The demo is rendered by tigr (not SDL): COMMAND_RECT -> tigrFillRect,
COMMAND_TEXT -> tigrPrint with tfont, COMMAND_ICON -> line drawings,
COMMAND_CLIP -> tigrClip. Mouse keys/buttons from tigrMouse,
tigrKeyHeld/tigrKeyDown/tigrReadChar are translated into
microui.input* calls.
mu_Command is a C union; Modula-2 has none. It is represented by a
BaseCommand header and the concrete command records; command buffers are
walked with CAST(CommandPtr, ...) and each command is cast to its record.TextWidthArgs,
TextHeightArgs, FrameArgs).drawText takes the string as ADDRESS + length (as in C) rather than an
open ARRAY OF CHAR, so substrings can be passed without copying.TCodepageType-style enums with non-consecutive ordinals do not apply here;
all microui constants are plain CONST.DEFINITION MODULE is parsed as part of
compiling its .mod, a procedure type only accepts a single, unnamed
formal parameter. Both PROCEDURE(a : T; b : T) and PROCEDURE(ADDRESS;
INTEGER) are rejected (the latter only in that context). Workaround: one
type-only parameter per procedure type; bundle extra arguments in a record.POINTER TO T: rejected in formal parameter lists and in
CAST(...) (CAST(POINTER TO BYTE, p) fails). Declare a named pointer type
alias (BytePtr = POINTER TO BYTE) and use it.SHORTREAL(integer) is miscompiled: it silently yields 0.
Conversion from INTEGER/CARDINAL to SHORTREAL must go through the
standard FLOAT function and be assigned to a SHORTREAL
(microuiHelpers.IntToReal / CardToReal). This silently broke the slider
thumb position and every %.2f/%.3g display until found.TRUNC and type aliases: TRUNC(x) where x's type is an alias
(Real = SHORTREAL) gives "argument to TRUNC must be a float point type".
Wrapping the expression in SHORTREAL(...) (or using a direct SHORTREAL
local) works. ROUND is not defined at all in ISO mode.EXIT is only allowed inside a LOOP, not in WHILE/REPEAT.ADDRESS does not support +; use ADDADR.
ADDRESS(array) / CAST(P, ADDRESS(array)) fails; use ADR(array)
(ADR must be imported FROM SYSTEM).f(x)^.field cannot be dereferenced directly;
assign the result to a local first. Function return values also cannot be
discarded - assign them to a dummy variable.WHILE/REPEAT conditions mixing INTEGER and CARDINAL (e.g.
i <= HIGH(a) with an INTEGER index) are rejected; convert HIGH with
VAL(INTEGER, HIGH(a)).beginRootContainer pushed unclippedRect through pushClipRect, which
intersects with the (empty) clip stack and produced a garbage clip rect;
every draw was then clipped away (the first headless render was blank).
It now pushes the unclipped rect directly, matching begin_root_container.mu_update_control had the hover/no-interact logic inverted; rewritten to
match the C.checkbox/textbox/slider/number hashed the pointee value instead of
the pointer; they now hash ADR(pointer) like the C.RealToStr (added for the port) was rewritten to round correctly and to
format %f / %g faithfully (fixed an off-by-one zero-pad and a lost
minus sign in %g), and no longer relies on the broken SHORTREAL(int).tigrFillRect alone to paint microui rectangles.
tigr's tigrFillRect fills only the interior of a rectangle (it does
x+=1; y+=1; w-=2; h-=2), so every rectangle was shrunk by one pixel and
all 1px-high/wide rectangles (the control and window borders) disappeared -
which showed up as the top of the window being "eaten" by the title bar.
The fix is to draw tigrRect (1px outline) followed by tigrFillRect
(interior): together they fill the exact rectangle, with clipping and
alpha blending, and single-pixel rects still show.TIGR_2X, which forces a 2X scale
(1600x1200 for the 800x600 bitmap). On a screen whose client area is
shorter than that, tigr crops the bitmap, cutting off the top rows of the
framebuffer - which is what "ate" the window title. TIGR_FIXED avoided
the crop but letterboxes the fixed bitmap (resizing just recentres it and
jumps between integer scales). The demo now uses TIGR_AUTO: tigr resizes
the bitmap to match the window (1:1), so the UI draws on the full, growing
canvas and resizing behaves like a normal resizable window. The renderer
must use bmp^.w / bmp^.h (not the compile-time size) for its clip and
clear.