Extract the popup input policy so native tests can pin it
The confirm-dialog policy — which physical control fires each modal role
per board class, when the confirm/cancel pair swaps, and which taps a
modal may answer at all — lived as anonymous-namespace state in
popup_system.cpp, reachable only by flashing hardware. The choice-list
half had already been extracted (choiceListNav); this moves the rest.
InputScheme, ModalHints, hintsFor and swapConfirmCancel are now pure
constexpr policy in popup_system.h. The caption tables and the swap
truth table (dial and touch zones always swap so OK confirms, buttons
swap only for tap dialogs, keyboard and free-touch never) are pinned by
native tests instead of being provable only on glass.
The free-touch tap qualification is now a TapQualifier class with the
same shape as the two-button convention's HoldGauge: pure, fed one
pointer frame per poll. Its semantics mirror the device path exactly —
arming requires a frame with no contact AND no pending tap edge (a stale
contact's lift reports both on one frame, and that frame must neither
arm nor resolve), the contact pins to where it began so cross-cell
travel reads as a drag, the edge consumes the contact exactly once so a
non-consuming driver edge cannot replay, and a tap faster than one poll
period is swallowed. The device keeps ONE instance where three hand
rolled latches were; the two consumers (modal cells, choice-list rows)
classify the qualified points and nothing else.
One deliberate asymmetry is preserved and now documented at the type:
single-action alerts acknowledge on the armed LEVEL plus the driver
edge, not on full qualification — any tap that starts after the alert
was drawn acknowledges it, including one faster than a poll period,
while a contact predating the alert still cannot (its edge frame is the
frame arming excludes). That behavior existed; making it a named
accessor (armed()) keeps the next reader from "fixing" it.
Seven new native tests pin the policy and the state machine, including
the artifact cases that motivated it: stale contact never resolves,
origin pinning, edge replay, sub-poll swallow, and mid-contact reset.
Verified on the T-Deck: a confirm modal opens with its Tap cells and a
qualified tap on Cancel resolves and tears it down. 2553 native tests;
all eight variants build.
Claude-Session: https://claude.ai/code/session_01ABhkBJsMKTZAxZh4Vh7jsF