Stability: Stable — UI values, reactivity, and the functions the UI forms desugar to.
std::ui is the UI toolkit of the game system: the element type, reactive signals, content and
layout functions, and the targets of the UI forms (column![…], button!{…}, text!(…) and so
on). std::ui::style declares the looks of controls.
UI is fn code. A component is a fn that returns an Element, wrapped in its
mode type (RootPage, Overlay, GameOverlay, Dialog), or a plain
Element for the game view. Styles are const fn data.
The prelude imports signal and Element. Import other items by path, for example
use std::ui::{Fit, each, show, thumbnail};.
pub mod style;Overview
| Item | Kind | Summary |
|---|---|---|
Element, Placed | builtin structs | A UI element; an element placed in a layer |
Align, Fit | enums | Flex alignment; how an image fills its box |
signal, effect, each, show | fn | Reactivity |
label, spacer, scroll, thumbnail, message | fn | Content |
Element methods | fn | Size, spacing, alignment, paint, focus order, placement |
column, row, layer | fn | Targets of the layout forms |
text, image, game_screen | fn | Targets of the content forms |
button, slider, toggle, select, option | fn | Targets of the control forms |
Elements
Struct: Element
#[builtin] pub struct Element;A UI element tree, laid out by the engine. An Element is an opaque value: build one with the
functions and forms of this module, then refine it with the layout methods.
Struct: Placed
#[builtin] pub struct Placed;An element placed in a layer![…]. Make one with Element::at or
Element::cover.
Enum: Align
pub enum Align { Start, Center, End, Stretch, SpaceBetween,}Flex alignment of the children of a row or column, used by Element::align.
| Variant | Along the row or column (main) | Across it (cross) |
|---|---|---|
Start | Packed at the start | At the start |
Center | Centred | Centred |
End | Packed at the end | At the end |
Stretch | Same as Start | Children without a fixed size are stretched to the full cross size |
SpaceBetween | The free space is shared equally between the children (with one child, same as Start) | Same as Start |
Enum: Fit
pub enum Fit { Stretch, Contain, Cover, Natural,}How an image fills its box, used by image!:
| Variant | Meaning |
|---|---|
Stretch | Fills the box, ignoring the aspect ratio |
Contain | The largest size that fits inside the box, keeping the aspect ratio, centred in the box |
Cover | Fills the box, keeping the aspect ratio; the image is cropped equally on both sides of the overflowing axis |
Natural | The image’s own pixel size, at the top-left corner of the box |
Components and reactivity
Components and reactivity follow SolidJS:
- A component runs once, when its mode is shown.
- Reactive sources are signal getters,
Setting::get,Meta::get, the reads of theNarrativehandle,saves::infoand the seen sets. - A reactive closure re-runs when a source it read changes. The reactive attributes and the
elements of the UI forms are such closures (see UI forms), so
text!(t!["Music: " {music().level}])updates whenevermusicchanges, while the rest of the component does not run again. - Handlers such as
on_clickare ordinary closures; they run when the event happens.
Signals exist only in fn code: the story and const fn cannot use them.
Function: signal(init)
#[builtin] pub fn signal<T>(init: T) -> (Fn() -> T, Fn(T));initT— the initial value.- Returns:
(Fn() -> T, Fn(T))— a getter and a setter. The getter is a reactive source. - Kind:
fn· Builtin: yes
let (hidden, set_hidden) = signal(false);Function: effect(run)
#[builtin] pub fn effect(run: Fn());runFn()— the closure to run.- Kind:
fn· Builtin: yes
Runs run now, and again whenever a source it read changes.
Function: each(items, item)
#[builtin] pub fn each<T>(items: Fn() -> Vec<T>, item: Fn(T) -> Element) -> Element;itemsFn() -> Vec<T>— the items; reactive.itemFn(T) -> Element— builds the element of one item.- Returns:
Element— one element per item, kept in sync withitems. - Kind:
fn· Builtin: yes
scroll(|| each(|| game.history(), |entry| entry_view(entry)))Function: show(when, then)
#[builtin] pub fn show(when: Fn() -> bool, then: Fn() -> Element) -> Element;whenFn() -> bool— the condition; reactive.thenFn() -> Element— builds the content.- Returns:
Element—then()whilewhen()istrue, else nothing. - Kind:
fn· Builtin: yes
Content
Function: label(text)
#[builtin] pub fn label(text: Text) -> Element;textText— the text.- Returns:
Element— static text. - Kind:
fn· Builtin: yes
The text uses System::text, or the current look’s text style inside a
button or toggle (see Interaction looks). text!(…) is the
reactive form with an optional style. Inside column!, row!, button! and toggle!, a bare
t![…] element becomes label(t![…]).
Function: spacer()
#[builtin] pub fn spacer() -> Element;- Returns:
Element— an empty element that grows, pushing its siblings apart. - Kind:
fn· Builtin: yes
Function: scroll(child)
#[builtin] pub fn scroll(child: Fn() -> Element) -> Element;childFn() -> Element— the content.- Returns:
Element— a scrollable box around the content. - Kind:
fn· Builtin: yes
Function: thumbnail(slot)
#[builtin] pub fn thumbnail(slot: SaveSlot) -> Element;slotSaveSlot— the slot.- Returns:
Element— the screenshot stored with the save inslot. - Kind:
fn· Builtin: yes
Function: message(game, style)
#[builtin] pub fn message(game: Narrative, style: TextStyle) -> Element;gameNarrative— the running game.styleTextStyle— the text style of the window (seestd::narrative::text).- Returns:
Element— the current line with its reveal, text effects, pauses and marks, and the click-wait glyph. - Kind:
fn· Builtin: yes
message is the body of the message window. It only exists where a Narrative handle exists:
in the game view and in game overlays.
Stability: Planned — NVL mode, a full-screen text mode where several lines stay on the screen until a page break, is not designed yet. It will change what the text window shows and how the backlog and
Narrative::linedescribe a step.
Layout
impl Element { #[builtin] pub fn width(self, px: u32) -> Element; #[builtin] pub fn height(self, px: u32) -> Element; #[builtin] pub fn padding(self, insets: Insets) -> Element; #[builtin] pub fn gap(self, px: u32) -> Element; #[builtin] pub fn align(self, main: Align, cross: Align) -> Element; #[builtin] pub fn grow(self, factor: u32) -> Element; #[builtin] pub fn background(self, paint: Paint) -> Element; #[builtin] pub fn opacity(self, opacity: Decimal) -> Element; #[builtin] pub fn focus_order(self, order: u32) -> Element; #[builtin] pub fn at(self, position: Point, anchor: Anchor) -> Placed; #[builtin] pub fn cover(self) -> Placed;}Layout is flexbox-like:
column![…]androw![…]are flex containers;gapandalignset the spacing and alignment of their children, andgrowis a child’s flex grow factor.layer![…]stacksPlacedchildren:atplaces a child’s anchor at a point, andcoverfills the layer.- Sizes and positions are pixels of the design resolution
(
System::resolution). - Layout is an engine pass over the element tree. It is recomputed on resize and is never saved.
All methods take the element by value and return the refined element, so they chain:
column![…].gap(16).align(Align::Center, Align::Center). Each is a builtin fn.
Element::width(px)
#[builtin] pub fn width(self, px: u32) -> Element;pxu32— a fixed width in pixels.
Element::height(px)
#[builtin] pub fn height(self, px: u32) -> Element;pxu32— a fixed height in pixels.
Element::padding(insets)
#[builtin] pub fn padding(self, insets: Insets) -> Element;insetsInsets— the inner spacing; build it withstd::screen::insets(top, right, bottom, left)(seestd::screen).
Element::gap(px)
#[builtin] pub fn gap(self, px: u32) -> Element;pxu32— the spacing between the children of a row or column.
Element::align(main, cross)
#[builtin] pub fn align(self, main: Align, cross: Align) -> Element;mainAlign— the alignment of the children along the row or column.crossAlign— the alignment across it.
Element::grow(factor)
#[builtin] pub fn grow(self, factor: u32) -> Element;factoru32— the flex grow factor: how much of the free space of its row or column the element takes.
Element::background(paint)
#[builtin] pub fn background(self, paint: Paint) -> Element;paintPaint— what is drawn behind the element.
Element::opacity(opacity)
#[builtin] pub fn opacity(self, opacity: Decimal) -> Element;opacityDecimal— the element’s opacity,0.0..=1.0.
Element::focus_order(order)
#[builtin] pub fn focus_order(self, order: u32) -> Element;orderu32— the element’s position in the focus order (see Focus).
Element::at(position, anchor)
#[builtin] pub fn at(self, position: Point, anchor: Anchor) -> Placed;positionPoint— a point of the layer.anchorAnchor— the point of the element placed there, such asTOP_LEFTorCENTER(seestd::screen).- Returns:
Placed— for alayer![…].
Element::cover()
#[builtin] pub fn cover(self) -> Placed;- Returns:
Placed— the element filling the whole layer.
use std::screen::CENTER;use std::ui::Fit;
fn Title() -> RootPage { RootPage(layer![ image!(fit = Fit::Cover, TITLE_BG).cover(), column![ button!{on_click = || nav::start(story::Main, (), |_end| nav::goto(Title)), t!["Start"]}, button!{on_click = || nav::call(Config), t!["Config"]}, button!{on_click = || nav::quit(), t!["Quit"]}, ].gap(16).at(point(640, 520), CENTER), ])}Focus
- The focusable elements are buttons, sliders, toggles and selects.
- Focus follows document order unless
focus_orderis set. - The first focusable element is focused when a mode opens. Focus is shown from the first focus
key on, and Enter and
PadButton::Southactivate the focused control only while focus is shown. - Up, Down, Left and Right, the D-pad, Tab and Shift+Tab move focus. On a focused slider or
select, Left and Right change the value instead: a slider by its
step, a select to the previous or next option. - These keys are built in and not bindable (see Bindings).
UI forms
The UI forms are written in .kix files and desugar to calls of the functions below with a props
struct. In fn and const fn code they build values; they are never steps. They follow the
language’s delimiter rules (see the Reference chapter
Forms):
name![…]takes elements only;name!(…)takes attributes and a fixed number of elements;name!{…}takes attributes and any number of elements.
Desugaring
- Reactive attributes become
key: Some(|| expr), orkey: || exprwhen the attribute is required. - Handler attributes (
on_*) becomekey: Some(expr). - Absent optional attributes become
None. A missing required attribute, an unknown attribute, or the wrong number of elements (text!andimage!take exactly one,game_screen!andslider!none) is a syntax error. - Elements. Each element
ebecomes|| e. Insidecolumn!,row!,button!andtoggle!, an element that is syntacticallyt![…]becomes|| ui::label(t![…]).
| Form | Desugars to | Attributes | Elements |
|---|---|---|---|
column![…], row![…] | ui::column([…]), ui::row([…]) | none | Element |
layer![…] | ui::layer([…]) | none | Placed |
text!(…) | ui::text(TextProps { … }) | style: reactive TextStyle | exactly one Text → content |
image!(…) | ui::image(ImageProps { … }) | fit: reactive Fit | exactly one ImageAsset → source |
game_screen!(…) | ui::game_screen(GameScreenProps { … }) | game: reactive Narrative, required | none |
button!{…} | ui::button(ButtonProps { … }) | on_click: handler Fn(); disabled: reactive bool; style: reactive ButtonStyle | Element → children |
slider!{…} | ui::slider(SliderProps { … }) | value: reactive Int<LO, HI>, required; on_change: handler Fn(Int<LO, HI>); step: reactive u32; disabled: reactive bool; style: reactive SliderStyle | none |
toggle!{…} | ui::toggle(ToggleProps { … }) | value: reactive bool, required; on_change: handler Fn(bool); disabled: reactive bool; style: reactive ButtonStyle | Element → children |
select!{…} | ui::select(SelectProps { … }) | value: reactive T, required; on_change: handler Fn(T); disabled: reactive bool; style: reactive ButtonStyle | ui::option(v, label) → options |
For example,
button!{on_click = || nav::back(), t!["Back"]}desugars to
ui::button(ButtonProps { on_click: Some(|| nav::back()), disabled: None, style: None, children: [|| ui::label(t!["Back"])],})and
slider!{value = music().level, on_change = |v| set_level(v)}to
ui::slider(SliderProps { value: || music().level, on_change: Some(|v| set_level(v)), step: None, disabled: None, style: None,})Function: column(children)
#[builtin] pub fn column<const N: usize>(children: [Fn() -> Element; N]) -> Element;What column![…] desugars to: the children stacked top to bottom.
Function: row(children)
#[builtin] pub fn row<const N: usize>(children: [Fn() -> Element; N]) -> Element;What row![…] desugars to: the children side by side, left to right.
Function: layer(children)
#[builtin] pub fn layer<const N: usize>(children: [Fn() -> Placed; N]) -> Element;What layer![…] desugars to: the Placed children stacked, later children on top.
Struct: TextProps
pub struct TextProps { pub style: Option<Fn() -> TextStyle>, pub content: Fn() -> Text,}The attributes and content of text!(…).
Function: text(props)
#[builtin] pub fn text(props: TextProps) -> Element;What text!(…) desugars to: reactive text. Without style, it uses
System::text, or the current look’s text style inside a button or toggle.
text!(style = NAME_STYLE, who_text(game))Struct: ImageProps
pub struct ImageProps { pub fit: Option<Fn() -> Fit>, pub source: Fn() -> ImageAsset,}The attributes and source of image!(…).
Function: image(props)
#[builtin] pub fn image(props: ImageProps) -> Element;What image!(…) desugars to: an image. The source is reactive, so
image!(if seen_image(cg) { cg } else { LOCKED }) switches as soon as the condition changes.
Struct: GameScreenProps
pub struct GameScreenProps { pub game: Fn() -> Narrative,}The attributes of game_screen!(…).
Function: game_screen(props)
#[builtin] pub fn game_screen(props: GameScreenProps) -> Element;What game_screen!(…) desugars to: the screen of the running game, as drawn by the story’s
renders. The game view usually puts it first in a layer![…], with .cover().
use std::narrative::text::text_style;use std::screen::BOTTOM_CENTER;use std::ui::message;
fn GameView(game: Narrative) -> Element { layer![ game_screen!(game = game).cover(), message(game, text_style(26, WHITE)).width(1000).at(point(640, 690), BOTTOM_CENTER), ]}Struct: ButtonProps
pub struct ButtonProps<const N: usize> { pub on_click: Option<Fn()>, pub disabled: Option<Fn() -> bool>, pub style: Option<Fn() -> ButtonStyle>, pub children: [Fn() -> Element; N],}The attributes and children of button!{…}.
Function: button(props)
#[builtin] pub fn button<const N: usize>(props: ButtonProps<N>) -> Element;What button!{…} desugars to: a focusable button. on_click runs on click or activation; a
disabled button ignores input and shows its disabled look (see
Interaction looks). A button paints its current look’s
background over its whole box and lays its children out centred on top, so a button without
children is drawn by the background alone, which makes image buttons:
button!{style = image_style(START, START_HOVER), on_click = || start_game()}.width(300).height(70)Struct: SliderProps
pub struct SliderProps<const LO: i64, const HI: i64> { pub value: Fn() -> Int<LO, HI>, pub on_change: Option<Fn(Int<LO, HI>)>, pub step: Option<Fn() -> u32>, pub disabled: Option<Fn() -> bool>, pub style: Option<Fn() -> SliderStyle>,}The attributes of slider!{…}. The range is the range of the value’s Int type: there are no
min or max attributes.
Function: slider(props)
#[builtin] pub fn slider<const LO: i64, const HI: i64>(props: SliderProps<LO, HI>) -> Element;What slider!{…} desugars to: a focusable slider from LO to HI, moving in steps of step.
Volume::level is an Int<0, 100>, so a volume slider needs no range:
slider!{ value = MUSIC.get().level, on_change = |v| MUSIC.set(Volume { level: v, muted: MUSIC.get().muted }),}Struct: ToggleProps
pub struct ToggleProps<const N: usize> { pub value: Fn() -> bool, pub on_change: Option<Fn(bool)>, pub disabled: Option<Fn() -> bool>, pub style: Option<Fn() -> ButtonStyle>, pub children: [Fn() -> Element; N],}The attributes and children of toggle!{…}.
Function: toggle(props)
#[builtin] pub fn toggle<const N: usize>(props: ToggleProps<N>) -> Element;What toggle!{…} desugars to: a focusable on/off control. Activating it (click, Enter or
PadButton::South) calls on_change with !value(). While the value is true, the toggle shows
its on look (see Interaction looks).
toggle!{value = SKIP_UNREAD.get(), on_change = |v| SKIP_UNREAD.set(v), t!["Skip unread text"]}Struct: SelectOption<T>
pub struct SelectOption<T> { pub value: T, pub label: Element,}An option of a select!{…}.
Function: option(value, label)
pub fn option<T>(value: T, label: Element) -> SelectOption<T> { SelectOption { value, label }}valueT— the value the option stands for.labelElement— what the option shows.- Returns:
SelectOption<T>— the optionvalue, shown aslabel. - Kind:
fn· Builtin: no
Struct: SelectProps<T, N>
pub struct SelectProps<T, const N: usize> { pub value: Fn() -> T, pub on_change: Option<Fn(T)>, pub disabled: Option<Fn() -> bool>, pub style: Option<Fn() -> ButtonStyle>, pub options: [Fn() -> SelectOption<T>; N],}The attributes and options of select!{…}.
Function: select(props)
#[builtin] pub fn select<T: Eq, const N: usize>(props: SelectProps<T, N>) -> Element;What select!{…} desugars to: a focusable control that cycles through its options. It shows the
label of the option whose value equals value(), so T must be Eq. Activating it calls
on_change with the value of the next option, wrapping around after the last one; with focus,
Right and Left step to the next and the previous option. When no option equals value(), the
next value is the first option’s.
use std::ui::{label, option};
select!{ value = WINDOW_OPACITY.get(), on_change = |v| WINDOW_OPACITY.set(v), option(50, label("50%")), option(80, label("80%")), option(100, label("100%")),}Stability: Planned — fades and transitions of UI elements are not supported yet; only a control’s look changes animate (see
ButtonStyle::change).
Last updated Oct 29, 2026