1
0
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:
Isse
2025-10-27 11:46:51 +01:00
committed by GitHub
parent f6fa74c665
commit 999e943e59
4 changed files with 127 additions and 55 deletions

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,
},

View File

@@ -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