NativeKeyboardView extends View. It does not inflate a grid of buttons from XML.
setKeys(keys, state, heightPx) stores List<PositionedKey> and requests layout.onDraw fills #111111 and draws each key as a round rect with label paint.onMeasure uses the height from the layout computer when set.Geometry comes from SofleLayoutComputer (implements KeyboardLayoutComputer): given width and layer name, it returns positioned keys. Snap distance for misses between keys uses maxSafeSnapPx from that computer when width is known.
Two independent definitions must stay in sync by hand:
src/keyboard/Layout.ts - RN preview / settingsandroid/.../SofleKeyData.kt - IME (source of truth for typing)The IME never reads Layout.ts at runtime. CLAUDE.md documents this. If you change one file only, preview and typing diverge. History includes a fix that added the emoji key to the native layout after it existed only on the RN side.
Layer names in data: base, lower, raise, adj, func.
SofleKeyData comments describe V5 structure: top row, left 4x5, right 4x5.
Hold annotations on base (from the file, not from memory):
a ctrl, s meta, d alt, f shift; h shift, j alt, k meta, l ctrl (timer path).holdAction set; touch path treats them as activate-on-down.Constants in NativeKeyboardView companion:
private const val HIT_EXPAND_DP = 2.5f
hitTest first checks expanded bounds (hitExpandPx = density * 2.5). If none hit, nearest key within snapThresholdPx * snapThresholdPx wins. Drawing still uses the unexpanded rect.
Authoritative design notes: GESTURE_ARCHITECTURE.md. Runtime: NativeKeyboardView.onTouchEvent + KeyboardState + TapMachine.
| Name | Value | Where |
|---|---|---|
| TAPPING_TERM_MS | 150 | NativeKeyboardView |
| REPEAT_INITIAL_DELAY_MS | 400 | NativeKeyboardView |
| REPEAT_INTERVAL_MS | 50 | NativeKeyboardView |
| TapMachine doubleTapMs | 300 (default) | TapMachine.kt |
DOWN: haptic, onKeyTapped immediately. If action is backspace/delete, also arm repeat timer (400ms then 50ms). MOVE off key cancels repeat. UP cancels repeat.
DOWN when holdAction != null and action not in dedicated-mod set:
onKeyTapped (type letter/space)onKeyHeld -> KeyboardState.applyHoldonKeyReleased -> releaseHoldSet in view: shift, ctrl, alt, lower, raise, func, adj.
DOWN: onKeyHeld immediately, remember pointer, modHoldOtherKeyPressed = false.
Other key DOWN while held: set modHoldOtherKeyPressed = true.
UP:
val wasQuickTap = !otherPressed &&
(now - modHoldStartTime) < TAPPING_TERM_MS
onKeyReleased(key)
if (wasQuickTap) onKeyTapped(key) // cycle latch/lock
MOVE does not cancel dedicated holds (comment in source: drift while holding LWR is expected).
layerHeld for layer holdseffectiveLayer = layerHeld ?: layerisShiftActive = latch or hold on shift only (not caps)isCapsActive = caps latch not NONEresolveLabel: upper if (shift && !caps) || (!shift && caps)onCharCommitted: clear LATCHED mods and latched layer; reset TapMachines; does not clear holdsTapMachine.check on modifier/layer cycleRepeat and hold track pointer ids. Second finger can type while a mod is held.
| Claim | Evidence |
|---|---|
| HIT_EXPAND 2.5, tap 150, repeat 400/50 | NativeKeyboardView companion |
| doubleTap 300 | TapMachine default |
| Dedicated mod set | DEDICATED_MOD_ACTIONS in NativeKeyboardView |
| Home row hold map | SofleKeyData BASE left/right rows |
| isShiftActive excludes caps | KeyboardState.kt lines for isShiftActive / resolveLabel |
| Dual layout rule | CLAUDE.md; Layout.ts vs SofleKeyData.kt |
| Canvas view not RN for IME keys | NativeKeyboardView; CodeKeyboardIME.onCreateInputView |