mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 11:11:30 +00:00
Merge pull request #6360 from dotCooCoo/fix/touchpad-ctrl-zoom
Canvas: Cmd/Ctrl-wheel input finer than a whole notch zooms; only whole notches scroll sideways
This commit is contained in:
@@ -4645,7 +4645,7 @@ fn wheel_over_a_numeric_panel_field_does_not_start_a_canvas_zoom_glide() {
|
||||
/// `ui.wheel`: with Zoom with Mouse Wheel off, Cmd/Ctrl-wheel zooms about the pointer while the
|
||||
/// option is off and scrolls sideways while it is on, and a trackpad pinch, two fingers spread on a touch
|
||||
/// screen, Alt + Cmd/Ctrl-wheel and Alt-wheel still zoom. A Cmd/Ctrl-wheel notch with a plain
|
||||
/// one right after it scrolls two notches in all, and a trackpad gesture during which Cmd/Ctrl
|
||||
/// one right after it scrolls two notches in all, and a wheel gesture during which Cmd/Ctrl
|
||||
/// is pressed and released scrolls sideways until the gesture ends, while an Alt gesture that
|
||||
/// pauses past egui's scroll timeout no longer holds Alt for the next move. With Zoom with
|
||||
/// Mouse Wheel also on, Cmd/Ctrl-wheel scrolls sideways and the wheel zooms.
|
||||
@@ -4747,16 +4747,16 @@ fn cmd_wheel_scrolls_sideways_preference() {
|
||||
(zoom - 1.25).abs() < 1e-6 && moved.x > notch - 0.5 && moved.x < 1.25 * notch + 0.5 && moved.y.abs() < 1e-3,
|
||||
"on: a pinch during a Cmd-wheel notch zooms by the pinch, and the notch scrolls sideways ({zoom}, {moved:?})"
|
||||
);
|
||||
// A trackpad gesture starts with no modifier, and Cmd is pressed and released during it:
|
||||
// egui keeps Cmd in its wheel state until the gesture ends, and the canvas keeps scrolling
|
||||
// sideways until then.
|
||||
// A gesture egui names as a wheel starts with no modifier, and Cmd is pressed and released
|
||||
// during it: egui keeps Cmd in its wheel state until the gesture ends, and the canvas keeps
|
||||
// scrolling sideways until then.
|
||||
let before = *app.view().unwrap();
|
||||
let swipe = |phase, modifiers| egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(0.0, 6.0),
|
||||
phase,
|
||||
modifiers,
|
||||
source: egui::MouseWheelSource::Unknown,
|
||||
source: egui::MouseWheelSource::Wheel,
|
||||
};
|
||||
app.synthetic.extend([
|
||||
egui::Event::PointerMoved(at),
|
||||
@@ -4772,7 +4772,7 @@ fn cmd_wheel_scrolls_sideways_preference() {
|
||||
let scroll = (after.center - before.center) * after.zoom;
|
||||
assert!(
|
||||
(after.zoom / before.zoom - 1.0).abs() < 1e-9 && (scroll.x + 12.0).abs() < 0.01 && scroll.y == 0.0,
|
||||
"on: a trackpad gesture scrolls sideways to its end after Cmd is let go ({}, {scroll:?})",
|
||||
"on: a wheel gesture scrolls sideways to its end after Cmd is let go ({}, {scroll:?})",
|
||||
after.zoom / before.zoom
|
||||
);
|
||||
// An Alt-wheel gesture pauses past egui's scroll timeout with no frame in between: egui
|
||||
@@ -4820,6 +4820,79 @@ fn cmd_wheel_scrolls_sideways_preference() {
|
||||
assert!(zooms_in(turn(&mut app, "")), "with Zoom with Mouse Wheel: the wheel zooms in about the pointer");
|
||||
}
|
||||
|
||||
/// With Scroll Sideways with Cmd/Ctrl + Wheel on, Cmd/Ctrl with a touchpad zooms about the pointer
|
||||
/// while whole line notches scroll sideways: a trackpad or point source, line deltas smaller than
|
||||
/// a notch, and a whole notch later in the same turn. With Zoom with Mouse Wheel on, Cmd/Ctrl with
|
||||
/// a touchpad zooms too.
|
||||
#[test]
|
||||
fn cmd_with_a_touchpad_zooms_and_a_whole_notch_scrolls_sideways() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let mut t = 0.0;
|
||||
let mut run = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||||
t += 1.0 / 60.0;
|
||||
let raw = egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))),
|
||||
time: Some(t),
|
||||
events,
|
||||
..Default::default()
|
||||
};
|
||||
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
run(&mut app, vec![]);
|
||||
let rect = app.canvas_rect.unwrap();
|
||||
let at = rect.center() + vec2(60.0, 40.0);
|
||||
let ev = |unit, dy: f32, source| egui::Event::MouseWheel {
|
||||
unit,
|
||||
delta: vec2(0.0, dy),
|
||||
phase: egui::TouchPhase::Move,
|
||||
modifiers: egui::Modifiers::COMMAND,
|
||||
source,
|
||||
};
|
||||
// Sends each event on its own frame, then lets egui finish and end the turn:
|
||||
// → (zoom ratio, scroll in points, how far the point under `at` moved).
|
||||
let mut turn = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||||
let before = *app.view().unwrap();
|
||||
run(app, vec![egui::Event::PointerMoved(at)]);
|
||||
for e in events {
|
||||
run(app, vec![e]);
|
||||
}
|
||||
for _ in 0..40 {
|
||||
run(app, vec![]);
|
||||
}
|
||||
let after = *app.view().unwrap();
|
||||
let moved = Xf::new(rect, &after).to_doc(at).distance(Xf::new(rect, &before).to_doc(at));
|
||||
(after.zoom / before.zoom, (after.center - before.center) * after.zoom, moved)
|
||||
};
|
||||
use egui::{MouseWheelSource as S, MouseWheelUnit as U};
|
||||
let zooms_in = |(zoom, _, moved): (f64, _, f64)| zoom > 1.0005 && moved < 1e-6;
|
||||
let sideways = |(zoom, scroll, _): (f64, vectorcraft_geom::Vec2, f64)| (zoom - 1.0).abs() < 1e-9 && scroll.x < -1.0 && scroll.y == 0.0;
|
||||
assert!(app.session.prefs.ctrl_wheel_scrolls_sideways, "the option is on by default");
|
||||
// A wheel message of 30 is a quarter of a 120 notch, and arrives as a quarter line.
|
||||
assert!(zooms_in(turn(&mut app, vec![ev(U::Line, 0.25, S::Unknown)])), "a line delta smaller than a notch zooms");
|
||||
assert!(zooms_in(turn(&mut app, vec![ev(U::Line, 0.25, S::Unknown), ev(U::Line, 0.25, S::Unknown)])), "several small deltas zoom");
|
||||
assert!(zooms_in(turn(&mut app, vec![ev(U::Line, 0.25, S::Unknown), ev(U::Line, 1.0, S::Unknown)])), "a whole notch in the same turn zooms");
|
||||
assert!(zooms_in(turn(&mut app, vec![ev(U::Point, 6.0, S::Unknown)])), "point deltas of an unnamed source zoom");
|
||||
assert!(zooms_in(turn(&mut app, vec![ev(U::Point, 6.0, S::Trackpad)])), "a trackpad scroll zooms");
|
||||
assert!(zooms_in(turn(&mut app, vec![ev(U::Point, 6.0, S::Momentum)])), "its momentum zooms");
|
||||
assert!(sideways(turn(&mut app, vec![ev(U::Line, 1.0, S::Unknown)])), "a whole notch scrolls sideways");
|
||||
assert!(sideways(turn(&mut app, vec![ev(U::Line, 2.0, S::Unknown)])), "a turn of two notches scrolls sideways");
|
||||
assert!(sideways(turn(&mut app, vec![ev(U::Line, 0.25, S::Wheel)])), "a part of a line from a named wheel scrolls sideways");
|
||||
// A turn ends when the wheel goes idle, so the next whole notch scrolls sideways again.
|
||||
assert!(zooms_in(turn(&mut app, vec![ev(U::Line, 0.25, S::Unknown)])), "a touchpad turn");
|
||||
assert!(sideways(turn(&mut app, vec![ev(U::Line, 1.0, S::Unknown)])), "the notch after it scrolls sideways");
|
||||
app.session.execute("prefs.set", &json!({"key": "zoomWithMouseWheel", "value": true})).unwrap();
|
||||
assert!(zooms_in(turn(&mut app, vec![ev(U::Line, 0.25, S::Unknown)])), "with Zoom with Mouse Wheel: a small line delta zooms");
|
||||
assert!(
|
||||
zooms_in(turn(&mut app, vec![ev(U::Line, 0.25, S::Unknown), ev(U::Line, 1.0, S::Unknown)])),
|
||||
"with Zoom with Mouse Wheel: a whole notch in the same turn zooms"
|
||||
);
|
||||
assert!(zooms_in(turn(&mut app, vec![ev(U::Point, 6.0, S::Trackpad)])), "with Zoom with Mouse Wheel: a trackpad scroll zooms");
|
||||
assert!(sideways(turn(&mut app, vec![ev(U::Line, 1.0, S::Unknown)])), "with Zoom with Mouse Wheel: a whole notch scrolls sideways");
|
||||
}
|
||||
|
||||
/// With Scroll Sideways with Cmd/Ctrl + Wheel on and Animated Zoom off, Alt + Cmd-wheel zooms
|
||||
/// about the pointer by egui's Cmd-wheel zoom, as Cmd-wheel does with the option off, and
|
||||
/// Cmd-wheel alone scrolls sideways.
|
||||
|
||||
@@ -65,6 +65,9 @@ pub(super) struct WheelTurn {
|
||||
/// When the last wheel event came (`InputState::time`), and whether a trackpad sent it.
|
||||
last: f64,
|
||||
trackpad: bool,
|
||||
/// A wheel event of this turn came from a touchpad ([`fine_steps`]), so Cmd/Ctrl zooms for the
|
||||
/// rest of the turn, also through a whole notch.
|
||||
fine: bool,
|
||||
/// The zoom (natural log) wheel notches still have to glide in.
|
||||
pub(super) glide: f64,
|
||||
}
|
||||
@@ -351,11 +354,26 @@ struct ThumbDrag {
|
||||
max: f32,
|
||||
}
|
||||
|
||||
/// Whether a wheel event comes from a touchpad and not from a mouse wheel's notches. egui names
|
||||
/// the source on the Mac and reports `Unknown` elsewhere. There, point deltas (Wayland) and line
|
||||
/// deltas that are not whole notches (Windows and X11) count as a touchpad. A browser reports a
|
||||
/// mouse wheel in points, so the web build counts only line deltas that are not whole notches.
|
||||
fn fine_steps(unit: egui::MouseWheelUnit, delta: egui::Vec2, source: egui::MouseWheelSource) -> bool {
|
||||
let notch = |d: f32| (d - d.round()).abs() < 1e-3;
|
||||
match source {
|
||||
egui::MouseWheelSource::Trackpad | egui::MouseWheelSource::Momentum => true,
|
||||
egui::MouseWheelSource::Wheel => false,
|
||||
egui::MouseWheelSource::Unknown if unit == egui::MouseWheelUnit::Point => !cfg!(target_arch = "wasm32"),
|
||||
egui::MouseWheelSource::Unknown => !notch(delta.x) || !notch(delta.y),
|
||||
}
|
||||
}
|
||||
|
||||
/// What the wheel, a pinch and a two-finger drag on a touch screen did over the canvas this frame:
|
||||
/// a zoom factor (about the pointer) and a scroll (screen points the content moves). The wheel scrolls, Cmd- and Alt-wheel (Option
|
||||
/// on the Mac) zoom; with General › Scroll Sideways with Cmd/Ctrl + Wheel, Cmd/Ctrl-wheel scrolls
|
||||
/// sideways instead, and Alt + Cmd/Ctrl-wheel still zooms. With General › Zoom with Mouse Wheel the
|
||||
/// wheel and Alt-wheel zoom, Shift-wheel scrolls up and down and Cmd/Ctrl-wheel sideways. `line` and `page`: points
|
||||
/// wheel and Alt-wheel zoom, Shift-wheel scrolls up and down and Cmd/Ctrl-wheel sideways. Cmd/Ctrl
|
||||
/// with a touchpad ([`fine_steps`]) zooms under either preference. `line` and `page`: points
|
||||
/// per wheel line and page. `turn`: kept by the caller across frames. With Performance › Animated
|
||||
/// Zoom (`animate`) a mouse wheel's notches zoom in a glide over a few frames, as scrolling does
|
||||
/// (#888); off, each notch zooms on the frame it arrives (#4283). A trackpad's fine steps zoom at
|
||||
@@ -385,7 +403,7 @@ pub(super) fn wheel(
|
||||
// `turn.alt` follows `turn.held` after each Start or Move, so a trackpad gesture that held
|
||||
// Alt keeps zooming until it ends, also after Alt is let go.
|
||||
for e in &i.events {
|
||||
if let egui::Event::MouseWheel { phase, modifiers, source, .. } = e {
|
||||
if let egui::Event::MouseWheel { unit, delta, phase, modifiers, source, .. } = e {
|
||||
// A wheel event that comes after egui's scroll timeout ends the turn first, as
|
||||
// `on_wheel_event` does (1 s for fingers resting on a trackpad mid-gesture, else
|
||||
// 0.15 s), also when no frame ran in between.
|
||||
@@ -393,9 +411,11 @@ pub(super) fn wheel(
|
||||
if i.time - turn.last > timeout {
|
||||
turn.gesture = false;
|
||||
turn.held = egui::Modifiers::NONE;
|
||||
turn.fine = false;
|
||||
}
|
||||
turn.last = i.time;
|
||||
turn.trackpad = *source == egui::MouseWheelSource::Trackpad;
|
||||
turn.fine |= fine_steps(*unit, *delta, *source);
|
||||
match phase {
|
||||
egui::TouchPhase::Start => {
|
||||
turn.gesture = true;
|
||||
@@ -413,6 +433,7 @@ pub(super) fn wheel(
|
||||
egui::TouchPhase::End | egui::TouchPhase::Cancel => {
|
||||
turn.gesture = false;
|
||||
turn.held = egui::Modifiers::NONE;
|
||||
turn.fine = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,6 +441,7 @@ pub(super) fn wheel(
|
||||
if !i.is_scrolling() {
|
||||
turn.gesture = false;
|
||||
turn.held = egui::Modifiers::NONE;
|
||||
turn.fine = false;
|
||||
}
|
||||
let (mut zoom, mut scroll) = (f64::from(i.zoom_delta()), i.smooth_scroll_delta);
|
||||
// Scroll Sideways with Cmd/Ctrl + Wheel: while egui's wheel state holds Cmd/Ctrl without
|
||||
@@ -427,7 +449,7 @@ pub(super) fn wheel(
|
||||
// default speed, which the app keeps. The canvas scrolls sideways by those points instead.
|
||||
// A trackpad or web pinch still zooms. With fingers on a touch screen, egui's zoom is the
|
||||
// fingers' alone, and the canvas leaves it as it is.
|
||||
if prefs.ctrl_wheel_scrolls_sideways && turn.held.command && !turn.held.alt && i.multi_touch().is_none() {
|
||||
if prefs.ctrl_wheel_scrolls_sideways && turn.held.command && !turn.held.alt && !turn.fine && i.multi_touch().is_none() {
|
||||
let pinch: f64 = i.events.iter().filter_map(|e| if let egui::Event::Zoom(f) = e { Some(f64::from(*f)) } else { None }).product();
|
||||
let wheel_zoom = zoom / pinch;
|
||||
if wheel_zoom.is_finite() && wheel_zoom > 0.0 {
|
||||
@@ -461,9 +483,15 @@ pub(super) fn wheel(
|
||||
let mut scroll = pan;
|
||||
for e in &i.events {
|
||||
match e {
|
||||
egui::Event::MouseWheel { unit, delta, modifiers, .. } => {
|
||||
egui::Event::MouseWheel { unit, delta, modifiers, source, .. } => {
|
||||
let d = points(unit, *delta);
|
||||
if modifiers.command {
|
||||
// The turn ends after egui's scroll timeout, as above.
|
||||
if i.time - turn.last > 0.150 {
|
||||
turn.fine = false;
|
||||
}
|
||||
turn.last = i.time;
|
||||
turn.fine |= fine_steps(*unit, *delta, *source);
|
||||
if modifiers.command && !turn.fine {
|
||||
scroll.x += d.x + d.y;
|
||||
} else if modifiers.shift {
|
||||
scroll.y += d.x + d.y;
|
||||
|
||||
+5
-3
@@ -3995,9 +3995,11 @@ desktop app): in View › Pixel Preview, on softens placed and embedded bitmaps;
|
||||
`disableAutoAddDelete` (off by default): on, the Pen no longer adds or deletes an anchor when a click lands on a
|
||||
segment or an anchor of a selected path.
|
||||
|
||||
`ctrlWheelScrollsSideways` (on by default): on, Cmd/Ctrl-wheel scrolls the canvas sideways instead of zooming while
|
||||
`zoomWithMouseWheel` is off; off, Cmd/Ctrl-wheel zooms about the pointer. Alt + Cmd/Ctrl-wheel and a pinch on a Mac
|
||||
trackpad, on a touch screen or in the web build still zoom about the pointer. With `zoomWithMouseWheel` off, once a
|
||||
`ctrlWheelScrollsSideways` (on by default): on, Cmd/Ctrl with a mouse wheel's whole notches scrolls the canvas sideways
|
||||
instead of zooming while `zoomWithMouseWheel` is off; off, Cmd/Ctrl-wheel zooms about the pointer. Alt + Cmd/Ctrl-wheel
|
||||
and a pinch on a Mac trackpad, on a touch screen or in the web build still zoom about the pointer. Under either
|
||||
wheel preference, Cmd/Ctrl zooms about the pointer for the rest of a wheel turn once the turn has a trackpad event,
|
||||
point deltas from an unnamed source in the desktop app, or line deltas that are not whole notches. With `zoomWithMouseWheel` off, once a
|
||||
trackpad gesture holds Alt, it zooms until it ends, also after Alt is let go. The control channel's
|
||||
`ui.wheel` with `cmd: true` turns the wheel with Cmd/Ctrl held.
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user