feat(progress_bar): animate determinate progress
This commit is contained in:
parent
4d39cf3d7b
commit
8b4c8adec8
3 changed files with 95 additions and 55 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue