Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make scroll_to_* animations configurable #4305

Merged
merged 5 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 20 additions & 14 deletions crates/egui/src/containers/scroll_area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::*;

#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct ScrollTarget {
struct ScrollingToTarget {
animation_time_span: (f64, f64),
target_offset: f32,
}
Expand All @@ -17,7 +17,7 @@ pub struct State {
pub offset: Vec2,

/// If set, quickly but smoothly scroll to this target offset.
offset_target: [Option<ScrollTarget>; 2],
offset_target: [Option<ScrollingToTarget>; 2],

/// Were the scroll bars visible last frame?
show_scroll: Vec2b,
Expand Down Expand Up @@ -799,7 +799,8 @@ impl Prepared {

for d in 0..2 {
// FrameState::scroll_delta is inverted from the way we apply the delta, so we need to negate it.
let mut delta = -scroll_delta[d];
let mut delta = -scroll_delta.0[d];
let mut animation = scroll_delta.1;

// We always take both scroll targets regardless of which scroll axes are enabled. This
// is to avoid them leaking to other scroll areas.
Expand All @@ -808,20 +809,25 @@ impl Prepared {
.frame_state_mut(|state| state.scroll_target[d].take());

if scroll_enabled[d] {
delta += if let Some((target_range, align)) = scroll_target {
if let Some(target) = scroll_target {
let frame_state::ScrollTarget {
range,
align,
animation: animation_update,
} = 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) = (target_range.min, target_range.max);
let (start, end) = (range.min, range.max);
let clip_start = clip_rect.min[d];
let clip_end = clip_rect.max[d];
let mut spacing = ui.spacing().item_spacing[d];

if let Some(align) = align {
let delta_update = if let Some(align) = align {
let center_factor = align.to_factor();

let offset =
lerp(target_range, center_factor) - lerp(visible_range, center_factor);
lerp(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);
Expand All @@ -834,9 +840,10 @@ impl Prepared {
} else {
// Ui is already in view, no need to adjust scroll.
0.0
}
} else {
0.0
};

delta += delta_update;
animation = animation_update;
};

if delta != 0.0 {
Expand All @@ -850,11 +857,10 @@ impl Prepared {
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 {
let animation_duration = (delta.abs() / animation.points_per_second)
.clamp(animation.duration.min, animation.duration.max);
state.offset_target[d] = Some(ScrollingToTarget {
animation_time_span: (now, now + animation_duration as f64),
target_offset,
});
Expand Down
32 changes: 28 additions & 4 deletions crates/egui/src/frame_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,30 @@ pub struct PerLayerState {
pub widget_with_tooltip: Option<Id>,
}

#[derive(Clone, Debug)]
pub struct ScrollTarget {
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
// The range that the scroll area should scroll to.
pub range: Rangef,

/// How should we align the rect within the visible area?
/// 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.
pub align: Option<Align>,

/// How should the scroll be animated?
pub animation: style::ScrollAnimation,
}

impl ScrollTarget {
pub fn new(range: Rangef, align: Option<Align>, animation: style::ScrollAnimation) -> Self {
Self {
range,
align,
animation,
}
}
}

#[cfg(feature = "accesskit")]
#[derive(Clone)]
pub struct AccessKitFrameState {
Expand Down Expand Up @@ -79,7 +103,7 @@ pub struct FrameState {
pub used_by_panels: Rect,

/// The current scroll area should scroll to this range (horizontal, vertical).
pub scroll_target: [Option<(Rangef, Option<Align>)>; 2],
pub scroll_target: [Option<ScrollTarget>; 2],

/// The current scroll area should scroll by this much.
///
Expand All @@ -90,7 +114,7 @@ pub struct FrameState {
///
/// 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,
pub scroll_delta: (Vec2, style::ScrollAnimation),

#[cfg(feature = "accesskit")]
pub accesskit_state: Option<AccessKitFrameState>,
Expand All @@ -113,7 +137,7 @@ impl Default for FrameState {
unused_rect: Rect::NAN,
used_by_panels: Rect::NAN,
scroll_target: [None, None],
scroll_delta: Vec2::default(),
scroll_delta: (Vec2::default(), style::ScrollAnimation::none()),
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(feature = "accesskit")]
accesskit_state: None,
highlight_next_frame: Default::default(),
Expand Down Expand Up @@ -153,7 +177,7 @@ impl FrameState {
*unused_rect = screen_rect;
*used_by_panels = Rect::NOTHING;
*scroll_target = [None, None];
*scroll_delta = Vec2::default();
*scroll_delta = Default::default();

#[cfg(debug_assertions)]
{
Expand Down
26 changes: 21 additions & 5 deletions crates/egui/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ use std::{any::Any, sync::Arc};

use crate::{
emath::{Align, Pos2, Rect, Vec2},
menu, AreaState, Context, CursorIcon, Id, LayerId, Order, PointerButton, Sense, Ui, WidgetRect,
WidgetText,
frame_state, menu, AreaState, Context, CursorIcon, Id, LayerId, Order, PointerButton, Sense,
Ui, WidgetRect, WidgetText,
};

// ----------------------------------------------------------------------------

/// The result of adding a widget to a [`Ui`].
Expand Down Expand Up @@ -846,9 +845,26 @@ impl Response {
/// # });
/// ```
pub fn scroll_to_me(&self, align: Option<Align>) {
self.scroll_to_me_animation(align, self.ctx.style().scroll_animation);
}

/// Like [`Self::scroll_to_me`], but allows you to specify the [`crate::style::ScrollAnimation`].
pub fn scroll_to_me_animation(
&self,
align: Option<Align>,
animation: crate::style::ScrollAnimation,
) {
self.ctx.frame_state_mut(|state| {
state.scroll_target[0] = Some((self.rect.x_range(), align));
state.scroll_target[1] = Some((self.rect.y_range(), align));
state.scroll_target[0] = Some(frame_state::ScrollTarget::new(
self.rect.x_range(),
align,
animation,
));
state.scroll_target[1] = Some(frame_state::ScrollTarget::new(
self.rect.y_range(),
align,
animation,
));
});
}

Expand Down
88 changes: 88 additions & 0 deletions crates/egui/src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ pub struct Style {

/// If true and scrolling is enabled for only one direction, allow horizontal scrolling without pressing shift
pub always_scroll_the_only_direction: bool,

/// The animation that should be used when scrolling a [`crate::ScrollArea`] using e.g. [Ui::scroll_to_rect].
pub scroll_animation: ScrollAnimation,
}

#[test]
Expand Down Expand Up @@ -692,6 +695,88 @@ impl ScrollStyle {

// ----------------------------------------------------------------------------

/// Scroll animation configuration, used when programmatically scrolling somewhere (e.g. with `[crate::Ui::scroll_to_cursor]`)
/// The animation duration is calculated based on the distance to be scrolled via `[ScrollAnimation::points_per_second]`
/// and can be clamped to a min / max duration via `[ScrollAnimation::duration]`.
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct ScrollAnimation {
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
/// With what speed should we scroll? (Default: 1000.0)
pub points_per_second: f32,

/// The min / max scroll duration.
pub duration: Rangef,
}

impl Default for ScrollAnimation {
fn default() -> Self {
Self {
points_per_second: 1000.0,
duration: Rangef::new(0.1, 0.3),
}
}
}

impl ScrollAnimation {
/// New scroll animation
pub fn new(points_per_second: f32, duration: Rangef) -> Self {
Self {
points_per_second,
duration,
}
}

/// No animation, scroll instantly.
pub fn none() -> Self {
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
Self {
points_per_second: f32::INFINITY,
duration: Rangef::new(0.0, 0.0),
}
}

/// Scroll with a fixed duration, regardless of distance.
pub fn duration(t: f32) -> Self {
lucasmerlin marked this conversation as resolved.
Show resolved Hide resolved
Self {
points_per_second: f32::INFINITY,
duration: Rangef::new(t, t),
}
}

pub fn ui(&mut self, ui: &mut crate::Ui) {
crate::Grid::new("scroll_animation").show(ui, |ui| {
ui.label("Scroll animation:");
ui.add(
DragValue::new(&mut self.points_per_second)
.speed(100.0)
.range(0.0..=5000.0),
);
ui.label("points/second");
ui.end_row();

ui.label("Min duration:");
ui.add(
DragValue::new(&mut self.duration.min)
.speed(0.01)
.range(0.0..=self.duration.max),
);
ui.label("seconds");
ui.end_row();

ui.label("Max duration:");
ui.add(
DragValue::new(&mut self.duration.max)
.speed(0.01)
.range(0.0..=1.0),
);
ui.label("seconds");
ui.end_row();
});
}
}

// ----------------------------------------------------------------------------

/// How and when interaction happens.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
Expand Down Expand Up @@ -1129,6 +1214,7 @@ impl Default for Style {
explanation_tooltips: false,
url_in_tooltip: false,
always_scroll_the_only_direction: false,
scroll_animation: ScrollAnimation::default(),
}
}
}
Expand Down Expand Up @@ -1425,6 +1511,7 @@ impl Style {
explanation_tooltips,
url_in_tooltip,
always_scroll_the_only_direction,
scroll_animation,
} = self;

visuals.light_dark_radio_buttons(ui);
Expand Down Expand Up @@ -1488,6 +1575,7 @@ impl Style {
ui.collapsing("📏 Spacing", |ui| spacing.ui(ui));
ui.collapsing("☝ Interaction", |ui| interaction.ui(ui));
ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui));
ui.collapsing("🔄 Scroll Animation", |ui| scroll_animation.ui(ui));

#[cfg(debug_assertions)]
ui.collapsing("🐛 Debug", |ui| debug.ui(ui));
Expand Down
40 changes: 34 additions & 6 deletions crates/egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use crate::{
containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
util::IdTypeMap, widgets::*, *,
};

// ----------------------------------------------------------------------------

/// This is what you use to place widgets.
Expand Down Expand Up @@ -1215,10 +1214,22 @@ impl Ui {
/// # });
/// ```
pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) {
self.scroll_to_rect_animation(rect, align, self.style.scroll_animation);
}

/// Same as [`Self::scroll_to_rect`], but allows you to specify the [`style::ScrollAnimation`].
pub fn scroll_to_rect_animation(
&self,
rect: Rect,
align: Option<Align>,
animation: style::ScrollAnimation,
) {
for d in 0..2 {
let range = Rangef::new(rect.min[d], rect.max[d]);
self.ctx()
.frame_state_mut(|state| state.scroll_target[d] = Some((range, align)));
self.ctx().frame_state_mut(|state| {
state.scroll_target[d] =
Some(frame_state::ScrollTarget::new(range, align, animation));
});
}
}

Expand All @@ -1245,11 +1256,22 @@ impl Ui {
/// # });
/// ```
pub fn scroll_to_cursor(&self, align: Option<Align>) {
self.scroll_to_cursor_animation(align, self.style.scroll_animation);
}

/// Same as [`Self::scroll_to_cursor`], but allows you to specify the [`style::ScrollAnimation`].
pub fn scroll_to_cursor_animation(
&self,
align: Option<Align>,
animation: style::ScrollAnimation,
) {
let target = self.next_widget_position();
for d in 0..2 {
let target = Rangef::point(target[d]);
self.ctx()
.frame_state_mut(|state| state.scroll_target[d] = Some((target, align)));
self.ctx().frame_state_mut(|state| {
state.scroll_target[d] =
Some(frame_state::ScrollTarget::new(target, align, animation));
});
}
}

Expand Down Expand Up @@ -1283,8 +1305,14 @@ impl Ui {
/// # });
/// ```
pub fn scroll_with_delta(&self, delta: Vec2) {
self.scroll_with_delta_animation(delta, self.style.scroll_animation);
}

/// Same as [`Self::scroll_with_delta`], but allows you to specify the [`style::ScrollAnimation`].
pub fn scroll_with_delta_animation(&self, delta: Vec2, animation: style::ScrollAnimation) {
self.ctx().frame_state_mut(|state| {
state.scroll_delta += delta;
state.scroll_delta.0 += delta;
state.scroll_delta.1 = animation;
});
}
}
Expand Down
Loading