kael_ui 0.2.0

Professional shadcn-inspired UI component library for Kael. 100+ accessible components for building beautiful, performant desktop applications.
use crate::theme::use_theme;
use kael::{prelude::FluentBuilder as _, *};

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SpinnerSize {
    Xs,
    Sm,
    Md,
    Lg,
    Xl,
}

impl SpinnerSize {
    fn to_pixels(self) -> Pixels {
        match self {
            SpinnerSize::Xs => px(16.0),
            SpinnerSize::Sm => px(20.0),
            SpinnerSize::Md => px(24.0),
            SpinnerSize::Lg => px(32.0),
            SpinnerSize::Xl => px(48.0),
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SpinnerVariant {
    Default,
    Primary,
    Secondary,
    Muted,
}

#[derive(IntoElement)]
pub struct Spinner {
    size: SpinnerSize,
    variant: SpinnerVariant,
    label: Option<SharedString>,
    style: StyleRefinement,
}

impl Spinner {
    pub fn new() -> Self {
        Self {
            size: SpinnerSize::Md,
            variant: SpinnerVariant::Default,
            label: None,
            style: StyleRefinement::default(),
        }
    }

    pub fn size(mut self, size: SpinnerSize) -> Self {
        self.size = size;
        self
    }

    pub fn variant(mut self, variant: SpinnerVariant) -> Self {
        self.variant = variant;
        self
    }

    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
        self.label = Some(label.into());
        self
    }
}

impl Default for Spinner {
    fn default() -> Self {
        Self::new()
    }
}

impl Styled for Spinner {
    fn style(&mut self) -> &mut StyleRefinement {
        &mut self.style
    }
}

impl RenderOnce for Spinner {
    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
        let theme = use_theme();
        let user_style = self.style;
        let size_px = self.size.to_pixels();
        let stroke_width = size_px * 0.15;

        let color = match self.variant {
            SpinnerVariant::Default => theme.tokens.foreground,
            SpinnerVariant::Primary => theme.tokens.primary,
            SpinnerVariant::Secondary => theme.tokens.secondary,
            SpinnerVariant::Muted => theme.tokens.muted_foreground,
        };

        let center = size_px * 0.5;
        let path_radius = (size_px - stroke_width) * 0.5;
        let dot_size = stroke_width * 1.2;
        let dot_offset = dot_size * 0.5;

        div()
            .flex()
            .flex_col()
            .items_center()
            .gap(px(8.0))
            .map(|mut this| {
                this.style().refine(&user_style);
                this
            })
            .child(
                div()
                    .size(size_px)
                    .relative()
                    .child(
                        div()
                            .absolute()
                            .inset_0()
                            .border(stroke_width)
                            .border_color(theme.tokens.muted)
                            .rounded(px(9999.0)),
                    )
                    .child(
                        div()
                            .absolute()
                            .size(dot_size)
                            .rounded(px(9999.0))
                            .bg(color)
                            .with_animation(
                                "spinner-orbit",
                                Animation::new(std::time::Duration::from_millis(700))
                                    .repeat_forever()
                                    .with_easing(crate::animations::easings::linear),
                                move |dot, delta| {
                                    let angle = delta * std::f32::consts::TAU;
                                    let x = center + path_radius * angle.cos() - dot_offset;
                                    let y = center + path_radius * angle.sin() - dot_offset;
                                    dot.left(x).top(y)
                                },
                            ),
                    ),
            )
            .when_some(self.label, |d, label| {
                d.child(
                    div()
                        .text_size(px(12.0))
                        .text_color(theme.tokens.muted_foreground)
                        .font_family(theme.tokens.font_family.clone())
                        .child(label),
                )
            })
    }
}