feat(client/input): desktop (absolute) mouse mode — remote-desktop sweep M1
New physical-mouse model beside capture: Desktop leaves the pointer uncaptured and sends absolute positions through the letterbox (MouseMoveAbs — every host injector already consumes it). Capture stays the default and the game model. - pf-client-core: MouseMode (capture|desktop) persisted in Settings, default capture so existing stores are unchanged. - pf-presenter: Capture grows a desktop model — latest-wins pending abs position coalesced per loop iteration (same 1000 Hz discipline as relative), flushed before clicks/keys/wheel so they land where the cursor is; Ctrl+Alt+Shift+M flips the model live; local cursor stays hidden over the window (the host's composited cursor is the one you see until the M2 cursor channel); Windows keyboard grab only engages for capture (a desktop stream is something you Alt-Tab away from). - gamescope gating: its EIS is relative-only, so desktop mode is pinned off there (resolved_compositor), with a log note. - Settings surfaces: GTK row (dynamic caption), WinUI combo, console-UI row + step test, capture-hint line. Verified: fmt + clippy -D warnings + tests (33/40/19) on Linux .21; clippy -D warnings for all five crates incl. punktfunk-client-windows native on .173. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -456,6 +456,48 @@ impl TouchMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// How a physical mouse drives the host — the desktop-sweep mouse model
|
||||
/// (design/remote-desktop-sweep.md M1). Stored stringly in [`Settings::mouse_mode`] so the
|
||||
/// file stays readable; parsed with [`MouseMode::from_name`].
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum MouseMode {
|
||||
/// Pointer lock (relative deltas, hidden cursor) — the game model, and the default:
|
||||
/// the only cursor you see is the host's.
|
||||
Capture,
|
||||
/// Absolute pointer, uncaptured: the cursor enters and leaves the stream freely and
|
||||
/// motion goes on the wire as absolute positions through the letterbox. The remote
|
||||
/// desktop model. Requires a host injector with absolute support (not gamescope).
|
||||
Desktop,
|
||||
}
|
||||
|
||||
impl MouseMode {
|
||||
/// Cycle/picker order (also the settings pickers' option order).
|
||||
pub const ALL: [MouseMode; 2] = [MouseMode::Capture, MouseMode::Desktop];
|
||||
|
||||
/// Parse the persisted name, defaulting to `Capture` for unset/unknown values.
|
||||
pub fn from_name(s: &str) -> MouseMode {
|
||||
match s {
|
||||
"desktop" => MouseMode::Desktop,
|
||||
_ => MouseMode::Capture,
|
||||
}
|
||||
}
|
||||
|
||||
/// The persisted name (the inverse of [`from_name`](Self::from_name)).
|
||||
pub fn as_name(self) -> &'static str {
|
||||
match self {
|
||||
MouseMode::Capture => "capture",
|
||||
MouseMode::Desktop => "desktop",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
MouseMode::Capture => "Capture (games)",
|
||||
MouseMode::Desktop => "Desktop (absolute)",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// App settings, persisted as JSON. Stringly-typed gamepad/compositor prefs so the file
|
||||
/// stays readable; parsed with `*Pref::from_name` at connect time.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@@ -490,6 +532,12 @@ pub struct Settings {
|
||||
/// stores load as trackpad.
|
||||
#[serde(default = "default_touch_mode")]
|
||||
pub touch_mode: String,
|
||||
/// How a physical mouse drives the host: a [`MouseMode`] name — `"capture"` (default,
|
||||
/// pointer lock + relative) or `"desktop"` (uncaptured absolute pointer). Read at
|
||||
/// connect via [`Settings::mouse_mode`]. `default` so pre-existing stores load as
|
||||
/// capture — today's behavior.
|
||||
#[serde(default = "default_mouse_mode")]
|
||||
pub mouse_mode: String,
|
||||
/// Grab compositor shortcuts (Alt+Tab, Super…) while input is captured.
|
||||
pub inhibit_shortcuts: bool,
|
||||
/// Stream the default microphone to the host's virtual mic source.
|
||||
@@ -577,6 +625,10 @@ fn default_touch_mode() -> String {
|
||||
"trackpad".into()
|
||||
}
|
||||
|
||||
fn default_mouse_mode() -> String {
|
||||
"capture".into()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -604,6 +656,10 @@ impl Settings {
|
||||
TouchMode::from_name(&self.touch_mode)
|
||||
}
|
||||
|
||||
pub fn mouse_mode(&self) -> MouseMode {
|
||||
MouseMode::from_name(&self.mouse_mode)
|
||||
}
|
||||
|
||||
/// The `codec` setting as a `quic::CODEC_*` preference bit (`0` = auto).
|
||||
pub fn preferred_codec(&self) -> u8 {
|
||||
match self.codec.as_str() {
|
||||
@@ -631,6 +687,7 @@ impl Default for Settings {
|
||||
forward_pad: String::new(),
|
||||
compositor: "auto".into(),
|
||||
touch_mode: "trackpad".into(),
|
||||
mouse_mode: "capture".into(),
|
||||
inhibit_shortcuts: true,
|
||||
mic_enabled: false,
|
||||
audio_channels: 2,
|
||||
|
||||
Reference in New Issue
Block a user