Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cfada7c
Update scene.rs
Tristan117 Oct 25, 2025
d8d615b
Update scene.rs
Tristan117 Oct 25, 2025
7db12f4
Update scene.rs
Tristan117 Oct 26, 2025
60d67b2
Update scene.rs
Tristan117 Oct 26, 2025
39a2ad3
Update scene.rs
Tristan117 Oct 26, 2025
814d003
Used scroll_zoom_speed from input_options for zoom speed
Tristan117 Oct 26, 2025
c6c0817
trying to fix scroll_zoom_speed
Tristan117 Oct 26, 2025
ccf8471
Trying to fix scroll_zoom_speed
Tristan117 Oct 26, 2025
f639aec
This should fix it
Tristan117 Oct 26, 2025
2487629
Updated documentation
Tristan117 Oct 26, 2025
e223a5d
Merge branch 'emilk:main' into patch-1
Tristan117 Oct 27, 2025
0c1754d
Update scene.rs
Tristan117 Oct 25, 2025
654859f
Update scene.rs
Tristan117 Oct 26, 2025
8b9feed
Used scroll_zoom_speed from input_options for zoom speed
Tristan117 Oct 26, 2025
c5ae483
Updated documentation
Tristan117 Oct 26, 2025
b937e93
Merge remote-tracking branch 'origin/patch-1' into patch-1
Tristan117 Oct 28, 2025
75a6034
Merge branch 'main' into scrolling_zooms
Tristan117 Nov 21, 2025
07688ca
Merge pull request #1 from Tristan117/scrolling_zooms
Tristan117 Nov 21, 2025
b10dd9b
Revert "Scrolling zooms"
Tristan117 Nov 21, 2025
7d5ad6d
Fixed typos and added option to demo app
Tristan117 Nov 21, 2025
04657c2
Merge pull request #3 from Tristan117/revert-1-scrolling_zooms
Tristan117 Nov 21, 2025
d94753f
Merge branch 'patch-1' into scrolling_zooms
Tristan117 Nov 21, 2025
1de380f
Merge pull request #4 from Tristan117/scrolling_zooms
Tristan117 Nov 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions crates/egui/src/containers/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub struct Scene {
sense: Sense,
max_inner_size: Vec2,
drag_pan_buttons: DragPanButtons,
scroll_zooms: bool,
}

/// Specifies which pointer buttons can be used to pan the scene by dragging.
Expand Down Expand Up @@ -80,6 +81,7 @@ impl Default for Scene {
sense: Sense::click_and_drag(),
max_inner_size: Vec2::splat(1000.0),
drag_pan_buttons: DragPanButtons::all(),
scroll_zooms: false,
}
}
}
Expand Down Expand Up @@ -130,6 +132,16 @@ impl Scene {
self
}

/// Specify whether scrolling the mousewheel without a modifier pans or
/// zooms the Scene. Touch input is not affected by this setting.
///
/// By default, this is `false`.
#[inline]
pub fn scroll_zooms(mut self, scroll_zooms: bool) -> Self {
self.scroll_zooms = scroll_zooms;
self
}

/// `scene_rect` contains the view bounds of the inner [`Ui`].
///
/// `scene_rect` will be mutated by any panning/zooming done by the user.
Expand Down Expand Up @@ -244,8 +256,21 @@ impl Scene {
&& resp.contains_pointer()
{
let pointer_in_scene = to_global.inverse() * mouse_pos;
let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta());
let mut zoom_delta = ui.ctx().input(|i| i.zoom_delta());
let mut pan_delta = Vec2::ZERO;

// If scrolling_zooms is set to true the scroll input will be consumed and
// added to any zoom input. This is required to support both mouse wheel
// and touch events.
if self.scroll_zooms {
let scroll_zoom_speed = ui.ctx().options(|opt| opt.input_options.scroll_zoom_speed);
let scroll_delta = ui
.ctx()
.input(|i| i.smooth_scroll_delta.x + i.smooth_scroll_delta.y);
zoom_delta += scroll_delta * scroll_zoom_speed;
} else {
pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
}

// Most of the time we can return early. This is also important to
// avoid `ui_from_scene` to change slightly due to floating point errors.
Expand Down
183 changes: 159 additions & 24 deletions crates/egui/src/input_state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,41 @@ pub struct InputState {

// ----------------------------------------------
// Scrolling:
#[cfg_attr(feature = "serde", serde(skip))]
wheel: WheelState,
//
/// 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,

/// 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.
///
/// 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.
///
/// When using a notched scroll-wheel this will spike very large for one frame,
/// then drop to zero. For a smoother experience, use [`Self::smooth_scroll_delta`].
pub raw_scroll_delta: Vec2,

/// How many points the user scrolled, smoothed over a few frames.
///
Expand Down Expand Up @@ -338,7 +371,11 @@ impl Default for InputState {
pointer: Default::default(),
touch_states: Default::default(),

wheel: 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,
raw_scroll_delta: Vec2::ZERO,
smooth_scroll_delta: Vec2::ZERO,
zoom_factor_delta: 1.0,
rotation_radians: 0.0,
Expand Down Expand Up @@ -417,15 +454,67 @@ impl InputState {
phase,
modifiers,
} => {
self.wheel.on_wheel_event(
viewport_rect,
&options,
time,
*unit,
*delta,
*phase,
*modifiers,
);
match phase {
crate::TouchPhase::Start => {
self.is_in_scroll_action = Some(true);
}
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;
}
}
}
}
Event::Zoom(factor) => {
zoom_factor_delta *= *factor;
Expand Down Expand Up @@ -462,11 +551,22 @@ impl InputState {
}
}

let is_scrolling = raw_scroll_delta != Vec2::ZERO || smooth_scroll_delta != Vec2::ZERO;
let last_scroll_time = if is_scrolling || self.is_in_scroll_action.is_some_and(|b| b) {
time
} else {
self.last_scroll_time
};

Self {
pointer,
touch_states: self.touch_states,

wheel: self.wheel,
is_in_scroll_action: self.is_in_scroll_action,
last_scroll_time,
unprocessed_scroll_delta,
unprocessed_scroll_delta_for_zoom,
raw_scroll_delta,
smooth_scroll_delta,
zoom_factor_delta,
rotation_radians,
Expand Down Expand Up @@ -636,11 +736,27 @@ impl InputState {
}

/// True if there is an active scroll action that might scroll more when using [`Self::smooth_scroll_delta`].
pub fn is_scrolling(&self) -> bool {
self.wheel.is_scrolling()
pub fn is_smooth_scrolling(&self) -> bool {
self.is_raw_scrolling() || self.smooth_scroll_delta != Vec2::ZERO
}

/// How long has it been (in seconds) since the last scroll event?
/// 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 {
(self.time - self.wheel.last_wheel_event) as f32
Expand Down Expand Up @@ -1530,7 +1646,12 @@ impl InputState {
raw,
pointer,
touch_states,
wheel,

is_in_scroll_action: _,
last_scroll_time,
unprocessed_scroll_delta,
unprocessed_scroll_delta_for_zoom,
raw_scroll_delta,
smooth_scroll_delta,
rotation_radians,
zoom_factor_delta,
Expand Down Expand Up @@ -1569,13 +1690,27 @@ impl InputState {
});
}

crate::containers::CollapsingHeader::new("⬍ Scroll")
.default_open(false)
.show(ui, |ui| {
wheel.ui(ui);
});

ui.label(format!("smooth_scroll_delta: {smooth_scroll_delta:4.1}x"));
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
));
if cfg!(debug_assertions) {
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!(
"smooth_scroll_delta: {smooth_scroll_delta:?} points"
));
ui.label(format!("zoom_factor_delta: {zoom_factor_delta:4.2}x"));
ui.label(format!("rotation_radians: {rotation_radians:.3} radians"));

Expand Down
7 changes: 7 additions & 0 deletions crates/egui_demo_lib/src/demo/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ use super::widget_gallery;
pub struct SceneDemo {
widget_gallery: widget_gallery::WidgetGallery,
scene_rect: Rect,
scroll_zooms: bool,
}

impl Default for SceneDemo {
fn default() -> Self {
Self {
widget_gallery: widget_gallery::WidgetGallery::default().with_date_button(false), // disable date button so that we don't fail the snapshot test
scene_rect: Rect::ZERO, // `egui::Scene` will initialize this to something valid
scroll_zooms: false,
}
}
}
Expand All @@ -39,6 +41,10 @@ impl crate::View for SceneDemo {
"You can pan by scrolling, and zoom using cmd-scroll. \
Double click on the background to reset view.",
);
ui.checkbox(
&mut self.scroll_zooms,
"Change mousewheel behaviour to zoom instead of scroll",
);
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
Expand All @@ -53,6 +59,7 @@ impl crate::View for SceneDemo {
.show(ui, |ui| {
let scene = Scene::new()
.max_inner_size([350.0, 1000.0])
.scroll_zooms(self.scroll_zooms)
.zoom_range(0.1..=2.0);

let mut reset_view = false;
Expand Down