keyboard
The on-screen keyboard and text field — the typing surface for a headset, where
there is no OS keyboard and no DOM <input> to fall back on. Both are Widget3ds, so
they drop into a widget-box / surface panel like any other control and work
identically as a flat overlay and rasterized onto a plane.
The logic lives in the pure models — key-layout (which keys, where, and what a long-press offers) and text-edit (code-point-correct editing) — so this file is paint plus gesture.
Long-press for accents
Holding a letter that has alternatives (a c e i n o s u y z) pops them up; slide
onto one and release to insert it, or release on the key itself for the plain
character. The whole press is one gesture — the phone convention.
That gesture is exactly why BoxChild.handlePointer captures: the popup opens
above the key, so by the time you've slid onto ö the pointer is far outside the
key's own rect, and a hit-test-per-event model would have lost the gesture at the
first move.
Demo
Tap the keys. ?123 switches to symbols, ⇧ shifts, and holding o (or a, e,
u…) opens the accent popup — drag onto one and release.
import { surface, widgetBox, box, textBlock, inputField, keyboard } from 'tosijs-3d'
import { svgElements, elements } from 'tosijs'
const { svg } = svgElements
const { div } = elements
const W = 380
const H = 300
const field = inputField({ value: 'hold o for ö', placeholder: 'type something…' })
const kb = keyboard({
onKey: (ch) => field.insert(ch),
onAction: (a) => field.action(a),
})
const s = surface({ width: W, height: H })
s.setContent(
box(
{ width: W, height: H, padding: 12, gap: 10, background: '#12151c' },
textBlock('SVG keyboard', { font: { size: 15, weight: 600 }, color: '#e6e6e6' })
)
)
s.openPanel({ x: 8, y: 44 }, widgetBox(
{ width: 364, padding: 8, gap: 8, background: '#0e1116' },
[field, kb]
), { title: 'Text entry', draggable: true })
const svgEl = svg({ viewBox: `0 0 ${W} ${H}`, width: W, height: H }, s.el)
const at = (e) => {
const r = svgEl.getBoundingClientRect()
return [((e.clientX - r.left) / r.width) * W, ((e.clientY - r.top) / r.height) * H]
}
svgEl.addEventListener('pointerdown', (e) => { s.handlePointer('down', ...at(e)); svgEl.setPointerCapture(e.pointerId) })
svgEl.addEventListener('pointermove', (e) => s.handlePointer('move', ...at(e)))
svgEl.addEventListener('pointerup', (e) => s.handlePointer('up', ...at(e)))
preview.append(div({ style: 'padding:16px;background:#0c0e14' }, svgEl))