Skip to content

Commit

Permalink
Smooth scrolling (#3884)
Browse files Browse the repository at this point in the history
This adds smooth scrolling in egui. This makes scrolling in a
`ScrollArea` using a notched mouse wheel a lot nicer.

`InputState::scroll_delta` has been replaced by
`InputState::raw_scroll_delta` and `InputState::smooth_scroll_delta`.
  • Loading branch information
emilk authored Jan 24, 2024
1 parent 200051d commit 5388e65
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 27 deletions.
19 changes: 11 additions & 8 deletions crates/egui/src/containers/scroll_area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ impl ScrollArea {
}
}
} else {
// Kinetic scrolling
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let dt = ui.input(|i| i.unstable_dt);
Expand Down Expand Up @@ -781,12 +782,12 @@ impl Prepared {
&& scroll_enabled[0] != scroll_enabled[1];
for d in 0..2 {
if scroll_enabled[d] {
let scroll_delta = ui.ctx().frame_state(|fs| {
let scroll_delta = ui.ctx().input_mut(|input| {
if always_scroll_enabled_direction {
// no bidirectional scrolling; allow horizontal scrolling without pressing shift
fs.scroll_delta[0] + fs.scroll_delta[1]
input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1]
} else {
fs.scroll_delta[d]
input.smooth_scroll_delta[d]
}
});

Expand All @@ -795,15 +796,17 @@ impl Prepared {

if scrolling_up || scrolling_down {
state.offset[d] -= scroll_delta;
// Clear scroll delta so no parent scroll will use it.
ui.ctx().frame_state_mut(|fs| {

// Clear scroll delta so no parent scroll will use it:
ui.ctx().input_mut(|input| {
if always_scroll_enabled_direction {
fs.scroll_delta[0] = 0.0;
fs.scroll_delta[1] = 0.0;
input.smooth_scroll_delta[0] = 0.0;
input.smooth_scroll_delta[1] = 0.0;
} else {
fs.scroll_delta[d] = 0.0;
input.smooth_scroll_delta[d] = 0.0;
}
});

state.scroll_stuck_to_end[d] = false;
}
}
Expand Down
8 changes: 0 additions & 8 deletions crates/egui/src/frame_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,6 @@ pub(crate) struct FrameState {
/// Initialized to `None` at the start of each frame.
pub(crate) tooltip_state: Option<TooltipFrameState>,

/// Set to [`InputState::scroll_delta`] on the start of each frame.
///
/// Cleared by the first [`ScrollArea`] that makes use of it.
pub(crate) scroll_delta: Vec2, // TODO(emilk): move to `InputState` ?

/// horizontal, vertical
pub(crate) scroll_target: [Option<(Rangef, Option<Align>)>; 2],

Expand All @@ -67,7 +62,6 @@ impl Default for FrameState {
unused_rect: Rect::NAN,
used_by_panels: Rect::NAN,
tooltip_state: None,
scroll_delta: Vec2::ZERO,
scroll_target: [None, None],
#[cfg(feature = "accesskit")]
accesskit_state: None,
Expand All @@ -89,7 +83,6 @@ impl FrameState {
unused_rect,
used_by_panels,
tooltip_state,
scroll_delta,
scroll_target,
#[cfg(feature = "accesskit")]
accesskit_state,
Expand All @@ -105,7 +98,6 @@ impl FrameState {
*unused_rect = input.screen_rect();
*used_by_panels = Rect::NOTHING;
*tooltip_state = None;
*scroll_delta = input.scroll_delta;
*scroll_target = [None, None];

#[cfg(debug_assertions)]
Expand Down
73 changes: 64 additions & 9 deletions crates/egui/src/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,24 @@ pub struct InputState {
/// (We keep a separate [`TouchState`] for each encountered touch device.)
touch_states: BTreeMap<TouchDeviceId, TouchState>,

/// How many points the user scrolled.
/// Used for smoothing the scroll delta.
unprocessed_scroll_delta: Vec2,

/// 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.
///
/// The delta dictates how the _content_ should move.
///
Expand All @@ -42,7 +59,10 @@ pub struct InputState {
///
/// 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.
pub scroll_delta: Vec2,
///
/// [`crate::ScrollArea`] will both read and write to this field, so that
/// at the end of the frame this will be zero if a scroll-area consumed the delta.
pub smooth_scroll_delta: Vec2,

/// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture).
///
Expand Down Expand Up @@ -125,7 +145,9 @@ impl Default for InputState {
raw: Default::default(),
pointer: Default::default(),
touch_states: Default::default(),
scroll_delta: Vec2::ZERO,
unprocessed_scroll_delta: Vec2::ZERO,
raw_scroll_delta: Vec2::ZERO,
smooth_scroll_delta: Vec2::ZERO,
zoom_factor_delta: 1.0,
screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)),
pixels_per_point: 1.0,
Expand Down Expand Up @@ -171,7 +193,7 @@ impl InputState {
let pointer = self.pointer.begin_frame(time, &new);

let mut keys_down = self.keys_down;
let mut scroll_delta = Vec2::ZERO;
let mut raw_scroll_delta = Vec2::ZERO;
let mut zoom_factor_delta = 1.0;
for event in &mut new.events {
match event {
Expand All @@ -189,7 +211,7 @@ impl InputState {
}
}
Event::Scroll(delta) => {
scroll_delta += *delta;
raw_scroll_delta += *delta;
}
Event::Zoom(factor) => {
zoom_factor_delta *= *factor;
Expand All @@ -198,6 +220,22 @@ impl InputState {
}
}

let mut unprocessed_scroll_delta = self.unprocessed_scroll_delta;

let smooth_scroll_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.
unprocessed_scroll_delta += raw_scroll_delta;
let dt = stable_dt.at_most(0.1);
let t = crate::emath::exponential_smooth_factor(0.90, 0.1, dt); // reach _% in _ seconds. TODO: parameterize

smooth_scroll_delta = t * unprocessed_scroll_delta;
unprocessed_scroll_delta -= smooth_scroll_delta;
}

let mut modifiers = new.modifiers;

let focused_changed = self.focused != new.focused
Expand All @@ -215,7 +253,9 @@ impl InputState {
Self {
pointer,
touch_states: self.touch_states,
scroll_delta,
unprocessed_scroll_delta,
raw_scroll_delta,
smooth_scroll_delta,
zoom_factor_delta,
screen_rect,
pixels_per_point,
Expand Down Expand Up @@ -282,8 +322,11 @@ impl InputState {
)
}

/// The [`crate::Context`] will call this at the end of each frame to see if we need a repaint.
pub fn wants_repaint(&self) -> bool {
self.pointer.wants_repaint() || self.scroll_delta != Vec2::ZERO || !self.events.is_empty()
self.pointer.wants_repaint()
|| self.unprocessed_scroll_delta.abs().max_elem() > 0.2
|| !self.events.is_empty()
}

/// Count presses of a key. If non-zero, the presses are consumed, so that this will only return non-zero once.
Expand Down Expand Up @@ -1007,7 +1050,11 @@ impl InputState {
raw,
pointer,
touch_states,
scroll_delta,

unprocessed_scroll_delta,
raw_scroll_delta,
smooth_scroll_delta,

zoom_factor_delta,
screen_rect,
pixels_per_point,
Expand Down Expand Up @@ -1042,7 +1089,15 @@ impl InputState {
});
}

ui.label(format!("scroll_delta: {scroll_delta:?} points"));
if cfg!(debug_assertions) {
ui.label(format!(
"unprocessed_scroll_delta: {unprocessed_scroll_delta:?} 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!("screen_rect: {screen_rect:?} points"));
ui.label(format!(
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1042,7 +1042,7 @@ impl Ui {
/// ```
pub fn scroll_with_delta(&self, delta: Vec2) {
self.ctx()
.frame_state_mut(|state| state.scroll_delta += delta);
.input_mut(|input| input.smooth_scroll_delta += delta);
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/egui_plot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1115,7 +1115,7 @@ impl Plot {
}
}
if allow_scroll {
let scroll_delta = ui.input(|i| i.scroll_delta);
let scroll_delta = ui.input(|i| i.smooth_scroll_delta);
if scroll_delta != Vec2::ZERO {
transform.translate_bounds(-scroll_delta);
auto_bounds = false.into();
Expand Down

0 comments on commit 5388e65

Please sign in to comment.