Repository

lib-ui

Kinogaki's UI framework: an immediate-mode UI (begun from egui 0.36.2, now owned code) with Kinogaki's own theme, icons, docking, numeric input and editor widgets.

GitHub
kinogaki/lib-ui (private)
Folder
libs/lib-ui/
Crates
5
Rust lines
96,265 in src, 13,759 in tests
depends on (an edge implied by others shows when you point at a box)used by tests alone

Crates

CrateWhat it isFilesLinesTest lines
kinogaki-dialogsNative file dialogs behind one API: open one file, open several, save, or choose a directory, with file type filters, a starting directory and a suggested name.47360
kinogaki-propertiesProperty schemas: what an inspector shows, as data.122,836265
kinogaki-uiKinogaki UI: the immediate-mode UI framework Kinogaki applications are built on.30783,82512,940
kinogaki-ui-shellKinogaki UI's native shell: opens the windows, runs the winit event loop, turns window events into kinogaki_ui input, paints each frame with kinogaki-ui-wgpu and drives an App.205,735491
kinogaki-ui-wgpukinogaki-ui drawn with wgpu.103,13363

README

lib-ui

Kinogaki's UI framework: an immediate-mode UI (begun from egui 0.36.2, now owned code) with Kinogaki's own theme, icons, docking, numeric input and editor widgets. It is a Cargo workspace with no dependency on Kinogaki Studio, so other Kinogaki applications can build on it. Icons are drawn by lib-svg (kinogaki-svg), checked out beside this repository (Studio keeps both as sibling submodules):

It builds beside its sibling lib-svg (../lib-svg), and its kinogaki-properties reaches the SDK at ../../sdk for its optional conversions. In the Kinogaki Studio tree every library sits at libs/<name> beside the others and the SDK at ../../sdk; clone the root repository (git clone kinogaki/studio kinogaki-studio) and run its tools/setup.py.

PathContents
crates/ui (kinogaki-ui)The framework: geometry (math), colors (color), shapes, tessellation and text (paint), input, layout, containers and widgets, plus Kinogaki's theme tokens and fonts, the shared Rounded icon set, docking panels, the property sheet renderer, and editor widgets: held-key marking menu, Tab command browser, icon shelf, property sheets (typed inspector rows), floating parameter card, family context menus
crates/wgpu (kinogaki-ui-wgpu)The renderer: a frame's shapes and paint callbacks drawn with wgpu (RenderState, Callback), and capture: whole-UI screenshots without a window, for tests and captures
crates/shell (kinogaki-ui-shell)The native shell: windows and immediate viewports on winit, input and clipboard, the run loop and App (run_native)
crates/properties (kinogaki-properties)Property schemas as data (no UI): sections and typed fields with values, and the edits a renderer reports; numeric entry with units and arithmetic
crates/dialogs (kinogaki-dialogs)Native file dialogs behind one API (open, open several, save, choose directory; filters, starting directory, suggested name), presented without blocking the frame loop and answered through a callback: AppKit panels as sheets on macOS, the Common Item Dialog on its own thread on Windows, Linux not yet (Unsupported)
vendor/Every third-party crate the build uses, frozen as source at fixed versions (vendor/FROZEN.md); nothing resolves from crates.io
tools/vendor.pyFreezes crates into vendor/ and writes each workspace's [patch.crates-io] block

Other workspaces resolve the crates lib-ui freezes to this vendor/ through their own [patch.crates-io] block (paths ../lib-ui/vendor/... from a library, libs/lib-ui/vendor/... from the Kinogaki Studio root, which also freezes its own few in its vendor/): lib-viewport, lib-color, lib-math, lib-composit, lib-render and the root workspace. The root's tools/check_architecture.py fails when Studio or lib-ui resolve anything from crates.io, when one of these workspaces holds a registry copy of a crate lib-ui freezes, or when a vendored directory is missing from its FROZEN.md. After changing a dependency, re-run python3 tools/vendor.py patch (and align) in the workspaces that use it.

The framework began as egui 0.36.2 and is now Kinogaki's own code; docs/HISTORY.md lists what changed and what was removed.

Applications depend on the crates by path (crates/ui, crates/wgpu, crates/shell) and write kinogaki_ui::…; their workspace manifest carries the frozen [patch.crates-io] block (python3 tools/vendor.py patch) so every crate resolves to vendor/. Widgets take a list of MenuItems (label, icon, family, unavailable reason) and report the index the user chose; the application owns its command catalog and what running a command means. docs/ARCHITECTURE.md describes the layers and widget contracts; docs/components/ has a page per component, generated from its documentation by tools/component_docs.py.

The Kinogaki Studio root pins a reviewed commit of this repository at libs/lib-ui; commit and push here before updating that pin, and run the root's python3 tools/check_all.py for integration changes. The owned Kinogaki code is private and not open-source licensed; the code derived from egui and the frozen crates keep their MIT/Apache-2.0 and other licenses (see THIRD_PARTY_NOTICES.md).

Its documents

The repository's architecture document as written there (code blocks and links left out), and the opening of each other document.

lib-ui architecture (libs/lib-ui/docs/ARCHITECTURE.md)

lib-ui architecture

Layers

kinogaki-ui is everything that needs no window or GPU: math (geometry), color, paint (shapes, tessellation, text layout and the glyph atlas; glyphs are filled by kinogaki-svg's exact-area MaskRasterizer), input, layout, containers and widgets, and the Kinogaki modules below. It compiles in parallel with wgpu and winit. kinogaki-ui-wgpu draws a frame's triangles and paint callbacks with wgpu and holds capture; kinogaki-ui-shell runs winit windows (one per viewport), input, clipboard and the App loop; a window reads and writes the system clipboard on a thread of its own, in the order asked, so putting tens of megabytes there never stalls it (OutputCommand::ReplaceText puts a text in place of another only while the clipboard still holds it: an app's copy made in full after a short one went there at once). The code began as egui 0.36.2 and is ours now (docs/HISTORY.md). Every crate forbids unsafe code except kinogaki-ui-shell, which denies it and allows it only in src/sys.rs: winit's current event loop shared with immediate viewports, and the Metal layer's presentsWithTransaction for jitter-free live resize. wgpu and winit are the platform layers, frozen as source with every other third-party crate in vendor/.

kinogaki-ui modules

ModuleResponsibility
themeThe style tokens below (Theme::*); install_theme configures fonts, spacing and visuals from them and installs the icon loader
iconsThe embedded pixel-art icons (assets/icons-pixel/, generated by tools/pixel_icons.py) and their image loader: each parsed once by kinogaki-svg (currentColor white, so tints color it) and rasterized at the exact pixel size each use asks for, every pixel solid or clear; also loads .svg URIs; icon, icon_button
dockSplit/tab panel tree with drag-to-dock targets; each tab group's "+" adds a pane not in it, a tab's × closes its pane (the last docked pane stays) and its right-click menu floats or closes it; floating panes are native windows (immediate viewports, embedded windows where the backend has one window) with a Dock button; close/float/summon serve the application's Window menu, whose pane items window_menu_items draws (checked while shown; the clicked panel is returned for summon); DockResponse reports tabs, ×, "+" and its entries, Dock buttons and tab menus; the application draws each panel's content; show_with_style accepts an optional DockStyle for the split gap and add/close artwork, while show retains the 5-point gap, text plus and drawn cross
desktopThe desktop remembered per user between runs: the main window's frame (WindowFrame: position, size, maximized, full screen; visible_position brings a frame on a removed display back to the primary one) and the dock layout as text (DockLayout::to_text/parse, unknown panes dropped). The shell's NativeOptions::remember_desktop reads desktop.txt in the app's state_dir, reopens the window there, installs the dock text for DockLayout::remembered, and writes on change at the end of a frame (no button held) and on quit; DesktopMemory runs the same outside the shell (tests)
captureScreenshots of a whole UI without a window: a surfaceless wgpu device (render_state), one frame's input at a size and display scale (Screen, input), the frame drawn offscreen by the same kinogaki-ui-wgpu renderer a window uses (render, Frame::image), write_png; Headless runs an application's frame body (App: frame, busy, wait, named targets), settles on its background-work signal (never a clock), and runs Scripts of input steps (move, click, key, text, wheel, settle, shot)
widgets::menu_barSegmented groups: a run of text items drawn as one field-colored control, rounded only at its outer ends, with hairline dividers between items (segmented_buttons). MenuBar: menu titles as such a group with menu-bar tracking (a click opens; while open, moving onto another title opens it, Left/Right step with wrap-around; Escape or a click elsewhere closes, and that click does nothing else; the titles are registered with Popup::press_through so a click on another title switches menus). Checked rows: a menu closure that begins with menu::check_column(ui) gives every Button after it a check column (menu::check_width: the measured "✓" and Theme::gap(), on the art-pixel grid), so labels line up; Button::checked(true) draws "✓" there in the row's ink, unfilled, announced as a checkbox. The flag lives on that frame's menu Ui (children made after it inherit it; submenus, menu bars and other popups do not); outside a flagged menu a checked button makes room for its own mark only. selected keeps the selection fill and is not for menu choices
widgets::bindingsKey bindings as data: Chord (key + ⌘/Shift/⌥, platform labels), Binding of an application target to a chord in a context, Bindings (lookup from the innermost context of a KeyContext chain outwards, labels, conflicts: clashes and shadows), and capture_chord for a shortcut editor (the pressed chord, Escape cancels; the keys are swallowed)
widgets::list_editorSettings-page list editing: a row's Up / Down / Remove buttons (row_steps) and the Step they ask for (Step::apply), a selectable text cell, a frameless small_icon_button
widgets::markingHeld-key marking menu: family columns split around the pointer, clamped to bounds
widgets::radialRadial marking menu: eight compass pills (icon, label, shortcut) around a dead zone, one vertical list of items below South (no groups or columns; a hovered row opens nothing); pick resolves a flick or press-hold-drag-release from the press point alone, so a gesture works before the menu draws; the layout shifts to stay on screen
widgets::browserTab browser: categories (with muted section headings) expanding into rows under family subheads; items can be hidden while browsing yet searchable; ranked search (exact, prefix, alias, word prefix, substring; recency breaks ties) with the family or unavailable reason on each result; keyboard/wheel navigation, modal dismissal
widgets::shelfAlias-style shelf: tabs (top corners rounded; the active one in the body's color, joined to it with no line; the others recessed) over one compact row of tiles (icon over a label wrapped on words into at most two lines, each tile as wide as its label needs, never ellipsized; label_lines lets applications check their labels); or, with labels, each tile its label alone on one line, as tall as an icon tile and Theme::tile_gap apart (lay it out with labels_height) grouped by labelled dividers; overflow fades and arrows; an optional filter field at the right of the tab strip (the application shows the matching tools); when editable it reports ShelfEdits (+ tile and tab, right-click remove/rename/divider/move, tab rename/reset/delete, drag to reorder or onto another tab) for the application to apply and store
widgets::tableData table for very many rows: the application gives a row count and writes each shown cell's text, so a frame costs the rows on screen; the position is a row number in double precision; header clicks report sorts (with ▲/▼ on the sorted column); wheel and outer-edge scrollbar, sideways scrolling; click, Shift+click and drag select a cell range that ⌘C copies as tab-separated text
widgets::treeTree view (an outliner): the application gives the visible rows (TreeRow, built with TreeRow::new(key, depth, label) and builders: depth, label, icon, branch open or closed, selection, ink, flags lit and inherited, movable, a trailing text such as a count; a label too long for its room ends in "…" before the trailing text gives way) and flag columns (TreeColumn); TreeView::show draws them in a virtualized scroll area and returns TreeEvents: toggle, pick (Shift adds, ⌘ toggles), open (double click), a row drag and its Drop (before, into, after, end), Maya-style flag columns (press toggles; dragging down the column reports every row crossed, then the release), rename and a right-click menu per row. header draws the columns' icons over a strip; TreeResponse::rows carries each row's Response for drops from elsewhere
widgets::consoleA log as rows (Maya's Script Editor history): the application pushes Entrys (level, time, source, path, text, repeat count; a push with the last entry's number replaces it) into ConsoleState, which keeps the latest 10,000 by default, and applies the ConsoleEvents show reports (a path clicked, Clear). A strip of level toggles with their counts (numbers only), the text filter (source, path, text; any case), Wrap and Clear over rows of mark, h:mm:ss, source, path link and text in the level's color with ×N; follows the newest rows unless scrolled up; click, Shift+click or drag select rows, ⌘C or the right-click Copy copies them as lines, ⌘A selects every row shown. The filtered rows are kept as entries arrive, so a frame costs the rows on screen. badge draws a status bar's per-level counts (marks and numbers, nothing at zero) for the application to open its console from
widgets::timelineTime slider (Houdini's playbar, Maya's time slider). The application gives the range, the current time, Units (frames at fps, or clock h:mm:ss; times are seconds), sorted Marks (MarkKind::Layer, Key), whether the range is editable, the playing flag and the speeds offered; Timeline::show draws transport buttons (previous mark, step back, play/stop, step forward, next mark), the current time field, a ruler whose ticks and labels adapt to zoom (trackpad pan, pinch / wheel / ⌘-scroll zoom, middle or Space drag pan, as the graph canvas), the range fields and the speed choice, and returns TimelineEvents: scrub started, moved and ended, set time, stepped (with its target), set range, play, stop, speed. Timeline::show_ruler draws the ruler alone (no transport or fields), for rows aligned to it while the transport lives elsewhere. TimelineState keeps the view and the scrub under way. Playback maps the input clock to the time to show each frame (from the start, so slow frames never drift; frames whole; stops at the end)
widgets::propertiesThe one renderer of property schemas (kinogaki-properties, re-exported): Sheet::new(salt).metrics(..).color_display(&map).show(ui, &mut schema) -> Edits draws sections (STRIP bands) and subsections (RAISED), each collapsible and remembered per key, then every field kind on one grid (104-pt labels, 30-pt rows, 22-pt controls, 12-pt inset; Metrics::compact() for cards and popovers): numbers (drag to scrub, slider-in-field, stepper; typed arithmetic and units committed on Enter or blur, Escape drops), vectors with axis letters and a uniform lock, crop-style fields, colors (swatch through the host's display mapping, default srgb, picker on click, R/G/B(/A) fields), segmented choices, dropdowns with info text, toggles, text, secrets (a fixed run of bullets whatever the length, typed masked, never copied, shown only while the eye is held and never in captures: conceal_secrets), paths (folder, and a picker of nodes it may name instead when the host gives choices), node paths (picker with Relative paths; entries that don't fit head the ones that do, greyed and not choosable), group expressions (pick, error outline), names (menu), info and values, buttons, chips, ramps (plot plus key rows), code (a CodeEditor below the label in the sheet's language, Sheet::code_language); focus, hover, disabled (reason in the tooltip), read-only (Field::read_only: full contrast in an outlined field, no input, no chevron, stepper arrows or path button; ramps and palettes show their strip or swatches only), error and mixed states. A field's value never clips: when it is wider than its field the unit goes first, then the number takes shorter forms (Number::compact_texts: fewer decimals, then 12k); a vector, crop or color row whose fields would be narrower than 56 points beside the label puts them on a line of their own below it; a trailing connection button (linked fill by socket kind), icon or swatch. SettingsBody::draw renders a schema as a settings page (sections as headings, keywords searched). record_rows/recorded_row let tests find drawn rows by label
widgets::code_editorCodeEditor::new(salt, &mut text).language(&lang).diagnostics(&d).show(ui) -> CodeResponse: many lines in the interface face with a line-number gutter; the host's CodeLanguage (kinogaki-properties' code) supplies token colors, completions at the cursor and hover text, so the widget knows no language. Diagnostics with a span draw as pixel zig-zag underlines and gutter marks (ERROR, ACCENT for warnings, MUTED for notes) with the message on hover. Tab/Shift-Tab indent and dedent, Enter keeps indentation and indents after :; completions take Up/Down, Tab/Enter and Escape. Edits are live; started/finished group them into bursts (a 400 ms pause noticed by the next frame or edit, Cmd-Enter, blur), with no timer. Grows to max_height then scrolls, or fills the visible height; a pixel grip along the bottom edge drags the height in whole lines (four up to the visible height; double-click gives it back to the text), which the host keeps (lines(Option<u32>) in, resized/lines out; Kind::Code { lines } and Action::Resize in a sheet)
widgets::breadcrumb/ scene / Part / Mesh 1: every segment but the current one a click target lit in the accent on hover; when the path is wider than its room, leading segments give way to "…", whose menu lists them. Reports the clicked index
widgets::cardFloating parameter card: title, More/Less, Done/×, scrolling body (a compact property sheet)
widgets::settingsMulti-page settings window (about 70% of the screen by default, resizable from its corner grip, the size reported for the application to remember): page list, search across every page (hits grouped under their page), labelled rows, OK/Apply/Cancel/Reset page over an application-owned draft
widgets::graphNode-graph canvas. GraphCanvas draws a network given as data (model: GraphNodes as 136 × 40 tiles with their type's name, an icon, a preview or nothing (TileIcon::Empty; the category accent alone), title, detail, edge flags and top/bottom ports, or as rows nodes with a header and one socket row per port, typed colors and constants; GraphWires; FlagSpecs) and returns GraphEvents: pick (Shift adds, ⌘ toggles), flag clicks, open (double click), a move gesture (started, moved by a network delta, ended), connect, rewire and disconnect (lifting wires optional), background wire click, marquee, cut (Y drag), menu requests and in-place rename. The application applies them to its graph and undo history; the canvas keeps only the view and the gesture under way (GraphState). Its parts serve canvases drawing their own items: CanvasView (pan and zoom, network ↔ screen, frame above a reserved strip, navigate: two-finger pan, pinch / wheel / ⌘-scroll zoom about the pointer, middle or Space drag pan), dots, marquee, rename_field and Wire (vertical or sideways bezier with distance, cut-line and marquee hit tests); picks for what the application keeps between frames (marquee_wires, click_wire, NewPicks to raise what became picked, Moving for each moved node's start) and GraphNode::drop_input (the input a wire dropped on a node's body takes)
widgets::grouped_menuContext-menu submenus per family run

kinogaki-properties

Data-only property schemas, with no UI dependency, so libraries without UI can describe their parameters. Schema<K> (the host's key type K) holds Items: Sections, headings and Fields. A field has a label, a Kind (number with Number: unit, hard range, soft span, speed, decimals and NumberStyle; vector; fields; color; choice; dropdown; toggle; text; path; node path; expression; name; info; value; button; buttons; chips; ramp), a Value, enabled with a reason, tooltip, search keywords, error and mixed flags, an optional Socket, a Trailing slot, and its Animation (static, animated, keyed or overridden: its value field's tint, ANIMATED, KEYED, OVERRIDDEN) with whether the host keys it (keyable). A renderer reports Edits<K>: changed keys (new values are in the schema), gesture start and finish, Actions (press, browse, pick, index, socket, trailing, key: an Alt+click on a keyable field's label, relative, lock) and the typing draft. Schemas hold no host state or callbacks; hosts rebuild them from their data and apply the edits. numeric reads typed numbers: bounded arithmetic, relative forms (*=2), length units with exact conversions, the field's own unit word.

kinogaki-dialogs

One builder (FileDialog::open/open_many/save/directory, filter, starting_directory, file_name, title, detached) and one call, present(done), which returns at once; done runs once with the chosen paths (none when cancelled) or an Error. The application stores the answer and wakes its UI (Studio's editor keeps one dialog open at a time).

  • macos.rs: NSOpenPanel/NSSavePanel on the main thread, begun as a sheet on the key (or main) window, or modeless when detached; AppKit calls the completion block on the main thread. objc2's bindings are safe here: no unsafe code.
  • windows.rs: IFileOpenDialog/IFileSaveDialog on a thread of its own (a COM single-threaded apartment) owned by the active window, through raw vtables and system functions declared in the file. The crate denies unsafe code (instead of the workspace's forbid) and allows it only in this module; every block carries its safety argument. Written but not yet compiled or run on Windows.
  • unsupported.rs: Linux and the rest answer Error::Unsupported until an XDG portal backend is written.

Style tokens

Every value shared by more than one widget or application is a Theme constant in crates/ui/src/theme.rs. Use the token, never its number: the editor's reference captures check that the look stays pixel-identical, and a token change is a deliberate visual change. Geometry that belongs to one widget (a marking column's width, the browser's 190-point category column, shelf padding) stays a named constant in that widget. Sizes are in points, and one point is one art pixel (see Pixel art below). The user's element size (set_element_size, Settings ▸ Interface in the editor) scales rows, buttons and fields: icon_button(), interact_size() and the row heights, their defaults times 85–125% (ELEMENT_SIZES, 5% steps) in whole art pixels. It is per thread (the UI thread's), read from Theme by every widget. Type and icons keep their pixels.

Colors (sRGB). The neutral ramp goes from dark to light; the accent marks selection and the active item. Ink on a fill comes from Theme::ink_on(fill) (dim_ink_on for disabled or secondary ink): black on a light fill (the accent, ACTIVE, any color black contrasts with better than TEXT, by WCAG contrast), TEXT on a dark one. Every widget's text and icons on a fill ask it, so they never disagree; accent-colored text on a dark background is not ink on a fill and keeps its color.

TokenValueUsed for
BACKDROPgrey 26panel and canvas background, title rows
STRIPgrey 38pane tab strips, shelf tab strip, ramp field
FIELDgrey 41text fields, resting buttons
RAISEDgrey 46floating windows, shelf body, creation hint
SURFACEgrey 50popups, browser, tool card, tiles, pills, inspector
BORDERgrey 58hairlines, hovered fills, group separators
PRESSEDgrey 72pressed buttons
HOVER_BORDERgrey 110outline of the pane under the pointer, hovered property fields
DISABLED_INKgrey 110disabled property values and their field outline
MUTEDgrey 140headings, hints, unavailable items
TEXTgrey 217text and icons
ACCENT231, 160, 63selection, active tool/tab/row, value pills, guides
ERROR222, 72, 62errors, invalid typed values
TEMPLATE168, 118, 240template objects in the viewport, the outliner's Template flag
ACTIVE104, 196, 84the one of several alternatives in effect (an active Render Output): its flag, its node
ANIMATED46, 66, 42an animated property's value field (it has a channel)
KEYED62, 98, 50an animated property's value field at one of its keys
OVERRIDDEN70, 52, 96a property's value field set from elsewhere (a subnet parameter driving it)
AXES201,99,83 · 137,176,105 · 101,151,194X, Y, Z

Corner radii (points).

TokenValueUsed for
ROW_RADIUS2browser categories and result rows
CHIP_RADIUS3hover-tool, stroke and creation hints; ramp field; Mesh Check rows
NODE_RADIUS3.0network node bodies
TILE_RADIUS4icon tiles, shelf tabs, marking rows, value pills, active pane tab
CONTROL_RADIUS5buttons, fields, popup windows
PANE_RADIUS6docked panes, floating tool card

Spacing and sizes (points).

TokenValueUsed for
gap()2inset around and between toolbar buttons; default item spacing
tile_gap()2between shelf tiles and shelf tabs
icon_button()28 × elementsquare icon tiles (toolbar, shelf): 3 around the icon
icon_size()22an 11-pixel icon at pixel::ICON_SCALE; also compact icon buttons in property rows
BUTTON_PADDING7 × 3text buttons
interact_size()24 × 22 × elementminimum interactive widget
SLIDER_WIDTH120sliders
tab_height()24 × elementpane tab strips, browser title row
card_row()26 × elementtool card rows (card::ROW_HEIGHT)
menu_row()26 × elementmarking-menu rows
list_row()28 × elementbrowser categories and results
tree_row()24 × elementtree rows (an outliner, a library)
PROPERTY_LABEL104property sheets' label column (inset gap() either side)
property_row()24 × elementproperty rows (22-pt controls inside)
section_row()24 × elementproperty section bands (STRIP)
subsection_row()20 × elementproperty subsection bands (RAISED)
top_bar(), status_bar()26, 25 × elementan application's top bar and status bar
SECTION_TRACKING0.66letter spacing of section labels (0.06 em)
CONNECTION_SIZE12material parameters' round connection button
TOGGLE_SIZE, TOGGLE_THUMB28 × 16, 10property toggles
SHELF_TILE64 × 54widest shelf tile (24 pt icon over a two-line label); tiles narrow to their label, down to 40
SHELF_GAP2between shelf tiles
SHELF_DIVIDER9extra space before a shelf divider

Type. One face, MEK Sans, drawn on its 15-pixel em (pixel::FONT_PIXELS), with the platform's monospace face (Menlo on macOS, Consolas on Windows, DejaVu Sans Mono on Linux; the bundled fonts in crates/ui/fonts/ after it) for the glyphs it lacks.

TokenValueUsed for
text_size(), Theme::font()15all UI text, every text style; canvases zoom it by whole ems

Pixel art

The whole interface is drawn on a grid of square art pixels, one point each (2 × 2 physical pixels on a Retina-class display). Layout and input are in points. The context draws at one texel per art pixel (hosts report every display scale through pixel::pixels_per_point, which keeps it and answers 1; input conversion keeps the display's), and the shell's painter and the capture harness scale the frame up with nearest-neighbour sampling (kinogaki_ui_wgpu::pixel::Upscale::compose, which the capture harness's offscreen frames use too). Application content stays at the display's resolution: meshes textured with a user texture (a 3D view's render, an image) and paint callbacks are drawn straight onto the target at its scale, in paint order between the scaled-up interface layers (what is drawn over them, such as viewport overlays, is blended over as pixel art); applications size that content with pixel::content_pixels_per_point. Text is drawn at whole multiples of the pixel face's em (pixel::font_size), whatever size a widget asks for, with the pen on whole pixels and no line gap. The tessellator draws without feathering, with square corners (rounded rectangles leave their corner pixels out) and strokes at least one art pixel wide; glyphs and icons are rasterized with coverage on or off. pixel::snap, pixel::line_width and pixel::outline put a widget's own drawing on the grid. Letter spacing snaps each gap to whole art pixels before accumulating positions, including the gap before a truncation ellipsis, so fractional tracking cannot alternate between different gaps in uppercase section headings.

Widget contract

Widgets are immediate-mode functions over MenuItem { label, icon, family, unavailable, accent }; an empty icon shows the label alone (menus, the browser, marking menus), moved to where the icon was. Consecutive items with the same family form a group. An item with an unavailable reason is drawn muted, explains itself on hover and is never reported as chosen. Each widget returns the chosen index and the rectangles it drew, which applications use for pointer routing and interaction tests. Widgets hold no application state; persistent navigation (the browser's query, category and cursor) lives in a small state value owned by the caller. Proportions, radii and colors come from Theme, so applications look alike without restating them.

Every typed field (a number, a line of text, a rename) ends its edit one way, Response::edit_ended: Enter, Tab or a click elsewhere applies what was typed, and Escape drops it. Nothing applies while typing, so typing 1000 never applies 1, 10 and 100 on the way. Numbers read typed text with numeric::evaluate (arithmetic and units); a field with units of its own, such as a timeline's frames, gives its parser. A rename applies its name trimmed, and one left blank keeps the old name. The Escape that ends typing is that edit's alone: it is gone from the input (Memory::escaped says whose it was), so nothing under the field acts on it, though a dialog, the browser or the settings window whose own field it was closes on it. Search and filter fields apply as they are typed, and show no hint text.

Tests

crates/ui/tests drive widgets headlessly through kinogaki_ui::Context::run_ui with real input events: docking identity, numeric parsing and editing, icon layout, marking menu choice and bounds, browser navigation/search/dismissal, shelf tabs and tiles, card controls, and the consumed dismissing click of the menu bar and combo popups. Studio additionally renders these widgets through its offscreen GPU harness and compares captures pixel for pixel when they change.

History: from egui 0.36.2 to Kinogaki UI (libs/lib-ui/docs/HISTORY.md)

Kinogaki UI started as the egui 0.36.2 stack (commit 49682f8, MIT OR Apache-2.0; see THIRD_PARTY_NOTICES.md), vendored first as a fork that tracked upstream with numbered patches. On 2026-10-18 it was merged into lib-ui's own crates and stopped tracking upstream: the code is ours, changed in place like the rest of lib-ui.