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 {
114 let id: SharedString = uuid::Uuid::new_v4().to_string().into();
115 let id = (TypeId::of::<DefaultIdType>(), id.into());
116
117 Self {
118 id: id.into(),
119 style: StyleRefinement::default(),
120 title: None,
121 message: None,
122 type_: None,
123 icon: None,
124 autohide: true,
125 action_builder: None,
126 content_builder: None,
127 on_click: None,
128 closing: false,
129 }
130 }
131
132 pub fn message(mut self, message: impl Into<SharedString>) -> Self {
134 self.message = Some(message.into());
135 self
136 }
137
138 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 {
147 Self::new()
148 .message(message)
149 .with_type(NotificationType::Success)
150 }
151
152 pub fn warning(message: impl Into<SharedString>) -> Self {
154 Self::new()
155 .message(message)
156 .with_type(NotificationType::Warning)
157 }
158
159 pub fn error(message: impl Into<SharedString>) -> Self {
161 Self::new()
162 .message(message)
163 .with_type(NotificationType::Error)
164 }
165
166 pub fn id<T: Sized + 'static>(mut self) -> Self {
173 self.id = TypeId::of::<T>().into();
174 self
175 }
176
177 pub fn id1<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
179 self.id = (TypeId::of::<T>(), key.into()).into();
180 self
181 }
182
183 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
187 self.title = Some(title.into());
188 self
189 }
190
191 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
195 self.icon = Some(icon.into());
196 self
197 }
198
199 pub fn with_type(mut self, type_: NotificationType) -> Self {
201 self.type_ = Some(type_);
202 self
203 }
204
205 pub fn autohide(mut self, autohide: bool) -> Self {
207 self.autohide = autohide;
208 self
209 }
210
211 pub fn on_click(
213 mut self,
214 on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
215 ) -> Self {
216 self.on_click = Some(Rc::new(on_click));
217 self
218 }
219
220 pub fn action<F>(mut self, action: F) -> Self
222 where
223 F: Fn(&mut Window, &mut Context<Self>) -> Button + 'static,
224 {
225 self.action_builder = Some(Rc::new(action));
226 self
227 }
228
229 pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context<Self>) {
231 self.closing = true;
232 cx.notify();
233
234 cx.spawn(async move |view, cx| {
236 Timer::after(Duration::from_secs_f32(0.15)).await;
237 cx.update(|cx| {
238 if let Some(view) = view.upgrade() {
239 view.update(cx, |view, cx| {
240 view.closing = false;
241 cx.emit(DismissEvent);
242 });
243 }
244 })
245 })
246 .detach()
247 }
248
249 pub fn content(
251 mut self,
252 content: impl Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static,
253 ) -> Self {
254 self.content_builder = Some(Rc::new(content));
255 self
256 }
257}
258impl EventEmitter<DismissEvent> for Notification {}
259impl FluentBuilder for Notification {}
260impl Styled for Notification {
261 fn style(&mut self) -> &mut StyleRefinement {
262 &mut self.style
263 }
264}
265impl Render for Notification {
266 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
267 let closing = self.closing;
268 let icon = match self.type_ {
269 None => self.icon.clone(),
270 Some(type_) => Some(type_.icon(cx)),
271 };
272 let has_icon = icon.is_some();
273
274 h_flex()
275 .id("notification")
276 .group("")
277 .occlude()
278 .relative()
279 .w_112()
280 .border_1()
281 .border_color(cx.theme().border)
282 .bg(cx.theme().popover)
283 .rounded(cx.theme().radius_lg)
284 .shadow_md()
285 .py_3p5()
286 .px_4()
287 .gap_3()
288 .refine_style(&self.style)
289 .when_some(icon, |this, icon| {
290 this.child(div().absolute().py_3p5().left_4().child(icon))
291 })
292 .child(
293 v_flex()
294 .flex_1()
295 .overflow_hidden()
296 .when(has_icon, |this| this.pl_6())
297 .when_some(self.title.clone(), |this, title| {
298 this.child(div().text_sm().font_semibold().child(title))
299 })
300 .when_some(self.message.clone(), |this, message| {
301 this.child(div().text_sm().child(message))
302 })
303 .when_some(self.content_builder.clone(), |this, child_builder| {
304 this.child(child_builder(window, cx))
305 }),
306 )
307 .when_some(self.action_builder.clone(), |this, action_builder| {
308 this.child(action_builder(window, cx).small().mr_3p5())
309 })
310 .when_some(self.on_click.clone(), |this, on_click| {
311 this.on_click(cx.listener(move |view, event, window, cx| {
312 view.dismiss(window, cx);
313 on_click(event, window, cx);
314 }))
315 })
316 .child(
317 h_flex()
318 .absolute()
319 .top_3p5()
320 .right_3p5()
321 .invisible()
322 .group_hover("", |this| this.visible())
323 .child(
324 Button::new("close")
325 .icon(IconName::Close)
326 .ghost()
327 .xsmall()
328 .on_click(cx.listener(|this, _, window, cx| this.dismiss(window, cx))),
329 ),
330 )
331 .with_animation(
332 ElementId::NamedInteger("slide-down".into(), closing as u64),
333 Animation::new(Duration::from_secs_f64(0.25))
334 .with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
335 move |this, delta| {
336 if closing {
337 let x_offset = px(0.) + delta * px(45.);
338 let opacity = 1. - delta;
339 this.left(px(0.) + x_offset)
340 .shadow_none()
341 .opacity(opacity)
342 .when(opacity < 0.85, |this| this.shadow_none())
343 } else {
344 let y_offset = px(-45.) + delta * px(45.);
345 let opacity = delta;
346 this.top(px(0.) + y_offset)
347 .opacity(opacity)
348 .when(opacity < 0.85, |this| this.shadow_none())
349 }
350 },
351 )
352 }
353}
354
355pub struct NotificationList {
357 pub(crate) notifications: VecDeque<Entity<Notification>>,
359 expanded: bool,
360 _subscriptions: HashMap<NotificationId, Subscription>,
361}
362
363impl NotificationList {
364 pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
365 Self {
366 notifications: VecDeque::new(),
367 expanded: false,
368 _subscriptions: HashMap::new(),
369 }
370 }
371
372 pub fn push(
373 &mut self,
374 notification: impl Into<Notification>,
375 window: &mut Window,
376 cx: &mut Context<Self>,
377 ) {
378 let notification = notification.into();
379 let id = notification.id.clone();
380 let autohide = notification.autohide;
381
382 self.notifications.retain(|note| note.read(cx).id != id);
384
385 let notification = cx.new(|_| notification);
386
387 self._subscriptions.insert(
388 id.clone(),
389 cx.subscribe(¬ification, move |view, _, _: &DismissEvent, cx| {
390 view.notifications.retain(|note| id != note.read(cx).id);
391 view._subscriptions.remove(&id);
392 }),
393 );
394
395 self.notifications.push_back(notification.clone());
396 if autohide {
397 cx.spawn_in(window, async move |_, cx| {
399 Timer::after(Duration::from_secs(5)).await;
400
401 if let Err(err) =
402 notification.update_in(cx, |note, window, cx| note.dismiss(window, cx))
403 {
404 tracing::error!("failed to auto hide notification: {:?}", err);
405 }
406 })
407 .detach();
408 }
409 cx.notify();
410 }
411
412 pub(crate) fn close(
413 &mut self,
414 id: impl Into<NotificationId>,
415 window: &mut Window,
416 cx: &mut Context<Self>,
417 ) {
418 let id: NotificationId = id.into();
419 if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) {
420 n.update(cx, |note, cx| note.dismiss(window, cx))
421 }
422 cx.notify();
423 }
424
425 pub fn clear(&mut self, _: &mut Window, cx: &mut Context<Self>) {
426 self.notifications.clear();
427 cx.notify();
428 }
429
430 pub fn notifications(&self) -> Vec<Entity<Notification>> {
431 self.notifications.iter().cloned().collect()
432 }
433}
434
435impl Render for NotificationList {
436 fn render(
437 &mut self,
438 window: &mut gpui::Window,
439 cx: &mut gpui::Context<Self>,
440 ) -> impl IntoElement {
441 let size = window.viewport_size();
442 let items = self.notifications.iter().rev().take(10).rev().cloned();
443
444 div().absolute().top_4().right_4().child(
445 v_flex()
446 .id("notification-list")
447 .h(size.height - px(8.))
448 .on_hover(cx.listener(|view, hovered, _, cx| {
449 view.expanded = *hovered;
450 cx.notify()
451 }))
452 .gap_3()
453 .children(items),
454 )
455 }
456}