kael_ui 0.2.0

Professional shadcn-inspired UI component library for Kael. 100+ accessible components for building beautiful, performant desktop applications.
use crate::components::button::{Button, ButtonVariant};
use crate::components::icon::Icon;
use crate::components::icon_source::IconSource;
use crate::theme::use_theme;
use kael::{prelude::FluentBuilder as _, *};
use std::rc::Rc;

#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub enum EmptyStateSize {
    Sm,
    #[default]
    Md,
    Lg,
}

impl EmptyStateSize {
    fn icon_size(self) -> Pixels {
        match self {
            EmptyStateSize::Sm => px(32.0),
            EmptyStateSize::Md => px(48.0),
            EmptyStateSize::Lg => px(64.0),
        }
    }

    fn title_size(self) -> Pixels {
        match self {
            EmptyStateSize::Sm => px(14.0),
            EmptyStateSize::Md => px(18.0),
            EmptyStateSize::Lg => px(24.0),
        }
    }

    fn description_size(self) -> Pixels {
        match self {
            EmptyStateSize::Sm => px(12.0),
            EmptyStateSize::Md => px(14.0),
            EmptyStateSize::Lg => px(16.0),
        }
    }

    fn gap(self) -> Pixels {
        match self {
            EmptyStateSize::Sm => px(12.0),
            EmptyStateSize::Md => px(16.0),
            EmptyStateSize::Lg => px(20.0),
        }
    }
}

#[derive(IntoElement)]
pub struct EmptyState {
    id: ElementId,
    icon: Option<IconSource>,
    title: SharedString,
    description: Option<SharedString>,
    action: Option<(SharedString, Rc<dyn Fn(&mut Window, &mut App)>)>,
    secondary_action: Option<(SharedString, Rc<dyn Fn(&mut Window, &mut App)>)>,
    size: EmptyStateSize,
    style: StyleRefinement,
}

impl EmptyState {
    pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
        Self {
            id: id.into(),
            icon: None,
            title: title.into(),
            description: None,
            action: None,
            secondary_action: None,
            size: EmptyStateSize::default(),
            style: StyleRefinement::default(),
        }
    }

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

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

    pub fn action(
        mut self,
        label: impl Into<SharedString>,
        handler: impl Fn(&mut Window, &mut App) + 'static,
    ) -> Self {
        self.action = Some((label.into(), Rc::new(handler)));
        self
    }

    pub fn secondary_action(
        mut self,
        label: impl Into<SharedString>,
        handler: impl Fn(&mut Window, &mut App) + 'static,
    ) -> Self {
        self.secondary_action = Some((label.into(), Rc::new(handler)));
        self
    }

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

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

impl RenderOnce for EmptyState {
    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
        let theme = use_theme();
        let user_style = self.style;
        let icon_size = self.size.icon_size();
        let title_size = self.size.title_size();
        let description_size = self.size.description_size();
        let gap = self.size.gap();
        let id = self.id.clone();

        div()
            .id(self.id)
            .flex()
            .flex_col()
            .items_center()
            .justify_center()
            .gap(gap)
            .p(px(24.0))
            .map(|mut this| {
                this.style().refine(&user_style);
                this
            })
            .when_some(self.icon, |d, icon_src| {
                d.child(
                    Icon::new(icon_src)
                        .size(icon_size)
                        .color(theme.tokens.muted_foreground),
                )
            })
            .child(
                div()
                    .flex()
                    .flex_col()
                    .items_center()
                    .gap(px(8.0))
                    .child(
                        div()
                            .text_size(title_size)
                            .font_weight(FontWeight::SEMIBOLD)
                            .text_color(theme.tokens.foreground)
                            .font_family(theme.tokens.font_family.clone())
                            .text_align(TextAlign::Center)
                            .child(self.title),
                    )
                    .when_some(self.description, |d, desc| {
                        d.child(
                            div()
                                .text_size(description_size)
                                .text_color(theme.tokens.muted_foreground)
                                .font_family(theme.tokens.font_family.clone())
                                .text_align(TextAlign::Center)
                                .max_w(px(320.0))
                                .child(desc),
                        )
                    }),
            )
            .when(
                self.action.is_some() || self.secondary_action.is_some(),
                |d| {
                    d.child(
                        div()
                            .flex()
                            .items_center()
                            .gap(px(12.0))
                            .mt(px(8.0))
                            .when_some(self.action, |d, (label, handler)| {
                                let handler_clone = handler.clone();
                                d.child(
                                    Button::new(
                                        ElementId::Name(format!("{}-action", id).into()),
                                        label,
                                    )
                                    .variant(ButtonVariant::Default)
                                    .on_click(
                                        move |_, window, cx| {
                                            (handler_clone)(window, cx);
                                        },
                                    ),
                                )
                            })
                            .when_some(self.secondary_action, |d, (label, handler)| {
                                let handler_clone = handler.clone();
                                d.child(
                                    Button::new(
                                        ElementId::Name(format!("{}-secondary", id).into()),
                                        label,
                                    )
                                    .variant(ButtonVariant::Ghost)
                                    .on_click(
                                        move |_, window, cx| {
                                            (handler_clone)(window, cx);
                                        },
                                    ),
                                )
                            }),
                    )
                },
            )
    }
}