From 8b4c8adec830d74802931b551cd34f5c046ea6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:00:09 +0200 Subject: [PATCH] feat(progress_bar): animate determinate progress --- src/widget/progress_bar/animation.rs | 39 ++++++++++++++++-- src/widget/progress_bar/circular.rs | 50 +++++++++++------------ src/widget/progress_bar/linear.rs | 61 ++++++++++++++++------------ 3 files changed, 95 insertions(+), 55 deletions(-) diff --git a/src/widget/progress_bar/animation.rs b/src/widget/progress_bar/animation.rs index 52a3432..6baf507 100644 --- a/src/widget/progress_bar/animation.rs +++ b/src/widget/progress_bar/animation.rs @@ -2,6 +2,39 @@ use crate::anim::smootherstep; use iced::time::Instant; use std::time::Duration; +const LAG: f32 = 0.1; + +pub struct Progress { + pub current: f32, + last: Instant, +} + +impl Default for Progress { + fn default() -> Self { + Self { + current: 0.0, + last: Instant::now(), + } + } +} + +impl Progress { + /// Smoothly chases `target` using exponential decay. + /// Returns `true` if still animating and a redraw should be requested. + pub fn update(&mut self, target: f32, now: Instant) -> bool { + let dt = (now - self.last).as_secs_f32(); + self.last = now; + let next = self.current + (target - self.current) * (1.0 - (-dt / LAG).exp()); + if (next - target).abs() > 0.001 { + self.current = next; + true + } else { + self.current = target; + false + } + } +} + #[derive(Clone, Copy)] pub struct Animation { expanding: bool, @@ -26,12 +59,12 @@ impl Animation { pub fn timed_transition( &self, cycle_duration: Duration, - rotation_duration: Duration, + period: Duration, wrap: f32, now: Instant, ) -> Self { - let additional = ((now - self.last).as_secs_f32() / rotation_duration.as_secs_f32() - * u32::MAX as f32) as u32; + let additional = + ((now - self.last).as_secs_f32() / period.as_secs_f32() * u32::MAX as f32) as u32; let new_offset = self.offset.wrapping_add(additional); if !cycle_duration.is_zero() && now.duration_since(self.start) > cycle_duration { diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index 684467f..42c8e64 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -1,5 +1,5 @@ //! Show a circular progress indicator. -use super::animation::Animation; +use super::animation::{Animation, Progress}; use super::style::StyleSheet; use iced::advanced::layout; use iced::advanced::renderer; @@ -24,7 +24,7 @@ where bar_height: f32, style: Theme::Style, cycle_duration: Duration, - rotation_duration: Duration, + period: Duration, progress: Option, } @@ -39,7 +39,7 @@ where bar_height: 4.0, style: Theme::Style::default(), cycle_duration: Duration::from_millis(1500), - rotation_duration: Duration::from_secs(2), + period: Duration::from_secs(2), progress: None, } } @@ -68,10 +68,10 @@ where self } - /// Sets the base rotation duration of this [`Circular`]. This is the duration that a full - /// rotation would take if the cycle duration were set to 0.0 (no expanding or contracting) - pub fn rotation_duration(mut self, duration: Duration) -> Self { - self.rotation_duration = duration; + /// Sets the base period of this [`Circular`]. This is the duration that a full rotation + /// would take if the cycle duration were set to 0.0 (no expanding or contracting) + pub fn period(mut self, duration: Duration) -> Self { + self.period = duration; self } @@ -101,7 +101,7 @@ where struct State { animation: Animation, cache: canvas::Cache, - progress: Option, + progress: Progress, } impl Widget for Circular @@ -145,23 +145,21 @@ where _viewport: &Rectangle, ) { let state = tree.state.downcast_mut::(); - if self.progress.is_some() { - if state.progress != self.progress { - state.progress = self.progress; - state.cache.clear(); - } - return; - } if let Event::Window(window::Event::RedrawRequested(now)) = event { - let (_, wrap) = self.min_wrap(self.size / 2.0 - self.bar_height); - state.animation = state.animation.timed_transition( - self.cycle_duration, - self.rotation_duration, - wrap, - *now, - ); - state.cache.clear(); - shell.request_redraw(); + if let Some(target) = self.progress { + if state.progress.update(target, *now) { + state.cache.clear(); + shell.request_redraw(); + } + } else { + let (_, wrap) = self.min_wrap(self.size / 2.0 - self.bar_height); + state.animation = + state + .animation + .timed_transition(self.cycle_duration, self.period, wrap, *now); + state.cache.clear(); + shell.request_redraw(); + } } } @@ -231,7 +229,7 @@ where draw_cap(frame, start, true); }; - if let Some(progress) = self.progress { + if self.progress.is_some() { if let Some(border_color) = custom_style.border_color { for radius_offset in [self.bar_height / 2.0, -(self.bar_height / 2.0)] { let border_path = @@ -244,7 +242,7 @@ where ); } } - draw_bar(frame, 0.0, progress); + draw_bar(frame, 0.0, state.progress.current); } else { let (min, wrap) = self.min_wrap(track_radius); let (start, end) = state diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 881d59c..7bcbb4f 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -1,5 +1,5 @@ //! Show a linear progress indicator. -use super::animation::Animation; +use super::animation::{Animation, Progress}; use super::style::StyleSheet; use iced::advanced::layout; use iced::advanced::renderer; @@ -23,7 +23,7 @@ where girth: Length, style: Theme::Style, cycle_duration: Duration, - traversal_duration: Duration, + period: Duration, progress: Option, } @@ -38,7 +38,7 @@ where girth: Length::Fixed(4.0), style: Theme::Style::default(), cycle_duration: Duration::from_millis(1500), - traversal_duration: Duration::from_secs(2), + period: Duration::from_secs(2), progress: None, } } @@ -67,10 +67,10 @@ where self } - /// Sets the base traversal duration of this [`Linear`]. This is the duration that a full - /// traversal would take if the cycle duration were set to 0.0 (no expanding or contracting) - pub fn traversal_duration(mut self, duration: Duration) -> Self { - self.traversal_duration = duration; + /// Sets the base period of this [`Linear`]. This is the duration that a full traversal + /// would take if the cycle duration were set to 0.0 (no expanding or contracting) + pub fn period(mut self, duration: Duration) -> Self { + self.period = duration; self } @@ -90,6 +90,12 @@ where } } +#[derive(Default)] +struct State { + animation: Animation, + progress: Progress, +} + impl Widget for Linear where Message: Clone, @@ -97,11 +103,11 @@ where Renderer: advanced::Renderer, { fn tag(&self) -> tree::Tag { - tree::Tag::of::() + tree::Tag::of::() } fn state(&self) -> tree::State { - tree::State::new(Animation::default()) + tree::State::new(State::default()) } fn size(&self) -> Size { @@ -131,20 +137,21 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) { - if self.progress.is_some() { - return; - } - - let animation = tree.state.downcast_mut::(); - + let state = tree.state.downcast_mut::(); if let Event::Window(window::Event::RedrawRequested(now)) = event { - *animation = animation.timed_transition( - self.cycle_duration, - self.traversal_duration, - WRAP_LENGTH, - *now, - ); - shell.request_redraw(); + if let Some(target) = self.progress { + if state.progress.update(target, *now) { + shell.request_redraw(); + } + } else { + state.animation = state.animation.timed_transition( + self.cycle_duration, + self.period, + WRAP_LENGTH, + *now, + ); + shell.request_redraw(); + } } } @@ -160,7 +167,7 @@ where ) { let bounds = layout.bounds(); let custom_style = theme.appearance(&self.style, self.progress.is_some(), false); - let animation = tree.state.downcast_ref::(); + let state = tree.state.downcast_ref::(); renderer.fill_quad( renderer::Quad { @@ -203,11 +210,13 @@ where } }; - if let Some(progress) = self.progress { - draw_segment(0.0, progress); + if self.progress.is_some() { + draw_segment(0.0, state.progress.current); } else { let (bar_start, bar_end) = - animation.bar_positions(self.cycle_duration, MIN_LENGTH, WRAP_LENGTH); + state + .animation + .bar_positions(self.cycle_duration, MIN_LENGTH, WRAP_LENGTH); let length = bar_end - bar_start; let start = bar_start % 1.0; let right_width = (1.0 - start).min(length);