v0.7.1 (build 925)

Import #

import { InputText, Button, ScrollView, blurAll } from '/rom/std/koya-controls.js';

// Alternatively, import these names from '/rom/std/koya-ui.js'.

Design philosophy #

Controls are composable, not opinionated. You supply pre-created visual elements (background, focus ring, text renderable) and the control wires up the interaction logic — keyboard input, caret management, animation triggers, focus cycling. This means you keep full creative control over appearance while getting the behaviour for free.

Focus and tab navigation #

Controls within the same window share a focus system. Only the active control receives keyboard and text events. Tab and Shift+Tab cycle focus using HTML-style tab-index semantics:

  • tabIndex > 0 — sorted ascending, focused first.
  • tabIndex === 0 — registration order, after positives.
  • tabIndex < 0 — excluded from the tab cycle entirely.

Call blurAll(window) to programmatically remove focus from the active control.

InputText #

Single-line text input, equivalent to <input type="text">. Handles caret positioning, horizontal scrolling, placeholder text, clipboard (Ctrl+C/X/V), and arrow/Home/End navigation.

import { Window, InputText } from '/rom/std/koya-ui.js';

const win = await Window.create({ role: 'window', size: { x: 400, y: 200 }, keyboardInteractivity: 'on_demand' });

const bg = await win.createElement({
    renderable: { type: 'box', colour: [0.12, 0.14, 0.18, 1] },
    item: { size: { x: 260, y: 36 } }
});

const fg = await win.createElement({
    renderable: { type: 'text', size: 14, colour: [0.9, 0.95, 1, 1] },
    contentAlign: { x: 'start', y: 'start' }
});

const input = await InputText.create(win, {
    background: bg,
    foreground: fg,
    size: { x: 260, y: 36 },
    placeholder: 'Type here…',
    onInput: (value) => console.log('value:', value),
    onSubmit: (value) => console.log('submit:', value)
});

await input.master.attach(await win.createElement({ item: { position: { x: 70, y: 80 } } }));

InputText.create options #

InputText.create
Async factory. Assembles the element tree, measures font metrics, creates the caret, and registers for focus management.
  • options.background : Element — Visual backdrop; receives the click-to-focus handler.
  • options.focus : Element? — Optional focus-ring element, toggled visible on focus.
  • options.foreground : Element — Text renderable the control manages. Should use contentAlign: { x: 'start', y: 'start' }.
  • options.size : { x, y } — Overall control dimensions. Default { x: 200, y: 32 }.
  • options.padding : { l, r, t, b } — Inner padding. Default { l: 8, r: 8, t: 0, b: 0 }.
  • options.value : string — Initial text value.
  • options.placeholder : string — Shown when empty and unfocused.
  • options.textColour : [r,g,b,a] — Colour for value text.
  • options.placeholderColour : [r,g,b,a] — Colour for placeholder text.
  • options.caretColour : [r,g,b,a] — Caret colour.
  • options.caretWidth : number — Caret pixel width. Default 2.
  • options.tabIndex : number — Tab-cycle order. Default 0.
  • options.onInput : (value: string) => void — Called on every text change.
  • options.onFocus : () => void — Called when focus is gained.
  • options.onBlur : () => void — Called when focus is lost.
  • options.onSubmit : (value: string) => void — Called when Enter is pressed.
  • options.password : boolean — Masks the value and composition with bullets. Disables copy/cut and omits surrounding text from input methods; password purpose and sensitive/hidden hints are set automatically.
  • options.revealed : boolean — Initially show a password; clipboard and input-method privacy remain in force.
  • options.disabled : boolean — Refuse editing, focus and tab navigation until enabled.
  • options.maxLength : number — Maximum number of Unicode codepoints; default unlimited.
InputText.create(window: Window, options: InputTextOptions): Promise<InputText>

InputText methods #

getValue
Returns the current text value. Synchronous.
input.getValue(): string
setValue
Replaces the text value programmatically. Sanitises control characters and triggers a re-render.
input.setValue(text: string): Promise<void>
isFocused
Returns true if this input currently has focus. Synchronous.
input.isFocused(): boolean
focus
Gives focus to this input, blurring the previously active control.
input.focus(): Promise<void>
blur
Removes focus from this input.
input.blur(): Promise<void>
setPlaceholder
Updates the placeholder text.
input.setPlaceholder(text: string): Promise<void>
setOnInput
Replaces the input callback. Synchronous.
input.setOnInput(fn: function | null): void
setOnFocus
Replaces the focus callback. Synchronous.
input.setOnFocus(fn: function | null): void
setOnBlur
Replaces the blur callback. Synchronous.
input.setOnBlur(fn: function | null): void
setOnSubmit
Replaces the submit callback. Synchronous.
input.setOnSubmit(fn: function | null): void
destroy
Unregisters from focus management, removes handlers, and destroys internal elements.
input.destroy(): Promise<void>
setDisabled
Disable editing/focus and hide the caret, or enable the input again.
input.setDisabled(disabled: boolean): Promise<void>
setRevealed
Show or mask a password without exporting it to clipboard or input methods.
input.setRevealed(revealed: boolean): Promise<void>
setSize
Resize the complete input and its viewport, then update scrolling.
input.setSize(size: { x, y }): Promise<void>
Note

The input.master element is the top-level container. Attach it to your layout tree to position the control.

InputText keyboard shortcuts #

KeyAction
Left / RightMove caret one character
Home / EndMove caret to start / end
BackspaceDelete character before caret
DeleteDelete character after caret
Ctrl+CCopy full value to clipboard
Ctrl+XCut full value to clipboard
Ctrl+VPaste from clipboard at caret
EnterFire onSubmit callback
Tab / Shift+TabCycle focus to next / previous control

InputText animations #

If any of the user-provided elements (background, focus, foreground) have named animations registered, InputText will trigger them on state transitions:

  • 'focus' — played on all user elements when the input gains focus.
  • 'blur' — played on all user elements when the input loses focus.

Button #

Composable, animation-driven button. You provide the visual elements and register named animations; the control handles mouse interaction and fires them at the right time.

import { Window, Button } from '/rom/std/koya-ui.js';

const win = await Window.create({ role: 'window', size: { x: 400, y: 200 } });

const bg = await win.createElement({
    renderable: { type: 'box', colour: [0.2, 0.5, 1.0, 1] },
    item: { size: { x: 160, y: 44 } }
});

const label = await win.createElement({
    renderable: { type: 'text', string: 'Click me', size: 14, colour: [1, 1, 1, 1] },
    contentAlign: { x: 'centre', y: 'centre' },
    item: { size: { x: 160, y: 44 } }
});

await bg.addAnimation('hover', [
    { time: 0.0, colour: [0.2, 0.5, 1.0, 1] },
    { time: 0.15, colour: [0.3, 0.6, 1.0, 1] }
]);
await bg.addAnimation('actionOn', [
    { time: 0.0, scale: { x: 1, y: 1 } },
    { time: 0.08, scale: { x: 0.96, y: 0.96 } }
]);
await bg.addAnimation('actionOff', [
    { time: 0.0, scale: { x: 0.96, y: 0.96 } },
    { time: 0.12, scale: { x: 1, y: 1 } }
]);

const btn = await Button.create(win, {
    background: bg,
    foreground: label,
    size: { x: 160, y: 44 },
    onClick: () => console.log('clicked!')
});

Button.create options #

Button.create
Async factory. Assembles the element tree and wires mouse handlers on the background.
  • options.background : Element — Required. Visual backdrop and hit target.
  • options.foreground : Element — Required. Text label element.
  • options.elements : Element[] — Optional extra decorative elements inserted between background and foreground.
  • options.size : { x, y } — Master dimensions. Default { x: 200, y: 40 }.
  • options.disabled : boolean — Initial disabled state.
  • options.disabledOpacity : number — Opacity when disabled. Default 0.4.
  • options.tabIndex : number — Tab-cycle order. Default -1 (excluded).
  • options.onClick : () => void — Called on a successful click (press + release while hovered).
Button.create(window: Window, options: ButtonOptions): Promise<Button>

Button methods #

isDisabled
Returns current disabled state. Synchronous.
button.isDisabled(): boolean
isHovered
Returns true if the pointer is over the button. Synchronous.
button.isHovered(): boolean
isPressed
Returns true if the button is currently held down. Synchronous.
button.isPressed(): boolean
setDisabled
Enables or disables the button. When disabled, mouse interactions are ignored and a fade animation plays.
button.setDisabled(value: boolean): Promise<void>
setLabel
Updates the foreground text string.
button.setLabel(text: string): Promise<void>
setOnClick
Replaces the click callback. Synchronous.
button.setOnClick(fn: function | null): void
destroy
Removes mouse handlers and destroys the master element tree.
button.destroy(): Promise<void>

Button animations #

The button triggers well-known animation names on its user-provided elements. Register these on any of background, foreground, or elements[] for visual feedback:

NameTrigger
hoverPointer enters the button
actionOnPointer button pressed
actionOffPointer button released or pointer exits

If an element does not have a given animation name, it is silently skipped.

blurAll #

blurAll
Blurs the currently focused control (if any) in the given window.
blurAll(window: Window): Promise<void>

ScrollView #

A native kinetic scroll viewport. The engine moves and clips content, recognizes touch dragging, simulates inertia, and reports scroll lifecycle events. The control adds no JavaScript movement timers and does not join keyboard focus or Tab navigation. It is exported by both /rom/std/koya-controls.js and /rom/std/koya-ui.js.

import { Window, ScrollView } from '/rom/std/koya-ui.js';
const win = await Window.create({role: 'window', size: {x: 320, y: 240}, acceptPointerEvents: true});
const view = await ScrollView.create(win, {
    size: {x: 320, y: 240},
    contentSize: {x: 320, y: 960},
    scroll: {axis: 'y'},
    onScroll(event) {
        if(event.phase === 'end') console.log('Stopped', event.scrollOffset);
    },
    children: [{
        item: {aabb: {min: {x: 0, y: 0}, max: {x: 320, y: 960}}},
        renderable: {type: 'box', colour: [0.2, 0.3, 0.4, 1]},
        contentAlign: 'fill'
    }]
});
await win.attach(view.master);
// Existing elements can also be attached: await row.attach(view.content);
await view.scrollTo({x: 0, y: 120});
// await view.stopScroll(); // Cancels movement; preserves offset.
// await view.setContentSize({x: 320, y: 1440}); // After adding rows.
// await view.destroy(); // Destroys the viewport and its entire content subtree.

ScrollView.create options #

OptionMeaning
sizeViewport layout size; default {x: 320, y: 240}.
contentSizeExplicit content wrapper size; defaults to size. Increase it beyond the viewport on the desired axis to allow scrolling. Both sizes require finite, non-negative dimensions.
scrollNative boolean or ScrollOptions; default true (vertical). Supports axis, wheelStep, friction, minVelocity, stopDelayMs, and enabled; see the native scrolling guide.
childrenElement creation options passed as children of content; default []. Use explicit item bounds or attach existing elements to view.content.
onScrollOptional observer receiving native start/change/end/cancel events, scrollOffset, scrollLimit, velocity, and coasting. end means input and inertia have settled; interruption produces cancel.

Attach view.master to your layout. view.content is the owned content wrapper; attach rows or other controls there. Its size is explicit: adding descendants does not automatically enlarge it. Call setContentSize when content grows or shrinks. setSize changes the viewport only. Layout and offsets use UI units. The control owns all attached descendants and destroys them with itself; detach anything you intend to keep first.

ScrollView methods #

setSize
Sets the viewport’s layout size. A root still fills the window; parent layout determines its actual bounds.
view.setSize(size: {x, y}): Promise<boolean>
setContentSize
Sets the content wrapper size; native limits and offset reclamp on the next layout pass.
view.setContentSize(size: {x, y}): Promise<boolean>
setScroll
Replaces native configuration; cancels active input. false disables scrolling and resets offset.
view.setScroll(options: boolean | ScrollOptions = true): Promise<boolean>
scrollTo
Cancels input and moves to a clamped offset immediately; does not start a new scroll lifecycle.
view.scrollTo(offset: {x, y}): Promise<boolean>
stopScroll
Cancels inertia and active gestures in this subtree, preserving offset. Observers receive cancel with reason scroll-stopped.
view.stopScroll(): Promise<boolean>
getScrollOffset
Reads the current clamped offset in UI units; rejects after control destruction.
view.getScrollOffset(): Promise<{x, y}>
setOnScroll
Replaces the observer synchronously without replacing the engine handler or interrupting scrolling.
view.setOnScroll(callback: function | null): void
destroy
Idempotently destroys master, content, and all descendants. Queued observer notifications are suppressed after destruction.
view.destroy(): Promise<void>

Mutation methods return false after destruction, except setOnScroll (a local observer setter); getScrollOffset rejects. The control has no scrollbar or elastic overscroll. Native wheel/touch routing, nested scroll behavior, tuning defaults, units, and platform differences are documented in Native scrolling. Touch dragging cancels child activation after taking over; native momentum needs no script animation loop. Try koya -m koya-base/assets -i examples/std-scroll-view.js for a 30-row example.