liora_components/
notification.rs1use crate::motion::pop_in;
2use gpui::{
3 App, Context, Entity, Global, IntoElement, Render, SharedString, Window, div, prelude::*, px,
4};
5use liora_core::{Config, push_passive_portal};
6use liora_icons::Icon;
7use liora_icons_lucide::IconName;
8use std::time::Duration;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum NotificationType {
12 Info,
13 Success,
14 Warning,
15 Error,
16}
17
18#[derive(Clone)]
19pub struct NotificationItem {
20 pub id: usize,
21 pub title: SharedString,
22 pub description: Option<SharedString>,
23 pub msg_type: NotificationType,
24}
25
26pub struct NotificationManager {
27 notifications: Vec<NotificationItem>,
28 next_id: usize,
29}
30
31pub struct NotificationManagerGlobal(pub Entity<NotificationManager>);
32impl Global for NotificationManagerGlobal {}
33
34impl NotificationManager {
35 pub fn new() -> Self {
36 Self {
37 notifications: vec![],
38 next_id: 0,
39 }
40 }
41
42 pub fn init(cx: &mut App) {
43 if !cx.has_global::<NotificationManagerGlobal>() {
44 let manager = cx.new(|_| Self::new());
45 cx.set_global(NotificationManagerGlobal(manager));
46 }
47 }
48
49 pub fn show(
50 title: impl Into<SharedString>,
51 description: Option<SharedString>,
52 msg_type: NotificationType,
53 cx: &mut App,
54 ) {
55 Self::init(cx);
56 let manager = cx.global::<NotificationManagerGlobal>().0.clone();
57 let title = title.into();
58
59 manager.update(cx, |this, cx| {
60 let id = this.next_id;
61 this.notifications.push(NotificationItem {
62 id,
63 title: title.clone(),
64 description: description.clone(),
65 msg_type,
66 });
67 this.next_id += 1;
68
69 let async_cx = cx.to_async();
70 let executor = cx.background_executor().clone();
71 cx.foreground_executor()
72 .spawn(async move {
73 executor.timer(Duration::from_secs(4)).await;
74 async_cx.update(|cx| {
75 if cx.has_global::<NotificationManagerGlobal>() {
76 let manager = cx.global::<NotificationManagerGlobal>().0.clone();
77 manager.update(cx, |this, cx| {
78 this.notifications.retain(|n| n.id != id);
79 cx.notify();
80 });
81 }
82 });
83 })
84 .detach();
85
86 cx.notify();
87 });
88 }
89}
90
91impl Render for NotificationManager {
92 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
93 let items = self.notifications.clone();
94 if items.is_empty() {
95 return div();
96 }
97
98 let theme = cx.global::<Config>().theme.clone();
99
100 div()
101 .absolute()
102 .top_8()
103 .right_8()
104 .flex()
105 .flex_col()
106 .items_end()
107 .gap_4()
108 .children(items.into_iter().map(|item| {
109 let (color, icon) = match item.msg_type {
110 NotificationType::Info => (theme.primary.base, IconName::Info),
111 NotificationType::Success => (theme.success.base, IconName::Check),
112 NotificationType::Warning => (theme.warning.base, IconName::TriangleAlert),
113 NotificationType::Error => (theme.danger.base, IconName::CircleX),
114 };
115
116 pop_in(
117 ("liora-notification", item.id),
118 div()
119 .w(px(320.0))
120 .bg(theme.neutral.card)
121 .border_1()
122 .border_color(theme.neutral.border)
123 .p_4()
124 .rounded(px(theme.radius.md))
125 .shadow_lg()
126 .flex()
127 .flex_row()
128 .gap_3()
129 .child(Icon::new(icon).size(px(24.0)).color(color))
130 .child(
131 div()
132 .flex_1()
133 .flex()
134 .flex_col()
135 .gap_1()
136 .child(div().font_weight(gpui::FontWeight::BOLD).child(item.title))
137 .when_some(item.description, |s, d| {
138 s.child(
139 div().text_sm().text_color(theme.neutral.text_3).child(d),
140 )
141 }),
142 ),
143 )
144 }))
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 #[test]
151 fn notifications_use_liora_motion() {
152 let source = include_str!("notification.rs")
153 .split("#[cfg(test)]")
154 .next()
155 .unwrap();
156
157 assert!(source.contains("pop_in("));
158 assert!(source.contains("liora-notification"));
159 }
160}
161
162pub fn show_notification(
163 title: impl Into<SharedString>,
164 description: Option<SharedString>,
165 msg_type: NotificationType,
166 cx: &mut App,
167) {
168 NotificationManager::show(title, description, msg_type, cx);
169}
170
171pub fn render_notifications(cx: &mut App) {
172 if cx.has_global::<NotificationManagerGlobal>() {
173 let manager = cx.global::<NotificationManagerGlobal>().0.clone();
174 if !manager.read(cx).notifications.is_empty() {
175 push_passive_portal(move |_window, _cx| manager.clone().into_any_element(), cx);
176 }
177 }
178}