feat(viewport): optional touchpad pan for wheel-scrolling touchpads

Outside macOS a touchpad's two-finger drag arrives as wheel scrolls that
nothing tells from a wheel. A new option (Options, off by default) makes
every scroll pan and Ctrl+scroll — what a pinch sends there — zoom.

Fixes #1560

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-08 11:07:37 +03:00
1 parent 17d2954098
commit 69057c55da
29 files changed
+83 -11

No files matched your search

+2
View File
@@ -662,6 +662,7 @@ impl OpenCADStudio {
polar: self.polar_mode,
polar_increment_deg: self.polar_increment_deg,
zoom_wheel_reversed: self.zoom_wheel_reversed,
touchpad_pan: self.touchpad_pan,
zoom_factor: self.zoom_factor,
cursor_size: self.cursor_size,
pick_box: self.pick_box,
@@ -762,6 +763,7 @@ impl OpenCADStudio {
self.polar_mode = s.polar;
self.polar_increment_deg = s.polar_increment_deg;
self.zoom_wheel_reversed = s.zoom_wheel_reversed;
self.touchpad_pan = s.touchpad_pan;
self.zoom_factor = crate::app::settings::clamp_zoom_factor(s.zoom_factor);
// Like the drafting-rotation field below, the Options field edits a
// buffer rather than the value, so it is reseeded whenever the value
+6
View File
@@ -8383,6 +8383,12 @@ impl OpenCADStudio {
Task::none()
}
Message::TouchpadPanChanged(enabled) => {
self.touchpad_pan = enabled;
self.persist_settings_if_changed();
Task::none()
}
Message::ZoomFactorChanged(factor) => {
self.zoom_factor = crate::app::settings::clamp_zoom_factor(factor);
// The field beside the slider reads the same value, so the
+33 -11
View File
@@ -42,9 +42,6 @@ enum ScrollIntent {
Zoom { notches: f32 },
/// Trackpad movement to pan by, in screen pixels.
///
/// Built only where a pixel delta can only mean a trackpad, so it has no
/// constructor on the other targets.
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
Pan { dx: f32, dy: f32 },
}
@@ -55,11 +52,22 @@ const ZOOM_DIR_FLIP_PX: f32 = 2.0;
/// A wheel notch zooms; a trackpad's two fingers pan.
///
/// A wheel reports notches, a precise-scrolling device reports pixels, and that
/// is the whole distinction — there is no modifier to check and no setting to
/// read. The pixels only mean "trackpad" on macOS: the browser build reports
/// every wheel notch as pixels too, so everywhere else they keep zooming.
fn scroll_intent(delta: mouse::ScrollDelta) -> ScrollIntent {
/// On macOS a wheel reports notches and a trackpad pixels, and that is the
/// whole distinction. Elsewhere a touchpad's two-finger drag arrives as wheel
/// notches (the browser build reports every wheel as pixels), so nothing tells
/// it from a wheel: with `touchpad_pan` on, every scroll pans and Ctrl+scroll —
/// what a pinch sends there — zooms.
fn scroll_intent(delta: mouse::ScrollDelta, touchpad_pan: bool, ctrl: bool) -> ScrollIntent {
/// Screen pixels one wheel line pans.
const LINE_PX: f32 = 40.0;
if touchpad_pan && !ctrl {
return match delta {
mouse::ScrollDelta::Pixels { x, y } => ScrollIntent::Pan { dx: x, dy: y },
mouse::ScrollDelta::Lines { x, y } => {
ScrollIntent::Pan { dx: x * LINE_PX, dy: y * LINE_PX }
}
};
}
match delta {
// Both axes go straight through: the two-finger gesture moves the
// drawing the same way the middle-button drag moves it. If a pan ever
@@ -5673,7 +5681,7 @@ properties={:.1}ms picked={}",
/// A wheel notch zooms; a trackpad's two fingers pan. Which one a delta
/// means is `scroll_intent`'s call.
pub(super) fn on_viewport_scroll(&mut self, delta: mouse::ScrollDelta) -> Task<Message> {
match scroll_intent(delta) {
match scroll_intent(delta, self.touchpad_pan, self.ctrl_down) {
ScrollIntent::Zoom { notches } => {
let mut s =
notches * crate::app::settings::zoom_notch_steps(self.zoom_factor);
@@ -6701,10 +6709,24 @@ mod scroll_intent_tests {
use crate::scene::view::camera::Camera;
use iced::mouse::ScrollDelta;
/// With the touchpad option, a scroll pans (wheel lines as pixels) and
/// Ctrl+scroll — a pinch — still zooms.
#[test]
fn touchpad_pan_scrolls_pan_and_ctrl_zooms() {
assert_eq!(
scroll_intent(ScrollDelta::Lines { x: 0.5, y: -0.25 }, true, false),
ScrollIntent::Pan { dx: 20.0, dy: -10.0 }
);
assert_eq!(
scroll_intent(ScrollDelta::Lines { x: 0.0, y: 1.0 }, true, true),
ScrollIntent::Zoom { notches: 1.0 }
);
}
#[test]
fn wheel_notches_zoom() {
assert_eq!(
scroll_intent(ScrollDelta::Lines { x: 0.0, y: 1.0 }),
scroll_intent(ScrollDelta::Lines { x: 0.0, y: 1.0 }, false, false),
ScrollIntent::Zoom { notches: 1.0 }
);
}
@@ -6715,7 +6737,7 @@ mod scroll_intent_tests {
#[test]
fn two_finger_scroll_pans() {
assert_eq!(
scroll_intent(ScrollDelta::Pixels { x: 4.0, y: 6.0 }),
scroll_intent(ScrollDelta::Pixels { x: 4.0, y: 6.0 }, false, false),
ScrollIntent::Pan { dx: 4.0, dy: 6.0 }
);
}