armas_basic/components/
alert.rs1use crate::ext::ArmasContextExt;
8use crate::icon;
9use crate::{Card, CardVariant, Theme};
10use egui::{vec2, Color32, Sense, Ui};
11
12const CORNER_RADIUS: f32 = 8.0; const PADDING: f32 = 16.0; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum AlertVariant {
19 #[default]
21 Info,
22 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
49pub 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 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 #[must_use]
99 pub fn title(mut self, title: impl Into<String>) -> Self {
100 self.title = Some(title.into());
101 self
102 }
103
104 #[must_use]
106 pub const fn variant(mut self, variant: AlertVariant) -> Self {
107 self.variant = variant;
108 self
109 }
110
111 #[must_use]
113 pub const fn destructive(mut self) -> Self {
114 self.variant = AlertVariant::Destructive;
115 self
116 }
117
118 #[must_use]
120 pub const fn color(mut self, color: Color32) -> Self {
121 self.custom_color = Some(color);
122 self
123 }
124
125 #[must_use]
127 pub const fn dismissible(mut self, dismissible: bool) -> Self {
128 self.dismissible = dismissible;
129 self
130 }
131
132 #[must_use]
134 pub const fn width(mut self, width: f32) -> Self {
135 self.width = Some(width);
136 self
137 }
138
139 #[must_use]
141 pub const fn show_icon(mut self, show: bool) -> Self {
142 self.show_icon = show;
143 self
144 }
145
146 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 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 card.show(ui, |ui| {
187 ui.horizontal(|ui| {
188 ui.spacing_mut().item_spacing.x = 12.0;
189
190 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 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 if self.dismissible {
215 ui.allocate_space(ui.available_size());
216
217 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
251pub struct AlertResponse {
253 pub response: egui::Response,
255 pub dismissed: bool,
257}
258
259pub fn alert(ui: &mut Ui, message: impl Into<String>) {
261 Alert::new(message).show(ui);
262}
263
264pub fn alert_destructive(ui: &mut Ui, message: impl Into<String>) {
266 Alert::new(message).destructive().show(ui);
267}