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"> </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.
| Event | Payload and behavior |
|---|---|
input | Draft string, including a blank or temporary minus sign. |
change | Normalized number on Enter, focus loss or a step-button action. |
invalid | Draft string when confirmation cannot parse a finite number. |
cancel | Normalized 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.