1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 06:10:06 -04:00

Quickly animate scroll when calling ui.scroll_to_cursor etc (#4119)

Uses ease-in-ease-out interpolation, with a time between 0.1s and 0.3s,
depending on the distance needed to scroll.


![smooth-scroll-to-target](https://github.com/emilk/egui/assets/1148717/c5c8556d-476b-4597-842b-aa0e5927fbb9)
This commit is contained in:
Emil Ernerfeldt
2024-03-04 20:00:13 +01:00
committed by GitHub
parent e29022efc4
commit 18eeb01f57
6 changed files with 141 additions and 22 deletions

View File

@@ -2,6 +2,13 @@
use crate::*;
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct ScrollTarget {
animation_time_span: (f64, f64),
target_offset: f32,
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
@@ -9,6 +16,9 @@ pub struct State {
/// Positive offset means scrolling down/right
pub offset: Vec2,
/// If set, quickly but smoothly scroll to this target offset.
offset_target: [Option<ScrollTarget>; 2],
/// Were the scroll bars visible last frame?
show_scroll: Vec2b,
@@ -35,6 +45,7 @@ impl Default for State {
fn default() -> Self {
Self {
offset: Vec2::ZERO,
offset_target: Default::default(),
show_scroll: Vec2b::FALSE,
content_is_too_large: Vec2b::FALSE,
scroll_bar_interaction: Vec2b::FALSE,
@@ -559,25 +570,56 @@ impl ScrollArea {
state.vel[d] = input.pointer.velocity()[d];
});
state.scroll_stuck_to_end[d] = false;
state.offset_target[d] = None;
} else {
state.vel[d] = 0.0;
}
}
} 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);
for d in 0..2 {
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
let friction = friction_coeff * dt;
if friction > state.vel.length() || state.vel.length() < stop_speed {
state.vel = Vec2::ZERO;
} else {
state.vel -= friction * state.vel.normalized();
// Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it
state.offset -= state.vel * dt;
ctx.request_repaint();
if let Some(scroll_target) = state.offset_target[d] {
state.vel[d] = 0.0;
if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 {
// Arrived
state.offset[d] = scroll_target.target_offset;
state.offset_target[d] = None;
} else {
// Move towards target
let t = emath::interpolation_factor(
scroll_target.animation_time_span,
ui.input(|i| i.time),
dt,
emath::ease_in_ease_out,
);
if t < 1.0 {
state.offset[d] =
emath::lerp(state.offset[d]..=scroll_target.target_offset, t);
ctx.request_repaint();
} else {
// Arrived
state.offset[d] = scroll_target.target_offset;
state.offset_target[d] = None;
}
}
} else {
// Kinetic scrolling
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let friction = friction_coeff * dt;
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
state.vel[d] = 0.0;
} else {
state.vel[d] -= friction * state.vel[d].signum();
// Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it
state.offset[d] -= state.vel[d] * dt;
ctx.request_repaint();
}
}
}
}
}
@@ -716,11 +758,11 @@ impl Prepared {
let scroll_target = content_ui
.ctx()
.frame_state_mut(|state| state.scroll_target[d].take());
if let Some((scroll, align)) = scroll_target {
if let Some((target_range, align)) = scroll_target {
let min = content_ui.min_rect().min[d];
let clip_rect = content_ui.clip_rect();
let visible_range = min..=min + clip_rect.size()[d];
let (start, end) = (scroll.min, scroll.max);
let (start, end) = (target_range.min, target_range.max);
let clip_start = clip_rect.min[d];
let clip_end = clip_rect.max[d];
let mut spacing = ui.spacing().item_spacing[d];
@@ -729,7 +771,7 @@ impl Prepared {
let center_factor = align.to_factor();
let offset =
lerp(scroll, center_factor) - lerp(visible_range, center_factor);
lerp(target_range, center_factor) - lerp(visible_range, center_factor);
// Depending on the alignment we need to add or subtract the spacing
spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0);
@@ -745,7 +787,24 @@ impl Prepared {
};
if delta != 0.0 {
state.offset[d] += delta;
let target_offset = state.offset[d] + delta;
if let Some(animation) = &mut state.offset_target[d] {
// For instance: the user is continuously calling `ui.scroll_to_cursor`,
// so we don't want to reset the animation, but perhaps update the target:
animation.target_offset = target_offset;
} else {
// The further we scroll, the more time we take.
// TODO(emilk): let users configure this in `Style`.
let now = ui.input(|i| i.time);
let points_per_second = 1000.0;
let animation_duration =
(delta.abs() / points_per_second).clamp(0.1, 0.3);
state.offset_target[d] = Some(ScrollTarget {
animation_time_span: (now, now + animation_duration as f64),
target_offset,
});
}
ui.ctx().request_repaint();
}
}
@@ -808,6 +867,7 @@ impl Prepared {
});
state.scroll_stuck_to_end[d] = false;
state.offset_target[d] = None;
}
}
}
@@ -952,6 +1012,7 @@ impl Prepared {
// some manual action taken, scroll not stuck
state.scroll_stuck_to_end[d] = false;
state.offset_target[d] = None;
} else {
state.scroll_start_offset_from_top_left[d] = None;
}

View File

@@ -223,7 +223,7 @@ impl InputState {
let mut unprocessed_scroll_delta = self.unprocessed_scroll_delta;
let smooth_scroll_delta;
let mut smooth_scroll_delta = Vec2::ZERO;
{
// Mouse wheels often go very large steps.
@@ -233,8 +233,15 @@ impl InputState {
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;
for d in 0..2 {
if unprocessed_scroll_delta[d].abs() < 1.0 {
smooth_scroll_delta[d] = unprocessed_scroll_delta[d];
unprocessed_scroll_delta[d] = 0.0;
} else {
smooth_scroll_delta[d] = t * unprocessed_scroll_delta[d];
unprocessed_scroll_delta[d] -= smooth_scroll_delta[d];
}
}
}
let mut modifiers = new.modifiers;

View File

@@ -679,6 +679,7 @@ impl Response {
/// Adjust the scroll position until this UI becomes visible.
///
/// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc.
/// If `align` is `None`, it'll scroll enough to bring the UI into view.
///
/// See also: [`Ui::scroll_to_cursor`], [`Ui::scroll_to_rect`]. [`Ui::scroll_with_delta`].

View File

@@ -1002,6 +1002,7 @@ impl Ui {
/// Adjust the scroll position of any parent [`ScrollArea`] so that the given [`Rect`] becomes visible.
///
/// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc.
/// If `align` is `None`, it'll scroll enough to bring the cursor into view.
///
/// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_cursor`]. [`Ui::scroll_with_delta`]..
@@ -1028,6 +1029,7 @@ impl Ui {
/// Adjust the scroll position of any parent [`ScrollArea`] so that the cursor (where the next widget goes) becomes visible.
///
/// If `align` is [`Align::TOP`] it means "put the top of the rect at the top of the scroll area", etc.
/// If `align` is not provided, it'll scroll enough to bring the cursor into view.
///
/// See also: [`Response::scroll_to_me`], [`Ui::scroll_to_rect`]. [`Ui::scroll_with_delta`].