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:
@@ -8,7 +8,8 @@
|
||||
//! every iteration so loss self-heals with no refresh timer.
|
||||
|
||||
use punktfunk_core::quic::{
|
||||
encode_cursor_state_datagram, CursorShape, CursorState, CURSOR_SHAPE_MAX_SIDE, CURSOR_VISIBLE,
|
||||
encode_cursor_state_datagram, CursorShape, CursorState, CURSOR_RELATIVE_HINT,
|
||||
CURSOR_SHAPE_MAX_SIDE, CURSOR_VISIBLE,
|
||||
};
|
||||
|
||||
/// Per-session forward state, owned by the encode loop (the thread that binds frames).
|
||||
@@ -30,7 +31,10 @@ impl CursorForwarder {
|
||||
|
||||
/// Called once per encode-loop iteration with the bound frame's overlay (also on repeat
|
||||
/// iterations — the state datagram is the plane's loss heal, so it goes out every tick).
|
||||
/// `None` overlay = hidden pointer (or no bitmap yet): state only, `visible` clear.
|
||||
/// Flag mapping (M3): a visible overlay states VISIBLE; a hidden-but-known overlay
|
||||
/// (an app grabbed/hid the pointer) states RELATIVE_HINT — the client should flip to
|
||||
/// captured relative; `None` (no bitmap has EVER arrived) states neither — never hint
|
||||
/// off a cold start, only off an observed hide.
|
||||
pub(super) fn tick(
|
||||
&mut self,
|
||||
cursor: Option<&pf_frame::CursorOverlay>,
|
||||
@@ -38,7 +42,7 @@ impl CursorForwarder {
|
||||
shape_tx: &tokio::sync::mpsc::UnboundedSender<CursorShape>,
|
||||
) {
|
||||
let flags = match cursor {
|
||||
Some(ov) => {
|
||||
Some(ov) if ov.visible => {
|
||||
if self.sent_serial != Some(ov.serial) {
|
||||
if let Some(shape) = shape_from_overlay(ov) {
|
||||
// Bridge full ⇒ control task gone ⇒ session is tearing down anyway.
|
||||
@@ -49,6 +53,7 @@ impl CursorForwarder {
|
||||
self.last_pos = (ov.x + ov.hot_x as i32, ov.y + ov.hot_y as i32);
|
||||
CURSOR_VISIBLE
|
||||
}
|
||||
Some(_) => CURSOR_RELATIVE_HINT,
|
||||
None => 0,
|
||||
};
|
||||
let state = CursorState {
|
||||
@@ -111,6 +116,7 @@ mod tests {
|
||||
serial: 3,
|
||||
hot_x: hot.0,
|
||||
hot_y: hot.1,
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2007,10 +2007,16 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
||||
}
|
||||
// Cursor channel (M2): every iteration — new frame OR repeat — states the pointer
|
||||
// (self-healing under datagram loss) and forwards a changed shape via the control
|
||||
// bridge. `frame` is the newest bound frame either way.
|
||||
// bridge. `frame` is the newest bound frame either way. A hidden-but-known pointer
|
||||
// (overlay with `visible: false`) is the M3 relative-mode hint.
|
||||
if let Some(fwd) = cursor_fwd.as_mut() {
|
||||
fwd.tick(frame.cursor.as_ref(), &conn, &cursor_shape_tx);
|
||||
}
|
||||
// The overlay now surfaces hidden pointers too (for the hint above) — strip them
|
||||
// HERE, after forwarding, so no blend path ever draws an invisible cursor.
|
||||
if frame.cursor.as_ref().is_some_and(|c| !c.visible) {
|
||||
frame.cursor = None;
|
||||
}
|
||||
if perf && diag_at.elapsed() >= std::time::Duration::from_secs(2) {
|
||||
let secs = diag_at.elapsed().as_secs_f64();
|
||||
tracing::info!(
|
||||
|
||||
Reference in New Issue
Block a user