diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 229870db9d4..ae0eb96b255 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1,5 +1,6 @@ #![allow(clippy::needless_range_loop)] +use crate::style::ScrollAnimation; use crate::*; #[derive(Clone, Copy, Debug)] @@ -789,7 +790,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. @@ -798,7 +800,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]; @@ -807,7 +809,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 = @@ -824,11 +826,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; @@ -842,8 +850,8 @@ impl Prepared { // 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 e1414bba166..c4feaa3d0c5 100644 --- a/crates/egui/src/frame_state.rs +++ b/crates/egui/src/frame_state.rs @@ -1,3 +1,4 @@ +use crate::style::ScrollAnimation; use crate::{id::IdSet, *}; #[derive(Clone, Copy, Debug)] @@ -39,7 +40,7 @@ pub(crate) struct FrameState { pub(crate) tooltip_state: Option, /// The current scroll area should scroll to this range (horizontal, vertical). - pub(crate) scroll_target: [Option<(Rangef, Option)>; 2], + pub(crate) scroll_target: [Option<(Rangef, Option, ScrollAnimation)>; 2], /// The current scroll area should scroll by this much. /// @@ -50,7 +51,7 @@ pub(crate) 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(crate) scroll_delta: Vec2, + pub(crate) scroll_delta: (Vec2, ScrollAnimation), #[cfg(feature = "accesskit")] pub(crate) accesskit_state: Option, @@ -74,7 +75,7 @@ impl Default for FrameState { used_by_panels: Rect::NAN, tooltip_state: None, scroll_target: [None, None], - scroll_delta: Vec2::default(), + scroll_delta: (Vec2::default(), ScrollAnimation::none()), #[cfg(feature = "accesskit")] accesskit_state: None, highlight_this_frame: Default::default(), @@ -112,7 +113,7 @@ impl FrameState { *used_by_panels = Rect::NOTHING; *tooltip_state = None; *scroll_target = [None, None]; - *scroll_delta = Vec2::default(); + *scroll_delta = (Vec2::default(), ScrollAnimation::none()); #[cfg(debug_assertions)] { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 264d53d5983..028eecad045 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -1,5 +1,6 @@ use std::{any::Any, sync::Arc}; +use crate::style::ScrollAnimation; use crate::{ emath::{Align, Pos2, Rect, Vec2}, menu, ComboBox, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetRect, @@ -739,9 +740,13 @@ 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: 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 f2410747dfb..3fb0c279a12 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -229,6 +229,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, } impl Style { @@ -624,6 +627,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( + crate::DragValue::new(&mut self.points_per_second) + .speed(100.0) + .clamp_range(0.0..=5000.0), + ); + ui.label("points/second"); + ui.end_row(); + + ui.label("Min duration:"); + ui.add( + crate::DragValue::new(&mut self.min_duration) + .speed(0.01) + .clamp_range(0.0..=self.max_duration), + ); + ui.label("seconds"); + ui.end_row(); + + ui.label("Max duration:"); + ui.add( + crate::DragValue::new(&mut self.max_duration) + .speed(0.01) + .clamp_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))] @@ -1053,6 +1126,7 @@ impl Default for Style { explanation_tooltips: false, url_in_tooltip: false, always_scroll_the_only_direction: false, + scroll_animation: ScrollAnimation::default(), } } } @@ -1346,6 +1420,7 @@ impl Style { explanation_tooltips, url_in_tooltip, always_scroll_the_only_direction, + scroll_animation, } = self; visuals.light_dark_radio_buttons(ui); @@ -1409,6 +1484,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 f3227109e36..b6ddf6a5b85 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -5,6 +5,7 @@ use std::{any::Any, hash::Hash, sync::Arc}; use epaint::mutex::RwLock; +use crate::style::ScrollAnimation; use crate::{ containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer, util::IdTypeMap, widgets::*, *, @@ -1052,10 +1053,19 @@ impl Ui { /// # }); /// ``` pub fn scroll_to_rect(&self, rect: Rect, align: Option) { + self.scroll_to_rect_animation(rect, align, self.style.scroll_animation) + } + + pub fn scroll_to_rect_animation( + &self, + rect: Rect, + align: Option, + animation: 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))); } } @@ -1082,11 +1092,15 @@ impl Ui { /// # }); /// ``` pub fn scroll_to_cursor(&self, align: Option) { + self.scroll_to_cursor_animation(align, self.style.scroll_animation) + } + + pub fn scroll_to_cursor_animation(&self, align: Option, animation: 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))); } } @@ -1120,8 +1134,13 @@ impl Ui { /// # }); /// ``` pub fn scroll_with_delta(&self, delta: Vec2) { + self.scroll_with_delta_animation(delta, self.style.scroll_animation) + } + + pub fn scroll_with_delta_animation(&self, delta: Vec2, animation: ScrollAnimation) { self.ctx().frame_state_mut(|state| { - state.scroll_delta += delta; + state.scroll_delta.0 += delta; + state.scroll_delta.1 = animation; }); } }