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 if self.closing {
234 return;
235 }
236 self.closing = true;
237 cx.notify();
238
239 cx.spawn(async move |view, cx| {
241 Timer::after(Duration::from_secs_f32(0.15)).await;
242 cx.update(|cx| {
243 if let Some(view) = view.upgrade() {
244 view.update(cx, |view, cx| {
245 view.closing = false;
246 cx.emit(DismissEvent);
247 });
248 }
249 })
250 })
251 .detach()
252 }
253
254 pub fn content(
256 mut self,
257 content: impl Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement + 'static,
258 ) -> Self {
259 self.content_builder = Some(Rc::new(content));
260 self
261 }
262}
263impl EventEmitter<DismissEvent> for Notification {}
264impl FluentBuilder for Notification {}
265impl Styled for Notification {
266 fn style(&mut self) -> &mut StyleRefinement {
267 &mut self.style
268 }
269}
270impl Render for Notification {
271 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
272 let content = self.content_builder.clone().map(|builder| builder(self, window, cx));
273 let action = self.action_builder.clone().map(|builder| builder(self, window, cx).small().mr_3p5());
274
275 let closing = self.closing;
276 let icon = match self.type_ {
277 None => self.icon.clone(),
278 Some(type_) => Some(type_.icon(cx)),
279 };
280 let has_icon = icon.is_some();
281
282 h_flex()
283 .id("notification")
284 .group("")
285 .occlude()
286 .relative()
287 .w_112()
288 .border_1()
289 .border_color(cx.theme().border)
290 .bg(cx.theme().popover)
291 .rounded(cx.theme().radius_lg)
292 .shadow_md()
293 .py_3p5()
294 .px_4()
295 .gap_3()
296 .refine_style(&self.style)
297 .when_some(icon, |this, icon| {
298 this.child(div().absolute().py_3p5().left_4().child(icon))
299 })
300 .child(
301 v_flex()
302 .flex_1()
303 .overflow_hidden()
304 .when(has_icon, |this| this.pl_6())
305 .when_some(self.title.clone(), |this, title| {
306 this.child(div().text_sm().font_semibold().child(title))
307 })
308 .when_some(self.message.clone(), |this, message| {
309 this.child(div().text_sm().child(message))
310 })
311 .when_some(content, |this, content| {
312 this.child(content)
313 }),
314 )
315 .when_some(action, |this, action| {
316 this.child(action)
317 })
318 .when_some(self.on_click.clone(), |this, on_click| {
319 this.on_click(cx.listener(move |view, event, window, cx| {
320 view.dismiss(window, cx);
321 on_click(event, window, cx);
322 }))
323 })
324 .child(
325 h_flex()
326 .absolute()
327 .top_3p5()
328 .right_3p5()
329 .invisible()
330 .group_hover("", |this| this.visible())
331 .child(
332 Button::new("close")
333 .icon(IconName::Close)
334 .ghost()
335 .xsmall()
336 .on_click(cx.listener(|this, _, window, cx| this.dismiss(window, cx))),
337 ),
338 )
339 .with_animation(
340 ElementId::NamedInteger("slide-down".into(), closing as u64),
341 Animation::new(Duration::from_secs_f64(0.25))
342 .with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
343 move |this, delta| {
344 if closing {
345 let x_offset = px(0.) + delta * px(45.);
346 let opacity = 1. - delta;
347 this.left(px(0.) + x_offset)
348 .shadow_none()
349 .opacity(opacity)
350 .when(opacity < 0.85, |this| this.shadow_none())
351 } else {
352 let y_offset = px(-45.) + delta * px(45.);
353 let opacity = delta;
354 this.top(px(0.) + y_offset)
355 .opacity(opacity)
356 .when(opacity < 0.85, |this| this.shadow_none())
357 }
358 },
359 )
360 }
361}
362
363pub struct NotificationList {
365 pub(crate) notifications: VecDeque<Entity<Notification>>,
367 expanded: bool,
368 _subscriptions: HashMap<NotificationId, Subscription>,
369}
370
371impl NotificationList {
372 pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
373 Self {
374 notifications: VecDeque::new(),
375 expanded: false,
376 _subscriptions: HashMap::new(),
377 }
378 }
379
380 pub fn push(
381 &mut self,
382 notification: impl Into<Notification>,
383 window: &mut Window,
384 cx: &mut Context<Self>,
385 ) {
386 let notification = notification.into();
387 let id = notification.id.clone();
388 let autohide = notification.autohide;
389
390 self.notifications.retain(|note| note.read(cx).id != id);
392
393 let notification = cx.new(|_| notification);
394
395 self._subscriptions.insert(
396 id.clone(),
397 cx.subscribe(¬ification, move |view, _, _: &DismissEvent, cx| {
398 view.notifications.retain(|note| id != note.read(cx).id);
399 view._subscriptions.remove(&id);
400 }),
401 );
402
403 self.notifications.push_back(notification.clone());
404 if autohide {
405 cx.spawn_in(window, async move |_, cx| {
407 Timer::after(Duration::from_secs(5)).await;
408
409 if let Err(err) =
410 notification.update_in(cx, |note, window, cx| note.dismiss(window, cx))
411 {
412 tracing::error!("failed to auto hide notification: {:?}", err);
413 }
414 })
415 .detach();
416 }
417 cx.notify();
418 }
419
420 pub(crate) fn close(
421 &mut self,
422 id: impl Into<NotificationId>,
423 window: &mut Window,
424 cx: &mut Context<Self>,
425 ) {
426 let id: NotificationId = id.into();
427 if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) {
428 n.update(cx, |note, cx| note.dismiss(window, cx))
429 }
430 cx.notify();
431 }
432
433 pub fn clear(&mut self, _: &mut Window, cx: &mut Context<Self>) {
434 self.notifications.clear();
435 cx.notify();
436 }
437
438 pub fn notifications(&self) -> Vec<Entity<Notification>> {
439 self.notifications.iter().cloned().collect()
440 }
441}
442
443impl Render for NotificationList {
444 fn render(
445 &mut self,
446 window: &mut gpui::Window,
447 cx: &mut gpui::Context<Self>,
448 ) -> impl IntoElement {
449 let size = window.viewport_size();
450 let items = self.notifications.iter().rev().take(10).rev().cloned();
451
452 div().absolute().top_4().right_4().child(
453 v_flex()
454 .id("notification-list")
455 .h(size.height - px(8.))
456 .on_hover(cx.listener(|view, hovered, _, cx| {
457 view.expanded = *hovered;
458 cx.notify()
459 }))
460 .gap_3()
461 .children(items),
462 )
463 }
464}