From f8a73b7de909d72a90f9908b61fbdde88e0debcb Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 13 Jan 2024 19:00:45 +0000 Subject: [PATCH] Implement animated scroll_to_top on scrollable --- widget/Cargo.toml | 1 + widget/src/overlay/menu.rs | 3 +- widget/src/pick_list.rs | 6 +-- widget/src/scrollable.rs | 75 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 128a7c387b..871f46687d 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -21,6 +21,7 @@ qr_code = ["canvas", "qrcode"] unicode-segmentation = "1.6" num-traits = "0.2" thiserror = "1" +keyframe = "1.1" [dependencies.iced_runtime] version = "0.1" diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index f7bdeef6e5..597bf7f8b3 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -41,7 +41,7 @@ where impl<'a, T, Message, Renderer> Menu<'a, T, Message, Renderer> where T: ToString + Clone, - Message: 'a, + Message: 'a + Clone, Renderer: text::Renderer + 'a, Renderer::Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet, @@ -177,6 +177,7 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet, + Message: Clone, { pub fn new( menu: Menu<'a, T, Message, Renderer>, diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 0a1e2a9962..9b4203d09e 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -147,7 +147,7 @@ impl<'a, T: 'a, Message, Renderer> Widget where T: Clone + ToString + Eq + 'static, [T]: ToOwned>, - Message: 'a, + Message: 'a + Clone, Renderer: text::Renderer + 'a, Renderer::Theme: StyleSheet + scrollable::StyleSheet @@ -281,7 +281,7 @@ impl<'a, T: 'a, Message, Renderer> From> where T: Clone + ToString + Eq + 'static, [T]: ToOwned>, - Message: 'a, + Message: 'a + Clone, Renderer: text::Renderer + 'a, Renderer::Theme: StyleSheet + scrollable::StyleSheet @@ -545,7 +545,7 @@ pub fn overlay<'a, T, Message, Renderer>( ) -> Option> where T: Clone + ToString, - Message: 'a, + Message: 'a + Clone, Renderer: text::Renderer + 'a, Renderer::Theme: StyleSheet + scrollable::StyleSheet diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index a83ed985ce..48b308ba0c 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,6 +1,8 @@ //! Navigate an endless amount of content with a scrollbar. +use std::time::Instant; +use keyframe::{keyframes, AnimationSequence, functions::EaseOutQuint}; use crate::core::event::{self, Event}; -use crate::core::keyboard; +use crate::core::{keyboard, window}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; @@ -17,6 +19,7 @@ use crate::runtime::Command; pub use crate::style::scrollable::{Scrollbar, Scroller, StyleSheet}; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; +use crate::core::window::RedrawRequest; /// A widget that can vertically display an infinite amount of content with a /// scrollbar. @@ -32,7 +35,9 @@ where direction: Direction, content: Element<'a, Message, Renderer>, on_scroll: Option Message + 'a>>, + on_animate_finished: Option, style: ::Style, + scroll_to_top: bool, } impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer> @@ -49,10 +54,24 @@ where direction: Default::default(), content: content.into(), on_scroll: None, + on_animate_finished: None, style: Default::default(), + scroll_to_top: false, } } + /// Message to send when a `scroll_to_top` finishes. + pub fn on_animate_finished(mut self, msg: Message) -> Self { + self.on_animate_finished = Some(msg); + self + } + + /// Scrolls the scrollable to the top. + pub fn scroll_to_top(mut self, v: bool) -> Self { + self.scroll_to_top = v; + self + } + /// Sets the [`Id`] of the [`Scrollable`]. pub fn id(mut self, id: Id) -> Self { self.id = Some(id); @@ -203,6 +222,7 @@ impl<'a, Message, Renderer> Widget where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, + Message: Clone, { fn tag(&self) -> tree::Tag { tree::Tag::of::() @@ -301,6 +321,8 @@ where shell, self.direction, &self.on_scroll, + self.on_animate_finished.as_ref(), + self.scroll_to_top, |event, layout, cursor, clipboard, shell, viewport| { self.content.as_widget_mut().on_event( &mut tree.children[0], @@ -406,6 +428,7 @@ where Message: 'a, Renderer: 'a + crate::core::Renderer, Renderer::Theme: StyleSheet, + Message: Clone, { fn from( text_input: Scrollable<'a, Message, Renderer>, @@ -491,7 +514,7 @@ pub fn layout( /// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] /// accordingly. -pub fn update( +pub fn update( state: &mut State, event: Event, layout: Layout<'_>, @@ -500,6 +523,8 @@ pub fn update( shell: &mut Shell<'_, Message>, direction: Direction, on_scroll: &Option Message + '_>>, + on_animate_finished: Option<&Message>, + scroll_to_top: bool, update_content: impl FnOnce( Event, Layout<'_>, @@ -515,6 +540,47 @@ pub fn update( let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); + if scroll_to_top { + let (last_render, keyframes) = state.animate.get_or_insert_with(|| { + let offs = match state.offset_y { + Offset::Absolute(v) => v, + Offset::Relative(_) => panic!(), + }; + let keyframes = keyframes![(offs, 0.0, EaseOutQuint), (0.0, 0.5)]; + (Instant::now(), keyframes) + }); + + if let Event::Window(window::Event::RedrawRequested(_)) = event { + let _ = keyframes.advance_by(last_render.elapsed().as_secs_f64()); + state.offset_y = Offset::Absolute(keyframes.now()); + *last_render = Instant::now(); + let finished = keyframes.finished(); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + + if finished { + if let Some(msg) = on_animate_finished { + shell.publish(msg.clone()); + } + } else { + shell.request_redraw(RedrawRequest::NextFrame); + } + + return event::Status::Captured; + } else { + // ignore any other events while we're animating + return event::Status::Ignored; + } + } else { + state.animate = None; + } + let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = @@ -1033,7 +1099,8 @@ fn notify_on_scroll( } /// The local state of a [`Scrollable`]. -#[derive(Debug, Clone, Copy)] +#[derive(Clone)] +#[allow(missing_debug_implementations)] pub struct State { scroll_area_touched_at: Option, offset_y: Offset, @@ -1042,6 +1109,7 @@ pub struct State { x_scroller_grabbed_at: Option, keyboard_modifiers: keyboard::Modifiers, last_notified: Option, + animate: Option<(Instant, AnimationSequence)>, } impl Default for State { @@ -1054,6 +1122,7 @@ impl Default for State { x_scroller_grabbed_at: None, keyboard_modifiers: keyboard::Modifiers::default(), last_notified: None, + animate: None, } } }