1use std::rc::Rc;
2
3use gpui::{
4 App, ClickEvent, ElementId, Empty, Hsla, InteractiveElement, IntoElement, ParentElement as _,
5 RenderOnce, Role, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window,
6 div, prelude::FluentBuilder as _, px, rems, transparent_white,
7};
8
9use crate::{
10 ActiveTheme as _, Colorize, Icon, IconName, Sizable, Size, StyledExt, h_flex,
11 text::{Text, TextViewStyle},
12};
13
14#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
16pub enum AlertVariant {
17 #[default]
18 Default,
19 Info,
20 Success,
21 Warning,
22 Error,
23}
24
25impl AlertVariant {
26 fn fg(&self, cx: &App) -> Hsla {
27 match self {
28 Self::Default => cx.theme().foreground,
29 Self::Info => cx.theme().info,
30 Self::Success => cx.theme().success,
31 Self::Warning => cx.theme().warning,
32 Self::Error => cx.theme().danger,
33 }
34 }
35
36 fn bg(&self, cx: &App) -> Hsla {
37 match self {
38 Self::Default => cx.theme().background,
39 Self::Info => cx.theme().info.mix_oklab(transparent_white(), 0.04),
40 Self::Success => cx.theme().success.mix_oklab(transparent_white(), 0.04),
41 Self::Warning => cx.theme().warning.mix_oklab(transparent_white(), 0.04),
42 Self::Error => cx.theme().danger.mix_oklab(transparent_white(), 0.04),
43 }
44 }
45
46 fn border_color(&self, cx: &App) -> Hsla {
47 match self {
48 Self::Default => cx.theme().border,
49 Self::Info => cx.theme().info.mix_oklab(transparent_white(), 0.3),
50 Self::Success => cx.theme().success.mix_oklab(transparent_white(), 0.3),
51 Self::Warning => cx.theme().warning.mix_oklab(transparent_white(), 0.3),
52 Self::Error => cx.theme().danger.mix_oklab(transparent_white(), 0.3),
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 visible(mut self, visible: bool) -> Self {
146 self.visible = visible;
147 self
148 }
149
150 pub fn on_close(
152 mut self,
153 on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
154 ) -> Self {
155 self.on_close = Some(Rc::new(on_close));
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 bg = self.variant.bg(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 .role(Role::Alert)
193 .w_full()
194 .text_color(fg)
195 .bg(bg)
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(bg.opacity(0.8)))
242 .active(|this| this.bg(bg.opacity(0.9)))
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}