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:
parent
6e116097c0
commit
76bc13ff40
3 changed files with 57 additions and 62 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue