1use frus_core::{BorderRadius, Color, Insets, Rect, Scene, ShapeBorder, TextAlign, TextStyle};
26use frus_layout::{Align, Dimension, FlexDirection, Justify, Style};
27
28use crate::interaction::Status;
29use crate::portal::Placement;
30use crate::rowcolumn::VerticalDirection;
31use crate::theme::Theme;
32use crate::widget::Widget;
33
34pub const DIALOG_INSET_PADDING: Insets = Insets {
37 top: 24.0,
38 right: 40.0,
39 bottom: 24.0,
40 left: 40.0,
41};
42pub const DIALOG_MIN_WIDTH: f32 = 280.0;
44pub const DIALOG_RADIUS: f32 = 28.0;
46pub const DIALOG_ELEVATION: f32 = 6.0;
48const EDGE: f32 = 24.0;
50const CONTENT_GAP: f32 = 16.0;
52const TITLE_ONLY_GAP: f32 = 20.0;
54const ACTION_GAP: f32 = 8.0;
57
58fn title_style(over: Option<TextStyle>, theme: Option<&Theme>) -> TextStyle {
61 over.or(theme.and_then(|t| t.widgets.dialog.title_text_style))
62 .unwrap_or_else(|| crate::theme::type_scale(theme).headline_small)
63}
64
65fn content_style(over: Option<TextStyle>, theme: Option<&Theme>) -> TextStyle {
67 over.or(theme.and_then(|t| t.widgets.dialog.content_text_style))
68 .unwrap_or_else(|| crate::theme::type_scale(theme).body_medium)
69}
70
71fn texted<Msg: Clone + 'static>(
75 style: TextStyle,
76 align: Option<TextAlign>,
77 child: Box<dyn Widget<Msg>>,
78) -> crate::Themed<Msg> {
79 crate::Themed::tweak(
80 move |t: &mut Theme| {
81 t.widgets.text.style = style;
82 if let Some(align) = align {
83 t.widgets.text.align = Some(align);
84 }
85 },
86 child,
87 )
88}
89
90struct DialogSurface<Msg> {
93 children: Vec<Box<dyn Widget<Msg>>>,
94 background: Option<Color>,
95 elevation: Option<f32>,
96 shadow_color: Option<Color>,
97 surface_tint: Option<Color>,
98 shape: Option<ShapeBorder>,
99 inset_padding: Option<Insets>,
100 min_width: Option<f32>,
101 max_width: Option<f32>,
102}
103
104impl<Msg> DialogSurface<Msg> {
105 fn elevation(&self, theme: Option<&Theme>) -> f32 {
106 self.elevation
107 .or(theme.and_then(|t| t.widgets.dialog.elevation))
108 .unwrap_or(DIALOG_ELEVATION)
109 }
110
111 fn shape(&self, theme: Option<&Theme>) -> ShapeBorder {
114 crate::resolve_shape(
115 self.shape,
116 theme.and_then(|t| t.widgets.dialog.shape),
117 None,
118 ShapeBorder::rounded(DIALOG_RADIUS),
119 )
120 }
121
122 fn background(&self, theme: &Theme) -> Color {
128 let base = self
129 .background
130 .or(theme.widgets.dialog.color)
131 .unwrap_or(theme.scheme.surface_container_high);
132 match self.surface_tint.or(theme.widgets.dialog.surface_tint) {
133 Some(tint) => base.surface_tint(tint, self.elevation(Some(theme))),
134 None => base,
135 }
136 }
137
138 fn sizing(&self, theme: Option<&Theme>) -> Style {
139 let margin = self
140 .inset_padding
141 .or(theme.and_then(|t| t.widgets.dialog.inset_padding))
142 .unwrap_or(DIALOG_INSET_PADDING);
143 Style {
144 flex_direction: FlexDirection::Column,
145 height: Dimension::Auto,
146 min_width: Dimension::Length(self.min_width.unwrap_or(DIALOG_MIN_WIDTH)),
147 max_width: self.max_width.map_or(Dimension::Auto, Dimension::Length),
148 margin,
149 ..Default::default()
150 }
151 }
152}
153
154impl<Msg: Clone> Widget<Msg> for DialogSurface<Msg> {
155 fn style(&self) -> Style {
156 self.sizing(None)
157 }
158
159 fn style_themed(&self, theme: &Theme) -> Style {
160 self.sizing(Some(theme))
161 }
162
163 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
164 &self.children
165 }
166
167 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
168 let o = status.opacity;
169 let shape = self.shape(Some(theme));
170 let radius = shape
173 .as_rounded(bounds)
174 .map(|(_, radius)| radius)
175 .unwrap_or(BorderRadius::ZERO);
176 let depth = self.elevation(Some(theme));
177 let shadow = self
181 .shadow_color
182 .or(theme.widgets.dialog.shadow_color)
183 .unwrap_or(Color::TRANSPARENT);
184 if depth > 0.0 && shadow.a > 0.0 {
185 frus_core::paint_elevation(scene, bounds, radius, depth, shadow.fade(o));
187 }
188 scene.draw_shape(bounds, shape, self.background(theme).fade(o));
189 }
190
191 fn on_click(&self) -> Option<Msg> {
192 None
193 }
194}
195
196pub struct Dialog<Msg = crate::callback::Callback> {
202 open: bool,
203 on_dismiss: Option<Msg>,
204 content: Option<Box<dyn Widget<Msg>>>,
205 panel: Option<Box<dyn Widget<Msg>>>,
206 children: Vec<Box<dyn Widget<Msg>>>,
207 background: Option<Color>,
208 elevation: Option<f32>,
209 shadow_color: Option<Color>,
210 surface_tint: Option<Color>,
211 shape: Option<ShapeBorder>,
212 inset_padding: Option<Insets>,
213 min_width: Option<f32>,
214 max_width: Option<f32>,
215}
216
217impl<Msg: Clone + 'static> Dialog<Msg> {
218 pub fn new(open: bool) -> Self {
220 Self {
221 open,
222 on_dismiss: None,
223 content: None,
224 panel: None,
225 children: Vec::new(),
226 background: None,
227 elevation: None,
228 shadow_color: None,
229 surface_tint: None,
230 shape: None,
231 inset_padding: None,
232 min_width: None,
233 max_width: None,
234 }
235 }
236
237 #[must_use]
240 pub fn on_dismiss(mut self, message: impl Into<Msg>) -> Self {
241 self.on_dismiss = Some(message.into());
242 self
243 }
244
245 #[must_use]
247 pub fn child(mut self, content: impl Widget<Msg> + 'static) -> Self {
248 self.content = Some(Box::new(content));
249 self
250 }
251
252 #[must_use]
255 pub fn background(mut self, color: Color) -> Self {
256 self.background = Some(color);
257 self
258 }
259
260 #[must_use]
262 pub fn elevation(mut self, elevation: f32) -> Self {
263 self.elevation = Some(elevation);
264 self
265 }
266
267 #[must_use]
270 pub fn shadow_color(mut self, color: Color) -> Self {
271 self.shadow_color = Some(color);
272 self
273 }
274
275 #[must_use]
278 pub fn surface_tint(mut self, color: Color) -> Self {
279 self.surface_tint = Some(color);
280 self
281 }
282
283 #[must_use]
291 pub fn shape(mut self, shape: ShapeBorder) -> Self {
292 self.shape = Some(shape);
293 self
294 }
295
296 #[must_use]
299 pub fn radius(self, radius: impl Into<BorderRadius>) -> Self {
300 self.shape(ShapeBorder::rounded(radius))
301 }
302
303 #[must_use]
306 pub fn inset_padding(mut self, padding: Insets) -> Self {
307 self.inset_padding = Some(padding);
308 self
309 }
310
311 #[must_use]
314 pub fn min_width(mut self, width: f32) -> Self {
315 self.min_width = Some(width);
316 self
317 }
318
319 #[must_use]
321 pub fn max_width(mut self, width: f32) -> Self {
322 self.max_width = Some(width);
323 self
324 }
325
326 #[must_use]
328 pub fn body(mut self, body: impl Widget<Msg> + 'static) -> Self {
329 self.panel = self.content.take().map(|content| {
330 Box::new(DialogSurface {
331 children: vec![content],
332 background: self.background,
333 elevation: self.elevation,
334 shadow_color: self.shadow_color,
335 surface_tint: self.surface_tint,
336 shape: self.shape,
337 inset_padding: self.inset_padding,
338 min_width: self.min_width,
339 max_width: self.max_width,
340 }) as Box<dyn Widget<Msg>>
341 });
342 self.children = vec![Box::new(body)];
343 self
344 }
345}
346
347impl<Msg: Clone> Widget<Msg> for Dialog<Msg> {
348 fn style(&self) -> Style {
349 Style {
350 height: Dimension::Percent(1.0),
351 flex_direction: FlexDirection::Column,
352 ..Default::default()
353 }
354 }
355
356 fn fill_axes(&self, _theme: &Theme) -> crate::widget::FillAxes {
358 crate::widget::FillAxes::WIDTH
359 }
360
361 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
362 &self.children
363 }
364
365 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
366
367 fn on_click(&self) -> Option<Msg> {
368 None
369 }
370
371 fn overlay(&self) -> Option<(&dyn Widget<Msg>, Placement)> {
372 self.panel.as_ref().map(|p| (p.as_ref(), Placement::Center))
373 }
374
375 fn overlay_dismiss(&self) -> Option<Msg> {
376 self.on_dismiss.clone()
377 }
378
379 fn anim_target(&self) -> Option<f32> {
380 Some(if self.open { 1.0 } else { 0.0 })
381 }
382}
383
384pub type ActionsAlignment = Justify;
386
387pub struct AlertDialog<Msg = crate::callback::Callback> {
396 dialog: Dialog<Msg>,
397 icon: Option<Box<dyn Widget<Msg>>>,
398 icon_color: Option<Color>,
399 title: Option<Box<dyn Widget<Msg>>>,
400 title_text_style: Option<TextStyle>,
401 content: Option<Box<dyn Widget<Msg>>>,
402 content_text_style: Option<TextStyle>,
403 actions: Vec<Box<dyn Widget<Msg>>>,
404 actions_alignment: Option<ActionsAlignment>,
405 actions_overflow_alignment: Option<Align>,
406 actions_overflow_direction: Option<VerticalDirection>,
407 actions_overflow_button_spacing: Option<f32>,
408}
409
410impl<Msg: Clone + 'static> AlertDialog<Msg> {
411 pub fn new(open: bool) -> Self {
413 Self {
414 dialog: Dialog::new(open),
415 icon: None,
416 icon_color: None,
417 title: None,
418 title_text_style: None,
419 content: None,
420 content_text_style: None,
421 actions: Vec::new(),
422 actions_alignment: None,
423 actions_overflow_alignment: None,
424 actions_overflow_direction: None,
425 actions_overflow_button_spacing: None,
426 }
427 }
428
429 #[must_use]
432 pub fn on_dismiss(mut self, message: impl Into<Msg>) -> Self {
433 self.dialog = self.dialog.on_dismiss(message);
434 self
435 }
436
437 #[must_use]
439 pub fn icon(mut self, icon: impl Widget<Msg> + 'static) -> Self {
440 self.icon = Some(Box::new(icon));
441 self
442 }
443
444 #[must_use]
446 pub fn icon_color(mut self, color: Color) -> Self {
447 self.icon_color = Some(color);
448 self
449 }
450
451 #[must_use]
453 pub fn title(self, title: impl Into<String>) -> Self {
454 let text = crate::Text::new(title);
455 self.title_widget(text)
456 }
457
458 #[must_use]
460 pub fn title_widget(mut self, title: impl Widget<Msg> + 'static) -> Self {
461 self.title = Some(Box::new(title));
462 self
463 }
464
465 #[must_use]
467 pub fn title_text_style(mut self, style: TextStyle) -> Self {
468 self.title_text_style = Some(style);
469 self
470 }
471
472 #[must_use]
474 pub fn content(self, content: impl Into<String>) -> Self {
475 let text = crate::Text::new(content);
476 self.content_widget(text)
477 }
478
479 #[must_use]
481 pub fn content_widget(mut self, content: impl Widget<Msg> + 'static) -> Self {
482 self.content = Some(Box::new(content));
483 self
484 }
485
486 #[must_use]
488 pub fn content_text_style(mut self, style: TextStyle) -> Self {
489 self.content_text_style = Some(style);
490 self
491 }
492
493 #[must_use]
495 pub fn action(mut self, action: impl Widget<Msg> + 'static) -> Self {
496 self.actions.push(Box::new(action));
497 self
498 }
499
500 #[must_use]
503 pub fn actions_alignment(mut self, alignment: ActionsAlignment) -> Self {
504 self.actions_alignment = Some(alignment);
505 self
506 }
507
508 #[must_use]
513 pub fn actions_overflow_alignment(mut self, alignment: Align) -> Self {
514 self.actions_overflow_alignment = Some(alignment);
515 self
516 }
517
518 #[must_use]
526 pub fn actions_overflow_direction(mut self, direction: VerticalDirection) -> Self {
527 self.actions_overflow_direction = Some(direction);
528 self
529 }
530
531 #[must_use]
535 pub fn actions_overflow_button_spacing(mut self, spacing: f32) -> Self {
536 self.actions_overflow_button_spacing = Some(spacing);
537 self
538 }
539
540 #[must_use]
542 pub fn background(mut self, color: Color) -> Self {
543 self.dialog = self.dialog.background(color);
544 self
545 }
546
547 #[must_use]
549 pub fn elevation(mut self, elevation: f32) -> Self {
550 self.dialog = self.dialog.elevation(elevation);
551 self
552 }
553
554 #[must_use]
556 pub fn shadow_color(mut self, color: Color) -> Self {
557 self.dialog = self.dialog.shadow_color(color);
558 self
559 }
560
561 #[must_use]
563 pub fn surface_tint(mut self, color: Color) -> Self {
564 self.dialog = self.dialog.surface_tint(color);
565 self
566 }
567
568 #[must_use]
570 pub fn shape(mut self, shape: ShapeBorder) -> Self {
571 self.dialog = self.dialog.shape(shape);
572 self
573 }
574
575 #[must_use]
577 pub fn inset_padding(mut self, padding: Insets) -> Self {
578 self.dialog = self.dialog.inset_padding(padding);
579 self
580 }
581
582 #[must_use]
584 pub fn min_width(mut self, width: f32) -> Self {
585 self.dialog = self.dialog.min_width(width);
586 self
587 }
588
589 #[must_use]
591 pub fn max_width(mut self, width: f32) -> Self {
592 self.dialog = self.dialog.max_width(width);
593 self
594 }
595
596 #[must_use]
602 pub fn body(self, body: impl Widget<Msg> + 'static) -> Dialog<Msg> {
603 let AlertDialog {
604 dialog,
605 icon,
606 icon_color,
607 title,
608 title_text_style,
609 content,
610 content_text_style,
611 actions,
612 actions_alignment,
613 actions_overflow_alignment,
614 actions_overflow_direction,
615 actions_overflow_button_spacing,
616 } = self;
617 let has_icon = icon.is_some();
618 let has_title = title.is_some();
619 let has_content = content.is_some();
620 let column = crate::ThemeBuilder::boxed(move |theme| {
621 let mut column = crate::Flex::column().align(Align::Stretch);
625 if let Some(icon) = icon {
626 let below = if has_title {
629 CONTENT_GAP
630 } else if has_content {
631 0.0
632 } else {
633 EDGE
634 };
635 let color = icon_color
636 .or(theme.widgets.dialog.icon_color)
637 .unwrap_or(theme.scheme.secondary);
638 column = column.child(
639 crate::Flex::column()
640 .align(Align::Center)
641 .padding_each(EDGE, EDGE, below, EDGE)
642 .child(crate::Themed::tweak(
643 move |t: &mut Theme| t.widgets.icon.color = Some(color),
644 icon,
645 )),
646 );
647 }
648 if let Some(title) = title {
649 let above = if has_icon { 0.0 } else { EDGE };
650 let below = if has_content { 0.0 } else { TITLE_ONLY_GAP };
651 let align = if has_icon {
652 TextAlign::Center
653 } else {
654 TextAlign::Start
655 };
656 column = column.child(
663 crate::Flex::column()
664 .padding_each(above, EDGE, below, EDGE)
665 .child(
666 crate::Flex::row()
667 .justify(if has_icon {
668 Justify::Center
669 } else {
670 Justify::Start
671 })
672 .child(texted(
673 title_style(title_text_style, Some(theme)),
674 Some(align),
675 title,
676 )),
677 ),
678 );
679 }
680 if let Some(content) = content {
681 let above = if has_icon || has_title {
682 CONTENT_GAP
683 } else {
684 EDGE
685 };
686 column = column.child(
687 crate::Flex::column()
688 .padding_each(above, EDGE, EDGE, EDGE)
689 .child(texted(
690 content_style(content_text_style, Some(theme)),
691 None,
692 content,
693 )),
694 );
695 }
696 if !actions.is_empty() {
697 let mut row = crate::OverflowBar::new()
704 .alignment(actions_alignment.unwrap_or(Justify::End))
705 .spacing(ACTION_GAP)
706 .overflow_alignment(actions_overflow_alignment.unwrap_or(Align::End))
707 .overflow_direction(actions_overflow_direction.unwrap_or_default())
708 .overflow_spacing(actions_overflow_button_spacing.unwrap_or(0.0));
709 for action in actions {
710 row = row.child_boxed(action);
711 }
712 column = column.child(
713 crate::Flex::column()
714 .padding_each(0.0, EDGE, EDGE, EDGE)
715 .child(row),
716 );
717 }
718 Box::new(column) as Box<dyn Widget<Msg>>
719 });
720 dialog.child(column).body(body)
721 }
722}
723
724const SIMPLE_TITLE_PADDING: Insets = Insets {
726 top: 24.0,
727 right: 24.0,
728 bottom: 0.0,
729 left: 24.0,
730};
731const SIMPLE_CONTENT_PADDING: Insets = Insets {
735 top: 12.0,
736 right: 0.0,
737 bottom: 16.0,
738 left: 0.0,
739};
740const OPTION_PADDING: Insets = Insets {
742 top: 8.0,
743 right: 24.0,
744 bottom: 8.0,
745 left: 24.0,
746};
747
748pub struct SimpleDialogOption<Msg = crate::callback::Callback> {
755 child: Option<Box<dyn Widget<Msg>>>,
756 on_press: Option<Msg>,
757 padding: Option<Insets>,
758}
759
760impl<Msg: Clone + 'static> SimpleDialogOption<Msg> {
761 pub fn new(label: impl Into<String>, message: Msg) -> Self {
763 Self {
764 child: Some(Box::new(crate::Text::new(label))),
765 on_press: Some(message),
766 padding: None,
767 }
768 }
769
770 pub fn with_child(child: impl Widget<Msg> + 'static) -> Self {
772 Self {
773 child: Some(Box::new(child)),
774 on_press: None,
775 padding: None,
776 }
777 }
778
779 #[must_use]
782 pub fn on_press(mut self, message: impl Into<Msg>) -> Self {
783 self.on_press = Some(message.into());
784 self
785 }
786
787 #[must_use]
789 pub fn padding(mut self, padding: Insets) -> Self {
790 self.padding = Some(padding);
791 self
792 }
793
794 pub fn build(self) -> Box<dyn Widget<Msg>> {
796 let padding = self.padding.unwrap_or(OPTION_PADDING);
797 let mut row = crate::Flex::row().align(Align::Center).padding_each(
798 padding.top,
799 padding.right,
800 padding.bottom,
801 padding.left,
802 );
803 if let Some(child) = self.child {
804 row = row.child_boxed(child);
805 }
806 let mut ink = crate::InkWell::new();
807 if let Some(message) = self.on_press {
808 ink = ink.on_click(message);
809 }
810 Box::new(ink.child(row))
811 }
812}
813
814pub struct SimpleDialog<Msg = crate::callback::Callback> {
820 dialog: Dialog<Msg>,
821 title: Option<Box<dyn Widget<Msg>>>,
822 title_text_style: Option<TextStyle>,
823 title_padding: Option<Insets>,
824 content_padding: Option<Insets>,
825 content_text_style: Option<TextStyle>,
826 options: Vec<Box<dyn Widget<Msg>>>,
827}
828
829impl<Msg: Clone + 'static> SimpleDialog<Msg> {
830 pub fn new(open: bool) -> Self {
832 Self {
833 dialog: Dialog::new(open),
834 title: None,
835 title_text_style: None,
836 title_padding: None,
837 content_padding: None,
838 content_text_style: None,
839 options: Vec::new(),
840 }
841 }
842
843 #[must_use]
845 pub fn on_dismiss(mut self, message: impl Into<Msg>) -> Self {
846 self.dialog = self.dialog.on_dismiss(message);
847 self
848 }
849
850 #[must_use]
852 pub fn title(self, title: impl Into<String>) -> Self {
853 let text = crate::Text::new(title);
854 self.title_widget(text)
855 }
856
857 #[must_use]
859 pub fn title_widget(mut self, title: impl Widget<Msg> + 'static) -> Self {
860 self.title = Some(Box::new(title));
861 self
862 }
863
864 #[must_use]
866 pub fn title_text_style(mut self, style: TextStyle) -> Self {
867 self.title_text_style = Some(style);
868 self
869 }
870
871 #[must_use]
873 pub fn title_padding(mut self, padding: Insets) -> Self {
874 self.title_padding = Some(padding);
875 self
876 }
877
878 #[must_use]
881 pub fn content_padding(mut self, padding: Insets) -> Self {
882 self.content_padding = Some(padding);
883 self
884 }
885
886 #[must_use]
888 pub fn content_text_style(mut self, style: TextStyle) -> Self {
889 self.content_text_style = Some(style);
890 self
891 }
892
893 #[must_use]
895 pub fn option(mut self, option: impl Widget<Msg> + 'static) -> Self {
896 self.options.push(Box::new(option));
897 self
898 }
899
900 #[must_use]
902 pub fn background(mut self, color: Color) -> Self {
903 self.dialog = self.dialog.background(color);
904 self
905 }
906
907 #[must_use]
909 pub fn elevation(mut self, elevation: f32) -> Self {
910 self.dialog = self.dialog.elevation(elevation);
911 self
912 }
913
914 #[must_use]
916 pub fn shape(mut self, shape: ShapeBorder) -> Self {
917 self.dialog = self.dialog.shape(shape);
918 self
919 }
920
921 #[must_use]
923 pub fn inset_padding(mut self, padding: Insets) -> Self {
924 self.dialog = self.dialog.inset_padding(padding);
925 self
926 }
927
928 #[must_use]
930 pub fn body(self, body: impl Widget<Msg> + 'static) -> Dialog<Msg> {
931 let SimpleDialog {
932 dialog,
933 title,
934 title_text_style,
935 title_padding,
936 content_padding,
937 content_text_style,
938 options,
939 } = self;
940 let has_options = !options.is_empty();
941 let column = crate::ThemeBuilder::boxed(move |theme| {
942 let mut column = crate::Flex::column().align(Align::Stretch);
943 if let Some(title) = title {
944 let pad = title_padding.unwrap_or(SIMPLE_TITLE_PADDING);
945 let bottom = if has_options { 0.0 } else { pad.bottom };
949 column = column.child(
950 crate::Flex::column()
951 .padding_each(pad.top, pad.right, bottom, pad.left)
952 .child(texted(
953 title_style(title_text_style, Some(theme)),
954 None,
955 title,
956 )),
957 );
958 }
959 if has_options {
960 let pad = content_padding.unwrap_or(SIMPLE_CONTENT_PADDING);
961 let mut list = crate::Flex::column().align(Align::Stretch);
962 for option in options {
963 list = list.child_boxed(option);
964 }
965 column = column.child(
966 crate::Flex::column()
967 .padding_each(pad.top, pad.right, pad.bottom, pad.left)
968 .child(texted(
969 content_style(content_text_style, Some(theme)),
970 None,
971 Box::new(list),
972 )),
973 );
974 }
975 Box::new(column) as Box<dyn Widget<Msg>>
976 });
977 dialog.child(column).body(body)
978 }
979}
980
981#[cfg(test)]
982mod tests {
983 use super::*;
984 use crate::{build_ui, Container, Runtime, Size, Text};
985
986 #[derive(Clone, Debug, PartialEq)]
987 enum Msg {
988 Close,
989 Delete,
990 }
991
992 fn texts(dialog: &Dialog<Msg>) -> Vec<String> {
993 build_ui(
994 dialog,
995 Size::new(400.0, 800.0),
996 &Runtime::default(),
997 &Theme::default(),
998 )
999 .scene()
1000 .primitives()
1001 .iter()
1002 .filter_map(|p| match p {
1003 frus_core::Primitive::Text { text, .. } => Some(text.clone()),
1004 _ => None,
1005 })
1006 .collect()
1007 }
1008
1009 #[test]
1012 fn a_closed_dialog_is_an_overlay_at_rest() {
1013 let dialog = Dialog::<Msg>::new(false)
1014 .child(Text::new("Question"))
1015 .body(Container::new());
1016 assert_eq!(Widget::<Msg>::anim_target(&dialog), Some(0.0));
1017 }
1018
1019 #[test]
1020 fn an_open_dialog_is_a_centred_overlay_that_the_scrim_closes() {
1021 let dialog = Dialog::<Msg>::new(true)
1022 .on_dismiss(Msg::Close)
1023 .child(Text::new("Question"))
1024 .body(Container::new());
1025 let (_, placement) = Widget::<Msg>::overlay(&dialog).expect("an open dialog is shown");
1026 assert_eq!(placement, Placement::Center);
1027 assert_eq!(Widget::<Msg>::overlay_dismiss(&dialog), Some(Msg::Close));
1028 assert_eq!(Widget::<Msg>::anim_target(&dialog), Some(1.0));
1029 }
1030
1031 #[test]
1036 fn a_dialog_that_must_be_answered_has_an_inert_scrim() {
1037 let dialog = Dialog::<Msg>::new(true)
1038 .child(Text::new("Question"))
1039 .body(Container::new());
1040 assert_eq!(Widget::<Msg>::overlay_dismiss(&dialog), None);
1041 }
1042
1043 #[test]
1046 fn the_surface_is_the_reference_s_box() {
1047 let dialog = Dialog::<Msg>::new(true)
1048 .child(Text::new("."))
1049 .body(Container::new());
1050 let (panel, _) = Widget::<Msg>::overlay(&dialog).expect("shown");
1051 assert_eq!(panel.style().min_width, Dimension::Length(DIALOG_MIN_WIDTH));
1052 assert_eq!(panel.style().margin, DIALOG_INSET_PADDING);
1053 }
1054
1055 #[test]
1062 fn long_actions_at_a_large_text_scale_stack_inside_the_dialog() {
1063 let window = Size::new(400.0, 800.0);
1064 let _surface = crate::MediaQuery::new(window)
1065 .with_text_scaler(2.0)
1066 .install();
1067 let dialog = AlertDialog::<Msg>::new(true)
1068 .title("Delete this task?")
1069 .content("This cannot be undone.")
1070 .action(crate::dsl::button("Cancel", Msg::Close))
1071 .action(crate::dsl::button("Delete permanently", Msg::Delete))
1072 .body(Container::new());
1073 let ui = build_ui(&dialog, window, &Runtime::default(), &Theme::default());
1074 let theme = Theme::default();
1075 let mut surface = None;
1076 let mut buttons = Vec::new();
1077 for p in ui.scene().primitives() {
1078 if let frus_core::Primitive::Rect { rect, color, .. } = p {
1079 if color.r == theme.scheme.primary.r && color.g == theme.scheme.primary.g {
1080 buttons.push(*rect);
1081 } else if rect.width > 200.0 && rect.height > 100.0 && color.a == 1.0 {
1082 surface.get_or_insert(*rect);
1083 }
1084 }
1085 }
1086 let surface = surface.expect("the dialog's surface");
1087 assert!(
1088 surface.x >= 0.0 && surface.x + surface.width <= window.width,
1089 "the surface itself is on the screen: {surface:?}"
1090 );
1091 assert_eq!(buttons.len(), 2, "both buttons: {buttons:?}");
1092 for button in &buttons {
1093 assert!(
1094 button.x >= surface.x && button.x + button.width <= surface.x + surface.width,
1095 "{button:?} is inside {surface:?}"
1096 );
1097 }
1098 assert_ne!(
1099 buttons[0].y, buttons[1].y,
1100 "and they stacked rather than squashed: {buttons:?}"
1101 );
1102 }
1103
1104 #[test]
1106 fn an_alert_dialog_shows_its_title_its_content_and_its_actions() {
1107 let dialog = AlertDialog::<Msg>::new(true)
1108 .title("Delete this task?")
1109 .content("This cannot be undone.")
1110 .action(crate::dsl::button("Cancel", Msg::Close))
1111 .action(crate::dsl::button("Delete", Msg::Delete))
1112 .body(Container::new());
1113 let found = texts(&dialog);
1114 for wanted in [
1115 "Delete this task?",
1116 "This cannot be undone.",
1117 "Cancel",
1118 "Delete",
1119 ] {
1120 assert!(
1121 found.iter().any(|t| t == wanted),
1122 "{wanted:?} is missing from {found:?}"
1123 );
1124 }
1125 }
1126
1127 #[test]
1132 fn the_title_and_the_content_take_the_reference_s_two_roles() {
1133 let dialog = AlertDialog::<Msg>::new(true)
1134 .title("Heading")
1135 .content("Body")
1136 .max_width(340.0)
1137 .body(Container::new());
1138 let ui = build_ui(
1139 &dialog,
1140 Size::new(400.0, 800.0),
1141 &Runtime::default(),
1142 &Theme::default(),
1143 );
1144 let size_of = |wanted: &str| {
1145 ui.scene()
1146 .primitives()
1147 .iter()
1148 .find_map(|p| match p {
1149 frus_core::Primitive::Text { text, size, .. } if text == wanted => Some(*size),
1150 _ => None,
1151 })
1152 .expect("drawn")
1153 };
1154 let theme = Theme::default();
1155 assert_eq!(
1156 size_of("Heading"),
1157 theme.text.headline_small.resolved().size
1158 );
1159 assert_eq!(size_of("Body"), theme.text.body_medium.resolved().size);
1160 assert!(size_of("Heading") > size_of("Body"));
1161 }
1162
1163 #[test]
1166 fn a_simple_dialog_lists_options_that_answer_it() {
1167 let dialog = SimpleDialog::<Msg>::new(true)
1168 .title("Set backup account")
1169 .option(SimpleDialogOption::new("someone@example.com", Msg::Close).build())
1170 .option(SimpleDialogOption::new("Add account", Msg::Delete).build())
1171 .body(Container::new());
1172 let found = texts(&dialog);
1173 for wanted in ["Set backup account", "someone@example.com", "Add account"] {
1174 assert!(
1175 found.iter().any(|t| t == wanted),
1176 "{wanted:?} is missing from {found:?}"
1177 );
1178 }
1179 fn walk(widget: &dyn Widget<Msg>, out: &mut Vec<Msg>) {
1183 if let Some(message) = widget.on_click() {
1184 out.push(message);
1185 }
1186 for child in widget.children() {
1187 walk(child.as_ref(), out);
1188 }
1189 }
1190 let (panel, _) = Widget::<Msg>::overlay(&dialog).expect("an open dialog is shown");
1191 let mut clicks = Vec::new();
1192 walk(panel, &mut clicks);
1193 assert!(
1194 clicks.contains(&Msg::Close) && clicks.contains(&Msg::Delete),
1195 "an option that cannot be chosen is not an option: {clicks:?}"
1196 );
1197 }
1198
1199 #[test]
1203 fn an_option_s_row_reaches_both_edges_of_the_dialog() {
1204 let dialog = SimpleDialog::<Msg>::new(true)
1205 .title("Pick one")
1206 .option(SimpleDialogOption::new("An option", Msg::Close).build())
1207 .body(Container::new());
1208 let ui = build_ui(
1209 &dialog,
1210 Size::new(400.0, 800.0),
1211 &Runtime::default(),
1212 &Theme::default(),
1213 );
1214 let surface = ui
1217 .scene()
1218 .primitives()
1219 .iter()
1220 .find_map(|p| match p {
1221 frus_core::Primitive::Rect { rect, radius, .. } if radius.top_left > 0.0 => {
1222 Some(*rect)
1223 }
1224 _ => None,
1225 })
1226 .expect("the dialog's surface is drawn");
1227 let option = ui
1228 .scene()
1229 .primitives()
1230 .iter()
1231 .find_map(|p| match p {
1232 frus_core::Primitive::Text { text, bounds, .. } if text == "An option" => {
1233 Some(*bounds)
1234 }
1235 _ => None,
1236 })
1237 .expect("the option is drawn");
1238 assert!(
1239 (option.x - (surface.x + OPTION_PADDING.left)).abs() < 0.5,
1240 "the option is inset twice: surface {surface:?}, option {option:?}"
1241 );
1242 }
1243
1244 #[test]
1248 fn an_icon_centres_the_title_under_it() {
1249 let title_of = |icon: bool| {
1250 let mut dialog = AlertDialog::<Msg>::new(true).title("Heading");
1251 if icon {
1252 dialog = dialog.icon(Text::new("!"));
1253 }
1254 let dialog = dialog.body(Container::new());
1255 build_ui(
1256 &dialog,
1257 Size::new(400.0, 800.0),
1258 &Runtime::default(),
1259 &Theme::default(),
1260 )
1261 .scene()
1262 .primitives()
1263 .iter()
1264 .find_map(|p| match p {
1265 frus_core::Primitive::Text {
1266 text,
1267 align,
1268 bounds,
1269 ..
1270 } if text == "Heading" => Some((*align, *bounds)),
1271 _ => None,
1272 })
1273 .expect("the title is drawn")
1274 };
1275 let (with, with_box) = title_of(true);
1276 let (without, without_box) = title_of(false);
1277 assert_eq!(with, TextAlign::Center, "an icon must centre the title");
1278 assert_eq!(
1279 without,
1280 TextAlign::Start,
1281 "without one it starts at the leading edge"
1282 );
1283 let centre = |b: frus_core::Rect| b.x + b.width / 2.0;
1287 assert!(
1288 (centre(with_box) - 200.0).abs() < 1.0,
1289 "the title is not on the dialog's centre line: {with_box:?}"
1290 );
1291 assert!(
1292 centre(without_box) < centre(with_box) - 10.0,
1293 "the title did not move when an icon was put above it: {without_box:?}"
1294 );
1295 }
1296}