From 033bb05526d8d0e93d2628e62d2a01b499cdd62f Mon Sep 17 00:00:00 2001 From: BradySimon Date: Wed, 29 Jan 2025 23:14:37 -0500 Subject: [PATCH 1/4] Animate `radio` widget --- widget/src/radio.rs | 62 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/widget/src/radio.rs b/widget/src/radio.rs index fab29f5654..d23d055ac3 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -56,7 +56,10 @@ //! column![a, b, c, all].into() //! } //! ``` +use std::time::Instant; + use crate::core::alignment; +use crate::core::animation::{Animation, Easing}; use crate::core::border::{self, Border}; use crate::core::layout; use crate::core::mouse; @@ -267,6 +270,17 @@ 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 Widget for Radio<'_, Message, Theme, Renderer> where @@ -275,11 +289,24 @@ 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) + .quick(), + 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 +327,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 +353,7 @@ where fn update( &mut self, - _state: &mut Tree, + tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, @@ -358,8 +384,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.scale_in.is_animating(*now) { + shell.request_redraw(); + } } else if self .last_status .is_some_and(|last_status| last_status != current_status) @@ -422,7 +453,11 @@ where style.background, ); - if self.is_selected { + let state = tree.state.downcast_ref::>(); + if self.is_selected || state.scale_in.is_animating(state.now) { + 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 +466,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, }, From 84c3e5290a7fb31f1338921f41cf8924ef07147f Mon Sep 17 00:00:00 2001 From: BradySimon Date: Thu, 30 Jan 2025 13:11:18 -0500 Subject: [PATCH 2/4] Add feature flag for animations --- Cargo.toml | 2 ++ examples/scrollable/Cargo.toml | 3 +++ widget/Cargo.toml | 1 + widget/src/radio.rs | 31 ++++++++++++++++++++++++++----- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 133c2362c5..6a3971fb41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,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 6d1f054e56..c1795fa4d6 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 = [] [dependencies] iced_renderer.workspace = true diff --git a/widget/src/radio.rs b/widget/src/radio.rs index d23d055ac3..bc7f142be6 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -281,6 +281,20 @@ where pub text_state: widget::text::State, } +impl State +where + Paragraph: text::Paragraph, +{ + /// Whether there is an active animation. + fn is_animating(&self) -> bool { + if cfg!(feature = "animations") { + self.scale_in.is_animating(self.now) + } else { + false + } + } +} + impl Widget for Radio<'_, Message, Theme, Renderer> where @@ -388,7 +402,7 @@ where let state = tree.state.downcast_mut::>(); state.now = *now; self.last_status = Some(current_status); - if state.scale_in.is_animating(*now) { + if state.is_animating() { shell.request_redraw(); } } else if self @@ -454,10 +468,17 @@ where ); let state = tree.state.downcast_ref::>(); - if self.is_selected || state.scale_in.is_animating(state.now) { - 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); + if self.is_selected || state.is_animating() { + let dot_size = if cfg!(feature = "animations") { + state.scale_in.interpolate(0.0, dot_size, state.now) + } else { + dot_size + }; + let alpha = if cfg!(feature = "animations") { + state.scale_in.interpolate(0.0, 1.0, state.now) + } else { + 1.0 + }; renderer.fill_quad( renderer::Quad { bounds: Rectangle { From 74d8367c7850d0f48acbb8096c6d6b8a61c12e7f Mon Sep 17 00:00:00 2001 From: BradySimon Date: Thu, 30 Jan 2025 19:20:51 -0500 Subject: [PATCH 3/4] Use `crate::core::time` instead of `std::time` for radio animation --- widget/src/radio.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/widget/src/radio.rs b/widget/src/radio.rs index bc7f142be6..e23e0c5572 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -56,8 +56,6 @@ //! column![a, b, c, all].into() //! } //! ``` -use std::time::Instant; - use crate::core::alignment; use crate::core::animation::{Animation, Easing}; use crate::core::border::{self, Border}; @@ -65,6 +63,7 @@ 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}; From 4d8ecb6a91ada259210da229c53cb3b1d0591f19 Mon Sep 17 00:00:00 2001 From: BradySimon Date: Thu, 6 Mar 2025 20:10:50 -0500 Subject: [PATCH 4/4] Reduce number of `cfg!` calls --- widget/src/radio.rs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/widget/src/radio.rs b/widget/src/radio.rs index e23e0c5572..42fee9a2e9 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -56,6 +56,8 @@ //! 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}; @@ -286,11 +288,7 @@ where { /// Whether there is an active animation. fn is_animating(&self) -> bool { - if cfg!(feature = "animations") { - self.scale_in.is_animating(self.now) - } else { - false - } + self.scale_in.is_animating(self.now) } } @@ -310,7 +308,11 @@ where now: Instant::now(), scale_in: Animation::new(self.is_selected) .easing(Easing::EaseInOut) - .quick(), + .duration(if cfg!(feature = "animations") { + Duration::from_millis(200) + } else { + Duration::ZERO + }), text_state: widget::text::State::default(), }) } @@ -468,16 +470,9 @@ where let state = tree.state.downcast_ref::>(); if self.is_selected || state.is_animating() { - let dot_size = if cfg!(feature = "animations") { - state.scale_in.interpolate(0.0, dot_size, state.now) - } else { - dot_size - }; - let alpha = if cfg!(feature = "animations") { - state.scale_in.interpolate(0.0, 1.0, state.now) - } else { - 1.0 - }; + 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 {