CalmUI
Components

Scrolling and text entry

Reusable form controls and drag/drop in the shared game UI runtime.

Open ControlsShowcase in the GameUI project to try a text field, dropdown, checkbox, switch, tabs and a scrollable list. DragDropShowcase contains two panels with transferable items. These are source components, not Studio widgets.

The shared GoldenPanel keeps bright input surfaces distinct from a warmer primary-200/300/400 gradient face. Its inherited text uses primary-900; the existing gold frame, typography, padding and control dimensions remain shared.

Reuse a control

A focusable scroll region (tabindex="0") supports Up/Down navigation in the shared runtime. While there is more content in that direction, it scrolls by 40 logical pixels and keeps focus. At an endpoint, normal directional focus navigation resumes. Tab and Left/Right can leave the region without scrolling to the end. Explicit directional links take precedence. Editing fields, range controls and active item pickups keep their existing navigation behavior. Busy/inert content and regions below a modal barrier do not scroll.

In Unreal, drag a scrollbar thumb with the primary mouse button to move through its content. The drag remains owned by that View when the pointer leaves the thumb or crosses another panel. Releasing the button, pressing Back, or hiding the View releases the drag. Horizontal pointer movement and button release do not trigger another scroll layout.

GameUI groups components under components/buttons, components/forms, components/layout, components/feedback and components/tooltips. For example, import TextInput from ./components/forms/TextInput.calmui in a root-level View. Component names and props remain unchanged. An open Dropdown elevates its entire positioned group. Its list is anchored below the toggle's border box with a small gap, or above it when space below is insufficient. Popup painting and hit testing use the shared CSS paint order. Switch uses a primary-500 track when off and success-600 when on, with darker state borders and a cream thumb. Position and color both distinguish its state; the surrounding panel does not change color. Studio's Components section displays these physical subfolders as collapsible groups, using source paths without requiring saved virtual-folder metadata.

Import the component from your project and supply its value. Handle its local event in the parent View. For example, after importing GameUI's TextInput:

<TextInput elementId="island-name"
  label="$loc('GameUI::Settings.IslandName', 'Nazwa wyspy')"
  placeholder="$loc('GameUI::Settings.IslandNamePlaceholder', 'Wpisz nazwę…')"
  :value="name" :disabled="false" :readOnly="false"
  @input="setName" />

The parent script returns name and setName(value: string). Input is a local event; it does not write gameplay state or call a Blueprint automatically. @change reports confirmation with Enter. The shared editor owns Unicode text, selection, insertion, deletion and Undo/Redo. Studio and Unreal deliver host keyboard and clipboard input. Text, selection and caret are painted in the game canvas. Readonly fields allow selection but reject edits.

Input icons may occupy the field's padding: overflow:hidden clips descendants at the padding edge, not the text content edge. The editable text is clipped separately to its reserved content area. Anonymous layout wrappers do not paint additional backgrounds over placeholder or entered text.

Multi-line text

Add data-calmui-multiline="true" to an element that already has data-calmui-editable="true" to edit several lines in one box:

<button role="textbox" data-calmui-editable="true" data-calmui-multiline="true"
  :data-value="message" class="message-box" @input="setMessage" @change="commit">&#160;</button>
.message-box { height: 124px; padding: 12px 16px; line-height: 24px; }

The text wraps at the width of the content box, both while the field is idle and while you type. Lines are line-height apart. Long words without spaces break between characters. Give the element a height (here four 24px lines plus padding and border): the box does not grow with its text, it scrolls vertically to keep the caret visible and shows the first line again when it loses focus.

Up and Down move the caret to the previous or next visual line and keep its column across shorter lines; on the first or last line they go to the start or end of the text. Home and End go to the start and end of the visual line; Ctrl+Home and Ctrl+End to the start and end of the text. A click places the caret on the line and column under the pointer, and dragging selects across lines.

Enter confirms the text with change, as in a single-line field, and typed or pasted line breaks become spaces, so the value stays one paragraph. Add data-calmui-enter="newline" when line breaks belong to the value: Enter and Shift+Enter then insert a line break, Ctrl+Enter confirms, and pasted text keeps its lines. The input, change and blur events are the same as for a single-line field.

Placeholder and optional icons

GameUI's TextInput accepts leadingIcon and trailingIcon, using names from the built-in icon catalog. Either side can be omitted; plain fields retain their normal padding. By default icons are decorative: clicking them focuses the field rather than triggering an independent action. Text, selection and caret stay inside the space between the icons.

<TextInput elementId="item-search"
  label="$loc('GameUI::Search.Label', 'Wyszukiwanie')"
  placeholder="$loc('GameUI::Search.Placeholder', 'Wpisz nazwę przedmiotu…')"
  leadingIcon="search" trailingIcon="sliders-horizontal"
  :value="search" :disabled="false" :readOnly="false" @input="setSearch" />

Start search as an empty string to show the muted placeholder. Typing hides it; clearing the field restores it. The placeholder is a hint, never the field's value. ControlsShowcase includes both a plain populated field and this empty icon field.

Validation and a trailing action

In Studio, open a View using TextInput. Supply description for help text or error for a validation message. Both size from their content outside the field. Provide a unique elementId per control for label/help/error associations, and use localized text for those props just as for label.

Add trailingActionLabel and handle @action to make the trailing icon a separate focusable button. If no trailingIcon is supplied, it uses x. The parent implements clear/search/etc.; pressing the icon does not change the field automatically. Readonly and disabled also disable the action. Without the action label, the icon stays decorative.

FormField supplies label, description, error, elementId and a default slot for custom controls. SettingRow places label and description beside its default control slot, wrapping at narrow widths. Their disabled prop only styles surrounding text: also disable the child control.

Numeric drafts and quantity selection

Open the files under Components → Forms in the GameUI project. Import NumberInput for stepped decimal values or QuantitySelector for an integer quantity with decrease/increase buttons. After importing the latter:

<QuantitySelector elementId="transfer-quantity"
  label="$loc('GameUI::Inventory.Quantity', 'Ilość')"
  :value="quantity" :min="1" :max="available" @change="setQuantity" />

The parent provides numeric quantity and available, and a setQuantity(next: number) handler that updates the parent value. Selecting a quantity does not transfer items or write the C++ Store automatically.

EventPayload and behavior
inputDraft string, including a blank or temporary minus sign.
changeNormalized number on Enter, focus loss or a step-button action.
invalidDraft string when confirmation cannot parse a finite number.
cancelNormalized parent value; Escape/Back restores it and clears the parse error.

Invalid drafts remain editable instead of becoming zero. Escape first cancels active input-method composition; another Escape cancels the draft. Parent changes to value, bounds, step or editability replace obsolete drafts. Handle change by updating the parent value to accept the selected number.

NumberInput accepts value, min, max, step, showSteppers and decimalSeparator, plus the label/help/error and disabled/readonly props. Defaults are min 0, max Number.MAX_SAFE_INTEGER, step 1 and no steppers. Set decimalSeparator="," to display decimal commas. Editing accepts a dot or comma, but not grouping separators or exponent notation. Values clamp and snap while preserving both endpoints. Invalid ranges disable editing; invalid steps fall back to 1.

QuantitySelector uses integer step 1, rounds its minimum up and maximum down, and defaults the minimum to 1. Supply the available maximum: without it the range is closed and the control is disabled. The invalid-number message and step-button labels use GameUI localization keys.

Tabs and dropdown options use stable ui:key values. Each option dispatches its own event payload, including after reordering. A panel hidden with ui:show does not receive pointer or keyboard focus through its descendants. Tabs use rounded surfaces for both selected and unselected items, without separator lines. Selection uses a warmer gold gradient; hover gently lifts the tab. ControlsShowcase places this wrapping row at the top of its panel. GameUI controls provide keyboard and controller focus styles. Keep box-sizing: border-box on full-width controls so padding and borders remain inside the available width.

Transparent controls support state-driven backgrounds and outlines even when their normal state draws only child content. Checkbox and switch provide hover and pressed feedback; focus and controller focus outline the entire control. Moving focus removes the outline from the previous control. Color-only background shorthand works in these state rules, as does background-color; rounded highlights follow the control's border-radius.

IME composition has a separate host adapter. Platform IME and virtual-keyboard acceptance must be checked on the target platform; a successful package test is not a claim that every operating-system input method has been validated.

Range sliders and progress bars

GameUI ScrollArea explicitly sets color to --color-primary-700, which also colors the native scrollbar thumb. Its trailing padding keeps list content clear of the scrollbar against the light panel surface.

Open ProgressShowcase in GameUI for animated HP and XP bars. It starts at 50/100 HP and 500/1000 XP with automatic value changes off. The liquid moves immediately even when values stay fixed. Use Uruchom pokaz to enable automatic value changes or use the HP/XP buttons. Zatrzymaj pokaz stops only automatic value changes, not the liquid. This is local demonstration state, not gameplay state. ProgressBar reuses the project's panel palette and typography. Its parent supplies label, numeric value, numeric max, and variant (hp or xp). The component owns its fill geometry; do not pass fillStyle. max defaults to 100; invalid or non-positive maxima produce an empty bar. Values are clamped to the range, with missing or non-finite values treated as zero. Optional valueText overrides the default value / max label without changing geometry. A rounded window reveals the translated fill without stretching its circular ends. CSS owns the 650 ms value transition. Two translucent, multi-stop linear gradients overlap across the fill. Smaller color bands soften the motion; XP stays light blue without a dark gray stop. They move back and forth with gentle easing over 5 and 7.5 seconds, starting in opposite directions. The layers stay clipped inside the fill and do not change its value, layout or hit testing. HP uses success colors and XP uses secondary colors from the project tokens. Studio Timeline Pause and Fixed time still intentionally stop all animation; choose Play to resume.

ControlsShowcase includes Głośność, a horizontal RangeSlider. Drag or click the track, use Left/Right for one step, Home/End for the endpoints, or Page Up/Page Down for ten steps. Controller Left/Right changes the value while Up/Down moves to another control. The parent receives a numeric input value; change confirms a pointer release or keyboard step. Nothing changes game audio until you connect that value yourself.

The component takes min, max, step, value, label, elementId and disabled, optional unit and optional valueText. It owns fill and thumb geometry: do not pass fillStyle or thumbStyle. Defaults are 0–100 with step 1. Values are clamped and snapped; both endpoints remain reachable when the span is not a multiple of step. A non-positive/non-finite step falls back to 1; an invalid range disables the control. Use unit="%" for percentages; the default unit is empty. valueText overrides the displayed/accessibility label, while emitted input and change values remain numeric. Update the parent's value from these events to keep this controlled component synchronized.

Pass numbers as expressions (:value="50", :max="100"), not plain string attributes (value="50"). Studio mock data must use JSON numbers too.

For custom components, mark a focusable element with data-calmui-range="true", role="slider", aria-valuemin, aria-valuemax, aria-valuenow and data-step. The runtime clamps and snaps pointer values to the step. Invalid numeric ranges are non-interactive. disabled and readonly prevent edits.

Scrolling

Use overflow: auto or overflow: scroll on a constrained container. Its content keeps its normal layout; a column with flex-wrap: nowrap does not wrap merely because it has a max-height. Wheel input moves the content. A visible scrollbar supports dragging. Scroll positions survive source refreshes for stable element IDs. Keyboard navigation reveals focused descendants.

In Studio Preview, use the wheel for the UI and Ctrl+Wheel for preview zoom. overflow: hidden remains clipping only. For very large collections, use a fixed-height virtual list.

Scroll an element into view from the game

A game can bring an element into view without moving focus, for example to follow a narrated word through a long text. Reveal Element on the Local Player subsystem takes the screen name, the element's authored id and an optional duration (default 0.18 seconds, clamped to 5; zero is immediate). It returns false when the screen is not visible or an argument is empty. The renderer resolves the id after layout, then scrolls every scrollable ancestor vertically with an eased step and finishes with the regular reveal. Keyboard, controller and pointer state do not change. A repeated request for the same screen instance and element is ignored, a request for another element replaces the running one, and hiding the screen or losing the element drops it.

Calm->RevealElement(TEXT("Narrator"), TEXT("narrator-word-12"), 0.18f);

Drag and drop

GameUI's example uses stable domain IDs and two matching groups:

<button data-drag-key="carrot" data-drag-group="inventory">Carrot</button>
<section data-drop-group="inventory" data-drop-key="chest" tabindex="0"
  @drop="moveItem"></section>

moveItem receives { key, target, accepted }. Change the parent collection only after an accepted drop. A drag does not generate a click. Escape cancels it. For keyboard or controller input, focus the item and accept, focus the target panel and accept again. The moved item's stable drag key restores focus.

The shared runtime also delivers dragstart, dragenter, dragleave and dragend. dragstart supplies the item key; dragend includes the acceptance result. Its additive outside boolean is true only for a pointer release over empty space with no hit-testable UI in the participating views. It is false for Escape/cancellation, keyboard drags, and incompatible UI surfaces. A game may use outside to request dropping an inventory stack into the world; never infer that intent from accepted: false alone. Keep gameplay validation in the game, not in a demonstration component.

Unreal coordinates pointer drags across managed screens with matching drag/drop groups. Transparent background containers should use pointer-events: none, with pointer-events: auto restored on their interactive panels. Foreground panels still block drops through to obscured targets.

Compatibility

Range sliders declare the RangeControls capability. Packages using the editable or drag/drop attributes declare the InteractiveControls capability; data-calmui-multiline adds MultilineTextEditing. Older runtimes reject these packages rather than silently showing non-interactive controls. Update Studio and the plugin together. Existing Button, SquareButton and MainMenu styles do not need changes.

Form descriptions use overflow-wrap: anywhere for long unbroken words in constrained columns. This requires the EmergencyTextWrapping capability. Keep CLI, Studio/WASM and Unreal on matching builds when updating these forms.

On this page