From 76bc13ff40eec798a0937d87fce20b699688c69a 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: Tue, 23 Jun 2026 18:30:28 +0200 Subject: [PATCH] feat(progress_bar): improve determinate animations Switches to using a critically damped spring animation, which prevents issues with rapid progress updates, and is generally smoother. --- examples/application/src/main.rs | 18 +++++++- src/widget/progress_bar/animation.rs | 33 +++++++------- src/widget/progress_bar/linear.rs | 68 ++++++++++------------------ 3 files changed, 57 insertions(+), 62 deletions(-) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 05841f5b..2b4999b1 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -84,6 +84,7 @@ pub enum Message { Hi2, Hi3, Tick, + ValueChanged(f32), } /// The [`App`] stores application-specific state. @@ -95,6 +96,7 @@ pub struct App { hidden: bool, keybinds: HashMap, progress: f32, + progress_slider: f32, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -137,6 +139,7 @@ impl cosmic::Application for App { hidden: true, keybinds: HashMap::new(), progress: 0.0, + progress_slider: 0.0, }; let command = app.update_title(); @@ -185,6 +188,9 @@ impl cosmic::Application for App { Message::Tick => { self.progress = (self.progress + 0.01) % 1.0; } + Message::ValueChanged(value) => { + self.progress_slider = value; + } } Task::none() } @@ -201,7 +207,7 @@ impl cosmic::Application for App { .map_or("No page selected", String::as_str); let centered = widget::container( - widget::column::with_capacity(14) + widget::column::with_capacity(16) .push(widget::text::body(page_content)) .push( widget::text_input::text_input("", &self.input_1) @@ -240,6 +246,16 @@ impl cosmic::Application for App { widget::progress_bar::linear::Linear::new() .girth(10.0) .progress(self.progress) + .width(Length::Fill), + ) + .push( + widget::slider(0.0..=1.0, self.progress_slider, Message::ValueChanged) + .step(0.001), + ) + .push( + widget::progress_bar::linear::Linear::new() + .girth(10.0) + .progress(self.progress_slider) .width(Length::Fill) .markers([0.25, 0.5, 0.75]) .segment_spacing(2), diff --git a/src/widget/progress_bar/animation.rs b/src/widget/progress_bar/animation.rs index a9d52831..0be5c0d3 100644 --- a/src/widget/progress_bar/animation.rs +++ b/src/widget/progress_bar/animation.rs @@ -2,45 +2,44 @@ use crate::anim::smootherstep; use iced::time::Instant; use std::time::Duration; -const LAG: f32 = 0.1; +/// Angular frequency of the critically damped spring (snappiness) +const OMEGA: f32 = 15.0; #[derive(Default)] pub struct Progress { pub current: f32, - target: Option, + velocity: f32, last: Option, } impl Progress { - /// Smoothly chases `target` using exponential decay. + /// Chases `target` using a critically damped spring. /// Returns `true` if a redraw should be requested. pub fn update(&mut self, target: f32, now: Instant) -> bool { // Don't animate on start let Some(last) = self.last else { self.current = target; - self.target = Some(target); + self.velocity = 0.0; self.last = Some(now); return false; }; - // Sync animation clock when target changes - if self.target != Some(target) { - self.target = Some(target); - self.last = Some(now); - return true; - } - let dt = (now - last).as_secs_f32(); self.last = Some(now); - let diff = target - self.current; + let displacement = self.current - target; - if diff.abs() > 0.001 { - self.current += diff * (1.0 - (-dt / LAG).exp()); - true - } else { + if displacement.abs() < 0.001 && self.velocity.abs() < 0.001 { self.current = target; - false + self.velocity = 0.0; + return false; } + + let exp = (-OMEGA * dt).exp(); + let coeff = self.velocity + OMEGA * displacement; + self.current = target + (displacement + coeff * dt) * exp; + self.velocity = (self.velocity - OMEGA * coeff * dt) * exp; + + true } } diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index b94540ee..9bd9fa02 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -206,9 +206,9 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.x + x, + x: bounds.x + x * bounds.width, y: bounds.y, - width, + width: width * bounds.width, height: bounds.height, }, border, @@ -248,7 +248,6 @@ where let x_width = (seg_hi - seg_lo) * drawable; let segment_radius = [r_left, r_right, r_right, r_left].into(); - let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); let border = iced::Border { width: border_width, color: border_color, @@ -257,34 +256,16 @@ where // empty segment if current_p < seg_lo { - draw_quad( - renderer, - x_start * bounds.width, - x_width * bounds.width, - custom_style.track_color, - border, - ); + draw_quad(renderer, x_start, x_width, custom_style.track_color, border); } // filled segment else if current_p > seg_hi { - draw_quad( - renderer, - x_start * bounds.width, - x_width * bounds.width, - custom_style.bar_color, - border, - ); + draw_quad(renderer, x_start, x_width, custom_style.bar_color, border); } // partially filled segment else { - draw_quad( - renderer, - x_start * bounds.width, - x_width * bounds.width, - custom_style.track_color, - border, - ); - + let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); + draw_quad(renderer, x_start, x_width, custom_style.track_color, border); renderer.with_layer( Rectangle { x: bounds.x + x_start * bounds.width, @@ -293,13 +274,7 @@ where height: bounds.height, }, |renderer| { - draw_quad( - renderer, - x_start * bounds.width, - x_width * bounds.width, - custom_style.bar_color, - border, - ); + draw_quad(renderer, x_start, x_width, custom_style.bar_color, border); }, ); } @@ -307,6 +282,20 @@ where } // indeterminate progress bar else { + // draw track + draw_quad( + renderer, + 0.0, + 1.0, + custom_style.track_color, + iced::Border { + width: border_width, + color: border_color, + radius: radius.into(), + }, + ); + + // draw bar let (bar_start, bar_end) = state .animation @@ -316,19 +305,10 @@ where let right_width = (1.0 - start).min(length); let left_width = length - right_width; let border = iced::Border { - width: border_width, - color: border_color, radius: radius.into(), + ..iced::Border::default() }; - draw_quad( - renderer, - 0.0, - bounds.width, - custom_style.track_color, - border, - ); - renderer.with_layer( Rectangle { x: bounds.x, @@ -337,7 +317,7 @@ where height: bounds.height, }, |renderer| { - draw_quad(renderer, 0.0, bounds.width, custom_style.bar_color, border); + draw_quad(renderer, 0.0, 1.0, custom_style.bar_color, border); }, ); @@ -349,7 +329,7 @@ where height: bounds.height, }, |renderer| { - draw_quad(renderer, 0.0, bounds.width, custom_style.bar_color, border); + draw_quad(renderer, 0.0, 1.0, custom_style.bar_color, border); }, ); }