mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 23:00:04 -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,
|
unit,
|
||||||
delta,
|
delta,
|
||||||
modifiers,
|
modifiers,
|
||||||
|
phase: egui::TouchPhase::Move,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
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(),
|
consumed: self.egui_ctx.wants_pointer_input(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::MouseWheel { delta, .. } => {
|
WindowEvent::MouseWheel { delta, phase, .. } => {
|
||||||
self.on_mouse_wheel(window, *delta);
|
self.on_mouse_wheel(window, *delta, *phase);
|
||||||
EventResponse {
|
EventResponse {
|
||||||
repaint: true,
|
repaint: true,
|
||||||
consumed: self.egui_ctx.wants_pointer_input(),
|
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);
|
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
||||||
|
|
||||||
self.egui_input.events.push(egui::Event::MouseWheel {
|
self.egui_input.events.push(egui::Event::MouseWheel {
|
||||||
unit: egui::MouseWheelUnit::Point,
|
unit: egui::MouseWheelUnit::Point,
|
||||||
delta: Vec2::new(delta.x, delta.y) / pixels_per_point,
|
delta: Vec2::new(delta.x, delta.y) / pixels_per_point,
|
||||||
|
phase: to_egui_touch_phase(*phase),
|
||||||
modifiers: self.egui_input.modifiers,
|
modifiers: self.egui_input.modifiers,
|
||||||
});
|
});
|
||||||
EventResponse {
|
EventResponse {
|
||||||
@@ -680,12 +681,7 @@ impl State {
|
|||||||
self.egui_input.events.push(egui::Event::Touch {
|
self.egui_input.events.push(egui::Event::Touch {
|
||||||
device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
|
device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
|
||||||
id: egui::TouchId::from(touch.id),
|
id: egui::TouchId::from(touch.id),
|
||||||
phase: match touch.phase {
|
phase: to_egui_touch_phase(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,
|
|
||||||
},
|
|
||||||
pos: egui::pos2(
|
pos: egui::pos2(
|
||||||
touch.location.x as f32 / pixels_per_point,
|
touch.location.x as f32 / pixels_per_point,
|
||||||
touch.location.y 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);
|
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,
|
egui::vec2(x as f32, y as f32) / pixels_per_point,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
let phase = to_egui_touch_phase(phase);
|
||||||
let modifiers = self.egui_input.modifiers;
|
let modifiers = self.egui_input.modifiers;
|
||||||
self.egui_input.events.push(egui::Event::MouseWheel {
|
self.egui_input.events.push(egui::Event::MouseWheel {
|
||||||
unit,
|
unit,
|
||||||
delta,
|
delta,
|
||||||
|
phase,
|
||||||
modifiers,
|
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 {
|
fn to_egui_theme(theme: winit::window::Theme) -> Theme {
|
||||||
match theme {
|
match theme {
|
||||||
winit::window::Theme::Dark => Theme::Dark,
|
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.
|
/// as when swiping down on a touch-screen or track-pad with natural scrolling.
|
||||||
delta: Vec2,
|
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.
|
/// The state of the modifier keys at the time of the event.
|
||||||
modifiers: Modifiers,
|
modifiers: Modifiers,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -225,6 +225,16 @@ pub struct InputState {
|
|||||||
/// Time of the last scroll event.
|
/// Time of the last scroll event.
|
||||||
last_scroll_time: f64,
|
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.
|
/// Used for smoothing the scroll delta.
|
||||||
unprocessed_scroll_delta: Vec2,
|
unprocessed_scroll_delta: Vec2,
|
||||||
|
|
||||||
@@ -357,6 +367,7 @@ impl Default for InputState {
|
|||||||
pointer: Default::default(),
|
pointer: Default::default(),
|
||||||
touch_states: Default::default(),
|
touch_states: Default::default(),
|
||||||
|
|
||||||
|
is_in_scroll_action: None,
|
||||||
last_scroll_time: f64::NEG_INFINITY,
|
last_scroll_time: f64::NEG_INFINITY,
|
||||||
unprocessed_scroll_delta: Vec2::ZERO,
|
unprocessed_scroll_delta: Vec2::ZERO,
|
||||||
unprocessed_scroll_delta_for_zoom: 0.0,
|
unprocessed_scroll_delta_for_zoom: 0.0,
|
||||||
@@ -440,16 +451,24 @@ impl InputState {
|
|||||||
Event::MouseWheel {
|
Event::MouseWheel {
|
||||||
unit,
|
unit,
|
||||||
delta,
|
delta,
|
||||||
|
phase,
|
||||||
modifiers,
|
modifiers,
|
||||||
} => {
|
} => {
|
||||||
|
match phase {
|
||||||
|
crate::TouchPhase::Start => {
|
||||||
|
self.is_in_scroll_action = Some(true);
|
||||||
|
}
|
||||||
|
crate::TouchPhase::Move => {
|
||||||
let mut delta = match unit {
|
let mut delta = match unit {
|
||||||
MouseWheelUnit::Point => *delta,
|
MouseWheelUnit::Point => *delta,
|
||||||
MouseWheelUnit::Line => options.line_scroll_speed * *delta,
|
MouseWheelUnit::Line => options.line_scroll_speed * *delta,
|
||||||
MouseWheelUnit::Page => viewport_rect.height() * *delta,
|
MouseWheelUnit::Page => viewport_rect.height() * *delta,
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_horizontal = modifiers.matches_any(options.horizontal_scroll_modifier);
|
let is_horizontal =
|
||||||
let is_vertical = modifiers.matches_any(options.vertical_scroll_modifier);
|
modifiers.matches_any(options.horizontal_scroll_modifier);
|
||||||
|
let is_vertical =
|
||||||
|
modifiers.matches_any(options.vertical_scroll_modifier);
|
||||||
|
|
||||||
if is_horizontal && !is_vertical {
|
if is_horizontal && !is_vertical {
|
||||||
// Treat all scrolling as horizontal scrolling.
|
// Treat all scrolling as horizontal scrolling.
|
||||||
@@ -490,6 +509,13 @@ impl InputState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
crate::TouchPhase::End | crate::TouchPhase::Cancel => {
|
||||||
|
if let Some(is_in_scroll_action) = &mut self.is_in_scroll_action {
|
||||||
|
*is_in_scroll_action = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Event::Zoom(factor) => {
|
Event::Zoom(factor) => {
|
||||||
zoom_factor_delta *= *factor;
|
zoom_factor_delta *= *factor;
|
||||||
}
|
}
|
||||||
@@ -542,7 +568,7 @@ impl InputState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let is_scrolling = raw_scroll_delta != Vec2::ZERO || smooth_scroll_delta != Vec2::ZERO;
|
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
|
time
|
||||||
} else {
|
} else {
|
||||||
self.last_scroll_time
|
self.last_scroll_time
|
||||||
@@ -552,6 +578,7 @@ impl InputState {
|
|||||||
pointer,
|
pointer,
|
||||||
touch_states: self.touch_states,
|
touch_states: self.touch_states,
|
||||||
|
|
||||||
|
is_in_scroll_action: self.is_in_scroll_action,
|
||||||
last_scroll_time,
|
last_scroll_time,
|
||||||
unprocessed_scroll_delta,
|
unprocessed_scroll_delta,
|
||||||
unprocessed_scroll_delta_for_zoom,
|
unprocessed_scroll_delta_for_zoom,
|
||||||
@@ -708,6 +735,27 @@ impl InputState {
|
|||||||
.map_or(self.smooth_scroll_delta, |touch| touch.translation_delta)
|
.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?
|
/// How long has it been (in seconds) since the use last scrolled?
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn time_since_last_scroll(&self) -> f32 {
|
pub fn time_since_last_scroll(&self) -> f32 {
|
||||||
@@ -1599,6 +1647,7 @@ impl InputState {
|
|||||||
pointer,
|
pointer,
|
||||||
touch_states,
|
touch_states,
|
||||||
|
|
||||||
|
is_in_scroll_action: _,
|
||||||
last_scroll_time,
|
last_scroll_time,
|
||||||
unprocessed_scroll_delta,
|
unprocessed_scroll_delta,
|
||||||
unprocessed_scroll_delta_for_zoom,
|
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!(
|
ui.label(format!(
|
||||||
"Time since last scroll: {:.1} s",
|
"Time since last scroll: {:.1} s",
|
||||||
time - last_scroll_time
|
time - last_scroll_time
|
||||||
|
|||||||
Reference in New Issue
Block a user