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