diff --git a/core/Cargo.toml b/core/Cargo.toml index 6bb646c6cc..eb150fa4f0 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -32,7 +32,7 @@ web-time.workspace = true dark-light.workspace = true dark-light.optional = true -lilt = "0.5.0" +lilt = "0.6.0" [dev-dependencies] approx = "0.5" diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 15f4d2f6a9..013c9d14b9 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -41,4 +41,4 @@ ouroboros.optional = true qrcode.workspace = true qrcode.optional = true -lilt = "0.5.0" +lilt = "0.6.0" diff --git a/widget/src/button.rs b/widget/src/button.rs index 5a65672d38..dfcdddbfb4 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -579,7 +579,7 @@ pub enum Status { /// The [`AnimationTarget`] represents, through its ['FloatRepresentable`] /// implementation the ratio of color mixing between the base and hover colors. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq)] enum AnimationTarget { Active, Hovered, diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 039456763d..46e51c8f15 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -10,14 +10,19 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ self, Background, Border, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::task::{self, Task}; -use crate::runtime::Action; - +use crate::runtime::{ + task::{self, Task}, + Action, +}; +use lilt::Animated; +use lilt::Easing::EaseOut; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; +use std::time::Instant; /// A widget that can vertically display an infinite amount of content with a /// scrollbar. @@ -38,6 +43,7 @@ pub struct Scrollable< content: Element<'a, Message, Theme, Renderer>, on_scroll: Option Message + 'a>>, class: Theme::Class<'a>, + animation_duration_ms: f32, } impl<'a, Message, Theme, Renderer> Scrollable<'a, Message, Theme, Renderer> @@ -57,6 +63,7 @@ where content: content.into(), on_scroll: None, class: Theme::default(), + animation_duration_ms: 200., } .validate() } @@ -305,7 +312,7 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::new(self.animation_duration_ms)) } fn children(&self) -> Vec { @@ -444,10 +451,14 @@ where return event::Status::Ignored; }; - state.scroll_y_to(scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - )); + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + true, + ); + shell.request_redraw(window::RedrawRequest::NextFrame); let _ = notify_on_scroll( state, @@ -476,10 +487,14 @@ where scrollbars.grab_y_scroller(cursor_position), scrollbars.y, ) { - state.scroll_y_to(scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - )); + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + false, + ); + shell.request_redraw(window::RedrawRequest::NextFrame); state.y_scroller_grabbed_at = Some(scroller_grabbed_at); @@ -507,10 +522,14 @@ where }; if let Some(scrollbar) = scrollbars.x { - state.scroll_x_to(scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - )); + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + true, + ); + shell.request_redraw(window::RedrawRequest::NextFrame); let _ = notify_on_scroll( state, @@ -539,10 +558,14 @@ where scrollbars.grab_x_scroller(cursor_position), scrollbars.x, ) { - state.scroll_x_to(scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - )); + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + false, + ); + shell.request_redraw(window::RedrawRequest::NextFrame); state.x_scroller_grabbed_at = Some(scroller_grabbed_at); @@ -609,6 +632,19 @@ where state.x_scroller_grabbed_at = None; state.y_scroller_grabbed_at = None; + // Reset animations durations from instantaneous to default. + // This is necessary because we change the animation duration when + // grabbing the scrollbars, and are unable to access the animation + // duration in all methods, such as `scroll_to` and `snap_to`. + state.y_animation = state + .y_animation + .clone() + .duration(self.animation_duration_ms); + state.x_animation = state + .x_animation + .clone() + .duration(self.animation_duration_ms); + return event_status; } @@ -647,6 +683,7 @@ where }; state.scroll(delta, self.direction, bounds, content_bounds); + shell.request_redraw(window::RedrawRequest::NextFrame); event_status = if notify_on_scroll( state, @@ -692,6 +729,9 @@ where bounds, content_bounds, ); + shell.request_redraw( + window::RedrawRequest::NextFrame, + ); state.scroll_area_touched_at = Some(cursor_position); @@ -711,6 +751,13 @@ where event_status = event::Status::Captured; } + Event::Window(window::Event::RedrawRequested(now)) => { + if state.x_animation.in_progress(now) + || state.y_animation.in_progress(now) + { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } _ => {} } @@ -1087,7 +1134,7 @@ fn notify_on_scroll( true } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] struct State { scroll_area_touched_at: Option, offset_y_relative: f32, @@ -1096,20 +1143,8 @@ struct State { x_scroller_grabbed_at: Option, keyboard_modifiers: keyboard::Modifiers, last_notified: Option, -} - -impl Default for State { - fn default() -> Self { - Self { - scroll_area_touched_at: None, - offset_y_relative: 0.0, - y_scroller_grabbed_at: None, - offset_x_relative: 0.0, - x_scroller_grabbed_at: None, - keyboard_modifiers: keyboard::Modifiers::default(), - last_notified: None, - } - } + y_animation: Animated, + x_animation: Animated, } impl operation::Scrollable for State { @@ -1226,8 +1261,22 @@ impl Viewport { impl State { /// Creates a new [`State`] with the scrollbar(s) at the beginning. - pub fn new() -> Self { - State::default() + pub fn new(animation_duration_ms: f32) -> Self { + Self { + scroll_area_touched_at: None, + offset_y_relative: 0.0, + y_scroller_grabbed_at: None, + offset_x_relative: 0.0, + x_scroller_grabbed_at: None, + keyboard_modifiers: keyboard::Modifiers::default(), + last_notified: None, + y_animation: Animated::new(0.0) + .easing(EaseOut) + .duration(animation_duration_ms), + x_animation: Animated::new(0.0) + .easing(EaseOut) + .duration(animation_duration_ms), + } } /// Apply a scrolling offset to the current [`State`], given the bounds of @@ -1259,6 +1308,7 @@ impl State { align(vertical_alignment, delta.y), ); + let now = Instant::now(); if bounds.height < content_bounds.height { self.offset_y_relative = ((Offset::Relative(self.offset_y_relative) @@ -1266,6 +1316,7 @@ impl State { - delta.y) .clamp(0.0, content_bounds.height - bounds.height)) / (content_bounds.height - bounds.height); + self.y_animation.transition(self.offset_y_relative, now); } if bounds.width < content_bounds.width { @@ -1275,6 +1326,7 @@ impl State { - delta.x) .clamp(0.0, content_bounds.width - bounds.width)) / (content_bounds.width - bounds.width); + self.x_animation.transition(self.offset_x_relative, now); } } @@ -1282,22 +1334,43 @@ impl State { /// /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at /// the end. - pub fn scroll_y_to(&mut self, percentage: f32) { - self.offset_y_relative = percentage.clamp(0.0, 1.0); + /// + /// When `instantaneous` is set to `true`, the transition uses no animation. + pub fn scroll_y_to(&mut self, percentage: f32, instantaneous: bool) { + let percentage = percentage.clamp(0.0, 1.0); + self.offset_y_relative = percentage; + if instantaneous { + self.y_animation + .transition_instantaneous(percentage, Instant::now()); + } else { + self.y_animation.transition(percentage, Instant::now()); + } } /// Scrolls the [`Scrollable`] to a relative amount along the x axis. /// /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at /// the end. - pub fn scroll_x_to(&mut self, percentage: f32) { - self.offset_x_relative = percentage.clamp(0.0, 1.0); + /// + /// When `instantaneous` is set to `true`, the transition uses no animation. + pub fn scroll_x_to(&mut self, percentage: f32, instantaneous: bool) { + let percentage = percentage.clamp(0.0, 1.0); + self.offset_x_relative = percentage; + if instantaneous { + self.x_animation + .transition_instantaneous(percentage, Instant::now()); + } else { + self.x_animation.transition(percentage, Instant::now()); + } } /// Snaps the scroll position to a [`RelativeOffset`]. pub fn snap_to(&mut self, offset: RelativeOffset) { + let now = Instant::now(); self.offset_x_relative = offset.x.clamp(0.0, 1.0); self.offset_y_relative = offset.y.clamp(0.0, 1.0); + self.x_animation.transition(self.offset_x_relative, now); + self.y_animation.transition(self.offset_y_relative, now); } /// Scroll to the provided [`AbsoluteOffset`]. @@ -1307,10 +1380,15 @@ impl State { bounds: Rectangle, content_bounds: Rectangle, ) { + let now = Instant::now(); self.offset_x_relative = Offset::Absolute(offset.x.max(0.0)) - .relative(bounds.width, content_bounds.width); + .relative(bounds.width, content_bounds.width) + .clamp(0.0, 1.0); self.offset_y_relative = Offset::Absolute(offset.y.max(0.0)) - .relative(bounds.height, content_bounds.height); + .relative(bounds.height, content_bounds.height) + .clamp(0.0, 1.0); + self.x_animation.transition(self.offset_x_relative, now); + self.y_animation.transition(self.offset_y_relative, now); } /// Returns the scrolling translation of the [`State`], given a [`Direction`], @@ -1323,7 +1401,10 @@ impl State { ) -> Vector { Vector::new( if let Some(horizontal) = direction.horizontal() { - Offset::Relative(self.offset_x_relative).translation( + Offset::Relative( + self.x_animation.animate(|target| target, Instant::now()), + ) + .translation( bounds.width, content_bounds.width, horizontal.alignment, @@ -1332,7 +1413,10 @@ impl State { 0.0 }, if let Some(vertical) = direction.vertical() { - Offset::Relative(self.offset_y_relative).translation( + Offset::Relative( + self.y_animation.animate(|target| target, Instant::now()), + ) + .translation( bounds.height, content_bounds.height, vertical.alignment, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index fa4bdb84c6..7eae057889 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -98,7 +98,7 @@ pub enum CursorAnimationType { /// The [`AnimationTarget`] represents, through its ['FloatRepresentable`] /// implementation the ratio of opacity of the cursor during it's blink effect. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq)] enum AnimationTarget { Shown, Hidden,