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-sdk —
axl-cc(andaxl-c++alias) compile.cppsource against a freestanding-UEFI g++ + the C++ ABI runtime inlibaxl-cxx.a(operator new/delete /__cxa_pure_virtual) andlibaxl.a(__cxa_atexit/__dso_handle/.init_arraywalker). Shipped 2026-05-28. SeeAXLMM-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 (
FXMAPFUNCanalog)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:
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).
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::expectedfactories over the C public surface) — does NOT gate AGT. Implementation deferred perAXLMM-Design.md.
The reasoning for deferring axlmm:
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.
C++ calls C trivially.
axl-sdkheaders wrap declarations inextern "C". AGT callsaxl_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.RAII is already covered via
AXL_AUTOPTR(Type)— a GCC cleanup attribute macro that g++ supports natively. ~14 axl-sdk types already register it viaAXL_DEFINE_AUTOPTR_CLEANUP. Scope-bound free works in C++ identically to C.axlmm provides ergonomic enhancements, not capabilities.
.write("hi")vsaxl_stream_write(s, "hi", 2)is nicer but not transformative; sticky-error chains save a few lines per call;std::string_viewoverloads 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 callNo
axlmm::*types in headers or implementationC++ 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-inputnew module). Violating them breaks the door-open property for future widget toolkits.
Pure C, no C++ ABI leakage.
axl-gfxandaxl-inputstay strict C. No struct layouts assuming destructors, no function-pointer slots that imply a hiddenthis, no allocators tied to C++ scope.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.axl-inputis toolkit-agnostic. It produces raw events as a unifiedAxlInputEvent(type / coords / keycode / modifiers / timestamp). Source registration reusesaxl-loop(axl-sdk’sGMainLoopequivalent) —axl_input_attach_mouse/axl_input_attach_key/axl_input_attach_touchare thin wrappers overaxl_loop_add_event(for arbitrary EFI events) andaxl_loop_add_key_press(for keyboard) that translate the underlying UEFI protocol payloads intoAxlInputEventand dispatch via anAxlInputCallback. 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. Ifaxl-inputever speaks any toolkit’s dispatch dialect — widget message maps, signal-slot wiring, per-widget handlers — the door closes.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.”
axl-gfxprimitives only grow, never narrow for AGT’s convenience. Once a primitive is inaxl-gfx, it has the broader C contract. AGT may wrap it more narrowly, but cannot askaxl-gfxto shed generality.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:
AgtBitmapButton — SHIPPED. A button whose face is a bitmap / icon (with an optional caption), including a procedural stock-icon mode. (14 diagnostic-app files.)
AgtIcon — SHIPPED. 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 sharedAgtTextEditcore + a verticalAgtScrollBar): cursor as (line, column), Up/Down with a sticky goal column,Enterinserts a newline, vertical scroll (cursor-follow + wheel), and the inner-rect text clip — which back-filledAgtEditField(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 reuseAgtLabel’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 sharedAgtTableBase(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) andAgtTableEdit(an editable cell grid — a single active cell with in-cell editing via an overlaidAgtEditField). Distinct fromAgtGrid, 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::drawsubclass. 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_timercadence AgtSpinner uses). Substrate ready: axl-sdk v0.23.0 addedaxl_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 thinAgtDrawContext::blit_rectforwarder 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
AgtObjectas an intrusive doubly-linked list (FOX-shape). This is deliberate and must NOT be swapped for a node-basedaxl-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, andadd_*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 anAgtTreeView, the entries of anAgtComboBox/AgtFileDialog, the viewport of anAgtScrollFrame— that backing store should be anaxl-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}, thepopup/frame/tooltipdrop-Shadows,gamma_correct). Mutable globalAgtPalette::current(). A JSON5 theme file feeds it viaagt_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 whosedraw_frame/draw_button/draw_checkbox/ … paint widget chrome. A widget’sdraw()is a thinstyle().draw_xxx(ctx, *this)delegation, so the swappableAgtStyle::current()(or a per-subtreeset_style) re-skins the look-and-feel as a unit.AgtStyleis itself the concrete default; subclass + override onedraw_*to retheme a control (Qt’sQFusionStyle : 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’swell_bg, a panel’spanel_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 (abg_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’schangeEvent(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_bgrecolor through the inheritedAgtFrame::restylewith 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 aconst-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 takeAxlTransform*directly, so a wrapper would only add conversions. (Through Phase 3 AGT had its own float 2×3AgtTransform; the Phase 4 consolidation dropped it.) The matrix math is fully general so the model matches QtQTransform/ cairo and needs no API change later.AgtDrawContextcarries a transform + asave/restorestack. The render walk pushes each child’stranslate(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 callingaxl_gfx_*with device coordinates.The substrate stays paradigm-agnostic. The transform lives in the painting layer (
AgtDrawContext), never pushed intoaxl-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_rect—IDENTITY/TRANSLATE/SCALEuse the integer rect path;AFFINE/PROJECTIVEmap the four corners and fill the convex quad viaaxl_gfx_fill_path(a rotated rounded rect drops its corner rounding — no rotated-rounded primitive).draw_text_ttf—IDENTITY/TRANSLATEuse the cachedaxl_ttf_draw; anything above (scale included, since the cached path can’t scalepx_size) routes through the vectoraxl_ttf_draw_transform. Bitmapdraw_texthas 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 viaaxl_gfx_push_clip_rect_transformed.blit—IDENTITY/TRANSLATEuse the cheapaxl_gfx_blit; a scale/rotation wraps the source in a temporary buffer and routes through the bilinearaxl_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-demobaseline, 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’smapToGlobal, GTK’sgtk_widget_translate_coordinates. This also retires the duplicate parent-walks (absolute_origin_of, the per-widgetfind_window/root_windowhelpers).Hit-testing mirrors drawing.
hit_test_recursiveaccumulates 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 toaxl_transform_invert; nothing produces one today.The render trace records local coordinates.
RecordingDrawContextcaptures 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 |
|
|
C++ loop wrapper |
|
|
Toolkit-level loop |
|
|
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:
Draw-call tracing in tests. AGT’s unit suite asserts render structure through a hand-rolled
RecordingDrawContextfixture (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’sdraw()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.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:
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 tomax_mode(). Plan:AgtAppgains ause_native_resolution()that tries native then falls back; axedit’s “largest mode” default becomes “native, else largest.” A--res WxHoverride still wins.DPI-aware integer UI scale (strategic, bigger). AGT has a FIXED
AgtPalettetext_sizeand 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 affineaxl_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.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:
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.Fluent builder —
Widget::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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
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:
Modal —
int run(app)blocks in a nestedaxl_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/AgtFileDialogwrap this; their OK/Cancel buttons target the dialog itself with the result code as the command id, soon_command_dismissexitsrun()with that value.Modeless —
void show(app)floats the dialog over a fully interactive parent: non-blocking (returns immediately; the app’s normalrun()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, andset_on_close( target, id)registers the completion command (a non-blocking dialog can’t return a code, so the outcome arrives asAGT_SEL_COMMAND(id)and the caller readsresult()).dismiss()routes toclose()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_buttonclaims the seat keyboard focus for the clicked surface), so both the parent and the floating dialog stay typeable. Lifetime: a modeless dialog must outliveshow()— keep it on the heap or as a member, never a stack local (unlike the blocking modalAgtMessageBox::infohelpers).
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]
AxlInputTypediscriminator + unifiedAxlInputEventstruct (type, timestamp, coords, buttons, wheel, keycode, unicode, modifiers) +AXL_INPUT_BUTTON_*/AXL_INPUT_MOD_*bitfields +AxlInputCallbacktypedef[ ] Mouse —
axl_input_attach_mouse(loop, cb, data)that registersEFI_SIMPLE_POINTER_PROTOCOL’sWaitForInputevent viaaxl_loop_add_event, reads pointer / button state on dispatch, producesAxlInputEventfor the callback. Touch viaEFI_ABSOLUTE_POINTER_PROTOCOLlayered on later.[ ] Keyboard —
axl_input_attach_key(loop, cb, data), thin wrapper over the existingaxl_loop_add_key_pressthat translatesAxlInputKeyinto the unifiedAxlInputEventshape 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-ccdispatch by file extension; pure-C consumers unaffected[x]
libaxl-cxx.aarchive with operator new/delete (scalar + array + sized + placement) routed toaxl_malloc/axl_free, plus__cxa_pure_virtualstub[x]
src/runtime/axl-cxxabi.cinlibaxl.a:__dso_handle,__cxa_atexit(→axl_atexit),.init_arraywalker called from_axl_initso global ctors fire beforemain[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: nothrow/catch, nostd::stdexcept, error-code returns throughout[x] RTTI: PROVEN BROKEN (
__dynamic_cast,__cxxabiv1::__*_type_infovtables unresolvable). AGT design adapts: nodynamic_cast, notypeid; AgtObject hierarchy uses virtual dispatch only[x]
-ffixed-x18added 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.shfor the pinned ARM bare-metal toolchain (aarch64-none-elf-g++14.3.Rel1);install.shauto-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 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— documentsAxlFont/AxlGlyph, multiple built-in fonts (LaffStd, Unifont subset),scripts/gen-bdf-font.pyfor adding more, UTF-8-first input, clipping, double-buffering, line/outline primitives. Vblank-aware present deferred per Phase 0f.[x]
src/data/README.md— documentsaxl_utf8_decodeper-codepoint iterator (companion to existingaxl_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 theaxl-inputmodule (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.hso the new font types surface in generated API docs.[x] New
docs/sphinx/modules/input.rst— Sphinx page foraxl-input, mirrors the pattern ingfx.rst.[x]
docs/sphinx/index.rstlistsinput.rst.[x] Cross-linked the new sections from
AGT-Design.mdsubstrate 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-objectprimitive. 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 oraxl-inputdirectly).
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
AxlGfxPixelis BGRA-ordered to match the UEFI GOP framebuffer exactly (zero conversion cost at present-to-screen time). Callers who prefer RGB-style literals useAXL_GFX_RGB(r, g, b)/AXL_GFX_RGBAmacros + the named-color palette (AXL_GFX_REDetc.) 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) acceptAxlGfxColorand convert at the call boundary — single shuffle per call, negligible cost. APIs that take pixel arrays (blit, buffer_pixels) stay onAxlGfxPixel— 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
AxlFontuses 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.rodataplacement (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 classicfirstChar/lastCharrange-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 theaglnamespace is fine (different namespace, no actual symbol collision), or alternative names likeAgtObservable/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/. DoxygenSphinx + 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.