ui: preserve line and point zoom distance across scroll speeds (#6146)

This commit is contained in:
batistalucasv committed 2026-11-11 13:57:51 -03:00
1 parent 215485dd9a
commit ff0856570d
3 files changed
+116 -51

No files matched your search

+4 -13
View File
@@ -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.
+26 -2
View File
@@ -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);
}
_ => {}
+86 -36
View File
@@ -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();
}
}