Skip to main content

guise/feedback/
alert.rs

1//! `Alert` — an inline callout for info, success, warning, and error states.
2
3use gpui::prelude::*;
4use gpui::{div, px, App, ClickEvent, FontWeight, IntoElement, SharedString, Window};
5
6use crate::devtools::Probed;
7use crate::icon::Glyph;
8use crate::input::ClickHandler;
9use crate::style::{surface, ColorValue, Variant};
10use crate::theme::{theme, Size};
11
12/// A colored message callout.
13#[derive(IntoElement)]
14pub struct Alert {
15  title: Option<SharedString>,
16  message: SharedString,
17  variant: Variant,
18  color: ColorValue,
19  icon: Option<Glyph>,
20  on_close: Option<ClickHandler>,
21}
22
23impl Alert {
24  pub fn new(message: impl Into<SharedString>) -> Self {
25    Alert {
26      title: None,
27      message: message.into(),
28      variant: Variant::Light,
29      color: ColorValue::default(),
30      icon: None,
31      on_close: None,
32    }
33  }
34
35  pub fn title(mut self, title: impl Into<SharedString>) -> Self {
36    self.title = Some(title.into());
37    self
38  }
39
40  pub fn variant(mut self, variant: Variant) -> Self {
41    self.variant = variant;
42    self
43  }
44
45  pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
46    self.color = color.into();
47    self
48  }
49
50  /// A leading glyph (a Lucide [`IconName`](crate::IconName) or a character).
51  pub fn icon(mut self, icon: impl Into<Glyph>) -> Self {
52    self.icon = Some(icon.into());
53    self
54  }
55
56  pub fn on_close(
57    mut self,
58    handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
59  ) -> Self {
60    self.on_close = Some(Box::new(handler));
61    self
62  }
63}
64
65impl RenderOnce for Alert {
66  fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
67    let t = theme(cx);
68    let s = surface(t, self.color, self.variant);
69    let filled = self.variant == Variant::Filled;
70    let body_color = if filled { s.fg } else { t.text().hsla() };
71    let radius = t.radius(Size::Md);
72    let pad = t.spacing(Size::Md);
73
74    let mut content = div().flex().flex_col().gap(px(2.0)).flex_1();
75    if let Some(title) = self.title {
76      content = content.child(
77        div()
78          .font_weight(FontWeight::BOLD)
79          .text_size(px(t.font_size(Size::Sm)))
80          .text_color(s.fg)
81          .child(title),
82      );
83    }
84    content = content.child(
85      div()
86        .text_size(px(t.font_size(Size::Sm)))
87        .text_color(body_color)
88        .child(self.message),
89    );
90
91    let mut row = div()
92      .flex()
93      .items_start()
94      .gap(px(10.0))
95      .p(px(pad))
96      .rounded(px(radius))
97      .bg(s.bg);
98    if let Some(border) = s.border {
99      row = row.border_1().border_color(border);
100    }
101    if let Some(icon) = self.icon {
102      row = row.child(
103        div()
104          .text_size(px(t.font_size(Size::Md)))
105          .text_color(s.fg)
106          .child(icon),
107      );
108    }
109    row = row.child(content);
110    if let Some(handler) = self.on_close {
111      let close_color = if filled { s.fg } else { t.dimmed().hsla() };
112      row = row.child(
113        div()
114          .id("guise-alert-close")
115          .text_color(close_color)
116          .child(SharedString::new_static("\u{00d7}"))
117          .on_click(handler),
118      );
119    }
120    row.probe("Alert").attr("variant", self.variant.label())
121  }
122}