Smooth scrolling (#3884)
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`.
This commit is contained in:
parent
200051d5d8
commit
5388e65623
|
|
@ -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);
|
||||
|
|
@ -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]
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
@ -89,7 +83,6 @@ impl FrameState {
|
|||
unused_rect,
|
||||
used_by_panels,
|
||||
tooltip_state,
|
||||
scroll_delta,
|
||||
scroll_target,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_state,
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ 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.
|
||||
///
|
||||
|
|
@ -42,7 +45,24 @@ 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,
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// [`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).
|
||||
///
|
||||
|
|
@ -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,
|
||||
|
|
@ -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 {
|
||||
|
|
@ -189,7 +211,7 @@ impl InputState {
|
|||
}
|
||||
}
|
||||
Event::Scroll(delta) => {
|
||||
scroll_delta += *delta;
|
||||
raw_scroll_delta += *delta;
|
||||
}
|
||||
Event::Zoom(factor) => {
|
||||
zoom_factor_delta *= *factor;
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
@ -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.
|
||||
|
|
@ -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,
|
||||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue