From cdfb8b30680de164280b8b90fbc08a1638e597e2 Mon Sep 17 00:00:00 2001 From: Nick Senger Date: Tue, 6 Jun 2023 21:12:41 -0700 Subject: feat: add progress indicators example --- examples/progress_indicators/Cargo.toml | 13 + examples/progress_indicators/README.md | 14 + examples/progress_indicators/src/circular.rs | 425 +++++++++++++++++++++++++++ examples/progress_indicators/src/easing.rs | 67 +++++ examples/progress_indicators/src/linear.rs | 325 ++++++++++++++++++++ examples/progress_indicators/src/main.rs | 104 +++++++ 6 files changed, 948 insertions(+) create mode 100644 examples/progress_indicators/Cargo.toml create mode 100644 examples/progress_indicators/README.md create mode 100644 examples/progress_indicators/src/circular.rs create mode 100644 examples/progress_indicators/src/easing.rs create mode 100644 examples/progress_indicators/src/linear.rs create mode 100644 examples/progress_indicators/src/main.rs (limited to 'examples') diff --git a/examples/progress_indicators/Cargo.toml b/examples/progress_indicators/Cargo.toml new file mode 100644 index 00000000..89ceb127 --- /dev/null +++ b/examples/progress_indicators/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "progress_indicators" +version = "0.1.0" +authors = ["Nick Senger "] +edition = "2021" +publish = false + +[dependencies] +flo_curves = "0.7" +iced = { path = "../..", features = ["canvas"] } +iced_core = { path = "../../core" } +iced_widget = { path = "../../widget" } +lazy_static = "1.4" diff --git a/examples/progress_indicators/README.md b/examples/progress_indicators/README.md new file mode 100644 index 00000000..acacca8f --- /dev/null +++ b/examples/progress_indicators/README.md @@ -0,0 +1,14 @@ +## Progress indicators + +Example implementation of animated indeterminate progress indicators. + +
+ + + +
+ +You can run it with `cargo run`: +``` +cargo run --package progress_indicators +``` diff --git a/examples/progress_indicators/src/circular.rs b/examples/progress_indicators/src/circular.rs new file mode 100644 index 00000000..2a9f7e1e --- /dev/null +++ b/examples/progress_indicators/src/circular.rs @@ -0,0 +1,425 @@ +//! Show a circular progress indicator. +use iced::widget::canvas::{self, Cursor, Program, Renderer as CanvasRenderer}; +use iced::window; +use iced_core::event::{self, Event}; +use iced_core::time::Instant; +use iced_core::widget::tree::{self, Tree}; +use iced_core::window::RedrawRequest; +use iced_core::{layout, Size}; +use iced_core::{renderer, Vector}; +use iced_core::{ + Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, + Renderer, Shell, Widget, +}; + +use super::easing::{self, Easing}; + +use std::borrow::Cow; +use std::f32::consts::PI; +use std::time::Duration; + +type R = iced_widget::renderer::Renderer; + +const MIN_RADIANS: f32 = PI / 8.0; +const WRAP_RADIANS: f32 = 2.0 * PI - PI / 4.0; +const PROCESSION_VELOCITY: u32 = u32::MAX / 120; + +#[allow(missing_debug_implementations)] +pub struct Circular<'a, Theme> +where + Theme: StyleSheet, +{ + size: f32, + bar_height: f32, + style: ::Style, + easing: Cow<'a, Easing>, + cycle_duration: Duration, +} + +impl<'a, Theme> Circular<'a, Theme> +where + Theme: StyleSheet, +{ + /// Creates a new [`Circular`] with the given content. + pub fn new() -> Self { + Circular { + size: 40.0, + bar_height: 4.0, + style: ::Style::default(), + easing: Cow::Borrowed(&easing::STANDARD), + cycle_duration: Duration::from_millis(600), + } + } + + /// Sets the size of the [`Circular`]. + pub fn size(mut self, size: f32) -> Self { + self.size = size; + self + } + + /// Sets the bar height of the [`Circular`]. + pub fn bar_height(mut self, bar_height: f32) -> Self { + self.bar_height = bar_height; + self + } + + /// Sets the style variant of this [`Circular`]. + pub fn style(mut self, style: ::Style) -> Self { + self.style = style; + self + } + + /// Sets the easing of this [`Circular`]. + pub fn easing(mut self, easing: impl Into>) -> Self { + self.easing = easing.into(); + self + } + + /// Sets the cycle duration of this [`Circular`]. + pub fn cycle_duration(mut self, duration: Duration) -> Self { + self.cycle_duration = duration / 2; + self + } +} + +impl<'a, Theme> Default for Circular<'a, Theme> +where + Theme: StyleSheet, +{ + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Copy)] +enum State { + Expanding { + start: Instant, + progress: f32, + procession: u32, + }, + Contracting { + start: Instant, + progress: f32, + procession: u32, + }, +} + +impl Default for State { + fn default() -> Self { + Self::Expanding { + start: Instant::now(), + progress: 0.0, + procession: 0, + } + } +} + +impl State { + fn next(&self, now: Instant) -> Self { + match self { + Self::Expanding { procession, .. } => Self::Contracting { + start: now, + progress: 0.0, + procession: procession.wrapping_add(PROCESSION_VELOCITY), + }, + Self::Contracting { procession, .. } => Self::Expanding { + start: now, + progress: 0.0, + procession: procession.wrapping_add( + PROCESSION_VELOCITY.wrapping_add( + ((WRAP_RADIANS / (2.0 * PI)) * u32::MAX as f32) as u32, + ), + ), + }, + } + } + + 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, procession, .. + } => Self::Expanding { + start: *start, + progress, + procession: procession.wrapping_add(PROCESSION_VELOCITY), + }, + Self::Contracting { + start, procession, .. + } => Self::Contracting { + start: *start, + progress, + procession: procession.wrapping_add(PROCESSION_VELOCITY), + }, + } + } + + fn procession(&self) -> f32 { + match self { + Self::Expanding { procession, .. } + | Self::Contracting { procession, .. } => { + *procession as f32 / u32::MAX as f32 + } + } + } +} + +impl<'a, Message, Theme> Widget> for Circular<'a, Theme> +where + Message: 'a + Clone, + Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn width(&self) -> Length { + Length::Fixed(self.size) + } + + fn height(&self) -> Length { + Length::Fixed(self.size) + } + + fn layout( + &self, + _renderer: &iced_widget::renderer::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.size).height(self.size); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + _layout: Layout<'_>, + _cursor_position: Point, + _renderer: &iced_widget::renderer::Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let state = tree.state.downcast_mut::(); + + if let Event::Window(window::Event::RedrawRequested(now)) = event { + *state = state.timed_transition(self.cycle_duration, now); + + shell.request_redraw(RedrawRequest::NextFrame); + } + + event::Status::Ignored + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut R, + theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let state = tree.state.downcast_ref::(); + + renderer.with_translation( + Vector::new(bounds.x, bounds.y), + |renderer| { + renderer.draw( as Program< + Message, + R, + >>::draw( + &StateWithStyle { + state, + style: &self.style, + bar_height: self.bar_height, + easing: self.easing.as_ref(), + }, + &(), + renderer, + theme, + bounds, + Cursor::Unavailable, + )); + }, + ); + } +} + +impl<'a, Message, Theme> From> + for Element<'a, Message, R> +where + Message: Clone + 'a, + Theme: StyleSheet + 'a, +{ + fn from(circular: Circular<'a, Theme>) -> Self { + Self::new(circular) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the progress indicator. + pub background: Option, + /// The track [`Color`] of the progress indicator. + pub track_color: Color, + /// The bar [`Color`] of the progress indicator. + pub bar_color: Color, +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + background: None, + track_color: Color::TRANSPARENT, + bar_color: Color::BLACK, + } + } +} + +/// A set of rules that dictate the style of an indicator. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the active [`Appearance`] of a indicator. + fn appearance(&self, style: &Self::Style) -> Appearance; +} + +impl StyleSheet for iced::Theme { + type Style = (); + + fn appearance(&self, _style: &Self::Style) -> Appearance { + let palette = self.extended_palette(); + + Appearance { + background: None, + track_color: palette.background.weak.color, + bar_color: palette.primary.base.color, + } + } +} + +struct StateWithStyle<'a, Theme> +where + Theme: StyleSheet, +{ + state: &'a State, + style: &'a ::Style, + easing: &'a Easing, + bar_height: f32, +} + +impl<'a, Message, Theme> + canvas::Program> + for StateWithStyle<'a, Theme> +where + Theme: StyleSheet, +{ + type State = (); + + fn update( + &self, + _state: &mut Self::State, + _event: canvas::Event, + _bounds: Rectangle, + _cursor: canvas::Cursor, + ) -> (canvas::event::Status, Option) { + (canvas::event::Status::Ignored, None) + } + + fn draw( + &self, + _state: &Self::State, + renderer: &iced_widget::renderer::Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: canvas::Cursor, + ) -> Vec { + let mut frame = canvas::Frame::new(renderer, bounds.size()); + let custom_style = ::appearance(theme, self.style); + let track_radius = frame.width() / 2.0 - self.bar_height; + + if let Some(Background::Color(color)) = custom_style.background { + let background_path = + canvas::Path::circle(frame.center(), track_radius); + frame.fill(&background_path, color); + } + + let track_path = canvas::Path::circle(frame.center(), track_radius); + + frame.stroke( + &track_path, + canvas::Stroke::default() + .with_color(custom_style.track_color) + .with_width(self.bar_height), + ); + + let mut builder = canvas::path::Builder::new(); + + let start = self.state.procession() * 2.0 * PI; + + match self.state { + State::Expanding { progress, .. } => { + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle: start, + end_angle: start + + MIN_RADIANS + + WRAP_RADIANS * (self.easing.y_at_x(*progress)), + }); + } + State::Contracting { progress, .. } => { + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle: start + + WRAP_RADIANS * (self.easing.y_at_x(*progress)), + end_angle: start + MIN_RADIANS + WRAP_RADIANS, + }); + } + } + + let bar_path = builder.build(); + + frame.stroke( + &bar_path, + canvas::Stroke::default() + .with_color(custom_style.bar_color) + .with_width(self.bar_height), + ); + + vec![frame.into_geometry()] + } +} diff --git a/examples/progress_indicators/src/easing.rs b/examples/progress_indicators/src/easing.rs new file mode 100644 index 00000000..10479659 --- /dev/null +++ b/examples/progress_indicators/src/easing.rs @@ -0,0 +1,67 @@ +use flo_curves::bezier::Curve; +use flo_curves::*; +use lazy_static::lazy_static; + +use std::borrow::Cow; + +lazy_static! { + pub static ref EXAMPLES: [Easing; 3] = [ + Easing::CubicBezier(Curve::from_points( + Coord2(0.0, 0.0), + (Coord2(0.05, 0.7), Coord2(0.1, 1.0)), + Coord2(1.0, 1.0), + )), + Easing::CubicBezier(Curve::from_points( + Coord2(0.0, 0.0), + (Coord2(0.3, 0.0), Coord2(0.8, 0.15)), + Coord2(1.0, 1.0), + )), + Easing::CubicBezier(Curve::from_points( + Coord2(0.0, 0.0), + (Coord2(0.2, 0.0), Coord2(0.0, 1.0)), + Coord2(1.0, 1.0), + )) + ]; + pub static ref STANDARD: Easing = { + Easing::CubicBezier(Curve::from_points( + Coord2(0.0, 0.0), + (Coord2(0.2, 0.0), Coord2(0.0, 1.0)), + Coord2(1.0, 1.0), + )) + }; +} + +#[derive(Clone, Debug)] +pub enum Easing { + BezierPath(Vec>), + CubicBezier(Curve), +} + +impl Easing { + pub fn y_at_x(&self, x: f32) -> f32 { + let x = x as f64; + + match self { + Self::BezierPath(curves) => curves + .iter() + .find_map(|curve| { + (curve.start_point().0 <= x && curve.end_point().0 >= x) + .then(|| curve.point_at_pos(x).1 as f32) + }) + .unwrap_or_default(), + Self::CubicBezier(curve) => curve.point_at_pos(x).1 as f32, + } + } +} + +impl<'a> From for Cow<'a, Easing> { + fn from(easing: Easing) -> Self { + Cow::Owned(easing) + } +} + +impl<'a> From<&'a Easing> for Cow<'a, Easing> { + fn from(easing: &'a Easing) -> Self { + Cow::Borrowed(easing) + } +} diff --git a/examples/progress_indicators/src/linear.rs b/examples/progress_indicators/src/linear.rs new file mode 100644 index 00000000..735892e7 --- /dev/null +++ b/examples/progress_indicators/src/linear.rs @@ -0,0 +1,325 @@ +//! Show a linear progress indicator. +use iced::window; +use iced_core::event::{self, Event}; +use iced_core::renderer; +use iced_core::time::Instant; +use iced_core::widget::tree::{self, Tree}; +use iced_core::window::RedrawRequest; +use iced_core::{layout, Size}; +use iced_core::{ + Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, + Shell, Widget, +}; + +use super::easing::{self, Easing}; + +use std::borrow::Cow; +use std::time::Duration; + +#[allow(missing_debug_implementations)] +pub struct Linear<'a, Renderer> +where + Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, +{ + width: Length, + height: Length, + style: ::Style, + easing: Cow<'a, Easing>, + cycle_duration: Duration, +} + +impl<'a, Renderer> Linear<'a, Renderer> +where + Renderer: iced_widget::core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a new [`Linear`] with the given content. + pub fn new() -> Self { + Linear { + width: Length::Fixed(100.0), + height: Length::Fixed(4.0), + style: ::Style::default(), + easing: Cow::Borrowed(&easing::STANDARD), + cycle_duration: Duration::from_millis(600), + } + } + + /// Sets the width of the [`Linear`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Linear`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the style variant of this [`Linear`]. + pub fn style( + mut self, + style: ::Style, + ) -> Self { + self.style = style; + self + } + + /// Sets the motion easing of this [`Linear`]. + pub fn easing(mut self, easing: impl Into>) -> Self { + self.easing = easing.into(); + self + } + + /// Sets the cycle duration of this [`Linear`]. + pub fn cycle_duration(mut self, duration: Duration) -> Self { + self.cycle_duration = duration / 2; + self + } +} + +impl<'a, Renderer> Default for Linear<'a, Renderer> +where + Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn default() -> Self { + Self::new() + } +} + +#[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<'a, Message, Renderer> Widget for Linear<'a, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + iced_core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + _layout: Layout<'_>, + _cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let state = tree.state.downcast_mut::(); + + if let Event::Window(window::Event::RedrawRequested(now)) = event { + *state = state.timed_transition(self.cycle_duration, now); + + shell.request_redraw(RedrawRequest::NextFrame); + } + + event::Status::Ignored + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let custom_style = theme.appearance(&self.style); + let state = tree.state.downcast_ref::(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(custom_style.track_color), + ); + + match state { + State::Expanding { progress, .. } => renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: self.easing.y_at_x(*progress) * bounds.width, + height: bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(custom_style.bar_color), + ), + + State::Contracting { progress, .. } => renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + + self.easing.y_at_x(*progress) * bounds.width, + y: bounds.y, + width: (1.0 - self.easing.y_at_x(*progress)) + * bounds.width, + height: bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(custom_style.bar_color), + ), + } + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: iced_core::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(linear: Linear<'a, Renderer>) -> Self { + Self::new(linear) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The track [`Color`] of the progress indicator. + pub track_color: Color, + /// The bar [`Color`] of the progress indicator. + pub bar_color: Color, +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + track_color: Color::TRANSPARENT, + bar_color: Color::BLACK, + } + } +} + +/// A set of rules that dictate the style of an indicator. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the active [`Appearance`] of a indicator. + fn appearance(&self, style: &Self::Style) -> Appearance; +} + +impl StyleSheet for iced::Theme { + type Style = (); + + fn appearance(&self, _style: &Self::Style) -> Appearance { + let palette = self.extended_palette(); + + Appearance { + track_color: palette.background.weak.color, + bar_color: palette.primary.base.color, + } + } +} diff --git a/examples/progress_indicators/src/main.rs b/examples/progress_indicators/src/main.rs new file mode 100644 index 00000000..136b8d8c --- /dev/null +++ b/examples/progress_indicators/src/main.rs @@ -0,0 +1,104 @@ +use iced::executor; +use iced::widget::{column, container, row, slider, text}; +use iced::{Application, Command, Element, Length, Settings, Theme}; + +use std::time::Duration; + +mod circular; +mod easing; +mod linear; + +use circular::Circular; +use linear::Linear; + +pub fn main() -> iced::Result { + ProgressIndicators::run(Settings { + antialiasing: true, + ..Default::default() + }) +} + +struct ProgressIndicators { + cycle_duration: f32, +} + +impl Default for ProgressIndicators { + fn default() -> Self { + Self { + cycle_duration: 2.0, + } + } +} + +#[derive(Debug, Clone, Copy)] +enum Message { + CycleDurationChanged(f32), +} + +impl Application for ProgressIndicators { + type Message = Message; + type Flags = (); + type Executor = executor::Default; + type Theme = Theme; + + fn new(_flags: Self::Flags) -> (Self, Command) { + (Self::default(), Command::none()) + } + + fn title(&self) -> String { + String::from("Progress Indicators - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::CycleDurationChanged(duration) => { + self.cycle_duration = duration; + } + } + + Command::none() + } + + fn view(&self) -> Element { + let column = easing::EXAMPLES + .iter() + .zip(["Decelerating:", "Accelerating:", "Standard:"]) + .fold(column![], |column, (easing, label)| { + column.push( + row![ + text(label).width(150), + Linear::new().easing(easing).cycle_duration( + Duration::from_secs_f32(self.cycle_duration) + ), + Circular::new().easing(easing).cycle_duration( + Duration::from_secs_f32(self.cycle_duration) + ) + ] + .align_items(iced::Alignment::Center) + .spacing(20.0), + ) + }) + .spacing(20); + + container( + column.push( + row(vec![ + text("Cycle duration:").into(), + slider(1.0..=1000.0, self.cycle_duration * 100.0, |x| { + Message::CycleDurationChanged(x / 100.0) + }) + .width(150.0) + .into(), + text(format!("{:.2}s", self.cycle_duration)).into(), + ]) + .align_items(iced::Alignment::Center) + .spacing(20.0), + ), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} -- cgit From 2ebc92319711e6fa0dda310939257334625b59c9 Mon Sep 17 00:00:00 2001 From: Nick Senger Date: Wed, 7 Jun 2023 15:19:11 -0700 Subject: feat: use lyon for easing --- examples/loading_spinners/Cargo.toml | 14 + examples/loading_spinners/README.md | 14 + examples/loading_spinners/src/circular.rs | 444 +++++++++++++++++++++++++++ examples/loading_spinners/src/easing.rs | 132 ++++++++ examples/loading_spinners/src/linear.rs | 324 +++++++++++++++++++ examples/loading_spinners/src/main.rs | 118 +++++++ examples/progress_indicators/Cargo.toml | 13 - examples/progress_indicators/README.md | 14 - examples/progress_indicators/src/circular.rs | 425 ------------------------- examples/progress_indicators/src/easing.rs | 67 ---- examples/progress_indicators/src/linear.rs | 325 -------------------- examples/progress_indicators/src/main.rs | 104 ------- 12 files changed, 1046 insertions(+), 948 deletions(-) create mode 100644 examples/loading_spinners/Cargo.toml create mode 100644 examples/loading_spinners/README.md create mode 100644 examples/loading_spinners/src/circular.rs create mode 100644 examples/loading_spinners/src/easing.rs create mode 100644 examples/loading_spinners/src/linear.rs create mode 100644 examples/loading_spinners/src/main.rs delete mode 100644 examples/progress_indicators/Cargo.toml delete mode 100644 examples/progress_indicators/README.md delete mode 100644 examples/progress_indicators/src/circular.rs delete mode 100644 examples/progress_indicators/src/easing.rs delete mode 100644 examples/progress_indicators/src/linear.rs delete mode 100644 examples/progress_indicators/src/main.rs (limited to 'examples') diff --git a/examples/loading_spinners/Cargo.toml b/examples/loading_spinners/Cargo.toml new file mode 100644 index 00000000..f37017c6 --- /dev/null +++ b/examples/loading_spinners/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "loading_spinners" +version = "0.1.0" +authors = ["Nick Senger "] +edition = "2021" +publish = false + +[dependencies] +flo_curves = "0.7" +iced = { path = "../..", features = ["canvas"] } +iced_core = { path = "../../core" } +iced_widget = { path = "../../widget" } +once_cell = "1" +lyon = "1" diff --git a/examples/loading_spinners/README.md b/examples/loading_spinners/README.md new file mode 100644 index 00000000..3573c6f6 --- /dev/null +++ b/examples/loading_spinners/README.md @@ -0,0 +1,14 @@ +## Loading Spinners + +Example implementation of animated indeterminate loading spinners. + + + +You can run it with `cargo run`: +``` +cargo run --package loading_spinners +``` diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs new file mode 100644 index 00000000..0d01e7a1 --- /dev/null +++ b/examples/loading_spinners/src/circular.rs @@ -0,0 +1,444 @@ +//! Show a circular progress indicator. +use iced::widget::canvas::{self, Cursor, Program, Renderer as CanvasRenderer}; +use iced::window; +use iced_core::event::{self, Event}; +use iced_core::time::Instant; +use iced_core::widget::tree::{self, Tree}; +use iced_core::window::RedrawRequest; +use iced_core::{layout, Size}; +use iced_core::{renderer, Vector}; +use iced_core::{ + Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, + Renderer, Shell, Widget, +}; + +use super::easing::{self, Easing}; + +use std::f32::consts::PI; +use std::time::Duration; + +type R = iced_widget::renderer::Renderer; + +const MIN_RADIANS: f32 = PI / 8.0; +const WRAP_RADIANS: f32 = 2.0 * PI - PI / 4.0; +const BASE_ROTATION_SPEED: u32 = u32::MAX / 80; + +#[allow(missing_debug_implementations)] +pub struct Circular<'a, Theme> +where + Theme: StyleSheet, +{ + size: f32, + bar_height: f32, + style: ::Style, + easing: &'a Easing, + cycle_duration: Duration, + rotation_speed: u32, +} + +impl<'a, Theme> Circular<'a, Theme> +where + Theme: StyleSheet, +{ + /// Creates a new [`Circular`] with the given content. + pub fn new() -> Self { + Circular { + size: 40.0, + bar_height: 4.0, + style: ::Style::default(), + easing: &easing::STANDARD, + cycle_duration: Duration::from_millis(600), + rotation_speed: BASE_ROTATION_SPEED, + } + } + + /// Sets the size of the [`Circular`]. + pub fn size(mut self, size: f32) -> Self { + self.size = size; + self + } + + /// Sets the bar height of the [`Circular`]. + pub fn bar_height(mut self, bar_height: f32) -> Self { + self.bar_height = bar_height; + self + } + + /// Sets the style variant of this [`Circular`]. + pub fn style(mut self, style: ::Style) -> Self { + self.style = style; + self + } + + /// Sets the easing of this [`Circular`]. + pub fn easing(mut self, easing: &'a Easing) -> Self { + self.easing = easing; + self + } + + /// Sets the cycle duration of this [`Circular`]. + pub fn cycle_duration(mut self, duration: Duration) -> Self { + self.cycle_duration = duration / 2; + self + } + + /// Sets the rotation speed of this [`Circular`]. Must be set to between 0.0 and 10.0. + /// Defaults to 1.0. + pub fn rotation_speed(mut self, speed: f32) -> Self { + let multiplier = speed.min(10.0).max(0.0); + self.rotation_speed = (BASE_ROTATION_SPEED as f32 * multiplier) as u32; + self + } +} + +impl<'a, Theme> Default for Circular<'a, Theme> +where + Theme: StyleSheet, +{ + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Copy)] +enum State { + Expanding { + start: Instant, + progress: f32, + procession: u32, + }, + Contracting { + start: Instant, + progress: f32, + procession: u32, + }, +} + +impl Default for State { + fn default() -> Self { + Self::Expanding { + start: Instant::now(), + progress: 0.0, + procession: 0, + } + } +} + +impl State { + fn next(&self, now: Instant) -> Self { + match self { + Self::Expanding { procession, .. } => Self::Contracting { + start: now, + progress: 0.0, + procession: procession.wrapping_add(BASE_ROTATION_SPEED), + }, + Self::Contracting { procession, .. } => Self::Expanding { + start: now, + progress: 0.0, + procession: procession.wrapping_add( + BASE_ROTATION_SPEED.wrapping_add( + ((WRAP_RADIANS / (2.0 * PI)) * u32::MAX as f32) as u32, + ), + ), + }, + } + } + + fn start(&self) -> Instant { + match self { + Self::Expanding { start, .. } | Self::Contracting { start, .. } => { + *start + } + } + } + + fn timed_transition( + &self, + cycle_duration: Duration, + rotation_speed: u32, + 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, rotation_speed, elapsed), + } + } + + fn with_elapsed( + &self, + cycle_duration: Duration, + rotation_speed: u32, + elapsed: Duration, + ) -> Self { + let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); + match self { + Self::Expanding { + start, procession, .. + } => Self::Expanding { + start: *start, + progress, + procession: procession.wrapping_add(rotation_speed), + }, + Self::Contracting { + start, procession, .. + } => Self::Contracting { + start: *start, + progress, + procession: procession.wrapping_add(rotation_speed), + }, + } + } + + fn procession(&self) -> f32 { + match self { + Self::Expanding { procession, .. } + | Self::Contracting { procession, .. } => { + *procession as f32 / u32::MAX as f32 + } + } + } +} + +impl<'a, Message, Theme> Widget> for Circular<'a, Theme> +where + Message: 'a + Clone, + Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn width(&self) -> Length { + Length::Fixed(self.size) + } + + fn height(&self) -> Length { + Length::Fixed(self.size) + } + + fn layout( + &self, + _renderer: &iced_widget::renderer::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.size).height(self.size); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + _layout: Layout<'_>, + _cursor_position: Point, + _renderer: &iced_widget::renderer::Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let state = tree.state.downcast_mut::(); + + if let Event::Window(window::Event::RedrawRequested(now)) = event { + *state = state.timed_transition( + self.cycle_duration, + self.rotation_speed, + now, + ); + + shell.request_redraw(RedrawRequest::NextFrame); + } + + event::Status::Ignored + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut R, + theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let state = tree.state.downcast_ref::(); + + renderer.with_translation( + Vector::new(bounds.x, bounds.y), + |renderer| { + renderer.draw( as Program< + Message, + R, + >>::draw( + &StateWithStyle { + state, + style: &self.style, + bar_height: self.bar_height, + easing: self.easing, + }, + &(), + renderer, + theme, + bounds, + Cursor::Unavailable, + )); + }, + ); + } +} + +impl<'a, Message, Theme> From> + for Element<'a, Message, R> +where + Message: Clone + 'a, + Theme: StyleSheet + 'a, +{ + fn from(circular: Circular<'a, Theme>) -> Self { + Self::new(circular) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the progress indicator. + pub background: Option, + /// The track [`Color`] of the progress indicator. + pub track_color: Color, + /// The bar [`Color`] of the progress indicator. + pub bar_color: Color, +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + background: None, + track_color: Color::TRANSPARENT, + bar_color: Color::BLACK, + } + } +} + +/// A set of rules that dictate the style of an indicator. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the active [`Appearance`] of a indicator. + fn appearance(&self, style: &Self::Style) -> Appearance; +} + +impl StyleSheet for iced::Theme { + type Style = (); + + fn appearance(&self, _style: &Self::Style) -> Appearance { + let palette = self.extended_palette(); + + Appearance { + background: None, + track_color: palette.background.weak.color, + bar_color: palette.primary.base.color, + } + } +} + +struct StateWithStyle<'a, Theme> +where + Theme: StyleSheet, +{ + state: &'a State, + style: &'a ::Style, + easing: &'a Easing, + bar_height: f32, +} + +impl<'a, Message, Theme> + canvas::Program> + for StateWithStyle<'a, Theme> +where + Theme: StyleSheet, +{ + type State = (); + + fn update( + &self, + _state: &mut Self::State, + _event: canvas::Event, + _bounds: Rectangle, + _cursor: canvas::Cursor, + ) -> (canvas::event::Status, Option) { + (canvas::event::Status::Ignored, None) + } + + fn draw( + &self, + _state: &Self::State, + renderer: &iced_widget::renderer::Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: canvas::Cursor, + ) -> Vec { + let mut frame = canvas::Frame::new(renderer, bounds.size()); + let custom_style = ::appearance(theme, self.style); + let track_radius = frame.width() / 2.0 - self.bar_height; + + if let Some(Background::Color(color)) = custom_style.background { + let background_path = + canvas::Path::circle(frame.center(), track_radius); + frame.fill(&background_path, color); + } + + let track_path = canvas::Path::circle(frame.center(), track_radius); + + frame.stroke( + &track_path, + canvas::Stroke::default() + .with_color(custom_style.track_color) + .with_width(self.bar_height), + ); + + let mut builder = canvas::path::Builder::new(); + + let start = self.state.procession() * 2.0 * PI; + + match self.state { + State::Expanding { progress, .. } => { + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle: start, + end_angle: start + + MIN_RADIANS + + WRAP_RADIANS * (self.easing.y_at_x(*progress)), + }); + } + State::Contracting { progress, .. } => { + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle: start + + WRAP_RADIANS * (self.easing.y_at_x(*progress)), + end_angle: start + MIN_RADIANS + WRAP_RADIANS, + }); + } + } + + let bar_path = builder.build(); + + frame.stroke( + &bar_path, + canvas::Stroke::default() + .with_color(custom_style.bar_color) + .with_width(self.bar_height), + ); + + vec![frame.into_geometry()] + } +} diff --git a/examples/loading_spinners/src/easing.rs b/examples/loading_spinners/src/easing.rs new file mode 100644 index 00000000..04befa15 --- /dev/null +++ b/examples/loading_spinners/src/easing.rs @@ -0,0 +1,132 @@ +use iced_core::Point; +use lyon::algorithms::measure::PathMeasurements; +use lyon::path::builder::NoAttributes; +use lyon::path::path::BuilderImpl; +use lyon::path::Path; + +use once_cell::sync::Lazy; + +pub static EMPHASIZED: Lazy = Lazy::new(|| { + Easing::builder() + .cubic_bezier_to([0.05, 0.0], [0.133333, 0.06], [0.166666, 0.4]) + .cubic_bezier_to([0.208333, 0.82], [0.25, 1.0], [1.0, 1.0]) + .build() +}); + +pub static EMPHASIZED_DECELERATE: Lazy = Lazy::new(|| { + Easing::builder() + .cubic_bezier_to([0.05, 0.7], [0.1, 1.0], [1.0, 1.0]) + .build() +}); + +pub static EMPHASIZED_ACCELERATE: Lazy = Lazy::new(|| { + Easing::builder() + .cubic_bezier_to([0.3, 0.0], [0.8, 0.15], [1.0, 1.0]) + .build() +}); + +pub static STANDARD: Lazy = Lazy::new(|| { + Easing::builder() + .cubic_bezier_to([0.2, 0.0], [0.0, 1.0], [1.0, 1.0]) + .build() +}); + +pub static STANDARD_DECELERATE: Lazy = Lazy::new(|| { + Easing::builder() + .cubic_bezier_to([0.0, 0.0], [0.0, 1.0], [1.0, 1.0]) + .build() +}); + +pub static STANDARD_ACCELERATE: Lazy = Lazy::new(|| { + Easing::builder() + .cubic_bezier_to([0.3, 0.0], [1.0, 1.0], [1.0, 1.0]) + .build() +}); + +pub struct Easing { + path: Path, + measurements: PathMeasurements, +} + +impl Easing { + pub fn builder() -> Builder { + Builder::new() + } + + pub fn y_at_x(&self, x: f32) -> f32 { + let mut sampler = self.measurements.create_sampler( + &self.path, + lyon::algorithms::measure::SampleType::Normalized, + ); + let sample = sampler.sample(x); + + sample.position().y + } +} + +pub struct Builder(NoAttributes); + +impl Builder { + pub fn new() -> Self { + let mut builder = Path::builder(); + builder.begin(lyon::geom::point(0.0, 0.0)); + + Self(builder) + } + + /// Adds a line segment. Points must be between 0,0 and 1,1 + pub fn line_to(mut self, to: impl Into) -> Self { + self.0.line_to(Self::point(to)); + + self + } + + /// Adds a quadratic bézier curve. Points must be between 0,0 and 1,1 + pub fn quadratic_bezier_to( + mut self, + ctrl: impl Into, + to: impl Into, + ) -> Self { + self.0 + .quadratic_bezier_to(Self::point(ctrl), Self::point(to)); + + self + } + + /// Adds a cubic bézier curve. Points must be between 0,0 and 1,1 + pub fn cubic_bezier_to( + mut self, + ctrl1: impl Into, + ctrl2: impl Into, + to: impl Into, + ) -> Self { + self.0.cubic_bezier_to( + Self::point(ctrl1), + Self::point(ctrl2), + Self::point(to), + ); + + self + } + + pub fn build(mut self) -> Easing { + self.0.line_to(lyon::geom::point(1.0, 1.0)); + self.0.end(false); + + let path = self.0.build(); + let measurements = PathMeasurements::from_path(&path, 0.0); + + Easing { path, measurements } + } + + fn point(p: impl Into) -> lyon::geom::Point { + let p: Point = p.into(); + lyon::geom::point(p.x.min(1.0).max(0.0), p.y.min(1.0).max(0.0)) + } +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs new file mode 100644 index 00000000..eb8a7408 --- /dev/null +++ b/examples/loading_spinners/src/linear.rs @@ -0,0 +1,324 @@ +//! Show a linear progress indicator. +use iced::window; +use iced_core::event::{self, Event}; +use iced_core::renderer; +use iced_core::time::Instant; +use iced_core::widget::tree::{self, Tree}; +use iced_core::window::RedrawRequest; +use iced_core::{layout, Size}; +use iced_core::{ + Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, + Shell, Widget, +}; + +use super::easing::{self, Easing}; + +use std::time::Duration; + +#[allow(missing_debug_implementations)] +pub struct Linear<'a, Renderer> +where + Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, +{ + width: Length, + height: Length, + style: ::Style, + easing: &'a Easing, + cycle_duration: Duration, +} + +impl<'a, Renderer> Linear<'a, Renderer> +where + Renderer: iced_widget::core::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Creates a new [`Linear`] with the given content. + pub fn new() -> Self { + Linear { + width: Length::Fixed(100.0), + height: Length::Fixed(4.0), + style: ::Style::default(), + easing: &easing::STANDARD, + cycle_duration: Duration::from_millis(600), + } + } + + /// Sets the width of the [`Linear`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Linear`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the style variant of this [`Linear`]. + pub fn style( + mut self, + style: ::Style, + ) -> Self { + self.style = style; + self + } + + /// Sets the motion easing of this [`Linear`]. + pub fn easing(mut self, easing: &'a Easing) -> Self { + self.easing = easing; + self + } + + /// Sets the cycle duration of this [`Linear`]. + pub fn cycle_duration(mut self, duration: Duration) -> Self { + self.cycle_duration = duration / 2; + self + } +} + +impl<'a, Renderer> Default for Linear<'a, Renderer> +where + Renderer: iced_core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn default() -> Self { + Self::new() + } +} + +#[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<'a, Message, Renderer> Widget for Linear<'a, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + iced_core::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + _layout: Layout<'_>, + _cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let state = tree.state.downcast_mut::(); + + if let Event::Window(window::Event::RedrawRequested(now)) = event { + *state = state.timed_transition(self.cycle_duration, now); + + shell.request_redraw(RedrawRequest::NextFrame); + } + + event::Status::Ignored + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let custom_style = theme.appearance(&self.style); + let state = tree.state.downcast_ref::(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(custom_style.track_color), + ); + + match state { + State::Expanding { progress, .. } => renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: self.easing.y_at_x(*progress) * bounds.width, + height: bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(custom_style.bar_color), + ), + + State::Contracting { progress, .. } => renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + + self.easing.y_at_x(*progress) * bounds.width, + y: bounds.y, + width: (1.0 - self.easing.y_at_x(*progress)) + * bounds.width, + height: bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(custom_style.bar_color), + ), + } + } +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: Clone + 'a, + Renderer: iced_core::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(linear: Linear<'a, Renderer>) -> Self { + Self::new(linear) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The track [`Color`] of the progress indicator. + pub track_color: Color, + /// The bar [`Color`] of the progress indicator. + pub bar_color: Color, +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + track_color: Color::TRANSPARENT, + bar_color: Color::BLACK, + } + } +} + +/// A set of rules that dictate the style of an indicator. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the active [`Appearance`] of a indicator. + fn appearance(&self, style: &Self::Style) -> Appearance; +} + +impl StyleSheet for iced::Theme { + type Style = (); + + fn appearance(&self, _style: &Self::Style) -> Appearance { + let palette = self.extended_palette(); + + Appearance { + track_color: palette.background.weak.color, + bar_color: palette.primary.base.color, + } + } +} diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs new file mode 100644 index 00000000..a78e9590 --- /dev/null +++ b/examples/loading_spinners/src/main.rs @@ -0,0 +1,118 @@ +use iced::executor; +use iced::widget::{column, container, row, slider, text}; +use iced::{Application, Command, Element, Length, Settings, Theme}; + +use std::time::Duration; + +mod circular; +mod easing; +mod linear; + +use circular::Circular; +use linear::Linear; + +pub fn main() -> iced::Result { + LoadingSpinners::run(Settings { + antialiasing: true, + ..Default::default() + }) +} + +struct LoadingSpinners { + cycle_duration: f32, +} + +impl Default for LoadingSpinners { + fn default() -> Self { + Self { + cycle_duration: 2.0, + } + } +} + +#[derive(Debug, Clone, Copy)] +enum Message { + CycleDurationChanged(f32), +} + +impl Application for LoadingSpinners { + type Message = Message; + type Flags = (); + type Executor = executor::Default; + type Theme = Theme; + + fn new(_flags: Self::Flags) -> (Self, Command) { + (Self::default(), Command::none()) + } + + fn title(&self) -> String { + String::from("Loading Spinners - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::CycleDurationChanged(duration) => { + self.cycle_duration = duration; + } + } + + Command::none() + } + + fn view(&self) -> Element { + let column = [ + &easing::EMPHASIZED, + &easing::EMPHASIZED_DECELERATE, + &easing::EMPHASIZED_ACCELERATE, + &easing::STANDARD, + &easing::STANDARD_DECELERATE, + &easing::STANDARD_ACCELERATE, + ] + .iter() + .zip([ + "Emphasized:", + "Emphasized Decelerate:", + "Emphasized Accelerate:", + "Standard:", + "Standard Decelerate:", + "Standard Accelerate:", + ]) + .fold(column![], |column, (easing, label)| { + column.push( + row![ + text(label).width(250), + Linear::new().easing(easing).cycle_duration( + Duration::from_secs_f32(self.cycle_duration) + ), + Circular::new().easing(easing).cycle_duration( + Duration::from_secs_f32(self.cycle_duration) + ) + ] + .align_items(iced::Alignment::Center) + .spacing(20.0), + ) + }) + .spacing(20); + + container( + column.push( + row(vec![ + text("Cycle duration:").into(), + slider(1.0..=1000.0, self.cycle_duration * 100.0, |x| { + Message::CycleDurationChanged(x / 100.0) + }) + .width(200.0) + .into(), + text(format!("{:.2}s", self.cycle_duration)).into(), + ]) + .align_items(iced::Alignment::Center) + .spacing(20.0), + ), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/progress_indicators/Cargo.toml b/examples/progress_indicators/Cargo.toml deleted file mode 100644 index 89ceb127..00000000 --- a/examples/progress_indicators/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "progress_indicators" -version = "0.1.0" -authors = ["Nick Senger "] -edition = "2021" -publish = false - -[dependencies] -flo_curves = "0.7" -iced = { path = "../..", features = ["canvas"] } -iced_core = { path = "../../core" } -iced_widget = { path = "../../widget" } -lazy_static = "1.4" diff --git a/examples/progress_indicators/README.md b/examples/progress_indicators/README.md deleted file mode 100644 index acacca8f..00000000 --- a/examples/progress_indicators/README.md +++ /dev/null @@ -1,14 +0,0 @@ -## Progress indicators - -Example implementation of animated indeterminate progress indicators. - - - -You can run it with `cargo run`: -``` -cargo run --package progress_indicators -``` diff --git a/examples/progress_indicators/src/circular.rs b/examples/progress_indicators/src/circular.rs deleted file mode 100644 index 2a9f7e1e..00000000 --- a/examples/progress_indicators/src/circular.rs +++ /dev/null @@ -1,425 +0,0 @@ -//! Show a circular progress indicator. -use iced::widget::canvas::{self, Cursor, Program, Renderer as CanvasRenderer}; -use iced::window; -use iced_core::event::{self, Event}; -use iced_core::time::Instant; -use iced_core::widget::tree::{self, Tree}; -use iced_core::window::RedrawRequest; -use iced_core::{layout, Size}; -use iced_core::{renderer, Vector}; -use iced_core::{ - Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, - Renderer, Shell, Widget, -}; - -use super::easing::{self, Easing}; - -use std::borrow::Cow; -use std::f32::consts::PI; -use std::time::Duration; - -type R = iced_widget::renderer::Renderer; - -const MIN_RADIANS: f32 = PI / 8.0; -const WRAP_RADIANS: f32 = 2.0 * PI - PI / 4.0; -const PROCESSION_VELOCITY: u32 = u32::MAX / 120; - -#[allow(missing_debug_implementations)] -pub struct Circular<'a, Theme> -where - Theme: StyleSheet, -{ - size: f32, - bar_height: f32, - style: ::Style, - easing: Cow<'a, Easing>, - cycle_duration: Duration, -} - -impl<'a, Theme> Circular<'a, Theme> -where - Theme: StyleSheet, -{ - /// Creates a new [`Circular`] with the given content. - pub fn new() -> Self { - Circular { - size: 40.0, - bar_height: 4.0, - style: ::Style::default(), - easing: Cow::Borrowed(&easing::STANDARD), - cycle_duration: Duration::from_millis(600), - } - } - - /// Sets the size of the [`Circular`]. - pub fn size(mut self, size: f32) -> Self { - self.size = size; - self - } - - /// Sets the bar height of the [`Circular`]. - pub fn bar_height(mut self, bar_height: f32) -> Self { - self.bar_height = bar_height; - self - } - - /// Sets the style variant of this [`Circular`]. - pub fn style(mut self, style: ::Style) -> Self { - self.style = style; - self - } - - /// Sets the easing of this [`Circular`]. - pub fn easing(mut self, easing: impl Into>) -> Self { - self.easing = easing.into(); - self - } - - /// Sets the cycle duration of this [`Circular`]. - pub fn cycle_duration(mut self, duration: Duration) -> Self { - self.cycle_duration = duration / 2; - self - } -} - -impl<'a, Theme> Default for Circular<'a, Theme> -where - Theme: StyleSheet, -{ - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone, Copy)] -enum State { - Expanding { - start: Instant, - progress: f32, - procession: u32, - }, - Contracting { - start: Instant, - progress: f32, - procession: u32, - }, -} - -impl Default for State { - fn default() -> Self { - Self::Expanding { - start: Instant::now(), - progress: 0.0, - procession: 0, - } - } -} - -impl State { - fn next(&self, now: Instant) -> Self { - match self { - Self::Expanding { procession, .. } => Self::Contracting { - start: now, - progress: 0.0, - procession: procession.wrapping_add(PROCESSION_VELOCITY), - }, - Self::Contracting { procession, .. } => Self::Expanding { - start: now, - progress: 0.0, - procession: procession.wrapping_add( - PROCESSION_VELOCITY.wrapping_add( - ((WRAP_RADIANS / (2.0 * PI)) * u32::MAX as f32) as u32, - ), - ), - }, - } - } - - 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, procession, .. - } => Self::Expanding { - start: *start, - progress, - procession: procession.wrapping_add(PROCESSION_VELOCITY), - }, - Self::Contracting { - start, procession, .. - } => Self::Contracting { - start: *start, - progress, - procession: procession.wrapping_add(PROCESSION_VELOCITY), - }, - } - } - - fn procession(&self) -> f32 { - match self { - Self::Expanding { procession, .. } - | Self::Contracting { procession, .. } => { - *procession as f32 / u32::MAX as f32 - } - } - } -} - -impl<'a, Message, Theme> Widget> for Circular<'a, Theme> -where - Message: 'a + Clone, - Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::default()) - } - - fn width(&self) -> Length { - Length::Fixed(self.size) - } - - fn height(&self) -> Length { - Length::Fixed(self.size) - } - - fn layout( - &self, - _renderer: &iced_widget::renderer::Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.size).height(self.size); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - _layout: Layout<'_>, - _cursor_position: Point, - _renderer: &iced_widget::renderer::Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let state = tree.state.downcast_mut::(); - - if let Event::Window(window::Event::RedrawRequested(now)) = event { - *state = state.timed_transition(self.cycle_duration, now); - - shell.request_redraw(RedrawRequest::NextFrame); - } - - event::Status::Ignored - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut R, - theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let state = tree.state.downcast_ref::(); - - renderer.with_translation( - Vector::new(bounds.x, bounds.y), - |renderer| { - renderer.draw( as Program< - Message, - R, - >>::draw( - &StateWithStyle { - state, - style: &self.style, - bar_height: self.bar_height, - easing: self.easing.as_ref(), - }, - &(), - renderer, - theme, - bounds, - Cursor::Unavailable, - )); - }, - ); - } -} - -impl<'a, Message, Theme> From> - for Element<'a, Message, R> -where - Message: Clone + 'a, - Theme: StyleSheet + 'a, -{ - fn from(circular: Circular<'a, Theme>) -> Self { - Self::new(circular) - } -} - -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The [`Background`] of the progress indicator. - pub background: Option, - /// The track [`Color`] of the progress indicator. - pub track_color: Color, - /// The bar [`Color`] of the progress indicator. - pub bar_color: Color, -} - -impl std::default::Default for Appearance { - fn default() -> Self { - Self { - background: None, - track_color: Color::TRANSPARENT, - bar_color: Color::BLACK, - } - } -} - -/// A set of rules that dictate the style of an indicator. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the active [`Appearance`] of a indicator. - fn appearance(&self, style: &Self::Style) -> Appearance; -} - -impl StyleSheet for iced::Theme { - type Style = (); - - fn appearance(&self, _style: &Self::Style) -> Appearance { - let palette = self.extended_palette(); - - Appearance { - background: None, - track_color: palette.background.weak.color, - bar_color: palette.primary.base.color, - } - } -} - -struct StateWithStyle<'a, Theme> -where - Theme: StyleSheet, -{ - state: &'a State, - style: &'a ::Style, - easing: &'a Easing, - bar_height: f32, -} - -impl<'a, Message, Theme> - canvas::Program> - for StateWithStyle<'a, Theme> -where - Theme: StyleSheet, -{ - type State = (); - - fn update( - &self, - _state: &mut Self::State, - _event: canvas::Event, - _bounds: Rectangle, - _cursor: canvas::Cursor, - ) -> (canvas::event::Status, Option) { - (canvas::event::Status::Ignored, None) - } - - fn draw( - &self, - _state: &Self::State, - renderer: &iced_widget::renderer::Renderer, - theme: &Theme, - bounds: Rectangle, - _cursor: canvas::Cursor, - ) -> Vec { - let mut frame = canvas::Frame::new(renderer, bounds.size()); - let custom_style = ::appearance(theme, self.style); - let track_radius = frame.width() / 2.0 - self.bar_height; - - if let Some(Background::Color(color)) = custom_style.background { - let background_path = - canvas::Path::circle(frame.center(), track_radius); - frame.fill(&background_path, color); - } - - let track_path = canvas::Path::circle(frame.center(), track_radius); - - frame.stroke( - &track_path, - canvas::Stroke::default() - .with_color(custom_style.track_color) - .with_width(self.bar_height), - ); - - let mut builder = canvas::path::Builder::new(); - - let start = self.state.procession() * 2.0 * PI; - - match self.state { - State::Expanding { progress, .. } => { - builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, - start_angle: start, - end_angle: start - + MIN_RADIANS - + WRAP_RADIANS * (self.easing.y_at_x(*progress)), - }); - } - State::Contracting { progress, .. } => { - builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, - start_angle: start - + WRAP_RADIANS * (self.easing.y_at_x(*progress)), - end_angle: start + MIN_RADIANS + WRAP_RADIANS, - }); - } - } - - let bar_path = builder.build(); - - frame.stroke( - &bar_path, - canvas::Stroke::default() - .with_color(custom_style.bar_color) - .with_width(self.bar_height), - ); - - vec![frame.into_geometry()] - } -} diff --git a/examples/progress_indicators/src/easing.rs b/examples/progress_indicators/src/easing.rs deleted file mode 100644 index 10479659..00000000 --- a/examples/progress_indicators/src/easing.rs +++ /dev/null @@ -1,67 +0,0 @@ -use flo_curves::bezier::Curve; -use flo_curves::*; -use lazy_static::lazy_static; - -use std::borrow::Cow; - -lazy_static! { - pub static ref EXAMPLES: [Easing; 3] = [ - Easing::CubicBezier(Curve::from_points( - Coord2(0.0, 0.0), - (Coord2(0.05, 0.7), Coord2(0.1, 1.0)), - Coord2(1.0, 1.0), - )), - Easing::CubicBezier(Curve::from_points( - Coord2(0.0, 0.0), - (Coord2(0.3, 0.0), Coord2(0.8, 0.15)), - Coord2(1.0, 1.0), - )), - Easing::CubicBezier(Curve::from_points( - Coord2(0.0, 0.0), - (Coord2(0.2, 0.0), Coord2(0.0, 1.0)), - Coord2(1.0, 1.0), - )) - ]; - pub static ref STANDARD: Easing = { - Easing::CubicBezier(Curve::from_points( - Coord2(0.0, 0.0), - (Coord2(0.2, 0.0), Coord2(0.0, 1.0)), - Coord2(1.0, 1.0), - )) - }; -} - -#[derive(Clone, Debug)] -pub enum Easing { - BezierPath(Vec>), - CubicBezier(Curve), -} - -impl Easing { - pub fn y_at_x(&self, x: f32) -> f32 { - let x = x as f64; - - match self { - Self::BezierPath(curves) => curves - .iter() - .find_map(|curve| { - (curve.start_point().0 <= x && curve.end_point().0 >= x) - .then(|| curve.point_at_pos(x).1 as f32) - }) - .unwrap_or_default(), - Self::CubicBezier(curve) => curve.point_at_pos(x).1 as f32, - } - } -} - -impl<'a> From for Cow<'a, Easing> { - fn from(easing: Easing) -> Self { - Cow::Owned(easing) - } -} - -impl<'a> From<&'a Easing> for Cow<'a, Easing> { - fn from(easing: &'a Easing) -> Self { - Cow::Borrowed(easing) - } -} diff --git a/examples/progress_indicators/src/linear.rs b/examples/progress_indicators/src/linear.rs deleted file mode 100644 index 735892e7..00000000 --- a/examples/progress_indicators/src/linear.rs +++ /dev/null @@ -1,325 +0,0 @@ -//! Show a linear progress indicator. -use iced::window; -use iced_core::event::{self, Event}; -use iced_core::renderer; -use iced_core::time::Instant; -use iced_core::widget::tree::{self, Tree}; -use iced_core::window::RedrawRequest; -use iced_core::{layout, Size}; -use iced_core::{ - Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, - Shell, Widget, -}; - -use super::easing::{self, Easing}; - -use std::borrow::Cow; -use std::time::Duration; - -#[allow(missing_debug_implementations)] -pub struct Linear<'a, Renderer> -where - Renderer: iced_core::Renderer, - Renderer::Theme: StyleSheet, -{ - width: Length, - height: Length, - style: ::Style, - easing: Cow<'a, Easing>, - cycle_duration: Duration, -} - -impl<'a, Renderer> Linear<'a, Renderer> -where - Renderer: iced_widget::core::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates a new [`Linear`] with the given content. - pub fn new() -> Self { - Linear { - width: Length::Fixed(100.0), - height: Length::Fixed(4.0), - style: ::Style::default(), - easing: Cow::Borrowed(&easing::STANDARD), - cycle_duration: Duration::from_millis(600), - } - } - - /// Sets the width of the [`Linear`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Linear`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the style variant of this [`Linear`]. - pub fn style( - mut self, - style: ::Style, - ) -> Self { - self.style = style; - self - } - - /// Sets the motion easing of this [`Linear`]. - pub fn easing(mut self, easing: impl Into>) -> Self { - self.easing = easing.into(); - self - } - - /// Sets the cycle duration of this [`Linear`]. - pub fn cycle_duration(mut self, duration: Duration) -> Self { - self.cycle_duration = duration / 2; - self - } -} - -impl<'a, Renderer> Default for Linear<'a, Renderer> -where - Renderer: iced_core::Renderer, - Renderer::Theme: StyleSheet, -{ - fn default() -> Self { - Self::new() - } -} - -#[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<'a, Message, Renderer> Widget for Linear<'a, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + iced_core::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::default()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - _layout: Layout<'_>, - _cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let state = tree.state.downcast_mut::(); - - if let Event::Window(window::Event::RedrawRequested(now)) = event { - *state = state.timed_transition(self.cycle_duration, now); - - shell.request_redraw(RedrawRequest::NextFrame); - } - - event::Status::Ignored - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let custom_style = theme.appearance(&self.style); - let state = tree.state.downcast_ref::(); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color(custom_style.track_color), - ); - - match state { - State::Expanding { progress, .. } => renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: self.easing.y_at_x(*progress) * bounds.width, - height: bounds.height, - }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color(custom_style.bar_color), - ), - - State::Contracting { progress, .. } => renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x - + self.easing.y_at_x(*progress) * bounds.width, - y: bounds.y, - width: (1.0 - self.easing.y_at_x(*progress)) - * bounds.width, - height: bounds.height, - }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color(custom_style.bar_color), - ), - } - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: Clone + 'a, - Renderer: iced_core::Renderer + 'a, - Renderer::Theme: StyleSheet, -{ - fn from(linear: Linear<'a, Renderer>) -> Self { - Self::new(linear) - } -} - -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The track [`Color`] of the progress indicator. - pub track_color: Color, - /// The bar [`Color`] of the progress indicator. - pub bar_color: Color, -} - -impl std::default::Default for Appearance { - fn default() -> Self { - Self { - track_color: Color::TRANSPARENT, - bar_color: Color::BLACK, - } - } -} - -/// A set of rules that dictate the style of an indicator. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the active [`Appearance`] of a indicator. - fn appearance(&self, style: &Self::Style) -> Appearance; -} - -impl StyleSheet for iced::Theme { - type Style = (); - - fn appearance(&self, _style: &Self::Style) -> Appearance { - let palette = self.extended_palette(); - - Appearance { - track_color: palette.background.weak.color, - bar_color: palette.primary.base.color, - } - } -} diff --git a/examples/progress_indicators/src/main.rs b/examples/progress_indicators/src/main.rs deleted file mode 100644 index 136b8d8c..00000000 --- a/examples/progress_indicators/src/main.rs +++ /dev/null @@ -1,104 +0,0 @@ -use iced::executor; -use iced::widget::{column, container, row, slider, text}; -use iced::{Application, Command, Element, Length, Settings, Theme}; - -use std::time::Duration; - -mod circular; -mod easing; -mod linear; - -use circular::Circular; -use linear::Linear; - -pub fn main() -> iced::Result { - ProgressIndicators::run(Settings { - antialiasing: true, - ..Default::default() - }) -} - -struct ProgressIndicators { - cycle_duration: f32, -} - -impl Default for ProgressIndicators { - fn default() -> Self { - Self { - cycle_duration: 2.0, - } - } -} - -#[derive(Debug, Clone, Copy)] -enum Message { - CycleDurationChanged(f32), -} - -impl Application for ProgressIndicators { - type Message = Message; - type Flags = (); - type Executor = executor::Default; - type Theme = Theme; - - fn new(_flags: Self::Flags) -> (Self, Command) { - (Self::default(), Command::none()) - } - - fn title(&self) -> String { - String::from("Progress Indicators - Iced") - } - - fn update(&mut self, message: Message) -> Command { - match message { - Message::CycleDurationChanged(duration) => { - self.cycle_duration = duration; - } - } - - Command::none() - } - - fn view(&self) -> Element { - let column = easing::EXAMPLES - .iter() - .zip(["Decelerating:", "Accelerating:", "Standard:"]) - .fold(column![], |column, (easing, label)| { - column.push( - row![ - text(label).width(150), - Linear::new().easing(easing).cycle_duration( - Duration::from_secs_f32(self.cycle_duration) - ), - Circular::new().easing(easing).cycle_duration( - Duration::from_secs_f32(self.cycle_duration) - ) - ] - .align_items(iced::Alignment::Center) - .spacing(20.0), - ) - }) - .spacing(20); - - container( - column.push( - row(vec![ - text("Cycle duration:").into(), - slider(1.0..=1000.0, self.cycle_duration * 100.0, |x| { - Message::CycleDurationChanged(x / 100.0) - }) - .width(150.0) - .into(), - text(format!("{:.2}s", self.cycle_duration)).into(), - ]) - .align_items(iced::Alignment::Center) - .spacing(20.0), - ), - ) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} -- cgit From 204c9d6f52dab7b04ecec35220b15768296dfe1e Mon Sep 17 00:00:00 2001 From: Nick Senger Date: Wed, 7 Jun 2023 16:48:11 -0700 Subject: fix: rotation in terms of duration --- examples/loading_spinners/src/circular.rs | 83 ++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 29 deletions(-) (limited to 'examples') diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index 0d01e7a1..4f5b6fe4 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -33,7 +33,7 @@ where style: ::Style, easing: &'a Easing, cycle_duration: Duration, - rotation_speed: u32, + rotaion_duration: Duration, } impl<'a, Theme> Circular<'a, Theme> @@ -48,7 +48,7 @@ where style: ::Style::default(), easing: &easing::STANDARD, cycle_duration: Duration::from_millis(600), - rotation_speed: BASE_ROTATION_SPEED, + rotaion_duration: Duration::from_secs(2), } } @@ -82,11 +82,10 @@ where self } - /// Sets the rotation speed of this [`Circular`]. Must be set to between 0.0 and 10.0. - /// Defaults to 1.0. - pub fn rotation_speed(mut self, speed: f32) -> Self { - let multiplier = speed.min(10.0).max(0.0); - self.rotation_speed = (BASE_ROTATION_SPEED as f32 * multiplier) as u32; + /// 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) + pub fn rotation_duration(mut self, duration: Duration) -> Self { + self.rotaion_duration = duration; self } } @@ -105,12 +104,14 @@ enum State { Expanding { start: Instant, progress: f32, - procession: u32, + rotation: u32, + last: Instant, }, Contracting { start: Instant, progress: f32, - procession: u32, + rotation: u32, + last: Instant, }, } @@ -119,27 +120,30 @@ impl Default for State { Self::Expanding { start: Instant::now(), progress: 0.0, - procession: 0, + rotation: 0, + last: Instant::now(), } } } impl State { - fn next(&self, now: Instant) -> Self { + fn next(&self, additional_rotation: u32, now: Instant) -> Self { match self { - Self::Expanding { procession, .. } => Self::Contracting { + Self::Expanding { rotation, .. } => Self::Contracting { start: now, progress: 0.0, - procession: procession.wrapping_add(BASE_ROTATION_SPEED), + rotation: rotation.wrapping_add(additional_rotation), + last: now, }, - Self::Contracting { procession, .. } => Self::Expanding { + Self::Contracting { rotation, .. } => Self::Expanding { start: now, progress: 0.0, - procession: procession.wrapping_add( + rotation: rotation.wrapping_add( BASE_ROTATION_SPEED.wrapping_add( ((WRAP_RADIANS / (2.0 * PI)) * u32::MAX as f32) as u32, ), ), + last: now, }, } } @@ -152,50 +156,71 @@ impl State { } } + fn last(&self) -> Instant { + match self { + Self::Expanding { last, .. } | Self::Contracting { last, .. } => { + *last + } + } + } + fn timed_transition( &self, cycle_duration: Duration, - rotation_speed: u32, + rotation_duration: Duration, 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(now), - _ => self.with_elapsed(cycle_duration, rotation_speed, elapsed), + elapsed if elapsed > cycle_duration => { + self.next(additional_rotation, now) + } + _ => self.with_elapsed( + cycle_duration, + additional_rotation, + elapsed, + now, + ), } } fn with_elapsed( &self, cycle_duration: Duration, - rotation_speed: u32, + additional_rotation: u32, elapsed: Duration, + now: Instant, ) -> Self { let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); match self { Self::Expanding { - start, procession, .. + start, rotation, .. } => Self::Expanding { start: *start, progress, - procession: procession.wrapping_add(rotation_speed), + rotation: rotation.wrapping_add(additional_rotation), + last: now, }, Self::Contracting { - start, procession, .. + start, rotation, .. } => Self::Contracting { start: *start, progress, - procession: procession.wrapping_add(rotation_speed), + rotation: rotation.wrapping_add(additional_rotation), + last: now, }, } } - fn procession(&self) -> f32 { + fn rotation(&self) -> f32 { match self { - Self::Expanding { procession, .. } - | Self::Contracting { procession, .. } => { - *procession as f32 / u32::MAX as f32 + Self::Expanding { rotation, .. } + | Self::Contracting { rotation, .. } => { + *rotation as f32 / u32::MAX as f32 } } } @@ -248,7 +273,7 @@ where if let Event::Window(window::Event::RedrawRequested(now)) = event { *state = state.timed_transition( self.cycle_duration, - self.rotation_speed, + self.rotaion_duration, now, ); @@ -406,7 +431,7 @@ where let mut builder = canvas::path::Builder::new(); - let start = self.state.procession() * 2.0 * PI; + let start = self.state.rotation() * 2.0 * PI; match self.state { State::Expanding { progress, .. } => { -- cgit From 56eacdb3583e701cde8cca074e151b24c3fd8df3 Mon Sep 17 00:00:00 2001 From: Nick Senger Date: Thu, 8 Jun 2023 07:47:57 -0700 Subject: comment: cleanup imports --- examples/loading_spinners/Cargo.toml | 7 +--- examples/loading_spinners/src/circular.rs | 63 +++++++++++++++---------------- examples/loading_spinners/src/easing.rs | 21 ++++++----- examples/loading_spinners/src/linear.rs | 32 ++++++++-------- 4 files changed, 58 insertions(+), 65 deletions(-) (limited to 'examples') diff --git a/examples/loading_spinners/Cargo.toml b/examples/loading_spinners/Cargo.toml index f37017c6..ee9a48aa 100644 --- a/examples/loading_spinners/Cargo.toml +++ b/examples/loading_spinners/Cargo.toml @@ -6,9 +6,6 @@ edition = "2021" publish = false [dependencies] -flo_curves = "0.7" -iced = { path = "../..", features = ["canvas"] } -iced_core = { path = "../../core" } -iced_widget = { path = "../../widget" } +iced = { path = "../..", features = ["advanced", "canvas"] } +lyon_algorithms = "1" once_cell = "1" -lyon = "1" diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index 4f5b6fe4..8639780a 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -1,23 +1,21 @@ //! Show a circular progress indicator. -use iced::widget::canvas::{self, Cursor, Program, Renderer as CanvasRenderer}; -use iced::window; -use iced_core::event::{self, Event}; -use iced_core::time::Instant; -use iced_core::widget::tree::{self, Tree}; -use iced_core::window::RedrawRequest; -use iced_core::{layout, Size}; -use iced_core::{renderer, Vector}; -use iced_core::{ - Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, - Renderer, Shell, Widget, -}; +use iced::advanced::layout; +use iced::advanced::renderer; +use iced::advanced::widget::tree::{self, Tree}; +use iced::advanced::{Clipboard, Layout, Renderer, Shell, Widget}; +use iced::event; +use iced::time::Instant; +use iced::widget::canvas::{self, Cursor, Program}; +use iced::window::{self, RedrawRequest}; +use iced::{Background, Color, Element, Rectangle}; +use iced::{Event, Length, Point, Size, Vector}; use super::easing::{self, Easing}; use std::f32::consts::PI; use std::time::Duration; -type R = iced_widget::renderer::Renderer; +type R = iced::Renderer; const MIN_RADIANS: f32 = PI / 8.0; const WRAP_RADIANS: f32 = 2.0 * PI - PI / 4.0; @@ -249,7 +247,7 @@ where fn layout( &self, - _renderer: &iced_widget::renderer::Renderer, + _renderer: &iced::Renderer, limits: &layout::Limits, ) -> layout::Node { let limits = limits.width(self.size).height(self.size); @@ -264,7 +262,7 @@ where event: Event, _layout: Layout<'_>, _cursor_position: Point, - _renderer: &iced_widget::renderer::Renderer, + _renderer: &iced::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { @@ -299,22 +297,22 @@ where renderer.with_translation( Vector::new(bounds.x, bounds.y), |renderer| { - renderer.draw( as Program< - Message, - R, - >>::draw( - &StateWithStyle { - state, - style: &self.style, - bar_height: self.bar_height, - easing: self.easing, - }, - &(), + canvas::Renderer::draw( renderer, - theme, - bounds, - Cursor::Unavailable, - )); + as Program>>::draw( + &StateWithStyle { + state, + style: &self.style, + bar_height: self.bar_height, + easing: self.easing, + }, + &(), + renderer, + theme, + bounds, + Cursor::Unavailable, + ), + ); }, ); } @@ -384,8 +382,7 @@ where bar_height: f32, } -impl<'a, Message, Theme> - canvas::Program> +impl<'a, Message, Theme> Program> for StateWithStyle<'a, Theme> where Theme: StyleSheet, @@ -405,7 +402,7 @@ where fn draw( &self, _state: &Self::State, - renderer: &iced_widget::renderer::Renderer, + renderer: &iced::Renderer, theme: &Theme, bounds: Rectangle, _cursor: canvas::Cursor, diff --git a/examples/loading_spinners/src/easing.rs b/examples/loading_spinners/src/easing.rs index 04befa15..665b3329 100644 --- a/examples/loading_spinners/src/easing.rs +++ b/examples/loading_spinners/src/easing.rs @@ -1,9 +1,7 @@ -use iced_core::Point; -use lyon::algorithms::measure::PathMeasurements; -use lyon::path::builder::NoAttributes; -use lyon::path::path::BuilderImpl; -use lyon::path::Path; +use iced::Point; +use lyon_algorithms::measure::PathMeasurements; +use lyon_algorithms::path::{builder::NoAttributes, path::BuilderImpl, Path}; use once_cell::sync::Lazy; pub static EMPHASIZED: Lazy = Lazy::new(|| { @@ -56,7 +54,7 @@ impl Easing { pub fn y_at_x(&self, x: f32) -> f32 { let mut sampler = self.measurements.create_sampler( &self.path, - lyon::algorithms::measure::SampleType::Normalized, + lyon_algorithms::measure::SampleType::Normalized, ); let sample = sampler.sample(x); @@ -69,7 +67,7 @@ pub struct Builder(NoAttributes); impl Builder { pub fn new() -> Self { let mut builder = Path::builder(); - builder.begin(lyon::geom::point(0.0, 0.0)); + builder.begin(lyon_algorithms::geom::point(0.0, 0.0)); Self(builder) } @@ -110,7 +108,7 @@ impl Builder { } pub fn build(mut self) -> Easing { - self.0.line_to(lyon::geom::point(1.0, 1.0)); + self.0.line_to(lyon_algorithms::geom::point(1.0, 1.0)); self.0.end(false); let path = self.0.build(); @@ -119,9 +117,12 @@ impl Builder { Easing { path, measurements } } - fn point(p: impl Into) -> lyon::geom::Point { + fn point(p: impl Into) -> lyon_algorithms::geom::Point { let p: Point = p.into(); - lyon::geom::point(p.x.min(1.0).max(0.0), p.y.min(1.0).max(0.0)) + lyon_algorithms::geom::point( + p.x.min(1.0).max(0.0), + p.y.min(1.0).max(0.0), + ) } } diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index eb8a7408..6d151760 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -1,15 +1,13 @@ //! Show a linear progress indicator. -use iced::window; -use iced_core::event::{self, Event}; -use iced_core::renderer; -use iced_core::time::Instant; -use iced_core::widget::tree::{self, Tree}; -use iced_core::window::RedrawRequest; -use iced_core::{layout, Size}; -use iced_core::{ - Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, - Shell, Widget, -}; +use iced::advanced::layout; +use iced::advanced::renderer::{self, Quad}; +use iced::advanced::widget::tree::{self, Tree}; +use iced::advanced::{Clipboard, Layout, Shell, Widget}; +use iced::event; +use iced::time::Instant; +use iced::window::{self, RedrawRequest}; +use iced::{Background, Color, Element, Rectangle}; +use iced::{Event, Length, Point, Size}; use super::easing::{self, Easing}; @@ -18,7 +16,7 @@ use std::time::Duration; #[allow(missing_debug_implementations)] pub struct Linear<'a, Renderer> where - Renderer: iced_core::Renderer, + Renderer: iced::advanced::Renderer, Renderer::Theme: StyleSheet, { width: Length, @@ -30,7 +28,7 @@ where impl<'a, Renderer> Linear<'a, Renderer> where - Renderer: iced_widget::core::Renderer, + Renderer: iced::advanced::Renderer, Renderer::Theme: StyleSheet, { /// Creates a new [`Linear`] with the given content. @@ -80,7 +78,7 @@ where impl<'a, Renderer> Default for Linear<'a, Renderer> where - Renderer: iced_core::Renderer, + Renderer: iced::advanced::Renderer, Renderer::Theme: StyleSheet, { fn default() -> Self { @@ -156,7 +154,7 @@ impl State { impl<'a, Message, Renderer> Widget for Linear<'a, Renderer> where Message: 'a + Clone, - Renderer: 'a + iced_core::Renderer, + Renderer: 'a + iced::advanced::Renderer, Renderer::Theme: StyleSheet, { fn tag(&self) -> tree::Tag { @@ -253,7 +251,7 @@ where ), State::Contracting { progress, .. } => renderer.fill_quad( - renderer::Quad { + Quad { bounds: Rectangle { x: bounds.x + self.easing.y_at_x(*progress) * bounds.width, @@ -276,7 +274,7 @@ impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Message: Clone + 'a, - Renderer: iced_core::Renderer + 'a, + Renderer: iced::advanced::Renderer + 'a, Renderer::Theme: StyleSheet, { fn from(linear: Linear<'a, Renderer>) -> Self { -- cgit From 0148cfc82b0076a050e13d72a7c1b144a8c67033 Mon Sep 17 00:00:00 2001 From: Nick Senger Date: Thu, 8 Jun 2023 07:51:32 -0700 Subject: fix: RedrawRequest::NextFrame -> RedrawRequest::At() --- examples/loading_spinners/src/circular.rs | 6 +++++- examples/loading_spinners/src/linear.rs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) (limited to 'examples') diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index 8639780a..56ffa6f5 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -266,6 +266,8 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { + const FRAME_RATE: u64 = 60; + let state = tree.state.downcast_mut::(); if let Event::Window(window::Event::RedrawRequested(now)) = event { @@ -275,7 +277,9 @@ where now, ); - shell.request_redraw(RedrawRequest::NextFrame); + shell.request_redraw(RedrawRequest::At( + now + Duration::from_millis(1000 / FRAME_RATE), + )); } event::Status::Ignored diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index 6d151760..ea0807c2 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -194,12 +194,16 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { + const FRAME_RATE: u64 = 60; + let state = tree.state.downcast_mut::(); if let Event::Window(window::Event::RedrawRequested(now)) = event { *state = state.timed_transition(self.cycle_duration, now); - shell.request_redraw(RedrawRequest::NextFrame); + shell.request_redraw(RedrawRequest::At( + now + Duration::from_millis(1000 / FRAME_RATE), + )); } event::Status::Ignored -- cgit From 9d69af10ccb4317f01bff32e2e1e58d498b2fc89 Mon Sep 17 00:00:00 2001 From: Nick Senger Date: Thu, 8 Jun 2023 20:09:10 -0700 Subject: comment: fix typo --- examples/loading_spinners/src/circular.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'examples') diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index 56ffa6f5..4e7bdc18 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -31,7 +31,7 @@ where style: ::Style, easing: &'a Easing, cycle_duration: Duration, - rotaion_duration: Duration, + rotation_duration: Duration, } impl<'a, Theme> Circular<'a, Theme> @@ -46,7 +46,7 @@ where style: ::Style::default(), easing: &easing::STANDARD, cycle_duration: Duration::from_millis(600), - rotaion_duration: Duration::from_secs(2), + rotation_duration: Duration::from_secs(2), } } @@ -83,7 +83,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) pub fn rotation_duration(mut self, duration: Duration) -> Self { - self.rotaion_duration = duration; + self.rotation_duration = duration; self } } @@ -273,7 +273,7 @@ where if let Event::Window(window::Event::RedrawRequested(now)) = event { *state = state.timed_transition( self.cycle_duration, - self.rotaion_duration, + self.rotation_duration, now, ); -- cgit From 290b47f312471db74e8b149e0f78df3891456208 Mon Sep 17 00:00:00 2001 From: Nick Senger Date: Fri, 9 Jun 2023 11:03:52 -0700 Subject: refactor: remove unnecessary canvas complexity --- examples/loading_spinners/src/circular.rs | 187 +++++++++++------------------- 1 file changed, 67 insertions(+), 120 deletions(-) (limited to 'examples') diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index 4e7bdc18..55363de1 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -5,7 +5,7 @@ use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::{Clipboard, Layout, Renderer, Shell, Widget}; use iced::event; use iced::time::Instant; -use iced::widget::canvas::{self, Cursor, Program}; +use iced::widget::canvas; use iced::window::{self, RedrawRequest}; use iced::{Background, Color, Element, Rectangle}; use iced::{Event, Length, Point, Size, Vector}; @@ -15,8 +15,6 @@ use super::easing::{self, Easing}; use std::f32::consts::PI; use std::time::Duration; -type R = iced::Renderer; - const MIN_RADIANS: f32 = PI / 8.0; const WRAP_RADIANS: f32 = 2.0 * PI - PI / 4.0; const BASE_ROTATION_SPEED: u32 = u32::MAX / 80; @@ -98,7 +96,7 @@ where } #[derive(Clone, Copy)] -enum State { +enum Animation { Expanding { start: Instant, progress: f32, @@ -113,7 +111,7 @@ enum State { }, } -impl Default for State { +impl Default for Animation { fn default() -> Self { Self::Expanding { start: Instant::now(), @@ -124,7 +122,7 @@ impl Default for State { } } -impl State { +impl Animation { fn next(&self, additional_rotation: u32, now: Instant) -> Self { match self { Self::Expanding { rotation, .. } => Self::Contracting { @@ -224,7 +222,14 @@ impl State { } } -impl<'a, Message, Theme> Widget> for Circular<'a, Theme> +#[derive(Default)] +struct State { + animation: Animation, + cache: canvas::Cache, +} + +impl<'a, Message, Theme> Widget> + for Circular<'a, Theme> where Message: 'a + Clone, Theme: StyleSheet, @@ -271,12 +276,13 @@ where let state = tree.state.downcast_mut::(); if let Event::Window(window::Event::RedrawRequested(now)) = event { - *state = state.timed_transition( + state.animation = state.animation.timed_transition( self.cycle_duration, self.rotation_duration, now, ); + state.cache.clear(); shell.request_redraw(RedrawRequest::At( now + Duration::from_millis(1000 / FRAME_RATE), )); @@ -288,42 +294,76 @@ where fn draw( &self, tree: &Tree, - renderer: &mut R, + renderer: &mut iced::Renderer, theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); let state = tree.state.downcast_ref::(); + let bounds = layout.bounds(); + let custom_style = + ::appearance(theme, &self.style); + + let geometry = state.cache.draw(renderer, bounds.size(), |frame| { + let track_radius = frame.width() / 2.0 - self.bar_height; + let track_path = canvas::Path::circle(frame.center(), track_radius); + + frame.stroke( + &track_path, + canvas::Stroke::default() + .with_color(custom_style.track_color) + .with_width(self.bar_height), + ); + + let mut builder = canvas::path::Builder::new(); + + let start = state.animation.rotation() * 2.0 * PI; + + match state.animation { + Animation::Expanding { progress, .. } => { + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle: start, + end_angle: start + + MIN_RADIANS + + WRAP_RADIANS * (self.easing.y_at_x(progress)), + }); + } + Animation::Contracting { progress, .. } => { + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle: start + + WRAP_RADIANS * (self.easing.y_at_x(progress)), + end_angle: start + MIN_RADIANS + WRAP_RADIANS, + }); + } + } + + let bar_path = builder.build(); + + frame.stroke( + &bar_path, + canvas::Stroke::default() + .with_color(custom_style.bar_color) + .with_width(self.bar_height), + ); + }); renderer.with_translation( Vector::new(bounds.x, bounds.y), |renderer| { - canvas::Renderer::draw( - renderer, - as Program>>::draw( - &StateWithStyle { - state, - style: &self.style, - bar_height: self.bar_height, - easing: self.easing, - }, - &(), - renderer, - theme, - bounds, - Cursor::Unavailable, - ), - ); + renderer.draw_primitive(geometry.0); }, ); } } impl<'a, Message, Theme> From> - for Element<'a, Message, R> + for Element<'a, Message, iced::Renderer> where Message: Clone + 'a, Theme: StyleSheet + 'a, @@ -375,96 +415,3 @@ impl StyleSheet for iced::Theme { } } } - -struct StateWithStyle<'a, Theme> -where - Theme: StyleSheet, -{ - state: &'a State, - style: &'a ::Style, - easing: &'a Easing, - bar_height: f32, -} - -impl<'a, Message, Theme> Program> - for StateWithStyle<'a, Theme> -where - Theme: StyleSheet, -{ - type State = (); - - fn update( - &self, - _state: &mut Self::State, - _event: canvas::Event, - _bounds: Rectangle, - _cursor: canvas::Cursor, - ) -> (canvas::event::Status, Option) { - (canvas::event::Status::Ignored, None) - } - - fn draw( - &self, - _state: &Self::State, - renderer: &iced::Renderer, - theme: &Theme, - bounds: Rectangle, - _cursor: canvas::Cursor, - ) -> Vec { - let mut frame = canvas::Frame::new(renderer, bounds.size()); - let custom_style = ::appearance(theme, self.style); - let track_radius = frame.width() / 2.0 - self.bar_height; - - if let Some(Background::Color(color)) = custom_style.background { - let background_path = - canvas::Path::circle(frame.center(), track_radius); - frame.fill(&background_path, color); - } - - let track_path = canvas::Path::circle(frame.center(), track_radius); - - frame.stroke( - &track_path, - canvas::Stroke::default() - .with_color(custom_style.track_color) - .with_width(self.bar_height), - ); - - let mut builder = canvas::path::Builder::new(); - - let start = self.state.rotation() * 2.0 * PI; - - match self.state { - State::Expanding { progress, .. } => { - builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, - start_angle: start, - end_angle: start - + MIN_RADIANS - + WRAP_RADIANS * (self.easing.y_at_x(*progress)), - }); - } - State::Contracting { progress, .. } => { - builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, - start_angle: start - + WRAP_RADIANS * (self.easing.y_at_x(*progress)), - end_angle: start + MIN_RADIANS + WRAP_RADIANS, - }); - } - } - - let bar_path = builder.build(); - - frame.stroke( - &bar_path, - canvas::Stroke::default() - .with_color(custom_style.bar_color) - .with_width(self.bar_height), - ); - - vec![frame.into_geometry()] - } -} -- cgit