Fix touch/mouse bridge offset #134

Merged
albert merged 2 commits from fix/touch-bridge-grabs-clickpad into main 2026-08-19 05:58:57 +00:00
Owner

On a Legion Go S, touch-to-mouse mapped roughly the top-left quarter of the
panel to the whole screen, with everything past it pinned at the edge. Two
defects compounded:

looks_like_touchscreen() tested only for BTN_TOUCH plus absolute X/Y axes.
Clickpads report exactly that, so the handheld's touchpad (INPUT_PROP_POINTER
| INPUT_PROP_BUTTONPAD, range 0..400) was grabbed alongside the real
0..1920x0..1200 panel. And main kept a single DeviceRange -- whichever
device opened first -- and applied it to every reader's raw coordinates. The
panel normalized against 0..400 saturates after 20.8% of its width and 33% of
its height.

Which device opens first is arbitrary: evdev::enumerate() is a bare read_dir
with no sorting, so the panel winning the race was luck. It held until a
session switch re-probed the USB device and moved the touchpad from event6 to
event10.

Require the device to be direct -- INPUT_PROP_DIRECT, or claiming neither
INPUT_PROP_POINTER nor BTN_TOOL_FINGER -- mirroring how udev's input_id
builtin classifies these, with the fallback keeping panels that omit the
property working. The rule is a pure function over a TouchCaps struct so it is
testable without a real device node.

Independently, carry already-normalized coordinates in TouchUpdate so each
device is scaled against its own range. The reader thread already had that
range; it just wasn't using it. This makes the enumeration-order dependence
structurally impossible rather than merely unlikely.

The tablet bridge carried an identical copy of the shared-range defect (not
the filter one -- it excludes INPUT_PROP_DIRECT by design, since it re-maps
indirect devices). Fixed the same way, but with no tablet hardware to hand
tablet_to_touch still wants re-testing on a real device.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

On a Legion Go S, touch-to-mouse mapped roughly the top-left quarter of the panel to the whole screen, with everything past it pinned at the edge. Two defects compounded: `looks_like_touchscreen()` tested only for BTN_TOUCH plus absolute X/Y axes. Clickpads report exactly that, so the handheld's touchpad (INPUT_PROP_POINTER | INPUT_PROP_BUTTONPAD, range 0..400) was grabbed alongside the real 0..1920x0..1200 panel. And `main` kept a single DeviceRange -- whichever device opened first -- and applied it to every reader's raw coordinates. The panel normalized against 0..400 saturates after 20.8% of its width and 33% of its height. Which device opens first is arbitrary: evdev::enumerate() is a bare read_dir with no sorting, so the panel winning the race was luck. It held until a session switch re-probed the USB device and moved the touchpad from event6 to event10. Require the device to be direct -- INPUT_PROP_DIRECT, or claiming neither INPUT_PROP_POINTER nor BTN_TOOL_FINGER -- mirroring how udev's input_id builtin classifies these, with the fallback keeping panels that omit the property working. The rule is a pure function over a TouchCaps struct so it is testable without a real device node. Independently, carry already-normalized coordinates in TouchUpdate so each device is scaled against its own range. The reader thread already had that range; it just wasn't using it. This makes the enumeration-order dependence structurally impossible rather than merely unlikely. The tablet bridge carried an identical copy of the shared-range defect (not the filter one -- it excludes INPUT_PROP_DIRECT by design, since it re-maps indirect devices). Fixed the same way, but with no tablet hardware to hand tablet_to_touch still wants re-testing on a real device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On a Legion Go S, touch-to-mouse mapped roughly the top-left quarter of the
panel to the whole screen, with everything past it pinned at the edge. Two
defects compounded:

`looks_like_touchscreen()` tested only for BTN_TOUCH plus absolute X/Y axes.
Clickpads report exactly that, so the handheld's touchpad (INPUT_PROP_POINTER
| INPUT_PROP_BUTTONPAD, range 0..400) was grabbed alongside the real
0..1920x0..1200 panel. And `main` kept a single DeviceRange -- whichever
device opened first -- and applied it to every reader's raw coordinates. The
panel normalized against 0..400 saturates after 20.8% of its width and 33% of
its height.

Which device opens first is arbitrary: evdev::enumerate() is a bare read_dir
with no sorting, so the panel winning the race was luck. It held until a
session switch re-probed the USB device and moved the touchpad from event6 to
event10.

Require the device to be direct -- INPUT_PROP_DIRECT, or claiming neither
INPUT_PROP_POINTER nor BTN_TOOL_FINGER -- mirroring how udev's input_id
builtin classifies these, with the fallback keeping panels that omit the
property working. The rule is a pure function over a TouchCaps struct so it is
testable without a real device node.

Independently, carry already-normalized coordinates in TouchUpdate so each
device is scaled against its own range. The reader thread already had that
range; it just wasn't using it. This makes the enumeration-order dependence
structurally impossible rather than merely unlikely.

The tablet bridge carried an identical copy of the shared-range defect (not
the filter one -- it excludes INPUT_PROP_DIRECT by design, since it re-maps
indirect devices). Fixed the same way, but with no tablet hardware to hand
tablet_to_touch still wants re-testing on a real device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs(input): record how the clickpad-range bug was caught
All checks were successful
CI / Version harmony (pull_request) Successful in 12s
CI / Arch neutrality (pull_request) Successful in 11s
CI / ShellCheck (pull_request) Successful in 15s
CI / CI image (pull_request) Successful in 23s
CI / CI image (Android) (pull_request) Successful in 21s
CI / CI images (pull_request) Successful in 0s
CI / Rustfmt (pull_request) Successful in 12s
CI / Warm cargo registry (pull_request) Successful in 22s
CI / Android media (cargo-ndk build) (pull_request) Successful in 34s
CI / Android companion (unit tests) (pull_request) Successful in 1m52s
CI / Clippy (pull_request) Successful in 1m52s
CI / Build (pull_request) Successful in 3m54s
CI / Test (pull_request) Successful in 4m50s
CI / E2E (pull_request) Successful in 4m41s
CI / Firewall E2E (pull_request) Successful in 5m33s
CI / Package (.deb smoke build) (pull_request) Successful in 2m47s
6136f0d9c0
The journal already names the bug: one "Grabbed touchscreen" line per grabbed
device, and more than one line is itself the signal. Write that down, along
with the evtest recipe against the bridge's own synthetic device, the
164-counts-per-raw-unit arithmetic that identifies the divisor in use, and the
two red herrings -- the touchpad that kept working (a second, ungrabbed
relative node on the same USB interface) and an apparent wrap to 0 (a new
stroke, not an overflow).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
albert changed title from fix/touch-bridge-grabs-clickpad to Fix touch/mouse bridge offset 2026-08-19 05:52:56 +00:00
albert merged commit 74674f9a5a into main 2026-08-19 05:58:57 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
albert/shepherd-launcher!134
No description provided.