Skip to main content

egui_charts/ui_kit/alert/
mod.rs

1//! Alert Component
2//!
3//! A semantic message box for displaying status information to users.
4//!
5//! # Usage
6//!
7//! ```
8//! use egui_charts::ui_kit::alert::Alert;
9//!
10//! # egui::__run_test_ui(|ui| {
11//! // Simple alert
12//! Alert::info("This is an informational message").show(ui);
13//!
14//! // Alert with title
15//! Alert::error("Failed to load data")
16//!     .with_title("Connection Error")
17//!     .show(ui);
18//!
19//! // Dismissible alert
20//! Alert::warning("Your session will expire soon")
21//!     .dismissible(true)
22//!     .show(ui);
23//! # });
24//! ```
25
26use crate::localization as l10n;
27use egui::{Color32, Response, RichText, Ui, Vec2};
28
29use crate::icons::{Icon, icons};
30use crate::theme::{ThemeExt, UiColors};
31use crate::tokens::DESIGN_TOKENS;
32use crate::ui_kit::buttons::{ButtonSize, IconButton};
33
34/// Alert severity levels
35#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
36pub enum AlertKind {
37    /// Informational message (blue)
38    #[default]
39    Info,
40    /// Success message (green)
41    Success,
42    /// Warning message (orange/amber)
43    Warning,
44    /// Error message (red)
45    Error,
46}
47
48impl AlertKind {
49    /// The theme's status colour for this kind, which tints the icon, fill and border
50    pub fn color(&self, ui: &UiColors) -> Color32 {
51        match self {
52            AlertKind::Info => ui.info,
53            AlertKind::Success => ui.success,
54            AlertKind::Warning => ui.warning,
55            AlertKind::Error => ui.error,
56        }
57    }
58
59    /// Get the icon for this alert kind
60    pub fn icon(&self) -> &'static Icon {
61        match self {
62            AlertKind::Info => &icons::INFO,
63            AlertKind::Success => &icons::SUCCESS,
64            AlertKind::Warning => &icons::WARNING,
65            AlertKind::Error => &icons::STATUS_ERROR,
66        }
67    }
68}
69
70/// Response from showing an Alert
71pub struct AlertResponse {
72    /// The underlying egui Response
73    pub response: Response,
74    /// Whether the dismiss button was clicked
75    pub dismissed: bool,
76}
77
78/// A semantic message box for displaying status information
79pub struct Alert {
80    kind: AlertKind,
81    title: Option<String>,
82    message: String,
83    dismissible: bool,
84    show_icon: bool,
85}
86
87impl Alert {
88    /// Create a new alert with the given kind and message
89    pub fn new(kind: AlertKind, message: impl Into<String>) -> Self {
90        Self {
91            kind,
92            title: None,
93            message: message.into(),
94            dismissible: false,
95            show_icon: true,
96        }
97    }
98
99    /// Create an info alert
100    pub fn info(message: impl Into<String>) -> Self {
101        Self::new(AlertKind::Info, message)
102    }
103
104    /// Create a success alert
105    pub fn success(message: impl Into<String>) -> Self {
106        Self::new(AlertKind::Success, message)
107    }
108
109    /// Create a warning alert
110    pub fn warning(message: impl Into<String>) -> Self {
111        Self::new(AlertKind::Warning, message)
112    }
113
114    /// Create an error alert
115    pub fn error(message: impl Into<String>) -> Self {
116        Self::new(AlertKind::Error, message)
117    }
118
119    /// Set the alert title
120    pub fn with_title(mut self, title: impl Into<String>) -> Self {
121        self.title = Some(title.into());
122        self
123    }
124
125    /// Make the alert dismissible
126    pub fn dismissible(mut self, dismissible: bool) -> Self {
127        self.dismissible = dismissible;
128        self
129    }
130
131    /// Show or hide the icon
132    pub fn show_icon(mut self, show: bool) -> Self {
133        self.show_icon = show;
134        self
135    }
136
137    /// Show the alert
138    pub fn show(self, ui: &mut Ui) -> AlertResponse {
139        let theme = ui.active_theme();
140        let color = self.kind.color(&theme.ui);
141
142        let frame = egui::Frame::new()
143            .fill(color.gamma_multiply(0.1))
144            .stroke(egui::Stroke::new(
145                DESIGN_TOKENS.stroke.hairline,
146                color.gamma_multiply(0.4),
147            ))
148            .corner_radius(theme.radius.md)
149            .inner_margin(egui::Margin::same(theme.spacing.lg as i8));
150
151        let mut dismissed = false;
152
153        let inner_response = frame.show(ui, |ui| {
154            // Every alert fills the width, dismissible or not.
155            ui.set_width(ui.available_width());
156            ui.horizontal(|ui| {
157                // Icon
158                if self.show_icon {
159                    let icon = self.kind.icon();
160                    let icon_size = DESIGN_TOKENS.sizing.icon_md;
161                    ui.add(icon.as_image_tinted(Vec2::splat(icon_size), color));
162                }
163
164                // Content
165                ui.vertical(|ui| {
166                    // Title, then the message, muted so the title stands out
167                    let mut message_color = theme.ui.text;
168                    if let Some(title) = &self.title {
169                        ui.label(RichText::new(l10n::text(ui.ctx(), title)).strong());
170                        ui.add_space(ui.active_theme().spacing.xs);
171                        message_color = theme.ui.text_secondary;
172                    }
173                    ui.label(
174                        RichText::new(l10n::text(ui.ctx(), &self.message)).color(message_color),
175                    );
176                });
177
178                // Dismiss button
179                if self.dismissible {
180                    ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
181                        dismissed = IconButton::new(&icons::CLOSE)
182                            .size(ButtonSize::SM)
183                            .tooltip("Dismiss")
184                            .show(ui)
185                            .clicked();
186                    });
187                }
188            });
189        });
190
191        AlertResponse {
192            response: inner_response.response,
193            dismissed,
194        }
195    }
196}