diff --git a/Cargo.toml b/Cargo.toml index 3c19290bdf..ce86de6cc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,8 @@ strict-assertions = ["iced_renderer/strict-assertions"] unconditional-rendering = ["iced_winit/unconditional-rendering"] # Enables support for the `sipper` library sipper = ["iced_runtime/sipper"] +# Enables widget animations +animations = ["iced_widget/animations"] [dependencies] iced_debug.workspace = true diff --git a/examples/scrollable/Cargo.toml b/examples/scrollable/Cargo.toml index 23f4bc2d89..9b86d69d7f 100644 --- a/examples/scrollable/Cargo.toml +++ b/examples/scrollable/Cargo.toml @@ -8,3 +8,6 @@ publish = false [dependencies] iced.workspace = true iced.features = ["debug"] + +[features] +animations = ["iced/animations"] diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 024314ff54..b2b5b636cf 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -27,6 +27,7 @@ wgpu = ["iced_renderer/wgpu"] markdown = ["dep:pulldown-cmark", "dep:url"] highlighter = ["dep:iced_highlighter"] advanced = [] +animations = [] crisp = [] [dependencies] diff --git a/widget/src/radio.rs b/widget/src/radio.rs index fab29f5654..42fee9a2e9 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -56,12 +56,16 @@ //! column![a, b, c, all].into() //! } //! ``` +use std::time::Duration; + use crate::core::alignment; +use crate::core::animation::{Animation, Easing}; use crate::core::border::{self, Border}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::text; +use crate::core::time::Instant; use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; @@ -267,6 +271,27 @@ where } } +struct State +where + Paragraph: text::Paragraph, +{ + /// The last update instant - used for animations. + pub now: Instant, + /// Animation scaling the dot in and out. + pub scale_in: Animation, + pub text_state: widget::text::State, +} + +impl State +where + Paragraph: text::Paragraph, +{ + /// Whether there is an active animation. + fn is_animating(&self) -> bool { + self.scale_in.is_animating(self.now) + } +} + impl Widget for Radio<'_, Message, Theme, Renderer> where @@ -275,11 +300,28 @@ where Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { - tree::Tag::of::>() + tree::Tag::of::>() } fn state(&self) -> tree::State { - tree::State::new(widget::text::State::::default()) + tree::State::new(State:: { + now: Instant::now(), + scale_in: Animation::new(self.is_selected) + .easing(Easing::EaseInOut) + .duration(if cfg!(feature = "animations") { + Duration::from_millis(200) + } else { + Duration::ZERO + }), + text_state: widget::text::State::default(), + }) + } + + fn diff(&self, tree: &mut Tree) { + let state = tree.state.downcast_mut::>(); + if self.is_selected != state.scale_in.value() { + state.scale_in.go_mut(self.is_selected, Instant::now()); + } } fn size(&self) -> Size { @@ -300,12 +342,11 @@ where self.spacing, |_| layout::Node::new(Size::new(self.size, self.size)), |limits| { - let state = tree - .state - .downcast_mut::>(); + let state = + tree.state.downcast_mut::>(); widget::text::layout( - state, + &mut state.text_state, renderer, limits, &self.label, @@ -327,7 +368,7 @@ where fn update( &mut self, - _state: &mut Tree, + tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, @@ -358,8 +399,13 @@ where } }; - if let Event::Window(window::Event::RedrawRequested(_now)) = event { + if let Event::Window(window::Event::RedrawRequested(now)) = event { + let state = tree.state.downcast_mut::>(); + state.now = *now; self.last_status = Some(current_status); + if state.is_animating() { + shell.request_redraw(); + } } else if self .last_status .is_some_and(|last_status| last_status != current_status) @@ -422,7 +468,11 @@ where style.background, ); - if self.is_selected { + let state = tree.state.downcast_ref::>(); + if self.is_selected || state.is_animating() { + let dot_size = + state.scale_in.interpolate(0.0, dot_size, state.now); + let alpha = state.scale_in.interpolate(0.0, 1.0, state.now); renderer.fill_quad( renderer::Quad { bounds: Rectangle { @@ -431,24 +481,23 @@ where width: bounds.width - dot_size, height: bounds.height - dot_size, }, - border: border::rounded(dot_size / 2.0), + border: border::rounded(size / 2.0), ..renderer::Quad::default() }, - style.dot_color, + style.dot_color.scale_alpha(alpha), ); } } { let label_layout = children.next().unwrap(); - let state: &widget::text::State = - tree.state.downcast_ref(); + let state: &State = tree.state.downcast_ref(); crate::text::draw( renderer, defaults, label_layout.bounds(), - state.raw(), + state.text_state.raw(), crate::text::Style { color: style.text_color, },