AgtRender — App + Window + DrawContext

AgtRender — Application loop, window root, draw context

The runtime spine: an AgtApp event-loop owner, an AgtWindow top-level widget that holds the off-screen back buffer + drives the per-frame render walk, and an AgtDrawContext that carries the per-frame drawing state into each widget’s draw().

Headers:

  • <agt/agt-app.hpp>AgtApp application + event loop

  • <agt/agt-window.hpp>AgtWindow top-level widget + back buffer + render walk + hit-test + hover dispatch

  • <agt/agt-draw-context.hpp>AgtDrawContext per-frame drawing state (active target, origin accumulator, draw-op forwarders, RecordingDrawContext seam for tests)

  • <agt/agt-popup-surface.hpp>AgtPopupSurface a reusable helper that hosts a transient overlay (tooltip, menu, combo dropdown) on its OWN compositor child surface above the window, tracked on the window’s open-popup list so the surface is freed before the compositor is torn down

These three sit between core (which knows nothing about rendering) and widgets (which draw through the context but don’t know about the loop or the back buffer). AgtApp binds the compositor seat to AgtWindow (an AgtSurfaceHost), whose on_surface_* listener feeds dispatch_input; AgtWindow drives render_subtree over the widget tree once per dirty frame; widget draw() methods receive an AgtDrawContext & and emit axl_gfx_*-level calls through the context’s virtual forwarders.

AgtApp — the application event loop

AgtApp is the C++ entry point for an AGT application: one instance per process, owning the underlying AxlLoop and pumping events until quit() is called or the loop runs out of sources.

It is intentionally thin — consumers reach the AxlLoop * directly via AgtApp::loop to register timers, idle callbacks, and additional input sources beyond the window-bound seat.

AgtApp::set_window(AgtWindow *) binds the compositor seat (C7) on the window’s compositor: axl_compositor_attach_pointer / _attach_keyboard on the loop plus keyboard focus to the window surface. The seat hit-tests its surface tree, owns a self-presenting cursor, and routes events to the window’s AgtSurfaceHost listener → on_surface_*dispatch_input. A modal dialog (AgtDialog::run) is a HOSTED window (C7 P5): it renders onto a fullscreen child surface of the MAIN window’s compositor (AgtWindow::host_on_) rather than owning one, so the seat never moves. push_modal confines input to it with an exclusive axl_compositor_pointer_grab on that surface + routes keyboard focus to it (saving the prior focus); pop_modal ungrabs and restores the saved focus, then run() destroys the child surface. set_window returns false if the window is headless (no compositor) or the seat attach fails — the window still constructs and renders, just without input.

AgtWindow — top-level + back buffer

AgtWindow is the root of a rendered AGT widget tree. It owns the off-screen AxlGfxBuffer that descendants composite into, drives the per-frame render walk, and presents the buffer to the screen via axl_gfx_buffer_present after each pass.

Unlike every other widget, AgtWindow’s geometry is in screen coordinates — its size matches the display resolution, its origin is (0, 0). Children store parent-local coordinates and are translated into the back buffer’s coordinate space by the render walk through the draw context’s origin accumulator.

render_subtree(AgtWidget *, AgtDrawContext &) is a public static by design — tests use it to drive the render walk against a RecordingDrawContext without a real back buffer or GOP. The render walk translate()s the context’s affine transform by the widget’s own (x, y) before calling its draw(), so a widget paints in its OWN local frame ((0, 0) = its top-left) and the context maps those local coords to device pixels (Qt/GTK-shape model — see AGT-Design “Coordinate model”). Widgets author local coordinates with no manual origin arithmetic; double-translating (adding x() / y() again inside draw) is a class of bug a visual baseline would silently encode as “expected” — the unit tests around render_subtree (which record local coords) catch it at the math level instead.

dispatch_input(AgtEvent &) hit-tests the pointer / touch position against the widget tree (last-child-first so topmost sibling wins overlaps), emits synthetic AGT_SEL_ENTER / AGT_SEL_LEAVE on hover transitions, and delivers the original event to the hit-tested widget in widget-local coordinates. hovered_ is the pointer kept in sync with the hover state; on_widget_destroyed clears it when the hovered widget dies mid-flight.

Mouse capture

AgtWindow::capture_mouse(AgtWidget *) grabs subsequent positional events for one widget until release_mouse(). While captured, every motion / button / wheel / touch event routes to the captor in its local coordinates, regardless of the hit-test result. ENTER/LEAVE emission is suspended for the duration — other widgets see no synthetic events, and the captor keeps whatever hover state it had at capture time. On release, the next motion event re-establishes hover state naturally through the normal hit-test path.

Used by drag-sensitive widgets (AgtSlider, future scroll bars, drag-and-drop sources) so a drag that wanders off the widget’s bounds still delivers motion + release events to the originator. Matches FOX FXWindow::grab and Win32 SetCapture. Captor destruction mid-drag clears the capture via on_widget_destroyed, the same UAF-guard pattern used for hovered_.

Keyboard focus

AgtWindow::set_focus(AgtWidget *) directs subsequent AGT_SEL_KEYPRESS / AGT_SEL_KEYRELEASE events to one widget regardless of pointer position. Setting focus emits AGT_SEL_FOCUSOUT to the previously-focused widget first, then AGT_SEL_FOCUSIN to the new one — the same paired-transition shape dispatch_input uses for hover (ENTER/LEAVE). Setting focus to the currently-focused widget is a no-op; setting it to NULL clears focus. release_focus() is the explicit-NULL spelling.

Key events drop silently when no widget holds the focus. This is the v0.1 contract: there’s no automatic focus-to-window fallback for key events, so consumers that want window-level shortcut handling explicitly call set_focus(&window) after construction (or per-event in a custom dispatcher).

Focus is independent of mouse routingdispatch_input hit-tests positional events normally regardless of focus state. A widget can have focus without being hovered, and vice versa. Focus-target destruction clears focused_ via on_widget_destroyed (same UAF-guard pattern as hovered_ and captured_).

No-display fallback. Constructing an AgtWindow when axl_gfx_available() returns false (e.g. AArch64 QEMU without --gpu) leaves the back buffer NULL. redraw() short-circuits cleanly in that state; widget construction, hit-testing, and message dispatch still work against the stored bounds. This is the test-suite path: assemble and exercise a widget tree without needing a display.

AgtDrawContext — per-frame render state

AgtDrawContext carries the per-frame drawing state passed into AgtWidget::draw: the active off-screen buffer, the accumulated parent-local origin offset, and virtual forwarders for every axl_gfx_* draw op widgets call.

The context is RAII over axl_gfx_target_buffer: constructing it with a back buffer captures the previously-active target (via axl_gfx_get_current_target, axl-sdk v0.19.3+), switches to the new buffer, and the destructor restores the captured target. Nested contexts (a popup over a dialog over a main window, all rendering to their own back buffers) compose correctly without explicit target-stack management at the call site.

Draw forwarders

Widgets draw through ctx.fill_rect / ctx.draw_text / ctx.draw_text_ttf / ctx.clear / ctx.push_clip / ctx.pop_clip / ctx.fill_path / ctx.stroke_path / ctx.fill_rounded_rect / ctx.blit / ctx.blur / ctx.shadow_rect — each a thin virtual forwarder over the matching axl_gfx_* C API. Virtual dispatch is the seam tests use to capture draw traces via RecordingDrawContext (in test/unit/agt-recording-draw-context.hpp). Widgets MAY still call axl_gfx_* directly for primitives the context doesn’t expose (draw_line, draw_polyline, capture); the trade-off is those calls won’t appear in the trace.

ctx.shadow_rect (a soft rectangular drop shadow into the active target) forwards to axl-sdk’s G6 effects API (axl_gfx_draw_shadow). It is the chrome behind the floating drop shadows under AgtMenu / AgtMovableFrame. It no-ops cleanly when headless (NULL buffer / no GOP), so it doesn’t perturb RecordingDrawContext trace tests except where the Op::ShadowRect op is explicitly asserted. (The modal veil’s frosting moved to the compositor in C7 Phase 6 — axl_surface_set_backdrop_blur frosts the live backdrop at composite time — so the old buffer-level ctx.blur veil readback is gone.)

ctx.draw_text is the bitmap path (axl-gfx’s pixel-grid LaffStd 8×16 font, taking an int scale factor). ctx.draw_text_ttf is the anti-aliased vector path (axl-sdk v0.20.x axl_ttf_draw, taking a float px_size). Both have the same top-aligned (x, y) contract at the AGT layer — draw_text_ttf internally translates to axl_ttf_draw’s baseline-origin coordinates by querying axl_ttf_metrics at the requested px_size and shifting y down by ascent. Every widget label (AgtLabel, AgtCheckBox, AgtRadioButton) uses the TTF path; the bitmap path stays available for callers that genuinely want pixel-grid text.

Coordinate model

Every widget stores its bounds in parent-local coordinates and paints in its own local frame ((0, 0) = its top-left). AgtDrawContext is AGT’s QPainter / cairo_t: it owns the current affine transform(), and the forwarders map the local coordinates a widget passes to device (back-buffer) pixels before calling axl_gfx_*. The render walk in AgtWindow::redrawrender_subtree translate()s the transform by each widget’s (x, y) descending and untranslate()s ascending. A draw method writes ctx.fill_rect(8, 0, …) for an 8-pixel inset — no manual origin arithmetic. See AGT-Design “Coordinate model” for the full rationale (Qt alien-widget / GTK single-surface analog, why this replaced the earlier manual-absolute model, and the affine / rotation scope).

The transform is axl-sdk’s AxlTransform (a full 3×3 homography); AGT carries no parallel type since the axl-gfx render primitives take AxlTransform* directly. Every forwarder keys a fast path vs. a transform path off axl_transform_classify: translate/scale use the integer rect, cached-glyph, and plain-blit paths; a rotation/shear maps the geometry through the axl transform primitives (fill_path quad, axl_ttf_draw_transform, push_clip_rect_transformed, blit_transform). No widget produces a non-translate transform in v0.1 (the walk only translates), so the transform paths are exercised by consumer apps and transform-demo, not the widget set. origin_x()/origin_y() survive as the transform’s current translation (used by the walk + tests); absolute/window-space position is the explicit AgtWidget::map_to_window() query.

RecordingDrawContext seam

The virtual draw forwarders exist so test fixtures can intercept them. RecordingDrawContext (header-only, in test/unit/agt-recording-draw-context.hpp) overrides every forwarder to flat-capture the call into a 128-entry trace. Tests build a widget in a known state, hand it a RecordingDrawContext, call widget->draw(ctx) (or AgtWindow::render_subtree(widget, ctx)), and assert on count() / at(i).op / op-specific fields. No QEMU, no GOP, no back buffer required.

This closes the gap left by visual baselines — which can encode wrong-since-day-one bugs as “expected” if a render math error happens to look visually plausible. Trace-based tests catch render-decision bugs (state→color mapping, label centering math, op sequencing) at the unit level, where the visual baseline only catches them as pixel diffs at integration time.

See the API Reference section for the full surface.

API Reference

AgtApp

class AgtApp : public AgtObject

Application-level event loop integration for AGT.

Wraps AxlLoop with RAII so consumers don’t need AXL_AUTOPTR(AxlLoop) at the call site. In v0.1 this is a thin wrapper — consumers can access the underlying AxlLoop * via loop() to register timers, idle callbacks, input sources, etc. As AGT matures, common patterns will graduate to AgtApp methods.

AgtApp is the C++ equivalent of GTK’s Gtk::Application: one per process, lifetime spans the event loop, tears down everything on destruction.

Like FOX’s FXApp : FXObject, AgtApp is an AgtObject so it can be a widget’s command target and ships built-in standard commands — wire a button straight to AgtApp::ID_QUIT and it quits the loop with no subclass and no message map:

int main(int, char **) {
    AgtApp app;
    AgtWindow window;
    new AgtButton(&window, 100, 100, 260, 60, "Quit",
                  &app, AgtApp::ID_QUIT);   // built-in
    app.set_window(&window);
    window.redraw();
    return app.run();
}

Public Types

Built-in standard command ids (FOX FXApp::ID_QUIT shape). A widget targeting &app with one of these gets the framework’s own handler — no subclass / AGT_MAP_* needed.

Values:

enumerator ID_QUIT

request the event loop to exit

enumerator ID_DUMP

print the active window’s widget tree

enumerator ID_LAST

Public Functions

AgtApp()

Construct the app and initialize the event loop.

Post: loop() returns a valid AxlLoop * ready for source registration.

~AgtApp()

Destroy the app and free the event loop. Any sources still registered on the loop are torn down through their AxlLoop free-functions.

AgtApp(const AgtApp&) = delete
AgtApp(AgtApp&&) = delete
AgtApp &operator=(const AgtApp&) = delete
AgtApp &operator=(AgtApp&&) = delete
int run()

Run the event loop until quit() is called or all sources have been removed.

Returns:

0 on clean exit, 1 if the loop wasn’t constructed (allocation failure — check loop() != NULL after construction), 2 on a signal-driven exit such as Ctrl-C (axl_loop_run returned negative).

void quit()

Request the event loop to exit. Safe to call from any callback running on the loop.

long on_quit(AgtObject *sender, AgtEvent *ev)

Built-in AGT_SEL_COMMAND handler for ID_QUIT: calls quit(). Reached when a widget targets &app with AgtApp::ID_QUIT (the framework’s own handler — consumers don’t call this directly).

long on_dump(AgtObject *sender, AgtEvent *ev)

Built-in handler for ID_DUMP: print the active window’s widget tree (class name + bounds, indented by depth) to the console — a debug aid (FOX FXApp::ID_DUMP). No-op text when no window is set.

inline AxlLoop *loop() noexcept

Underlying AxlLoop * handle. Use for source registration: axl_loop_add_event(app.loop(), …); axl_loop_add_timeout(app.loop(), ms, cb, data); axl_input_attach_mouse(app.loop(), cb, data);

Returns NULL only if construction failed. Callers should check loop() != NULL before registering sources. The constructor doesn’t throw (we’re -fno-exceptions).

bool set_window(AgtWindow *window) noexcept

Set the top-level window that receives input. Internally, this binds the compositor SEAT on the window’s compositor (C7): axl_compositor_attach_pointer / _attach_keyboard on the loop

  • keyboard focus to the window surface. The seat then hit-tests its surface tree, owns the (self-presenting) cursor, and routes events to window’s AgtSurfaceHost listener → on_surface_* → the existing dispatch_input + redraw. Passing NULL detaches the seat. Only one window per app in v0.1; calling set_window twice rebinds (the previous window’s input goes silent).

Returns true if the seat attached (pointer + keyboard), false if the loop was never constructed (allocation failure), the window is headless (no compositor — no GOP), or the seat attach failed. A detach-only call (set_window(nullptr)) returns true. A false return doesn’t fail the app — it just means input won’t reach the window; the demo runs to the auto-exit timer in that case.

inline AgtWindow *window() const noexcept

Currently-bound top-level window, or NULL.

bool set_resolution(int width, int height) noexcept

Switch the display to width x height and rebuild the window for it. Finds the matching GOP mode (axl_gfx_find_mode), switches to it, recreates the main window’s compositor + surface at the new geometry, re-binds the seat, and repaints — the live-resolution-change entry point. Open popups (menus / tooltips / combo dropdowns) are dismissed.

Returns:

true on success; false if there is no bound window, a modal dialog is open (its surface lives on the compositor being recreated), no GOP mode matches width x height, or the mode switch / rebuild failed. Boot-services only (GOP-gated), so it returns false on a headless / no-GOP run.

bool push_modal(AgtWindow *modal) noexcept

Push modal as the active modal and confine input to it (C7 P5). The seat stays on the MAIN window’s compositor; modal is a child surface of it (AgtWindow::host_on_), so this takes an EXCLUSIVE axl_compositor_pointer_grab on modal’s surface (a press outside its subtree is swallowed — the veil is fullscreen, so there is no outside) and routes keyboard focus to that surface, saving the previously-focused surface to restore on pop_modal. Used by AgtDialog::run to install itself before blocking on axl_loop_iterate_until. Returns false if the stack is full or modal is NULL; in that case the dialog should abort gracefully (dismiss with a sentinel code). Headless (no compositor / no surface) it is a pure stack push — the grab/focus calls are skipped.

void pop_modal() noexcept

Pop the top-of-stack modal: release its pointer grab and restore the keyboard focus saved at push_modal (the revealed dialog’s surface, or the main window’s). Safe to call on an empty stack (no-op). Pair every push_modal that returned true with one pop_modalAgtDialog::run does this via RAII (the dismissal path always pops before returning).

AgtWindow *modal_top() const noexcept

Current top of the modal stack, or NULL if none. Exposed for scripted scenario tests + diagnostics. This is the surface holding the pointer grab + keyboard focus when non-NULL.

inline int modal_depth() const noexcept

Stack depth (number of active push_modal calls minus pop_modal calls). Zero means no modals; MODAL_STACK_MAX means the stack is full and further push_modal calls fail.

Public Static Functions

static AgtApp *current() noexcept

The current application instance, or NULL before any AgtApp exists.

AGT has ONE event loop, reached as an app-global service (the Qt qApp / FOX getApp() / GTK default-main-context shape) — NOT a per-window resource. A widget anywhere in any window/dialog/popup reaches it via widget->window()->loop(), which falls back to AgtApp::current()->loop() when the host window carries no explicit loop (see AgtWindow::loop()). So a new surface-hosting widget does NOT need to bind a loop for its descendants’ timers to fire — that is automatic. Use this accessor directly only when you have no widget in hand (e.g. a free function that must reach the loop).

One app per process (auto-registered in the ctor, cleared in the dtor), mirroring AgtPalette::current() / AgtStyle::current().

Public Static Attributes

static constexpr int MODAL_STACK_MAX = 4

Maximum modal nesting depth. Four covers any realistic stacked-dialog scenario (a confirmation atop a prompt atop a config dialog atop the main window — already deep) and keeps the stack a small inline array with no heap. Push attempts past this return false and the dialog falls back to running against the main window — caller catches the rejection and avoids the over-deep dialog.

AgtWindow

class AgtWindow : public AgtWidget, public AgtSurfaceHost

Subclassed by AgtDialog, AgtMainWindow

Public Types

enum FocusDir

Spatial direction for focus_direction (FOX SEL_FOCUS_* shape).

Values:

enumerator FOCUS_UP
enumerator FOCUS_DOWN
enumerator FOCUS_LEFT
enumerator FOCUS_RIGHT
enum PresentMode

What redraw() will flush to the screen this frame.

Values:

enumerator PRESENT_NONE

nothing changed — no present

enumerator PRESENT_DAMAGE

present only the accumulated damage bbox

enumerator PRESENT_FULL

present the whole buffer

Public Functions

AgtWindow() noexcept

Construct a fullscreen window at the current display resolution. Builds the owned compositor + one opaque top-level surface (C7); back_buf() aliases that surface’s draw buffer. If any of axl_gfx_available / axl_gfx_get_info / axl_compositor_new / axl_surface_create reports failure, the window stays headless (back_buf() NULL) and redraw() short-circuits. Consumers can check back_buf() after construction to decide whether visual output is possible.

explicit AgtWindow(AgtApp &app) noexcept

Convenience ctor (FOX new FXMainWindow(&app, …) brevity): construct as above, then bind the app’s input seat to this window via app.set_window(this). Lets a simple app drop the separate app.set_window(&window) call:

AgtApp    app;
AgtWindow window(app);   // seat bound here
The explicit set_window() stays the primary API — reach for it when the binding is dynamic (re-targeting the seat, modal dialogs). Children added after this still receive input; the seat binds the window, not a snapshot of its tree.

~AgtWindow() noexcept override

Free the back buffer (if any). Inherited dtor cascades into children.

void redraw()

Walk the dirty subtree, paint it into the back buffer, and blit to the screen. Short-circuits when there’s no back buffer. Safe to call repeatedly; the dirty flag is cleared as each widget draws, so a fully clean tree only pays the back-buffer-present cost (one GOP blt).

In v0.1 the walk visits every descendant every frame even if only one widget is dirty; region tracking that skips clean subtrees is a future optimization. Return value from axl_gfx_buffer_present is intentionally ignored — redraw() is best-effort.

void draw(AgtDrawContext &ctx) override

Default background fill — opaque black. Override to paint a different color or a custom background pattern.

inline AxlGfxBuffer *back_buf() const noexcept

Underlying back buffer, or NULL if no display was available at construction (or allocation failed). Exposed for advanced widgets that need direct pixel access; most code should go through axl_gfx_* while the buffer is the active target (it is during the redraw walk).

inline AxlCompositor *compositor() const noexcept

The per-display compositor this window owns (C7), or NULL when headless (no GOP). AgtApp reaches it via here to attach seat input (Phase 3). The window is one fullscreen, opaque top-level surface on it; back_buf() is that surface’s draw buffer.

inline AxlSurface *surface() const noexcept
bool rebuild_owned_output_() noexcept

Recreate the owned compositor + window surface at the CURRENT GOP mode (the caller must axl_gfx_set_mode first). Used by AgtApp::set_resolution to react to a resolution change: the window re-sizes to the new framebuffer and rebuilds its surface tree. Open popups are dismissed (their surfaces ride on the freed compositor); the SEAT is the app’s to manage (detach before / re-attach after — this never touches seat sources). No-op + false for a hosted/headless window (only the owning window has a compositor to rebuild).

Returns:

true on success, false if not the owning window or on alloc failure (the window is then headless).

inline AxlCompositor *surface_compositor() const noexcept

The compositor this window’s surface tree actually lives on: the OWNED one, or — for a HOSTED window (a modal dialog) — the parent’s borrowed compositor (C7 P5). Unlike compositor() (the owned one, NULL for a hosted window — what AgtApp binds the seat to), this is what a child surface is created on, grabbed on, and presented through. A popup opened from inside a dialog uses THIS so it lands on the live compositor. NULL when headless.

inline AgtWidget *hovered() const noexcept

Currently-hovered widget, or NULL if none (pointer outside window, never moved, or the previously-hovered widget was destroyed since the last dispatch). Exposed for scripted scenario tests that need to assert “after this move, the

hovered widget is X”; the tracking state is otherwise private to

dispatch_input.

void capture_mouse(AgtWidget *w) noexcept

Grab subsequent mouse events for w until release_mouse. While captured, every positional event (motion / button / wheel / touch) is routed to w in w’s local coordinates, regardless of where the pointer actually is. Hover-transition emission (AGT_SEL_ENTER / AGT_SEL_LEAVE) is suspended during capture — the captor keeps its existing hover state, and other widgets see no synthetic events.

Used by drag-sensitive widgets (sliders, scroll bars, drag-and-drop sources) so a drag that wanders off the widget’s bounds still delivers motion + release events to the originator. Matches FOX FXWindow::grab and Win32 SetCapture.

The grab is a LIFO stack (GRAB_STACK_MAX deep): capture_mouse PUSHES w as the active captor and release_mouse POPS it, restoring the previous captor — so a nested grab (a submenu over a menu) hands capture back up the chain on close. The common single-grab case (a slider drag) is just a balanced push/pop at depth 1. Passing NULL pops (== release_mouse()). Maps onto the compositor seat’s pointer-grab stack (see AXL-Compositor-Design).

void release_mouse() noexcept

Pop the top mouse grab; the captor beneath (if any) becomes active again. Subsequent positional events route to it, or — when the stack empties — through the normal hit-test path. No synthetic events; the next motion re-establishes hover. An extra pop on an empty stack is a safe no-op.

inline AgtWidget *mouse_captor() const noexcept

Active captor (top of the grab stack), or NULL if none. Exposed for scripted scenario tests + diagnostics.

void set_focus(AgtWidget *w) noexcept

Direct keyboard events to w. All subsequent AGT_SEL_KEYPRESS / AGT_SEL_KEYRELEASE dispatches go to the focused widget regardless of pointer position (matches FOX / Win32 / X11 focus models). Synthetic AGT_SEL_FOCUSOUT fires on the previously-focused widget first, then AGT_SEL_FOCUSIN fires on w — a widget that responds to both sees a clean transition.

Passing NULL is equivalent to release_focus(). Setting focus to the currently-focused widget is a no-op (no synthetic events emitted).

void release_focus() noexcept

Equivalent to set_focus(nullptr).

inline AgtWidget *focused() const noexcept

Currently-focused widget, or NULL if no widget holds the focus. Exposed for scripted scenario tests + diagnostics.

void focus_next(bool reverse) noexcept

Move keyboard focus to the next (or, with reverse, previous) Tab-focusable widget in document order, wrapping within the active focus scope (the innermost is_focus_scope() ancestor of the current focus, else the window). Skips non-focusable / disabled / hidden widgets. No-op when the scope has no focusable widgets. This is what a window-level Tab / Shift+Tab triggers; consumers can also call it directly. See docs/AGT-Input-Focus-Design.md §2.

bool focus_direction(FocusDir dir) noexcept

Geometric DIRECTIONAL focus (the FOX / GTK arrow-key model): move keyboard focus to the nearest focusable widget lying in dir from the current focus, within the active focus scope — Right picks the nearest control to the right, Down the nearest below, etc. (centre-to-centre distance, weighting the cross-axis offset so the same row / column is preferred). A focus group (radio set) counts as ONE unit (its own members are skipped — arrows within a group are the group’s job). No wrap (Tab wraps; arrows stop at the scope edge). This is what an arrow key the focused widget didn’t consume triggers (edit-cursor / slider / list / radio all keep their arrows; an inert control like a button lets the arrow move focus — the FOX WM_GETDLGCODE equivalent).

Returns:

true if focus moved (the caller then consumes the key); false when nothing is focused or no candidate lies that way.

long on_key_press(AgtObject *sender, AgtEvent *ev)

Window key handler — Tab / Shift+Tab cycle focus (focus_next). Reached when a key bubbles up unconsumed from the focused widget (see dispatch_input). Subclasses (AgtDialog) extend this with Enter / Escape; they call focus_next for the Tab case.

void push_focus() noexcept

Save the current focus on a LIFO stack. A menu / popup / dialog that grabs focus pushes on open and pop_focus()es on close, so focus returns to where it was — the model behind every toolkit’s “the menu closes, the document regains focus.” UAF-safe: a saved widget destroyed before the matching pop is NULLed (pop then just releases focus). See docs/AGT-Input-Focus-Design.md §2.5.

void pop_focus() noexcept

Pop the focus saved by the matching push_focus() and restore it (a NULL entry — empty stack or a since-destroyed widget — releases focus). Balanced with push_focus; an extra pop is a safe no-op.

void drive_tooltip(AgtWidget *target, uint16_t sel, int sx, int sy) noexcept

Drive the hover-tooltip machine for an externally-routed positional event over target. The mouse-grab short-circuit bypasses the window’s own tooltip handling, so a grab owner that routes its own surfaces (AgtMenuBar’s menu session) calls this to keep tooltips working for the widget under the pointer. Pass the hit widget (or NULL to cancel a pending tip).

bool add_accelerator(uint32_t keycode, uint32_t mods, AgtObject *target, uint16_t command_id) noexcept

Register a bare-keycode accelerator (e.g. F10, a function or nav key) with an exact modifier set mods (lock bits — Caps/Num/ Scroll — are ignored in the match). On a matching AGT_SEL_KEYPRESS the window emits AGT_SEL_COMMAND(@a command_id) to target and consumes the key before the focused widget sees it. For a Ctrl+<letter> chord use add_ctrl_accelerator instead (it spans both device-dependent Ctrl encodings).

Returns:

false if the table is full or keycode is 0.

bool add_ctrl_accelerator(char letter, AgtObject *target, uint16_t command_id) noexcept

Register a Ctrl+<letter> accelerator (e.g. Ctrl+S). letter is the (case-insensitive) letter; the match spans both Ctrl encodings via axl_input_ctrl_letter, so it works on a physical keyboard and a serial console alike. Fires like add_accelerator.

Returns:

false if the table is full or letter is not a letter.

inline void clear_accelerators() noexcept

Drop all registered accelerators.

inline int accelerator_count() const noexcept

Number of registered accelerators. For tests / diagnostics.

void on_surface_enter(int x, int y) noexcept override
void on_surface_motion(int x, int y, uint32_t modifiers) noexcept override
void on_surface_button(uint32_t button, bool pressed, int x, int y, uint32_t modifiers, uint32_t click_count, bool dragging) noexcept override
void on_surface_axis(int dx, int dy, uint32_t modifiers) noexcept override
void on_surface_key(const AxlInputEvent *ev) noexcept override
inline void set_loop(AxlLoop *loop) noexcept

OPTIONAL per-window loop override — you almost never need this. It pins an explicit AxlLoop on this window; AgtApp::set_window uses it on the main window as a small fast-path, and it is the key the owning window’s dtor passes to detach its compositor seat/frame-clock sources. Passing NULL clears the override.

Do NOT call this to “give a dialog/popup a loop” so its child timers fire — that is automatic: loop() falls back to the app-global loop (see below), so a hosted surface’s descendants reach it with no binding. (Tier-1 added set_loop to dialogs for exactly that reason; Tier-2’s fallback made it unnecessary.)

AxlLoop *loop() const noexcept

The event loop this window’s widgets schedule timers on. Returns the explicit per-window override if one was pinned (set_loop), otherwise the app-global loop via AgtApp::current()->loop().

This is the canonical widget→loop access path. AGT has ONE event loop, reached as an app service (the Qt qApp / FOX getApp() shape) — any widget in any window, dialog, or popup arms a timer via widget->window()->loop() and it Just Works, because this never dead-ends at a window nobody bound. A new surface-hosting widget needs NO loop wiring of its own. NULL only before any AgtApp exists (or a genuinely headless run with no app).

inline AgtWidget *tooltip_target() const noexcept

The widget whose tooltip is pending (timer armed) or showing, or NULL. Exposed for scenario tests + diagnostics.

inline bool tooltip_shown() const noexcept

True while the shared tooltip is currently visible.

inline AgtTooltip *tooltip_widget() const noexcept

The shared tooltip instance, created lazily on first show (NULL before that). Exposed for scenario tests + diagnostics.

void show_pending_tooltip() noexcept

Pop the pending tooltip now — the work the hover-timer callback does. No-op if nothing is pending or the target lost its tip. Public so scenario tests can fire it deterministically without running the event loop.

void invalidate_region(int ax, int ay, int w, int h, int margin = 0) noexcept

Union a widget’s painted footprint (absolute coords, inflated by margin on every side) into this frame’s damage bbox, clamped to the window. Called by AgtWidget::mark_dirty / set_bounds; also usable directly to invalidate an arbitrary region.

inline void invalidate_all() noexcept

Force the next redraw() to present the entire buffer (e.g. after a theme swap or any change the damage path can’t bound).

inline void note_structure_changed() noexcept

Mark that the widget tree’s structure changed — the next redraw() full-presents (a newly attached / detached subtree’s footprint is unknown to the damage path).

virtual PresentMode present_mode() const noexcept

The present mode redraw() would use right now (does not reset). Exposed for scenario tests + diagnostics. Virtual so a window with a whole-surface render (the dialog veil — buffer_clear ignores the damage clip, and a backdrop-blur surface recomposites its full rect) can coalesce PRESENT_DAMAGE up to PRESENT_FULL.

inline bool has_damage() const noexcept

True if any damage has accumulated since the last present.

bool damage_bbox(AxlGfxClip *out) const noexcept

Current damage bounding box (buffer-local).

Returns:

true with out filled when damage exists; false (and out untouched) when clean. For scenario tests + diagnostics.

inline void on_tree_mutated() noexcept override

AgtObject hook: a child attached to / detached from the window itself — full-present next frame.

void mark_dirty() noexcept override

A window invalidating ITSELF (e.g. a subclass cycling its background in a command handler) is a window-wide change with no boundable footprint — force a full present. window() returns NULL for the window itself, so the base mark_dirty would otherwise add no damage and the change would never reach the screen on the incremental path.

AgtWidget *widget_at(int sx, int sy) noexcept

Hit-test: find the deepest widget whose absolute bounds contain sx, sy in screen coordinates. Returns the window itself if no child is under the point; returns NULL if the point is outside the window entirely (the window is fullscreen so this normally requires negative or past-edge coordinates).

void dispatch_input(AgtEvent &ev)

Dispatch a raw input event through the widget tree. Routes mouse / touch events to the widget under the pointer by hit-testing and translating to widget-local coordinates; emits synthetic AGT_SEL_ENTER / AGT_SEL_LEAVE events on hover transitions; key events would go to whichever widget has focus, but focus tracking is a future phase, so for now they drop silently.

Mutates ev — the position fields are rewritten to widget-local coordinates before delivery, so callers that re-inspect ev after dispatch_input returns will see the translated values.

Callers (AgtApp’s mouse / key callbacks) construct an AgtEvent from the raw AxlInputEvent and hand it off here; the window owns the hover/focus tracking and the hit-test math.

void on_widget_destroyed(AgtWidget *w) noexcept override

Notification from a descendant AgtWidget’s destructor — clears hovered_ (and, when focus tracking arrives, the focused pointer) if it matches the destroyed widget, preventing the UAF that would otherwise hit on the next mouse event. Inspects w as a pointer value only — the widget is mid-destruction, so dereferencing would be UB on the now-defunct subclass vtable.

void track_popup_(AgtPopupSurface *p) noexcept

C7 P4 popup registry (called by AgtPopupSurface). A popup with a live surface links onto open_popups_ so the window can forget_surface_() every one before freeing the compositor — see AgtPopupSurface’s friend note. Not for general use.

void untrack_popup_(AgtPopupSurface *p) noexcept

Public Static Functions

static void render_subtree(AgtWidget *root, AgtDrawContext &ctx)

Drive the depth-first render walk over root using ctx. Translates the context’s origin by each widget’s parent-local offset before invoking its draw(), recurses into widget children (is_widget() gates the descent so a non-widget AgtObject in the tree is skipped without UB), then untranslates back. Equivalent to what redraw() does internally but takes a caller-supplied context, so tests can drive the walk with a synthetic AgtDrawContext(nullptr) and a fixture widget that records ctx.origin_x() / origin_y() inside its draw() to assert on coordinate accumulation. This is what test_render_walk_* in agt-test-render-walk.cpp uses to catch the double-translate class of bug without relying on a visual baseline.

Public Static Attributes

static constexpr uint16_t ID_LAST = AgtWidget::ID_LAST

FOX-style message-ID chain (see AgtObject::ID_LAST).

static constexpr int ACCEL_MAX = 32

Max registered accelerators (fixed table — no heap).

static constexpr uint32_t TOOLTIP_DELAY_MS = 600

Hover-tooltip delay (ms) — how long the pointer must rest over a tooltip-bearing widget before the tip pops.

AgtDrawContext

class AgtDrawContext

Public Functions

explicit AgtDrawContext(AxlGfxBuffer *buf) noexcept

Switch the draw target to buf. Pass NULL for a no-op context (used by AgtWindow::redraw() when there’s no back buffer — e.g. a test run with no display). The previously-active target is captured and restored by the destructor.

virtual ~AgtDrawContext() noexcept

Restore whatever draw target was active when this context was constructed.

AgtDrawContext(const AgtDrawContext&) = delete
AgtDrawContext(AgtDrawContext&&) = delete
AgtDrawContext &operator=(const AgtDrawContext&) = delete
AgtDrawContext &operator=(AgtDrawContext&&) = delete
inline AxlGfxBuffer *buffer() const noexcept

Active back buffer, or NULL when none was available (no display attached or allocation failed).

inline int origin_x() const noexcept

Current origin (sum of all ancestor parent-local offsets). Widget draw methods add their own sub-element offsets to these before calling the draw forwarders.

inline int origin_y() const noexcept
void translate(int dx, int dy) noexcept

Descend into a child: shift the origin by the child’s parent-local offset. Paired with untranslate() after the child (and its subtree) has been drawn. Add/subtract is enough since AGT has no rotated or scaled widgets in v0.1.

void untranslate(int dx, int dy) noexcept
inline const AxlTransform &transform() const noexcept

The current local→device affine. Widgets map a local point to device coords via axl_transform_map_point(ctx.transform(), ...).

inline void set_transform(const AxlTransform &t) noexcept

Replace the current transform outright (rare; translate / concat / save+restore are the usual path).

inline void concat(const AxlTransform &t) noexcept

Pre-concatenate t into the current transform (cairo user-space semantics: subsequent draws are transformed by t first, then the prior transform). Used to apply a widget-local scale / rotation on top of the inherited offset.

void save() noexcept

Push the current transform onto the save stack. Pair with restore(). Silently ignores overflow past the fixed depth (deep enough for any realistic explicit-transform nesting; the per-child render walk uses translate/untranslate, not the stack, so depth here tracks explicit saves only).

void restore() noexcept

Pop the transform saved by the matching save(). No-op on underflow.

virtual void fill_rect(int x, int y, int w, int h, AxlGfxPixel color)

Solid-fill the rectangle at (x, y, w, h). Coordinates are WIDGET-LOCAL — the context maps them through the current transform to device pixels (see “Coordinate model”).

virtual void fill_rect_vgradient(int x, int y, int w, int h, AxlGfxPixel top, AxlGfxPixel bottom)

Fill a rect with a VERTICAL linear gradient from top (at the rect’s top edge) to bottom (at its bottom edge). Coordinates are widget-local; the gradient + rect are built in device space internally (translate-only for now — no scale/rotate on the axis). A convenience over the raw AxlGfxGradient lifecycle for the common glossy-fill case (buttons, progress bars).

virtual void draw_text(const AxlFont *font, int x, int y, const char *text, AxlGfxPixel color, int scale)

Render UTF-8 text at (x, y) using font, scaled by scale (1 = native). Coordinates are widget-local (mapped through the transform). Bitmap-font path — for pixel-grid console glyphs. For proportional UI text use draw_text_ttf.

virtual void draw_text_ttf(AxlTtf *ttf, int x, int y, const char *text, float px_size, AxlGfxPixel color)

Render UTF-8 text via ttf at px_size pixels. (x, y) is the top-left of the rendered cellaxl_ttf_draw’s baseline-origin convention is hidden inside this forwarder (we add px_size’s ascent to land on the baseline), so the call site reads the same as the bitmap draw_text: top-aligned at (x, y). Coordinates are widget-local (mapped through the transform). Alpha modulation is honored on buffer targets (AA glyph coverage blends with what’s there).

Pass axl_ttf_default() for the built-in DejaVu Sans subset (ASCII + Latin-1 + a few typographic punctuation marks). Custom TTF buffers load via axl_ttf_load once at app start and live for the lifetime of the AGT window.

virtual void clear(AxlGfxPixel color)

Wipe the entire active draw target with color. Works uniformly: with a bound back buffer, calls axl_gfx_buffer_clear; with a screen target (no buffer bound), routes through a full-extent axl_gfx_fill_rect covering the screen dimensions. Headless (no GOP, no buffer) cleanly no-ops.

virtual void push_clip(AxlGfxClip rect)

Push a clipping rectangle. Intersects with the previous stack top (or rect itself if empty). Pair with pop_clip. Coordinates are widget-local — mapped through the transform to device, like the draw forwarders.

virtual void pop_clip()

Pop the top clip off the stack.

virtual void fill_path(const AxlGfxPath *path, AxlGfxPixel color)

Fill the area enclosed by path with color. Forwards to axl_gfx_fill_path. The path is built externally (typically retained per-widget so the trace cost stays pointer-sized and the path object can be reused across redraws). NULL or empty path no-ops (axl-gfx returns AXL_ERR); widget code can pass conditionally-built paths without an outer null-check.

virtual void stroke_path(const AxlGfxPath *path, AxlGfxPixel color, float width)

Stroke the outline of path with color and width width. Forwards to axl_gfx_stroke_path. Width is passed through to axl-gfx but is currently rasterized as 1-pixel-thick regardless (see axl_gfx_stroke_path doc); AGT retains the width parameter so callers don’t need to change when proper thick-line rendering lands.

virtual void fill_rounded_rect(int x, int y, int w, int h, float radius, AxlGfxPixel color)

Fill a rectangle with rounded corners. Forwards to axl_gfx_fill_rounded_rect — the immediate-mode convenience over the retained path API for the case AGT_FRAME_ROUNDED uses (button + panel backgrounds). radius is clamped by axl-gfx to min(w, h) / 2; radius == 0 produces a plain rect (equivalent to fill_rect).

virtual void fill_rounded_rect_vgradient(int x, int y, int w, int h, float radius, AxlGfxPixel top, AxlGfxPixel bottom)

Fill a rounded rect with a VERTICAL gradient (topbottom). The rounded-corner counterpart of fill_rect_vgradient (forwards to axl_gfx_fill_rounded_rect_gradient, AA corners and all). Widget-local coords; translate-only on the axis for now.

virtual void blit(const AxlGfxPixel *pixels, int x, int y, int w, int h)

Blit pixels (row-major BGRX, length w * h) to the active target at (x, y). Forwards to axl_gfx_blit after rejecting NULL pixels and non-positive w/h — the uint32_t cast in the underlying API would silently turn negative extents into huge unsigned values otherwise, so the guard lives at the AGT edge.

Coordinates are widget-local (the destination position is mapped through the transform; the w/h source pixels are not scaled here). Negative x / y are PERMITTED — callers compositing an oversized image inside a smaller frame pass a negative local origin so the centered position is correct. Callers doing this MUST wrap the call in a matching push_clip so the content is masked to the frame. AgtImage uses exactly this pattern.

virtual void blit_rect(const AxlGfxPixel *pixels, int src_stride, int src_x, int src_y, int dst_x, int dst_y, int w, int h)

Blit the sub-rect [src_x, src_y, w, h] of a source image (row-major BGRX, src_stride pixels per row) to the active target at (dst_x, dst_y). Forwards to axl_gfx_blit_rect — the no-per-frame-copy path for a sprite-sheet CELL (a sub-rect of a wider sheet, whose rows are not contiguous). Same guards + transform handling as blit: axis-aligned maps the destination position; scaled / rotated extracts the cell into a temp buffer and bilinear-samples. Coordinates are widget-local; wrap in push_clip if the cell can exceed the frame.

virtual void shadow_rect(int x, int y, int w, int h, AxlGfxPixel color, uint32_t radius, bool gamma_correct = true)

Cast a soft rectangular drop shadow into the active target: a w x h shadow whose top-left sits at (x, y) in widget-local coords (position mapped through the transform), tinted color (use a translucent tint), blurred by radius pixels. Allocates a temporary opaque shape buffer and forwards to axl_gfx_draw_shadow; the caller then paints the real content on top. Used for the floating-element depth cue under popup menus + dialog cards. gamma_correct composites the soft edge in linear light (the physically-correct default, palette gamma_correct); pass false for a flatter sRGB blend. Gamma is saved/restored either way.

AgtPopupSurface

class AgtPopupSurface : public AgtSurfaceHost

Public Functions

AgtPopupSurface() noexcept = default
~AgtPopupSurface() noexcept override
AgtPopupSurface(const AgtPopupSurface&) = delete
AgtPopupSurface &operator=(const AgtPopupSurface&) = delete
void set_interactive(AgtPopupFunc on_dismiss, AgtPopupFunc on_activate, void *user, bool take_grab = true) noexcept

Make this popup INTERACTIVE before open: the surface catches input over the content rect (the shadow border stays transparent) and routes seat pointer events into the content widget; a press INSIDE fires on_activate after the content handles it. When take_grab (the default), it also takes a seat pointer-grab on show so a press OUTSIDE dismisses (on_dismiss) — the standalone popup (combo, top-level menu bar) case. Pass take_grab=false for a popup nested INSIDE another’s grab subtree (a menu under the bar’s session grab, a submenu under its parent menu): it routes its content but the enclosing grab owns dismiss. Passive (the default, e.g. a tooltip) leaves the surface input-transparent and takes no grab.

bool open(AgtWindow *win, AgtWidget *content, AxlSurface *parent_surface = nullptr) noexcept

Ensure a child surface sized to hold content (its bounds inflated by content->dirty_margin() on every side for the drop shadow) exists. parent_surface is the surface to parent it under (a nested popup — menu-under-bar, submenu-under-menu); NULL parents it under win’s top-level surface (the default popup). Re-fits if the content size changed. Input-transparent unless set_interactive was called. No-op (returns false) when win is headless (no compositor).

Returns:

true if a live surface is ready.

void close() noexcept

Hide + destroy the surface (NULL-safe / idempotent) and recomposite so the vacated region is restored from the window beneath.

inline bool is_open() const noexcept

True while a live surface exists.

void move_content_to(int ox, int oy) noexcept

Position the content’s top-left at OUTPUT (ox, oy) — the surface origin is offset back by the shadow inset so the shadow stays in-bounds.

void show_and_present() noexcept

Show the surface, render the content into it, and present (composite the window + popups, flush the damaged region).

void refresh() noexcept

Re-render the content into the surface + present — after the content changed by a route the popup didn’t drive (e.g. a combo’s arrow-key browse forwarded to the hosted list). No-op when closed / headless.

inline AxlSurface *surface() const noexcept

The child surface, or NULL when closed / headless.

inline int inset() const noexcept

The shadow inset (content offset within the surface), in pixels.

void on_surface_motion(int x, int y, uint32_t modifiers) noexcept override
void on_surface_button(uint32_t button, bool pressed, int x, int y, uint32_t modifiers, uint32_t click_count, bool dragging) noexcept override
void on_surface_axis(int dx, int dy, uint32_t modifiers) noexcept override

Friends

friend class AgtWindow