Immediate-mode UI
The optional modkit/ui.h package is the application-UI layer: immediate-mode
widgets, Clay v0.14 box layout, ModKit’s retained shaping and bidi pipeline,
Canvas rendering, platform input, preferences and accessibility snapshots.
Enable it with --ui. This enables the required FONT, SHAPING and CLAY
packages; Slug remains optional and is not implied. Built-in transitions use
mk_ease directly and do not require the optional Tween package.
#include <modkit/ui.h>
mk_font_t font = mk_font_load("fonts/Inter.ttf", 18, MK_FONT_AUTO);mk_font_family_t family = mk_font_family_create(&font, 1);
mk_ui_desc_t desc;mk_ui_desc_init(&desc);desc.window = mk_get_main_window();desc.fonts[MK_UI_FONT_BODY] = family;
mk_ui_t ui = MK_UI_INVALID;mk_ui_create(&desc, &ui);ui.h is deliberately absent from the umbrella header: including its scoped
layout surface is an explicit choice. UI is C-only in V1 and has no JavaScript
binding.
Declare, finish, render
Section titled “Declare, finish, render”Build the interface once per frame. Widget calls return activation or change
results from events resolved at mk_ui_begin.
mk_ui_begin(ui, NULL);
mk_ui_box_desc_t column;mk_ui_box_desc_init(&column);column.width = mk_ui_grow(0, 0);column.padding = (mk_ui_insets_t){16, 16, 16, 16};column.gap = 8;
mk_ui_column_begin(ui, mk_ui_id_str(ui, "settings"), &column);mk_ui_toggle(ui, mk_ui_id_str(ui, "cloud"), "Cloud sync", &cloud_sync);if (mk_ui_button(ui, mk_ui_id_str(ui, "apply"), "Apply")) { apply_settings();}mk_ui_box_end(ui);
mk_ui_end(ui);
mk_begin_pass(&pass);mk_ui_render(ui);mk_end_pass();Use stable IDs and push an ID scope around repeated rows. Duplicate IDs log a diagnostic but do not blank the frame; only genuinely unbalanced box, ID or style scopes skip rendering.
Why text is not a Clay text element
Section titled “Why text is not a Clay text element”Clay lays out boxes only. Its word-oriented text callback cannot express
paragraph bidi reordering, cross-word shaping or shaping-aware line breaking,
so UI never emits CLAY_TEXT.
Each string instead has a cached mk_text_layout. Its shaped minimum width is
the longest unbreakable run and its preferred width is the widest unwrapped
hard-break line. UI gives those intrinsics to Clay, reads the assigned width,
wraps the retained layout, then performs a second Clay pass with that width
frozen.
At mk_ui_begin, UI applies pointer, scroll delta and elapsed time once against
Clay’s completed scroll-container registry. Both layout passes then see the
same pointer and perform no further mutating scroll update. This lets virtual
lists use the current scroll offset while ensuring momentum advances exactly
once. A newly wrapped paragraph can make its scroll container overflow and
change the width available next frame; that known case converges one frame
later.
The standalone modkit/clay.h bridge remains available and unchanged. There
is no raw-Clay escape hatch inside a UI frame yet; it will be reconsidered with
a replay callback after this two-pass contract is stable.
Text and font policy
Section titled “Text and font policy”Every role defaults to a caller-supplied family loaded with MK_FONT_AUTO.
DISPLAY and MONO inherit BODY when omitted. Slug is legal as an explicit role
override for large display text, zoomable interfaces or world-space panels,
but is not a good default for small interface text. Unsupported Slug rendering
falls back to MTSDF without changing the UI API.
mk_ui_text_edit operates on a bounded caller-owned UTF-8 buffer. It includes
grapheme and word movement, selection, shaped hit testing, clipboard commands,
undo/redo, password and read-only modes, IME composition, and distinct
single-line submit and multiline newline behavior.
Rich static text accepts mk_text_style_span_t ranges through
mk_ui_text_desc_t.spans, preserving shaping and bidi across style changes.
Set MK_UI_TEXT_SELECTABLE for browser-like pointer and keyboard selection and
query its UTF-8 byte range with mk_ui_text_get_selection. This surface styles
text only; inline links and interactive objects are deferred.
mk_ui_number_float and mk_ui_number_int combine horizontal drag adjustment
with direct entry on Enter or double-click. Escape restores the original value;
Enter or focus loss commits it. Integer paths retain exact integer stepping.
Range controls and collections
Section titled “Range controls and collections”mk_ui_slider_int complements the floating-point slider, and
mk_ui_progress publishes a determinate, non-interactive range. Slider and
progress fills render behind their label and current value.
Tabs use mk_ui_tab_list_begin, mk_ui_tab and
mk_ui_tab_panel_begin. Only the selected tab is in normal Tab traversal;
Left/Right and Home/End switch tabs and move focus as one composite.
mk_ui_virtual_list_begin returns a half-open visible range for a fixed-row
list. Declare that range with mk_ui_virtual_list_item_begin/end; the helper
adds stable index scopes and list-position semantics. Overscan and programmatic
nearest/start/center/end scrolling are supported. Variable-height rows are
deferred.
Input, themes and semantics
Section titled “Input, themes and semantics”Automatic event subscription uses priority 100 and filters by window ID. Pointer presses use ModKit pointer capture and activate only on release over the same enabled widget. Events hit-test the previous completed frame, so a new popup or modal receives pointer input starting on its next frame.
Manual event and navigation feeds support offscreen surfaces and application
action maps. Tab follows declaration order, directional input uses spatial
navigation, range controls consume horizontal adjustment, tab lists own
horizontal and Home/End navigation, and modals trap focus. Gamepad deadzone and
repeat timing are configurable on mk_ui_desc_t.
Keyboard, gamepad, accessibility and programmatic focus changes scroll into
view once. Pointer-focused controls are not anchored during later wheel or
touch scrolling.
Dark, light and high-contrast themes are built in. UI follows available system theme, text scale, high contrast, reduced motion and safe-area signals by default, with independent opt-out flags. Typed push/pop overrides cover color, scalar and font roles.
mk_ui_motion_t configures global hover, press, focus, enter and exit durations
and easing. Paint-only transitions cannot perturb layout and reduced-motion
snaps them to their final state. mk_ui_presence_begin keeps a subtree alive
through enter/exit fades, immediately disables its descendants during exit,
and blocks pointer fall-through by default.
Built-in widgets publish a semantic snapshot at mk_ui_end, including
PROGRESS, SPIN_BUTTON, TAB_LIST, TAB and TAB_PANEL roles. These additions use
MK_ACCESSIBILITY_NODE_VERSION 3. Native accessibility adapters do not exist
yet, so snapshots are currently verified by tests rather than consumed by the
operating system.
See example 83_ui for responsive columns, scrolling range controls, numeric
entry, selectable rich text, editable mixed-direction text, touch/controller
navigation, tabs, a virtualized server list, a fading modal and theme switching.