Skip to main content

guise/feedback/
alert.rs

1//! `Alert` — an inline callout for info, success, warning, and error states.
2
3use gpui::prelude::*;
4use gpui::{div, px, App, ClickEvent, FontWeight, IntoElement, SharedString, Window};
5
6use crate::input::ClickHandler;
7use crate::style::{surface, ColorValue, Variant};
8use crate::theme::{theme, Size};
9
10/// A colored message callout. The Mantine `Alert`.
11#[derive(IntoElement)]
12pub struct Alert {
13    title: Option<SharedString>,
14    message: SharedString,
15    variant: Variant,
16    color: ColorValue,
17    icon: Option<SharedString>,
18    on_close: Option<ClickHandler>,
19}
20
21impl Alert {
22    pub fn new(message: impl Into<SharedString>) -> Self {
23        Alert {
24            title: None,
25            message: message.into(),
26            variant: Variant::Light,
27            color: ColorValue::default(),
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 variant(mut self, variant: Variant) -> Self {
39        self.variant = variant;
40        self
41    }
42
43    pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
44        self.color = color.into();
45        self
46    }
47
48    /// A leading glyph (e.g. an icon character).
49    pub fn icon(mut self, icon: impl Into<SharedString>) -> Self {
50        self.icon = Some(icon.into());
51        self
52    }
53
54    pub fn on_close(
55        mut self,
56        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
57    ) -> Self {
58        self.on_close = Some(Box::new(handler));
59        self
60    }
61}
62
63impl RenderOnce for Alert {
64    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
65        let t = theme(cx);
66        let s = surface(t, self.color, self.variant);
67        let filled = self.variant == Variant::Filled;
68        let body_color = if filled { s.fg } else { t.text().hsla() };
69        let radius = t.radius(Size::Md);
70        let pad = t.spacing(Size::Md);
71
72        let mut content = div().flex().flex_col().gap(px(2.0)).flex_1();
73        if let Some(title) = self.title {
74            content = content.child(
75                div()
76                    .font_weight(FontWeight::BOLD)
77                    .text_size(px(t.font_size(Size::Sm)))
78                    .text_color(s.fg)
79                    .child(title),
80            );
81        }
82        content = content.child(
83            div()
84                .text_size(px(t.font_size(Size::Sm)))
85                .text_color(body_color)
86                .child(self.message),
87        );
88
89        let mut row = div()
90            .flex()
91            .items_start()
92            .gap(px(10.0))
93            .p(px(pad))
94            .rounded(px(radius))
95            .bg(s.bg);
96        if let Some(border) = s.border {
97            row = row.border_1().border_color(border);
98        }
99        if let Some(icon) = self.icon {
100            row = row.child(
101                div()
102                    .text_size(px(t.font_size(Size::Md)))
103                    .text_color(s.fg)
104                    .child(icon),
105            );
106        }
107        row = row.child(content);
108        if let Some(handler) = self.on_close {
109            let close_color = if filled { s.fg } else { t.dimmed().hsla() };
110            row = row.child(
111                div()
112                    .id("guise-alert-close")
113                    .text_color(close_color)
114                    .child(SharedString::new_static("\u{00d7}"))
115                    .on_click(handler),
116            );
117        }
118        row
119    }
120}