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.
This commit is contained in:
Vukašin Vojinović 2026-06-23 18:30:28 +02:00 committed by GitHub
parent 6e116097c0
commit 76bc13ff40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 57 additions and 62 deletions

View file

@ -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<KeyBind, Action>,
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),

View file

@ -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<f32>,
velocity: f32,
last: Option<Instant>,
}
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
}
}

View file

@ -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);
},
);
}