Build your UI once. Test it deterministically. Run it anywhere.
Imprint UI is a tiny, dependency-free, software-rendered C++17 GUI framework with an automation-first contract: the host drives everything, so the same input sequence always produces the same pixels — UI logic you can assert on pixel by pixel, in CI, with no display attached. The same UI source tree compiles for Windows, Linux, macOS, the browser (WebAssembly) and the Nintendo DS — develop and preview on your PC, then ship the very same code to the device.
One UI source tree. One pixel buffer. Many targets.
Try it live in your browser — the page above runs the WebAssembly build; the Nintendo DS frame comes from the same source compiled with devkitARM.
The same showcase, unmodified, on four native shells — including a 690 KB ROM at 60 fps on the Nintendo DS:
No GPU required. No OS GUI toolkit required. No platform-specific UI code.
same UI source
│
┌───────────┼───────────┐
↓ ↓ ↓
Windows Linux macOS
│ (X11/FB) │
└───────────┼───────────┘
↓
WebAssembly ← try it in your browser
↓
Nintendo DS
↓
your embedded board (C-ABI)
Measured footprints (Release builds of the showcase app above):
| Target | UI code+data | RAM (statics) | Framebuffer | Shipped size |
|---|---|---|---|---|
| Nintendo DS | 543 KB text + 11 KB data | 7.7 KB BSS | 96 KB (256×192×2 B) | 646 KB .nds |
| WebAssembly | — | — | 256×192×4 B | 250 KB single .js file, runs from file:// |
- Retained-mode widget tree —
Button,Label,Dialog,FlexPanel,GraphicsViewand more - Design files — describe a UI in a small text format (
.ui), validate and pack it at build time, load it from a C array on any target; a preview app renders files directly - Software rendering into a raw pixel buffer — no GPU, no external rendering library; the buffer format is fixed at build time (
COLOR_DEPTH) - Deterministic repaint-on-demand — dirty tracking, shell owns the loop, no hidden redraws
- C-ABI as a first-class citizen — stable
zbapiC interface with Python (ctypes), WebAssembly and C smoke-test hosts - Automation-friendly by contract — the host-drives-everything model means a script can replace the user: feed input, pump frames, assert on pixels; single-threaded and timer-free, so drivers never sleep — the test battery includes an end-to-end
automationsuite driven through the public API - Embedded-grade — no RTTI, 16-bit color (abgr1555), integer-only geometry option, non-atomic refcounting option (NDS has no libatomic)
- Zero-allocation hot paths — RAII
ClipGuard, event tombstoning,Subscription - UTF-8 text throughout — built-in 5x7 bitmap glyph fallback (auto-subsetted from source strings); optional runtime TTF text via vendored stb_truetype, vendored stb codecs (PNG/JPEG) and a hand-written GIF writer
- C++17, CMake, static libraries — everything is composable, nothing is forced
GPU-accelerated drawing (the render kernel stays CPU software rasterization) · animation/transition system · runtime backend switching · multithreaded rendering · IME composition · RTL layout. Imprint UI deliberately stays small: one widget tree, one pixel buffer, one input stream — everything else is the host's job.
#include "imapp.hpp"
#include "imui.hpp"
int main()
{
auto app = zb::app::make_app();
app->create_window(320, 240);
auto* win = static_cast<zb::app::CanvasWindow*>(app->window().get());
auto btn = std::make_unique<zb::ui::Button>();
btn->set_size(100, 40);
btn->set_text("Click Me");
btn->clicked += [] { printf("Hello!\n"); };
win->root().add_child(std::move(btn));
app->paint();
}The same screen described as a design file (tools/examples/menu.ui):
column id="root" spacing=6 padding=10
label id="title" text="Settings"
checkbox id="sound" text="Sound"
slider min=0 max=100 step=10
list_box rows=3 items="Easy" "Normal" "Hard"
row spacing=4
button id="ok" text="OK"
button id="cancel" text="Cancel"
Pack it at build time with ui_embed (fails the build on invalid files), then
parse_ui_text + build() materialize it — the same code path on every
platform. Preview interactively with the ui_preview app:
UI_PREVIEW_FILES="tools/examples/menu.ui" cmake -B build/build_linux -DSTORY=ui_preview -DIM_SHELL_BACKEND=FB && cmake --build build/build_linux
| Target | Command | Notes |
|---|---|---|
| Windows (MSVC) | cmake -S . -B build/build_win && cmake --build build/build_win |
zero-dependency default (32bpp) |
| Runtime TTF text | cmake -S . -B build/build_rt_ttf -DUSE_TTF_RUNTIME=ON && cmake --build build/build_rt_ttf |
runtime glyph rasterization (batch L-5): apps load a font via TtfFamily, no external dependency |
| macOS (AppKit) | cmake -S . -B build/build_mac && cmake --build build/build_mac |
no deployment-target pin (toolchain default), no extra options |
| Linux (X11) | cmake -S . -B build/build_linux -DIM_SHELL_BACKEND=X11 && cmake --build build/build_linux |
input-capable backend |
| Linux (framebuffer) | cmake -S . -B build/build_linux -DIM_SHELL_BACKEND=FB && cmake --build build/build_linux |
presents only; use X11 for interaction |
| Nintendo DS | docker run --rm -v $PWD:/src -w /src devkitpro/devkitarm:20260610 sh -c 'cmake -S . -B build/build_nds -DCMAKE_TOOLCHAIN_FILE=cmake/nds.toolchain.cmake && cmake --build build/build_nds' |
produces build/build_nds/bin/tictactoe.nds; add -DSTORY=showcase for the showcase ROM (it additionally needs the host-built ui_embed and asset_gen passed as -DUI_EMBED_EXECUTABLE= / -DASSET_GEN_EXECUTABLE=) |
| WebAssembly | demo/wasm/build.sh (docker emscripten) |
includes a node smoke test |
| Python | build the binding shared lib, then SDL_VIDEODRIVER=dummy python3 demo/python/myapp.py --lib <libzbapi> |
ctypes + pygame host |
Tests: test/test_imui — plain asserts, no framework; automatic on desktop builds, skipped on NDS.
The app owns a fixed-size pixel buffer (create_window(w, h)) and
never re-lays out for a window resize. Desktop shells (win32 / X11 /
macOS) open the window at buffer size and let you resize it freely: the
buffer is presented scaled to fit, aspect preserved, centered on a black
letterbox, with nearest-neighbor resampling — the same buffer at the
same window size renders identically on every desktop platform. Pointer
input maps back through the same integer formula the stretch uses
(buf = (win - dest) * buf / dest), so hit-testing stays exact at any
scale; clicks on the letterbox are ignored. The NDS and framebuffer
shells present 1:1; WASM/Python hosts scale host-side.
Suggested reading order (first pass for a new maintainer):
docs/getting-started.md— run your first app and make it your own (~5 minutes)- this README → Build (get a binary running on every target)
docs/ARCHITECTURE.md§1–§2 — what the system is, module map & dependency rulesdocs/ARCHITECTURE.md§3–§5 — the normative contracts, targets, limitationsdocs/code-contract.md— the API-level contractsdocs/design-file.md— when working with.uifiles
Where to look by task: touching public API → code-contract.md first (the contract changes before the API) · new target / pixel format / build option → docs/backlog.md & ARCHITECTURE §4 · .ui grammar or packaging → design-file.md · C-ABI host → zbapi.h + ARCHITECTURE §4.8 · build & run commands → Build below.
docs/getting-started.md— from a fresh clone to your own app: run thehellostory, understand theIApp/CanvasWindowseam, register your own storydocs/ARCHITECTURE.md— the as-built architecture: module map & dependency rules, contracts (frame lifecycle, input, pixel model, text, events, errors, C-ABI hosts, build options), and known limitationsdocs/backlog.md— the living backlog: architecture items, product feature batches (L/I/F), and condition-triggered itemsdocs/code-contract.md— the API-level interface contract: error paths, UTF-8/text, glyph provider, tree mutation, layout invalidation, alloc budget, the presentation-seam converterdocs/design-file.md— the.uidesign-file format: grammar, packaging pipeline, materialization semanticsbinding/include/zbapi.h— the C-ABI surface for hosts (Python, WASM, C); host rules in ARCHITECTURE §4.8
Hello (-DSTORY=hello) — the getting-started app: a label and a click-counting button; copy it to start your own app (see docs/getting-started.md).
Showcase (-DSTORY=showcase) — the widget gallery behind the multi-target montage: boots dark, opens on an animated chart drawn with the framework's own rasterizer (anti-aliased curve over a gradient area on a rounded card, revealed step by step by an app-side tween), a device-status control panel (progress bars, START/STOP, dark/light theme), and an all-widgets page with alpha asset compositing (a 9-slice shadow card and an accent-tinted ball; the assets are generated at build time by tools/asset_gen). The frames in assets/showcase/ come from these builds — the recorder is fully deterministic, producing byte-identical GIFs on Windows, macOS and Linux; the WASM variant is playable online (tyouhyou.github.io/imprint, built with demo/wasm/build.sh showcase), and the same sources build the NDS ROM.
TicTacToe (default story) — a human-vs-computer game exercising dialogs, buttons, layout and repaint-on-demand; the NDS build produces build/build_nds/bin/tictactoe.nds. A third app, ui_preview (-DSTORY=ui_preview), renders design files from UI_PREVIEW_FILES (space-separated paths; left/right keys switch documents).
| Windows | macOS | Linux (X11) | WebAssembly | Nintendo DS | Python host |
|---|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
MIT © 2026 tyou hyou











