Skip to content
modkitv0.2

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.

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.

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.

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.

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.

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.