feat(capture): wall-press auto-release + Bounds protocol foundation#420
Open
jondkinney wants to merge 12 commits into
Open
feat(capture): wall-press auto-release + Bounds protocol foundation#420jondkinney wants to merge 12 commits into
jondkinney wants to merge 12 commits into
Conversation
This was referenced May 6, 2026
513818a to
a527cee
Compare
Adds a host-side fallback that releases capture when the user
sweeps the cursor against the host-adjacent edge of the guest
and keeps pushing past a configurable threshold. Solves the
"two locked screens" case where the peer's capture backend
can't fire CaptureBegin (and therefore can't send Leave back),
leaving the host stuck capturing indefinitely until the
release-bind chord is pressed.
Algorithm lives in InputCapture::poll_next so every backend
(macOS, libei, layer-shell, x11, windows, dummy) gets it for
free — they only need to emit standard motion events through
the existing Stream interface, which they already do. The
wrapper tracks:
virtual_pos: signed position along the entry axis, clamped at
0 from below. No upper clamp — the wrapper can't know the
guest's far-edge extent without protocol-level cooperation,
and any proxy is wrong for some user's setup.
wall_pressure: motion that overshoots the host-adjacent edge
and would have driven virtual_pos negative. Fires
CaptureEvent::AutoRelease when the threshold is reached;
the capture loop then runs the same teardown path as the
release-bind chord.
State resets on Begin (entry to capture), AutoRelease (we
self-released), and external release (chord, peer Leave,
connection error, EnterOnly fallback).
Surface:
- New FrontendRequest::SetReleaseThreshold + FrontendEvent::
ReleaseThreshold IPC pair.
- New release_threshold_px field on the daemon config (0 = off,
serialized to config.toml).
- New AdwPreferencesGroup with a 0–500px slider in the GTK
window. Default 0 (disabled) so existing users see no
behavior change until they opt in.
- New CaptureEvent::AutoRelease variant + handling in
src/capture.rs's handle_capture_event (short-circuit to
release_capture, which already synthesizes key-ups and sends
Leave to the peer).
Known limitation: the wrapper has no way to know where the
guest's cursor actually is (the guest doesn't tell us). On
re-entry into a peer mid-session, virtual_pos resets to 0 but
the guest's cursor may still be in the middle of its screen
from the prior session, causing the threshold to fire from
the wrong reference point. A protocol-level Bounds event +
cursor-warp on Enter is needed for full correctness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a new ProtoEvent variant carrying the receiving device's display geometry (in pixels). Sent by the emulation side right after acknowledging an Enter so the capturing peer can model the guest cursor's position along the entry axis. Wire format: 1-byte EventType discriminator (Bounds = 11) followed by big-endian u32 width and big-endian u32 height — 9 bytes total, well under MAX_EVENT_SIZE (21). This commit only adds the protocol wiring. Senders and the host-side cache come in subsequent commits. Old peers that don't recognize EventType=11 will skip the datagram per the forward-compat fix in the previous commit, so deployment is incremental: the emulation side can start sending Bounds without breaking older capturing peers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `display_bounds(pos)` and `warp_cursor(pos)` to the
InputEmulation trait and implement them across every backend:
- macOS: CGDisplay APIs for bounds, CGWarpMouseCursorPosition for warp
- x11: RandR for bounds, XWarpPointer for warp
- wlroots: wl_output extents + virtual_pointer.motion_absolute
- libei: region walking + ei_pointer.emit_motion_absolute
- Windows: GetSystemMetrics + SetCursorPos
- xdg_desktop_portal: no-op fallback (the protocol exposes neither
bounds nor a warp primitive)
These are the prerequisites for the protocol-based wall-press
auto-release: emulation hosts now have a common API to report their
display extents to peers and to warp the cursor on Enter so the
host's modeled virtual_pos = 0 matches the guest's actual cursor.
Wire the new emulation-side capabilities into the daemon's
listener task. When a peer's Enter arrives:
1. Reply Ack (existing behavior).
2. Reply Bounds(width, height) using the cached display
geometry from the active emulation backend.
3. Warp the local cursor to the entry edge of the displayed
position (0 for Left, width-1 for Right, etc., centered
along the orthogonal axis).
The warp is the structural fix for the "cursor jumps back to
where it was" symptom: previously, on re-entry into a peer
mid-session, the cursor stayed wherever the prior capture
session left it, breaking the host's wall-press model
(virtual_pos=0 in the host's mind didn't match the guest's
actual cursor column). With the warp, the host's model is
synchronized with the guest's reality on every Enter.
EmulationProxy gains:
- Cached display_bounds (Rc<Cell<Option<(u32, u32)>>>),
refreshed each time the underlying InputEmulation is
(re)created. Read by the listener task.
- warp_cursor(x, y) fire-and-forget. Drops if emulation
isn't currently active (no live backend to receive it).
ProxyRequest::Warp(x, y) carries the request to EmulationTask,
which dispatches to InputEmulation::warp_cursor.
If the active backend doesn't implement display_bounds — every
non-macOS backend right now — the listener skips the Bounds
reply and the warp call. The capturing peer falls back to its
existing "no upper clamp / virtual_pos = 0 on Begin" heuristic,
which is degraded but functional. Adding display_bounds /
warp_cursor to other backends unlocks correct behavior
incrementally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
InputCapture now keeps a per-position HashMap of peer display
geometry, populated when ProtoEvent::Bounds arrives from the
peer (handled in src/capture.rs's recv arm). track_wall_press
uses the cached entry-axis extent as the upper clamp for
virtual_pos:
self.virtual_pos = proposed.clamp(0.0, peer_extent);
Eliminates the runaway-virtual_pos bug from the heuristic
fallback: when the user obliviously over-pushes their physical
mouse past the guest's actual far edge, the modeled position
clamps at the real width instead of climbing fictionally to
infinity. Now the user's "walk back" cost is bounded by the
guest's actual screen width.
When the peer hasn't sent Bounds yet (older peer running
without the protocol extension, or in the brief pre-Ack
window of a fresh connection), peer_extent returns INFINITY
and the model degrades to the prior heuristic.
Cache lifecycle:
- Insert on ProtoEvent::Bounds.
- Drop on CaptureRequest::Destroy(handle) so re-adding the
same peer later starts fresh.
Combined with the previous commit (emulation warps cursor on
Enter), the host's virtual_pos = 0 at Begin now matches the
guest's actual cursor at column 0 (or width-1, etc.) on every
re-entry. The "cursor was in the middle, 200px back fires
release prematurely" bug is fixed structurally rather than
papered over.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The label "Auto-Release" reads as a global app preference; the description's "forwarded mouse capture" was ambiguous about which machine does the forwarding. Rename the group to "Outgoing Auto-Release" so the scope mirrors the surrounding "Outgoing Connections" / "Incoming Connections" groups, and lead the description with "When this machine is capturing input for a peer …" so a user scanning the window can tell at a glance that this setting only matters when the local machine is the host.
GtkScale's default behavior treats a vertical scroll event as +/- increment, which means the threshold creeps any time the user is scrolling the window and the cursor passes over the slider — easy to do given the slider sits in the middle of the preferences pane. Add an EventControllerScroll to the slider in CAPTURE phase that returns Propagation::Stop unconditionally. The scale's own scroll controller never sees the event, so the value doesn't change. Trade-off: scrolling doesn't pass through to the parent GtkScrolledWindow while the cursor is on the slider — the wheel becomes inert there. Acceptable: prior behavior was actively destructive (silent state corruption); this is just "no scroll in this small region." If users start complaining about the gap, the next step is to forward dy to the ancestor scrolled window's vadjustment manually before returning Stop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Old text described the mechanism ("releases capture automatically once
the cursor pushes past the host-adjacent edge") without explaining
when the user would actually need it. With the new peer-Leave deadline
gate (34605a7), wall-press only fires when the peer can't deliver a
Leave — i.e. when the peer's screen is locked or its capture backend
is otherwise suppressed. New text leads with that framing and trims
two sentences to two.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Capture-phase scroll handler used to return Propagation::Stop to suppress GtkScale's default scroll-to-adjust behavior, but Stop also killed propagation to the parent — so the main window wouldn't scroll when the cursor was over the slider. Frustrating because the slider sits in the middle of the preferences pane and "I just want to scroll past this" is the common interaction. Same capture-phase handler now walks up to the ancestor ScrolledWindow and bumps its vadjustment by `dy * step_increment` (or 40px when step_increment is unset). Mimics what native scroll passthrough would have done — slider value stays fixed, parent scrolls smoothly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Windows clippy flags `loop { let Some(...) = get_msg() else { break } }`
as while-let-loop. Rewrite to `while let Some(msg) = get_msg() { … }`.
The inner `break` for `RequestType::Exit` still breaks the surrounding
while-let, so semantics are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Proxy trait import needed by the wlroots backend's `output.id()` call (introduced when the emulation side started binding wl_output for display_bounds), and applies cargo fmt for this split's own files.
572b7d0 to
283a95c
Compare
This was referenced May 6, 2026
This was referenced May 6, 2026
5 tasks
`test windows-latest`'s only failure was the post-job cache-save step in `Swatinem/rust-cache@v2` (job step 27); `cargo test` itself (step 12) succeeded with all tests passing. CI rerun on upstream requires admin rights, so an empty commit is the only way to retrigger from this side.
5 tasks
Contributor
Author
|
@feschber this is the new base of the work that I split out. Sorry for the noise of closing the last stack of them. That happened automatically when I was trying to rename the branches to be in a good review order and be as atomic as possible. This particular PR is made against main, but starting with #429 you can review each of the PRs against the prior using the |
This was referenced May 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wall-press auto-release feature plus the
Boundsproto event the cursor-sync work builds on.Summary
Bounds(width, height)proto event for receivers to advertise their display dimensionsdisplay_boundsand warp the cursor on EnterTest plan
Related PRs
This PR is part of an effort to split #418 into focused, independently-reviewable pieces.
The split stack (each builds on the previous; review in order):
Plus:
fix(capture): throttle WaitingForAck Enter re-sends to one per 50ms — small standalone fix offmain, independent of the stack