1use 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 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 if is_light(bg) { darken(accent, 0.35) } else { fg }
121 };
122 (bg, text, border)
123}
124
125fn is_light(c: Color32) -> bool {
126 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}