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