use std::time::Duration;
use crate::animation::AnimationName;
use crate::color::Rgb;
use crate::geometry::{Rect, Size};
use crate::style::CellStyle;
use crate::text;
use crate::widget::{MeasureCx, PaintCx, Widget};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SpinnerStyle {
#[default]
Arc,
Dots,
Orbit,
Pop,
Pulse,
Quarters,
Slices,
}
impl SpinnerStyle {
pub const ALL: [Self; 7] =
[Self::Arc, Self::Dots, Self::Orbit, Self::Pop, Self::Pulse, Self::Quarters, Self::Slices];
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Arc => "arc",
Self::Dots => "dots",
Self::Orbit => "orbit",
Self::Pop => "pop",
Self::Pulse => "pulse",
Self::Quarters => "quarters",
Self::Slices => "slices",
}
}
#[must_use]
pub fn animation(self) -> &'static str {
match self {
Self::Arc => "spinner-arc",
Self::Dots => "spinner-dots",
Self::Orbit => "spinner-orbit",
Self::Pop => "spinner-pop",
Self::Pulse => "spinner-pulse",
Self::Quarters => "spinner-quarters",
Self::Slices => "spinner-slices",
}
}
}
impl From<SpinnerStyle> for AnimationName {
fn from(style: SpinnerStyle) -> Self {
Self::from(style.animation())
}
}
const DONE_ANIMATION: &str = "spinner-done";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Spinner {
animation: AnimationName,
label: Option<String>,
variant: Option<String>,
done: bool,
}
impl Default for Spinner {
fn default() -> Self {
Self { animation: SpinnerStyle::default().into(), label: None, variant: None, done: false }
}
}
impl Spinner {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn style(mut self, style: SpinnerStyle) -> Self {
self.animation = style.into();
self
}
#[must_use]
pub fn animation(mut self, name: impl Into<AnimationName>) -> Self {
self.animation = name.into();
self
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn variant(mut self, variant: impl Into<String>) -> Self {
self.variant = Some(variant.into());
self
}
#[must_use]
pub fn done(mut self, done: bool) -> Self {
self.done = done;
self
}
fn cell_style(&self, cx: &mut PaintCx<'_>) -> CellStyle {
let style = cx.style("spinner", self.variant.as_deref(), &[]).text();
CellStyle { fg: Some(style.fg.unwrap_or_else(|| cx.color("accent"))), ..style }
}
fn paint_done(&self, cx: &mut PaintCx<'_>, area: Rect) {
let style = self.cell_style(cx);
let started = match *cx.memory::<Finish>() {
Finish::Unseen | Finish::Resting => None,
Finish::Since { at, from } => Some((at, from)),
Finish::Spinning => {
let now = cx.now();
let turning = cx.animation(self.animation.as_str(), style, Some(Duration::ZERO));
Some((now, turning.style.fg))
}
};
let since = started.filter(|_| !cx.reduced_motion());
let from = CellStyle { fg: since.and_then(|(_, from)| from).or(style.fg), ..style };
let cell = cx.animation(DONE_ANIMATION, from, since.map(|(at, _)| at));
*cx.memory::<Finish>() = match since {
Some((at, from)) if !cell.finished => Finish::Since { at, from },
_ => Finish::Resting,
};
cx.text(area.x, area.y, &cell.glyph, cell.style, 1);
}
fn paint_turning(&self, cx: &mut PaintCx<'_>, area: Rect) {
*cx.memory::<Finish>() = Finish::Spinning;
let style = self.cell_style(cx);
let cell = cx.animation(self.animation.as_str(), style, Some(Duration::ZERO));
cx.text(area.x, area.y, &cell.glyph, cell.style, 1);
}
}
#[derive(Debug, Clone, Copy, Default)]
enum Finish {
#[default]
Unseen,
Spinning,
Since { at: Duration, from: Option<Rgb> },
Resting,
}
impl<Msg: 'static> Widget<Msg> for Spinner {
fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
let label = self.label.as_deref().map_or(0, |label| text::width(label).saturating_add(2));
Size::new(label.saturating_add(1), 1).min(available)
}
fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
if self.done {
self.paint_done(cx, area);
} else {
self.paint_turning(cx, area);
}
if let Some(label) = &self.label {
let label_style = cx.style("spinner-label", self.variant.as_deref(), &[]).text();
let budget = area.width.saturating_sub(2);
let shown = text::truncate(label, budget).into_owned();
cx.text(area.x + 2, area.y, &shown, label_style, budget);
}
}
}
#[cfg(test)]
mod tests;