koya-controls
Standard text, button, and native kinetic scroll controls.
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 #
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 usecontentAlign: { 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. Default2.options.tabIndex:number— Tab-cycle order. Default0.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 #
input.getValue(): stringinput.setValue(text: string): Promise<void>true if this input currently has focus. Synchronous.input.isFocused(): booleaninput.focus(): Promise<void>input.blur(): Promise<void>input.setPlaceholder(text: string): Promise<void>input.setOnInput(fn: function | null): voidinput.setOnFocus(fn: function | null): voidinput.setOnBlur(fn: function | null): voidinput.setOnSubmit(fn: function | null): voidinput.destroy(): Promise<void>input.setDisabled(disabled: boolean): Promise<void>input.setRevealed(revealed: boolean): Promise<void>input.setSize(size: { x, y }): Promise<void>The input.master element is the top-level container. Attach it to your layout tree to position the control.
InputText keyboard shortcuts #
| Key | Action |
|---|---|
| Left / Right | Move caret one character |
| Home / End | Move caret to start / end |
| Backspace | Delete character before caret |
| Delete | Delete character after caret |
| Ctrl+C | Copy full value to clipboard |
| Ctrl+X | Cut full value to clipboard |
| Ctrl+V | Paste from clipboard at caret |
| Enter | Fire onSubmit callback |
| Tab / Shift+Tab | Cycle 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 #
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. Default0.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 #
button.isDisabled(): booleantrue if the pointer is over the button. Synchronous.button.isHovered(): booleantrue if the button is currently held down. Synchronous.button.isPressed(): booleanbutton.setDisabled(value: boolean): Promise<void>button.setLabel(text: string): Promise<void>button.setOnClick(fn: function | null): voidbutton.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:
| Name | Trigger |
|---|---|
hover | Pointer enters the button |
actionOn | Pointer button pressed |
actionOff | Pointer button released or pointer exits |
If an element does not have a given animation name, it is silently skipped.
blurAll #
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 #
| Option | Meaning |
|---|---|
| size | Viewport layout size; default {x: 320, y: 240}. |
| contentSize | Explicit 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. |
| scroll | Native boolean or ScrollOptions; default true (vertical). Supports axis, wheelStep, friction, minVelocity, stopDelayMs, and enabled; see the native scrolling guide. |
| children | Element creation options passed as children of content; default []. Use explicit item bounds or attach existing elements to view.content. |
| onScroll | Optional 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 #
view.setSize(size: {x, y}): Promise<boolean>view.setContentSize(size: {x, y}): Promise<boolean>view.setScroll(options: boolean | ScrollOptions = true): Promise<boolean>view.scrollTo(offset: {x, y}): Promise<boolean>view.stopScroll(): Promise<boolean>view.getScrollOffset(): Promise<{x, y}>view.setOnScroll(callback: function | null): voidview.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.