Ansuz is a cross-platform UI framework written in Odin.
It offers an immediate-mode authoring style with a retained internal state manager, so you can write straightforward per-frame UI code while the framework tracks widget state, interaction, and transitions across frames.
Ansuz is still in development, the syntax especially is still quite inconsistant. PRs welcome.
- Immediate-mode API with internal state retention
- Flexible layout system:
- Flex containers
- Grid containers
- Scroll containers
- Stack containers and floating overlay layers
- Built-in widgets:
- Label and heading
- Button
- Menu button
- Checkbox
- Slider
- Dropdown
- Text input (single-line and multi-line)
- Image widget
- Collapsible header
- Popup rendering for dropdowns and menu lists
- Animation support with easing functions and optional host-provided delta time
- Font support:
- Built-in bitmap font
- Manager default font plus per-widget overrides
- TTF loading for higher-quality text
- Multiple backends:
- SDL3 renderer backend
- Software renderer backend (embedded-style framebuffer path)
- WebGL backend for
js_wasm32
- C ABI and Arduino static-library workflow for freestanding hosts
ansuz/: Core UI framework packagebackend_sdl/: SDL3 backend implementationbackend_soft/: Software framebuffer backendbackend_webgl/: WebGL backend for web buildsbindings/c/: Complete C ABI and public header for Arduino/freestanding usedemo/: Desktop SDL demodemo_soft/: Software renderer demodemo_web/: WebAssembly/WebGL demodemo_arduino_oled/: Arduino OLED demo and static-library build
- Odin compiler (recent version with
js_wasm32support for web builds) - SDL3 available for desktop demos/backends
- Python (optional, only to serve web demo locally)
This is the minimal frame loop pattern with the SDL backend:
package main
import ansuz "../ansuz"
import backend "../backend_sdl"
main :: proc() {
sdl := backend.create(960, 540, "ansuz app")
if !sdl.init(&sdl, sdl.width, sdl.height) {
return
}
defer sdl.shutdown(&sdl)
mgr: ansuz.Manager
ansuz.init(&mgr, &sdl)
defer ansuz.shutdown(&mgr)
// Built-in bitmap font defaults are available immediately.
ansuz.DEFAULT_FONT_SCALE = 2
for !ansuz.should_quit(&mgr) {
ansuz.frame_begin(&mgr)
ansuz.flex_begin(
&mgr,
axis = .Vertical,
gap = 10,
size = {ansuz.SIZE_GROW, ansuz.SIZE_GROW},
padding = {16, 16, 16, 16},
)
ansuz.label(&mgr, "Hello from ansuz")
_ = ansuz.button(&mgr, "Click")
ansuz.flex_end(&mgr)
ansuz.frame_end(&mgr)
free_all(context.temp_allocator)
}
}From the repository root:
odin run demoOn Windows, make sure SDL3.dll is discoverable by the executable. When running
from the repository root, either keep SDL3.dll in the root directory or have it
on PATH.
odin run demo_softWindows:
cd demo_web
.\build.bat
cd web
py -m http.server 8080macOS/Linux:
cd demo_web
./build.sh
cd web
python3 -m http.server 8080Then open http://localhost:8080.
cd demo_arduino_oled
.\build.bat m4
# or:
.\build.bat rp2040The build produces build/libansuz.a and build/ansuz.h. The Arduino sketch
constructs the UI directly through the C interface; no application-specific UI
code remains inside the Odin object. See
bindings/c/README.md for the lifecycle and API usage.
Typical usage follows this order each frame:
frame_begin- Build layout and widgets (
flex_begin/flex_end,grid_begin/grid_end, widget calls) frame_endfree_all(context.temp_allocator)if the frame used Odin temporary allocations
Internally, ansuz resolves layout, emits draw commands, handles deferred text/custom draws, executes backend rendering, and updates persistent widget state.
frame_begin also accepts an optional dt value. Pass frame_begin(&mgr, dt)
when the host application owns timing, or omit dt to use the default animation
tick path.
Ansuz resets its internal frame arena at frame_begin, but it does not reset the
host application's context.temp_allocator. If your frame code builds labels,
menus, option arrays, formatted strings, or other short-lived values with
context.temp_allocator or helpers such as fmt.tprintf, reset that allocator
after frame_end:
for !ansuz.should_quit(&mgr) {
ansuz.frame_begin(&mgr, dt)
// Build UI here. Values allocated from context.temp_allocator only need to
// live until frame_end has rendered the frame.
ansuz.frame_end(&mgr)
free_all(context.temp_allocator)
}Do not store pointers, slices, or strings allocated from context.temp_allocator
in persistent app state. Clone those values with the regular allocator first, or
store them in your own explicitly managed allocation.
Use flex_begin/flex_end for row or column layout, grid_begin/grid_end
for explicit cells, scroll_begin/scroll_end for clipped scroll regions, and
stack_begin/stack_end when children should share the same parent rectangle.
Flex containers, stack containers, and leaf box calls accept floating = true.
Floating boxes are emitted after the normal UI pass, which makes them suitable
for modal backgrounds, dialogs, and other overlays. The manager's modal_owner
field can be set while a modal is open so interaction outside the modal's ID
stack is blocked.
Dropdowns and menu_button use the popup overlay pass. Popup lists carry their
own font, sizing, text, border, hover, and selected-state colors, and input is
blocked behind an open or just-closed popup to avoid click-through.
ansuz is backend-agnostic through the Backend interface in ansuz/backend.odin.
A backend provides function pointers for:
- Initialization and shutdown
- Per-frame begin/end hooks
- Draw command execution
- Text measurement
- Event polling
- Font upload
- Clipboard access for text input where supported
This allows the same UI code to run on desktop, embedded-style software pipelines, and web targets with backend-specific rendering/event handling.
- Omit
fontto use the manager's default font. The built-in bitmap font is the initial default. - Use
FONT_BUILTINas an explicit per-widget override, even after setting a TTF default. - Use
load_fontandset_default_fontto change the manager default for all widgets. - Demos use OpenSans for anti-aliased text rendering.
Text colors use a plain Color override, while interactive widgets accept a
Widget_Color palette for their state colors. All color arguments have built-in
defaults and can be omitted.
Pointer-based helpers such as animate_f32, animate_color, and spring_f32
are still available for ordinary widget state. ID-based variants such as
animate_f32_id, animate_color_id, spring_f32_id, cancel_animation_id,
and is_animating_id let callers drive animations from explicit widget IDs.
Core widget modules currently include:
widgets.odinmenu.odincheckbox.odinslider.odindropdown.odintextinput.odinscroll.odinimage.odin
Additional supporting systems include animations/easing, transitions, interaction state, and reactive value plumbing.
SDL2.dllmay be present in the workspace, but the current desktop backend implementation imports SDL3 (vendor:sdl3).

