gpui_component/
alert.rs

1use std::rc::Rc;
2
3use gpui::{
4    div, prelude::FluentBuilder as _, px, rems, App, ClickEvent, ElementId, Empty, Hsla,
5    InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString,
6    StatefulInteractiveElement, StyleRefinement, Styled, Window,
7};
8
9use crate::{
10    h_flex,
11    text::{Text, TextViewStyle},
12    ActiveTheme as _, Icon, IconName, Sizable, Size, StyledExt,
13};
14
15/// The variant of the [`Alert`].
16#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
17pub enum AlertVariant {
18    #[default]
19    Secondary,
20    Info,
21    Success,
22    Warning,
23    Error,
24}
25
26impl AlertVariant {
27    fn fg(&self, cx: &App) -> Hsla {
28        match self {
29            AlertVariant::Secondary => cx.theme().secondary_foreground,
30            AlertVariant::Info => cx.theme().info,
31            AlertVariant::Success => cx.theme().success,
32            AlertVariant::Warning => cx.theme().warning,
33            AlertVariant::Error => cx.theme().danger,
34        }
35    }
36
37    fn color(&self, cx: &App) -> Hsla {
38        match self {
39            AlertVariant::Secondary => cx.theme().secondary,
40            AlertVariant::Info => cx.theme().info,
41            AlertVariant::Success => cx.theme().success,
42            AlertVariant::Warning => cx.theme().warning,
43            AlertVariant::Error => cx.theme().danger,
44        }
45    }
46
47    fn border_color(&self, cx: &App) -> Hsla {
48        match self {
49            AlertVariant::Secondary => cx.theme().border,
50            AlertVariant::Info => cx.theme().info,
51            AlertVariant::Success => cx.theme().success,
52            AlertVariant::Warning => cx.theme().warning,
53            AlertVariant::Error => cx.theme().danger,
54        }
55    }
56}
57
58/// Alert used to display a message to the user.
59#[derive(IntoElement)]
60pub struct Alert {
61    id: ElementId,
62    style: StyleRefinement,
63    variant: AlertVariant,
64    icon: Icon,
65    title: Option<SharedString>,
66    message: Text,
67    size: Size,
68    banner: bool,
69    on_close: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
70    visible: bool,
71}
72
73impl Alert {
74    /// Create a new alert with the given message.
75    pub fn new(id: impl Into<ElementId>, message: impl Into<Text>) -> Self {
76        Self {
77            id: id.into(),
78            style: StyleRefinement::default(),
79            variant: AlertVariant::default(),
80            icon: Icon::new(IconName::Info),
81            title: None,
82            message: message.into(),
83            size: Size::default(),
84            banner: false,
85            visible: true,
86            on_close: None,
87        }
88    }
89
90    /// Create a new info [`AlertVariant::Info`] with the given message.
91    pub fn info(id: impl Into<ElementId>, message: impl Into<Text>) -> Self {
92        Self::new(id, message)
93            .with_variant(AlertVariant::Info)
94            .icon(IconName::Info)
95    }
96
97    /// Create a new [`AlertVariant::Success`] alert with the given message.
98    pub fn success(id: impl Into<ElementId>, message: impl Into<Text>) -> Self {
99        Self::new(id, message)
100            .with_variant(AlertVariant::Success)
101            .icon(IconName::CircleCheck)
102    }
103
104    /// Create a new [`AlertVariant::Warning`] alert with the given message.
105    pub fn warning(id: impl Into<ElementId>, message: impl Into<Text>) -> Self {
106        Self::new(id, message)
107            .with_variant(AlertVariant::Warning)
108            .icon(IconName::TriangleAlert)
109    }
110
111    /// Create a new [`AlertVariant::Error`] alert with the given message.
112    pub fn error(id: impl Into<ElementId>, message: impl Into<Text>) -> Self {
113        Self::new(id, message)
114            .with_variant(AlertVariant::Error)
115            .icon(IconName::CircleX)
116    }
117
118    /// Sets the [`AlertVariant`] of the alert.
119    pub fn with_variant(mut self, variant: AlertVariant) -> Self {
120        self.variant = variant;
121        self
122    }
123
124    /// Set the icon for the alert.
125    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
126        self.icon = icon.into();
127        self
128    }
129
130    /// Set the title for the alert.
131    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
132        self.title = Some(title.into());
133        self
134    }
135
136    /// Set alert as banner style.
137    ///
138    /// The `banner` style will make the alert take the full width of the container and not border and radius.
139    /// This mode will not display `title`.
140    pub fn banner(mut self) -> Self {
141        self.banner = true;
142        self
143    }
144
145    /// Set the visibility of the alert.
146    pub fn visible(mut self, visible: bool) -> Self {
147        self.visible = visible;
148        self
149    }
150
151    /// Set alert as closable, true will show Close icon.
152    pub fn on_close(
153        mut self,
154        on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
155    ) -> Self {
156        self.on_close = Some(Rc::new(on_close));
157        self
158    }
159}
160
161impl Sizable for Alert {
162    fn with_size(mut self, size: impl Into<Size>) -> Self {
163        self.size = size.into();
164        self
165    }
166}
167
168impl Styled for Alert {
169    fn style(&mut self) -> &mut gpui::StyleRefinement {
170        &mut self.style
171    }
172}
173
174impl RenderOnce for Alert {
175    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
176        if !self.visible {
177            return Empty.into_any_element();
178        }
179
180        let (radius, padding_x, padding_y, gap) = match self.size {
181            Size::XSmall => (cx.theme().radius, px(12.), px(6.), px(6.)),
182            Size::Small => (cx.theme().radius, px(12.), px(8.), px(6.)),
183            Size::Large => (cx.theme().radius_lg, px(20.), px(14.), px(12.)),
184            _ => (cx.theme().radius, px(16.), px(10.), px(12.)),
185        };
186
187        let color = self.variant.color(cx);
188        let fg = self.variant.fg(cx);
189        let border_color = self.variant.border_color(cx);
190
191        h_flex()
192            .id(self.id)
193            .w_full()
194            .text_color(fg)
195            .bg(color.opacity(0.08))
196            .px(padding_x)
197            .py(padding_y)
198            .gap(gap)
199            .justify_between()
200            .text_sm()
201            .border_1()
202            .border_color(border_color)
203            .when(!self.banner, |this| this.rounded(radius).items_start())
204            .refine_style(&self.style)
205            .child(
206                div()
207                    .flex()
208                    .flex_1()
209                    .when(self.banner, |this| this.items_center())
210                    .overflow_hidden()
211                    .gap(gap)
212                    .child(
213                        div()
214                            .when(!self.banner, |this| this.mt(px(5.)))
215                            .child(self.icon),
216                    )
217                    .child(
218                        div()
219                            .flex_1()
220                            .overflow_hidden()
221                            .gap_3()
222                            .when(!self.banner, |this| {
223                                this.when_some(self.title, |this, title| {
224                                    this.child(
225                                        div().w_full().truncate().font_semibold().child(title),
226                                    )
227                                })
228                            })
229                            .child(
230                                self.message
231                                    .style(TextViewStyle::default().paragraph_gap(rems(0.2))),
232                            ),
233                    ),
234            )
235            .when_some(self.on_close, |this, on_close| {
236                this.child(
237                    div()
238                        .id("close")
239                        .p_0p5()
240                        .rounded(cx.theme().radius)
241                        .hover(|this| this.bg(color.opacity(0.1)))
242                        .active(|this| this.bg(color.opacity(0.2)))
243                        .on_click(move |ev, window, cx| {
244                            on_close(ev, window, cx);
245                        })
246                        .child(
247                            Icon::new(IconName::Close)
248                                .with_size(self.size.max(Size::Medium))
249                                .flex_shrink_0(),
250                        ),
251                )
252            })
253            .into_any_element()
254    }
255}