feat(client/windows): invert scroll direction

The Apple client's toggle, wired through the shared session presenter:
Settings::invert_scroll -> SessionOpts -> Capture, applied at the single
seam where wheel deltas enter (before accumulation, so the fractional
remainders stay consistent with what was actually sent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 17:29:07 +02:00
parent 7a1809547b
commit 51f7ff1b3f
6 changed files with 36 additions and 9 deletions
+1
View File
@@ -158,6 +158,7 @@ pub fn run(target: Option<&str>) -> u8 {
v => v, v => v,
}, },
touch_mode: settings_at_start.touch_mode(), touch_mode: settings_at_start.touch_mode(),
invert_scroll: settings_at_start.invert_scroll,
json_status, json_status,
on_connected: Some(Box::new(move |fingerprint: [u8; 32]| { on_connected: Some(Box::new(move |fingerprint: [u8; 32]| {
let fp_hex = trust::hex(&fingerprint); let fp_hex = trust::hex(&fingerprint);
+1
View File
@@ -373,6 +373,7 @@ mod session_main {
v => v, v => v,
}, },
touch_mode: settings.touch_mode(), touch_mode: settings.touch_mode(),
invert_scroll: settings.invert_scroll,
json_status: true, json_status: true,
on_connected: Some(Box::new(|fingerprint: [u8; 32]| { on_connected: Some(Box::new(|fingerprint: [u8; 32]| {
// This host's card carries the accent bar in the desktop client now. // This host's card carries the accent bar in the desktop client now.
+15 -5
View File
@@ -380,6 +380,10 @@ pub(crate) fn settings_page(
let touch_combo = setting_combo(ctx, "Touch input", touch_names, touch_i, |s, i| { let touch_combo = setting_combo(ctx, "Touch input", touch_names, touch_i, |s, i| {
s.touch_mode = TOUCH_MODES[i].0.to_string(); s.touch_mode = TOUCH_MODES[i].0.to_string();
}); });
let invert_scroll_toggle =
setting_toggle(ctx, "Invert scroll direction", s.invert_scroll, |s, on| {
s.invert_scroll = on
});
let shortcuts_toggle = setting_toggle( let shortcuts_toggle = setting_toggle(
ctx, ctx,
"Capture system shortcuts (Alt+Tab, Win, \u{2026})", "Capture system shortcuts (Alt+Tab, Win, \u{2026})",
@@ -523,11 +527,17 @@ pub(crate) fn settings_page(
); );
out.extend(group( out.extend(group(
Some("Keyboard & mouse"), Some("Keyboard & mouse"),
vec![described( vec![
shortcuts_toggle, described(
"Alt+Tab, the Windows key and friends reach the host while the stream has \ shortcuts_toggle,
input captured. Off, they act on this machine instead.", "Alt+Tab, the Windows key and friends reach the host while the stream \
)], has input captured. Off, they act on this machine instead.",
),
described(
invert_scroll_toggle,
"Reverses the wheel and trackpad scroll direction sent to the host.",
),
],
None, None,
)); ));
("Input", out) ("Input", out)
+5
View File
@@ -533,6 +533,10 @@ pub struct Settings {
/// wait only adds a delay. /// wait only adds a delay.
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub auto_wake: bool, pub auto_wake: bool,
/// Reverse the wheel/trackpad scroll direction sent to the host (the Apple client's
/// "Invert scroll direction"). Default off = the host scrolls the way this machine does.
#[serde(default)]
pub invert_scroll: bool,
/// Match-window resolution policy (design/midstream-resolution-resize.md D1): the /// Match-window resolution policy (design/midstream-resolution-resize.md D1): the
/// stream mode follows the session window — the connect asks for the window's pixel /// stream mode follows the session window — the connect asks for the window's pixel
/// size and a mid-session resize renegotiates the host's virtual display + encoder /// size and a mid-session resize renegotiates the host's virtual display + encoder
@@ -622,6 +626,7 @@ impl Default for Settings {
fullscreen_on_stream: true, fullscreen_on_stream: true,
library_enabled: false, library_enabled: false,
auto_wake: true, auto_wake: true,
invert_scroll: false,
match_window: false, match_window: false,
last_window_w: 0, last_window_w: 0,
last_window_h: 0, last_window_h: 0,
+11 -3
View File
@@ -53,6 +53,8 @@ pub struct Capture {
/// The touchscreen input model for this session, and — for trackpad/pointer — the /// The touchscreen input model for this session, and — for trackpad/pointer — the
/// gesture state machine finger events feed. /// gesture state machine finger events feed.
touch_mode: TouchMode, touch_mode: TouchMode,
/// Reverse the scroll direction sent to the host ([`Settings::invert_scroll`]).
invert_scroll: bool,
gestures: Gestures, gestures: Gestures,
} }
@@ -68,7 +70,11 @@ fn send(connector: &NativeClient, kind: InputKind, code: u32, x: i32, y: i32, fl
} }
impl Capture { impl Capture {
pub fn new(connector: Arc<NativeClient>, touch_mode: TouchMode) -> Capture { pub fn new(
connector: Arc<NativeClient>,
touch_mode: TouchMode,
invert_scroll: bool,
) -> Capture {
Capture { Capture {
connector, connector,
captured: false, captured: false,
@@ -79,6 +85,7 @@ impl Capture {
scroll_acc: (0.0, 0.0), scroll_acc: (0.0, 0.0),
touch_slots: HashMap::new(), touch_slots: HashMap::new(),
touch_mode, touch_mode,
invert_scroll,
gestures: Gestures::new(touch_mode == TouchMode::Trackpad), gestures: Gestures::new(touch_mode == TouchMode::Trackpad),
} }
} }
@@ -194,9 +201,10 @@ impl Capture {
return; return;
} }
self.flush_motion(); // scroll happens at the latest cursor position self.flush_motion(); // scroll happens at the latest cursor position
let sign = if self.invert_scroll { -1.0 } else { 1.0 };
let (mut ax, mut ay) = self.scroll_acc; let (mut ax, mut ay) = self.scroll_acc;
ay += f64::from(dy) * 120.0; ay += f64::from(dy) * 120.0 * sign;
ax += f64::from(dx) * 120.0; ax += f64::from(dx) * 120.0 * sign;
let vy = ay.trunc() as i32; let vy = ay.trunc() as i32;
if vy != 0 { if vy != 0 {
ay -= f64::from(vy); ay -= f64::from(vy);
+3 -1
View File
@@ -48,6 +48,8 @@ pub struct SessionOpts {
/// `Pointer` (absolute cursor), or `Touch` (real multi-touch passthrough). Latched per /// `Pointer` (absolute cursor), or `Touch` (real multi-touch passthrough). Latched per
/// session — a mouse-only client leaves this at the default and never sees a finger. /// session — a mouse-only client leaves this at the default and never sees a finger.
pub touch_mode: TouchMode, pub touch_mode: TouchMode,
/// Reverse the scroll direction sent to the host ([`Settings::invert_scroll`]).
pub invert_scroll: bool,
/// Emit the `{"ready":true}` stdout line after the first presented frame. /// Emit the `{"ready":true}` stdout line after the first presented frame.
pub json_status: bool, pub json_status: bool,
/// Called once on `Connected` with the host's fingerprint (trust persistence is the /// Called once on `Connected` with the host's fingerprint (trust persistence is the
@@ -811,7 +813,7 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
.ok(); .ok();
gamepad.attach(c.clone()); gamepad.attach(c.clone());
st.clock_offset = Some(c.clock_offset_shared()); st.clock_offset = Some(c.clock_offset_shared());
let mut cap = Capture::new(c.clone(), opts.touch_mode); let mut cap = Capture::new(c.clone(), opts.touch_mode, opts.invert_scroll);
cap.engage(); // capture engages when the stream starts (ui_stream parity) cap.engage(); // capture engages when the stream starts (ui_stream parity)
apply_capture(&mut window, &mouse, true); apply_capture(&mut window, &mouse, true);
st.capture = Some(cap); st.capture = Some(cap);