This document defines how coding agents interact with the akar codebase. Read it before touching anything.
A GPU-accelerated immediate-mode UI component library with a C ABI. The Rust crates are the implementation; akar.h is the contract. An agent that needs to use akar from any language targets the C API, not the Rust API directly.
akar is primarily built by agents and is designed to be used by other projects that need a cross-platform UI framework which works and debugs well for agents — especially multi-modal LLMs that can read the screenshots it produces. The demo-rust binary ships with a full visual debug toolchain (see "Debug toolchain" below) so an agent can make a change, see the result, and iterate with no human in the loop.
- Confirm which epic is active (
epics/— lowest-numbered withoutStatus: Done). Pick the next epic from the roadmap or open a new one; do not rely on this or any other doc for epic status — read the epic files directly. - Read the full epic before touching any file.
- Cross-reference
DEVELOP.mdfor local dependency paths and architectural constraints.
The relevant dependencies and design references are cloned locally under ~/Projects/. Prefer reading these checkouts over web searches, crates.io documentation, or GitHub browsing: they are the local source of truth for internals and undocumented behavior. These are reference-only checkouts, not Cargo path dependencies.
| Project | Local path | Read first / use it for |
|---|---|---|
| glyphon | ~/Projects/glyphon/src/ |
text_render.rs, text_atlas.rs — text renderer akar wraps |
| wgpu | ~/Projects/wgpu/ |
GPU pipeline, render passes, buffer management (wgpu 29 internals) |
| glam | ~/Projects/glam-rs/ |
Math types and geometry conventions (Vec2, Vec4, Mat4) |
| taffy | ~/Projects/taffy/ |
Flexbox/Grid layout behavior and the layout-engine internals akar wraps |
| winit | ~/Projects/winit/ |
Event and window integration; use only in akar-winit or examples |
| Project | Local path | Read first / use it for |
|---|---|---|
| daftprompt | ~/Projects/daftprompt/ |
The successor to sugacode and a real akar application. Read src/ui/render.rs and src/ui/ for canvas, drawer, search, and integration patterns. |
| Project | Local path | Read first / use it for |
|---|---|---|
| egui | ~/Projects/egui/ |
Immediate-mode Painter, Response, Id/memory, and layout-cursor design |
| Dear ImGui | ~/Projects/imgui/ |
Draw-list, clipping, input, docking, and C API patterns |
| Nuklear | ~/Projects/Nuklear/ |
Minimal backend-agnostic immediate-mode C API design |
| sokol | ~/Projects/sokol/ |
Clean language-neutral C API and header design (sokol_gfx.h) |
| Zed / GPUI | ~/Projects/zed/crates/gpui/ |
Production wgpu UI: scene graph, element/layout protocol, platform abstraction |
| xilem | ~/Projects/xilem/ |
Retained-mode architecture, widget lifecycle, and accessibility concepts that akar deliberately defers |
| daisyUI | ~/Projects/daisyui/ |
Component catalog shape, naming, and token-based themes |
| shadcn/ui | ~/Projects/shadcn_ui/ |
Component API ergonomics and composition patterns |
Do not fetch URLs for these projects. Read files locally.
- Do not impose an event loop — akar is driven by the developer's loop.
- Do not impose an async runtime — all akar APIs are synchronous.
- Do not add windowing (winit, SDL, GLFW) to
akar-coreorakar-components. Windowing belongs inakar-winitand is always optional. - Do not add accessibility scaffolding in v1. Document the punt if relevant.
- Do not edit
akar.hdirectly — it is alwayscbindgen-generated fromakar-c-api.
This is the primary feedback loop for UI work, and it is built specifically for agents. akar's demo-rust binary ships with a complete capture/inspect toolchain so an agent can see, isolate, script, and diff its UI — no human intermediation and no external screen-capture tooling required.
The screenshot captures exactly what akar rendered (no OS chrome) using wgpu intermediate-texture readback. It works identically on macOS, Windows, and Linux. Design and history are in epics/013-screenshot-utility.md, epics/014-screenshot-enhancements.md, and epics/015-component-isolation.md.
# Basic: full-window screenshot after default 5s delay, then exit
cargo run --bin demo-rust -- --screenshot /tmp/demo.png --exit
# Configurable delay (float seconds; 0 = first frame). Lets agents iterate fast.
cargo run --bin demo-rust -- --screenshot /tmp/demo.png --delay 0.5 --exit--script <FILE> drives the demo into a non-idle state (hover, press, focus, open dropdown/modal) and captures the result without manual interaction. --script and --screenshot are mutually exclusive — the script issues its own screenshot lines (and may issue several). Line-based, one step advanced per frame for precise frame alignment:
# comment
hover @submit_button # @label OR bare X Y
delay 0.1
click @submit_button # same-frame press+release → is_clicked fires this frame
press left # hold a button across frames (active-state shots)
release left
scroll 0 -120
key Enter
type "hello"
screenshot /tmp/pressed.png # capture on this frame; can repeat for before/after
Element addressing is labels-first on top of coordinates. Labels are a HashMap<String, NodeId> in akar-layout; the demo registers ~20 of its named interaction nodes (e.g. @navbar_dropdown, @form_submit). Coordinates remain the fallback for inline-computed rects (dropdown items, list rows) that have no NodeId to register.
Labels only resolve to a real rect once their subtree is actually part of the computed layout tree this frame. The demo's tabs (list/canvas/stats/form) are mutually exclusive — only the active tab's widgets are laid out — so a script that addresses @form_name/@form_notes/etc. without first activating the Form tab must run under --component form (see "Component isolation" below), which forces that tab active before the script's first step. Running such a script plain (no --component) resolves those labels to a zero rect and silently clicks/types at the origin.
# Print "name x y w h" for every labeled layout node, then exit (element discovery)
cargo run --bin demo-rust -- --dump-layout
# Structured JSON dump for the captured frame: every draw call (incl. culled, with
# z-order and scissor), labeled layout rects, and an input snapshot.
cargo run --bin demo-rust -- --dump-frame /tmp/frame.json --screenshot /tmp/x.png --exitPlain --dump-layout (no --component) internally sweeps all four tabs so every registered label gets a real rect regardless of which tab is active by default — you don't need --component <tab> just to discover coordinates. --component <name> --dump-layout still isolates to that one component's tree (other labels report 0 0 0 0, as expected). A small, fixed set of labels (heading, paragraph, link, card) are showcase-only widgets never part of the full page — they always report 0 0 0 0 outside of --component <that-name>; that's expected, not a bug.
--dump-frame uses a gated recording mode in DrawList that snapshots {call, scissor} before the scissor-cull early-return, so culled calls are included — useful for "why didn't my quad render?" debugging.
--component <name> renders a single component, forces its interesting state once (open drawer, open dropdown, etc.), and auto-crops the PNG to that component's bounding box + padding — removing unrelated UI as visual noise.
# Discovery: list isolable component names and exit
cargo run --bin demo-rust -- --list-components
# Isolate just the drawer (forced open), auto-cropped
cargo run --bin demo-rust -- --component drawer --screenshot /tmp/drawer.png --exit
# Composes with --script (force runs once, script may then transition state)
cargo run --bin demo-rust -- --component dropdown --script /tmp/hover_item.txtUnknown component names print an error with the valid list and exit non-zero. Composes with --dump-frame (the dump reflects only the isolated component's calls). Demo-only feature; no akar-core/akar-layout/akar-components involvement.
The standalone akar-diff binary compares two PNGs — no GPU, no akar deps:
# Visual diff: changed pixels in red, unchanged dimmed to 30%
akar-diff --diff /tmp/baseline.png /tmp/current.png -o /tmp/diff.png
# CI gate: exit non-zero when changed-pixel ratio exceeds threshold
akar-diff --compare /tmp/baseline.png /tmp/current.png --threshold 0.5Multi-capture in one run is expressed via multiple screenshot lines in a --script (before/after), not a separate flag. Baselines are caller-managed file paths; perceptual diff is deferred (pixel-exact for v1).
- Make your change.
--list-components/--dump-layoutto find what to capture and where it is.--component <name> --screenshot …for a tight, noise-free image (or--screenshotfor the full window).--scriptwhen the issue is in an interactive state.--dump-framewhen the visual alone isn't enough.akar-diff --compareagainst a baseline to verify the fix didn't regress.
| Crate | Owns | Must NOT touch |
|---|---|---|
akar-core |
wgpu pipelines, draw list, scissor, input state struct | Layout, components, windowing, C API |
akar-layout |
taffy wrapper, flex tree → pixel rect resolution | Rendering, components |
akar-components |
All UI components; calls core + layout | wgpu directly, windowing |
akar-c-api |
extern "C" surface, AkarCtx opaque handle |
Business logic (delegates to components) |
akar-winit |
winit event → akar input bridge | Everything else |
Once akar-c-api exists, agents integrating akar from non-Rust languages must:
- Link against the compiled shared library (
libakar.dylib/libakar.so/akar.dll). - Include the generated
akar.h(do not write it manually — it iscbindgenoutput). - Call
akar_ctx_new(device_ptr, queue_ptr, surface_format_raw, AKAR_FONT_SOURCE_BUNDLED)to create anAkarCtx*.surface_format_rawis the wgpu texture-format discriminant for the target surface; use the bundled font source for the normal reproducible default. All subsequent calls take this pointer as the first argument. - Each frame: call
akar_begin_frame(ctx, width, height, dpi), submit input viaakar_set_mouse*/akar_push_charetc., call component functions, callakar_end_frame(ctx). - Call
akar_ctx_free(ctx)on shutdown.
No heap allocations are expected on the caller side beyond the context handle. All internal buffers are owned by the context.
The draw list is the internal rendering queue. Agents extending the renderer must follow:
- All draw calls are submitted via
DrawList::push(DrawCall)duringbegin_frame→end_frame. - Before GPU upload,
DrawList::flush()culls calls whose AABB does not intersect the active scissor rect. This is automatic and invisible to component authors. - Scissor rects are pushed/popped in a stack (
DrawList::push_scissor/pop_scissor). Scroll areas push a scissor before rendering children. - Z-order is explicit: each
DrawCallcarries az: f32. The draw list sorts ascending before flush.
Each component function:
- Calls
akar-layoutto query resolved pixel bounds for its node ID. - Checks hit-test from the input state to determine hover/active/focus/click.
- If clicked, mutates any caller-owned state (
*checked,*selected, etc.) immediately — before submitting draw calls. Drawing must always reflect the post-click value, never the pre-click one. A component that draws first and mutates after looks correct in isolation but silently lags one frame behind every click, and needs an unrelated event (e.g. a mouse move) to ever catch up. See checkbox/switch/radio/tab_bar history. - Submits background rect + border rect + text (if any) to the draw list via
akar-coreprimitives, using the already-updated state. - Returns a state enum (
Idle | Hovered | Pressed | Focused) or a typed result (Clicked: bool,value: f32, etc.). - Must work correctly with a zero-area rect (when the layout system gives it no space).
For scroll containers and list components:
- Always push a scissor rect before rendering children; pop it after.
- Expose
list_clip(total, item_height, scroll_y)so developers can avoid submitting off-screen items entirely. - Do not call
glyphon::Buffer::shape_until_scrollfor items outside the scissor rect.
When working with canvas changes, verify at one overview level and one interactive-portal level using screenshots. Use --component or --screenshot with examples/canvas-basic-rust/.
Low-detail canvas interaction is group-level only — hover/press/click on the whole object, not child widgets. CanvasInput operates on WorldRect bounds.
Canvas text is display-only. It never creates focus, widget state, or text-buffer IDs. Use portal mode for interactive text inputs, selects, buttons, or any component requiring child interaction.
canvas_portal_begin/end push/pop scissors. The portal subtree renders through normal component APIs — no canvas-specific component variants needed. Portal layouts must use unique namespace_id values to avoid widget ID collisions.
The scissor stack intersects automatically. A portal inside a canvas is clipped to both the portal bounds and the canvas bounds.
glyphon text renders after quads globally. Do not expect strict quad/text ordering within a frame — this is a known renderer limitation.
Reference: examples/canvas-basic-rust/ for the canonical LOD + portal pattern.
- No live GPU in CI — component logic and layout resolution must be testable without a real wgpu device.
- A
MockDrawListthat records submitted calls is the primary unit-test tool. - Visual verification uses the debug toolchain (see "Debug toolchain" above). The fastest loop is usually
--component <name> --screenshot …for a tight, noise-free image;--scriptfor interactive states;--dump-layout/--dump-framewhen the visual alone is not enough;akar-diff --compareagainst a baseline to detect regressions. - C ABI tests are written in C and compiled as integration tests under
crates/akar-c-api/tests/. - Run
cargo test --workspaceto execute all tests.
- Follow the conventions in
DEVELOP.md→ Coding Conventions. - No emojis in source or docs unless explicitly requested.
- No comments unless the WHY is non-obvious. Code should be self-documenting.