Surface support
This table describes the source checked on 6 November 2026. It covers custom surface views. It does not certify an installed app or a physical device.
All three clients draw controls in the player's finish. They use painted keys and text. They do not open a system keyboard or render HTML.
“Yes” means the code draws or handles the field. “Stored” means it accepts the field but does not use it for that task. Bug numbers refer to Known gaps.
Surface and layout
| Node or option | Browser controller | iOS | Android |
|---|---|---|---|
v: 1, root |
Required. Unknown nodes are dropped. | Same. | Same. |
Surface id |
Resets text, toggles and strokes, and cancels touches (held buttons let go). | Resets local state and cancels touches. | Resets local state and cancels touches. |
turn.you |
Blinking LCD lamp and shell glow. | Same, plus feedback on a new turn. | Same, plus feedback on a new turn. |
turn.name, turn.color |
Default turn heading and lamp/glow colour. | Same. | Same. |
accent |
Keyboard submit-key colour. Selection rings keep their own colour. | Same. | Same. |
minApp |
View metadata. No renderer version gate. | Same. | Same. |
legacy |
Host selects a fallback before delivery. | Same. | Same. |
row, column, children |
Horizontal or vertical split. First 12 children. | Same. | Same. |
Part id |
Stored. Child paths locate parts. | Same. | Same. |
weight |
Share of free space, 0–50. | Same. | Same. |
Part size |
Fixed extent on the parent axis, 8–2000. | Same. | Same. |
Group gap |
Space between children, 0–80, including updates. | Yes, including updates. | Yes, including updates. |
spacer |
Empty space. No touch target. | Same. | Same. |
| Automatic MENU key | On the topmost LCD; rightmost breaks a tie. | Same. | Same. |
minApp does not replace room requires. A surface needs an LCD to get an automatic MENU key. The first surface app versions are listed in SURFACE_APPS; this table adds no new version promise.
Key shapes
keys.shape |
Browser controller | iOS | Android |
|---|---|---|---|
round |
Circular rubber key. A lone key can grow to buzzer size. | Same. | Same. |
square |
Rectangle with capped height. | Same. | Same. |
tile |
Broad rectangle with rounded corners. | Same. | Same. |
screen |
Square glass display in a rubber bezel. | Same. | Same. |
paddle |
Full-height rectangle. Label, colour, badge and selected ring work. | Same. | Same. |
All shapes handle the same key events and buttons.
Key options
| Option | Browser controller | iOS | Android |
|---|---|---|---|
keys.keys |
First 32 keys, within the shared node budget. | Same. | Same. |
keys.columns |
1–8, or automatic packing, including updates. | Yes, including updates. | Yes, including updates. |
Key id |
Event/toggle identity. Missing IDs become key-N. |
Same. | Same. |
label |
Printed on every shape. screen also supports a caption below an icon. |
Same. | Same. |
icon |
All names in the icon list. | Same, using SurfaceGlyphs.swift too. |
Same. |
label with icon |
Both on screen. Other shapes draw the icon alone. |
Same. | Same. |
color |
Tints round, square, tile and paddle keys. Tints screen contents. | Same. | Same. |
fill |
Fills screen glass when color is valid. Ignored on other shapes. |
Same, including updates. | Yes on screen. Ignored on other shapes. |
badge |
Uppercase pill on screen and paddle. Trimmed to 12 UTF-16 units. |
Same, including updates. | Same. Width is uncapped. B5. |
badgeTone |
alert: red. good: green. Omit for dark. |
Same. | Same. |
Badge on round, square, tile |
Stored but not drawn. | Same. | Same. |
selected |
Ring on every shape. Used initially for a toggle; otherwise follows the host. | Same. | Same. |
disabled |
Dimmed; presses ignored. | Same, for touch and VoiceOver. | Dimmed; presses ignored. |
event, payload |
Release on the same key sends the event and payload. | Same. | Same. |
button |
Standard button 0–15 stays down while held. | Same. | Same. |
toggle |
Local selection group. Same-ID views keep the local choice. | Same. | Same. |
Non-tool value |
Stringified during normalization. Used as a confirm target. | Same. | Same. |
send |
Sends the selected value/ID as target. Disabled until a choice exists. |
Same. | Same. |
gestures: ['hold'] |
Event at 450 ms. Suppresses the release event. | Same. | Same. |
gestures: ['double'] |
Delays a single tap for 280 ms. Paired releases send gesture: 'double'. |
Same. | Uses the second press to recognize the pair. |
tool: 'color', tool value |
Selects local ink for the first canvas. | Same. | Same. |
tool: 'width', tool value |
Selects local brush width, 1–24. | Same. | Same. |
tool: 'undo' |
Removes every chunk of the last stroke and sends its ID. | Removes all local chunks with that stroke ID. | Same as iOS. |
tool: 'clear' |
Clears local strokes and sends the first canvas's clear event. | Same. | Same. |
Key size |
Brush-dot size on round width-tool keys. Does not size a key rectangle. | Same. | Same. |
image |
Inline PNG/GIF/JPEG/WebP accepted. Not painted. | Same. | Same. |
Keep IDs distinct. Use one behaviour per key. In particular, do not combine a held button with event gestures. fill needs a valid colour. Badges do not depend on fill.
LCD
| Option | Browser controller | iOS | Android |
|---|---|---|---|
lcd |
Glass display with text and indicators. | Same. | Same. |
title, text, sub |
Uppercase, single lines. Shrunk or squeezed to fit; clipped to glass. | Same. | Same. |
| Text wrapping or rich text | Unsupported. There is no wrap, font or HTML option. |
Same. | Same. |
tone |
good, bad, alert tint the main text. normal and dim use normal ink. |
Same. | Same. |
timer.left, timer.total |
Local countdown, digits and progress bar. | Same. | Same. |
meter.value, meter.max |
Segmented progress bar. | Same. | Same. |
meter.label |
Visible on wide or large glass. Narrow glass shows the bar alone. | Same. | Same. |
tips |
Rotate every 4.5 seconds. A nonempty sub takes priority. |
Same. | Same. |
flash.text, flash.tone, flash.id |
One shared flash, once per unseen ID, about 1.4 seconds. IDs survive surface changes. | Same. | Same. |
entry |
Keyboard text/cursor or slots. Ignored when glass height exceeds 170 design units. | Same. | Same. |
secret, cover |
Covered until held. Visual privacy only. | Same. Also covers during screen capture. | Covered until held; window capture policy is outside the renderer. |
pages.index, pages.count |
Page dots when glass height is at most 170. No page-change event. | Same. | Same. |
Use one entry LCD and one keyboard. Use one flash at a time. The timer is a display; the host decides when time runs out.
Canvas
| Option or data | Browser controller | iOS | Android |
|---|---|---|---|
canvas, aspect |
Drawing paper centred in its slot, with the requested aspect ratio. | Same. | Same. |
event |
Sends normalized stroke chunks with points, color, width, stroke. |
Same. | Same. |
enabled |
False blocks new drawing and dims the paper. Does not disable undo/clear tools. | Same. | Same. |
clearEvent, undoEvent |
Tool event names. Default to clear and undo. |
Same. | Same. |
ink.value |
Blocks new drawing at zero. No automatic ink spending. | Same. | Same. |
ink.max |
Stored. Does not set a separate visual meter. | Same. | Same. |
Shared canvas[].points, color, width, stroke |
Restores when local strokes are empty. Clears when shared strokes become empty. New strokes number on from the restored ones. | Same restore/clear policy. Reseeds the counter. | Same as iOS. |
| Nonempty shared canvas replacement | Does not replace existing local strokes. | Same. | Same. |
Use one canvas. Keep authoritative drawing and ink limits on the host. Shared strokes are not a general drawing synchronizer.
Keyboard and trackpad
| Node or option | Browser controller | iOS | Android |
|---|---|---|---|
keyboard |
Painted keys. One shared editor per surface. | Same. | Same. |
language |
Game language, player language and English. Unsupported tags fall back to English. | Same, from bundled layout data. | Same, from bundled layout data. |
charset |
letters, text, digits. Text adds spaces and punctuation. |
Same. | Same. |
maxLength |
Grapheme limit when Intl.Segmenter exists; otherwise code points. |
Swift character limit. | Extended grapheme limit. |
slots |
Fixed entry boxes. Overrides maxLength. |
Same. | Same. |
submit.event, submit.label |
Sends trimmed, nonempty { text }. Clears the editor. |
Same. | Same. |
Keyboard enabled |
False dims keys and blocks typing. | Same. | Same. |
| Layout pages, shift, RTL, Hangul and kana | Handled. Chinese has pinyin letters, without Hanzi candidates. | Same. | Same. |
Layout alt characters |
Available in the data API. No touch menu exposes them. This limits accented and Indic entry. | Same. | Same. |
trackpad |
Relative pointer, tap, drag and two-finger gestures. | Same. | Same. |
Trackpad event |
Sends { dx, dy } motion. |
Same. | Same. |
clickEvent |
Tap/hold-drag sends { button: 0, down }. Two-finger tap uses button 2. |
Same. | Same. |
scrollEvent |
Two-finger scroll sends { dx, dy }. |
Same. | Same. |
Trackpad enabled |
False blocks new touches. | Same. | Same. |
Trackpads send game events. They do not grant system-control permission. See Keyboards for language limits.
Accessibility
| Feature | Browser controller | iOS | Android |
|---|---|---|---|
| Painted labels, keys and LCDs | Canvases are hidden from assistive technology. No semantic control tree. B6. | VoiceOver elements and key actions. Secret text stays covered. | Accessibility nodes and click actions. |
| Badge value and selected state | No semantic value/state. B6. | Badge value and selected trait. | Missing from nodes. B6. |
| Hold/double actions | Touch only. B6. | VoiceOver custom actions on enabled keys. | Touch only. B6. |
Known gaps
These are source findings. No app code was changed for this table.
| Bug | Finding |
|---|---|
| B1 | Fixed: the browser re-lays a surface out when gap, key shape or columns change. |
| B2 | Fixed: iOS repaints a key when its fill, badge or badge colour changes. |
| B3 | Fixed: a new browser surface ID cancels active touches and releases held buttons (test: tests/testing-surface-swap.test.mjs). |
| B4 | Fixed: Android cuts badges to 12 characters, as the browser and iOS do. |
| B5 | Fixed: Android caps the badge pill at the key's width and squeezes its text. |
| B6 | Browser surfaces lack semantic controls. Android omits badge values, selected state and gesture actions that iOS exposes. |
| B7 | Fixed: iOS offers VoiceOver's hold and double-tap actions only on enabled keys. |
| B8 | Fixed: browser undo removes every chunk of the last restored stroke, as the apps do. |
| B9 | Fixed: after restoring a shared drawing, the browser numbers new strokes after the restored ones. |
Source map
Browser behaviour comes from surface builders and layout (in the SDK download: src/surface.mjs), touch and editor handling (in the SDK download: web/surface.mjs), and painting (in the SDK download: web/surface-paint.mjs). surface-layout.mjs (in the SDK download: web/surface-layout.mjs) handles the separate wheel/flight controllers; custom trees use layoutSurface in src/surface.mjs.
iOS uses Sources/Surfaces/SurfaceModel.swift, SurfacePaint.swift, SurfaceGlyphs.swift, GameSurfaceView.swift, SurfaceKeyboard.swift and SurfaceTrackpad.swift.
Android uses GameSurfaceModel.kt, GameSurfacePaint.kt, GameSurfaceView.kt, SurfaceKeyboard.kt, SurfaceTrackpad.kt and SurfaceKeyGestures.kt in controller/plus.