AGT Design

AGT (AximCode GUI Toolkit) — Design

Status: Phase 2 widget toolkit COMPLETE (full widget set, layouts, dialogs, menus, render/visual harness) in this repo (aximcode/agt); work has moved on to the graphical text editor (axedit, single-buffer Phases 1–3 + 5 shipped — see AGT-Editor-Design.md) and a completed Qt-like input/focus rework. Persistent user settings + theming (file-backed config, a descriptor-driven settings dialog, theme persistence) are designed in AGT-Settings-Design.md (planned). The Phase 2 sub-phase log below is kept as the build record. Supersedes the Phase G2 stub in axl-sdk’s ROADMAP.md.

Lived in axl-sdk’s docs/ until AGT had its own repo; moved here 2026-05-28 once the bootstrap shipped. Cross-references to axl-sdk content (substrate docs, ROADMAP, AXLMM-Design, etc.) use absolute GitHub URLs from here; AGT-internal references stay relative.

Goal

A C++ widget toolkit for pre-boot UEFI applications. Provides windows, dialogs, controls, menus, layout containers, and a message-driven event model suitable for vendor diagnostic tools, service applications, and BMC local UIs.

Built on:

  • axl — axl-sdk’s C library (foundation)

  • axl-gfx — graphics substrate in axl-sdk core (src/gfx/README.md)

  • axl-input — input substrate in axl-sdk core (src/input/README.md)

  • C++ toolchain in axl-sdkaxl-cc (and axl-c++ alias) compile .cpp source against a freestanding-UEFI g++ + the C++ ABI runtime in libaxl-cxx.a (operator new/delete / __cxa_pure_virtual) and libaxl.a (__cxa_atexit / __dso_handle / .init_array walker). Shipped 2026-05-28. See AXLMM-Design.md §”Toolchain & constraints”.

AGT calls the C library directly — there is no axlmm wrapper class layer in AGT v0.1. See “Why AGT doesn’t depend on axlmm in v0.1” for the rationale.

Architecturally inspired by the FOX Toolkit:

  • C++ from the ground up; no GObject-equivalent in the stack

  • Message-target dispatch via static map macros (FXMAPFUNC analog)

  • Parent-child ownership tree (no ref counting)

  • Layout managers are widgets, composed by nesting

Why FOX-shape, not GTK-shape

Two architectures are conceivable on top of axl-gfx:

  1. GTK-shape (4-library stack) — C widget toolkit + C++ wrapper, mirroring GNOME’s GLib / GTK / glibmm / gtkmm. Requires a GObject equivalent in the C layer (type system, signals, properties, ref counting).

  2. FOX-shape — single C++ widget toolkit on top of the C library directly. C++ inheritance does the work a GObject runtime would do in the GTK-shape stack. (An optional C++ wrapper layer — axlmm — may eventually exist to add ergonomics; see “Why AGT doesn’t depend on axlmm in v0.1”. AGT v0.1 ships without it.)

The 4-library stack only pays off when there are C consumers of the widget toolkit. axl-sdk’s apps are predominantly CLI tools (sysinfo, netinfo, lspci, fetch, grep, find) which will never want widgets. Realistic widget-toolkit consumers — vendor pre-boot service tools, OEM diagnostic UIs, BMC local consoles — are universally C++ because their application logic benefits from C++ regardless of which toolkit they use.

For our environment, the C widget layer in a GTK-shape stack would be dead code that carries the cost of a GObject-equivalent runtime without anyone benefiting. FOX-shape is right-sized for our consumer reality.

Keeping the GTK-shape option open

This decision is reversible. axl-gfx and axl-input are designed as paradigm-agnostic C primitives — a future GTK-shape toolkit could be built on the same substrate without refactoring axl-sdk. Retained-mode, immediate-mode, GTK-shape, or any other widget paradigm is a downstream choice.

The substrate discipline rules below are the contract that keeps this door open. They are not aspirational; every axl-gfx / axl-input change must be evaluated against them.

Layering

       Vendor pre-boot C++ application
                  │
                  ▼
                AGT (C++, FOX-shape)         [aximcode/agt]
                  │
                  ▼  (calls C APIs directly + AXL_AUTOPTR for RAII)
              axl + axl-gfx + axl-input      [axl-sdk]

axl-gfx and axl-input are siblings in axl-sdk core. Neither depends on the other or on AGT. AGT depends on all three (axl, axl-gfx, axl-input) and calls their C APIs directly from C++ — the extern "C" declarations in axl-sdk headers make this zero-ceremony. RAII for owned C handles comes from the existing AXL_AUTOPTR(Type) macro (GCC cleanup attribute; g++ supports it natively).

A future optional layer — axlmm, axl-sdk’s C++ wrapper modeled on glibmm — may eventually sit between AGT and the C APIs to add ergonomics (method syntax, sticky error chains, std::expected factories, range-for adapters). Implementation is deferred until AGT’s actual usage patterns inform its design. See “Why AGT doesn’t depend on axlmm in v0.1” and AXLMM-Design.md.

Why AGT doesn’t depend on axlmm in v0.1

The original design pinned axlmm as a prerequisite (“axlmm CPP1 toolchain validation is the single biggest risk for AGT”). Phase 1 work split that into two distinct concerns:

  • C++ toolchain validation (does g++ + freestanding-UEFI + libaxl-cxx.a + crt0 work?) — this is what AGT actually depends on. Shipped 2026-05-28 (see Phase 1 above).

  • axlmm wrapper class library (RAII handles + method syntax + sticky errors + std::expected factories over the C public surface) — does NOT gate AGT. Implementation deferred per AXLMM-Design.md.

The reasoning for deferring axlmm:

  1. glibmm came AFTER glib had real consumers (glib 1998 → glibmm 2002; four years of evolution informed the wrapper design). Designing axlmm without a real C++ consumer means guessing which methods to wrap, which overloads to provide, which factory shapes to use. Risk: ~2000 LOC of wrappers AGT doesn’t use or doesn’t fit.

  2. C++ calls C trivially. axl-sdk headers wrap declarations in extern "C". AGT calls axl_loop_new(), axl_gfx_fill_rect(), axl_input_attach_mouse() directly from C++ with zero ceremony. The phase-1 toolchain work proved this end-to-end.

  3. RAII is already covered via AXL_AUTOPTR(Type) — a GCC cleanup attribute macro that g++ supports natively. ~14 axl-sdk types already register it via AXL_DEFINE_AUTOPTR_CLEANUP. Scope-bound free works in C++ identically to C.

  4. axlmm provides ergonomic enhancements, not capabilities. .write("hi") vs axl_stream_write(s, "hi", 2) is nicer but not transformative; sticky-error chains save a few lines per call; std::string_view overloads save a .data(), .size() pair. Across a 10,000-line AGT codebase the gain is real but not load-bearing.

What this means for AGT v0.1:

  • AGT is C++ throughout, but calls the C API directly

  • RAII via AXL_AUTOPTR(AxlStream) s = axl_stream_new(...) etc.

  • Error handling via if (rc != AXL_OK) return rc; per call

  • No axlmm::* types in headers or implementation

  • C++ features used: classes + virtual dispatch + lambdas + <utility> + <type_traits> + <array> + <span> + <string_view> + std::expected (where useful) — everything in the freestanding libstdc++ subset proven workable by Phase 1

If/when AGT v0.1 ships and usage patterns make a wrapper layer clearly valuable, Phase 4 (above) revisits axlmm implementation. Until then, the design doc captures the spec for future use without gating AGT on it.

Substrate discipline rules

These rules govern Phase 0 work in axl-sdk (axl-gfx gap closure

  • axl-input new module). Violating them breaks the door-open property for future widget toolkits.

  1. Pure C, no C++ ABI leakage. axl-gfx and axl-input stay strict C. No struct layouts assuming destructors, no function-pointer slots that imply a hidden this, no allocators tied to C++ scope.

  2. Substrate is stateless or holds only driver state. No “window,” “widget tree,” or “dispatch table” concepts in axl-gfx. Functions take buffers, draw to buffers. The toolkit on top owns hierarchy and lifecycle.

  3. axl-input is toolkit-agnostic. It produces raw events as a unified AxlInputEvent (type / coords / keycode / modifiers / timestamp). Source registration reuses axl-loop (axl-sdk’s GMainLoop equivalent) — axl_input_attach_mouse / axl_input_attach_key / axl_input_attach_touch are thin wrappers over axl_loop_add_event (for arbitrary EFI events) and axl_loop_add_key_press (for keyboard) that translate the underlying UEFI protocol payloads into AxlInputEvent and dispatch via an AxlInputCallback. This keeps DRY with axl-loop’s source/dispatch machinery instead of building a parallel queue. Toolkits translate raw events into their own internal dispatch model at the layer above. If axl-input ever speaks any toolkit’s dispatch dialect — widget message maps, signal-slot wiring, per-widget handlers — the door closes.

  4. Font system exposes glyph metrics + rasterization, not text layout. The current 8x16 bitmap blitter satisfies this. A real font system extends this contract; it does not weaken it with “draw text widget into rect with line wrapping.”

  5. axl-gfx primitives only grow, never narrow for AGT’s convenience. Once a primitive is in axl-gfx, it has the broader C contract. AGT may wrap it more narrowly, but cannot ask axl-gfx to shed generality.

  6. Document each new primitive with paradigm-neutrality test. Each addition: “What would a retained-mode tree-based toolkit use this for? What would an immediate-mode toolkit use it for?” If neither answer makes sense, the primitive is paradigm-specific and belongs in AGT, not axl-gfx.

AGT architecture

Class hierarchy (sketch)

AgtObject
  - virtual destructor, parent pointer, child list
  - no type registry, no ref counting

AgtWidget : AgtObject
  - bounding rect (x, y, w, h)
  - dirty flag
  - virtual void draw(AgtDrawContext &)
  - virtual bool handle_event(const AgtEvent &)

AgtWindow : AgtWidget
  - top-level, owns back buffer
  - presents via axl-gfx double-buffer primitive

AgtApp
  - top-level event loop integration
  - wraps axl_loop, subscribes to axl-input

Widget set (v0.1 scope)

Targets a complete pre-boot diagnostic / service UI. ~25 classes:

  • Containers: AgtVBox, AgtHBox, AgtGrid, AgtFrame, AgtScrollFrame, AgtTabView (notebook tabs)

  • Containers / data: AgtTreeView (virtualized over AxlNTree), AgtExpander (collapsible section)

  • Controls: AgtLabel, AgtButton, AgtBitmapButton, AgtCheckBox, AgtRadioButton, AgtToggleButton, AgtSwitch, AgtEditField (single-line), AgtEditBox (multi-line), AgtSpinBox, AgtListBox, AgtComboBox, AgtSlider, AgtScrollBar, AgtProgressBar

  • Windowing / chrome: AgtDialog, AgtMessageBox, AgtPromptDialog, AgtFileDialog, AgtToolBar, AgtStatusBar

  • Menus: AgtMenu, AgtMenuBar, AgtMenu

  • Decorative: AgtIcon, AgtSeparator, AgtSpacer, AgtGroupBox

Visibility: AgtWidget::set_visible hides a widget (and its subtree) from both the render walk and hit-testing — the primitive AgtTabView (one stacked page at a time) and AgtExpander (collapse) share.

The kitchen-sink example is the flagship gallery (a category-tabbed AgtTabView of the widget families); later widget batches slot into it.

Shipped post-v0.1-scope (the first ncwidgets-gap tranche): AgtPaned (split + draggable sash), AgtSearchEntry (magnifier + clear), AgtTooltip (hover-delayed floating label, on the window’s loop timer), AgtSpinner (phase-pinned animated busy indicator).

Also shipped from the roadmap below: the instrumentation family (AgtLight, AgtScale, AgtDial, AgtChart), AgtPasswordField, AgtProgressDialog, the declarative settings-forms framework (AgtForm / AgtFormBrowser), AgtLogView (the scrolling console / log-tail pane), the editable cell grid AgtTableEdit (on a shared AgtTableBase factored out of AgtTableView), and the interactive VT/ANSI emulator AgtTerminal.

The roadmap — per-widget design sketches, effort, and a suggested build order for the instrumentation family, secure input, and the specialized views (editable table, interactive terminal, progress dialog) — is now fully shipped; it lives in AGT-Widget-Roadmap.md as the record of what was built and why.

Next tranche (deferred until consumer demand, surfaced by the ncwidgets + diagnostic-app gap analyses):

Were in the v0.1 list above as the two most-used widgets in the diagnostic app — now SHIPPED:

  • AgtBitmapButtonSHIPPED. A button whose face is a bitmap / icon (with an optional caption), including a procedural stock-icon mode. (14 diagnostic-app files.)

  • AgtIconSHIPPED. A lightweight image element for list / tree / button glyphs and pass/fail/warn status badges. (7 diagnostic-app files.)

New widgets:

  • AgtEditBox — multi-line text entry. SHIPPED (: AgtFrame, embeds the shared AgtTextEdit core + a vertical AgtScrollBar): cursor as (line, column), Up/Down with a sticky goal column, Enter inserts a newline, vertical scroll (cursor-follow + wheel), and the inner-rect text clip — which back-filled AgtEditField (long lines no longer overrun). (9 diagnostic-app files.) Deferred — soft word-wrap to the inner width: v0.1 breaks lines only on an explicit \n (a long line is clipped, no horizontal scroll). Soft-wrap would reuse AgtLabel’s greedy wrap layout, but cursor Up/Down + click-to-byte must then map through the visual (wrapped) lines rather than the logical (\n) lines — a notable complexity bump (a logical line spans several visual rows) with its own test surface. Tracked for a later tranche once a consumer needs flowed multi-line entry; hard-newline editing covers the log / script / multi-field pre-boot cases today.

  • AgtTableView / AgtTableEdit — a virtualized DATA grid. SHIPPED as an AgtTable* family on a shared AgtTableBase (the column schema, per-cell-text row model, virtualized viewport, owned scrollbar, and draw skeleton): AgtTableView (read-only — whole-row selection + sortable text/numeric columns) and AgtTableEdit (an editable cell grid — a single active cell with in-cell editing via an overlaid AgtEditField). Distinct from AgtGrid, which is pure equal-cell layout — this holds data (rows of values + per-column widths/headers), not a grid of arbitrary child widgets. A tree-table (expandable rows × columns) is the natural later merge. Added for the general pre-boot tabular-readout + parameter-entry cases.

  • AgtCanvas — a custom-paint surface that hands the consumer a draw callback per frame, instead of requiring an AgtWidget::draw subclass. The escape hatch for charts / gauges / bespoke visuals.

  • AgtSprite — a sprite-sheet widget: animates a sub-rectangle of a source image, advanced off the loop timer (the same axl_loop_add_timer cadence AgtSpinner uses). Substrate ready: axl-sdk v0.23.0 added axl_gfx_blit_rect(buffer, src_stride, src_x, src_y, dst_x, dst_y, w, h) (blit one sheet cell without a per-frame CPU copy) — the only axl-sdk dependency this tranche had. AGT just needs a thin AgtDrawContext::blit_rect forwarder over it.

  • AgtAnimatedImage — a multi-frame image (frame sequence) cycled on the loop timer; the still-image analogue of AgtSpinner’s motion.

  • Animation primitives — a shared tick / timeline driver the spinner, sprite, and animated-image build on, so each animated widget isn’t re-implementing timer plumbing. Pins a fixed frame for deterministic visual baselines (the AgtSpinner phase() pattern).

Further out: charts, dials, dock panels; a terminal/VT widget is an explicit non-goal for the pre-boot scope.

Message dispatch (FOX-style)

Compile-time static map per class, no runtime registration:

class MyDialog : public AgtDialog {
public:
    long on_ok_clicked(AgtObject *sender, AgtEvent &ev);
    long on_cancel_clicked(AgtObject *sender, AgtEvent &ev);
    AGT_DECLARE_MAP();
};

AGT_MAP_BEGIN(MyDialog, AgtDialog)
    AGT_MAP_COMMAND(ID_OK,     &MyDialog::on_ok_clicked)
    AGT_MAP_COMMAND(ID_CANCEL, &MyDialog::on_cancel_clicked)
AGT_MAP_END()

Dispatch is a sorted lookup in the static table, with class-chain walk for inherited handlers. Same shape as FOX, MFC, wxWidgets. No dynamic registration, no allocation in the dispatch path.

Memory model

Parent-child ownership. A widget’s destructor cascades into its children. No ref counting, no std::shared_ptr. The application creates the top-level AgtWindow; everything else is owned by the window through the tree.

AgtApp’s lifetime spans the event loop; it is constructed once, runs to completion, and tears down everything on exit.

Data-structure policy: hierarchy vs. data

AGT consumes axl-sdk’s C container modules (axl-array, axl-list / axl-slist, axl-hash-table, axl-radix-tree, axl-queue, axl-ring-buf, axl-buf-pool) rather than reinventing them — but only for widget data, never for the widget hierarchy. The line:

  • Widget hierarchy → AGT-owned intrusive list. The parent / child / sibling tree lives on AgtObject as an intrusive doubly-linked list (FOX-shape). This is deliberate and must NOT be swapped for a node-based axl-list: ownership is the tree (the destructor cascade is the memory model), traversal is in the hot render / hit-test / dispatch paths, and intrusive links cost zero allocation. Same reasoning keeps the message-map dispatch a small linear scan rather than a hash table — the per-class tables are a handful of entries, where linear beats hashing.

  • Small, bounded UI state → fixed inline arrays. Counts that are naturally small and bounded for a pre-boot UI use fixed-capacity inline arrays with a documented cap (e.g. AgtApp::modal_stack_[4], AgtMenu::items_[32], AgtMenuBar::titles_[16], AgtEditField::text_[256]). Zero heap, deterministic, cache-friendly; the cap is a feature, not a limitation, and add_* returns NULL when full. Do not promote these to dynamic containers just because a container exists — the allocation + failure path buys nothing at these sizes.

  • Large or unbounded widget data → axl-sdk containers. When a widget backs a dataset whose size the toolkit doesn’t control — the rows of a future AgtListBox, the nodes of an AgtTreeView, the entries of an AgtComboBox / AgtFileDialog, the viewport of an AgtScrollFrame — that backing store should be an axl-array / axl-list / axl-radix-tree / axl-hash-table, not a hand-rolled growable buffer. These are the widgets where reusing axl-sdk infrastructure (and, if something is genuinely missing — e.g. an O(1)-indexed virtualized list for huge scrollable views — adding it to axl-sdk) is the right call. New container infrastructure belongs in paradigm-agnostic axl-sdk, never pushed up as an AGT-only structure or down as a toolkit concept inside the substrate (see Substrate discipline rules).

Rule of thumb: if the structure expresses ownership or layout of widgets, AGT owns it intrusively; if it holds content the widgets display, it’s an axl-sdk container.

Styling policy: Qt-shape palette (data) + style (renderer)

Styling splits into two independently-swappable axes, mirroring Qt’s QPalette / QStyle and resolved down the widget tree (own pin → nearest widget ancestor → global):

  • AgtPalette (<agt/agt-palette.hpp>) — the data: a plain struct of named tokens (accent, surface_bg, well_bg, text, thumb, text_size, …) plus nested per-widget-type sections (button.{rounded,glossy,gradient_sheen}, the popup/frame/ tooltip drop-Shadows, gamma_correct). Mutable global AgtPalette::current(). A JSON5 theme file feeds it via agt_palette_load("theme.json5") / agt_palette_parse(...) (keys are the token field names; colors CSS hex, metrics JSON numbers) — the overlay sits on top of the struct, never a per-widget lookup.

  • AgtStyle (<agt/agt-style.hpp>) — the renderer: a polymorphic object whose draw_frame / draw_button / draw_checkbox / … paint widget chrome. A widget’s draw() is a thin style().draw_xxx(ctx, *this) delegation, so the swappable AgtStyle::current() (or a per-subtree set_style) re-skins the look-and-feel as a unit. AgtStyle is itself the concrete default; subclass + override one draw_* to retheme a control (Qt’s QFusionStyle : QCommonStyle, collapsed to one base).

Three rules:

  • Per-control palette roles live where the role is known. The renderer (and AgtFrame::set_themed_bg(&AgtPalette::role)) reads the right token for each control — a scrollbar’s well_bg, a panel’s panel_bg — so a theme swap recolors every control correctly without a string registry. Per-instance setters (set_bg_color, set_thumb_color, …) pin a consumer override that survives a swap (a bg_explicit_-style bit per overridable token — Qt’s resolve-mask shape).

  • Live theming (recolor + relook). Reading is live at draw for the renderer (relook: swap the style → repaint), and palette colors are re-snapshotted by restyle() (Qt’s changeEvent(PaletteChange) shape) so a palette swap recolors already-built widgets. The app-level toggle is three lines:

    AgtPalette::current() = dark_palette;   // recolor
    window.restyle_tree();                  // re-snapshot the tree
    window.invalidate_all();  window.redraw();
    

    (A light/dark toggle is exactly this.) Frame-derived widgets that pick their surface via set_themed_bg recolor through the inherited AgtFrame::restyle with no per-widget override.

  • Renderer draws chrome; widgets keep stateful content. The style owns backgrounds, borders, indicators, and standard control chrome. Genuinely stateful/complex content stays widget-side (Qt’s split): AgtImage’s cached resample blit, AgtEditField’s scrolled text + cursor/selection, list/tree row layout — these can’t move into a const-ref renderer without de-const’ing it or duplicating state. Structural + algorithmic constants (bevel deltas, MAX_* caps, scrollbar strip width, wheel step, cursor width) stay file-local, not themed.

Invariant: every token’s default equals the value the widget shipped with, and AgtStyle::current() reproduces the prior drawing exactly — so an app that never touches either axis renders identically (the 18 visual baselines stayed 0-diff through the Qt-shape rearchitecture; the glossy-button default is the one intentional visual change, riding its own regenerated baselines).

Coordinate model: Qt/GTK-shape affine context

Widgets paint in their own local coordinate space(0, 0) is the widget’s top-left — exactly as in Qt (QPainter on a widget) and GTK (the pre-translated cairo_t). The AgtDrawContext is AGT’s QPainter / cairo_t: it owns the current affine transform and maps local coordinates to device (back-buffer) pixels. Widgets never add their absolute origin by hand.

This replaced an earlier manual-absolute model where each draw() added ctx.origin_x()/origin_y() to every coordinate. That matched none of Qt/FOX/GTK (all of which author in local space — Qt/GTK via a painter transform, FOX via a native sub-window per widget) and made a forgotten +origin a silent-mispaint bug class. AGT renders the widgets of a given surface into one axl-gfx back-buffer (no native sub-windows), so the Qt “alien widget” / GTK single-surface shape is the right analog: local authoring + a context transform.

Post-C7 (compositor adoption complete) the toplevel is no longer literally one buffer: the window, each popup (menu / combo / tooltip), and the modal dialog are each their own compositor surface with its own back-buffer, composited by the axl-sdk compositor. But the authoring discipline above is unchanged — within any one surface the widget tree still renders local-space into that surface’s single back-buffer; the surface boundary is where stacking, per-pixel alpha, and backdrop blur (the modal veil) live.

Rules:

  • The transform type is axl-sdk’s AxlTransform — a full 3×3 homography (double m[9], row-major, cairo multiply order) with the builder API in <axl/axl-math.h> (axl_transform_identity / _translate / _scale / _rotate / _shear / _multiply / _map_point / _map_rect / _invert / _classify). AGT does not carry a parallel transform type: the axl-sdk render primitives take AxlTransform* directly, so a wrapper would only add conversions. (Through Phase 3 AGT had its own float 2×3 AgtTransform; the Phase 4 consolidation dropped it.) The matrix math is fully general so the model matches Qt QTransform / cairo and needs no API change later.

  • AgtDrawContext carries a transform + a save/restore stack. The render walk pushes each child’s translate(x, y) descending and pops it ascending — that pairing is the transform stack. Forwarders (fill_rect, draw_text_ttf, push_clip, blit, …) take local coordinates and apply the current transform internally before calling axl_gfx_* with device coordinates.

  • The substrate stays paradigm-agnostic. The transform lives in the painting layer (AgtDrawContext), never pushed into axl-gfx; AGT hands axl-gfx final device geometry, just as Qt/GTK apply the CTM above the GPU/X11 driver.

  • Every primitive renders at any angle (Phase 4). Each forwarder keys a fast path vs. a transform path off axl_transform_classify:

    • fill_rect / fill_rounded_rectIDENTITY/TRANSLATE/SCALE use the integer rect path; AFFINE/PROJECTIVE map the four corners and fill the convex quad via axl_gfx_fill_path (a rotated rounded rect drops its corner rounding — no rotated-rounded primitive).

    • draw_text_ttfIDENTITY/TRANSLATE use the cached axl_ttf_draw; anything above (scale included, since the cached path can’t scale px_size) routes through the vector axl_ttf_draw_transform. Bitmap draw_text has no transform variant, so it no-ops under a rotation/shear (UI text uses the ttf path).

    • push_clip — axis-aligned uses the rect clip; AFFINE+ pushes a convex-quad clip via axl_gfx_push_clip_rect_transformed.

    • blitIDENTITY/TRANSLATE use the cheap axl_gfx_blit; a scale/rotation wraps the source in a temporary buffer and routes through the bilinear axl_gfx_blit_transform.

    No v0.1 widget produces a non-translate transform (the render walk only translates), so these paths are exercised by consumer apps and the transform-demo baseline, not the widget set — and the translate-only baselines stay byte-identical.

  • Absolute position is an explicit query. When something needs window/screen space (popup placement, mouse-capture mapping), AgtWidget::map_to_window() / absolute_origin() is the deliberate call — Qt’s mapToGlobal, GTK’s gtk_widget_translate_coordinates. This also retires the duplicate parent-walks (absolute_origin_of, the per-widget find_window/root_window helpers).

  • Hit-testing mirrors drawing. hit_test_recursive accumulates the integer parent-local origins down the tree to map a screen point into widget-local space (no v0.1 widget rotates, so the translate-only walk needs no matrix inverse) and honors the same child clip a viewport draws with, so scrolled-out content is neither painted nor clickable. An interactive rotated/scaled widget would switch this to axl_transform_invert; nothing produces one today.

  • The render trace records local coordinates. RecordingDrawContext captures what the widget authored (its own frame), so trace-test assertions are independent of where a parent places the widget.

Event loop integration

AGT runs on top of axl-loop, the axl-sdk equivalent of GLib’s GMainLoop. The mapping into the GNOME stack analogy this project follows:

Layer

GNOME

axl-sdk (today)

C event loop

GMainLoop + g_main_loop_add_source

AxlLoop + axl_loop_add_event / axl_loop_add_key_press

C++ loop wrapper

Glib::MainLoop

AXL_AUTOPTR(AxlLoop) + direct C calls (a future axlmm::Loop wrapper is parked — see AXLMM-Design.md)

Toolkit-level loop

Gtk::Application::run()

AgtApp::run()

AgtApp::run() constructs an AxlLoop, attaches axl-input sources via axl_input_attach_mouse / axl_input_attach_key / axl_input_attach_touch (all thin wrappers over axl_loop_add_event and axl_loop_add_key_press), dispatches incoming events through the widget tree’s message map, and schedules redraws via axl-loop deferred work. Idle handlers, timers, protocol-install notifications, and arbitrary EFI events compose through the same loop primitives that any other axl-sdk consumer uses — AGT does not own its own dispatch machinery.

Widgets reach the loop as an app-global service, not a per-window resource: there is ONE loop, and a widget anywhere (main window, dialog, popup) arms a timer via widget->window()->loop(), which falls back to AgtApp::current()->loop(). A surface-hosting widget needs no loop wiring of its own — do not call AgtWindow::set_loop() to “give a dialog a loop.” See AGT-Loop-Access.md for the canonical rule + rationale.

int main(int argc, char *argv[]) {
    axl_runtime_init();

    AgtApp app;
    MyMainWindow win;
    win.show();
    return app.run();   // returns when the main loop exits
}

Animation cadence: the compositor frame clock (planned adoption)

Today AGT schedules every repaint and animation off independent axl_loop_add_timers — AgtCursorBlink owns one, the hex/memory viewer polls on another (status + volatile re-read), and each new animated effect would add yet another. Independent timers have two costs: no present-throttle (a timer can drive its own present even when the surface is occluded or nothing visibly changed) and no shared monotonic frame time for delta-based easing.

axl-sdk’s compositor grew a Wayland-shaped frame-clock substrate (axl_compositor_attach_frame_clock + per-surface axl_surface_request_frame / axl_compositor_dispatch_frame, landed as compositor phase E7). The model: a surface that wants to animate requests a one-shot frame callback; the compositor fires all pending callbacks once per tick with the monotonic frame timestamp, then clears them — re-requesting inside the callback is the throttle (one redraw per frame). Repaints coalesce into a single present.

Adopted (D4-1, 2026-06-08). AgtApp::bind_seat_ attaches the frame clock to its loop on the active window’s compositor (alongside the input seat, with the symmetric detach in bind_seat_(nullptr) and the window dtor). Because the compositor stores one pending frame callback per surface (Wayland wl_surface.frame, latest-wins) and AGT widgets share their window’s single surface, the AgtWindow owns that one frame slot and fans each tick out to every registered animator — so concurrent effects can’t clobber each other’s slot and they coalesce into a single present per tick. The reusable AgtAnim helper (<agt/agt-anim.hpp>) is a 0→1 eased state machine that start() registers with its host window’s pump; the D4 flare effects (smooth scroll, dialog fade/scale, …) embed one.

What rides the frame clock vs what stays on axl_loop_add_timer. The frame clock fires at the display cadence (~60 fps) while any surface has a pending frame, then goes idle. That is the right driver for genuinely per-frame smooth motion (eased scroll/zoom, fades). It is the wrong driver for slow discrete or periodic work: AgtCursorBlink (a 530 ms caret blink) and the hexview volatile/status poll (a 250 ms re-read) deliberately stay on their own axl_loop_add_timer — putting them on per-frame callbacks would wake them ~33× / ~15× more often for no benefit (each tick just a time-check), and both already idle correctly on their own. (This is a considered deviation from the earlier note here that listed them as migration targets — the wakeup-cost analysis says don’t.) Plain one-shot scheduling (a 4 s auto-exit, a debounce) likewise stays on axl_loop_add_timer. Constraint carried from the editor design: a frame-driven pre-boot app, so every animated effect must degrade to instant if a frame budget is missed — AgtAnim is time-based (a missed frame self-heals by jumping progress forward) and effects gate on AgtAnim::can_animate() to snap to the settled state when no clock is reachable (no async, watch heap churn).

Retained rendering & draw-call tracing: AxlGfx display list (future/optional)

axl-sdk also shipped a retained command buffer (AxlGfxDisplayList, phase G9): every immediate-mode axl_gfx_* primitive has an axl_gfx_dl_* analogue that records an op instead of drawing, and axl_gfx_display_list_replay re-issues them against the active target. Two AGT uses, neither blocking:

  1. Draw-call tracing in tests. AGT’s unit suite asserts render structure through a hand-rolled RecordingDrawContext fixture (e.g. counting the offset/hex/ASCII text runs per hex row). The display list is purpose-built to supersede such a fixture — record a widget’s draw() into a list and introspect the ops (axl_gfx_display_list_op_at) with the real primitive vocabulary instead of a parallel recorder we maintain ourselves.

  2. Retained replay for any subtree whose draw is expensive but static between frames (record once, replay per frame). AGT’s immediate-mode draw is fine today, so this is opportunistic, not a redesign.

Display-mode selection & HiDPI scaling (planned adoption)

axl-sdk grew an EDID-driven display layer (<axl/axl-edid.h> + axl-gfx-surface.h: axl_gfx_set_native_mode, axl_gfx_get_dpi, axl_gfx_scale_for_dpi, axl_gfx_recommended_scale, axl_gfx_output_count/_get). Two AGT opportunities:

  1. Prefer the panel’s native mode (low-risk, do first). AGT today defaults to axl_gfx_max_mode() (AgtApp::set_resolution, axedit startup) — which the SDK explicitly warns “can land on a scaled / letterboxed non-native resolution.” axl_gfx_set_native_mode() switches to the EDID preferred timing (the real panel resolution) and fails cleanly when there is no EDID, so it is safe to attempt and fall back to max_mode(). Plan: AgtApp gains a use_native_resolution() that tries native then falls back; axedit’s “largest mode” default becomes “native, else largest.” A --res WxH override still wins.

  2. DPI-aware integer UI scale (strategic, bigger). AGT has a FIXED AgtPalette text_size and pixel metrics (paddings, frame widths, the 16 px scrollbar, icon sizes), so on a HiDPI panel the whole UI renders tiny. axl_gfx_recommended_scale() returns an integer factor (1 / 2 / 3, from the smaller-axis DPI; 1 when no EDID — the VM case) — exactly a toolkit’s HiDPI knob. Plan: at startup multiply the palette/style metrics by that factor. Do it by scaling the metrics, not wrapping the scene in the affine axl_gfx_scale() context — integer metric-scaling keeps text and 1 px borders crisp, where transform-scaling the whole framebuffer blurs them. This is the editor’s existing Ctrl-zoom generalized from “text only” to “the whole UI,” and pairs with native-mode selection (native res + correct scale = a usable HiDPI UI). Distinct from the editor’s font zoom (a user preference on text); the UI scale is a per-display hardware fact.

  3. Multi-output enumeration (axl_gfx_output_count/_get, AxlGfxOutput) — parked. AGT apps render to one GOP framebuffer; multi-monitor pre-boot layout is out of scope until a consumer needs it.

Widget construction — positional ctors + fluent builders

Every widget can be constructed two equivalent ways:

  1. Positional constructor — the FOX shape, e.g. new AgtButton(parent, x, y, w, h, "OK", &dlg, ID_ACCEPT). Geometry first, then the widget’s own args (label, target, …). A second, geometry-free ctor (new AgtButton(parent, "OK", …)) exists where a widget can size itself to its content for a layout container to place.

  2. Fluent builderWidget::build(parent).<setters>.create(), e.g.

    AgtButton::build(parent)
        .bounds(0, 0, 100, 36)      // or .at()/.size(); omit for geometry-free
        .label("OK")
        .target(&dlg, AgtDialog::ID_ACCEPT)
        .create();
    

    The builder names every field at the call site (killing the “which arg is which” problem of the long positional forms) and reads like the FluentGlutApp example. It is purely additive — the positional ctors are untouched, both forms coexist, and create() produces a widget byte-for-byte identical to the matching positional call.

How it works. build() returns a throwaway stack value (a WidgetBuilder) that accumulates config; create() constructs the widget into the parent tree and returns the raw, parent-owned pointer (freed by the normal destructor cascade — the builder owns nothing, so there is no RAII change versus new). The builders form a CRTP mixin stack that mirrors the widget inheritance, so each property is defined once at the level that owns it:

Builder mixin

Setters

Mirrors

AgtBuild<Self>

at / size / bounds / disabled / hidden

AgtWidget

AgtFrameBuild<Self>

border / border_width / padding / bg / themed_bg

AgtFrame

AgtLabelBuild<Self>

label / fg / align / text_size / wrap

AgtLabel

AgtButtonBuild<Self>

target

AgtButton

CRTP lives on the builders, not the widgets, so each setter returns the derived builder type and chaining composes across all four levels without the covariant-return trap — and the widget classes, the AGT_MAP_* message maps, and the constructors stay untouched. A concrete AgtFooBuilder derives the level matching its widget (AgtSliderBuilder : AgtFrameBuild<…>, AgtCheckBoxBuilder : AgtButtonBuild<…>, …) and adds only its own widget-specific setters.

Conventions. Entry build(), terminal create(); geometry verbs at/size/bounds; boolean flags are bare predicates (disabled()/checked()/flat(), each defaulting to true); value setters mirror the widget’s property noun (.label() not .text(), .target() not .command(), .fg() = label color); common setters only — rare cosmetic setters (corner radius, gradients, tooltips) stay as post-create() widget->set_x() calls (which still chain naturally off the returned pointer). Container builders construct the empty container; children are added to it normally afterward.

The pattern is a GoF Builder with Cline’s Named Parameter Idiom setters and a Fowler fluent interface. A declarative whole-tree variant (nested-children builders) was considered and deliberately not built — the imperative parent-pointer tree stays the model.

Dialogs — modal and modeless

AgtDialog : AgtWindow renders on a hosted child surface of the parent window’s compositor, and runs in one of two modes:

  • Modalint run(app) blocks in a nested axl_loop_iterate_until, grabs the seat (input is confined to the dialog), and dims the background with a frosted veil. It returns a result code on dismissal. AgtMessageBox / AgtPromptDialog / AgtFileDialog wrap this; their OK/Cancel buttons target the dialog itself with the result code as the command id, so on_command_dismiss exits run() with that value.

  • Modelessvoid show(app) floats the dialog over a fully interactive parent: non-blocking (returns immediately; the app’s normal run() loop keeps driving both), no seat grab, no veil. For a find/replace panel, a tool palette, a properties inspector. close(code) / is_open() manage its lifetime, and set_on_close( target, id) registers the completion command (a non-blocking dialog can’t return a code, so the outcome arrives as AGT_SEL_COMMAND(id) and the caller reads result()). dismiss() routes to close() while modeless, so the OK/Cancel buttons and Enter/Escape close it exactly as they dismiss a modal.

    The modeless surface is the same fullscreen child surface as modal, but cleared transparent (the parent shows through) with its compositor input region clipped to the card rect — clicks outside the card fall through to the parent, clicks on it route to the dialog. Keyboard follows the pointer via click-to-focus (AgtWindow::on_surface_button claims the seat keyboard focus for the clicked surface), so both the parent and the floating dialog stay typeable. Lifetime: a modeless dialog must outlive show() — keep it on the heap or as a member, never a stack local (unlike the blocking modal AgtMessageBox::info helpers).

Implementation plan

Phase 0 — axl-sdk substrate (in axl-sdk repo)

Estimate: 2–4 weeks.

axl-gfx gap closure (deltas from today’s surface):

  • [ ] Clipping rectangles (axl_gfx_push_clip / pop_clip)

  • [ ] Double-buffering primitive (off-screen buffer alloc + present)

  • [ ] Color blending (alpha composite)

  • [ ] Line / rect-outline / polyline drawing

  • [ ] Font metrics API (advance width, ascent, descent) — sets up for a real font system without committing to a format

  • [ ] ~~Vblank-aware present (frame pacing primitive)~~ — deferred. UEFI’s GOP exposes no WaitForVerticalRetrace-style API; any implementation would either be a fake 16ms-sleep stub (the punt-and-document anti-pattern) or hardware-specific MMIO that doesn’t generalize. AGT v0.1 has no animation (per Open Questions: Animation), so no consumer needs this yet. When a real animation consumer appears, design will be informed by actual frame-pacing needs (vsync vs fixed-tick vs timing-aware compositor) rather than guessed in advance.

Each addition validated against substrate discipline rule 6.

axl-input new module (src/input/, header <axl/axl-input.h>):

  • [x] AxlInputType discriminator + unified AxlInputEvent struct (type, timestamp, coords, buttons, wheel, keycode, unicode, modifiers) + AXL_INPUT_BUTTON_* / AXL_INPUT_MOD_* bitfields + AxlInputCallback typedef

  • [ ] Mouse — axl_input_attach_mouse(loop, cb, data) that registers EFI_SIMPLE_POINTER_PROTOCOL’s WaitForInput event via axl_loop_add_event, reads pointer / button state on dispatch, produces AxlInputEvent for the callback. Touch via EFI_ABSOLUTE_POINTER_PROTOCOL layered on later.

  • [ ] Keyboard — axl_input_attach_key(loop, cb, data), thin wrapper over the existing axl_loop_add_key_press that translates AxlInputKey into the unified AxlInputEvent shape so callers register one callback signature for all input kinds.

  • [ ] Tests in QEMU (mouse + keyboard injection harness)

Architectural note: Phase 0g initially shipped a parallel axl_input_poll / axl_input_wait event-queue API. It was retracted in commit dbbf2a4 once we noticed axl-loop already exposes the source-registration pattern we needed (axl_loop_add_event, axl_loop_add_key_press). The current design uses axl-loop directly via thin axl_input_attach_* wrappers — DRY, no parallel queue.

Both modules: strict C, paradigm-agnostic, fully tested. Phase 0 ships in an axl-sdk minor release before AGT bootstraps.

Phase 1 — C++ toolchain validation (in axl-sdk repo) — DONE 2026-05-28

Originally framed as “axlmm CPP1 toolchain validation”, this phase conflated two distinct concerns that the validation work surfaced:

  • (1a) C++ toolchain end-to-end — does g++ + freestanding-UEFI

    • linker + crt0 produce a working binary? What subset of libstdc++ is usable? What about exceptions / RTTI / global ctors / operator new? This is what AGT actually depends on.

  • (1b) axlmm wrapper class library — the C++ wrapper classes over the C public surface. Originally bundled with 1a as a single phase; revealed during validation to be a separate axis of work that doesn’t gate AGT.

Phase 1a shipped 2026-05-28 (commits ce3d6e6, 3a8fecb, fa2b636, 6f833cc, 9a3d32a, e02f9b8):

  • [x] axl-c++ wrapper script + axl-cc dispatch by file extension; pure-C consumers unaffected

  • [x] libaxl-cxx.a archive with operator new/delete (scalar + array + sized + placement) routed to axl_malloc / axl_free, plus __cxa_pure_virtual stub

  • [x] src/runtime/axl-cxxabi.c in libaxl.a: __dso_handle, __cxa_atexit (→ axl_atexit), .init_array walker called from _axl_init so global ctors fire before main

  • [x] Validated end-to-end on X64 + AARCH64: classes, virtual dispatch, lambdas, move semantics, global ctor/dtor LIFO, operator new/delete, header-only <array> / <span> / <string_view> / <type_traits> / <utility> / <initializer_list> / <new>

  • [x] Exceptions: PROVEN BROKEN (libsupc++ symbols unresolvable: __cxa_throw, _Unwind_Resume, __gxx_personality_v0). AGT design adapts: no throw/ catch, no std::stdexcept, error-code returns throughout

  • [x] RTTI: PROVEN BROKEN (__dynamic_cast, __cxxabiv1::__*_type_info vtables unresolvable). AGT design adapts: no dynamic_cast, no typeid; AgtObject hierarchy uses virtual dispatch only

  • [x] -ffixed-x18 added to AArch64 builds (latent UEFI ABI bug surfaced during toolchain investigation; independent of C++ but fixes a pre-existing problem)

  • [x] scripts/install-arm-toolchain.sh for the pinned ARM bare-metal toolchain (aarch64-none-elf-g++ 14.3.Rel1); install.sh auto-detects and builds C++ variant when present

The full validation findings + revised toolchain constraints are documented in AXLMM-Design.md §”Toolchain & constraints”.

Phase 1b (axlmm wrapper class library) — DEFERRED indefinitely. See AXLMM-Design.md §”Implementation status” and “Why AGT doesn’t depend on axlmm in v0.1” below.

Phase 2 — AGT bootstrap — CORE COMPLETE (started 2026-05-28; widget set + dialogs + menus + showcase landed 2026-05-29)

Estimate: 2–3 months to AGT v0.1.

Status (2026-05-29): every planned Phase 2 sub-phase through 2.7g has shipped — the FOX-shape widget chain (Frame / Label / Button / CheckBox / RadioButton / EditField / Slider / ProgressBar / Image), layout containers (VBox / HBox), modal dialogs (Dialog / MessageBox / PromptDialog), the menu system (MenuItem / MenuBar / AgtMenu with full keyboard nav), and the agt-demo showcase. make test runs 1908 assertions (954/arch); 12 visual baselines 0-diff. What remains for v0.1 is judgment-gated, not bootstrap: the data-display controls (ListBox / ComboBox / ScrollBar / ScrollFrame — deferred above) and the deferred polish items (submenus, translucent modal veil, image scaling, word-wrap, AgtToolBar / AgtStatusBar composites).

AGT bootstrap in aximcode/agt, calling axl-sdk C APIs directly. Originally framed as one item, broken into sub-phases as the bootstrap landed in aximcode/agt:

Phase 2.0 — Repo skeleton (DONE 2026-05-28, commit 7a72214):

  • [x] aximcode/agt repo bootstrapped: Makefile, axl-c++ pipeline, hello.efi proving end-to-end build on X64 + AARCH64

  • [x] AArch64 visual demo validated via run-qemu.sh --gpu (added in axl-sdk commit 99bfe5a for this). Without --gpu the AArch64 virt machine has no display device

Phase 2.1 — AgtApp (DONE 2026-05-28, commit bb48383):

  • [x] First real C++ class wrapping AxlLoop with RAII semantics; app.run() / app.quit() / app.loop() accessor

  • [x] Demonstrates AXL_AUTOPTR(AxlLoop) ownership pattern in a real class member, not just a local

Phase 2.2 — Polish (DONE 2026-05-28):

  • [x] 2.2a — File layout matches axl-sdk (commit 2535054): include/agt/<class>.hpp, src/<source>.cpp, examples/<demo>.cpp, tools/<app>.cpp, test/{unit,integration}/. libagt.a packaged via axl-c++ -c + ar rcs; consumers link via axl-c++ my-app.cpp libagt.a -o my-app.efi

  • [x] 2.2b — Doc migration (commits 6a001a3, 6ef4d1c, 15f71af): AGT-Design.md (this doc) and AGT-Coding-Style.md migrated from axl-sdk to here; project naming unified on AGT throughout (no more AGL/Agl separate class prefix); slim project CLAUDE.md pointing at the canonical docs

  • [x] 2.2c — Sphinx + Doxygen docs pipeline (commit 698f971): docs/sphinx/ + scripts/build-docs.sh + .github/     workflows/docs.yml deploying to Cloudflare Pages project agt-docs. Mirrors axl-sdk’s pipeline with C++-specific adaptations (doxygenclass for the C++ domain, *.hpp input pattern, no allocation-macro filter)

Phase 2.3 — AgtObject + AgtWidget core (DONE 2026-05-28, commit 4ca00f0):

  • [x] AgtObject: parent pointer + intrusive doubly-linked child list (first/last + prev/next sibling); virtual destructor cascades depth-first; add_child / remove_child / child_count API

  • [x] AgtWidget : AgtObject: bounding rect (x, y, w, h), dirty_ flag with mark_dirty() ancestor walk, virtual draw(AgtDrawContext &) + handle_event(const AgtEvent &) stubs. AgtDrawContext / AgtEvent are minimal placeholder classes — filled in by Phase 2.4 / 2.5

  • [x] hello.efi grown to a LoggedNode cascade demo proving construction/destruction order on both arches

Phase 2.4 — AgtWindow + double-buffered render (DONE 2026-05-28, commit 1092eba):

  • [x] AgtWindow : AgtWidget core (top-level, owns back buffer); headless mode (no GOP) leaves back_buf_ NULL and redraw() no-ops, keeping integration tests buildable

  • [x] Double-buffered render via axl_gfx_buffer_new / axl_gfx_target_buffer / axl_gfx_buffer_present; render_subtree free function does the depth-first walk

  • [x] AgtDrawContext filled in: RAII over axl_gfx_target_buffer, plus origin_x_/y_ accumulator that the render walk pushes via translate() / untranslate() as it descends. Clip rect deferred (axl-gfx already has its own push_clip/pop_clip stack; widgets push directly when needed)

  • [x] hello.efi grown to a three-panel demo (left, right, nested inside right) proving parent-local coord accumulation on both arches

Phase 2.4b — Visual regression harness (DONE 2026-05-28, commit c10bef2):

  • [x] scripts/visual-diff.py — PIL-based pixel diff with allowed-pixel-count + per-channel-fuzz tolerances; emits a side-by-side expected | actual | red-overlay diff PNG on failure

  • [x] scripts/test-visual.sh — for each (demo, arch), QEMU-launches the .efi with --screenshot, diffs against test/expected/<demo>-<arch>.png

  • [x] scripts/update-baselines.sh — regenerate workflow: rebuild → capture → compare-and-replace, with explicit “eyeball before committing” warnings per case

  • [x] Per-arch baselines checked in: test/expected/hello-x64.png + hello-aa64.png. Default tolerance is 0 (exact match) — empirically two consecutive captures of the same demo on the same arch are pixel- identical

  • [x] .github/workflows/visual.yml — CI gate. Untested in actual CI until both aximcode/agt + aximcode/axl-sdk are published (currently both local-only per feedback_release_approval_gate); workflow file is the documented intent

  • [x] make test-visual + make update-baselines — Makefile targets, built both arches as dependencies

  • Lives in agt rather than axl-sdk for now (axl-sdk’s gfx-demo has no baseline today; adding one would pile onto axl-sdk’s already-34-commits-unpushed pile). Scripts are AGT-agnostic — demo names + arches are configurable arrays at the top — so factoring out to axl-sdk when wanted is mechanical

Phase 2.4c — Unit-test harness with ratchet (DONE 2026-05-28, commit afe2252):

  • [x] test/unit/agt-test.hpp — minimal C++ port of axl-sdk’s axl-test.h; stateless (PASS/FAIL emitted to serial, runner counts via grep) so every suite shares one agt-test.efi without per-TU counter fragmentation

  • [x] test/unit/agt-test-{object,widget,draw-context}.cpp — 69 assertions covering AgtObject (intrusive child list, cascade, reparent, independent delete), AgtWidget (bounds, dirty propagation, parent attachment), AgtDrawContext (origin accumulator, translate/untranslate round-trip)

  • [x] test/integration/test-agt.sh — runs agt-test.efi per arch under run-qemu.sh (-nographic + auto-emitted reset -s in startup.nsh), aggregates PASS counts across arches, ratchets against .last-pass-count (gitignored, matches axl-sdk’s per-developer-floor convention)

  • [x] make test — builds tests for both arches + runs the harness. Currently 138 assertions (69 × 2 arches)

  • [x] .github/workflows/test.yml — CI gate. Same untested-until-published caveat as visual.yml

  • [x] END-marker check (### AGT TEST RUN END ###) catches crashed-partway-through binaries before they taint the ratchet

Phase 2.5a — Message-map infrastructure + filled-in AgtEvent (DONE 2026-05-28, commit 9cf30c3):

  • [x] AgtEvent: filled in with selectors (AGT_SEL_* constants 0..127 reserved for the framework), button/modifier bitfields, position + wheel + key payload, sender + message_id + consumed dispatch metadata

  • [x] AgtMetaClass + AgtMessageMap + AGT_MAP_* macros (AGT_DECLARE_MAP, AGT_MAP_BEGIN/END, AGT_MAP_COMMAND, AGT_MAP_COMMAND_RANGE, AGT_MAP_EVENT). Trampoline is a template that extracts the derived class from the member-pointer type and emits a safe static_cast<Class *> — no FOX-style reinterpret_cast on pointer-to-member.

  • [x] AgtObject::handle_message walks the metaclass chain from the dynamic type’s map up to AgtObject::metaclass (terminator, base_class == nullptr). First match wins.

  • [x] AgtWidget::handle_event default delegates to handle_message; subclasses can override for custom routing (Phase 2.5b’s AgtWindow will hit-test before dispatching).

  • [x] 46 new unit tests (5 AgtEvent + 9 message-map dispatch including class-chain walk, id ranges, sentinel termination, sender passthrough) — total assertion count 138 → 230

Phase 2.5b — AgtButton + hit testing + axl-input routing (DONE 2026-05-28, commit 5632a14):

  • [x] AgtButton widget: label, pressed/hover/disabled states, click detection (press + release both inside = click; release outside cancels via LEAVE clearing both flags)

  • [x] AgtWindow::widget_at + dispatch_input — hit-test walks tree depth-first, LAST-child first so the topmost widget wins overlaps (matches render order). Synthetic AGT_SEL_ENTER / AGT_SEL_LEAVE emitted on hover transitions; mouse / touch events delivered in widget-local coordinates.

  • [x] AgtApp::set_window attaches axl_input_attach_mouse + axl_input_attach_key; static callbacks translate AxlInputEventAgtEvent and call dispatch_input + redraw.

  • [x] hello.efi gains three buttons (Quit, Change Color, Disabled) via a HelloWindow : AgtWindow subclass that handles ID_QUIT + ID_CHANGE_COLOR through AGT_MAP_COMMAND. Per-arch baselines regenerated.

  • [x] Bug fix: the Phase 2.4 render_subtree walk double-translated — it both ctx.translate(w->x(),     w->y())’d before draw AND DemoPanel’s draw added ctx.origin_x() + x(). Latent because Phase 2.4’s visual baseline captured the doubled coords as “expected”. Surfaced when AgtButton’s y=600 rendered at y=1200 (off-screen). Fixed by codifying the contract “ctx.origin_x() IS the widget’s top-left in target coords”; AgtButton::draw uses it directly.

  • [x] +73 unit assertions (AgtButton state machine + AgtWindow hit-test + dispatch); total 230 → 326 (163 × 2 arches).

Phase 2.5c — Code-review pass + render-walk unit tests (DONE 2026-05-28, commits 6107b7d + 3bde9bd + 90e9ff5):

  • [x] Independent code-review agent ran on the Phase 2.5b codebase, producing ~200 findings. Applied: all 14 bugs, ~70 of ~87 smells, ~20 of ~23 doc-drift items, ~18 of ~21 style items, 12 of 14 test gaps. Remaining ~17 items explicitly deferred with rationale (need axl-gfx API extensions, scope-guard helpers, mock layer, etc.).

  • [x] Render-walk unit tests pin the coord math without relying on the visual baseline. AgtWindow::render_subtree exposed as a public static so tests drive the walk with a synthetic AgtDrawContext(nullptr) and a CapturingWidget fixture that records ctx.origin_x() per draw call. Catches the Phase 2.4 double-translate class of bug without an eyeball.

  • [x] Test count: 326 → 448 assertions (224 × 2 arches).

  • [x] Visual baseline unchanged (still 0-diff).

  • [x] New feedback memory agt-no-k-prefix-constants — captures that AGT/axl-sdk constants are SCREAMING_CASE, not Google’s kCamelCase (user-caught during this pass).

  • [x] Updated agt-tdd-mandatory memory — test-first is the preferred AGT pattern but not strict (user judgment-call, diverging from axl-sdk’s mandatory rule).

Phase 2.6 — AgtDrawContext routing + record-mode for testability (DONE):

Widget draw implementations used to call axl_gfx_* directly (axl_gfx_fill_rect, axl_gfx_draw_text, etc.), which meant tests could only verify rendering via pixel-perfect visual diff. That’s a regression test, not a correctness test — a wrong-since- day-one bug encodes itself into the baseline (Phase 2.4 cautionary tale). The render-walk tests added in 2.5c partially closed the gap (coord math is now unit-tested) but the actual draw decisions (colors picked by state, label positioning, clip handling) still went through opaque axl-gfx calls.

Fix shipped: lift the draw primitives onto AgtDrawContext. Widgets call ctx.fill_rect(...), ctx.draw_text(...), etc. Production AgtDrawContext delegates to axl_gfx_* (single virtual call per draw — negligible vs the pixel-shuffling work behind each one). Tests use the RecordingDrawContext subclass that captures every call into a flat trace buffer and assert against expected calls — pixel-free, deterministic.

  • [x] axl_gfx_get_current_target() added in axl-sdk v0.19.3 — paradigm-agnostic query used by AgtDrawContext to save/ restore the outer caller’s target across nested contexts (closes deferred code-review item #69)

  • [x] Draw methods added to AgtDrawContext: fill_rect, draw_text, clear, push_clip, pop_clip (each virtual, 1-line forwarder to the matching axl_gfx_*)

  • [x] AgtDrawContext ctor captures the previously-active target via the new query, dtor restores it — nested contexts compose correctly

  • [x] RecordingDrawContext (test fixture, header-only) flat- captures up to 128 ops; tests assert on count() / at(i).op / op-specific fields

  • [x] Migrated AgtButton::draw + AgtWindow::draw + HelloWindow::draw to ctx methods. Visual baseline still 0-diff on x64 + aa64 (make test-visual)

  • [x] Dropped the if (!ctx.buffer()) return; headless gate from AgtButton::draw + AgtWindow::draw — ctx methods no-op cleanly at the axl-gfx layer when headless, and removing the gate is what makes RecordingDrawContext see the full trace without a sentinel-buffer trick

  • [x] Render-trace tests in test/unit/agt-test-render-trace.cpp: AgtButton state→color (normal / hover / pressed / disabled), label centering math (horizontal, vertical, inset fallback), enabled/disabled label color, text round-trip, empty/NULL label suppression, scale/font defaults, AgtWindow background clear, AgtDrawContext nested save/restore, fixture self- tests. 448 → 560 assertions per arch (+112 total)

  • [x] Visual baseline unchanged — verifies the production AgtDrawContext + axl-gfx path end-to-end (integration concern), while unit tests verify AGT’s render-decision intent (correctness concern)

Design note on the routing: the AGT-Design principle “AGT calls axl-sdk C APIs directly” is about not building a wrapper library (axlmm); having a per-frame routing point at AgtDrawContext is a thin abstraction over the draw target, not a library wrapper. Matches what real toolkits do — FOX’s FXDC, GTK’s cairo_t. Widgets can still drop down to raw axl_gfx_* when they need a primitive the context doesn’t expose; the context is the default path because it’s the testable path.

If axl-gfx’s draw API needs adjustments to make the routing cleaner (e.g. a “record current target” handle for nested contexts per the deferred review item #69), axl-sdk changes are fair game — both repos are pre-1.0 and unpushed.

Phase 2.6b — Scripted scenario test DSL (defer until widget count justifies):

Once AGT has multiple interactive widgets (post-Phase 2.7 controls), end-to-end gesture tests become valuable. A thin DSL on top of dispatch_input + state inspectors:

Scenario s(&window);
s.move_mouse(150, 150).expect_hover(&btn_quit);
s.press_left()         .expect_pressed(&btn_quit);
s.release_left()       .expect_commands(1, ID_QUIT);

Deferred — premature with one button.

Phase 2.7+ — Widget set (after 2.6):

  • [x] Phase 2.7a — AgtFrame + AgtLabel + AgtButton refactor (DONE, bundled three commits) — FOX-shape chain AgtButton : AgtLabel : AgtFrame : AgtWidget inserted. AgtFrame contributes border style (NONE / LINE / RAISED / SUNKEN — THICK/GROOVE/RIDGE deferred but reserved in the enum), per-side padding, and a virtual effective_bg_color() hook subclasses override for state-dependent palettes. AgtLabel composes text caption with LEFT / CENTER / RIGHT alignment within the post- border, post-padding inner rect. AgtButton becomes a thin state-machine subclass with a raised border by default. enabled lifted from AgtButton to AgtWidget (matches FOX FXWindow::isEnabled). New label-demo.cpp showcases all four border styles; hello.efi baseline regenerated for the button bevel. Tests: 560 → ~820 per arch (3 commits).

  • [x] Phase 2.7-substrate-adopt (DONE, bundled two commits 0d7fca6 + this commit) — AGT consumes axl-sdk’s G1/ G2/G3 + fill_rect_i surface that landed 2026-05-28. Commit A (plumbing): three new virtuals on AgtDrawContext (fill_path / stroke_path / fill_rounded_rect) + matching RecordingDrawContext captures; fill_rect body simplified from inline negative-coord clamping to a forward to axl_gfx_fill_rect_i; production-path integration test pins the swap is behavior-preserving. Commit B (this): adds AGT_FRAME_ROUNDED border style + corner_radius_ field on AgtFrame; label-demo.cpp extended with a centered ROUNDED cell; visual baseline regenerated. G1 (AxlTtf text) and G2 (AxlPixmap images) are queued separately as 2.7-ttf-adopt and 2.7-image-adopt — each has open questions (font asset bundling, AgtImage shape) that warrant their own scope.

  • [x] Phase 2.7b — Module restructure + AgtVBox + AgtHBox + layout-demo.cpp (DONE, bundled two commits) — restructured src/ into per-module subdirs (core / render / widgets / layout) mirroring axl-sdk’s convention, with README.md per module as the single source of truth (Sphinx pages are thin myst_parser-include wrappers). Headers stay flat under include/agt/ so consumer #include lines don’t change. New layout module ships AgtVBox + AgtHBox with deliberately minimal v0.1 semantics (children retain their own size; container only positions them with configurable spacing; non-widget children skipped; layout-before-paint ordering so bg/border render on top of placed children). Auto-stretch + cross-fill SHIPPED later (AgtWidget::set_stretch(weight) main-axis proportional growth + set_fill_cross() + AgtGrid row/col weights + AgtSpacer springs; opt-in, 0-diff defaults — Qt/FOX LAYOUT_FILL_* shape). New examples/layout-demo.cpp showcases nested HBox-containing-VBox composition. Test count 830 → ~895 per arch.

  • [x] Phase 2.7c — Scripted scenario DSL (DONE, commits 7851e14 + 9a9e1d3)test/unit/agt-scenario.hpp Scenario fluent DSL drives AgtWindow::dispatch_input through the real hit-test / hover / dispatch path, with inline expect_* assertions and a RecordingDrawContext trace integration. CommandRecorder consolidated the per-file CountingTarget fixtures. In heavy use across every subsequent control / dialog / menu suite.

  • [x] Phase 2.7d-a — AgtCheckBox + AgtRadioButton + controls-demo.cpp (DONE, single commit) — first slice of Phase 2.7d’s controls. Both inherit AgtButton (reuse hover/pressed state machine + click contract + target wiring), add a checked_ / selected_ state field, a left-side indicator (square for checkbox, circle for radio), and a left-anchored label. AgtButton gains a virtual on_clicked() hook called BEFORE the SEL_COMMAND emit so subclasses mutate state in time for the handler to see the new value via the sender. AgtRadioButton’s on_clicked walks parent siblings (metaclass-pointer comparison; no RTTI) and clears any same-target_id sister radio for mutual exclusion. New controls-demo.cpp showcases both controls with pre-set mixed initial states. Test count 892 → ~975 per arch.

  • [x] Phase 2.7d-b/c — Core controls (DONE)AgtProgressBar (dd2a9cb), AgtWindow mouse-capture API (fa53b97), AgtSlider (c6cc882), AgtEditField (b1bd370, first keyboard-focus consumer) + keyboard focus routing (43a7995). AgtImage adopted via 2.7-image-adopt (15ae661, adds AgtDrawContext::blit). Shipped with widgets-demo.cpp.

  • [~] Phase 2.7d — data-display controls (IN PROGRESS): AgtScrollBar / AgtListBox / AgtComboBox / AgtScrollFrame / AgtSpinBox. These back unbounded datasets — per the Data-structure policy section, their backing stores consume axl-sdk containers (AgtListBox rows are an axl-array of axl_strdup-owned char*).

    • [x] AgtScrollBar (ddc4fb5) — proportional thumb, track-paging, thumb-drag; sibling to AgtSlider, range = (total, page).

    • [x] AgtListBox (54c758a) — scrolling single-selection list; owns an AgtScrollBar child; keyboard / wheel / click; axl-array-backed rows.

    • [~] Coordinate-model redesign (PREREQUISITE) — adopt the Qt/GTK affine context (see Coordinate model section). Phased, behavior-preserving: (1) AgtTransform affine value type 30bf1e0; (2) context transform + save/restore stack + map_to_window helper 61d4dd6; (3) the flip — forwarders apply the transform, widgets author local coords, RecordingDrawContext records local, trace tests → local 8b71f13; (5) hit-test child-clip (wants_child_clip) 6f9bd24; (6) retire duplicate parent-walks 412c757. (4) DONE — dropped AgtTransform for axl-sdk’s unified AxlTransform and routed every forwarder’s rotated/scaled path through the axl transform primitives (fill_path quad, axl_ttf_draw_transform, push_clip_rect_transformed, blit_transform), keyed off axl_transform_classify. Proven by transform-demo + its x64/aa64 baselines; the translate-only widget walk is unaffected.

    • [x] Event bubbling (PREREQUISITE, 77cda84)dispatch_input walks an unconsumed positional event up the parent chain (the model the core README documented but the code lacked); wheel-over-content reaches the scroll ancestor.

    • [x] AgtScrollFrame (6589a4b) — clipped viewport over arbitrary child content + owned scrollbar(s). Overrides wants_child_clip (local rect = its viewport) so render_subtree + hit-test confine children; holds one content_ child at (inner_offset - scroll); wheel + bars drive scroll_to (silent bar sync; bar.page == viewport so no clamp desync).

    • [x] AgtComboBox (f19d7bf) — collapsed field + dropdown popup REUSING AgtListBox (popup reparent/capture/dismiss model from AgtMenu). Browse vs. commit split; cascade-safe list ownership; commit emits last (UAF-guarded). Added AgtListBox::ensure_visible. Documented limit: dropdown scrollbar not draggable under the capture grab.

    • [x] Finalize (this commit) — data-display-demo.cpp (ListBox + ComboBox + ScrollFrame + ScrollBar) + x64/aa64 baselines; .rst API blocks + module-README entries for the four widgets + AgtStyle + AgtTransform + the JSON5 loader.

    • [ ] AgtSpinBox — deferred (lowest priority of the set).

    • Styling prerequisite SHIPPED: AgtStyle (b91423c) — centralized theme tokens; widgets snapshot defaults at ctor (see the Styling policy section). New data-display widgets are built theme-native against it.

    • Styling follow-up SHIPPED: JSON5 theme-file loader agt_style_load("theme.json5") + agt_style_parse(text, len, &style) mapping keys (the AgtStyle field names) → tokens. Colors are CSS hex strings via axl-sdk’s axl_gfx_color_parse; metrics are JSON numbers; the JSON5 grammar superset (comments / trailing commas / unquoted keys / single quotes) is accepted via axl-json. All-or-nothing overlay (a parse error or unparseable color leaves the target untouched); unknown keys ignored, absent keys unchanged. Purely additive — touches no widgets. Lone documented limit: text_size loads as an integer point size (no string→double parser in axl-sdk yet); fractional sizes remain code-only.

  • [x] Phase 2.7e — Dialogs (DONE, commits fbbf8d6 + febcc38)AgtDialog base (modal via axl_loop_iterate_until + an AgtApp modal stack; focus save/restore with a UAF guard; Tab/Shift+Tab focus cycling via can_focus(); Enter/Escape default/cancel-button shortcuts). AgtMessageBox (OK / OK-Cancel / Yes-No) + AgtPromptDialog (label + edit field) composites. Command dispatch later refactored from per-dialog trampolines to AgtDialog::on_command_dismiss self-target (1b0f2c0).

  • [x] Phase 2.7f — Menus (DONE, commits ec3ac8d + 5df6514)AgtMenuItem (borderless AgtButton + label + right-anchored shortcut + keyboard highlighted_ state) / AgtMenuBar (: AgtHBox of titles; owns popups) / AgtMenu (: AgtFrame, in-window child reparented to the window for paint-on-top z-order; grabs mouse capture + keyboard focus; rows are direct children laid out eagerly). Full Win32-style keyboard nav state machine (F10 / arrows / Enter / Escape / Left-Right menu switch). Submenus (popup-from-popup) deferred post-v0.1.

    Follow-up — menu-wide grab (now part of the input/focus rework, see AGT-Input-Focus-Design.md — the menu session is step 6 of its migration). Today each open AgtMenu takes an exclusive AgtWindow mouse capture (one popup owns the grab at a time). This is why two things are special- cased rather than free:

    • Stays-open had to be hand-coded: a top-level menu opens flush under its bar title, so the pointer is “outside” the popup the instant it moves, and the naive capture model read that as a click-away. Fixed in 69bacda (a top-level AgtMenu keeps its grab and stays open on motion/wheel/release; only a press outside is a click-away). Submenu↔parent navigation is likewise done by a dismiss-and-re-dispatch handoff that passes the single capture up/down the chain.

    • Mouse hover-switch between bar menus is absent. With File open, hovering Edit does NOT switch to it (you must click) — the File popup holds the exclusive grab, so the bar titles never see hover. Keyboard Left/Right already switches; the mouse can’t.

    The mature toolkits avoid both by holding a menu-wide grab for the whole active session rather than per-popup: GTK’s shared GtkMenuShell base (bar + every open menu) runs one navigation state machine under a single seat grab; Win32 runs an internal modal menu loop with the mouse captured for the session; FOX/Qt grab the whole popup-window stack. Motion is then hit-tested across every open surface at once (the bar titles and all open AgtMenus), so moving off one pane onto a sibling pane or back onto a bar title is one continuous grab — nothing dismisses just because the pointer left a single pane, and hover-switch falls out for free.

    Proposed shape for AGT: add a “capture group” / menu-session concept to AgtWindow (a set of widgets that jointly hold the grab), owned by AgtMenuBar for the duration a menu is open. Positional events route by hit-testing the bar + all open AgtMenus in top-of-stack order; a press that hits none of them is the click-away. This subsumes the per-popup capture, the special-cased stays-open branch in AgtMenu::handle_event, and the dismiss/re-dispatch submenu handoff, and adds mouse hover-switch. Scope: AgtMenuBar + AgtMenu dispatch + a small AgtWindow capture-group primitive. Related known issue to fix in the same pass: after the F10 keyboard menu closes, focus does not return to the previously-focused widget.

  • [x] Phase 2.7g — examples/agt-demo (DONE, commit 442de10) — menu bar + tool bar + center VBox of every control + status bar; ×2-arch visual baseline.

Future testability work (slot in as the surface justifies):

  • [ ] Multi-frame visual baselines — capture screenshots at multiple points during a scenario. Only useful once state-transition rendering bugs become a real concern; Phase 2.6’s render-trace covers most cases without QEMU

  • [x] Focus / keyboard scenario tests — landed with the focus chain (2.7-focus-routing), dialogs (2.7e), and the menu keyboard nav state machine (2.7f-B); the Scenario DSL drives key events through dispatch_input to the focused widget. test/unit/agt-test-focus.cpp + the dialog / menu suites cover Tab cycling, Enter/Escape, and the three-phase menu state machine.

  • [ ] Property-based testing for input state machines — random click sequences must leave widgets in well-defined states (universal invariants, not per-widget cases)

No parallel axlmm track — AGT’s actual usage of the C API IS the validation data that would inform a future axlmm wrapper layer (see Phase 4 below).

Phase 3 — First C++ consumer integration

Estimate: open.

Real-application port to AGT. Surfaces missing widgets, layout bugs, dispatch edge cases. Sets the bar for AGT v1.0.

Phase 4 — axlmm wrapper layer (if/when warranted)

Estimate: open; gated on AGT v0.1 ship + usage-pattern data.

After AGT has matured enough to show which axl-sdk C-API patterns get hit repeatedly across the codebase, the axlmm wrapper class library (deferred per AXLMM-Design.md) may resume implementation per the sub-phases in ROADMAP CPP1.7+. AGT can then incrementally migrate from direct C API + AXL_AUTOPTR to axlmm::* wrappers — or stay on the C API if the wrapper layer doesn’t pay off. The migration is incremental; both styles can coexist in the same source file.

Concrete trigger: AGT contributors report repeated ergonomic friction with a specific C-API pattern (sticky error handling, container iteration, factory error returns) that a wrapper would clearly mitigate. Without that signal, the wrapper layer doesn’t get built.

Phase 0z — Module documentation sweep (in axl-sdk) — DONE 2026-05-28

Ran after Phase 0 substrate work completed (axl-gfx gap closure + axl-input shipped), before Phase 1 toolchain validation. Batched at the end of Phase 0 rather than per-commit so the docs caught the final API shapes, not intermediate ones.

Module-level READMEs and Sphinx pages updated to reflect the new substrate surface:

  • [x] src/gfx/README.md — documents AxlFont/AxlGlyph, multiple built-in fonts (LaffStd, Unifont subset), scripts/gen-bdf-font.py for adding more, UTF-8-first input, clipping, double-buffering, line/outline primitives. Vblank-aware present deferred per Phase 0f.

  • [x] src/data/README.md — documents axl_utf8_decode per-codepoint iterator (companion to existing axl_utf8_to_ucs2). Positioned as the UTF-8-first walker for new code; existing UCS-2 helpers remain for UEFI-protocol interop.

  • [x] New src/input/README.md — documents the axl-input module (raw event API, mouse + keyboard + touch sources, axl-loop integration, paradigm-agnostic substrate role per substrate discipline rules).

  • [x] docs/sphinx/modules/gfx.rst — adds .. doxygenfile:: axl-font.h so the new font types surface in generated API docs.

  • [x] New docs/sphinx/modules/input.rst — Sphinx page for axl-input, mirrors the pattern in gfx.rst.

  • [x] docs/sphinx/index.rst lists input.rst.

  • [x] Cross-linked the new sections from AGT-Design.md substrate description.

Not a test-first phase — Sphinx build + visual proofread is the verification. Run ./scripts/build-docs.sh and spot-check the generated HTML for the new pages.

Future track: GNOME-shape AGT successor

Status: planned future direction, not on the near-term schedule. A second widget toolkit alongside (not replacing) AGT, realizing the full GNOME-style 4-library stack on the same axl-gfx + axl-input substrate.

Trigger to start: a real C consumer of widgets emerges, OR a C++ consumer that specifically wants GObject-style runtime features (dynamic signal binding, property change notification, language-binding generation, accessibility / introspection frameworks).

The substrate discipline rules above are designed precisely to guarantee that AGT and this future track coexist on the same axl-gfx + axl-input without either having to refactor the other. AGT consumers are not asked to migrate — the Qt-vs-GTK precedent applies: multiple credible toolkits coexist; consumers pick.

Stack

       C app                        C++ app
         │                            │
         ▼                            ▼
      <agt-gtk>                  <agt-gtkmm>      [separate repos,
         │                            │            names TBD]
         └────────┬───────────────────┘
                  ▼
            axl-object  +  axlmm                  [axl-sdk]
                  │             │
                  └──────┬──────┘
                         ▼
            axl + axl-gfx + axl-input             [axl-sdk]

axl-object is the GObject-equivalent C primitive: minimal runtime type system, signal emit / connect, property get / set / notify, ref / unref, weak references. Sits beside axl-gfx and axl-input in axl-sdk core, available to the future C widget toolkit and any other C consumer that wants runtime object features.

Phased plan (when triggered)

  • Phase G3 (axl-sdk)axl-object primitive. Type registration, signal emit / connect, property get / set / notify, ref / unref, weak references. Modeled on GObject but scoped to what a widget toolkit and a few sibling consumers actually need. Drop the parts of GObject that exist for GObject Introspection / language bindings until those have a consumer in axl-sdk’s world.

  • Phase G4 (new repo) — C widget toolkit, GTK-shape. Same widget-set scope as AGT v0.1 (containers, controls, dialogs, menus, decorative). Signal-driven dispatch instead of message maps; properties for state; CSS-style theming optional and deferred.

  • Phase G5 (new repo) — C++ wrapper of G4, gtkmm-shape. RAII handles, lambda signal handlers, property accessors. Depends on G4 + axlmm (not on AGT or axl-input directly).

What this is not

  • Not a replacement for AGT. Both toolkits ship and are supported independently. The substrate discipline rules above exist precisely to make coexistence cheap.

  • Not pre-built or pre-designed. Phase G3-G5 specs are written when triggered, not in advance. The commitment AGT makes today is exactly the substrate discipline rules — no scaffolding, no API surface, no design docs beyond this section.

  • Not gated on AGT v1.0. If a C widget consumer appears before AGT ships, Phase G3 can start in parallel without blocking AGT.

axl-input substrate sketch

Final shape determined in Phase 0 design. Initial sketch:

typedef enum {
    AXL_INPUT_NONE = 0,
    AXL_INPUT_MOUSE_MOVE,
    AXL_INPUT_MOUSE_BUTTON_DOWN,
    AXL_INPUT_MOUSE_BUTTON_UP,
    AXL_INPUT_MOUSE_WHEEL,
    AXL_INPUT_KEY_DOWN,
    AXL_INPUT_KEY_UP,
    AXL_INPUT_TOUCH_DOWN,
    AXL_INPUT_TOUCH_UP,
    AXL_INPUT_TOUCH_MOVE,
} AxlInputType;

#define AXL_INPUT_BUTTON_LEFT    (1u << 0)
#define AXL_INPUT_BUTTON_RIGHT   (1u << 1)
#define AXL_INPUT_BUTTON_MIDDLE  (1u << 2)

#define AXL_INPUT_MOD_SHIFT      (1u << 0)
#define AXL_INPUT_MOD_CTRL       (1u << 1)
#define AXL_INPUT_MOD_ALT        (1u << 2)
#define AXL_INPUT_MOD_META       (1u << 3)

typedef struct {
    AxlInputType  type;
    uint64_t      timestamp_us;
    int32_t       x, y;            // mouse / touch
    uint32_t      buttons;         // bitfield, current state
    int32_t       wheel_dx, wheel_dy;
    uint32_t      keycode;         // scan code
    uint32_t      unicode;         // translated character
    uint32_t      modifiers;       // bitfield
} AxlInputEvent;

/// Unified-event callback.  Returns AXL_SOURCE_CONTINUE or
/// AXL_SOURCE_REMOVE — same semantics as AxlKeyCallback /
/// AxlLoopCallback.
typedef bool (*AxlInputCallback)(const AxlInputEvent *event, void *data);

/// Mouse source — registers EFI_SIMPLE_POINTER_PROTOCOL's
/// WaitForInput event with the loop via axl_loop_add_event;
/// translates each dispatch into one or more AxlInputEvent calls.
/// Returns the axl-loop source ID (for axl_loop_remove_source).
uint32_t axl_input_attach_mouse(AxlLoop *loop, AxlInputCallback cb, void *data);

/// Keyboard source — thin wrapper over axl_loop_add_key_press that
/// translates AxlInputKey into the unified AxlInputEvent shape so
/// callers can register a single AxlInputCallback for all input
/// kinds instead of separate per-device callbacks.
uint32_t axl_input_attach_key(AxlLoop *loop, AxlInputCallback cb, void *data);

Open questions parked for later

  • Font format. 8x16 bitmap (current) covers v0.1. Real fonts — BDF, tiny in-tree TTF rasterizer, or PSF — is a post-Phase-0 decision once demand for non-Latin text / variable sizes / hinting is known.

  • AxlGfxColor / AxlGfxPixel split (RGBA-vs-BGRA ergonomics). Today AxlGfxPixel is BGRA-ordered to match the UEFI GOP framebuffer exactly (zero conversion cost at present-to-screen time). Callers who prefer RGB-style literals use AXL_GFX_RGB(r, g, b) / AXL_GFX_RGBA macros + the named-color palette (AXL_GFX_RED etc.) in <axl/axl-gfx.h> — these expand to a BGRA compound literal at compile time, so the cost is zero.

    If consumer ergonomic complaints surface despite the macros, the next escalation is a two-type split: AxlGfxColor (RGBA, what users construct and pass into APIs) + AxlGfxPixel (BGRA, storage format for buffers / raw blit data). APIs that take a single color (fill_rect, draw_text) accept AxlGfxColor and convert at the call boundary — single shuffle per call, negligible cost. APIs that take pixel arrays (blit, buffer_pixels) stay on AxlGfxPixel — no per-pixel swap on blit / present.

    This split mirrors GTK/Cairo’s “set color” vs “blit source” separation. Defer until a real consumer pain point appears; the macro approach is intended to be sufficient.

  • Font lookup data structure at scale. Current AxlFont uses a codepoint-sorted glyph array with O(log n) binary search. For built-in subsets (95-400 glyphs) and even hypothetical full BMP fonts (~57K glyphs) this is sub-microsecond per lookup and preserves static .rodata placement (zero init, zero allocation). Hash table or radix tree would regress performance at these sizes and force runtime initialization — wrong tool.

    The right optimization, if a consumer ships full Unicode coverage AND profiling shows text rendering is a bottleneck, is a two-tier dense+sparse layout: contiguous codepoint ranges (ASCII 0x20-0x7E, Latin-1, box-drawing, CJK Unified Ideographs at U+4E00-U+9FFF) become direct array[cp - base] indexing (O(1), preserves .rodata), with sparse remainder via the current binary search. Generator identifies dense spans and emits per-range index tables alongside the sparse array. This mirrors the classic firstChar/lastChar range-based bitmap-font tables but composes with sparse fallback.

    Until that consumer + profile data exist, do not touch the current single-tier binary search. See the design note in include/axl/axl-font.h.

  • Theming. GTK-CSS-level theming is overkill for pre-boot. AGT v0.1 ships one theme; v0.2+ revisits if consumers ask for branding / high-contrast / large-text variants.

  • Animation. Out of scope for v0.1. Frame-pacing primitives in axl-gfx (vblank, double-buffer present) leave the option open. Adding animation later is additive, not invasive.

  • Signal / observable primitive. AGT v0.1 dispatches UI input via message maps (FOX-style). If a need emerges for typed state-change notification (model-changes-view-updates patterns), a small C++ signal/observable template — modeled on Qt’s signals-and-slots or libsigc++, ~200 LOC — would go in AGT or axlmm. Do not pull GObject in for this; layering a C-based runtime type/signal system under C++ widgets pays double abstraction cost (see GObject discussion below in “Future track”). Naming collision warning: axl-sdk’s existing <axl/axl-signal.h> is the POSIX-flavored interrupt handler (Ctrl-C dispatch) — AxlSignal / axl_signal_* is taken. A future observer primitive must use a non-colliding name: AgtSignal<Args...> inside the agl namespace is fine (different namespace, no actual symbol collision), or alternative names like AgtObservable / AgtNotify / AgtEmitter.

  • Accessibility. Out of scope. Pre-boot UEFI is not the environment for screen-reader infrastructure. Theme variants cover the realistic asks (high-contrast, large-text).

  • Internationalization / RTL. Out of scope for v0.1. Font decision (above) interacts with this; revisit when both come into scope together.

  • License. AGT targets Apache-2.0 to match axl-sdk. FOX Toolkit is LGPL; AGT borrows architecture, not code.

  • Repo structure. Mirror axl-sdk layout — include/agt/ headers, src/ implementation, test/, examples/. Doxygen

    • Sphinx + Breathe docs pipeline. Same release process (scripts/bump-version.sh, GitHub releases, .deb / .rpm via fpm).

  • Versioning. AGT versions independently of axl-sdk. AGT pins a minimum axl-sdk version in its build. axl-sdk never pins AGT.