1use std::{
2 any::TypeId,
3 collections::{HashMap, VecDeque},
4 rc::Rc,
5 time::Duration,
6};
7
8use gpui::{
9 div, prelude::FluentBuilder, px, Animation, AnimationExt, AnyElement, App, AppContext,
10 ClickEvent, Context, DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _,
11 IntoElement, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
12 StyleRefinement, Styled, Subscription, Window,
13};
14use smol::Timer;
15
16use crate::{
17 animation::cubic_bezier,
18 button::{Button, ButtonVariants as _},
19 h_flex, v_flex, ActiveTheme as _, Icon, IconName, Sizable as _, StyledExt,
20};
21
22#[derive(Debug, Clone, Copy, Default)]
23pub enum NotificationType {
24 #[default]
25 Info,
26 Success,
27 Warning,
28 Error,
29}
30
31impl NotificationType {
32 fn icon(&self, cx: &App) -> Icon {
33 match self {
34 Self::Info => Icon::new(IconName::Info).text_color(cx.theme().info),
35 Self::Success => Icon::new(IconName::CircleCheck).text_color(cx.theme().success),
36 Self::Warning => Icon::new(IconName::TriangleAlert).text_color(cx.theme().warning),
37 Self::Error => Icon::new(IconName::CircleX).text_color(cx.theme().danger),
38 }
39 }
40}
41
42#[derive(Debug, PartialEq, Clone, Hash, Eq)]
43pub(crate) enum NotificationId {
44 Id(TypeId),
45 IdAndElementId(TypeId, ElementId),
46}
47
48impl From<TypeId> for NotificationId {
49 fn from(type_id: TypeId) -> Self {
50 Self::Id(type_id)
51 }
52}
53
54impl From<(TypeId, ElementId)> for NotificationId {
55 fn from((type_id, id): (TypeId, ElementId)) -> Self {
56 Self::IdAndElementId(type_id, id)
57 }
58}
59
60pub struct Notification {
62 id: NotificationId,
67 style: StyleRefinement,
68 type_: Option<NotificationType>,
69 title: Option<SharedString>,
70 message: Option<SharedString>,
71 icon: Option<Icon>,
72 autohide: bool,
73 action_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> Button>>,
74 content_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>>,
75 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
76 closing: bool,
77}
78
79impl From<String> for Notification {
80 fn from(s: String) -> Self {
81 Self::new().message(s)
82 }
83}
84
85impl From<SharedString> for Notification {
86 fn from(s: SharedString) -> Self {
87 Self::new().message(s)
88 }
89}
90
91impl From<&'static str> for Notification {
92 fn from(s: &'static str) -> Self {
93 Self::new().message(s)
94 }
95}
96
97impl From<(NotificationType, &'static str)> for Notification {
98 fn from((type_, content): (NotificationType, &'static str)) -> Self {
99 Self::new().message(content).with_type(type_)
100 }
101}
102
103impl From<(NotificationType, SharedString)> for Notification {
104 fn from((type_, content): (NotificationType, SharedString)) -> Self {
105 Self::new().message(content).with_type(type_)
106 }
107}
108
109struct DefaultIdType;
110
111impl Notification {
112 pub fn new() -> Self {
116 let id: SharedString = uuid::Uuid::new_v4().to_string().into();
117 let id = (TypeId::of::<DefaultIdType>(), id.into());
118
119 Self {
120 id: id.into(),
121 style: StyleRefinement::default(),
122 title: None,
123 message: None,
124 type_: None,
125 icon: None,
126 autohide: true,
127 action_builder: None,
128 content_builder: None,
129 on_click: None,
130 closing: false,
131 }
132 }
133
134 pub fn message(mut self, message: impl Into<SharedString>) -> Self {
135 self.message = Some(message.into());
136 self
137 }
138
139 pub fn info(message: impl Into<SharedString>) -> Self {
140 Self::new()
141 .message(message)
142 .with_type(NotificationType::Info)
143 }
144
145 pub fn success(message: impl Into<SharedString>) -> Self {
146 Self::new()
147 .message(message)
148 .with_type(NotificationType::Success)
149 }
150
151 pub fn warning(message: impl Into<SharedString>) -> Self {
152 Self::new()
153 .message(message)
154 .with_type(NotificationType::Warning)
155 }
156
157 pub fn error(message: impl Into<SharedString>) -> Self {
158 Self::new()
159 .message(message)
160 .with_type(NotificationType::Error)
161 }
162
163 pub fn id<T: Sized + 'static>(mut self) -> Self {
170 self.id = TypeId::of::<T>().into();
171 self
172 }
173
174 pub fn id1<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
176 self.id = (TypeId::of::<T>(), key.into()).into();
177 self
178 }
179
180 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
184 self.title = Some(title.into());
185 self
186 }
187
188 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
192 self.icon = Some(icon.into());
193 self
194 }
195
196 pub fn with_type(mut self, type_: NotificationType) -> Self {
198 self.type_ = Some(type_);
199 self
200 }
201
202 pub fn autohide(mut self, autohide: bool) -> Self {
204 self.autohide = autohide;
205 self
206 }
207
208 pub fn on_click(
210 mut self,
211 on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
212 ) -> Self {
213 self.on_click = Some(Rc::new(on_click));
214 self
215 }
216
217 pub fn action<F>(mut self, action: F) -> Self
219 where
220 F: Fn(&mut Window, &mut Context<Self>) -> Button + 'static,
221 {
222 self.action_builder = Some(Rc::new(action));
223 self
224 }
225
226 pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context<Self>) {
228 self.closing = true;
229 cx.notify();
230
231 cx.spawn(async move |view, cx| {
233 Timer::after(Duration::from_secs_f32(0.15)).await;
234 cx.update(|cx| {
235 if let Some(view) = view.upgrade() {
236 view.update(cx, |view, cx| {
237 view.closing = false;
238 cx.emit(DismissEvent);
239 });
240 }
241 })
242 })
243 .detach()
244 }
245
246 pub fn content(
248 mut self,
249 content: impl Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static,
250 ) -> Self {
251 self.content_builder = Some(Rc::new(content));
252 self
253 }
254}
255impl EventEmitter<DismissEvent> for Notification {}
256impl FluentBuilder for Notification {}
257impl Styled for Notification {
258 fn style(&mut self) -> &mut StyleRefinement {
259 &mut self.style
260 }
261}
262impl Render for Notification {
263 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
264 let closing = self.closing;
265 let icon = match self.type_ {
266 None => self.icon.clone(),
267 Some(type_) => Some(type_.icon(cx)),
268 };
269 let has_icon = icon.is_some();
270
271 h_flex()
272 .id("notification")
273 .group("")
274 .occlude()
275 .relative()
276 .w_112()
277 .border_1()
278 .border_color(cx.theme().border)
279 .bg(cx.theme().popover)
280 .rounded(cx.theme().radius_lg)
281 .shadow_md()
282 .py_3p5()
283 .px_4()
284 .gap_3()
285 .refine_style(&self.style)
286 .when_some(icon, |this, icon| {
287 this.child(div().absolute().py_3p5().left_4().child(icon))
288 })
289 .child(
290 v_flex()
291 .flex_1()
292 .overflow_hidden()
293 .when(has_icon, |this| this.pl_6())
294 .when_some(self.title.clone(), |this, title| {
295 this.child(div().text_sm().font_semibold().child(title))
296 })
297 .when_some(self.message.clone(), |this, message| {
298 this.child(div().text_sm().child(message))
299 })
300 .when_some(self.content_builder.clone(), |this, child_builder| {
301 this.child(child_builder(window, cx))
302 }),
303 )
304 .when_some(self.action_builder.clone(), |this, action_builder| {
305 this.child(action_builder(window, cx).small().mr_3p5())
306 })
307 .when_some(self.on_click.clone(), |this, on_click| {
308 this.on_click(cx.listener(move |view, event, window, cx| {
309 view.dismiss(window, cx);
310 on_click(event, window, cx);
311 }))
312 })
313 .child(
314 h_flex()
315 .absolute()
316 .top_3p5()
317 .right_3p5()
318 .invisible()
319 .group_hover("", |this| this.visible())
320 .child(
321 Button::new("close")
322 .icon(IconName::Close)
323 .ghost()
324 .xsmall()
325 .on_click(cx.listener(|this, _, window, cx| this.dismiss(window, cx))),
326 ),
327 )
328 .with_animation(
329 ElementId::NamedInteger("slide-down".into(), closing as u64),
330 Animation::new(Duration::from_secs_f64(0.25))
331 .with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
332 move |this, delta| {
333 if closing {
334 let x_offset = px(0.) + delta * px(45.);
335 let opacity = 1. - delta;
336 this.left(px(0.) + x_offset)
337 .shadow_none()
338 .opacity(opacity)
339 .when(opacity < 0.85, |this| this.shadow_none())
340 } else {
341 let y_offset = px(-45.) + delta * px(45.);
342 let opacity = delta;
343 this.top(px(0.) + y_offset)
344 .opacity(opacity)
345 .when(opacity < 0.85, |this| this.shadow_none())
346 }
347 },
348 )
349 }
350}
351
352pub struct NotificationList {
354 pub(crate) notifications: VecDeque<Entity<Notification>>,
356 expanded: bool,
357 _subscriptions: HashMap<NotificationId, Subscription>,
358}
359
360impl NotificationList {
361 pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
362 Self {
363 notifications: VecDeque::new(),
364 expanded: false,
365 _subscriptions: HashMap::new(),
366 }
367 }
368
369 pub fn push(
370 &mut self,
371 notification: impl Into<Notification>,
372 window: &mut Window,
373 cx: &mut Context<Self>,
374 ) {
375 let notification = notification.into();
376 let id = notification.id.clone();
377 let autohide = notification.autohide;
378
379 self.notifications.retain(|note| note.read(cx).id != id);
381
382 let notification = cx.new(|_| notification);
383
384 self._subscriptions.insert(
385 id.clone(),
386 cx.subscribe(¬ification, move |view, _, _: &DismissEvent, cx| {
387 view.notifications.retain(|note| id != note.read(cx).id);
388 view._subscriptions.remove(&id);
389 }),
390 );
391
392 self.notifications.push_back(notification.clone());
393 if autohide {
394 cx.spawn_in(window, async move |_, cx| {
396 Timer::after(Duration::from_secs(5)).await;
397
398 if let Err(err) =
399 notification.update_in(cx, |note, window, cx| note.dismiss(window, cx))
400 {
401 tracing::error!("failed to auto hide notification: {:?}", err);
402 }
403 })
404 .detach();
405 }
406 cx.notify();
407 }
408
409 pub(crate) fn close(
410 &mut self,
411 id: impl Into<NotificationId>,
412 window: &mut Window,
413 cx: &mut Context<Self>,
414 ) {
415 let id: NotificationId = id.into();
416 if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) {
417 n.update(cx, |note, cx| note.dismiss(window, cx))
418 }
419 cx.notify();
420 }
421
422 pub fn clear(&mut self, _: &mut Window, cx: &mut Context<Self>) {
423 self.notifications.clear();
424 cx.notify();
425 }
426
427 pub fn notifications(&self) -> Vec<Entity<Notification>> {
428 self.notifications.iter().cloned().collect()
429 }
430}
431
432impl Render for NotificationList {
433 fn render(
434 &mut self,
435 window: &mut gpui::Window,
436 cx: &mut gpui::Context<Self>,
437 ) -> impl IntoElement {
438 let size = window.viewport_size();
439 let items = self.notifications.iter().rev().take(10).rev().cloned();
440
441 div().absolute().top_4().right_4().child(
442 v_flex()
443 .id("notification-list")
444 .h(size.height - px(8.))
445 .on_hover(cx.listener(|view, hovered, _, cx| {
446 view.expanded = *hovered;
447 cx.notify()
448 }))
449 .gap_3()
450 .children(items),
451 )
452 }
453}