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