Skip to main content

egui_components/
alert.rs

1//! `Alert` widget — boxed inline message with optional title.
2
3use crate::common::Variant;
4use egui::{
5    pos2, vec2, Color32, FontId, Response, Sense, Stroke, Ui, Widget,
6};
7use egui_components_theme::{mix, Theme, ThemeColor};
8
9pub struct Alert {
10    title: Option<String>,
11    body: String,
12    variant: Variant,
13}
14
15impl Alert {
16    pub fn new(body: impl Into<String>) -> Self {
17        Self {
18            title: None,
19            body: body.into(),
20            variant: Variant::Secondary,
21        }
22    }
23    pub fn title(mut self, t: impl Into<String>) -> Self {
24        self.title = Some(t.into());
25        self
26    }
27    pub fn variant(mut self, v: Variant) -> Self {
28        self.variant = v;
29        self
30    }
31    pub fn info(self) -> Self {
32        self.variant(Variant::Info)
33    }
34    pub fn success(self) -> Self {
35        self.variant(Variant::Success)
36    }
37    pub fn warning(self) -> Self {
38        self.variant(Variant::Warning)
39    }
40    pub fn danger(self) -> Self {
41        self.variant(Variant::Danger)
42    }
43}
44
45impl Widget for Alert {
46    fn ui(self, ui: &mut Ui) -> Response {
47        let theme = Theme::get(ui.ctx());
48        let m = theme.metrics;
49        let c = theme.colors;
50
51        let (bg, fg, border) = alert_colors(&c, self.variant);
52
53        let pad_x = 14.0;
54        let pad_y = 12.0;
55        let width = ui.available_width();
56        let title_font = FontId::proportional(m.font_size_md);
57        let body_font = FontId::proportional(m.font_size_sm);
58
59        let max_text_w = width - pad_x * 2.0;
60        let title_galley = self.title.as_ref().map(|t| {
61            ui.ctx().fonts_mut(|f| {
62                f.layout(
63                    t.clone(),
64                    title_font.clone(),
65                    fg,
66                    max_text_w,
67                )
68            })
69        });
70        let body_galley = ui.ctx().fonts_mut(|f| {
71            f.layout(self.body.clone(), body_font.clone(), fg, max_text_w)
72        });
73
74        let mut content_h = body_galley.size().y;
75        if let Some(g) = &title_galley {
76            content_h += g.size().y + 4.0;
77        }
78        let total_size = vec2(width, content_h + pad_y * 2.0);
79        let (rect, response) = ui.allocate_exact_size(total_size, Sense::hover());
80
81        if ui.is_rect_visible(rect) {
82            let painter = ui.painter();
83            let radius = theme.corner();
84            painter.rect(
85                rect,
86                radius,
87                bg,
88                Stroke::new(1.0, border),
89                egui::StrokeKind::Inside,
90            );
91
92            let mut y = rect.top() + pad_y;
93            let x = rect.left() + pad_x;
94            if let Some(g) = title_galley {
95                painter.galley_with_override_text_color(pos2(x, y), g.clone(), fg);
96                y += g.size().y + 4.0;
97            }
98            painter.galley_with_override_text_color(pos2(x, y), body_galley, fg);
99        }
100
101        response
102    }
103}
104
105fn alert_colors(c: &ThemeColor, v: Variant) -> (Color32, Color32, Color32) {
106    let (accent, fg) = match v {
107        Variant::Info => (c.info_background, c.info_foreground),
108        Variant::Success => (c.success_background, c.success_foreground),
109        Variant::Warning => (c.warning_background, c.warning_foreground),
110        Variant::Danger => (c.danger_background, c.danger_foreground),
111        _ => (c.muted_foreground, c.foreground),
112    };
113    // Tint background by mixing the accent into the surface.
114    let bg = mix(c.background, accent, 0.10);
115    let border = mix(c.border, accent, 0.40);
116    let text = if matches!(v, Variant::Secondary) {
117        c.foreground
118    } else {
119        // For colored variants we prefer dark text on light tint.
120        if is_light(bg) { darken(accent, 0.35) } else { fg }
121    };
122    (bg, text, border)
123}
124
125fn is_light(c: Color32) -> bool {
126    // Perceptual luminance approximation.
127    let r = c.r() as f32;
128    let g = c.g() as f32;
129    let b = c.b() as f32;
130    (0.299 * r + 0.587 * g + 0.114 * b) > 140.0
131}
132
133fn darken(c: Color32, t: f32) -> Color32 {
134    mix(c, Color32::BLACK, t)
135}