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))