1use presentar_core::{
7 widget::{LayoutResult, TextStyle},
8 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event, Key,
9 Point, Rect, Size, TypeId, Widget,
10};
11use serde::{Deserialize, Serialize};
12use std::any::Any;
13use std::time::Duration;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
17pub enum ModalSize {
18 Small,
20 #[default]
22 Medium,
23 Large,
25 FullWidth,
27 Custom(u32),
29}
30
31impl ModalSize {
32 #[must_use]
34 pub const fn max_width(&self) -> f32 {
35 match self {
36 Self::Small => 300.0,
37 Self::Medium => 500.0,
38 Self::Large => 800.0,
39 Self::FullWidth => f32::MAX,
40 Self::Custom(w) => *w as f32,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
47pub enum BackdropBehavior {
48 #[default]
50 CloseOnClick,
51 Static,
53 None,
55}
56
57#[derive(Serialize, Deserialize)]
59pub struct Modal {
60 pub open: bool,
62 pub size: ModalSize,
64 pub backdrop: BackdropBehavior,
66 pub close_on_escape: bool,
68 pub title: Option<String>,
70 pub show_close_button: bool,
72 pub backdrop_color: Color,
74 pub background_color: Color,
76 pub border_radius: f32,
78 pub padding: f32,
80 test_id_value: Option<String>,
82 #[serde(skip)]
84 bounds: Rect,
85 #[serde(skip)]
87 content_bounds: Rect,
88 #[serde(skip)]
90 content: Option<Box<dyn Widget>>,
91 #[serde(skip)]
93 footer: Option<Box<dyn Widget>>,
94 #[serde(skip)]
96 animation_progress: f32,
97}
98
99impl Default for Modal {
100 fn default() -> Self {
101 Self {
102 open: false,
103 size: ModalSize::Medium,
104 backdrop: BackdropBehavior::CloseOnClick,
105 close_on_escape: true,
106 title: None,
107 show_close_button: true,
108 backdrop_color: Color::rgba(0.0, 0.0, 0.0, 0.5),
109 background_color: Color::WHITE,
110 border_radius: 8.0,
111 padding: 24.0,
112 test_id_value: None,
113 bounds: Rect::default(),
114 content_bounds: Rect::default(),
115 content: None,
116 footer: None,
117 animation_progress: 0.0,
118 }
119 }
120}
121
122impl Modal {
123 #[must_use]
125 pub fn new() -> Self {
126 Self::default()
127 }
128
129 #[must_use]
131 pub const fn open(mut self, open: bool) -> Self {
132 self.open = open;
133 self
134 }
135
136 #[must_use]
138 pub const fn size(mut self, size: ModalSize) -> Self {
139 self.size = size;
140 self
141 }
142
143 #[must_use]
145 pub const fn backdrop(mut self, behavior: BackdropBehavior) -> Self {
146 self.backdrop = behavior;
147 self
148 }
149
150 #[must_use]
152 pub const fn close_on_escape(mut self, enabled: bool) -> Self {
153 self.close_on_escape = enabled;
154 self
155 }
156
157 #[must_use]
159 pub fn title(mut self, title: impl Into<String>) -> Self {
160 self.title = Some(title.into());
161 self
162 }
163
164 #[must_use]
166 pub const fn show_close_button(mut self, show: bool) -> Self {
167 self.show_close_button = show;
168 self
169 }
170
171 #[must_use]
173 pub const fn backdrop_color(mut self, color: Color) -> Self {
174 self.backdrop_color = color;
175 self
176 }
177
178 #[must_use]
180 pub const fn background_color(mut self, color: Color) -> Self {
181 self.background_color = color;
182 self
183 }
184
185 #[must_use]
187 pub const fn border_radius(mut self, radius: f32) -> Self {
188 self.border_radius = radius;
189 self
190 }
191
192 #[must_use]
194 pub const fn padding(mut self, padding: f32) -> Self {
195 self.padding = padding;
196 self
197 }
198
199 pub fn content(mut self, widget: impl Widget + 'static) -> Self {
201 self.content = Some(Box::new(widget));
202 self
203 }
204
205 pub fn footer(mut self, widget: impl Widget + 'static) -> Self {
207 self.footer = Some(Box::new(widget));
208 self
209 }
210
211 #[must_use]
213 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
214 self.test_id_value = Some(id.into());
215 self
216 }
217
218 pub fn show(&mut self) {
220 self.open = true;
221 }
222
223 pub fn hide(&mut self) {
225 self.open = false;
226 }
227
228 pub fn toggle(&mut self) {
230 self.open = !self.open;
231 }
232
233 #[must_use]
235 pub const fn is_open(&self) -> bool {
236 self.open
237 }
238
239 #[must_use]
241 pub const fn animation_progress(&self) -> f32 {
242 self.animation_progress
243 }
244
245 #[must_use]
247 pub const fn content_bounds(&self) -> Rect {
248 self.content_bounds
249 }
250
251 fn calculate_modal_bounds(&self, viewport: Rect) -> Rect {
253 let max_width = self.size.max_width();
254 let modal_width = max_width.min(viewport.width - 32.0); let header_height = if self.title.is_some() { 56.0 } else { 0.0 };
258 let footer_height = if self.footer.is_some() { 64.0 } else { 0.0 };
259 let content_height = 200.0; let total_height = self
261 .padding
262 .mul_add(2.0, header_height + content_height + footer_height);
263 let modal_height = total_height.min(viewport.height - 64.0); let x = viewport.x + (viewport.width - modal_width) / 2.0;
266 let y = viewport.y + (viewport.height - modal_height) / 2.0;
267
268 Rect::new(x, y, modal_width, modal_height)
269 }
270}
271
272impl Widget for Modal {
273 fn type_id(&self) -> TypeId {
274 TypeId::of::<Self>()
275 }
276
277 fn measure(&self, constraints: Constraints) -> Size {
278 constraints.constrain(Size::new(constraints.max_width, constraints.max_height))
280 }
281
282 fn layout(&mut self, bounds: Rect) -> LayoutResult {
283 self.bounds = bounds;
284
285 if self.open {
286 self.content_bounds = self.calculate_modal_bounds(bounds);
287
288 if let Some(ref mut content) = self.content {
290 let header_height = if self.title.is_some() { 56.0 } else { 0.0 };
291 let footer_height = if self.footer.is_some() { 64.0 } else { 0.0 };
292
293 let content_rect = Rect::new(
294 self.content_bounds.x + self.padding,
295 self.content_bounds.y + header_height + self.padding,
296 self.padding.mul_add(-2.0, self.content_bounds.width),
297 self.padding.mul_add(
298 -2.0,
299 self.content_bounds.height - header_height - footer_height,
300 ),
301 );
302 content.layout(content_rect);
303 }
304
305 if let Some(ref mut footer) = self.footer {
307 let footer_rect = Rect::new(
308 self.content_bounds.x + self.padding,
309 self.content_bounds.y + self.content_bounds.height - 64.0 - self.padding,
310 self.padding.mul_add(-2.0, self.content_bounds.width),
311 64.0,
312 );
313 footer.layout(footer_rect);
314 }
315
316 self.animation_progress = (self.animation_progress + 0.15).min(1.0);
318 } else {
319 self.animation_progress = (self.animation_progress - 0.15).max(0.0);
321 }
322
323 LayoutResult {
324 size: bounds.size(),
325 }
326 }
327
328 fn paint(&self, canvas: &mut dyn Canvas) {
329 if self.animation_progress <= 0.0 {
330 return;
331 }
332
333 let opacity = self.animation_progress;
334
335 if self.backdrop != BackdropBehavior::None {
337 let backdrop_color = Color::rgba(
338 self.backdrop_color.r,
339 self.backdrop_color.g,
340 self.backdrop_color.b,
341 self.backdrop_color.a * opacity,
342 );
343 canvas.fill_rect(self.bounds, backdrop_color);
344 }
345
346 let y_offset = (1.0 - opacity) * 20.0;
348 let animated_bounds = Rect::new(
349 self.content_bounds.x,
350 self.content_bounds.y + y_offset,
351 self.content_bounds.width,
352 self.content_bounds.height,
353 );
354
355 let shadow_color = Color::rgba(0.0, 0.0, 0.0, 0.1 * opacity);
357 let shadow_bounds = Rect::new(
358 animated_bounds.x + 4.0,
359 animated_bounds.y + 4.0,
360 animated_bounds.width,
361 animated_bounds.height,
362 );
363 canvas.fill_rect(shadow_bounds, shadow_color);
364
365 canvas.fill_rect(animated_bounds, self.background_color);
367
368 if let Some(ref title) = self.title {
370 let title_pos = Point::new(
371 animated_bounds.x + self.padding,
372 animated_bounds.y + self.padding + 16.0, );
374 let title_style = TextStyle {
375 size: 18.0,
376 color: Color::BLACK,
377 ..Default::default()
378 };
379 canvas.draw_text(title, title_pos, &title_style);
380 }
381
382 if self.show_close_button {
384 let close_x = animated_bounds.x + animated_bounds.width - 40.0 - self.padding;
385 let close_y = animated_bounds.y + self.padding + 16.0;
386 let close_style = TextStyle {
387 size: 24.0,
388 color: Color::rgb(0.5, 0.5, 0.5),
389 ..Default::default()
390 };
391 canvas.draw_text("×", Point::new(close_x, close_y), &close_style);
392 }
393
394 if let Some(ref content) = self.content {
396 content.paint(canvas);
397 }
398
399 if let Some(ref footer) = self.footer {
401 footer.paint(canvas);
402 }
403 }
404
405 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
406 if !self.open {
407 return None;
408 }
409
410 match event {
411 Event::KeyDown {
412 key: Key::Escape, ..
413 } if self.close_on_escape => {
414 self.hide();
415 return Some(Box::new(ModalClosed {
416 reason: CloseReason::Escape,
417 }));
418 }
419 Event::MouseDown { position, .. } => {
420 if self.backdrop == BackdropBehavior::CloseOnClick {
422 let in_modal = position.x >= self.content_bounds.x
423 && position.x <= self.content_bounds.x + self.content_bounds.width
424 && position.y >= self.content_bounds.y
425 && position.y <= self.content_bounds.y + self.content_bounds.height;
426
427 if !in_modal {
428 self.hide();
429 return Some(Box::new(ModalClosed {
430 reason: CloseReason::Backdrop,
431 }));
432 }
433 }
434
435 if self.show_close_button {
437 let close_x =
438 self.content_bounds.x + self.content_bounds.width - 40.0 - self.padding;
439 let close_y = self.content_bounds.y + self.padding;
440 let on_close_btn = position.x >= close_x
441 && position.x <= close_x + 24.0
442 && position.y >= close_y
443 && position.y <= close_y + 24.0;
444
445 if on_close_btn {
446 self.hide();
447 return Some(Box::new(ModalClosed {
448 reason: CloseReason::CloseButton,
449 }));
450 }
451 }
452
453 if let Some(ref mut content) = self.content {
455 if let Some(msg) = content.event(event) {
456 return Some(msg);
457 }
458 }
459
460 if let Some(ref mut footer) = self.footer {
462 if let Some(msg) = footer.event(event) {
463 return Some(msg);
464 }
465 }
466 }
467 _ => {
468 if let Some(ref mut content) = self.content {
470 if let Some(msg) = content.event(event) {
471 return Some(msg);
472 }
473 }
474
475 if let Some(ref mut footer) = self.footer {
476 if let Some(msg) = footer.event(event) {
477 return Some(msg);
478 }
479 }
480 }
481 }
482
483 None
484 }
485
486 fn children(&self) -> &[Box<dyn Widget>] {
487 &[]
488 }
489
490 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
491 &mut []
492 }
493
494 fn is_focusable(&self) -> bool {
495 self.open
496 }
497
498 fn test_id(&self) -> Option<&str> {
499 self.test_id_value.as_deref()
500 }
501
502 fn bounds(&self) -> Rect {
503 self.bounds
504 }
505}
506
507impl Brick for Modal {
509 fn brick_name(&self) -> &'static str {
510 "Modal"
511 }
512
513 fn assertions(&self) -> &[BrickAssertion] {
514 &[BrickAssertion::MaxLatencyMs(16)]
515 }
516
517 fn budget(&self) -> BrickBudget {
518 BrickBudget::uniform(16)
519 }
520
521 fn verify(&self) -> BrickVerification {
522 BrickVerification {
523 passed: self.assertions().to_vec(),
524 failed: vec![],
525 verification_time: Duration::from_micros(10),
526 }
527 }
528
529 fn to_html(&self) -> String {
530 r#"<div class="brick-modal"></div>"#.to_string()
531 }
532
533 fn to_css(&self) -> String {
534 ".brick-modal { display: block; position: fixed; }".to_string()
535 }
536
537 fn test_id(&self) -> Option<&str> {
538 self.test_id_value.as_deref()
539 }
540}
541
542#[derive(Debug, Clone, Copy, PartialEq, Eq)]
544pub enum CloseReason {
545 Escape,
547 Backdrop,
549 CloseButton,
551 Programmatic,
553}
554
555#[derive(Debug, Clone)]
557pub struct ModalClosed {
558 pub reason: CloseReason,
560}
561
562#[derive(Debug, Clone)]
564pub struct ModalOpened;
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569
570 #[test]
575 fn test_modal_size_default() {
576 assert_eq!(ModalSize::default(), ModalSize::Medium);
577 }
578
579 #[test]
580 fn test_modal_size_max_width() {
581 assert_eq!(ModalSize::Small.max_width(), 300.0);
582 assert_eq!(ModalSize::Medium.max_width(), 500.0);
583 assert_eq!(ModalSize::Large.max_width(), 800.0);
584 assert_eq!(ModalSize::FullWidth.max_width(), f32::MAX);
585 assert_eq!(ModalSize::Custom(600).max_width(), 600.0);
586 }
587
588 #[test]
593 fn test_backdrop_behavior_default() {
594 assert_eq!(BackdropBehavior::default(), BackdropBehavior::CloseOnClick);
595 }
596
597 #[test]
602 fn test_modal_new() {
603 let modal = Modal::new();
604 assert!(!modal.open);
605 assert_eq!(modal.size, ModalSize::Medium);
606 assert_eq!(modal.backdrop, BackdropBehavior::CloseOnClick);
607 assert!(modal.close_on_escape);
608 assert!(modal.title.is_none());
609 assert!(modal.show_close_button);
610 }
611
612 #[test]
613 fn test_modal_builder() {
614 let modal = Modal::new()
615 .open(true)
616 .size(ModalSize::Large)
617 .backdrop(BackdropBehavior::Static)
618 .close_on_escape(false)
619 .title("Test Modal")
620 .show_close_button(false)
621 .border_radius(16.0)
622 .padding(32.0);
623
624 assert!(modal.open);
625 assert_eq!(modal.size, ModalSize::Large);
626 assert_eq!(modal.backdrop, BackdropBehavior::Static);
627 assert!(!modal.close_on_escape);
628 assert_eq!(modal.title, Some("Test Modal".to_string()));
629 assert!(!modal.show_close_button);
630 assert_eq!(modal.border_radius, 16.0);
631 assert_eq!(modal.padding, 32.0);
632 }
633
634 #[test]
635 fn test_modal_show_hide() {
636 let mut modal = Modal::new();
637 assert!(!modal.is_open());
638
639 modal.show();
640 assert!(modal.is_open());
641
642 modal.hide();
643 assert!(!modal.is_open());
644 }
645
646 #[test]
647 fn test_modal_toggle() {
648 let mut modal = Modal::new();
649 assert!(!modal.is_open());
650
651 modal.toggle();
652 assert!(modal.is_open());
653
654 modal.toggle();
655 assert!(!modal.is_open());
656 }
657
658 #[test]
659 fn test_modal_measure() {
660 let modal = Modal::new();
661 let size = modal.measure(Constraints::loose(Size::new(1024.0, 768.0)));
662 assert_eq!(size, Size::new(1024.0, 768.0));
663 }
664
665 #[test]
666 fn test_modal_layout_closed() {
667 let mut modal = Modal::new();
668 let result = modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
669 assert_eq!(result.size, Size::new(1024.0, 768.0));
670 assert_eq!(modal.animation_progress, 0.0);
671 }
672
673 #[test]
674 fn test_modal_layout_open() {
675 let mut modal = Modal::new().open(true);
676 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
677 assert!(modal.animation_progress > 0.0);
678 }
679
680 #[test]
681 fn test_modal_calculate_bounds() {
682 let modal = Modal::new().size(ModalSize::Medium);
683 let viewport = Rect::new(0.0, 0.0, 1024.0, 768.0);
684 let bounds = modal.calculate_modal_bounds(viewport);
685
686 assert!(bounds.x > 0.0);
688 assert!(bounds.y > 0.0);
689 assert!(bounds.width <= 500.0);
690 }
691
692 #[test]
693 fn test_modal_type_id() {
694 let modal = Modal::new();
695 assert_eq!(Widget::type_id(&modal), TypeId::of::<Modal>());
696 }
697
698 #[test]
699 fn test_modal_is_focusable() {
700 let modal = Modal::new();
701 assert!(!modal.is_focusable()); let modal_open = Modal::new().open(true);
704 assert!(modal_open.is_focusable()); }
706
707 #[test]
708 fn test_modal_test_id() {
709 let modal = Modal::new().with_test_id("my-modal");
710 assert_eq!(Widget::test_id(&modal), Some("my-modal"));
711 }
712
713 #[test]
714 fn test_modal_children_empty() {
715 let modal = Modal::new();
716 assert!(modal.children().is_empty());
717 }
718
719 #[test]
720 fn test_modal_bounds() {
721 let mut modal = Modal::new();
722 modal.layout(Rect::new(10.0, 20.0, 1024.0, 768.0));
723 assert_eq!(modal.bounds(), Rect::new(10.0, 20.0, 1024.0, 768.0));
724 }
725
726 #[test]
727 fn test_modal_backdrop_color() {
728 let modal = Modal::new().backdrop_color(Color::rgba(0.0, 0.0, 0.0, 0.7));
729 assert_eq!(modal.backdrop_color.a, 0.7);
730 }
731
732 #[test]
733 fn test_modal_background_color() {
734 let modal = Modal::new().background_color(Color::rgb(0.9, 0.9, 0.9));
735 assert_eq!(modal.background_color.r, 0.9);
736 }
737
738 #[test]
739 fn test_modal_escape_closes() {
740 let mut modal = Modal::new().open(true);
741 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
742
743 let result = modal.event(&Event::key_down(Key::Escape));
744 assert!(result.is_some());
745 assert!(!modal.is_open());
746 }
747
748 #[test]
749 fn test_modal_escape_disabled() {
750 let mut modal = Modal::new().open(true).close_on_escape(false);
751 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
752
753 let result = modal.event(&Event::key_down(Key::Escape));
754 assert!(result.is_none());
755 assert!(modal.is_open());
756 }
757
758 #[test]
759 fn test_modal_animation_progress() {
760 let modal = Modal::new();
761 assert_eq!(modal.animation_progress(), 0.0);
762 }
763
764 #[test]
765 fn test_modal_content_bounds() {
766 let mut modal = Modal::new().open(true);
767 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
768 let content_bounds = modal.content_bounds();
769 assert!(content_bounds.width > 0.0);
770 assert!(content_bounds.height > 0.0);
771 }
772
773 #[test]
778 fn test_close_reason_eq() {
779 assert_eq!(CloseReason::Escape, CloseReason::Escape);
780 assert_ne!(CloseReason::Escape, CloseReason::Backdrop);
781 }
782
783 #[test]
788 fn test_modal_closed_message() {
789 let msg = ModalClosed {
790 reason: CloseReason::CloseButton,
791 };
792 assert_eq!(msg.reason, CloseReason::CloseButton);
793 }
794
795 #[test]
796 fn test_modal_opened_message() {
797 let msg = ModalOpened;
798 assert_eq!(format!("{msg:?}"), "ModalOpened");
799 }
800
801 #[test]
806 fn test_modal_backdrop_none() {
807 let modal = Modal::new().backdrop(BackdropBehavior::None);
808 assert_eq!(modal.backdrop, BackdropBehavior::None);
809 }
810
811 #[test]
812 fn test_modal_backdrop_static() {
813 let modal = Modal::new().backdrop(BackdropBehavior::Static);
814 assert_eq!(modal.backdrop, BackdropBehavior::Static);
815 }
816
817 #[test]
818 fn test_modal_size_small() {
819 assert_eq!(ModalSize::Small.max_width(), 300.0);
820 }
821
822 #[test]
823 fn test_modal_size_full_width() {
824 assert_eq!(ModalSize::FullWidth.max_width(), f32::MAX);
825 }
826
827 #[test]
828 fn test_modal_children_mut_empty() {
829 let mut modal = Modal::new();
830 assert!(modal.children_mut().is_empty());
831 }
832
833 #[test]
834 fn test_modal_calculate_bounds_with_title() {
835 let modal = Modal::new().title("Test Title");
836 let viewport = Rect::new(0.0, 0.0, 1024.0, 768.0);
837 let bounds = modal.calculate_modal_bounds(viewport);
838 assert!(bounds.height > 0.0);
839 }
840
841 #[test]
842 fn test_modal_layout_animation_closes() {
843 let mut modal = Modal::new().open(true);
844 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
845 let prog1 = modal.animation_progress;
847 modal.open = false;
848 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
849 assert!(modal.animation_progress < prog1);
851 }
852
853 #[test]
854 fn test_modal_event_not_open_returns_none() {
855 let mut modal = Modal::new();
856 let result = modal.event(&Event::key_down(Key::Escape));
857 assert!(result.is_none());
858 }
859
860 #[test]
861 fn test_modal_other_key_does_nothing() {
862 let mut modal = Modal::new().open(true);
863 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
864 let result = modal.event(&Event::key_down(Key::Tab));
865 assert!(result.is_none());
866 assert!(modal.is_open());
867 }
868
869 #[test]
870 fn test_close_reason_programmatic() {
871 let reason = CloseReason::Programmatic;
872 assert_eq!(reason, CloseReason::Programmatic);
873 }
874
875 #[test]
876 fn test_close_reason_close_button() {
877 let reason = CloseReason::CloseButton;
878 assert_eq!(reason, CloseReason::CloseButton);
879 }
880
881 #[test]
882 fn test_modal_size_custom_value() {
883 let size = ModalSize::Custom(750);
884 assert_eq!(size.max_width(), 750.0);
885 }
886
887 #[test]
888 fn test_modal_backdrop_eq() {
889 assert_eq!(
890 BackdropBehavior::CloseOnClick,
891 BackdropBehavior::CloseOnClick
892 );
893 assert_ne!(BackdropBehavior::CloseOnClick, BackdropBehavior::Static);
894 }
895
896 #[test]
897 fn test_modal_size_eq() {
898 assert_eq!(ModalSize::Medium, ModalSize::Medium);
899 assert_ne!(ModalSize::Small, ModalSize::Large);
900 }
901
902 #[test]
907 fn test_modal_brick_name() {
908 let modal = Modal::new();
909 assert_eq!(modal.brick_name(), "Modal");
910 }
911
912 #[test]
913 fn test_modal_brick_assertions() {
914 let modal = Modal::new();
915 let assertions = modal.assertions();
916 assert!(!assertions.is_empty());
917 assert!(matches!(assertions[0], BrickAssertion::MaxLatencyMs(16)));
918 }
919
920 #[test]
921 fn test_modal_brick_budget() {
922 let modal = Modal::new();
923 let budget = modal.budget();
924 assert!(budget.layout_ms > 0);
926 assert!(budget.paint_ms > 0);
927 }
928
929 #[test]
930 fn test_modal_brick_verify() {
931 let modal = Modal::new();
932 let verification = modal.verify();
933 assert!(!verification.passed.is_empty());
934 assert!(verification.failed.is_empty());
935 }
936
937 #[test]
938 fn test_modal_brick_to_html() {
939 let modal = Modal::new();
940 let html = modal.to_html();
941 assert!(html.contains("brick-modal"));
942 }
943
944 #[test]
945 fn test_modal_brick_to_css() {
946 let modal = Modal::new();
947 let css = modal.to_css();
948 assert!(css.contains(".brick-modal"));
949 assert!(css.contains("display: block"));
950 assert!(css.contains("position: fixed"));
951 }
952
953 #[test]
954 fn test_modal_brick_test_id() {
955 let modal = Modal::new().with_test_id("my-modal");
956 assert_eq!(Brick::test_id(&modal), Some("my-modal"));
957 }
958
959 #[test]
960 fn test_modal_brick_test_id_none() {
961 let modal = Modal::new();
962 assert!(Brick::test_id(&modal).is_none());
963 }
964
965 #[test]
970 fn test_modal_backdrop_click_closes() {
971 let mut modal = Modal::new().open(true);
972 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
973
974 let result = modal.event(&Event::MouseDown {
976 position: Point::new(10.0, 10.0),
977 button: presentar_core::MouseButton::Left,
978 });
979
980 assert!(result.is_some());
981 assert!(!modal.is_open());
982 }
983
984 #[test]
985 fn test_modal_backdrop_static_no_close() {
986 let mut modal = Modal::new().open(true).backdrop(BackdropBehavior::Static);
987 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
988
989 let result = modal.event(&Event::MouseDown {
991 position: Point::new(10.0, 10.0),
992 button: presentar_core::MouseButton::Left,
993 });
994
995 assert!(result.is_none());
996 assert!(modal.is_open());
997 }
998
999 #[test]
1000 fn test_modal_click_inside_does_not_close() {
1001 let mut modal = Modal::new().open(true);
1002 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1003
1004 let center_x = modal.content_bounds.x + modal.content_bounds.width / 2.0;
1006 let center_y = modal.content_bounds.y + modal.content_bounds.height / 2.0;
1007
1008 let result = modal.event(&Event::MouseDown {
1009 position: Point::new(center_x, center_y),
1010 button: presentar_core::MouseButton::Left,
1011 });
1012
1013 assert!(result.is_none());
1015 assert!(modal.is_open());
1016 }
1017
1018 #[test]
1023 fn test_modal_close_button_click() {
1024 let mut modal = Modal::new().open(true);
1025 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1026
1027 let close_x = modal.content_bounds.x + modal.content_bounds.width - 40.0 - modal.padding;
1029 let close_y = modal.content_bounds.y + modal.padding;
1030
1031 let result = modal.event(&Event::MouseDown {
1032 position: Point::new(close_x + 10.0, close_y + 10.0),
1033 button: presentar_core::MouseButton::Left,
1034 });
1035
1036 assert!(result.is_some());
1037 assert!(!modal.is_open());
1038 }
1039
1040 #[test]
1041 fn test_modal_close_button_hidden() {
1042 let mut modal = Modal::new().open(true).show_close_button(false);
1043 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1044
1045 let close_x = modal.content_bounds.x + modal.content_bounds.width - 40.0 - modal.padding;
1047 let close_y = modal.content_bounds.y + modal.padding;
1048
1049 let result = modal.event(&Event::MouseDown {
1050 position: Point::new(close_x + 10.0, close_y + 10.0),
1051 button: presentar_core::MouseButton::Left,
1052 });
1053
1054 assert!(result.is_none());
1056 assert!(modal.is_open());
1057 }
1058
1059 #[test]
1064 fn test_modal_animation_opens() {
1065 let mut modal = Modal::new().open(true);
1066 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1067 assert!(modal.animation_progress > 0.0);
1068
1069 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1071 assert!(modal.animation_progress >= 0.15);
1072 }
1073
1074 #[test]
1075 fn test_modal_animation_caps_at_one() {
1076 let mut modal = Modal::new().open(true);
1077 for _ in 0..20 {
1079 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1080 }
1081 assert!((modal.animation_progress - 1.0).abs() < 0.01);
1082 }
1083
1084 #[test]
1085 fn test_modal_animation_closes_to_zero() {
1086 let mut modal = Modal::new().open(true);
1087 for _ in 0..20 {
1089 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1090 }
1091
1092 modal.open = false;
1093 for _ in 0..20 {
1095 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1096 }
1097 assert!(modal.animation_progress < 0.01);
1098 }
1099
1100 #[test]
1105 fn test_modal_calculate_bounds_centered() {
1106 let modal = Modal::new().size(ModalSize::Medium);
1107 let viewport = Rect::new(0.0, 0.0, 1024.0, 768.0);
1108 let bounds = modal.calculate_modal_bounds(viewport);
1109
1110 let expected_x = (1024.0 - bounds.width) / 2.0;
1112 assert!((bounds.x - expected_x).abs() < 1.0);
1113 }
1114
1115 #[test]
1116 fn test_modal_calculate_bounds_small_viewport() {
1117 let modal = Modal::new().size(ModalSize::Large); let viewport = Rect::new(0.0, 0.0, 400.0, 300.0); let bounds = modal.calculate_modal_bounds(viewport);
1120
1121 assert!(bounds.width <= 400.0 - 32.0);
1123 }
1124
1125 #[test]
1126 fn test_modal_calculate_bounds_with_footer() {
1127 let modal = Modal::new().title("Test");
1128 let viewport = Rect::new(0.0, 0.0, 1024.0, 768.0);
1129 let bounds = modal.calculate_modal_bounds(viewport);
1130
1131 assert!(bounds.height > 0.0);
1133 }
1134
1135 #[test]
1140 fn test_modal_size_large() {
1141 assert_eq!(ModalSize::Large.max_width(), 800.0);
1142 }
1143
1144 #[test]
1145 fn test_modal_size_custom_zero() {
1146 assert_eq!(ModalSize::Custom(0).max_width(), 0.0);
1148 }
1149
1150 #[test]
1155 fn test_close_reason_copy() {
1156 let reason = CloseReason::Escape;
1157 let copied: CloseReason = reason;
1158 assert_eq!(copied, CloseReason::Escape);
1159 }
1160
1161 #[test]
1162 fn test_close_reason_all_variants() {
1163 let reasons = [
1164 CloseReason::Escape,
1165 CloseReason::Backdrop,
1166 CloseReason::CloseButton,
1167 CloseReason::Programmatic,
1168 ];
1169 assert_eq!(reasons.len(), 4);
1170 }
1171
1172 #[test]
1177 fn test_modal_closed_clone() {
1178 let msg = ModalClosed {
1179 reason: CloseReason::Escape,
1180 };
1181 let cloned = msg;
1182 assert_eq!(cloned.reason, CloseReason::Escape);
1183 }
1184
1185 #[test]
1186 fn test_modal_opened_clone() {
1187 let msg = ModalOpened;
1188 let cloned = msg;
1189 assert_eq!(format!("{cloned:?}"), "ModalOpened");
1190 }
1191
1192 #[test]
1193 fn test_modal_closed_debug() {
1194 let msg = ModalClosed {
1195 reason: CloseReason::Backdrop,
1196 };
1197 let debug_str = format!("{msg:?}");
1198 assert!(debug_str.contains("Backdrop"));
1199 }
1200
1201 #[test]
1202 fn test_modal_opened_debug() {
1203 let msg = ModalOpened;
1204 let debug_str = format!("{msg:?}");
1205 assert!(debug_str.contains("ModalOpened"));
1206 }
1207
1208 #[test]
1213 fn test_modal_default_values() {
1214 let modal = Modal::default();
1215 assert!(!modal.open);
1216 assert_eq!(modal.size, ModalSize::Medium);
1217 assert_eq!(modal.backdrop, BackdropBehavior::CloseOnClick);
1218 assert!(modal.close_on_escape);
1219 assert!(modal.title.is_none());
1220 assert!(modal.show_close_button);
1221 assert_eq!(modal.border_radius, 8.0);
1222 assert_eq!(modal.padding, 24.0);
1223 }
1224
1225 #[test]
1230 fn test_modal_measure_constraints() {
1231 let modal = Modal::new();
1232 let size = modal.measure(Constraints::tight(Size::new(800.0, 600.0)));
1233 assert_eq!(size.width, 800.0);
1234 assert_eq!(size.height, 600.0);
1235 }
1236
1237 #[test]
1238 fn test_modal_children_mut() {
1239 let mut modal = Modal::new();
1240 assert!(modal.children_mut().is_empty());
1241 }
1242
1243 #[test]
1244 fn test_modal_mouse_move_does_nothing() {
1245 let mut modal = Modal::new().open(true);
1246 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1247
1248 let result = modal.event(&Event::MouseMove {
1249 position: Point::new(100.0, 100.0),
1250 });
1251 assert!(result.is_none());
1252 }
1253
1254 #[test]
1255 fn test_modal_title_setter() {
1256 let modal = Modal::new().title("Test Modal");
1257 let _ = modal;
1259 }
1260
1261 #[test]
1262 fn test_backdrop_behavior_copy() {
1263 let behavior = BackdropBehavior::Static;
1264 let copied: BackdropBehavior = behavior;
1265 assert_eq!(copied, BackdropBehavior::Static);
1266 }
1267
1268 #[test]
1269 fn test_modal_size_copy() {
1270 let size = ModalSize::Large;
1271 let copied: ModalSize = size;
1272 assert_eq!(copied, ModalSize::Large);
1273 }
1274
1275 #[test]
1276 fn test_close_reason_debug() {
1277 let reason = CloseReason::CloseButton;
1278 let debug_str = format!("{reason:?}");
1279 assert!(debug_str.contains("CloseButton"));
1280 }
1281}