Skip to main content

guise/feedback/
notification.rs

1//! `Notification` — an elevated toast card with an accent bar.
2
3use gpui::prelude::*;
4use gpui::{div, px, App, ClickEvent, FontWeight, IntoElement, SharedString, Window};
5
6use crate::input::ClickHandler;
7use crate::theme::{theme, ColorName, Size};
8
9/// A toast-style notification card. The Mantine `Notification`. Positioning and
10/// stacking are the host's responsibility; this is the visual card.
11#[derive(IntoElement)]
12pub struct Notification {
13    title: Option<SharedString>,
14    message: SharedString,
15    color: ColorName,
16    icon: Option<SharedString>,
17    on_close: Option<ClickHandler>,
18}
19
20impl Notification {
21    pub fn new(message: impl Into<SharedString>) -> Self {
22        Notification {
23            title: None,
24            message: message.into(),
25            color: ColorName::Blue,
26            icon: None,
27            on_close: None,
28        }
29    }
30
31    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
32        self.title = Some(title.into());
33        self
34    }
35
36    pub fn color(mut self, color: ColorName) -> Self {
37        self.color = color;
38        self
39    }
40
41    pub fn icon(mut self, icon: impl Into<SharedString>) -> Self {
42        self.icon = Some(icon.into());
43        self
44    }
45
46    pub fn on_close(
47        mut self,
48        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
49    ) -> Self {
50        self.on_close = Some(Box::new(handler));
51        self
52    }
53}
54
55impl RenderOnce for Notification {
56    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
57        let t = theme(cx);
58        let accent = t.color(self.color, t.primary_shade()).hsla();
59        let surface = t.surface().hsla();
60        let text = t.text().hsla();
61        let dimmed = t.dimmed().hsla();
62        let radius = t.radius(Size::Md);
63
64        let mut content = div().flex().flex_col().gap(px(2.0)).flex_1();
65        if let Some(title) = self.title {
66            content = content.child(
67                div()
68                    .font_weight(FontWeight::BOLD)
69                    .text_size(px(t.font_size(Size::Sm)))
70                    .text_color(text)
71                    .child(title),
72            );
73        }
74        content = content.child(
75            div()
76                .text_size(px(t.font_size(Size::Sm)))
77                .text_color(dimmed)
78                .child(self.message),
79        );
80
81        let mut row = div()
82            .flex()
83            .items_start()
84            .gap(px(12.0))
85            .w(px(360.0))
86            .p(px(t.spacing(Size::Md)))
87            .rounded(px(radius))
88            .bg(surface)
89            .border_1()
90            .border_color(t.border().hsla())
91            .shadow_md()
92            // Leading accent bar.
93            .child(div().w(px(4.0)).h(px(38.0)).rounded(px(4.0)).bg(accent));
94
95        if let Some(icon) = self.icon {
96            row = row.child(
97                div()
98                    .text_size(px(t.font_size(Size::Md)))
99                    .text_color(accent)
100                    .child(icon),
101            );
102        }
103        row = row.child(content);
104        if let Some(handler) = self.on_close {
105            row = row.child(
106                div()
107                    .id("guise-notification-close")
108                    .text_color(dimmed)
109                    .child(SharedString::new_static("\u{00d7}"))
110                    .on_click(handler),
111            );
112        }
113        row
114    }
115}