From 8c36ee7961cb7304a8c49f8e24f8decfd121c66e Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 2 Apr 2024 14:16:58 +0200 Subject: [PATCH] Make scroll animation configurable via Style and scroll_to_*_animation functions --- crates/egui/src/containers/scroll_area.rs | 22 ++++--- crates/egui/src/frame_state.rs | 8 +-- crates/egui/src/response.rs | 12 +++- crates/egui/src/style.rs | 76 +++++++++++++++++++++++ crates/egui/src/ui.rs | 31 ++++++++- 5 files changed, 132 insertions(+), 17 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index e564fbe1e4ab..bfe3cc614b38 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -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. @@ -808,7 +809,7 @@ impl Prepared { .frame_state_mut(|state| state.scroll_target[d].take()); if scroll_enabled[d] { - delta += if let Some((target_range, align)) = scroll_target { + let update = if let Some((target_range, align, animation)) = 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]; @@ -817,7 +818,7 @@ impl Prepared { let clip_end = clip_rect.max[d]; let mut spacing = ui.spacing().item_spacing[d]; - if let Some(align) = align { + let delta = if let Some(align) = align { let center_factor = align.to_factor(); let offset = @@ -834,11 +835,17 @@ impl Prepared { } else { // Ui is already in view, no need to adjust scroll. 0.0 - } + }; + Some((delta, animation)) } else { - 0.0 + None }; + if let Some((delta_update, animation_update)) = update { + delta += delta_update; + animation = animation_update; + } + if delta != 0.0 { let target_offset = state.offset[d] + delta; @@ -850,10 +857,9 @@ 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); + let animation_duration = (delta.abs() / animation.points_per_second) + .clamp(animation.min_duration, animation.max_duration); state.offset_target[d] = Some(ScrollTarget { animation_time_span: (now, now + animation_duration as f64), target_offset, diff --git a/crates/egui/src/frame_state.rs b/crates/egui/src/frame_state.rs index 184ab0d64a85..e612c4d902b8 100644 --- a/crates/egui/src/frame_state.rs +++ b/crates/egui/src/frame_state.rs @@ -79,7 +79,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)>; 2], + pub scroll_target: [Option<(Rangef, Option, style::ScrollAnimation)>; 2], /// The current scroll area should scroll by this much. /// @@ -90,7 +90,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, @@ -113,7 +113,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()), #[cfg(feature = "accesskit")] accesskit_state: None, highlight_next_frame: Default::default(), @@ -153,7 +153,7 @@ impl FrameState { *unused_rect = screen_rect; *used_by_panels = Rect::NOTHING; *scroll_target = [None, None]; - *scroll_delta = Vec2::default(); + *scroll_delta = (Vec2::default(), style::ScrollAnimation::none()); #[cfg(debug_assertions)] { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 04b11fde280e..1b9a2a6b445e 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -846,9 +846,17 @@ impl Response { /// # }); /// ``` pub fn scroll_to_me(&self, align: Option) { + self.scroll_to_me_animation(align, self.ctx.style().scroll_animation); + } + + pub fn scroll_to_me_animation( + &self, + align: Option, + 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((self.rect.x_range(), align, animation)); + state.scroll_target[1] = Some((self.rect.y_range(), align, animation)); }); } diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 6009a6b6376e..159c28f77fb2 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -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] @@ -692,6 +695,76 @@ impl ScrollStyle { // ---------------------------------------------------------------------------- +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct ScrollAnimation { + pub points_per_second: f32, + pub min_duration: f32, + pub max_duration: f32, +} + +impl Default for ScrollAnimation { + fn default() -> Self { + Self { + points_per_second: 1000.0, + min_duration: 0.1, + max_duration: 0.3, + } + } +} + +impl ScrollAnimation { + pub fn none() -> Self { + Self { + points_per_second: 0.0, + min_duration: 0.0, + max_duration: 0.0, + } + } + + pub fn duration(t: f32) -> Self { + Self { + points_per_second: 0.0, + min_duration: t, + max_duration: 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.min_duration) + .speed(0.01) + .range(0.0..=self.max_duration), + ); + ui.label("seconds"); + ui.end_row(); + + ui.label("Max duration:"); + ui.add( + DragValue::new(&mut self.max_duration) + .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))] @@ -1129,6 +1202,7 @@ impl Default for Style { explanation_tooltips: false, url_in_tooltip: false, always_scroll_the_only_direction: false, + scroll_animation: ScrollAnimation::default(), } } } @@ -1425,6 +1499,7 @@ impl Style { explanation_tooltips, url_in_tooltip, always_scroll_the_only_direction, + scroll_animation, } = self; visuals.light_dark_radio_buttons(ui); @@ -1488,6 +1563,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)); diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index a7a7712b7e20..0e61192ca1d2 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1215,10 +1215,20 @@ impl Ui { /// # }); /// ``` pub fn scroll_to_rect(&self, rect: Rect, align: Option) { + self.scroll_to_rect_animation(rect, align, self.style.scroll_animation); + } + + /// Same as [`Self::scroll_to_rect`], but allows you to specify the [`ScrollAnimation`]. + pub fn scroll_to_rect_animation( + &self, + rect: Rect, + align: Option, + 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))); + .frame_state_mut(|state| state.scroll_target[d] = Some((range, align, animation))); } } @@ -1245,11 +1255,20 @@ impl Ui { /// # }); /// ``` pub fn scroll_to_cursor(&self, align: Option) { + self.scroll_to_cursor_animation(align, self.style.scroll_animation); + } + + /// Same as [`Self::scroll_to_cursor`], but allows you to specify the [`ScrollAnimation`]. + pub fn scroll_to_cursor_animation( + &self, + align: Option, + 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))); + .frame_state_mut(|state| state.scroll_target[d] = Some((target, align, animation))); } } @@ -1283,8 +1302,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 [`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; }); } }