use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke};
use iced::{mouse, Element, Length, Point, Rectangle, Renderer, Size, Subscription, Theme};
use std::f32::consts::PI;
use std::time::{Duration, Instant};
pub type EasingFn = fn(f32) -> f32;
pub mod easing {
pub fn linear(t: f32) -> f32 {
t
}
pub fn ease_in(t: f32) -> f32 {
t * t
}
pub fn ease_out(t: f32) -> f32 {
1.0 - (1.0 - t) * (1.0 - t)
}
pub fn ease_in_out(t: f32) -> f32 {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
}
}
pub fn ease_in_cubic(t: f32) -> f32 {
t * t * t
}
pub fn ease_out_cubic(t: f32) -> f32 {
1.0 - (1.0 - t).powi(3)
}
pub fn emphasized(t: f32) -> f32 {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
}
pub struct CircularSpinner {
size: f32,
bar_height: f32,
progress: f32,
easing: EasingFn,
}
impl Default for CircularSpinner {
fn default() -> Self {
Self::new()
}
}
impl CircularSpinner {
#[must_use]
pub fn new() -> Self {
Self {
size: 40.0,
bar_height: 4.0,
progress: 0.0,
easing: easing::ease_in_out,
}
}
#[must_use]
pub fn size(mut self, size: f32) -> Self {
self.size = size;
self
}
#[must_use]
pub fn bar_height(mut self, height: f32) -> Self {
self.bar_height = height;
self
}
#[must_use]
pub fn progress(mut self, progress: f32) -> Self {
self.progress = progress % 1.0;
self
}
#[must_use]
pub fn easing(mut self, easing: EasingFn) -> Self {
self.easing = easing;
self
}
}
struct CircularProgram {
bar_height: f32,
progress: f32,
easing: EasingFn,
}
impl<Message> canvas::Program<Message, Theme> for CircularProgram {
type State = ();
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<Geometry> {
let mut frame = Frame::new(renderer, bounds.size());
let center = Point::new(bounds.width / 2.0, bounds.height / 2.0);
let radius = (bounds.width.min(bounds.height) / 2.0) - self.bar_height;
let palette = theme.extended_palette();
let track = Path::circle(center, radius);
frame.stroke(
&track,
Stroke::default()
.with_width(self.bar_height)
.with_color(palette.background.weak.color),
);
let rotation = self.progress * 2.0 * PI * 2.0;
let cycle_progress = (self.progress * 2.0) % 1.0;
let is_expanding = (self.progress * 2.0) < 1.0;
let min_angle = 0.1 * PI;
let max_angle = 1.5 * PI;
let (start_angle, sweep_angle) = if is_expanding {
let sweep = min_angle + (max_angle - min_angle) * (self.easing)(cycle_progress);
(rotation, sweep)
} else {
let sweep = max_angle - (max_angle - min_angle) * (self.easing)(cycle_progress);
let start = rotation + (max_angle - min_angle) * (self.easing)(cycle_progress);
(start, sweep)
};
let arc = Path::new(|builder| {
builder.arc(canvas::path::Arc {
center,
radius,
start_angle: iced::Radians(start_angle),
end_angle: iced::Radians(start_angle + sweep_angle),
});
});
frame.stroke(
&arc,
Stroke::default()
.with_width(self.bar_height)
.with_color(palette.primary.base.color),
);
vec![frame.into_geometry()]
}
}
impl<'a, Message: 'a> From<CircularSpinner> for Element<'a, Message, Theme> {
fn from(spinner: CircularSpinner) -> Self {
let size = spinner.size;
let program = CircularProgram {
bar_height: spinner.bar_height,
progress: spinner.progress,
easing: spinner.easing,
};
Canvas::new(program)
.width(Length::Fixed(size))
.height(Length::Fixed(size))
.into()
}
}
pub struct LinearSpinner {
width: Length,
height: f32,
progress: f32,
easing: EasingFn,
}
impl Default for LinearSpinner {
fn default() -> Self {
Self::new()
}
}
impl LinearSpinner {
#[must_use]
pub fn new() -> Self {
Self {
width: Length::Fill,
height: 4.0,
progress: 0.0,
easing: easing::ease_in_out,
}
}
#[must_use]
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
#[must_use]
pub fn height(mut self, height: f32) -> Self {
self.height = height;
self
}
#[must_use]
pub fn progress(mut self, progress: f32) -> Self {
self.progress = progress % 1.0;
self
}
#[must_use]
pub fn easing(mut self, easing: EasingFn) -> Self {
self.easing = easing;
self
}
}
struct LinearProgram {
progress: f32,
easing: EasingFn,
}
impl<Message> canvas::Program<Message, Theme> for LinearProgram {
type State = ();
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<Geometry> {
let mut frame = Frame::new(renderer, bounds.size());
let palette = theme.extended_palette();
let track = Path::rectangle(Point::ORIGIN, bounds.size());
frame.fill(&track, palette.background.weak.color);
let cycle_progress = (self.progress * 2.0) % 1.0;
let is_first_half = (self.progress * 2.0) < 1.0;
let eased = (self.easing)(cycle_progress);
let bar_width = bounds.width * 0.3;
let bar_x = if is_first_half {
eased * (bounds.width - bar_width)
} else {
(1.0 - eased) * (bounds.width - bar_width)
};
let bar = Path::rectangle(
Point::new(bar_x, 0.0),
Size::new(bar_width, bounds.height),
);
frame.fill(&bar, palette.primary.base.color);
vec![frame.into_geometry()]
}
}
impl<'a, Message: 'a> From<LinearSpinner> for Element<'a, Message, Theme> {
fn from(spinner: LinearSpinner) -> Self {
let program = LinearProgram {
progress: spinner.progress,
easing: spinner.easing,
};
Canvas::new(program)
.width(spinner.width)
.height(Length::Fixed(spinner.height))
.into()
}
}
pub struct DotsSpinner {
dot_count: usize,
dot_size: f32,
spacing: f32,
progress: f32,
}
impl Default for DotsSpinner {
fn default() -> Self {
Self::new()
}
}
impl DotsSpinner {
#[must_use]
pub fn new() -> Self {
Self {
dot_count: 3,
dot_size: 8.0,
spacing: 8.0,
progress: 0.0,
}
}
#[must_use]
pub fn dot_count(mut self, count: usize) -> Self {
self.dot_count = count.max(1);
self
}
#[must_use]
pub fn dot_size(mut self, size: f32) -> Self {
self.dot_size = size;
self
}
#[must_use]
pub fn spacing(mut self, spacing: f32) -> Self {
self.spacing = spacing;
self
}
#[must_use]
pub fn progress(mut self, progress: f32) -> Self {
self.progress = progress % 1.0;
self
}
}
struct DotsProgram {
dot_count: usize,
dot_size: f32,
spacing: f32,
progress: f32,
}
impl<Message> canvas::Program<Message, Theme> for DotsProgram {
type State = ();
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<Geometry> {
let mut frame = Frame::new(renderer, bounds.size());
let palette = theme.extended_palette();
let total_width =
self.dot_count as f32 * self.dot_size + (self.dot_count - 1) as f32 * self.spacing;
let start_x = (bounds.width - total_width) / 2.0;
let center_y = bounds.height / 2.0;
for i in 0..self.dot_count {
let x = start_x + i as f32 * (self.dot_size + self.spacing) + self.dot_size / 2.0;
let dot_offset = i as f32 / self.dot_count as f32;
let dot_progress = (self.progress + dot_offset) % 1.0;
let bounce = (dot_progress * PI).sin();
let y_offset = bounce * self.dot_size * 0.5;
let center = Point::new(x, center_y - y_offset);
let opacity = 0.4 + 0.6 * bounce;
let color = iced::Color {
a: opacity,
..palette.primary.base.color
};
let dot = Path::circle(center, self.dot_size / 2.0);
frame.fill(&dot, color);
}
vec![frame.into_geometry()]
}
}
impl<'a, Message: 'a> From<DotsSpinner> for Element<'a, Message, Theme> {
fn from(spinner: DotsSpinner) -> Self {
let width = spinner.dot_count as f32 * spinner.dot_size
+ (spinner.dot_count - 1) as f32 * spinner.spacing;
let height = spinner.dot_size * 2.0; let program = DotsProgram {
dot_count: spinner.dot_count,
dot_size: spinner.dot_size,
spacing: spinner.spacing,
progress: spinner.progress,
};
Canvas::new(program)
.width(Length::Fixed(width))
.height(Length::Fixed(height))
.into()
}
}
pub struct PulseSpinner {
size: f32,
progress: f32,
}
impl Default for PulseSpinner {
fn default() -> Self {
Self::new()
}
}
impl PulseSpinner {
#[must_use]
pub fn new() -> Self {
Self {
size: 24.0,
progress: 0.0,
}
}
#[must_use]
pub fn size(mut self, size: f32) -> Self {
self.size = size;
self
}
#[must_use]
pub fn progress(mut self, progress: f32) -> Self {
self.progress = progress % 1.0;
self
}
}
struct PulseProgram {
progress: f32,
}
impl<Message> canvas::Program<Message, Theme> for PulseProgram {
type State = ();
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<Geometry> {
let mut frame = Frame::new(renderer, bounds.size());
let palette = theme.extended_palette();
let center = Point::new(bounds.width / 2.0, bounds.height / 2.0);
let max_radius = bounds.width.min(bounds.height) / 2.0;
let pulse = (self.progress * PI * 2.0).sin() * 0.5 + 0.5;
let radius = max_radius * (0.6 + 0.4 * pulse);
let opacity = 0.3 + 0.7 * pulse;
let color = iced::Color {
a: opacity,
..palette.primary.base.color
};
let circle = Path::circle(center, radius);
frame.fill(&circle, color);
vec![frame.into_geometry()]
}
}
impl<'a, Message: 'a> From<PulseSpinner> for Element<'a, Message, Theme> {
fn from(spinner: PulseSpinner) -> Self {
let size = spinner.size;
let program = PulseProgram {
progress: spinner.progress,
};
Canvas::new(program)
.width(Length::Fixed(size))
.height(Length::Fixed(size))
.into()
}
}
#[derive(Debug, Clone)]
pub enum SpinnerMessage {
Tick(f32),
}
pub const DEFAULT_CYCLE_DURATION: Duration = Duration::from_millis(1500);
pub const DEFAULT_FRAME_DURATION: Duration = Duration::from_millis(16);
pub fn spinner_subscription() -> Subscription<SpinnerMessage> {
spinner_subscription_with_duration(DEFAULT_CYCLE_DURATION)
}
pub fn spinner_subscription_with_duration(cycle_duration: Duration) -> Subscription<SpinnerMessage> {
iced::time::every(DEFAULT_FRAME_DURATION).map(move |_| {
let elapsed = Instant::now().elapsed().as_secs_f32();
let cycle_secs = cycle_duration.as_secs_f32();
let progress = (elapsed % cycle_secs) / cycle_secs;
SpinnerMessage::Tick(progress)
})
}
pub fn calculate_progress(start: Instant, cycle_duration: Duration) -> f32 {
let elapsed = start.elapsed().as_secs_f32();
let cycle_secs = cycle_duration.as_secs_f32();
(elapsed % cycle_secs) / cycle_secs
}