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 { key: Key::Escape } if self.close_on_escape => {
412 self.hide();
413 return Some(Box::new(ModalClosed {
414 reason: CloseReason::Escape,
415 }));
416 }
417 Event::MouseDown { position, .. } => {
418 if self.backdrop == BackdropBehavior::CloseOnClick {
420 let in_modal = position.x >= self.content_bounds.x
421 && position.x <= self.content_bounds.x + self.content_bounds.width
422 && position.y >= self.content_bounds.y
423 && position.y <= self.content_bounds.y + self.content_bounds.height;
424
425 if !in_modal {
426 self.hide();
427 return Some(Box::new(ModalClosed {
428 reason: CloseReason::Backdrop,
429 }));
430 }
431 }
432
433 if self.show_close_button {
435 let close_x =
436 self.content_bounds.x + self.content_bounds.width - 40.0 - self.padding;
437 let close_y = self.content_bounds.y + self.padding;
438 let on_close_btn = position.x >= close_x
439 && position.x <= close_x + 24.0
440 && position.y >= close_y
441 && position.y <= close_y + 24.0;
442
443 if on_close_btn {
444 self.hide();
445 return Some(Box::new(ModalClosed {
446 reason: CloseReason::CloseButton,
447 }));
448 }
449 }
450
451 if let Some(ref mut content) = self.content {
453 if let Some(msg) = content.event(event) {
454 return Some(msg);
455 }
456 }
457
458 if let Some(ref mut footer) = self.footer {
460 if let Some(msg) = footer.event(event) {
461 return Some(msg);
462 }
463 }
464 }
465 _ => {
466 if let Some(ref mut content) = self.content {
468 if let Some(msg) = content.event(event) {
469 return Some(msg);
470 }
471 }
472
473 if let Some(ref mut footer) = self.footer {
474 if let Some(msg) = footer.event(event) {
475 return Some(msg);
476 }
477 }
478 }
479 }
480
481 None
482 }
483
484 fn children(&self) -> &[Box<dyn Widget>] {
485 &[]
486 }
487
488 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
489 &mut []
490 }
491
492 fn is_focusable(&self) -> bool {
493 self.open
494 }
495
496 fn test_id(&self) -> Option<&str> {
497 self.test_id_value.as_deref()
498 }
499
500 fn bounds(&self) -> Rect {
501 self.bounds
502 }
503}
504
505impl Brick for Modal {
507 fn brick_name(&self) -> &'static str {
508 "Modal"
509 }
510
511 fn assertions(&self) -> &[BrickAssertion] {
512 &[BrickAssertion::MaxLatencyMs(16)]
513 }
514
515 fn budget(&self) -> BrickBudget {
516 BrickBudget::uniform(16)
517 }
518
519 fn verify(&self) -> BrickVerification {
520 BrickVerification {
521 passed: self.assertions().to_vec(),
522 failed: vec![],
523 verification_time: Duration::from_micros(10),
524 }
525 }
526
527 fn to_html(&self) -> String {
528 r#"<div class="brick-modal"></div>"#.to_string()
529 }
530
531 fn to_css(&self) -> String {
532 ".brick-modal { display: block; position: fixed; }".to_string()
533 }
534
535 fn test_id(&self) -> Option<&str> {
536 self.test_id_value.as_deref()
537 }
538}
539
540#[derive(Debug, Clone, Copy, PartialEq, Eq)]
542pub enum CloseReason {
543 Escape,
545 Backdrop,
547 CloseButton,
549 Programmatic,
551}
552
553#[derive(Debug, Clone)]
555pub struct ModalClosed {
556 pub reason: CloseReason,
558}
559
560#[derive(Debug, Clone)]
562pub struct ModalOpened;
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567
568 #[test]
573 fn test_modal_size_default() {
574 assert_eq!(ModalSize::default(), ModalSize::Medium);
575 }
576
577 #[test]
578 fn test_modal_size_max_width() {
579 assert_eq!(ModalSize::Small.max_width(), 300.0);
580 assert_eq!(ModalSize::Medium.max_width(), 500.0);
581 assert_eq!(ModalSize::Large.max_width(), 800.0);
582 assert_eq!(ModalSize::FullWidth.max_width(), f32::MAX);
583 assert_eq!(ModalSize::Custom(600).max_width(), 600.0);
584 }
585
586 #[test]
591 fn test_backdrop_behavior_default() {
592 assert_eq!(BackdropBehavior::default(), BackdropBehavior::CloseOnClick);
593 }
594
595 #[test]
600 fn test_modal_new() {
601 let modal = Modal::new();
602 assert!(!modal.open);
603 assert_eq!(modal.size, ModalSize::Medium);
604 assert_eq!(modal.backdrop, BackdropBehavior::CloseOnClick);
605 assert!(modal.close_on_escape);
606 assert!(modal.title.is_none());
607 assert!(modal.show_close_button);
608 }
609
610 #[test]
611 fn test_modal_builder() {
612 let modal = Modal::new()
613 .open(true)
614 .size(ModalSize::Large)
615 .backdrop(BackdropBehavior::Static)
616 .close_on_escape(false)
617 .title("Test Modal")
618 .show_close_button(false)
619 .border_radius(16.0)
620 .padding(32.0);
621
622 assert!(modal.open);
623 assert_eq!(modal.size, ModalSize::Large);
624 assert_eq!(modal.backdrop, BackdropBehavior::Static);
625 assert!(!modal.close_on_escape);
626 assert_eq!(modal.title, Some("Test Modal".to_string()));
627 assert!(!modal.show_close_button);
628 assert_eq!(modal.border_radius, 16.0);
629 assert_eq!(modal.padding, 32.0);
630 }
631
632 #[test]
633 fn test_modal_show_hide() {
634 let mut modal = Modal::new();
635 assert!(!modal.is_open());
636
637 modal.show();
638 assert!(modal.is_open());
639
640 modal.hide();
641 assert!(!modal.is_open());
642 }
643
644 #[test]
645 fn test_modal_toggle() {
646 let mut modal = Modal::new();
647 assert!(!modal.is_open());
648
649 modal.toggle();
650 assert!(modal.is_open());
651
652 modal.toggle();
653 assert!(!modal.is_open());
654 }
655
656 #[test]
657 fn test_modal_measure() {
658 let modal = Modal::new();
659 let size = modal.measure(Constraints::loose(Size::new(1024.0, 768.0)));
660 assert_eq!(size, Size::new(1024.0, 768.0));
661 }
662
663 #[test]
664 fn test_modal_layout_closed() {
665 let mut modal = Modal::new();
666 let result = modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
667 assert_eq!(result.size, Size::new(1024.0, 768.0));
668 assert_eq!(modal.animation_progress, 0.0);
669 }
670
671 #[test]
672 fn test_modal_layout_open() {
673 let mut modal = Modal::new().open(true);
674 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
675 assert!(modal.animation_progress > 0.0);
676 }
677
678 #[test]
679 fn test_modal_calculate_bounds() {
680 let modal = Modal::new().size(ModalSize::Medium);
681 let viewport = Rect::new(0.0, 0.0, 1024.0, 768.0);
682 let bounds = modal.calculate_modal_bounds(viewport);
683
684 assert!(bounds.x > 0.0);
686 assert!(bounds.y > 0.0);
687 assert!(bounds.width <= 500.0);
688 }
689
690 #[test]
691 fn test_modal_type_id() {
692 let modal = Modal::new();
693 assert_eq!(Widget::type_id(&modal), TypeId::of::<Modal>());
694 }
695
696 #[test]
697 fn test_modal_is_focusable() {
698 let modal = Modal::new();
699 assert!(!modal.is_focusable()); let modal_open = Modal::new().open(true);
702 assert!(modal_open.is_focusable()); }
704
705 #[test]
706 fn test_modal_test_id() {
707 let modal = Modal::new().with_test_id("my-modal");
708 assert_eq!(Widget::test_id(&modal), Some("my-modal"));
709 }
710
711 #[test]
712 fn test_modal_children_empty() {
713 let modal = Modal::new();
714 assert!(modal.children().is_empty());
715 }
716
717 #[test]
718 fn test_modal_bounds() {
719 let mut modal = Modal::new();
720 modal.layout(Rect::new(10.0, 20.0, 1024.0, 768.0));
721 assert_eq!(modal.bounds(), Rect::new(10.0, 20.0, 1024.0, 768.0));
722 }
723
724 #[test]
725 fn test_modal_backdrop_color() {
726 let modal = Modal::new().backdrop_color(Color::rgba(0.0, 0.0, 0.0, 0.7));
727 assert_eq!(modal.backdrop_color.a, 0.7);
728 }
729
730 #[test]
731 fn test_modal_background_color() {
732 let modal = Modal::new().background_color(Color::rgb(0.9, 0.9, 0.9));
733 assert_eq!(modal.background_color.r, 0.9);
734 }
735
736 #[test]
737 fn test_modal_escape_closes() {
738 let mut modal = Modal::new().open(true);
739 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
740
741 let result = modal.event(&Event::KeyDown { key: Key::Escape });
742 assert!(result.is_some());
743 assert!(!modal.is_open());
744 }
745
746 #[test]
747 fn test_modal_escape_disabled() {
748 let mut modal = Modal::new().open(true).close_on_escape(false);
749 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
750
751 let result = modal.event(&Event::KeyDown { key: Key::Escape });
752 assert!(result.is_none());
753 assert!(modal.is_open());
754 }
755
756 #[test]
757 fn test_modal_animation_progress() {
758 let modal = Modal::new();
759 assert_eq!(modal.animation_progress(), 0.0);
760 }
761
762 #[test]
763 fn test_modal_content_bounds() {
764 let mut modal = Modal::new().open(true);
765 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
766 let content_bounds = modal.content_bounds();
767 assert!(content_bounds.width > 0.0);
768 assert!(content_bounds.height > 0.0);
769 }
770
771 #[test]
776 fn test_close_reason_eq() {
777 assert_eq!(CloseReason::Escape, CloseReason::Escape);
778 assert_ne!(CloseReason::Escape, CloseReason::Backdrop);
779 }
780
781 #[test]
786 fn test_modal_closed_message() {
787 let msg = ModalClosed {
788 reason: CloseReason::CloseButton,
789 };
790 assert_eq!(msg.reason, CloseReason::CloseButton);
791 }
792
793 #[test]
794 fn test_modal_opened_message() {
795 let _msg = ModalOpened;
796 }
798
799 #[test]
804 fn test_modal_backdrop_none() {
805 let modal = Modal::new().backdrop(BackdropBehavior::None);
806 assert_eq!(modal.backdrop, BackdropBehavior::None);
807 }
808
809 #[test]
810 fn test_modal_backdrop_static() {
811 let modal = Modal::new().backdrop(BackdropBehavior::Static);
812 assert_eq!(modal.backdrop, BackdropBehavior::Static);
813 }
814
815 #[test]
816 fn test_modal_size_small() {
817 assert_eq!(ModalSize::Small.max_width(), 300.0);
818 }
819
820 #[test]
821 fn test_modal_size_full_width() {
822 assert_eq!(ModalSize::FullWidth.max_width(), f32::MAX);
823 }
824
825 #[test]
826 fn test_modal_children_mut_empty() {
827 let mut modal = Modal::new();
828 assert!(modal.children_mut().is_empty());
829 }
830
831 #[test]
832 fn test_modal_calculate_bounds_with_title() {
833 let modal = Modal::new().title("Test Title");
834 let viewport = Rect::new(0.0, 0.0, 1024.0, 768.0);
835 let bounds = modal.calculate_modal_bounds(viewport);
836 assert!(bounds.height > 0.0);
837 }
838
839 #[test]
840 fn test_modal_layout_animation_closes() {
841 let mut modal = Modal::new().open(true);
842 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
843 let prog1 = modal.animation_progress;
845 modal.open = false;
846 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
847 assert!(modal.animation_progress < prog1);
849 }
850
851 #[test]
852 fn test_modal_event_not_open_returns_none() {
853 let mut modal = Modal::new();
854 let result = modal.event(&Event::KeyDown { key: Key::Escape });
855 assert!(result.is_none());
856 }
857
858 #[test]
859 fn test_modal_other_key_does_nothing() {
860 let mut modal = Modal::new().open(true);
861 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
862 let result = modal.event(&Event::KeyDown { key: Key::Tab });
863 assert!(result.is_none());
864 assert!(modal.is_open());
865 }
866
867 #[test]
868 fn test_close_reason_programmatic() {
869 let reason = CloseReason::Programmatic;
870 assert_eq!(reason, CloseReason::Programmatic);
871 }
872
873 #[test]
874 fn test_close_reason_close_button() {
875 let reason = CloseReason::CloseButton;
876 assert_eq!(reason, CloseReason::CloseButton);
877 }
878
879 #[test]
880 fn test_modal_size_custom_value() {
881 let size = ModalSize::Custom(750);
882 assert_eq!(size.max_width(), 750.0);
883 }
884
885 #[test]
886 fn test_modal_backdrop_eq() {
887 assert_eq!(
888 BackdropBehavior::CloseOnClick,
889 BackdropBehavior::CloseOnClick
890 );
891 assert_ne!(BackdropBehavior::CloseOnClick, BackdropBehavior::Static);
892 }
893
894 #[test]
895 fn test_modal_size_eq() {
896 assert_eq!(ModalSize::Medium, ModalSize::Medium);
897 assert_ne!(ModalSize::Small, ModalSize::Large);
898 }
899
900 #[test]
905 fn test_modal_brick_name() {
906 let modal = Modal::new();
907 assert_eq!(modal.brick_name(), "Modal");
908 }
909
910 #[test]
911 fn test_modal_brick_assertions() {
912 let modal = Modal::new();
913 let assertions = modal.assertions();
914 assert!(!assertions.is_empty());
915 assert!(matches!(assertions[0], BrickAssertion::MaxLatencyMs(16)));
916 }
917
918 #[test]
919 fn test_modal_brick_budget() {
920 let modal = Modal::new();
921 let budget = modal.budget();
922 assert!(budget.layout_ms > 0);
924 assert!(budget.paint_ms > 0);
925 }
926
927 #[test]
928 fn test_modal_brick_verify() {
929 let modal = Modal::new();
930 let verification = modal.verify();
931 assert!(!verification.passed.is_empty());
932 assert!(verification.failed.is_empty());
933 }
934
935 #[test]
936 fn test_modal_brick_to_html() {
937 let modal = Modal::new();
938 let html = modal.to_html();
939 assert!(html.contains("brick-modal"));
940 }
941
942 #[test]
943 fn test_modal_brick_to_css() {
944 let modal = Modal::new();
945 let css = modal.to_css();
946 assert!(css.contains(".brick-modal"));
947 assert!(css.contains("display: block"));
948 assert!(css.contains("position: fixed"));
949 }
950
951 #[test]
952 fn test_modal_brick_test_id() {
953 let modal = Modal::new().with_test_id("my-modal");
954 assert_eq!(Brick::test_id(&modal), Some("my-modal"));
955 }
956
957 #[test]
958 fn test_modal_brick_test_id_none() {
959 let modal = Modal::new();
960 assert!(Brick::test_id(&modal).is_none());
961 }
962
963 #[test]
968 fn test_modal_backdrop_click_closes() {
969 let mut modal = Modal::new().open(true);
970 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
971
972 let result = modal.event(&Event::MouseDown {
974 position: Point::new(10.0, 10.0),
975 button: presentar_core::MouseButton::Left,
976 });
977
978 assert!(result.is_some());
979 assert!(!modal.is_open());
980 }
981
982 #[test]
983 fn test_modal_backdrop_static_no_close() {
984 let mut modal = Modal::new().open(true).backdrop(BackdropBehavior::Static);
985 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
986
987 let result = modal.event(&Event::MouseDown {
989 position: Point::new(10.0, 10.0),
990 button: presentar_core::MouseButton::Left,
991 });
992
993 assert!(result.is_none());
994 assert!(modal.is_open());
995 }
996
997 #[test]
998 fn test_modal_click_inside_does_not_close() {
999 let mut modal = Modal::new().open(true);
1000 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1001
1002 let center_x = modal.content_bounds.x + modal.content_bounds.width / 2.0;
1004 let center_y = modal.content_bounds.y + modal.content_bounds.height / 2.0;
1005
1006 let result = modal.event(&Event::MouseDown {
1007 position: Point::new(center_x, center_y),
1008 button: presentar_core::MouseButton::Left,
1009 });
1010
1011 assert!(result.is_none());
1013 assert!(modal.is_open());
1014 }
1015
1016 #[test]
1021 fn test_modal_close_button_click() {
1022 let mut modal = Modal::new().open(true);
1023 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1024
1025 let close_x = modal.content_bounds.x + modal.content_bounds.width - 40.0 - modal.padding;
1027 let close_y = modal.content_bounds.y + modal.padding;
1028
1029 let result = modal.event(&Event::MouseDown {
1030 position: Point::new(close_x + 10.0, close_y + 10.0),
1031 button: presentar_core::MouseButton::Left,
1032 });
1033
1034 assert!(result.is_some());
1035 assert!(!modal.is_open());
1036 }
1037
1038 #[test]
1039 fn test_modal_close_button_hidden() {
1040 let mut modal = Modal::new().open(true).show_close_button(false);
1041 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1042
1043 let close_x = modal.content_bounds.x + modal.content_bounds.width - 40.0 - modal.padding;
1045 let close_y = modal.content_bounds.y + modal.padding;
1046
1047 let result = modal.event(&Event::MouseDown {
1048 position: Point::new(close_x + 10.0, close_y + 10.0),
1049 button: presentar_core::MouseButton::Left,
1050 });
1051
1052 assert!(result.is_none());
1054 assert!(modal.is_open());
1055 }
1056
1057 #[test]
1062 fn test_modal_animation_opens() {
1063 let mut modal = Modal::new().open(true);
1064 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1065 assert!(modal.animation_progress > 0.0);
1066
1067 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1069 assert!(modal.animation_progress >= 0.15);
1070 }
1071
1072 #[test]
1073 fn test_modal_animation_caps_at_one() {
1074 let mut modal = Modal::new().open(true);
1075 for _ in 0..20 {
1077 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1078 }
1079 assert!((modal.animation_progress - 1.0).abs() < 0.01);
1080 }
1081
1082 #[test]
1083 fn test_modal_animation_closes_to_zero() {
1084 let mut modal = Modal::new().open(true);
1085 for _ in 0..20 {
1087 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1088 }
1089
1090 modal.open = false;
1091 for _ in 0..20 {
1093 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1094 }
1095 assert!(modal.animation_progress < 0.01);
1096 }
1097
1098 #[test]
1103 fn test_modal_calculate_bounds_centered() {
1104 let modal = Modal::new().size(ModalSize::Medium);
1105 let viewport = Rect::new(0.0, 0.0, 1024.0, 768.0);
1106 let bounds = modal.calculate_modal_bounds(viewport);
1107
1108 let expected_x = (1024.0 - bounds.width) / 2.0;
1110 assert!((bounds.x - expected_x).abs() < 1.0);
1111 }
1112
1113 #[test]
1114 fn test_modal_calculate_bounds_small_viewport() {
1115 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);
1118
1119 assert!(bounds.width <= 400.0 - 32.0);
1121 }
1122
1123 #[test]
1124 fn test_modal_calculate_bounds_with_footer() {
1125 let modal = Modal::new().title("Test");
1126 let viewport = Rect::new(0.0, 0.0, 1024.0, 768.0);
1127 let bounds = modal.calculate_modal_bounds(viewport);
1128
1129 assert!(bounds.height > 0.0);
1131 }
1132
1133 #[test]
1138 fn test_modal_size_large() {
1139 assert_eq!(ModalSize::Large.max_width(), 800.0);
1140 }
1141
1142 #[test]
1143 fn test_modal_size_custom_zero() {
1144 assert_eq!(ModalSize::Custom(0).max_width(), 0.0);
1146 }
1147
1148 #[test]
1153 fn test_close_reason_copy() {
1154 let reason = CloseReason::Escape;
1155 let copied: CloseReason = reason;
1156 assert_eq!(copied, CloseReason::Escape);
1157 }
1158
1159 #[test]
1160 fn test_close_reason_all_variants() {
1161 let reasons = [
1162 CloseReason::Escape,
1163 CloseReason::Backdrop,
1164 CloseReason::CloseButton,
1165 CloseReason::Programmatic,
1166 ];
1167 assert_eq!(reasons.len(), 4);
1168 }
1169
1170 #[test]
1175 fn test_modal_closed_clone() {
1176 let msg = ModalClosed {
1177 reason: CloseReason::Escape,
1178 };
1179 let cloned = msg.clone();
1180 assert_eq!(cloned.reason, CloseReason::Escape);
1181 }
1182
1183 #[test]
1184 fn test_modal_opened_clone() {
1185 let msg = ModalOpened;
1186 let _cloned = msg.clone();
1187 }
1188
1189 #[test]
1190 fn test_modal_closed_debug() {
1191 let msg = ModalClosed {
1192 reason: CloseReason::Backdrop,
1193 };
1194 let debug_str = format!("{:?}", msg);
1195 assert!(debug_str.contains("Backdrop"));
1196 }
1197
1198 #[test]
1199 fn test_modal_opened_debug() {
1200 let msg = ModalOpened;
1201 let debug_str = format!("{:?}", msg);
1202 assert!(debug_str.contains("ModalOpened"));
1203 }
1204
1205 #[test]
1210 fn test_modal_default_values() {
1211 let modal = Modal::default();
1212 assert!(!modal.open);
1213 assert_eq!(modal.size, ModalSize::Medium);
1214 assert_eq!(modal.backdrop, BackdropBehavior::CloseOnClick);
1215 assert!(modal.close_on_escape);
1216 assert!(modal.title.is_none());
1217 assert!(modal.show_close_button);
1218 assert_eq!(modal.border_radius, 8.0);
1219 assert_eq!(modal.padding, 24.0);
1220 }
1221
1222 #[test]
1227 fn test_modal_measure_constraints() {
1228 let modal = Modal::new();
1229 let size = modal.measure(Constraints::tight(Size::new(800.0, 600.0)));
1230 assert_eq!(size.width, 800.0);
1231 assert_eq!(size.height, 600.0);
1232 }
1233
1234 #[test]
1235 fn test_modal_children_mut() {
1236 let mut modal = Modal::new();
1237 assert!(modal.children_mut().is_empty());
1238 }
1239
1240 #[test]
1241 fn test_modal_mouse_move_does_nothing() {
1242 let mut modal = Modal::new().open(true);
1243 modal.layout(Rect::new(0.0, 0.0, 1024.0, 768.0));
1244
1245 let result = modal.event(&Event::MouseMove {
1246 position: Point::new(100.0, 100.0),
1247 });
1248 assert!(result.is_none());
1249 }
1250
1251 #[test]
1252 fn test_modal_title_setter() {
1253 let modal = Modal::new().title("Test Modal");
1254 let _ = modal;
1256 }
1257
1258 #[test]
1259 fn test_backdrop_behavior_copy() {
1260 let behavior = BackdropBehavior::Static;
1261 let copied: BackdropBehavior = behavior;
1262 assert_eq!(copied, BackdropBehavior::Static);
1263 }
1264
1265 #[test]
1266 fn test_modal_size_copy() {
1267 let size = ModalSize::Large;
1268 let copied: ModalSize = size;
1269 assert_eq!(copied, ModalSize::Large);
1270 }
1271
1272 #[test]
1273 fn test_close_reason_debug() {
1274 let reason = CloseReason::CloseButton;
1275 let debug_str = format!("{:?}", reason);
1276 assert!(debug_str.contains("CloseButton"));
1277 }
1278}