diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 66b90d3e0c2..56d3fdf071a 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -503,36 +503,6 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu modifiers, }); - let scroll_multiplier = match unit { - egui::MouseWheelUnit::Page => { - canvas_size_in_points(runner.canvas(), runner.egui_ctx()).y - } - egui::MouseWheelUnit::Line => { - #[allow(clippy::let_and_return)] - let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit. - points_per_scroll_line - } - egui::MouseWheelUnit::Point => 1.0, - }; - - let mut delta = scroll_multiplier * delta; - - // Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed. - // This if-statement is equivalent to how `Modifiers.command` is determined in - // `modifiers_from_kb_event()`, but we cannot directly use that fn for a [`WheelEvent`]. - if event.ctrl_key() || event.meta_key() { - let factor = (delta.y / 200.0).exp(); - runner.input.raw.events.push(egui::Event::Zoom(factor)); - } else { - if event.shift_key() { - // Treat as horizontal scrolling. - // Note: one Mac we already get horizontal scroll events when shift is down. - delta = egui::vec2(delta.x + delta.y, 0.0); - } - - runner.input.raw.events.push(egui::Event::Scroll(delta)); - } - runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 0908f983857..8929af2f0fe 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -694,29 +694,6 @@ impl State { modifiers, }); } - let delta = match delta { - winit::event::MouseScrollDelta::LineDelta(x, y) => { - let points_per_scroll_line = 50.0; // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461 - egui::vec2(x, y) * points_per_scroll_line - } - winit::event::MouseScrollDelta::PixelDelta(delta) => { - egui::vec2(delta.x as f32, delta.y as f32) / pixels_per_point - } - }; - - if self.egui_input.modifiers.ctrl || self.egui_input.modifiers.command { - // Treat as zoom instead: - let factor = (delta.y / 200.0).exp(); - self.egui_input.events.push(egui::Event::Zoom(factor)); - } else if self.egui_input.modifiers.shift { - // Treat as horizontal scrolling. - // Note: one Mac we already get horizontal scroll events when shift is down. - self.egui_input - .events - .push(egui::Event::Scroll(egui::vec2(delta.x + delta.y, 0.0))); - } else { - self.egui_input.events.push(egui::Event::Scroll(delta)); - } } fn on_keyboard_input(&mut self, event: &winit::event::KeyEvent) { diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 6ea1959df04..7b4cf130d0c 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -442,6 +442,7 @@ impl ContextImpl { new_raw_input, viewport.repaint.requested_immediate_repaint_prev_frame(), pixels_per_point, + &self.memory.options, ); let screen_rect = viewport.input.screen_rect; diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 94aca791ce8..199529c00f9 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -530,21 +530,6 @@ pub enum Event { /// On touch-up first send `PointerButton{pressed: false, …}` followed by `PointerLeft`. PointerGone, - /// How many points (logical pixels) the user scrolled. - /// - /// The direction of the vector indicates how to move the _content_ that is being viewed. - /// So if you get positive values, the content being viewed should move to the right and down, - /// revealing new things to the left and up. - /// - /// A positive X-value indicates the content is being moved right, - /// as when swiping right on a touch-screen or track-pad with natural scrolling. - /// - /// A positive Y-value indicates the content is being moved down, - /// as when swiping down on a touch-screen or track-pad with natural scrolling. - /// - /// Shift-scroll should result in horizontal scrolling (it is up to the integrations to do this). - Scroll(Vec2), - /// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture). /// * `zoom = 1`: no change. /// * `zoom < 1`: pinch together @@ -577,16 +562,22 @@ pub enum Event { force: Option, }, - /// A raw mouse wheel event as sent by the backend (minus the z coordinate), - /// for implementing alternative custom controls. - /// Note that the same event can also trigger [`Self::Zoom`] and [`Self::Scroll`], - /// so you probably want to handle only one of them. + /// A raw mouse wheel event as sent by the backend. + /// + /// Used for scrolling. MouseWheel { - /// The unit of scrolling: points, lines, or pages. + /// The unit of `delta`: points, lines, or pages. unit: MouseWheelUnit, - /// The amount scrolled horizontally and vertically. The amount and direction corresponding - /// to one step of the wheel depends on the platform. + /// The direction of the vector indicates how to move the _content_ that is being viewed. + /// So if you get positive values, the content being viewed should move to the right and down, + /// revealing new things to the left and up. + /// + /// A positive X-value indicates the content is being moved right, + /// as when swiping right on a touch-screen or track-pad with natural scrolling. + /// + /// A positive Y-value indicates the content is being moved down, + /// as when swiping down on a touch-screen or track-pad with natural scrolling. delta: Vec2, /// The state of the modifier keys at the time of the event. diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 014a8491c74..b9e62c3c5b3 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -42,6 +42,11 @@ pub struct InputState { /// Used for smoothing the scroll delta. unprocessed_scroll_delta: Vec2, + /// Used for smoothing the scroll delta when zooming. + unprocessed_scroll_delta_for_zoom: f32, + + /// You probably want to use [`Self::smooth_scroll_delta`] instead. + /// /// The raw input of how many points the user scrolled. /// /// The delta dictates how the _content_ should move. @@ -152,6 +157,7 @@ impl Default for InputState { pointer: Default::default(), touch_states: Default::default(), unprocessed_scroll_delta: Vec2::ZERO, + unprocessed_scroll_delta_for_zoom: 0.0, raw_scroll_delta: Vec2::ZERO, smooth_scroll_delta: Vec2::ZERO, zoom_factor_delta: 1.0, @@ -177,6 +183,7 @@ impl InputState { mut new: RawInput, requested_immediate_repaint_prev_frame: bool, pixels_per_point: f32, + options: &crate::Options, ) -> Self { crate::profile_function!(); @@ -198,9 +205,15 @@ impl InputState { } let pointer = self.pointer.clone().begin_frame(time, &new); - let mut keys_down = self.keys_down.clone(); + let mut keys_down = self.keys_down; + let mut zoom_factor_delta = 1.0; // TODO(emilk): smoothing for zoom factor let mut raw_scroll_delta = Vec2::ZERO; - let mut zoom_factor_delta = 1.0; + + let mut unprocessed_scroll_delta = self.unprocessed_scroll_delta; + let mut unprocessed_scroll_delta_for_zoom = self.unprocessed_scroll_delta_for_zoom; + let mut smooth_scroll_delta = Vec2::ZERO; + let mut smooth_scroll_delta_for_zoom = 0.0; + for event in &mut new.events { match event { Event::Key { @@ -216,8 +229,51 @@ impl InputState { keys_down.remove(key); } } - Event::Scroll(delta) => { - raw_scroll_delta += *delta; + Event::MouseWheel { + unit, + delta, + modifiers, + } => { + let mut delta = match unit { + MouseWheelUnit::Point => *delta, + MouseWheelUnit::Line => options.line_scroll_speed * *delta, + MouseWheelUnit::Page => screen_rect.height() * *delta, + }; + + if modifiers.shift { + // Treat as horizontal scrolling. + // Note: one Mac we already get horizontal scroll events when shift is down. + delta = vec2(delta.x + delta.y, 0.0); + } + + raw_scroll_delta += delta; + + // Mouse wheels often go very large steps. + // A single notch on a logitech mouse wheel connected to a Macbook returns 14.0 raw_scroll_delta. + // So we smooth it out over several frames for a nicer user experience when scrolling in egui. + // BUT: if the user is using a nice smooth mac trackpad, we don't add smoothing, + // because it adds latency. + let is_smooth = match unit { + MouseWheelUnit::Point => delta.length() < 8.0, // a bit arbitrary here + MouseWheelUnit::Line | MouseWheelUnit::Page => false, + }; + + let is_zoom = modifiers.ctrl || modifiers.mac_cmd || modifiers.command; + + #[allow(clippy::collapsible_else_if)] + if is_zoom { + if is_smooth { + smooth_scroll_delta_for_zoom += delta.y; + } else { + unprocessed_scroll_delta_for_zoom += delta.y; + } + } else { + if is_smooth { + smooth_scroll_delta += delta; + } else { + unprocessed_scroll_delta += delta; + } + } } Event::Zoom(factor) => { zoom_factor_delta *= *factor; @@ -226,13 +282,44 @@ impl InputState { } } - self.smooth_scroll_delta = Vec2::ZERO; - self.create_scroll_delta(true, false); + { + let dt = stable_dt.at_most(0.1); + let t = crate::emath::exponential_smooth_factor(0.90, 0.1, dt); // reach _% in _ seconds. TODO(emilk): parameterize + + if unprocessed_scroll_delta != Vec2::ZERO { + for d in 0..2 { + if unprocessed_scroll_delta[d].abs() < 1.0 { + smooth_scroll_delta[d] += unprocessed_scroll_delta[d]; + unprocessed_scroll_delta[d] = 0.0; + } else { + let applied = t * unprocessed_scroll_delta[d]; + smooth_scroll_delta[d] += applied; + unprocessed_scroll_delta[d] -= applied; + } + } + } + + { + // Smooth scroll-to-zoom: + if unprocessed_scroll_delta_for_zoom.abs() < 1.0 { + smooth_scroll_delta_for_zoom += unprocessed_scroll_delta_for_zoom; + unprocessed_scroll_delta_for_zoom = 0.0; + } else { + let applied = t * unprocessed_scroll_delta_for_zoom; + smooth_scroll_delta_for_zoom += applied; + unprocessed_scroll_delta_for_zoom -= applied; + } + + zoom_factor_delta *= + (options.scroll_zoom_speed * smooth_scroll_delta_for_zoom).exp(); + } + } Self { pointer, touch_states: self.touch_states, - unprocessed_scroll_delta: self.unprocessed_scroll_delta, + unprocessed_scroll_delta, + unprocessed_scroll_delta_for_zoom, raw_scroll_delta, smooth_scroll_delta: self.smooth_scroll_delta, zoom_factor_delta, @@ -345,6 +432,7 @@ impl InputState { pub fn wants_repaint(&self) -> bool { self.pointer.wants_repaint() || self.unprocessed_scroll_delta.abs().max_elem() > 0.2 + || self.unprocessed_scroll_delta_for_zoom.abs() > 0.2 || !self.events.is_empty() // We need to wake up and check for press-and-hold for the context menu. @@ -1155,6 +1243,7 @@ impl InputState { touch_states, unprocessed_scroll_delta, + unprocessed_scroll_delta_for_zoom, raw_scroll_delta, smooth_scroll_delta, @@ -1196,6 +1285,9 @@ impl InputState { ui.label(format!( "unprocessed_scroll_delta: {unprocessed_scroll_delta:?} points" )); + ui.label(format!( + "unprocessed_scroll_delta_for_zoom: {unprocessed_scroll_delta_for_zoom:?} points" + )); } ui.label(format!("raw_scroll_delta: {raw_scroll_delta:?} points")); ui.label(format!( diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 00e6bfb99d0..acb18841547 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -231,10 +231,26 @@ pub struct Options { /// /// By default this is `true` in debug builds. pub warn_on_id_clash: bool, + + // ------------------------------ + // Input: + /// Multiplier for the scroll speed when reported in [`crate::MouseWheelUnit::Line`]s. + pub line_scroll_speed: f32, + + /// Controls the speed at which we zoom in when doing ctrl/cmd + scroll. + pub scroll_zoom_speed: f32, } impl Default for Options { fn default() -> Self { + // TODO(emilk): figure out why these constants need to be different on web and on native (winit). + let is_web = cfg!(target_arch = "wasm32"); + let line_scroll_speed = if is_web { + 8.0 + } else { + 40.0 // Scroll speed decided by consensus: https://github.com/emilk/egui/issues/461 + }; + Self { style: Default::default(), zoom_factor: 1.0, @@ -245,6 +261,10 @@ impl Default for Options { screen_reader: false, preload_font_glyphs: true, warn_on_id_clash: cfg!(debug_assertions), + + // Input: + line_scroll_speed, + scroll_zoom_speed: 1.0 / 200.0, } } } @@ -262,6 +282,9 @@ impl Options { screen_reader: _, // needs to come from the integration preload_font_glyphs: _, warn_on_id_clash, + + line_scroll_speed, + scroll_zoom_speed, } = self; use crate::Widget as _; @@ -300,6 +323,27 @@ impl Options { }); }); + CollapsingHeader::new("🖱 Input") + .default_open(false) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label("Line scroll speed"); + ui.add( + crate::DragValue::new(line_scroll_speed).clamp_range(0.0..=f32::INFINITY), + ) + .on_hover_text("How many lines to scroll with each tick of the mouse wheel"); + }); + ui.horizontal(|ui| { + ui.label("Scroll zoom speed"); + ui.add( + crate::DragValue::new(scroll_zoom_speed) + .clamp_range(0.0..=f32::INFINITY) + .speed(0.001), + ) + .on_hover_text("How fast to zoom with ctrl/cmd + scroll"); + }); + }); + ui.vertical_centered(|ui| crate::reset_button(ui, self, "Reset all")); } }