diff --git a/src/widget/progress_bar/animation.rs b/src/widget/progress_bar/animation.rs new file mode 100644 index 0000000..52a3432 --- /dev/null +++ b/src/widget/progress_bar/animation.rs @@ -0,0 +1,73 @@ +use crate::anim::smootherstep; +use iced::time::Instant; +use std::time::Duration; + +#[derive(Clone, Copy)] +pub struct Animation { + expanding: bool, + start: Instant, + last: Instant, + offset: u32, +} + +impl Default for Animation { + fn default() -> Self { + let now = Instant::now(); + Self { + expanding: true, + start: now, + last: now, + offset: 0, + } + } +} + +impl Animation { + pub fn timed_transition( + &self, + cycle_duration: Duration, + rotation_duration: 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 new_offset = self.offset.wrapping_add(additional); + + if !cycle_duration.is_zero() && now.duration_since(self.start) > cycle_duration { + let offset = if self.expanding { + new_offset + } else { + new_offset.wrapping_add((wrap * u32::MAX as f32) as u32) + }; + Self { + expanding: !self.expanding, + start: now, + last: now, + offset, + } + } else { + Self { + last: now, + offset: new_offset, + ..*self + } + } + } + + pub fn bar_positions(&self, cycle_duration: Duration, min: f32, wrap: f32) -> (f32, f32) { + let offset = self.offset as f32 / u32::MAX as f32; + let progress = if !cycle_duration.is_zero() { + smootherstep( + self.last.duration_since(self.start).as_secs_f32() / cycle_duration.as_secs_f32(), + ) + } else { + 1.0 + }; + if self.expanding { + (offset, offset + min + wrap * progress) + } else { + (offset + wrap * progress, offset + min + wrap) + } + } +} diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index fa8c38f..684467f 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -1,12 +1,11 @@ //! Show a circular progress indicator. +use super::animation::Animation; use super::style::StyleSheet; -use crate::anim::smootherstep; use iced::advanced::layout; use iced::advanced::renderer; use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; use iced::mouse; -use iced::time::Instant; use iced::widget::canvas; use iced::window; use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector}; @@ -23,7 +22,7 @@ where { size: f32, bar_height: f32, - style: ::Style, + style: Theme::Style, cycle_duration: Duration, rotation_duration: Duration, progress: Option, @@ -38,7 +37,7 @@ where Circular { size: 40.0, bar_height: 4.0, - style: ::Style::default(), + style: Theme::Style::default(), cycle_duration: Duration::from_millis(1500), rotation_duration: Duration::from_secs(2), progress: None, @@ -58,7 +57,7 @@ where } /// Sets the style variant of this [`Circular`]. - pub fn style(mut self, style: ::Style) -> Self { + pub fn style(mut self, style: Theme::Style) -> Self { self.style = style; self } @@ -70,7 +69,7 @@ where } /// Sets the base rotation duration of this [`Circular`]. This is the duration that a full - /// rotation would take if the cycle rotation were set to 0.0 (no expanding or contracting) + /// 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; self @@ -82,10 +81,10 @@ where self } - fn min_wrap_angle(&self, track_radius: f32) -> (f32, f32) { + fn min_wrap(&self, track_radius: f32) -> (f32, f32) { let cap_angle = self.bar_height / track_radius; let gap = MIN_ANGLE.0.max(cap_angle); - (gap - cap_angle, 2.0 * PI - gap * 2.0) + ((gap - cap_angle) / (2.0 * PI), 1.0 - gap / PI) } } @@ -98,120 +97,6 @@ where } } -#[derive(Clone, Copy)] -enum Animation { - Expanding { - start: Instant, - progress: f32, - rotation: u32, - last: Instant, - }, - Contracting { - start: Instant, - progress: f32, - rotation: u32, - last: Instant, - }, -} - -impl Default for Animation { - fn default() -> Self { - Self::Expanding { - start: Instant::now(), - progress: 0.0, - rotation: 0, - last: Instant::now(), - } - } -} - -impl Animation { - fn next(&self, additional_rotation: u32, wrap_angle: f32, now: Instant) -> Self { - match self { - Self::Expanding { rotation, .. } => Self::Contracting { - start: now, - progress: 0.0, - rotation: rotation.wrapping_add(additional_rotation), - last: now, - }, - Self::Contracting { rotation, .. } => Self::Expanding { - start: now, - progress: 0.0, - rotation: rotation.wrapping_add( - (f64::from((wrap_angle) / (2.0 * PI)) * f64::from(u32::MAX)) as u32, - ), - last: now, - }, - } - } - - fn start(&self) -> Instant { - match self { - Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start, - } - } - - fn last(&self) -> Instant { - match self { - Self::Expanding { last, .. } | Self::Contracting { last, .. } => *last, - } - } - - fn timed_transition( - &self, - cycle_duration: Duration, - rotation_duration: Duration, - wrap_angle: f32, - now: Instant, - ) -> Self { - let elapsed = now.duration_since(self.start()); - let additional_rotation = ((now - self.last()).as_secs_f32() - / rotation_duration.as_secs_f32() - * (u32::MAX) as f32) as u32; - - match elapsed { - elapsed if elapsed > cycle_duration => self.next(additional_rotation, wrap_angle, now), - _ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now), - } - } - - fn with_elapsed( - &self, - cycle_duration: Duration, - additional_rotation: u32, - elapsed: Duration, - now: Instant, - ) -> Self { - let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); - match self { - Self::Expanding { - start, rotation, .. - } => Self::Expanding { - start: *start, - progress, - rotation: rotation.wrapping_add(additional_rotation), - last: now, - }, - Self::Contracting { - start, rotation, .. - } => Self::Contracting { - start: *start, - progress, - rotation: rotation.wrapping_add(additional_rotation), - last: now, - }, - } - } - - fn rotation(&self) -> f32 { - match self { - Self::Expanding { rotation, .. } | Self::Contracting { rotation, .. } => { - *rotation as f32 / u32::MAX as f32 - } - } - } -} - #[derive(Default)] struct State { animation: Animation, @@ -261,25 +146,20 @@ where ) { let state = tree.state.downcast_mut::(); if self.progress.is_some() { - if !float_cmp::approx_eq!( - f32, - state.progress.unwrap_or_default(), - self.progress.unwrap_or_default() - ) { + if state.progress != self.progress { state.progress = self.progress; state.cache.clear(); } return; } if let Event::Window(window::Event::RedrawRequested(now)) = event { - let (_, wrap_angle) = self.min_wrap_angle(self.size / 2.0 - self.bar_height); + 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_angle, + wrap, *now, ); - state.cache.clear(); shell.request_redraw(); } @@ -299,8 +179,7 @@ where let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); - let custom_style = - ::appearance(theme, &self.style, self.progress.is_some(), true); + let custom_style = Theme::appearance(theme, &self.style, self.progress.is_some(), true); let geometry = state.cache.draw(renderer, bounds.size(), |frame| { let track_radius = frame.width() / 2.0 - self.bar_height; @@ -313,133 +192,65 @@ where .with_width(self.bar_height), ); - if let Some(progress) = self.progress { - // outer border - if let Some(border_color) = custom_style.border_color { - let border_path = - canvas::Path::circle(frame.center(), track_radius + self.bar_height / 2.0); + // Converts a track fraction to an angle in radians, with 0 being top of circle + let to_angle = |t: f32| t * 2.0 * PI - PI / 2.0; - frame.stroke( - &border_path, - canvas::Stroke::default() - .with_color(border_color) - .with_width(1.0), - ); - } - - // inner border - if let Some(border_color) = custom_style.border_color { - let border_path = - canvas::Path::circle(frame.center(), track_radius - self.bar_height / 2.0); - - frame.stroke( - &border_path, - canvas::Stroke::default() - .with_color(border_color) - .with_width(1.0), - ); - } - - // bar - let mut builder = canvas::path::Builder::new(); - - builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, - start_angle: Radians(-PI / 2.0), - end_angle: Radians(-PI / 2.0 + progress * 2.0 * PI), - }); - - let bar_path = builder.build(); - - frame.stroke( - &bar_path, - canvas::Stroke::default() - .with_color(custom_style.bar_color) - .with_width(self.bar_height), - ); - - let mut builder = canvas::path::Builder::new(); - - // get center of end of arc for rounded cap - let end_angle = -PI / 2.0 + progress * 2.0 * PI; - let end_center = - frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: end_center, - radius: self.bar_height / 2.0, - start_angle: Radians(end_angle), - end_angle: Radians(end_angle + PI), - }); - - // get center of start of arc for rounded cap - let start_angle = -PI / 2.0; - let start_center = frame.center() - + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: start_center, - radius: self.bar_height / 2.0, - start_angle: Radians(start_angle - PI), - end_angle: Radians(start_angle), - }); - - let cap_path = builder.build(); - frame.fill(&cap_path, custom_style.bar_color); - } else { - let mut builder = canvas::path::Builder::new(); - - let start = state.animation.rotation() * 2.0 * PI; - let (min_angle, wrap_angle) = self.min_wrap_angle(track_radius); - let (start_angle, end_angle) = match state.animation { - Animation::Expanding { progress, .. } => ( - start, - start + min_angle + wrap_angle * smootherstep(progress), - ), - Animation::Contracting { progress, .. } => ( - start + wrap_angle * smootherstep(progress), - start + min_angle + wrap_angle, - ), + let draw_cap = |frame: &mut canvas::Frame, t: f32, flip: bool| { + let angle = to_angle(t); + let center = frame.center() + Vector::new(angle.cos(), angle.sin()) * track_radius; + let (start_angle, end_angle) = if flip { + (angle - PI, angle) + } else { + (angle, angle + PI) }; + let mut builder = canvas::path::Builder::new(); builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, + center, + radius: self.bar_height / 2.0, start_angle: Radians(start_angle), end_angle: Radians(end_angle), }); + frame.fill(&builder.build(), custom_style.bar_color); + }; - let bar_path = builder.build(); - + let draw_bar = |frame: &mut canvas::Frame, start: f32, end: f32| { + let mut builder = canvas::path::Builder::new(); + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle: Radians(to_angle(start)), + end_angle: Radians(to_angle(end)), + }); frame.stroke( - &bar_path, + &builder.build(), canvas::Stroke::default() .with_color(custom_style.bar_color) .with_width(self.bar_height), ); + draw_cap(frame, end, false); + draw_cap(frame, start, true); + }; - let mut builder = canvas::path::Builder::new(); - - // get center of end of arc for rounded cap - let end_center = - frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: end_center, - radius: self.bar_height / 2.0, - start_angle: Radians(end_angle), - end_angle: Radians(end_angle + PI), - }); - - // get center of start of arc for rounded cap - let start_center = frame.center() - + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; - builder.arc(canvas::path::Arc { - center: start_center, - radius: self.bar_height / 2.0, - start_angle: Radians(start_angle - PI), - end_angle: Radians(start_angle), - }); - - let cap_path = builder.build(); - frame.fill(&cap_path, custom_style.bar_color); + if let Some(progress) = self.progress { + 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 = + canvas::Path::circle(frame.center(), track_radius + radius_offset); + frame.stroke( + &border_path, + canvas::Stroke::default() + .with_color(border_color) + .with_width(1.0), + ); + } + } + draw_bar(frame, 0.0, progress); + } else { + let (min, wrap) = self.min_wrap(track_radius); + let (start, end) = state + .animation + .bar_positions(self.cycle_duration, min, wrap); + draw_bar(frame, start, end); } }); diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 226b2b5..881d59c 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -1,19 +1,19 @@ //! Show a linear progress indicator. +use super::animation::Animation; +use super::style::StyleSheet; use iced::advanced::layout; -use iced::advanced::renderer::{self, Quad}; +use iced::advanced::renderer; use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; use iced::mouse; -use iced::time::Instant; use iced::window; use iced::{Background, Element, Event, Length, Rectangle, Size}; -use crate::anim::smootherstep; - -use super::style::StyleSheet; - use std::time::Duration; +const MIN_LENGTH: f32 = 0.15; +const WRAP_LENGTH: f32 = 0.618; // avoids animation repetition + #[must_use] pub struct Linear where @@ -23,6 +23,7 @@ where girth: Length, style: Theme::Style, cycle_duration: Duration, + traversal_duration: Duration, progress: Option, } @@ -37,6 +38,7 @@ where girth: Length::Fixed(4.0), style: Theme::Style::default(), cycle_duration: Duration::from_millis(1500), + traversal_duration: Duration::from_secs(2), progress: None, } } @@ -65,6 +67,13 @@ 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; + self + } + /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`. pub fn progress(mut self, progress: f32) -> Self { self.progress = Some(progress.clamp(0.0, 1.0)); @@ -81,65 +90,6 @@ where } } -#[derive(Clone, Copy)] -enum State { - Expanding { start: Instant, progress: f32 }, - Contracting { start: Instant, progress: f32 }, -} - -impl Default for State { - fn default() -> Self { - Self::Expanding { - start: Instant::now(), - progress: 0.0, - } - } -} - -impl State { - fn next(&self, now: Instant) -> Self { - match self { - Self::Expanding { .. } => Self::Contracting { - start: now, - progress: 0.0, - }, - Self::Contracting { .. } => Self::Expanding { - start: now, - progress: 0.0, - }, - } - } - - fn start(&self) -> Instant { - match self { - Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start, - } - } - - fn timed_transition(&self, cycle_duration: Duration, now: Instant) -> Self { - let elapsed = now.duration_since(self.start()); - - match elapsed { - elapsed if elapsed > cycle_duration => self.next(now), - _ => self.with_elapsed(cycle_duration, elapsed), - } - } - - fn with_elapsed(&self, cycle_duration: Duration, elapsed: Duration) -> Self { - let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); - match self { - Self::Expanding { start, .. } => Self::Expanding { - start: *start, - progress, - }, - Self::Contracting { start, .. } => Self::Contracting { - start: *start, - progress, - }, - } - } -} - impl Widget for Linear where Message: Clone, @@ -147,11 +97,11 @@ where Renderer: advanced::Renderer, { fn tag(&self) -> tree::Tag { - tree::Tag::of::() + tree::Tag::of::() } fn state(&self) -> tree::State { - tree::State::new(State::default()) + tree::State::new(Animation::default()) } fn size(&self) -> Size { @@ -185,11 +135,15 @@ where return; } - let state = tree.state.downcast_mut::(); + let animation = tree.state.downcast_mut::(); if let Event::Window(window::Event::RedrawRequested(now)) = event { - *state = state.timed_transition(self.cycle_duration, *now); - + *animation = animation.timed_transition( + self.cycle_duration, + self.traversal_duration, + WRAP_LENGTH, + *now, + ); shell.request_redraw(); } } @@ -206,16 +160,11 @@ where ) { let bounds = layout.bounds(); let custom_style = theme.appearance(&self.style, self.progress.is_some(), false); - let state = tree.state.downcast_ref::(); + let animation = tree.state.downcast_ref::(); renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }, + bounds, border: iced::Border { width: if custom_style.border_color.is_some() { 1.0 @@ -231,37 +180,18 @@ where Background::Color(custom_style.track_color), ); - if let Some(progress) = self.progress { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: progress * bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: 0., - color: iced::Color::TRANSPARENT, - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.bar_color), - ); - } else { - match state { - State::Expanding { progress, .. } => renderer.fill_quad( + let mut draw_segment = |x: f32, width: f32| { + if width > 0.001 { + renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.x, + x: bounds.x + x * bounds.width, y: bounds.y, - width: smootherstep(*progress) * bounds.width, + width: width * bounds.width, height: bounds.height, }, border: iced::Border { - width: 0., + width: 0.0, color: iced::Color::TRANSPARENT, radius: custom_style.border_radius.into(), }, @@ -269,27 +199,22 @@ where ..renderer::Quad::default() }, Background::Color(custom_style.bar_color), - ), - - State::Contracting { progress, .. } => renderer.fill_quad( - Quad { - bounds: Rectangle { - x: bounds.x + smootherstep(*progress) * bounds.width, - y: bounds.y, - width: (1.0 - smootherstep(*progress)) * bounds.width, - height: bounds.height, - }, - border: iced::Border { - width: 0., - color: iced::Color::TRANSPARENT, - radius: custom_style.border_radius.into(), - }, - snap: true, - ..renderer::Quad::default() - }, - Background::Color(custom_style.bar_color), - ), + ); } + }; + + if let Some(progress) = self.progress { + draw_segment(0.0, progress); + } else { + let (bar_start, bar_end) = + 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); + let left_width = length - right_width; + + draw_segment(start, right_width); + draw_segment(0.0, left_width); } } } diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs index 4e277b0..cb24ada 100644 --- a/src/widget/progress_bar/mod.rs +++ b/src/widget/progress_bar/mod.rs @@ -1,3 +1,4 @@ +mod animation; pub mod circular; pub mod linear; pub mod style;