1use jag_draw::{Brush, ColorLinPremul, Rect, RoundedRadii, RoundedRect};
4use jag_surface::Canvas;
5
6use crate::event::{
7 ElementState, EventHandler, EventResult, KeyboardEvent, MouseButton, MouseClickEvent,
8 MouseMoveEvent, ScrollEvent,
9};
10use crate::focus::FocusId;
11
12use super::Element;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum AlertSeverity {
17 Info,
18 Success,
19 Warning,
20 Error,
21}
22
23impl AlertSeverity {
24 pub fn bg_color(self) -> ColorLinPremul {
26 match self {
27 Self::Info => ColorLinPremul::from_srgba_u8([219, 234, 254, 255]),
28 Self::Success => ColorLinPremul::from_srgba_u8([220, 252, 231, 255]),
29 Self::Warning => ColorLinPremul::from_srgba_u8([254, 249, 195, 255]),
30 Self::Error => ColorLinPremul::from_srgba_u8([254, 226, 226, 255]),
31 }
32 }
33
34 pub fn text_color(self) -> ColorLinPremul {
36 match self {
37 Self::Info => ColorLinPremul::from_srgba_u8([30, 64, 175, 255]),
38 Self::Success => ColorLinPremul::from_srgba_u8([22, 101, 52, 255]),
39 Self::Warning => ColorLinPremul::from_srgba_u8([133, 77, 14, 255]),
40 Self::Error => ColorLinPremul::from_srgba_u8([153, 27, 27, 255]),
41 }
42 }
43
44 pub fn border_color(self) -> ColorLinPremul {
46 match self {
47 Self::Info => ColorLinPremul::from_srgba_u8([147, 197, 253, 255]),
48 Self::Success => ColorLinPremul::from_srgba_u8([134, 239, 172, 255]),
49 Self::Warning => ColorLinPremul::from_srgba_u8([253, 224, 71, 255]),
50 Self::Error => ColorLinPremul::from_srgba_u8([252, 165, 165, 255]),
51 }
52 }
53}
54
55pub struct Alert {
58 pub rect: Rect,
59 pub message: String,
61 pub severity: AlertSeverity,
63 pub font_size: f32,
65 pub radius: f32,
67 pub dismissible: bool,
69 pub dismissed: bool,
71 pub focus_id: FocusId,
73}
74
75impl Alert {
76 pub fn new(message: impl Into<String>, severity: AlertSeverity) -> Self {
78 Self {
79 rect: Rect {
80 x: 0.0,
81 y: 0.0,
82 w: 400.0,
83 h: 48.0,
84 },
85 message: message.into(),
86 severity,
87 font_size: 14.0,
88 radius: 6.0,
89 dismissible: true,
90 dismissed: false,
91 focus_id: FocusId(0),
92 }
93 }
94
95 fn dismiss_rect(&self) -> Rect {
97 let size = 24.0;
98 Rect {
99 x: self.rect.x + self.rect.w - size - 12.0,
100 y: self.rect.y + (self.rect.h - size) * 0.5,
101 w: size,
102 h: size,
103 }
104 }
105
106 pub fn hit_test_dismiss(&self, x: f32, y: f32) -> bool {
108 if !self.dismissible {
109 return false;
110 }
111 let r = self.dismiss_rect();
112 x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h
113 }
114
115 pub fn hit_test(&self, x: f32, y: f32) -> bool {
117 x >= self.rect.x
118 && x <= self.rect.x + self.rect.w
119 && y >= self.rect.y
120 && y <= self.rect.y + self.rect.h
121 }
122}
123
124impl Element for Alert {
129 fn rect(&self) -> Rect {
130 self.rect
131 }
132
133 fn set_rect(&mut self, rect: Rect) {
134 self.rect = rect;
135 }
136
137 fn render(&self, canvas: &mut Canvas, z: i32) {
138 if self.dismissed {
139 return;
140 }
141
142 let bg = self.severity.bg_color();
143 let border = self.severity.border_color();
144 let text_color = self.severity.text_color();
145
146 let rrect = RoundedRect {
148 rect: self.rect,
149 radii: RoundedRadii {
150 tl: self.radius,
151 tr: self.radius,
152 br: self.radius,
153 bl: self.radius,
154 },
155 };
156 jag_surface::shapes::draw_snapped_rounded_rectangle(
157 canvas,
158 rrect,
159 Some(Brush::Solid(bg)),
160 Some(1.0),
161 Some(Brush::Solid(border)),
162 z,
163 );
164
165 let text_x = self.rect.x + 16.0;
167 let text_y = self.rect.y + self.rect.h * 0.5 + self.font_size * 0.35;
168 canvas.draw_text_run_weighted(
169 [text_x, text_y],
170 self.message.clone(),
171 self.font_size,
172 400.0,
173 text_color,
174 z + 1,
175 );
176
177 if self.dismissible {
179 let dr = self.dismiss_rect();
180 canvas.draw_text_run_weighted(
181 [dr.x + 5.0, dr.y + dr.h - 5.0],
182 "\u{2715}".to_string(),
183 14.0,
184 400.0,
185 text_color,
186 z + 2,
187 );
188 }
189 }
190
191 fn focus_id(&self) -> Option<FocusId> {
192 if self.dismissible {
193 Some(self.focus_id)
194 } else {
195 None
196 }
197 }
198}
199
200impl EventHandler for Alert {
205 fn handle_mouse_click(&mut self, event: &MouseClickEvent) -> EventResult {
206 if self.dismissed {
207 return EventResult::Ignored;
208 }
209 if event.button != MouseButton::Left || event.state != ElementState::Pressed {
210 return EventResult::Ignored;
211 }
212 if self.hit_test_dismiss(event.x, event.y) {
213 self.dismissed = true;
214 EventResult::Handled
215 } else {
216 EventResult::Ignored
217 }
218 }
219
220 fn handle_keyboard(&mut self, _event: &KeyboardEvent) -> EventResult {
221 EventResult::Ignored
222 }
223
224 fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
225 EventResult::Ignored
226 }
227
228 fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
229 EventResult::Ignored
230 }
231
232 fn is_focused(&self) -> bool {
233 false
234 }
235
236 fn set_focused(&mut self, _focused: bool) {}
237
238 fn contains_point(&self, x: f32, y: f32) -> bool {
239 if self.dismissed {
240 return false;
241 }
242 self.hit_test(x, y)
243 }
244}
245
246#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn alert_severity_colors_differ() {
256 let info_bg = AlertSeverity::Info.bg_color();
257 let error_bg = AlertSeverity::Error.bg_color();
258 assert_ne!(info_bg, error_bg);
260 }
261
262 #[test]
263 fn alert_defaults() {
264 let a = Alert::new("Something happened", AlertSeverity::Info);
265 assert_eq!(a.message, "Something happened");
266 assert_eq!(a.severity, AlertSeverity::Info);
267 assert!(a.dismissible);
268 assert!(!a.dismissed);
269 }
270
271 #[test]
272 fn alert_dismiss_click() {
273 let mut a = Alert::new("Msg", AlertSeverity::Warning);
274 a.rect = Rect {
275 x: 0.0,
276 y: 0.0,
277 w: 400.0,
278 h: 48.0,
279 };
280 let dr = a.dismiss_rect();
281 let evt = MouseClickEvent {
282 button: MouseButton::Left,
283 state: ElementState::Pressed,
284 x: dr.x + 5.0,
285 y: dr.y + 5.0,
286 click_count: 1,
287 };
288 assert_eq!(a.handle_mouse_click(&evt), EventResult::Handled);
289 assert!(a.dismissed);
290 }
291
292 #[test]
293 fn alert_not_hittable_when_dismissed() {
294 let mut a = Alert::new("Msg", AlertSeverity::Error);
295 a.rect = Rect {
296 x: 0.0,
297 y: 0.0,
298 w: 400.0,
299 h: 48.0,
300 };
301 a.dismissed = true;
302 assert!(!a.contains_point(200.0, 24.0));
303 }
304
305 #[test]
306 fn alert_hit_test() {
307 let a = Alert::new("Msg", AlertSeverity::Success);
308 assert!(a.hit_test(200.0, 24.0));
309 assert!(!a.hit_test(500.0, 24.0));
310 }
311}