feat(host+client): host-driven mouse-model flip — remote-desktop sweep M3

The full Parsec model: launching a game from a desktop session flips the
client into captured relative automatically, and back — no chords.

- Capture overlay now distinguishes 'hidden' from 'no cursor yet':
  CursorOverlay grows a visible flag, overlay() returns Some whenever a
  bitmap is known (the encode loop strips invisible overlays after
  forwarding so no blend path ever draws one; the CPU composite guarded
  on visibility already).
- Host forwarder maps it onto the reserved wire bit: visible ⇒ VISIBLE,
  hidden-but-known ⇒ RELATIVE_HINT (an app grabbed/hid the pointer),
  never any hint before the first bitmap — a cold start can't flip a
  desktop session into capture.
- Presenter: edge-triggered auto-flip on hint changes via the new
  Capture::set_desktop (chord semantics untouched); a manual ⌃⌥⇧M sets
  an override latch that holds until the HOST's intent next changes, so
  the hint never fights the user. Leaving relative warps the local
  cursor to the host's last pointer position through the new
  content_to_window inverse-letterbox mapping (unit-tested against
  finger_to_content) — the hand-back is seamless.

Verified on .21: fmt + clippy -D warnings + tests (presenter 20 incl.
the inverse-mapping roundtrip, host 245).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:43:23 +02:00
co-authored by Claude Fable 5
parent 4a01bc4463
commit c3cbffe662
7 changed files with 166 additions and 17 deletions
+14
View File
@@ -133,6 +133,20 @@ impl Capture {
Some(self.desktop)
}
/// Set the mouse model directly (the M3 host-driven flip — `relative_hint` says a host
/// app grabbed/hid the pointer, so run relative; hint clear = back to absolute). Same
/// gating and motion hygiene as [`toggle_desktop`](Self::toggle_desktop); returns whether
/// the model actually changed.
pub fn set_desktop(&mut self, on: bool) -> bool {
if !self.abs_ok || self.desktop == on {
return false;
}
self.desktop = on;
self.pending_rel = (0, 0);
self.pending_abs = None;
true
}
/// Whether a regained focus should re-engage: yes unless the user released
/// deliberately (the chord keeps its meaning across an Alt-Tab).
pub fn should_reengage(&self) -> bool {