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#[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 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 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 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 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 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 pub fn with_variant(mut self, variant: AlertVariant) -> Self {
119 self.variant = variant;
120 self
121 }
122
123 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
125 self.icon = icon.into();
126 self
127 }
128
129 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
131 self.title = Some(title.into());
132 self
133 }
134
135 pub fn banner(mut self) -> Self {
140 self.banner = true;
141 self
142 }
143
144 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 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}