mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Add is_scrolling/is_smooth_scrolling util, checking for active scroll action. (#7669)
* Closes #7657 * [x] I have followed the instructions in the PR template On native this uses a new "touch phase" parameter of the mouse wheel event to know if a scroll action is done. --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
@@ -831,6 +831,7 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
|
||||
unit,
|
||||
delta,
|
||||
modifiers,
|
||||
phase: egui::TouchPhase::Move,
|
||||
}
|
||||
};
|
||||
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
|
||||
@@ -312,8 +312,8 @@ impl State {
|
||||
consumed: self.egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => {
|
||||
self.on_mouse_wheel(window, *delta);
|
||||
WindowEvent::MouseWheel { delta, phase, .. } => {
|
||||
self.on_mouse_wheel(window, *delta, *phase);
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: self.egui_ctx.wants_pointer_input(),
|
||||
@@ -545,12 +545,13 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::PanGesture { delta, .. } => {
|
||||
WindowEvent::PanGesture { delta, phase, .. } => {
|
||||
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
||||
|
||||
self.egui_input.events.push(egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: Vec2::new(delta.x, delta.y) / pixels_per_point,
|
||||
phase: to_egui_touch_phase(*phase),
|
||||
modifiers: self.egui_input.modifiers,
|
||||
});
|
||||
EventResponse {
|
||||
@@ -680,12 +681,7 @@ impl State {
|
||||
self.egui_input.events.push(egui::Event::Touch {
|
||||
device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
|
||||
id: egui::TouchId::from(touch.id),
|
||||
phase: match touch.phase {
|
||||
winit::event::TouchPhase::Started => egui::TouchPhase::Start,
|
||||
winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
|
||||
winit::event::TouchPhase::Ended => egui::TouchPhase::End,
|
||||
winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
|
||||
},
|
||||
phase: to_egui_touch_phase(touch.phase),
|
||||
pos: egui::pos2(
|
||||
touch.location.x as f32 / pixels_per_point,
|
||||
touch.location.y as f32 / pixels_per_point,
|
||||
@@ -738,7 +734,12 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_wheel(&mut self, window: &Window, delta: winit::event::MouseScrollDelta) {
|
||||
fn on_mouse_wheel(
|
||||
&mut self,
|
||||
window: &Window,
|
||||
delta: winit::event::MouseScrollDelta,
|
||||
phase: winit::event::TouchPhase,
|
||||
) {
|
||||
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
||||
|
||||
{
|
||||
@@ -754,10 +755,12 @@ impl State {
|
||||
egui::vec2(x as f32, y as f32) / pixels_per_point,
|
||||
),
|
||||
};
|
||||
let phase = to_egui_touch_phase(phase);
|
||||
let modifiers = self.egui_input.modifiers;
|
||||
self.egui_input.events.push(egui::Event::MouseWheel {
|
||||
unit,
|
||||
delta,
|
||||
phase,
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
@@ -970,6 +973,15 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn to_egui_touch_phase(phase: winit::event::TouchPhase) -> egui::TouchPhase {
|
||||
match phase {
|
||||
winit::event::TouchPhase::Started => egui::TouchPhase::Start,
|
||||
winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
|
||||
winit::event::TouchPhase::Ended => egui::TouchPhase::End,
|
||||
winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_egui_theme(theme: winit::window::Theme) -> Theme {
|
||||
match theme {
|
||||
winit::window::Theme::Dark => Theme::Dark,
|
||||
|
||||
@@ -535,6 +535,11 @@ pub enum Event {
|
||||
/// as when swiping down on a touch-screen or track-pad with natural scrolling.
|
||||
delta: Vec2,
|
||||
|
||||
/// The phase of the scroll, useful for trackpads.
|
||||
///
|
||||
/// If unknown set this to [`TouchPhase::Move`].
|
||||
phase: TouchPhase,
|
||||
|
||||
/// The state of the modifier keys at the time of the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
|
||||
@@ -225,6 +225,16 @@ pub struct InputState {
|
||||
/// Time of the last scroll event.
|
||||
last_scroll_time: f64,
|
||||
|
||||
/// If we are currently in a scroll action.
|
||||
///
|
||||
/// This is not the same as checking if [`Self::smooth_scroll_delta`], or
|
||||
/// [`Self::raw_scroll_delta`] are zero. This instead relies on the
|
||||
/// current touch phase received from the mouse wheel event.
|
||||
///
|
||||
/// This value is only `Some` if we have ever received a [`crate::TouchPhase::Start`] event and then
|
||||
/// know that the current platform supports it.
|
||||
is_in_scroll_action: Option<bool>,
|
||||
|
||||
/// Used for smoothing the scroll delta.
|
||||
unprocessed_scroll_delta: Vec2,
|
||||
|
||||
@@ -357,6 +367,7 @@ impl Default for InputState {
|
||||
pointer: Default::default(),
|
||||
touch_states: Default::default(),
|
||||
|
||||
is_in_scroll_action: None,
|
||||
last_scroll_time: f64::NEG_INFINITY,
|
||||
unprocessed_scroll_delta: Vec2::ZERO,
|
||||
unprocessed_scroll_delta_for_zoom: 0.0,
|
||||
@@ -440,53 +451,68 @@ impl InputState {
|
||||
Event::MouseWheel {
|
||||
unit,
|
||||
delta,
|
||||
phase,
|
||||
modifiers,
|
||||
} => {
|
||||
let mut delta = match unit {
|
||||
MouseWheelUnit::Point => *delta,
|
||||
MouseWheelUnit::Line => options.line_scroll_speed * *delta,
|
||||
MouseWheelUnit::Page => viewport_rect.height() * *delta,
|
||||
};
|
||||
|
||||
let is_horizontal = modifiers.matches_any(options.horizontal_scroll_modifier);
|
||||
let is_vertical = modifiers.matches_any(options.vertical_scroll_modifier);
|
||||
|
||||
if is_horizontal && !is_vertical {
|
||||
// Treat all scrolling as horizontal scrolling.
|
||||
// Note: one Mac we already get horizontal scroll events when shift is down.
|
||||
delta = vec2(delta.x + delta.y, 0.0);
|
||||
}
|
||||
if !is_horizontal && is_vertical {
|
||||
// Treat all scrolling as vertical scrolling.
|
||||
delta = vec2(0.0, delta.x + delta.y);
|
||||
}
|
||||
|
||||
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.matches_any(options.zoom_modifier);
|
||||
|
||||
#[expect(clippy::collapsible_else_if)]
|
||||
if is_zoom {
|
||||
if is_smooth {
|
||||
smooth_scroll_delta_for_zoom += delta.x + delta.y;
|
||||
} else {
|
||||
unprocessed_scroll_delta_for_zoom += delta.x + delta.y;
|
||||
match phase {
|
||||
crate::TouchPhase::Start => {
|
||||
self.is_in_scroll_action = Some(true);
|
||||
}
|
||||
} else {
|
||||
if is_smooth {
|
||||
smooth_scroll_delta += delta;
|
||||
} else {
|
||||
unprocessed_scroll_delta += delta;
|
||||
crate::TouchPhase::Move => {
|
||||
let mut delta = match unit {
|
||||
MouseWheelUnit::Point => *delta,
|
||||
MouseWheelUnit::Line => options.line_scroll_speed * *delta,
|
||||
MouseWheelUnit::Page => viewport_rect.height() * *delta,
|
||||
};
|
||||
|
||||
let is_horizontal =
|
||||
modifiers.matches_any(options.horizontal_scroll_modifier);
|
||||
let is_vertical =
|
||||
modifiers.matches_any(options.vertical_scroll_modifier);
|
||||
|
||||
if is_horizontal && !is_vertical {
|
||||
// Treat all scrolling as horizontal scrolling.
|
||||
// Note: one Mac we already get horizontal scroll events when shift is down.
|
||||
delta = vec2(delta.x + delta.y, 0.0);
|
||||
}
|
||||
if !is_horizontal && is_vertical {
|
||||
// Treat all scrolling as vertical scrolling.
|
||||
delta = vec2(0.0, delta.x + delta.y);
|
||||
}
|
||||
|
||||
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.matches_any(options.zoom_modifier);
|
||||
|
||||
#[expect(clippy::collapsible_else_if)]
|
||||
if is_zoom {
|
||||
if is_smooth {
|
||||
smooth_scroll_delta_for_zoom += delta.x + delta.y;
|
||||
} else {
|
||||
unprocessed_scroll_delta_for_zoom += delta.x + delta.y;
|
||||
}
|
||||
} else {
|
||||
if is_smooth {
|
||||
smooth_scroll_delta += delta;
|
||||
} else {
|
||||
unprocessed_scroll_delta += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::TouchPhase::End | crate::TouchPhase::Cancel => {
|
||||
if let Some(is_in_scroll_action) = &mut self.is_in_scroll_action {
|
||||
*is_in_scroll_action = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -542,7 +568,7 @@ impl InputState {
|
||||
}
|
||||
|
||||
let is_scrolling = raw_scroll_delta != Vec2::ZERO || smooth_scroll_delta != Vec2::ZERO;
|
||||
let last_scroll_time = if is_scrolling {
|
||||
let last_scroll_time = if is_scrolling || self.is_in_scroll_action.is_some_and(|b| b) {
|
||||
time
|
||||
} else {
|
||||
self.last_scroll_time
|
||||
@@ -552,6 +578,7 @@ impl InputState {
|
||||
pointer,
|
||||
touch_states: self.touch_states,
|
||||
|
||||
is_in_scroll_action: self.is_in_scroll_action,
|
||||
last_scroll_time,
|
||||
unprocessed_scroll_delta,
|
||||
unprocessed_scroll_delta_for_zoom,
|
||||
@@ -708,6 +735,27 @@ impl InputState {
|
||||
.map_or(self.smooth_scroll_delta, |touch| touch.translation_delta)
|
||||
}
|
||||
|
||||
/// True if there is an active scroll action that might scroll more when using [`Self::smooth_scroll_delta`].
|
||||
pub fn is_smooth_scrolling(&self) -> bool {
|
||||
self.is_raw_scrolling() || self.smooth_scroll_delta != Vec2::ZERO
|
||||
}
|
||||
|
||||
/// True if there is an active scroll action that might scroll more.
|
||||
///
|
||||
/// You probably want to use [`Self::is_smooth_scrolling`].
|
||||
pub fn is_raw_scrolling(&self) -> bool {
|
||||
if let Some(is_in_scroll_action) = self.is_in_scroll_action {
|
||||
is_in_scroll_action
|
||||
} else {
|
||||
// On certain platforms, like web, we don't get the start & stop scrolling events, so
|
||||
// we rely on a timer there.
|
||||
//
|
||||
// Tested on a mac touchpad 2025, where the largest observed gap between scroll events
|
||||
// was 68 ms. So 100 ms should most likely be good here.
|
||||
self.time_since_last_scroll() < 0.1
|
||||
}
|
||||
}
|
||||
|
||||
/// How long has it been (in seconds) since the use last scrolled?
|
||||
#[inline(always)]
|
||||
pub fn time_since_last_scroll(&self) -> f32 {
|
||||
@@ -1599,6 +1647,7 @@ impl InputState {
|
||||
pointer,
|
||||
touch_states,
|
||||
|
||||
is_in_scroll_action: _,
|
||||
last_scroll_time,
|
||||
unprocessed_scroll_delta,
|
||||
unprocessed_scroll_delta_for_zoom,
|
||||
@@ -1642,6 +1691,11 @@ impl InputState {
|
||||
});
|
||||
}
|
||||
|
||||
ui.label(format!(
|
||||
"is_scrolling: raw: {}, smooth: {}",
|
||||
self.is_raw_scrolling(),
|
||||
self.is_smooth_scrolling()
|
||||
));
|
||||
ui.label(format!(
|
||||
"Time since last scroll: {:.1} s",
|
||||
time - last_scroll_time
|
||||
|
||||
Reference in New Issue
Block a user