herogpui_components/
alert.rs1use gpui::{
4 px, AnyElement, App, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce,
5 SharedString, Styled, Window,
6};
7use herogpui_core::Color;
8use herogpui_theme::ActiveTheme;
9
10use crate::icons;
11
12#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
19#[derive(IntoElement)]
20pub struct Alert {
21 title: SharedString,
22 description: Option<SharedString>,
23 color: Color,
24 indicator: Option<AnyElement>,
27 children: Vec<AnyElement>,
30 sx: Option<Box<gpui::StyleRefinement>>,
32 radius: Option<Pixels>,
34}
35
36impl Alert {
37 pub fn status(mut self, status: Color) -> Self {
40 self.color = status;
41 self
42 }
43
44 pub fn new(title: impl Into<SharedString>) -> Self {
46 Self {
47 title: title.into(),
48 description: None,
49 color: Color::Default,
50 indicator: None,
51 children: Vec::new(),
52 sx: None,
53 radius: None,
54 }
55 }
56
57 pub fn description(mut self, d: impl Into<SharedString>) -> Self {
59 self.description = Some(d.into());
60 self
61 }
62
63 pub fn indicator(mut self, content: impl IntoElement) -> Self {
67 self.indicator = Some(content.into_any_element());
68 self
69 }
70
71 pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
75 self.radius = Some(radius.into());
76 self
77 }
78
79 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
84 crate::util::refine_sx(&mut self.sx, style);
85 self
86 }
87}
88
89impl ParentElement for Alert {
90 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
91 self.children.extend(elements);
93 }
94}
95
96impl RenderOnce for Alert {
97 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
98 let sem = cx.role(self.color);
99 let colors = cx.colors();
100 let layout = cx.layout();
101
102 let bg = colors.surface.background;
106 let fg = colors.foreground;
107 let role_fg = if self.color == Color::Default {
110 colors.foreground
111 } else {
112 sem.soft_foreground(colors.foreground)
113 };
114
115 let glyph = match self.color {
119 Color::Default | Color::Accent => icons::INFO_CIRCLE,
120 Color::Success => icons::CHECK_CIRCLE,
121 Color::Warning => icons::WARNING_TRIANGLE,
122 Color::Danger => icons::CIRCLE_EXCLAMATION,
123 };
124
125 let radius = self
126 .radius
127 .unwrap_or_else(|| crate::util::control_radius(cx));
128 let mut alert = gpui::div()
129 .flex()
130 .items_start()
131 .justify_start()
132 .gap(px(16.))
133 .w_full()
134 .px(px(16.))
135 .py(px(12.))
136 .rounded(radius)
137 .bg(bg)
138 .text_color(fg)
139 .debug_selector(|| "alert-root".to_owned());
140 alert = alert.shadow(layout.surface_shadow.clone());
144
145 let default_indicator_glyph = gpui::svg()
146 .size(px(16.))
147 .path(glyph)
148 .text_color(role_fg)
149 .flex_shrink_0();
150 let indicator_glyph = self
151 .indicator
152 .unwrap_or_else(|| default_indicator_glyph.into_any_element());
153 alert = alert.child(
154 gpui::div()
156 .p(px(4.))
157 .flex()
158 .items_center()
159 .justify_center()
160 .flex_shrink_0()
161 .debug_selector(|| "alert-indicator".to_owned())
162 .child(indicator_glyph),
163 );
164
165 let mut text_col = gpui::div()
173 .flex()
174 .flex_col()
175 .items_start()
176 .flex_1()
177 .min_w_0();
178 text_col = text_col.child(
179 gpui::div()
180 .text_size(px(14.)) .line_height(px(24.))
182 .font_weight(gpui::FontWeight::MEDIUM)
183 .text_color(role_fg)
184 .debug_selector(|| "alert-title".to_owned())
185 .child(self.title.to_string()),
186 );
187 text_col = text_col.debug_selector(|| "alert-content".to_owned());
188 if let Some(desc) = self.description {
189 text_col = text_col.child(
190 gpui::div()
191 .text_size(px(14.))
193 .line_height(px(20.))
195 .text_color(colors.muted)
196 .debug_selector(|| "alert-description".to_owned())
197 .child(desc.to_string()),
198 );
199 }
200 alert = alert.child(text_col);
201
202 alert = alert.children(self.children);
204
205 alert = crate::util::apply_sx(alert, &self.sx);
206 alert
207 }
208}
209
210#[cfg(test)]
214mod painted_tokens {
215 fn implementation() -> &'static str {
216 include_str!("alert.rs")
217 .split("#[cfg(test)]")
218 .next()
219 .expect("the implementation section is always present")
220 }
221
222 #[test]
223 fn the_alert_container_is_always_surface() {
224 let source = implementation();
225 assert!(
226 source.contains("let bg = colors.surface.background;"),
227 "every status must paint `bg-surface` (pinned `.alert`)"
228 );
229 assert!(
230 !source.contains("sem.soft()"),
231 "no alert container may paint a role soft background"
232 );
233 }
234
235 #[test]
236 fn the_indicator_follows_the_status_soft_foreground() {
237 let source = implementation();
238 let chain = source
242 .split("let default_indicator_glyph = gpui::svg()")
243 .nth(1)
244 .expect("the indicator must paint the glyph as a declared svg element")
245 .split(';')
246 .next()
247 .expect("the declaration must terminate");
248 assert!(
249 chain.contains(".text_color(role_fg)"),
250 "the indicator must paint the same token as the title: \
251 `text-foreground` on default, `text-{{role}}-soft-foreground` \
252 otherwise (pinned `.alert__indicator`)"
253 );
254 assert!(
255 !chain.contains("colors.muted"),
256 "the default indicator is `text-foreground`, not the muted tone"
257 );
258 }
259
260 #[test]
261 fn custom_indicator_keeps_the_shared_indicator_box() {
262 let source = implementation();
263 assert!(source.contains("pub fn indicator(mut self, content: impl IntoElement)"));
264 assert!(source.contains(".indicator\n .unwrap_or_else"));
265 assert!(source.contains(".p(px(4.))"));
266 assert!(source.contains(".size(px(16.))"));
267 }
268}
269
270crate::util::impl_component_styled!(Alert);