Skip to content

Latest commit

 

History

History
612 lines (521 loc) · 31.2 KB

File metadata and controls

612 lines (521 loc) · 31.2 KB

Additional implementation notes

For setup and runnable programs, start with basic usage and examples.

Retained Programming Model

UIRuntime is the main-thread owner for one native UI surface. It owns a renderer-neutral stable-slot arena, a unique surface ID, and revisioned SurfaceMetrics. Retained coordinates are logical units local to the surface, with (0, 0) at the top left. The host supplies both logical and framebuffer sizes plus DPI scale; Core does not include platform or Vulkan types.

UIView is a move-only ownership scope. Objects adopted by a view receive typed non-owning WidgetHandle<T> identities. Checked lookup validates surface, slot, generation, and concrete type in constant time. Destroying a parent view or its runtime destroys every descendant once and makes old handles stale.

struct PanelState {
    int selectedRow{0};
};

Nikerva::UIRuntime ui;
(void)ui.updateSurfaceMetrics({1280.0f, 720.0f}, {2560U, 1440U}, 2.0f);
Nikerva::UIView panel = ui.createView();
auto state = panel.emplace<PanelState>();
ui.resolve(state)->selectedRow = 3;

Signal<...> creates move-only Connection owners. A connection observes its source weakly, while ConnectionGroup lets a view or application object own several subscriptions. Destruction is safe in either order and callbacks may disconnect themselves during dispatch. Connecting may allocate; notification dispatch does not allocate inside the signal.

UIView::createRootPanel() and container-handle factories add persistent nodes to that same arena. UINodeRef performs checked parent/child/sibling traversal, insertion, moves, and reparenting. A presentation node may move inside its own UIView or attach below an ancestor view; it cannot move into an unrelated or shorter-lived view. This keeps visible topology separate from lifetime ownership.

Nikerva::UIRuntime ui;
Nikerva::UIView view = ui.createView();
auto panel = view.createRootPanel("statistics-panel");
auto fpsLabel = panel.createLabel({}, "fps");
auto reset = panel.createButton("Reset", "reset");

Nikerva::Property<double> fps{60.0};
Nikerva::Property<bool> canReset{true};
fpsLabel.bindText(fps.readOnly(), [](double value) {
    return std::to_string(value) + " FPS";
});
reset.bindEnabled(canReset.readOnly());

Property<T> is move-only but keeps a move-stable shared source state. It compares in set(), emits synchronously only for real changes, and exposes a weak ReadOnlyProperty<T>. Observation delivers the current value once and then future real changes. Widget bindings are stored by the target node, so source or target may be destroyed first. Text, enabled, checked, selected, visible, progress, and renderer-neutral image values support one-way transformation.

Slider values and checkbox states additionally support normalized two-way bindings. Change-origin suppression prevents an edit from echoing recursively. Explicit begin/update/commit/cancel events keep validation, commands, history, and rollback decisions in the application rather than the UI library.

Nikerva::Property<float> volume{0.5F};
auto slider = panel.createSlider(0.0F, 0.0F, 1.0F, "volume");
slider.bindValue(volume, [](float value) {
    return std::round(value * 10.0F) / 10.0F;
});

beginMutationTransaction() coalesces repeated node/property records while keeping setters and events synchronous. mutationDiagnostics() distinguishes application setter calls, comparisons, no-ops, node creation/destruction/moves, events, bindings, node work, and transaction coalescing. The standalone sample reports zero application setters, comparisons, events, node work, and allocations across 1,000 unchanged frames after warm-up.

Every real retained mutation now queues exact UIDirtyCategory work at mutation time. processDirty() drains coalesced epochs in style, measure, arrange, transform/clip, and dependent-result order. Measure bubbles to the nearest container whose containment flag is true; if that container's desired size changes, its parent-facing dependency is remeasured before the same call publishes. Ancestor roots suppress queued descendant roots, and mutations created while processing converge through bounded later epochs. A clean call returns immediately without walking the tree.

Nikerva::UIRetainedLayout layout;
layout.mode = Nikerva::UIRetainedLayoutMode::Vertical;
layout.gap = 6.0F;
layout.clipChildren = true;
panel.setLayout(layout);

Nikerva::UIRetainedLayoutItem row;
row.preferredSize = {120.0F, 24.0F};
fpsLabel.setLayoutItem(row);

ui.processDirty();

Child bounds and inherited clips are computed in surface-local logical units. Linear layout iteratively freezes children at minimum/maximum constraints and redistributes remaining space. Impossible minimums are preserved in presentation order and clipped by the container. Programmatic padding/gap override stylesheet values; omitting them inherits the style, and the two sources never add. UIRetainedContentSizing explicitly selects whether measured visible content replaces a child's preferred width, height, or both. Measured text, icons, gaps, and padding still clamp to the child's minimum/maximum and the parent's available allocation; ordinary labels do not force content sizing on their parents. A host resize changes only SurfaceMetrics; the runtime updates every affected descendant automatically. DPI-only changes preserve logical geometry while invalidating text/resource/submission dependencies for later retained rendering.

UITheme owns immutable style data plus an explicit non-zero revision. Nodes retain type/class/id selectors, visual state, resolved style, and property dependencies. Replacing a theme with the same revision creates no work even if the C++ object has different pointer identity. Metric differences queue layout; color, opacity, border, and hover-only differences queue paint/submission only. dirtyDiagnostics() reports queued/coalesced/processed categories, minimal layout roots and reasons, selector/measure/arrange visits, bounds/clip changes, and future hit/text/paint/resource/submission hooks.

Pointer and keyboard input now enter the retained runtime as validated UIPlatformEvent values. processInput() performs no work when its queue is empty. Pointer events query a private per-surface spatial grid maintained from the same structure/bounds/clip/visibility/interaction mutations used by NUI-04; the public API does not expose the grid algorithm.

Handlers receive UIRoutedEvent in capture, target, and bubble phases. They can stop later phases, suppress built-in activation, inspect typed drag payloads, or accept a drag operation. Focus, pointer capture, hover paths, tab order, default/cancel actions, popup/modal stacks, cursor/tooltip state, and drag sessions all store generation-checked handles. Hide, disable, destruction, surface focus loss, popup dismissal, and expired drag data clean them up deterministically. Forced pointer-capture release routes pointer-cancel and capture-lost while the old owner is still valid. Non-modal popup dismissal does not consume the new target's pointer event unless explicit policy requests it. processInput() converges dirty mutations caused by its final queued event before returning; an empty input queue still performs zero dirty or input work.

auto save = panel.createButton("Save", "save");
Nikerva::UIInteractionOptions interaction = *save.interaction();
interaction.defaultAction = true;
interaction.tooltip = "Save project";
save.setInteraction(interaction);
save.onEvent(Nikerva::UIEventType::Click,
    [](Nikerva::UIRoutedEvent& event) {
        // Application commands remain outside Nikerva Core.
    });

Nikerva::UIPlatformEvent move;
move.type = Nikerva::UIPlatformEventType::PointerMove;
move.position = {32.0F, 18.0F};
ui.queueInput(move);
ui.processInput();

Consecutive queued pointer moves coalesce to the latest absolute position and sum relative delta. Absolute placement controls such as UISplitter therefore track the newest pointer even when intermediate moves are skipped. A current capture owner may call requestPointerPosition(); the host consumes the latest logical request after processInput() and applies the platform warp. This keeps platform policy outside Core while supporting hidden anchored number scrubbing. inputDiagnostics() reports coalesced moves, pointer-position requests, index updates, candidates, path depths, phase deliveries, focus/capture/popup/drag changes, event cost, and explicit fallback walks. NUI-05 has no full-tree fallback.

Retained paint is renderer-neutral. processDirty() caches text layout and per-node shape, image, text, and custom fragments, then composes immutable ordered packets with logical translation, inherited clip, layer, and resource revision data. A position-only change reuses local geometry and changes only the packet. Clean calls retain the same snapshot revision and shared data.

ui.processDirty();
Nikerva::UIPaintSnapshot snapshot = ui.paintSnapshot();

// Copies keep their immutable packets and fragments alive independently.
auto packets = snapshot.packets();

UIFontCatalog supplies revisioned renderer-neutral glyph metrics and an optional opaque atlas resource reference. UITextLayoutOptions chooses wrapping or a single line, clip or measured ... overflow, and an optional full-text tooltip that is active only when geometry is actually truncated. Every text packet intersects its inherited clip with its own resolved bounds. Text geometry is cached by content, font, size, logical constraint, DPI, layout policy, and catalog revision. Image and atlas references use non-zero logical revisions, so an older snapshot remains safe while a backend consumes a newer one. UICustomPaint similarly owns immutable primitive data with an explicit revision and never stores renderer handles.

UITextWrapping provides four policies through setTextLayout():

  • Wrap preserves the existing behavior: break between Unicode codepoints.
  • NoWrap keeps one line, with Clip or Ellipsis overflow.
  • WordWrap prefers spaces/tabs and breaks after existing hyphens.
  • IdentifierWrap additionally prefers ASCII camelCase/acronym boundaries (Flight|Control, XML|Parser) and breaks after _, ., /, and \.

The two word-based policies share one line-break calculation between measurement and paint. They preserve explicit newlines (including CRLF and empty lines), display tabs as single spaces, and omit boundary whitespace at soft line breaks. Words too wide for an empty line fall back to codepoint wrapping. These are deterministic UI rules, not language-specific hyphenation or full Unicode word segmentation. They never insert a hyphen or modify the stored text. Wrapped text retains the existing clipping behavior; Ellipsis applies to NoWrap. A fixed row still needs sufficient height for its wrapped lines.

(void)actionLabel.setTextLayout({
    .wrapping = Nikerva::UITextWrapping::IdentifierWrap,
    .overflow = Nikerva::UITextOverflow::Clip,
});

UIRuntime::tooltip() is the resolved hover state. Applications that want visible tooltip paint create one UITooltipPresenter in a surface-filling retained overlay root. The presenter is non-interactive, follows the current logical pointer position, wraps at its configured maximum width, clamps to the surface, and disappears as soon as Core clears the hover tooltip.

paintDiagnostics() reports rebuilt or reused text layouts, glyphs, fragments, vertices, packets, snapshot publications, and referenced resources. Virtual collections publish through this same path. Cutover-required composite controls and NikreonEditor integration remain later stages; current UIFrame applications are unchanged.

VulkanRetainedRenderer is the Vulkan-side consumer for one native surface. The host owns frame fences and calls beginFrame(slot) only after that slot's fence signals. Each slot has independent mapped shape and textured-instance storage, so updating one slot cannot overwrite data used by another frame in flight. Unchanged ranges, descriptors, compatible batches, clips, and packet metadata are reused. The renderer caches a maximal compatible batch plan for each immutable snapshot. It may cross only non-overlapping packets inside the same popup/layer/clip domain; resource changes, custom ranges, and painter-order overlap remain explicit boundaries. Shape and glyph/image quads use instanced draws. Revisioned image/font resources retire only after no submitted slot and no current snapshot references them.

Core paint packets remain in logical surface coordinates. The Vulkan consumer converts positions, extents, scissor rectangles, border widths, and corner radii with the actual per-axis framebufferSize / logicalSize ratio. dpiScale is independent metadata for text/resource/style decisions; it is not a substitute for framebuffer conversion. The offscreen regression intentionally uses DPI 1.25 with a 1:1 framebuffer ratio and verifies the resulting pixel and clip.

#include "Nikerva/Renderer/Vulkan/VulkanRetainedRenderer.hpp"

Nikerva::VulkanRetainedRenderer backend{
    device, physicalDevice, graphicsQueue, graphicsQueueFamily,
    uploadCommandPool, renderPass};

// The host has already waited for frameFences[frameSlot].
backend.beginFrame(frameSlot);
const auto snapshotResult = backend.consume(ui.paintSnapshot());
if (snapshotResult != Nikerva::VulkanRetainedSnapshotResult::Updated &&
    snapshotResult != Nikerva::VulkanRetainedSnapshotResult::Reused) {
    backend.finishFrame(Nikerva::VulkanRetainedFrameDisposition::Abandoned);
    return;
}
backend.recordTimestampBegin(commandBuffer); // before the render pass
// begin render pass
backend.record(commandBuffer);
// end render pass
backend.recordTimestampEnd(commandBuffer);
// submit commandBuffer with frameFences[frameSlot]
backend.finishFrame(Nikerva::VulkanRetainedFrameDisposition::Submitted);

Resource uploads use immutable UIResourceReference revisions. Upload submits to the supplied graphics queue and waits on one upload fence, so the owning thread must externally synchronize queue access. Ordinary frames never wait inside the backend. Swapchain/render-pass replacement is host-owned: after all referencing frame fences complete, call recreatePipelines(newRenderPass). VulkanRetainedRendererDiagnostics separates preparation, geometry-write, resource-upload, command-generation, batch/draw, fragmentation/compaction, and GPU timestamp costs.

Virtual Collections

VirtualizationController is the renderer-neutral collection core. The application still owns every domain object and supplies only an ordered count and opaque 128-bit stable keys through UIVirtualItemSource. After an accepted application mutation, call the matching notifyInsert, notifyErase, notifyMove, or notifyUpdate; ordinary scrolling never reads the application collection. Filtering and sorting remain application-owned and use an explicit synchronize() of the new presentation order.

The controller keeps an incremental order/extent index. Fixed item extents use constant-time visible-range arithmetic. Variable extents use logarithmic cumulative lookup and setMeasuredExtent(). Both preserve the key at the top of the viewport when items or measurements change before it. Item extents are logical units and include any row gap the future collection control wants.

UIVirtualCellHost maps logical cells to consumer-owned widget templates. It must outlive the controller and must not call back into it. The controller calls resetCell() immediately before every new binding, so hover, handlers, edit buffers, and custom template state cannot leak from one item to another. Cell handles carry a generation that changes on recycle. Selection, expansion, focus, edit, capture, and accessibility identity live under the stable item key instead, so they survive recycling and application-owned filter/sort resets.

Nikerva::UIVirtualizationConfig config;
config.estimatedItemExtent = 24.0F;
config.overscanItems = 2;
config.recycleReserve = 4;

// cellHost creates/resets/binds future concrete widget templates.
Nikerva::VirtualizationController virtualItems{cellHost, config};
virtualItems.synchronize(itemSource);
virtualItems.setViewport({scrollOffset, viewportHeight});
const Nikerva::UIVirtualRealizeResult result = virtualItems.realize();

// The domain collection commits first, then only the changed keys are read.
domainItems.insert(domainItems.begin() + index, newItem);
virtualItems.notifyInsert(itemSource, index, 1);

The controller exposes live-cell, key-read, node-allocation, index-visit, extent-update, recycling, and per-realize timing diagnostics.

UIListView, responsive UIGridView, UITreeView, and UITableView turn that model into persistent retained controls. Each control owns its root, clipped scrolling content, bounded template pool, interaction state, and realized accessibility projection. The application still owns item data, filtering, sorting, commands, and drag payload lifetime. The source and every object captured by a cell factory must outlive the control.

#include "Nikerva/UI/Retained/Collections.hpp"

Nikerva::UICollectionCellFactory rows =
    [&names](const Nikerva::UIContainerHandle& parent) {
        auto root = parent.createContainer("project-row");
        auto label = root.createLabel({}, "project-row-name");
        Nikerva::UICollectionCellInstance cell;
        cell.root = root;
        cell.reset = [label] { (void)label.setText({}); };
        cell.bind = [&names, label](
            const Nikerva::UICollectionCellBinding& binding) {
            (void)label.setText(names.at(binding.virtualization.item));
        };
        cell.update = cell.bind;
        return cell;
    };

Nikerva::UICollectionViewOptions listOptions;
listOptions.virtualization.fixedItemExtent = 24.0F;
Nikerva::UIListView list{ui, panel, filteredKeys, rows, listOptions};
list.setViewportSize({320.0F, 240.0F});
ui.processDirty();

Nikerva::ConnectionGroup subscriptions;
subscriptions.add(list.onSelectionChanged(
    [](const Nikerva::UICollectionSelectionChange& change) {
        // Translate stable keys into an application command here.
    }));

// Mutate application data first, then describe only the affected range.
filteredKeys.insert(index, addedKeys);
list.notifyInsert(index, addedKeys.size());

setViewportSize() requests fixed root layout size; the effective viewport is always derived from final resolved root bounds. Ordinary fill/grow composition does not call it. UIGridView derives its lane count from that viewport and minimum cell width. UITreeView consumes an application-owned parent/child source and incrementally inserts or erases only visible flattened descendants during expand/collapse and structural mutations. UITableView virtualizes rows while retaining headers; column sort callbacks request an application reorder rather than sorting domain objects inside Core. Selection, tree expansion, row focus, and table cell focus use stable keys, so explicit filter/sort synchronize() calls do not attach state to recycled templates. Tree rows reserve a real retained disclosure button; its visible bounds are its exact pointer target, while row click selection, double-click expansion, Enter activation, and Left/Right navigation remain separate stable-key operations across recycling.

Every concrete collection owns a generic retained vertical UIScrollbar driven by its resolved virtual viewport and content extent. UIScrollbarOptions selects Auto, Always, or Hidden, orientation, line step, thickness, and minimum thumb size, plus ReserveSpace or OverlayContent placement. Collections default to editor-style Auto + ReserveSpace: overflow reserves a gutter, a short Auto collection keeps its full width, Always reserves, and Hidden never reserves. The reserved extent is the interactive scrollbar thickness plus reserveGap, so content paint, clips, and hit regions end before the visible track instead of touching it. Overlay placement ignores that separation and keeps the full viewport. The standalone control supports vertical or horizontal wheel, keyboard, track-page, and captured absolute-position thumb input. Collection wheel offsets clamp before entering virtualization, so overshoot reaches exact zero/maximum, and a handled nested wheel event does not also scroll an ancestor. Collection metric synchronization is internal output and never writes resolved viewport dimensions back into parent-facing layout inputs.

Cutover-Required Retained Controls

NUI-10 adds the reusable form, shell, popup, navigation, and composition controls required by the current NikreonEditor inventory. These remain isolated library controls: NikreonEditor does not use them until the later hard cutover.

Retained control Current named consumer
UITextField, UISearchFilterBar ContentBrowserPanel asset search/type filters and Project Browser form fields
UINumberField, UIVectorNumberField, UISliderNumberField Inspector scalar/vector channels, camera values, opacity, roughness, and other bounded numeric properties
UICheckboxHandle, UIToggleSwitch Inspector boolean rows and editor settings/tool-state presentation
UIPropertyGrid Inspector, Project Settings, Material Editor, Input Action, and Mapping Context property forms
UISplitter, UITabView, UIDockModel EditorUI hierarchy/viewport/inspector splits and bottom-dock tabs
UIMenuBar, UIContextMenu EditorUI command menus and ContentBrowserPanel item/filter menus
UIDialog EditorUI close guard plus project/content confirmation flows
UISearchableComboBox ToolbarPanel mode choices and ContentBrowserPanel type selection
UIBreadcrumb, UICollapsibleSection ContentBrowserPanel path navigation/folder tree and responsive editor sections
UIToolbar, UIStatusBar, UIProgressControl ToolbarPanel and ContentBrowserPanel action/status/import presentation
UIPickerView Inspector asset-valued fields and Content Browser selection flows
UICustomWidget editor viewport/custom preview composition and style-state extensions

UINumberFieldMode selects plain text entry, horizontal scrubbing, spinner buttons, or scrubbing plus spinner buttons without duplicating parsing, focus, range, or edit-lifecycle logic. UISliderNumberField composes the same exact entry with a retained slider and keeps both values synchronized by targeted mutations. Axis strips are optional presentation metadata for X/Y/Z/W channel compositions. UIVectorNumberField composes two to four persistent number fields, distributes the resolved width equally, and emits the active component plus the complete vector value in one typed transaction. It has preferred and minimum content measurements but no Inspector-specific fixed width, so a parent value column remains authoritative. Scrubbing uses accumulated relative motion, applies Shift/Ctrl fine/coarse multipliers per movement segment, hides the cursor after its threshold, returns it to the captured press anchor, and preserves the field's arranged/text origin while only its value changes. Optional units are a separate retained suffix label: they never enter the parse buffer, caret, or selection, hide while text editing is active, and return on commit/cancel. Genuinely truncated units use measured ellipsis plus a conditional full-value tooltip. UIToggleSwitch reuses checkbox focus, keyboard, checked-state, and begin/update/commit/cancel semantics while publishing a switch track/thumb visual through replacement custom paint, so no default checkbox rectangle leaks behind it. A standard retained checkbox remains the compact square-mark variant.

Composite controls own their retained nodes, local presentation state, and event connections. They destroy their complete subtree when the C++ wrapper is destroyed. The runtime, parent view, application-owned virtual source, and any objects captured by factories must outlive the corresponding control.

UIPropertyGridSource supplies stable keys and typed presentation metadata. The grid selects the exact editor for std::string, bool, float, std::array<float, 3>, or choice-key values and emits begin/update/commit/cancel events. Vector rows use one responsive X/Y/Z group and publish the complete array when one component changes. It never converts or validates values, mutates a Scene or asset, or records history. The application does that work and then calls notifyUpdate() for the accepted row. Only realized or explicitly changed realized rows read metadata. Because the default property text flow is SingleLineEllipsis, compact rows stay one line and a read-only value exposes its complete text through a tooltip only while it is genuinely truncated. A field may explicitly select UIPropertyTextFlow::Wrap; the grid then measures the resolved value-column width with Core's glyph metrics and updates that stable key's variable virtual extent in the viewport-dependency stage so following rows move below every rendered line.

#include "Nikerva/UI/Retained/PropertyGrid.hpp"

Nikerva::UIPropertyGrid properties{ui, inspectorHost, propertySource};
properties.setViewportSize({360.0F, 480.0F});

Nikerva::Connection edit = properties.onEdit(
    [&](const Nikerva::UIPropertyGridEdit& event) {
        // Validate and apply through the application's command/history layer.
        if (event.phase == Nikerva::UIEditPhase::Commit &&
            applyProperty(event.field, event.value)) {
            properties.notifyUpdate(indexOf(event.field), 1U);
        }
    });

Closed menu, combo, picker, and dialog popup roots stay retained but are automatically excluded from hit testing and paint snapshot composition by UIRuntime. Open roots retain a point, below-opener, or centered placement policy and Core recomputes/clamps placement from current surface-local bounds after resize and DPI changes. UIDockModel stores only a checked stable-key split/leaf/tab tree; serialization, native-window decisions, and editor workspace policy remain application-owned. UICustomWidget supplies a retained content host, optional local state, renderer-neutral custom paint, routed-event hooks, and explicit style-state classes without acquiring renderer or editor dependencies.

UIControlIconSet lets an application map renderer-neutral image resources to generic roles such as combo chevrons, TreeView disclosure, check, search/clear, spinner arrows, collapsible sections, submenu, close, and reset. Controls copy the relevant image values into persistent nodes when composed. Core never loads an atlas or depends on engine resource types. The host supplies any icon atlas; the small examples use text and shapes and require no icon atlas. Combo boxes use one static downward chevron. Searchable combinations focus their retained text field when opened, while small enums may disable search explicitly. Menu/combo popups opt into consuming a repeated press on their own opener without closing or duplicating the popup; ordinary popups retain outside-dismiss behavior.

NikervaRetainedControlsTests supplies broader control construction fixtures beyond the small preferences example. It covers focus, UTF-8 editing, capture, modal blocking, popup paint exclusion, styles, DPI, lifetime, typed PropertyGrid edits, changed/realized row work, dock validation, and clean- frame diagnostics.

Surface And Frame

UISurface gives each UI tree its own local coordinate space. UIFrame carries the surface, resolved style, shape renderer, text renderer, input context, and synchronized clipping.

UIContext input;
UIStyle style;
UISurface hud{{24.0f, 24.0f}, {320.0f, 180.0f}};
UIFrame frame{input, shapes, text, style, hud};

Label title{"hud.title", "Mission"};
title.setBounds({12.0f, 12.0f}, {180.0f, 24.0f});
title.setStyleClass("heading");
title.render(frame);

Widgets

The core target provides:

  • Button, Checkbox, Slider, NumberInput, and TextInput
  • Label, Panel, textured Image/Icon, NineSlicePanel, and ProgressBar
  • ScrollContainer with clipping, child ownership, content offsets, wheel scrolling, draggable thumbs, and theme-owned scrollbar styling
  • Linear, dock, stack, anchor, padding, gap, and alignment layouts

Composite controls own their editing details. Render Slider, NumberInput, and TextInput with render(frame) to draw shape, value text, selection, and caret without reaching into nested controls.

Slider volume{"hud.volume", 0.8f};
volume.setBounds({12.0f, 52.0f}, {180.0f, 20.0f});
volume.update(input, text, style);
volume.render(frame);

Declarative Builder Contract

UIBuilder exposes type-specific builders over one private element record. Unsupported combinations are rejected by C++ constraints instead of being stored and silently ignored.

Builder family Supported content and behavior
PanelBuilder / surface() Normal children, layout, scrolling, ordered custom paint
LabelBuilder / text() Text and tooltip only; never acquires action state
ImageBuilder / icon() Renderer-neutral texture ID, UVs, tint, intrinsic pixel size, fit, snapping
SeparatorBuilder / SpacerBuilder Layout-only visual separation or empty extent
ButtonBuilder Text, secondary text, icon, selected/enabled state, click callback
Choice/menu builders Popup children plus their typed choice/menu properties
Value builders Only the value, range, label, edit, and callback facets relevant to that control

Linear layouts support explicit minimum/maximum size, margin, grow, and shrink. Overlay children share the parent content rectangle: declaration order is painter order and input walks the same children front-to-back. Scroll clips propagate to input, images, text, and custom paint automatically.

UIImageFit::Contain, Cover, and Stretch operate on the supplied intrinsic pixel size and UV rectangle. UI coordinates are physical framebuffer pixels; pixel snapping rounds the image origin or both image edges in that coordinate space and never modifies atlas UVs.

Development Diagnostics

Configure NIKERVA_ENABLE_DIAGNOSTICS=ON to compile legacy frame validation and per-frame statistics in Debug or RelWithDebInfo. Release and MinSizeRel explicitly compile NIKERVA_DIAGNOSTICS=0, even when the option is enabled. With that instrumentation disabled, legacy statistics snapshots remain available and return zero values. Retained UIPaintSnapshot data and paintDiagnostics() are part of Core behavior and remain available in every configuration.

The builder reports stable, deduplicated layout findings. The Vulkan backend counts actual batches, descriptor/scissor changes, uploads, texture-slot splits, glyphs, and draw commands where they occur. statistics() returns the previous completed frame so a statistics UI does not recursively include its own partial submission. Instrumentation does not add flushes or render-order boundaries.

Styling

UIStyleParser supports a deliberately small CSS-like format with type, type.class, type#id, and type.class#id selectors. ID rules override class rules. Shared theme data is application-neutral; editor panel sizing, viewport colors, and toolbar configuration belong in the editor.

slider.hud {
    fill: 0.12, 0.14, 0.18, 1;
    accent: 0.36, 0.58, 0.82, 1;
}

text.heading {
    color: 0.92, 0.98, 1, 1;
    font-scale: 1.1;
}

Text inputs support focus, UTF-8 insertion, caret movement, selection, clipboard shortcuts, and horizontal overflow scrolling. Stylesheets can set their text, placeholder, selection, caret, and scrollbar colors.