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 {
|
} else {
|
||||||
|
// Kinetic scrolling
|
||||||
let stop_speed = 20.0; // Pixels per second.
|
let stop_speed = 20.0; // Pixels per second.
|
||||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||||
let dt = ui.input(|i| i.unstable_dt);
|
let dt = ui.input(|i| i.unstable_dt);
|
||||||
|
|
@ -781,12 +782,12 @@ impl Prepared {
|
||||||
&& scroll_enabled[0] != scroll_enabled[1];
|
&& scroll_enabled[0] != scroll_enabled[1];
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
if scroll_enabled[d] {
|
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 {
|
if always_scroll_enabled_direction {
|
||||||
// no bidirectional scrolling; allow horizontal scrolling without pressing shift
|
// 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 {
|
} else {
|
||||||
fs.scroll_delta[d]
|
input.smooth_scroll_delta[d]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -795,15 +796,17 @@ impl Prepared {
|
||||||
|
|
||||||
if scrolling_up || scrolling_down {
|
if scrolling_up || scrolling_down {
|
||||||
state.offset[d] -= scroll_delta;
|
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 {
|
if always_scroll_enabled_direction {
|
||||||
fs.scroll_delta[0] = 0.0;
|
input.smooth_scroll_delta[0] = 0.0;
|
||||||
fs.scroll_delta[1] = 0.0;
|
input.smooth_scroll_delta[1] = 0.0;
|
||||||
} else {
|
} else {
|
||||||
fs.scroll_delta[d] = 0.0;
|
input.smooth_scroll_delta[d] = 0.0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
state.scroll_stuck_to_end[d] = false;
|
state.scroll_stuck_to_end[d] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,6 @@ pub(crate) struct FrameState {
|
||||||
/// Initialized to `None` at the start of each frame.
|
/// Initialized to `None` at the start of each frame.
|
||||||
pub(crate) tooltip_state: Option<TooltipFrameState>,
|
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
|
/// horizontal, vertical
|
||||||
pub(crate) scroll_target: [Option<(Rangef, Option<Align>)>; 2],
|
pub(crate) scroll_target: [Option<(Rangef, Option<Align>)>; 2],
|
||||||
|
|
||||||
|
|
@ -67,7 +62,6 @@ impl Default for FrameState {
|
||||||
unused_rect: Rect::NAN,
|
unused_rect: Rect::NAN,
|
||||||
used_by_panels: Rect::NAN,
|
used_by_panels: Rect::NAN,
|
||||||
tooltip_state: None,
|
tooltip_state: None,
|
||||||
scroll_delta: Vec2::ZERO,
|
|
||||||
scroll_target: [None, None],
|
scroll_target: [None, None],
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
accesskit_state: None,
|
accesskit_state: None,
|
||||||
|
|
@ -89,7 +83,6 @@ impl FrameState {
|
||||||
unused_rect,
|
unused_rect,
|
||||||
used_by_panels,
|
used_by_panels,
|
||||||
tooltip_state,
|
tooltip_state,
|
||||||
scroll_delta,
|
|
||||||
scroll_target,
|
scroll_target,
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
accesskit_state,
|
accesskit_state,
|
||||||
|
|
@ -105,7 +98,6 @@ impl FrameState {
|
||||||
*unused_rect = input.screen_rect();
|
*unused_rect = input.screen_rect();
|
||||||
*used_by_panels = Rect::NOTHING;
|
*used_by_panels = Rect::NOTHING;
|
||||||
*tooltip_state = None;
|
*tooltip_state = None;
|
||||||
*scroll_delta = input.scroll_delta;
|
|
||||||
*scroll_target = [None, None];
|
*scroll_target = [None, None];
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ pub struct InputState {
|
||||||
/// (We keep a separate [`TouchState`] for each encountered touch device.)
|
/// (We keep a separate [`TouchState`] for each encountered touch device.)
|
||||||
touch_states: BTreeMap<TouchDeviceId, TouchState>,
|
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.
|
/// 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,
|
/// 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.
|
/// 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).
|
/// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture).
|
||||||
///
|
///
|
||||||
|
|
@ -125,7 +145,9 @@ impl Default for InputState {
|
||||||
raw: Default::default(),
|
raw: Default::default(),
|
||||||
pointer: Default::default(),
|
pointer: Default::default(),
|
||||||
touch_states: 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,
|
zoom_factor_delta: 1.0,
|
||||||
screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)),
|
screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)),
|
||||||
pixels_per_point: 1.0,
|
pixels_per_point: 1.0,
|
||||||
|
|
@ -171,7 +193,7 @@ impl InputState {
|
||||||
let pointer = self.pointer.begin_frame(time, &new);
|
let pointer = self.pointer.begin_frame(time, &new);
|
||||||
|
|
||||||
let mut keys_down = self.keys_down;
|
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;
|
let mut zoom_factor_delta = 1.0;
|
||||||
for event in &mut new.events {
|
for event in &mut new.events {
|
||||||
match event {
|
match event {
|
||||||
|
|
@ -189,7 +211,7 @@ impl InputState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Scroll(delta) => {
|
Event::Scroll(delta) => {
|
||||||
scroll_delta += *delta;
|
raw_scroll_delta += *delta;
|
||||||
}
|
}
|
||||||
Event::Zoom(factor) => {
|
Event::Zoom(factor) => {
|
||||||
zoom_factor_delta *= *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 mut modifiers = new.modifiers;
|
||||||
|
|
||||||
let focused_changed = self.focused != new.focused
|
let focused_changed = self.focused != new.focused
|
||||||
|
|
@ -215,7 +253,9 @@ impl InputState {
|
||||||
Self {
|
Self {
|
||||||
pointer,
|
pointer,
|
||||||
touch_states: self.touch_states,
|
touch_states: self.touch_states,
|
||||||
scroll_delta,
|
unprocessed_scroll_delta,
|
||||||
|
raw_scroll_delta,
|
||||||
|
smooth_scroll_delta,
|
||||||
zoom_factor_delta,
|
zoom_factor_delta,
|
||||||
screen_rect,
|
screen_rect,
|
||||||
pixels_per_point,
|
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 {
|
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.
|
/// 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,
|
raw,
|
||||||
pointer,
|
pointer,
|
||||||
touch_states,
|
touch_states,
|
||||||
scroll_delta,
|
|
||||||
|
unprocessed_scroll_delta,
|
||||||
|
raw_scroll_delta,
|
||||||
|
smooth_scroll_delta,
|
||||||
|
|
||||||
zoom_factor_delta,
|
zoom_factor_delta,
|
||||||
screen_rect,
|
screen_rect,
|
||||||
pixels_per_point,
|
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!("zoom_factor_delta: {zoom_factor_delta:4.2}x"));
|
||||||
ui.label(format!("screen_rect: {screen_rect:?} points"));
|
ui.label(format!("screen_rect: {screen_rect:?} points"));
|
||||||
ui.label(format!(
|
ui.label(format!(
|
||||||
|
|
|
||||||
|
|
@ -1042,7 +1042,7 @@ impl Ui {
|
||||||
/// ```
|
/// ```
|
||||||
pub fn scroll_with_delta(&self, delta: Vec2) {
|
pub fn scroll_with_delta(&self, delta: Vec2) {
|
||||||
self.ctx()
|
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 {
|
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 {
|
if scroll_delta != Vec2::ZERO {
|
||||||
transform.translate_bounds(-scroll_delta);
|
transform.translate_bounds(-scroll_delta);
|
||||||
auto_bounds = false.into();
|
auto_bounds = false.into();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue