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,
},
touch_mode: settings_at_start.touch_mode(),
invert_scroll: settings_at_start.invert_scroll,
json_status,
on_connected: Some(Box::new(move |fingerprint: [u8; 32]| {
let fp_hex = trust::hex(&fingerprint);
+1
View File
@@ -373,6 +373,7 @@ mod session_main {
v => v,
},
touch_mode: settings.touch_mode(),
invert_scroll: settings.invert_scroll,
json_status: true,
on_connected: Some(Box::new(|fingerprint: [u8; 32]| {
// 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| {
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(
ctx,
"Capture system shortcuts (Alt+Tab, Win, \u{2026})",
@@ -523,11 +527,17 @@ pub(crate) fn settings_page(
);
out.extend(group(
Some("Keyboard & mouse"),
vec![described(
shortcuts_toggle,
"Alt+Tab, the Windows key and friends reach the host while the stream has \
input captured. Off, they act on this machine instead.",
)],
vec![
described(
shortcuts_toggle,
"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,
));
("Input", out)
+5
View File
@@ -533,6 +533,10 @@ pub struct Settings {
/// wait only adds a delay.
#[serde(default = "default_true")]
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
/// 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
@@ -622,6 +626,7 @@ impl Default for Settings {
fullscreen_on_stream: true,
library_enabled: false,
auto_wake: true,
invert_scroll: false,
match_window: false,
last_window_w: 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
/// gesture state machine finger events feed.
touch_mode: TouchMode,
/// Reverse the scroll direction sent to the host ([`Settings::invert_scroll`]).
invert_scroll: bool,
gestures: Gestures,
}
@@ -68,7 +70,11 @@ fn send(connector: &NativeClient, kind: InputKind, code: u32, x: i32, y: i32, fl
}
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 {
connector,
captured: false,
@@ -79,6 +85,7 @@ impl Capture {
scroll_acc: (0.0, 0.0),
touch_slots: HashMap::new(),
touch_mode,
invert_scroll,
gestures: Gestures::new(touch_mode == TouchMode::Trackpad),
}
}
@@ -194,9 +201,10 @@ impl Capture {
return;
}
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;
ay += f64::from(dy) * 120.0;
ax += f64::from(dx) * 120.0;
ay += f64::from(dy) * 120.0 * sign;
ax += f64::from(dx) * 120.0 * sign;
let vy = ay.trunc() as i32;
if vy != 0 {
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
/// session — a mouse-only client leaves this at the default and never sees a finger.
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.
pub json_status: bool,
/// 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();
gamepad.attach(c.clone());
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)
apply_capture(&mut window, &mouse, true);
st.capture = Some(cap);