liora-components 0.1.0

Enterprise-style native GPUI component library for Liora applications.
Documentation
use crate::motion::pop_in;
use gpui::{
    App, Context, Entity, Global, IntoElement, Render, SharedString, Window, div, prelude::*, px,
};
use liora_core::{Config, push_passive_portal};
use liora_icons::Icon;
use liora_icons_lucide::IconName;
use std::time::Duration;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NotificationType {
    Info,
    Success,
    Warning,
    Error,
}

#[derive(Clone)]
pub struct NotificationItem {
    pub id: usize,
    pub title: SharedString,
    pub description: Option<SharedString>,
    pub msg_type: NotificationType,
}

pub struct NotificationManager {
    notifications: Vec<NotificationItem>,
    next_id: usize,
}

pub struct NotificationManagerGlobal(pub Entity<NotificationManager>);
impl Global for NotificationManagerGlobal {}

impl NotificationManager {
    pub fn new() -> Self {
        Self {
            notifications: vec![],
            next_id: 0,
        }
    }

    pub fn init(cx: &mut App) {
        if !cx.has_global::<NotificationManagerGlobal>() {
            let manager = cx.new(|_| Self::new());
            cx.set_global(NotificationManagerGlobal(manager));
        }
    }

    pub fn show(
        title: impl Into<SharedString>,
        description: Option<SharedString>,
        msg_type: NotificationType,
        cx: &mut App,
    ) {
        Self::init(cx);
        let manager = cx.global::<NotificationManagerGlobal>().0.clone();
        let title = title.into();

        manager.update(cx, |this, cx| {
            let id = this.next_id;
            this.notifications.push(NotificationItem {
                id,
                title: title.clone(),
                description: description.clone(),
                msg_type,
            });
            this.next_id += 1;

            let async_cx = cx.to_async();
            let executor = cx.background_executor().clone();
            cx.foreground_executor()
                .spawn(async move {
                    executor.timer(Duration::from_secs(4)).await;
                    async_cx.update(|cx| {
                        if cx.has_global::<NotificationManagerGlobal>() {
                            let manager = cx.global::<NotificationManagerGlobal>().0.clone();
                            manager.update(cx, |this, cx| {
                                this.notifications.retain(|n| n.id != id);
                                cx.notify();
                            });
                        }
                    });
                })
                .detach();

            cx.notify();
        });
    }
}

impl Render for NotificationManager {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let items = self.notifications.clone();
        if items.is_empty() {
            return div();
        }

        let theme = cx.global::<Config>().theme.clone();

        div()
            .absolute()
            .top_8()
            .right_8()
            .flex()
            .flex_col()
            .items_end()
            .gap_4()
            .children(items.into_iter().map(|item| {
                let (color, icon) = match item.msg_type {
                    NotificationType::Info => (theme.primary.base, IconName::Info),
                    NotificationType::Success => (theme.success.base, IconName::Check),
                    NotificationType::Warning => (theme.warning.base, IconName::TriangleAlert),
                    NotificationType::Error => (theme.danger.base, IconName::CircleX),
                };

                pop_in(
                    ("liora-notification", item.id),
                    div()
                        .w(px(320.0))
                        .bg(theme.neutral.card)
                        .border_1()
                        .border_color(theme.neutral.border)
                        .p_4()
                        .rounded(px(theme.radius.md))
                        .shadow_lg()
                        .flex()
                        .flex_row()
                        .gap_3()
                        .child(Icon::new(icon).size(px(24.0)).color(color))
                        .child(
                            div()
                                .flex_1()
                                .flex()
                                .flex_col()
                                .gap_1()
                                .child(div().font_weight(gpui::FontWeight::BOLD).child(item.title))
                                .when_some(item.description, |s, d| {
                                    s.child(
                                        div().text_sm().text_color(theme.neutral.text_3).child(d),
                                    )
                                }),
                        ),
                )
            }))
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn notifications_use_liora_motion() {
        let source = include_str!("notification.rs")
            .split("#[cfg(test)]")
            .next()
            .unwrap();

        assert!(source.contains("pop_in("));
        assert!(source.contains("liora-notification"));
    }
}

pub fn show_notification(
    title: impl Into<SharedString>,
    description: Option<SharedString>,
    msg_type: NotificationType,
    cx: &mut App,
) {
    NotificationManager::show(title, description, msg_type, cx);
}

pub fn render_notifications(cx: &mut App) {
    if cx.has_global::<NotificationManagerGlobal>() {
        let manager = cx.global::<NotificationManagerGlobal>().0.clone();
        if !manager.read(cx).notifications.is_empty() {
            push_passive_portal(move |_window, _cx| manager.clone().into_any_element(), cx);
        }
    }
}