feat(progress_bar): animate determinate progress

This commit is contained in:
Vukašin Vojinović 2026-04-20 14:00:09 +02:00 committed by Ashley Wulber
parent 4d39cf3d7b
commit 8b4c8adec8
3 changed files with 95 additions and 55 deletions

View file

@ -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 {

View file

@ -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<f32>,
}
@ -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<f32>,
progress: Progress,
}
impl<Message, Theme> Widget<Message, Theme, Renderer> for Circular<Theme>
@ -145,23 +145,21 @@ where
_viewport: &Rectangle,
) {
let state = tree.state.downcast_mut::<State>();
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

View file

@ -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<f32>,
}
@ -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<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Linear<Theme>
where
Message: Clone,
@ -97,11 +103,11 @@ where
Renderer: advanced::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<Animation>()
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(Animation::default())
tree::State::new(State::default())
}
fn size(&self) -> Size<Length> {
@ -131,20 +137,21 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) {
if self.progress.is_some() {
return;
}
let animation = tree.state.downcast_mut::<Animation>();
let state = tree.state.downcast_mut::<State>();
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::<Animation>();
let state = tree.state.downcast_ref::<State>();
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);