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)]
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#[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 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 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 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 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 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 pub fn with_variant(mut self, variant: AlertVariant) -> Self {
120 self.variant = variant;
121 self
122 }
123
124 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
126 self.icon = icon.into();
127 self
128 }
129
130 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
132 self.title = Some(title.into());
133 self
134 }
135
136 pub fn banner(mut self) -> Self {
141 self.banner = true;
142 self
143 }
144
145 pub fn visible(mut self, visible: bool) -> Self {
147 self.visible = visible;
148 self
149 }
150
151 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}