From ff0856570d2435c83f7d73b89a5899cb2a7c4ae7 Mon Sep 17 00:00:00 2001 From: batistalucasv Date: Wed, 11 Nov 2026 11:48:19 -0300 Subject: [PATCH] ui: preserve line and point zoom distance across scroll speeds (#6146) --- crates/ui-egui/src/app.rs | 17 ++--- crates/ui-egui/src/scroll.rs | 28 +++++++- crates/ui-egui/src/tests.rs | 122 ++++++++++++++++++++++++----------- 3 files changed, 116 insertions(+), 51 deletions(-) diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index 1923d929d6..8a59d4a5d6 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -303,20 +303,11 @@ impl WordApp { if ctx.zoom_factor() != zoom { ctx.set_zoom_factor(zoom); } - // Mouse-wheel line scroll speed (#6146): scales egui's own scroll areas while preserving - // default Ctrl/⌘+Line-wheel zoom distance across animation settings. + // Mouse-wheel line scroll speed (#6146): scales egui's own scroll areas. let base_line_speed = egui::InputOptions::default().line_scroll_speed; - let base_zoom_speed = egui::InputOptions::default().scroll_zoom_speed; - let factor = self.ui.scroll_speed.wheel_factor(); - let target_line_speed = base_line_speed * factor; - let target_zoom_speed = base_zoom_speed / factor; - if (ctx.options(|o| o.input_options.line_scroll_speed) - target_line_speed).abs() > 1e-4 - || (ctx.options(|o| o.input_options.scroll_zoom_speed) - target_zoom_speed).abs() > 1e-6 - { - ctx.options_mut(|o| { - o.input_options.line_scroll_speed = target_line_speed; - o.input_options.scroll_zoom_speed = target_zoom_speed; - }); + let target_line_speed = base_line_speed * self.ui.scroll_speed.wheel_factor(); + if (ctx.options(|o| o.input_options.line_scroll_speed) - target_line_speed).abs() > 1e-4 { + ctx.options_mut(|o| o.input_options.line_scroll_speed = target_line_speed); } // Animation effects: the host is asked every frame what the system says (it answers from // memory), so a change made in the system's settings is followed without a restart. diff --git a/crates/ui-egui/src/scroll.rs b/crates/ui-egui/src/scroll.rs index f4630dc741..73a1d81430 100644 --- a/crates/ui-egui/src/scroll.rs +++ b/crates/ui-egui/src/scroll.rs @@ -390,17 +390,41 @@ impl CanvasScroll { /// pinch the system reports as a pinch still follows the fingers. pub fn zoom(&mut self, input: &egui::InputState, opts: &InputOptions, animate: bool) -> f32 { // Fingers on a touch screen are measured directly, never eased. - if animate || input.multi_touch().is_some() { + if input.multi_touch().is_some() { self.zoom_pending = 0.0; return input.zoom_delta(); } + if animate { + self.zoom_pending = 0.0; + let factor = opts.line_scroll_speed / InputOptions::default().line_scroll_speed; + let has_line_wheel = input.events.iter().any(|e| { + matches!( + e, + Event::MouseWheel { + unit: MouseWheelUnit::Line, + modifiers, + .. + } if modifiers.matches_any(opts.zoom_modifier) + ) + }); + if has_line_wheel && factor > 0.0 && (factor - 1.0).abs() > 1e-4 { + let z = input.zoom_delta(); + if z.is_finite() && z > 0.0 { + return (z.ln() / factor).exp(); + } + } + return input.zoom_delta(); + } let mut zoom = 1.0_f32; let mut turned = 0.0_f32; for event in &input.events { match event { Event::Zoom(factor) => zoom *= factor, Event::MouseWheel { unit, delta, phase: TouchPhase::Move, modifiers, .. } if modifiers.matches_any(opts.zoom_modifier) => { - let d = egui_points(*unit, *delta, input, opts); + let d = match unit { + MouseWheelUnit::Line => *delta * InputOptions::default().line_scroll_speed, + _ => egui_points(*unit, *delta, input, opts), + }; turned += opts.scroll_zoom_speed * (d.x + d.y); } _ => {} diff --git a/crates/ui-egui/src/tests.rs b/crates/ui-egui/src/tests.rs index 7d9e9eb150..033269bd40 100644 --- a/crates/ui-egui/src/tests.rs +++ b/crates/ui-egui/src/tests.rs @@ -8329,74 +8329,124 @@ fn fresh_scroll_event_matching_previous_scaled_delta_is_scaled_after_warmup_ui() } /// Issue #6146 / PR #6167 review: changing mouse-wheel scroll speed (Slow / Normal / Fast) -/// must only affect scrolling distance, not Ctrl/⌘+Line-wheel zoom distance. +/// must only affect scrolling distance, not Ctrl/⌘+wheel zoom distance for either Line or Point events. /// Tests both animation settings (`animate == true` eased zoom and `animate == false` -/// `CanvasScroll::zoom` discrete steps). +/// `CanvasScroll::zoom` discrete steps) for both Line and Point mouse-wheel events. #[test] fn mouse_wheel_scroll_speed_does_not_alter_ctrl_wheel_zoom_distance() { let mut a = app(); let default_opts = egui::InputOptions::default(); let default_notch_zoom = (default_opts.scroll_zoom_speed * default_opts.line_scroll_speed).exp(); - let ctrl_wheel_event = || egui::Event::MouseWheel { + let ctrl_line_event = || egui::Event::MouseWheel { unit: egui::MouseWheelUnit::Line, delta: egui::vec2(0.0, 1.0), phase: egui::TouchPhase::Move, modifiers: egui::Modifiers::COMMAND, source: egui::MouseWheelSource::Unknown, }; + let ctrl_point_event = || egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: egui::vec2(0.0, 20.0), + phase: egui::TouchPhase::Move, + modifiers: egui::Modifiers::COMMAND, + source: egui::MouseWheelSource::Unknown, + }; // 1. Establish baseline zoom with Normal speed a.ui.scroll_speed = crate::scroll::ScrollSpeed::Normal; - let ctx_baseline = egui::Context::default(); - a.logic(&ctx_baseline); - let opts_baseline = ctx_baseline.options(|o| o.input_options); - let mut baseline_eased_zoom = 1.0; - let mut baseline_discrete_zoom = 1.0; - ctx_baseline - .run_ui(egui::RawInput { events: vec![ctrl_wheel_event()], ..Default::default() }, |ui| { - baseline_eased_zoom = ui.input(|i| i.zoom_delta()); + let mut baseline_eased_line_zoom = 1.0; + let mut baseline_discrete_line_zoom = 1.0; + let mut baseline_eased_point_zoom = 1.0; + let mut baseline_discrete_point_zoom = 1.0; + + // Line baseline: + let ctx_line_base = egui::Context::default(); + a.logic(&ctx_line_base); + let opts_line_base = ctx_line_base.options(|o| o.input_options); + ctx_line_base + .run_ui(egui::RawInput { events: vec![ctrl_line_event()], time: Some(1.0), ..Default::default() }, |ui| { let mut canvas_scroll = crate::scroll::CanvasScroll::default(); - baseline_discrete_zoom = ui.input(|i| canvas_scroll.zoom(i, &opts_baseline, false)); + baseline_eased_line_zoom = ui.input(|i| canvas_scroll.zoom(i, &opts_line_base, true)); + let mut canvas_scroll_discrete = crate::scroll::CanvasScroll::default(); + baseline_discrete_line_zoom = ui.input(|i| canvas_scroll_discrete.zoom(i, &opts_line_base, false)); }) .drop_without_applying_deltas(); - assert!((baseline_discrete_zoom - default_notch_zoom).abs() < 1e-3, "discrete zoom equals notch zoom"); - assert!(baseline_eased_zoom > 1.0 && baseline_eased_zoom < default_notch_zoom, "eased zoom begins in first frame"); + // Point baseline: + let ctx_point_base = egui::Context::default(); + a.logic(&ctx_point_base); + let opts_point_base = ctx_point_base.options(|o| o.input_options); + ctx_point_base + .run_ui(egui::RawInput { events: vec![ctrl_point_event()], time: Some(1.0), ..Default::default() }, |ui| { + let mut canvas_scroll = crate::scroll::CanvasScroll::default(); + baseline_eased_point_zoom = ui.input(|i| canvas_scroll.zoom(i, &opts_point_base, true)); + let mut canvas_scroll_discrete = crate::scroll::CanvasScroll::default(); + baseline_discrete_point_zoom = ui.input(|i| canvas_scroll_discrete.zoom(i, &opts_point_base, false)); + }) + .drop_without_applying_deltas(); + + assert!((baseline_discrete_line_zoom - default_notch_zoom).abs() < 1e-3, "discrete line zoom equals notch zoom"); + assert!(baseline_eased_line_zoom > 1.0 && baseline_eased_line_zoom < default_notch_zoom, "eased line zoom begins in first frame"); + assert!(baseline_eased_point_zoom > 1.0, "point zoom applies in eased mode"); + assert!(baseline_discrete_point_zoom > 1.0, "point zoom applies in discrete mode"); // 2. Verify all scroll speeds preserve exact same zoom distance across animation settings for speed in [crate::scroll::ScrollSpeed::Slow, crate::scroll::ScrollSpeed::Normal, crate::scroll::ScrollSpeed::Fast] { a.ui.scroll_speed = speed; - let ctx = egui::Context::default(); - a.logic(&ctx); - let opts = ctx.options(|o| o.input_options); + // Test Line-wheel zoom invariance: + let ctx_line = egui::Context::default(); + a.logic(&ctx_line); + let opts = ctx_line.options(|o| o.input_options); let factor = speed.wheel_factor(); assert!( (opts.line_scroll_speed - default_opts.line_scroll_speed * factor).abs() < 1e-4, "line scroll speed scales by factor for ordinary scrolling" ); - assert!( - (opts.scroll_zoom_speed - default_opts.scroll_zoom_speed / factor).abs() < 1e-6, - "scroll zoom speed inverts factor to preserve zoom distance" + assert_eq!( + opts.scroll_zoom_speed, default_opts.scroll_zoom_speed, + "scroll zoom speed remains unscaled default so point-wheel zoom is unchanged" ); - ctx.run_ui(egui::RawInput { events: vec![ctrl_wheel_event()], ..Default::default() }, |ui| { - // Eased zoom (animate == true) - let zoom_delta = ui.input(|i| i.zoom_delta()); - assert!( - (zoom_delta - baseline_eased_zoom).abs() < 1e-5, - "eased zoom delta for speed {speed:?} matches baseline ({zoom_delta} vs {baseline_eased_zoom})" - ); + ctx_line + .run_ui(egui::RawInput { events: vec![ctrl_line_event()], time: Some(1.0), ..Default::default() }, |ui| { + let mut canvas_scroll_eased = crate::scroll::CanvasScroll::default(); + let eased_zoom = ui.input(|i| canvas_scroll_eased.zoom(i, &opts, true)); + assert!( + (eased_zoom - baseline_eased_line_zoom).abs() < 1e-5, + "eased line zoom delta for speed {speed:?} matches baseline ({eased_zoom} vs {baseline_eased_line_zoom})" + ); - // CanvasScroll::zoom (animate == false) - let mut canvas_scroll = crate::scroll::CanvasScroll::default(); - let discrete_zoom = ui.input(|i| canvas_scroll.zoom(i, &opts, false)); - assert!( - (discrete_zoom - baseline_discrete_zoom).abs() < 1e-5, - "discrete CanvasScroll::zoom for speed {speed:?} matches baseline ({discrete_zoom} vs {baseline_discrete_zoom})" - ); - }) - .drop_without_applying_deltas(); + let mut canvas_scroll_discrete = crate::scroll::CanvasScroll::default(); + let discrete_zoom = ui.input(|i| canvas_scroll_discrete.zoom(i, &opts, false)); + assert!( + (discrete_zoom - baseline_discrete_line_zoom).abs() < 1e-5, + "discrete CanvasScroll::zoom for speed {speed:?} matches baseline ({discrete_zoom} vs {baseline_discrete_line_zoom})" + ); + }) + .drop_without_applying_deltas(); + + // Test Point-wheel zoom invariance: + let ctx_point = egui::Context::default(); + a.logic(&ctx_point); + let opts_point = ctx_point.options(|o| o.input_options); + ctx_point + .run_ui(egui::RawInput { events: vec![ctrl_point_event()], time: Some(1.0), ..Default::default() }, |ui| { + let mut canvas_scroll_eased = crate::scroll::CanvasScroll::default(); + let eased_zoom = ui.input(|i| canvas_scroll_eased.zoom(i, &opts_point, true)); + assert!( + (eased_zoom - baseline_eased_point_zoom).abs() < 1e-5, + "eased point zoom delta for speed {speed:?} matches baseline ({eased_zoom} vs {baseline_eased_point_zoom})" + ); + + let mut canvas_scroll_discrete = crate::scroll::CanvasScroll::default(); + let discrete_zoom = ui.input(|i| canvas_scroll_discrete.zoom(i, &opts_point, false)); + assert!( + (discrete_zoom - baseline_discrete_point_zoom).abs() < 1e-5, + "discrete CanvasScroll::zoom point delta for speed {speed:?} matches baseline ({discrete_zoom} vs {baseline_discrete_point_zoom})" + ); + }) + .drop_without_applying_deltas(); } }