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>—AgtAppapplication + event loop<agt/agt-window.hpp>—AgtWindowtop-level widget + back buffer + render walk + hit-test + hover dispatch<agt/agt-draw-context.hpp>—AgtDrawContextper-frame drawing state (active target, origin accumulator, draw-op forwarders, RecordingDrawContext seam for tests)<agt/agt-popup-surface.hpp>—AgtPopupSurfacea 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 routing — dispatch_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::redraw → render_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
AxlLoopwith RAII so consumers don’t needAXL_AUTOPTR(AxlLoop)at the call site. In v0.1 this is a thin wrapper — consumers can access the underlyingAxlLoop *vialoop()to register timers, idle callbacks, input sources, etc. As AGT matures, common patterns will graduate toAgtAppmethods.AgtAppis the C++ equivalent of GTK’sGtk::Application: one per process, lifetime spans the event loop, tears down everything on destruction.Like FOX’s
FXApp : FXObject,AgtAppis anAgtObjectso it can be a widget’s command target and ships built-in standard commands — wire a button straight toAgtApp::ID_QUITand 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_QUITshape). A widget targeting&appwith 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
-
enumerator ID_QUIT
Public Functions
-
AgtApp()
Construct the app and initialize the event loop.
Post:
loop()returns a validAxlLoop *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.
-
int run()
Run the event loop until
quit()is called or all sources have been removed.- Returns:
0 on clean exit,
1if the loop wasn’t constructed (allocation failure — checkloop() != NULLafter construction),2on a signal-driven exit such as Ctrl-C (axl_loop_runreturned 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_COMMANDhandler forID_QUIT: callsquit(). Reached when a widget targets&appwithAgtApp::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 (FOXFXApp::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() != NULLbefore 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_keyboardon the loopkeyboard focus to the window surface. The seat then hit-tests its surface tree, owns the (self-presenting) cursor, and routes events to
window’sAgtSurfaceHostlistener →on_surface_*→ the existingdispatch_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.
-
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 EXCLUSIVEaxl_compositor_pointer_grabon 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 onpop_modal. Used byAgtDialog::runto install itself before blocking onaxl_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 everypush_modalthat returned true with onepop_modal—AgtDialog::rundoes 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
AgtAppexists.AGT has ONE event loop, reached as an app-global service (the Qt
qApp/ FOXgetApp()/ GTK default-main-context shape) — NOT a per-window resource. A widget anywhere in any window/dialog/popup reaches it viawidget->window()->loop(), which falls back toAgtApp::current()->loop()when the host window carries no explicit loop (seeAgtWindow::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
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 ofaxl_gfx_available/axl_gfx_get_info/axl_compositor_new/axl_surface_createreports failure, the window stays headless (back_buf()NULL) andredraw()short-circuits. Consumers can checkback_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 viaapp.set_window(this). Lets a simple app drop the separateapp.set_window(&window)call:The explicitAgtApp app; AgtWindow window(app); // seat bound here
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_presentis 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).
AgtAppreaches 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_modefirst). Used byAgtApp::set_resolutionto 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 — whatAgtAppbinds 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::graband Win32SetCapture.The grab is a LIFO stack (
GRAB_STACK_MAXdeep):capture_mousePUSHES w as the active captor andrelease_mousePOPS 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_KEYRELEASEdispatches go to the focused widget regardless of pointer position (matches FOX / Win32 / X11 focus models). SyntheticAGT_SEL_FOCUSOUTfires on the previously-focused widget first, thenAGT_SEL_FOCUSINfires 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_GETDLGCODEequivalent).- 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 (seedispatch_input). Subclasses (AgtDialog) extend this with Enter / Escape; they callfocus_nextfor 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 withpush_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_KEYPRESSthe window emitsAGT_SEL_COMMAND(@a command_id)to target and consumes the key before the focused widget sees it. For a Ctrl+<letter> chord useadd_ctrl_acceleratorinstead (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 likeadd_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
AxlLoopon this window;AgtApp::set_windowuses 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 addedset_loopto 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 viaAgtApp::current()->loop().This is the canonical widget→loop access path. AGT has ONE event loop, reached as an app service (the Qt
qApp/ FOXgetApp()shape) — any widget in any window, dialog, or popup arms a timer viawidget->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 anyAgtAppexists (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_clearignores 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 basemark_dirtywould 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_LEAVEevents 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_inputreturns will see the translated values.Callers (AgtApp’s mouse / key callbacks) construct an
AgtEventfrom the rawAxlInputEventand 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 — clearshovered_(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 canforget_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-widgetAgtObjectin the tree is skipped without UB), then untranslates back. Equivalent to whatredraw()does internally but takes a caller-supplied context, so tests can drive the walk with a syntheticAgtDrawContext(nullptr)and a fixture widget that recordsctx.origin_x()/origin_y()inside itsdraw()to assert on coordinate accumulation. This is whattest_render_walk_*inagt-test-render-walk.cppuses 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.
-
AgtWindow() noexcept
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+restoreare 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 usestranslate/untranslate, not the stack, so depth here tracks explicitsaves 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
AxlGfxGradientlifecycle 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 cell —
axl_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 bitmapdraw_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 viaaxl_ttf_loadonce 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-extentaxl_gfx_fill_rectcovering 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
rectitself if empty). Pair withpop_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 (seeaxl_gfx_stroke_pathdoc); 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 tomin(w, h) / 2;radius == 0produces a plain rect (equivalent tofill_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 (top → bottom). The rounded-corner counterpart of
fill_rect_vgradient(forwards toaxl_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_blitafter 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_clipso 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 asblit: axis-aligned maps the destination position; scaled / rotated extracts the cell into a temp buffer and bilinear-samples. Coordinates are widget-local; wrap inpush_clipif 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, palettegamma_correct); pass false for a flatter sRGB blend. Gamma is saved/restored either way.
-
explicit AgtDrawContext(AxlGfxBuffer *buf) noexcept
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 unlessset_interactivewas 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
-
AgtPopupSurface() noexcept = default