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