Skip to main content

armas_basic/components/
alert.rs

1//! Alert Component
2//!
3//! Inline alert messages styled like shadcn/ui Alert.
4//! Supports info (default) and destructive variants.
5//! Built on top of Card component for consistency.
6
7use crate::ext::ArmasContextExt;
8use crate::icon;
9use crate::{Card, CardVariant, Theme};
10use egui::{vec2, Color32, Sense, Ui};
11
12// shadcn Alert constants
13const CORNER_RADIUS: f32 = 8.0; // rounded-lg
14const PADDING: f32 = 16.0; // p-4
15
16/// Alert variant
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum AlertVariant {
19    /// Informational alert (default)
20    #[default]
21    Info,
22    /// Destructive/error alert (red)
23    Destructive,
24}
25
26impl AlertVariant {
27    const fn color(self, theme: &Theme) -> Color32 {
28        match self {
29            Self::Info => theme.foreground(),
30            Self::Destructive => theme.destructive(),
31        }
32    }
33
34    fn background_color(self, theme: &Theme) -> Color32 {
35        match self {
36            Self::Info => theme.muted(),
37            Self::Destructive => theme.destructive().linear_multiply(0.08),
38        }
39    }
40
41    const fn border_color(self, theme: &Theme) -> Color32 {
42        match self {
43            Self::Info => theme.border(),
44            Self::Destructive => theme.destructive(),
45        }
46    }
47}
48
49/// Alert component for inline messages
50///
51/// Built on top of the Card component with custom styling for alerts
52///
53/// # Example
54///
55/// ```rust,no_run
56/// use armas_basic::components::{Alert, AlertVariant};
57///
58/// fn ui(ui: &mut egui::Ui) {
59///     // Default info alert
60///     Alert::new("Operation completed").show(ui);
61///
62///     // Destructive alert
63///     Alert::new("Something went wrong")
64///         .variant(AlertVariant::Destructive)
65///         .show(ui);
66///
67///     // Custom color alert
68///     Alert::new("Custom alert")
69///         .color(egui::Color32::from_rgb(100, 200, 150))
70///         .show(ui);
71/// }
72/// ```
73pub struct Alert {
74    variant: AlertVariant,
75    title: Option<String>,
76    message: String,
77    dismissible: bool,
78    width: Option<f32>,
79    show_icon: bool,
80    custom_color: Option<Color32>,
81}
82
83impl Alert {
84    /// Create a new alert
85    pub fn new(message: impl Into<String>) -> Self {
86        Self {
87            variant: AlertVariant::default(),
88            title: None,
89            message: message.into(),
90            dismissible: false,
91            width: None,
92            show_icon: true,
93            custom_color: None,
94        }
95    }
96
97    /// Set the alert title
98    #[must_use]
99    pub fn title(mut self, title: impl Into<String>) -> Self {
100        self.title = Some(title.into());
101        self
102    }
103
104    /// Set the variant
105    #[must_use]
106    pub const fn variant(mut self, variant: AlertVariant) -> Self {
107        self.variant = variant;
108        self
109    }
110
111    /// Make this a destructive alert
112    #[must_use]
113    pub const fn destructive(mut self) -> Self {
114        self.variant = AlertVariant::Destructive;
115        self
116    }
117
118    /// Set custom color (overrides variant color)
119    #[must_use]
120    pub const fn color(mut self, color: Color32) -> Self {
121        self.custom_color = Some(color);
122        self
123    }
124
125    /// Make the alert dismissible
126    #[must_use]
127    pub const fn dismissible(mut self, dismissible: bool) -> Self {
128        self.dismissible = dismissible;
129        self
130    }
131
132    /// Set a fixed width
133    #[must_use]
134    pub const fn width(mut self, width: f32) -> Self {
135        self.width = Some(width);
136        self
137    }
138
139    /// Show or hide the icon
140    #[must_use]
141    pub const fn show_icon(mut self, show: bool) -> Self {
142        self.show_icon = show;
143        self
144    }
145
146    /// Show the alert using Card component
147    ///
148    /// Returns `AlertResponse` with information about user interaction
149    pub fn show(self, ui: &mut Ui) -> AlertResponse {
150        let theme = ui.ctx().armas_theme();
151        let mut dismissed = false;
152        let alert_id = ui.make_persistent_id("alert");
153
154        let accent_color = self
155            .custom_color
156            .unwrap_or_else(|| self.variant.color(&theme));
157        let bg_color = if self.custom_color.is_some() {
158            Color32::from_rgba_unmultiplied(
159                accent_color.r(),
160                accent_color.g(),
161                accent_color.b(),
162                20,
163            )
164        } else {
165            self.variant.background_color(&theme)
166        };
167        let border_color = if self.custom_color.is_some() {
168            accent_color
169        } else {
170            self.variant.border_color(&theme)
171        };
172
173        // Build the Card with alert-specific styling (shadcn style)
174        let mut card = Card::new()
175            .variant(CardVariant::Outlined)
176            .fill(bg_color)
177            .stroke(border_color)
178            .corner_radius(CORNER_RADIUS)
179            .inner_margin(PADDING);
180
181        if let Some(width) = self.width {
182            card = card.width(width);
183        }
184
185        // Show the card with alert content
186        card.show(ui, |ui| {
187            ui.horizontal(|ui| {
188                ui.spacing_mut().item_spacing.x = 12.0;
189
190                // Icon
191                if self.show_icon {
192                    let icon_size = 16.0;
193                    let (rect, _) =
194                        ui.allocate_exact_size(vec2(icon_size, icon_size), Sense::hover());
195                    match self.variant {
196                        AlertVariant::Info => icon::draw_info(ui.painter(), rect, accent_color),
197                        AlertVariant::Destructive => {
198                            icon::draw_error(ui.painter(), rect, accent_color);
199                        }
200                    }
201                }
202
203                // Content
204                ui.vertical(|ui| {
205                    ui.spacing_mut().item_spacing.y = 4.0;
206                    if let Some(title) = &self.title {
207                        ui.strong(title);
208                    }
209
210                    ui.label(&self.message);
211                });
212
213                // Spacer pushes close button to the right
214                if self.dismissible {
215                    ui.allocate_space(ui.available_size());
216
217                    // Close button
218                    let btn_size = 20.0;
219                    let (close_rect, close_response) =
220                        ui.allocate_exact_size(vec2(btn_size, btn_size), Sense::click());
221                    if ui.is_rect_visible(close_rect) {
222                        if close_response.hovered() {
223                            ui.painter().rect_filled(close_rect, 4.0, theme.accent());
224                        }
225                        let icon_color = if close_response.hovered() {
226                            theme.foreground()
227                        } else {
228                            theme.muted_foreground()
229                        };
230                        let icon_rect =
231                            egui::Rect::from_center_size(close_rect.center(), vec2(12.0, 12.0));
232                        icon::draw_close(ui.painter(), icon_rect, icon_color);
233                    }
234
235                    if close_response.clicked() {
236                        dismissed = true;
237                    }
238                }
239            });
240        });
241
242        let response = ui.interact(ui.min_rect(), alert_id.with("response"), Sense::hover());
243
244        AlertResponse {
245            response,
246            dismissed,
247        }
248    }
249}
250
251/// Response from an alert
252pub struct AlertResponse {
253    /// The UI response
254    pub response: egui::Response,
255    /// Whether the alert was dismissed
256    pub dismissed: bool,
257}
258
259/// Simple helper to show an alert with just a message
260pub fn alert(ui: &mut Ui, message: impl Into<String>) {
261    Alert::new(message).show(ui);
262}
263
264/// Show a destructive alert
265pub fn alert_destructive(ui: &mut Ui, message: impl Into<String>) {
266    Alert::new(message).destructive().show(ui);
267}