Make `scroll_to_*` animations configurable (#4305)
<!-- Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md) before opening a Pull Request! * Keep your PR:s small and focused. * The PR title is what ends up in the changelog, so make it descriptive! * If applicable, add a screenshot or gif. * If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example. * Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to add commits to your PR. * Remember to run `cargo fmt` and `cargo cranky`. * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * When you have addressed a PR comment, mark it as resolved. Please be patient! I will review your PR, but my time is limited! --> * Closes #4295 I based this on #4303, I'll rebase once that one gets merged.
This commit is contained in:
parent
ae7672e336
commit
c5bea3113e
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,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);
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,30 @@ pub struct PerLayerState {
|
|||
pub widget_with_tooltip: Option<Id>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScrollTarget {
|
||||
// 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 {
|
||||
|
|
@ -172,7 +196,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.
|
||||
///
|
||||
|
|
@ -183,7 +207,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>,
|
||||
|
|
@ -206,7 +230,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(),
|
||||
|
|
@ -246,7 +270,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)]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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`].
|
||||
|
|
@ -888,9 +887,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,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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 {
|
||||
/// 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 {
|
||||
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 {
|
||||
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))]
|
||||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -1216,10 +1215,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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1246,11 +1257,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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1284,8 +1306,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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue