ec9a9a8892
Every swipe on the rack display classified as up-to-down. The first one took the display from the top row to the bottom row, and from there "down" is a no-op, so it was stuck permanently -- looking like dead gestures while the process was alive and the panel still grabbed. X and Y arrive as SEPARATE events, so a new contact's opening frame is ABS_MT_POSITION_X then ABS_MT_POSITION_Y. Treating (0, 0) as "start not yet known" meant the start was captured on the X event alone, recording a Y of zero. Every later comparison then measured from the top edge of the panel rather than from the finger: dy became the absolute Y coordinate, dwarfed dx, and the gesture came out vertical. A swipe at y=360 on a 720-tall panel reported 360 pixels of downward travel that never happened. The start position is now Option per axis. An axis that never reports contributes no displacement, so there is no sentinel to collide with a real coordinate. The slot bookkeeping moves into a SlotTracker fed by a small Touched enum rather than evdev's types, because it was untestable before and that is exactly where the bug lived -- the existing classifier tests built Track tuples by hand and skipped the decoding entirely. Six new tests drive realistic protocol-B streams: the regression itself, per-axis start capture, all four directions end to end, ghost contacts decoding as one swipe of the correct length, completion only once every contact lifts, and reset between gestures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>