mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 11:11:30 +00:00
Canvas: Cmd/Ctrl-wheel input finer than a whole notch zooms
With Scroll Sideways with Cmd/Ctrl + Wheel on, wheel() turned all Cmd/Ctrl wheel motion into a sideways scroll unless it arrived as a pinch event. A wheel turn now zooms about the pointer once it holds an event from a trackpad source, point deltas from an unnamed source in the desktop app, or line deltas that are not whole notches. Whole notches keep scrolling sideways, and the option's default is unchanged. The same rule applies with Zoom with Mouse Wheel on. egui-winit reports a wheel source only on macOS, so the delta decides elsewhere. Docs: docs/mcp.md, docs/target-app-parity.md.
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