← board

UMBRELLA — cross-platform GUI: the LCL widgetset model, done sparsely

North star — don't reinvent a round wheel

FPC/Lazarus already solved cross-platform GUI properly: an interface layer (LCL = forms/controls the app writes) sitting on a widgetset seam (TWS*), with the concrete backend (gtk2/gtk3/qt/win32/cocoa) selected at compile time (-ws). One binary, one widgetset baked in. That's the design we copy — the cut line, not the 20-year surface.

The Borland TComponent model underneath (owner/owned + parent/child as two separate axes, FreeNotification, published-property streaming) is the crown jewel; it makes ~200 components cheap because they derive from ~5 bases. Copy the hierarchy whole; constrain the backends. Cost is components × widgetsets, so we limit the widgetset axis, not the component axis.

What we accept

Windows is a target — like it or not, users are there. GUI on Windows is a real goal, not a someday. It rides the OS-portability umbrella [[feature-port-multi-os-abstraction]] (PE writer + MS x64 ABI) for the ability to emit a Windows binary at all; this umbrella owns the widget half on top.

Ground truth — what lib/pcl IS today (scout 2026-07-21)

Verdict a-minus: real LCL-shaped lib WITH a backend seam, but the seam leaks.

Three facts that shape the children

  1. The seam LEAKS. extctrls.pas (24 raw gtk_, e.g. gtk_paned_new :153), dialogs.pas (7), glarea.pas (5) call GTK directly, bypassing TWidgetSet. → a second widgetset is impossible until sealed. See [[feature-pcl-seam-seal]].
  2. Selection is hardwired. interfaces.pas:6 does uses gtk3widgets unconditionally — no --widgetset, no matrix, no hard-fail. See [[feature-pcl-widgetset-select]].
  3. tk today is NOT a PCL facelib/pcl/tk.pas is a separate Tcl/Tk string-eval embed (fat external libtcl/libtk dep, used by the NilPy IDE, Track E), explicitly "NOT a widget layer." The direction (below) is a thin tk face over the common PCL core, keeping the real-Tk embed only as interim IDE vehicle. Confirm before ripping it → Track U decide-nilpy-gui-tk-vs-pcl.

Settled design (2026-07-21, user) — the lowest-common-denominator rule

The load-bearing principle, and the real answer to "what is the right level":

The neutral core exposes only what ALL backends share — the intersection, not any toolkit's superset. GTK's extras, Qt's extras, Win32's extras stay below the seam. qt != gtk, so the API is their common subset. This is what keeps the seam thin and every backend cheap; it's the concrete meaning of "abstract at the right level."

Concrete calls (each a deliberate scope cap, not a gap to fill later):

Children (the plan)

  1. [[feature-pcl-seam-seal]] (Track B) — route extctrls/dialogs/glarea through TWidgetSet; zero raw gtk_ outside gtk3widgets.pas. The enabler — nothing else lands first. GTK-only, no behaviour change, fully testable today.
  2. [[feature-pcl-widgetset-select]] (Track B, small CLI touch) — --widgetset= + compile-time bake + the sparse widgetset×OS matrix as a hard compile error for unshipped/untested cells. With only gtk3 today it's a 1-cell matrix; it exists so adding win32 is a table entry, not a rewire.
  3. [[feature-pcl-win32-widgetset]] (Track B; blocked-by #1 and the Track A Windows PE work) — a second TWidgetSet subclass, native user32/gdi32, zero-dep. Best-effort, UN-GATED (no Windows box, Wine-smoke only).

The matrix (start sparse, grow by table entry)

widgetset linux windows notes
gtk3 ✅ ship+test ❌ 30-40 MB DLL swarm — refuse today's only working cell
win32 ❌ n/a ⚠️ best-effort / Wine-smoke child #3
qt 🔜 future, no ticket ❌ not delivered no consumer yet — don't pre-build

Every ❌/🔜 = a compile-time refusal with a reason, never a silent broken build.

Deliberate limits (say no in the ticket, not in a bug report later)

Known LCL-fidelity gaps (backlog, low prio — note now, fix when they bite)

Acceptance (umbrella)