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 let blur = depth * 4.0 + 8.0;
186 scene.shadow(
187 Rect::new(
188 bounds.x - blur,
189 bounds.y + depth * 2.0 - blur,
190 bounds.width + 2.0 * blur,
191 bounds.height + 2.0 * blur,
192 ),
193 shadow.fade(o),
194 radius.inflate(blur),
195 blur,
196 );
197 }
198 scene.draw_shape(bounds, shape, self.background(theme).fade(o));
199 }
200
201 fn on_click(&self) -> Option<Msg> {
202 None
203 }
204}
205
206pub struct Dialog<Msg = crate::callback::Callback> {
212 open: bool,
213 on_dismiss: Option<Msg>,
214 content: Option<Box<dyn Widget<Msg>>>,
215 panel: Option<Box<dyn Widget<Msg>>>,
216 children: Vec<Box<dyn Widget<Msg>>>,
217 background: Option<Color>,
218 elevation: Option<f32>,
219 shadow_color: Option<Color>,
220 surface_tint: Option<Color>,
221 shape: Option<ShapeBorder>,
222 inset_padding: Option<Insets>,
223 min_width: Option<f32>,
224 max_width: Option<f32>,
225}
226
227impl<Msg: Clone + 'static> Dialog<Msg> {
228 pub fn new(open: bool) -> Self {
230 Self {
231 open,
232 on_dismiss: None,
233 content: None,
234 panel: None,
235 children: Vec::new(),
236 background: None,
237 elevation: None,
238 shadow_color: None,
239 surface_tint: None,
240 shape: None,
241 inset_padding: None,
242 min_width: None,
243 max_width: None,
244 }
245 }
246
247 #[must_use]
250 pub fn on_dismiss(mut self, message: impl Into<Msg>) -> Self {
251 self.on_dismiss = Some(message.into());
252 self
253 }
254
255 #[must_use]
257 pub fn child(mut self, content: impl Widget<Msg> + 'static) -> Self {
258 self.content = Some(Box::new(content));
259 self
260 }
261
262 #[must_use]
265 pub fn background(mut self, color: Color) -> Self {
266 self.background = Some(color);
267 self
268 }
269
270 #[must_use]
272 pub fn elevation(mut self, elevation: f32) -> Self {
273 self.elevation = Some(elevation);
274 self
275 }
276
277 #[must_use]
280 pub fn shadow_color(mut self, color: Color) -> Self {
281 self.shadow_color = Some(color);
282 self
283 }
284
285 #[must_use]
288 pub fn surface_tint(mut self, color: Color) -> Self {
289 self.surface_tint = Some(color);
290 self
291 }
292
293 #[must_use]
301 pub fn shape(mut self, shape: ShapeBorder) -> Self {
302 self.shape = Some(shape);
303 self
304 }
305
306 #[must_use]
309 pub fn radius(self, radius: impl Into<BorderRadius>) -> Self {
310 self.shape(ShapeBorder::rounded(radius))
311 }
312
313 #[must_use]
316 pub fn inset_padding(mut self, padding: Insets) -> Self {
317 self.inset_padding = Some(padding);
318 self
319 }
320
321 #[must_use]
324 pub fn min_width(mut self, width: f32) -> Self {
325 self.min_width = Some(width);
326 self
327 }
328
329 #[must_use]
331 pub fn max_width(mut self, width: f32) -> Self {
332 self.max_width = Some(width);
333 self
334 }
335
336 #[must_use]
338 pub fn body(mut self, body: impl Widget<Msg> + 'static) -> Self {
339 self.panel = self.content.take().map(|content| {
340 Box::new(DialogSurface {
341 children: vec![content],
342 background: self.background,
343 elevation: self.elevation,
344 shadow_color: self.shadow_color,
345 surface_tint: self.surface_tint,
346 shape: self.shape,
347 inset_padding: self.inset_padding,
348 min_width: self.min_width,
349 max_width: self.max_width,
350 }) as Box<dyn Widget<Msg>>
351 });
352 self.children = vec![Box::new(body)];
353 self
354 }
355}
356
357impl<Msg: Clone> Widget<Msg> for Dialog<Msg> {
358 fn style(&self) -> Style {
359 Style {
360 height: Dimension::Percent(1.0),
361 flex_direction: FlexDirection::Column,
362 ..Default::default()
363 }
364 }
365
366 fn fill_axes(&self, _theme: &Theme) -> crate::widget::FillAxes {
368 crate::widget::FillAxes::WIDTH
369 }
370
371 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
372 &self.children
373 }
374
375 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
376
377 fn on_click(&self) -> Option<Msg> {
378 None
379 }
380
381 fn overlay(&self) -> Option<(&dyn Widget<Msg>, Placement)> {
382 self.panel.as_ref().map(|p| (p.as_ref(), Placement::Center))
383 }
384
385 fn overlay_dismiss(&self) -> Option<Msg> {
386 self.on_dismiss.clone()
387 }
388
389 fn anim_target(&self) -> Option<f32> {
390 Some(if self.open { 1.0 } else { 0.0 })
391 }
392}
393
394pub type ActionsAlignment = Justify;
396
397pub struct AlertDialog<Msg = crate::callback::Callback> {
406 dialog: Dialog<Msg>,
407 icon: Option<Box<dyn Widget<Msg>>>,
408 icon_color: Option<Color>,
409 title: Option<Box<dyn Widget<Msg>>>,
410 title_text_style: Option<TextStyle>,
411 content: Option<Box<dyn Widget<Msg>>>,
412 content_text_style: Option<TextStyle>,
413 actions: Vec<Box<dyn Widget<Msg>>>,
414 actions_alignment: Option<ActionsAlignment>,
415 actions_overflow_alignment: Option<Align>,
416 actions_overflow_direction: Option<VerticalDirection>,
417 actions_overflow_button_spacing: Option<f32>,
418}
419
420impl<Msg: Clone + 'static> AlertDialog<Msg> {
421 pub fn new(open: bool) -> Self {
423 Self {
424 dialog: Dialog::new(open),
425 icon: None,
426 icon_color: None,
427 title: None,
428 title_text_style: None,
429 content: None,
430 content_text_style: None,
431 actions: Vec::new(),
432 actions_alignment: None,
433 actions_overflow_alignment: None,
434 actions_overflow_direction: None,
435 actions_overflow_button_spacing: None,
436 }
437 }
438
439 #[must_use]
442 pub fn on_dismiss(mut self, message: impl Into<Msg>) -> Self {
443 self.dialog = self.dialog.on_dismiss(message);
444 self
445 }
446
447 #[must_use]
449 pub fn icon(mut self, icon: impl Widget<Msg> + 'static) -> Self {
450 self.icon = Some(Box::new(icon));
451 self
452 }
453
454 #[must_use]
456 pub fn icon_color(mut self, color: Color) -> Self {
457 self.icon_color = Some(color);
458 self
459 }
460
461 #[must_use]
463 pub fn title(self, title: impl Into<String>) -> Self {
464 let text = crate::Text::new(title);
465 self.title_widget(text)
466 }
467
468 #[must_use]
470 pub fn title_widget(mut self, title: impl Widget<Msg> + 'static) -> Self {
471 self.title = Some(Box::new(title));
472 self
473 }
474
475 #[must_use]
477 pub fn title_text_style(mut self, style: TextStyle) -> Self {
478 self.title_text_style = Some(style);
479 self
480 }
481
482 #[must_use]
484 pub fn content(self, content: impl Into<String>) -> Self {
485 let text = crate::Text::new(content);
486 self.content_widget(text)
487 }
488
489 #[must_use]
491 pub fn content_widget(mut self, content: impl Widget<Msg> + 'static) -> Self {
492 self.content = Some(Box::new(content));
493 self
494 }
495
496 #[must_use]
498 pub fn content_text_style(mut self, style: TextStyle) -> Self {
499 self.content_text_style = Some(style);
500 self
501 }
502
503 #[must_use]
505 pub fn action(mut self, action: impl Widget<Msg> + 'static) -> Self {
506 self.actions.push(Box::new(action));
507 self
508 }
509
510 #[must_use]
513 pub fn actions_alignment(mut self, alignment: ActionsAlignment) -> Self {
514 self.actions_alignment = Some(alignment);
515 self
516 }
517
518 #[must_use]
523 pub fn actions_overflow_alignment(mut self, alignment: Align) -> Self {
524 self.actions_overflow_alignment = Some(alignment);
525 self
526 }
527
528 #[must_use]
536 pub fn actions_overflow_direction(mut self, direction: VerticalDirection) -> Self {
537 self.actions_overflow_direction = Some(direction);
538 self
539 }
540
541 #[must_use]
545 pub fn actions_overflow_button_spacing(mut self, spacing: f32) -> Self {
546 self.actions_overflow_button_spacing = Some(spacing);
547 self
548 }
549
550 #[must_use]
552 pub fn background(mut self, color: Color) -> Self {
553 self.dialog = self.dialog.background(color);
554 self
555 }
556
557 #[must_use]
559 pub fn elevation(mut self, elevation: f32) -> Self {
560 self.dialog = self.dialog.elevation(elevation);
561 self
562 }
563
564 #[must_use]
566 pub fn shadow_color(mut self, color: Color) -> Self {
567 self.dialog = self.dialog.shadow_color(color);
568 self
569 }
570
571 #[must_use]
573 pub fn surface_tint(mut self, color: Color) -> Self {
574 self.dialog = self.dialog.surface_tint(color);
575 self
576 }
577
578 #[must_use]
580 pub fn shape(mut self, shape: ShapeBorder) -> Self {
581 self.dialog = self.dialog.shape(shape);
582 self
583 }
584
585 #[must_use]
587 pub fn inset_padding(mut self, padding: Insets) -> Self {
588 self.dialog = self.dialog.inset_padding(padding);
589 self
590 }
591
592 #[must_use]
594 pub fn min_width(mut self, width: f32) -> Self {
595 self.dialog = self.dialog.min_width(width);
596 self
597 }
598
599 #[must_use]
601 pub fn max_width(mut self, width: f32) -> Self {
602 self.dialog = self.dialog.max_width(width);
603 self
604 }
605
606 #[must_use]
612 pub fn body(self, body: impl Widget<Msg> + 'static) -> Dialog<Msg> {
613 let AlertDialog {
614 dialog,
615 icon,
616 icon_color,
617 title,
618 title_text_style,
619 content,
620 content_text_style,
621 actions,
622 actions_alignment,
623 actions_overflow_alignment,
624 actions_overflow_direction,
625 actions_overflow_button_spacing,
626 } = self;
627 let has_icon = icon.is_some();
628 let has_title = title.is_some();
629 let has_content = content.is_some();
630 let column = crate::ThemeBuilder::boxed(move |theme| {
631 let mut column = crate::Flex::column().align(Align::Stretch);
635 if let Some(icon) = icon {
636 let below = if has_title {
639 CONTENT_GAP
640 } else if has_content {
641 0.0
642 } else {
643 EDGE
644 };
645 let color = icon_color
646 .or(theme.widgets.dialog.icon_color)
647 .unwrap_or(theme.scheme.secondary);
648 column = column.child(
649 crate::Flex::column()
650 .align(Align::Center)
651 .padding_each(EDGE, EDGE, below, EDGE)
652 .child(crate::Themed::tweak(
653 move |t: &mut Theme| t.widgets.icon.color = Some(color),
654 icon,
655 )),
656 );
657 }
658 if let Some(title) = title {
659 let above = if has_icon { 0.0 } else { EDGE };
660 let below = if has_content { 0.0 } else { TITLE_ONLY_GAP };
661 let align = if has_icon {
662 TextAlign::Center
663 } else {
664 TextAlign::Start
665 };
666 column = column.child(
673 crate::Flex::column()
674 .padding_each(above, EDGE, below, EDGE)
675 .child(
676 crate::Flex::row()
677 .justify(if has_icon {
678 Justify::Center
679 } else {
680 Justify::Start
681 })
682 .child(texted(
683 title_style(title_text_style, Some(theme)),
684 Some(align),
685 title,
686 )),
687 ),
688 );
689 }
690 if let Some(content) = content {
691 let above = if has_icon || has_title {
692 CONTENT_GAP
693 } else {
694 EDGE
695 };
696 column = column.child(
697 crate::Flex::column()
698 .padding_each(above, EDGE, EDGE, EDGE)
699 .child(texted(
700 content_style(content_text_style, Some(theme)),
701 None,
702 content,
703 )),
704 );
705 }
706 if !actions.is_empty() {
707 let mut row = crate::OverflowBar::new()
714 .alignment(actions_alignment.unwrap_or(Justify::End))
715 .spacing(ACTION_GAP)
716 .overflow_alignment(actions_overflow_alignment.unwrap_or(Align::End))
717 .overflow_direction(actions_overflow_direction.unwrap_or_default())
718 .overflow_spacing(actions_overflow_button_spacing.unwrap_or(0.0));
719 for action in actions {
720 row = row.child_boxed(action);
721 }
722 column = column.child(
723 crate::Flex::column()
724 .padding_each(0.0, EDGE, EDGE, EDGE)
725 .child(row),
726 );
727 }
728 Box::new(column) as Box<dyn Widget<Msg>>
729 });
730 dialog.child(column).body(body)
731 }
732}
733
734const SIMPLE_TITLE_PADDING: Insets = Insets {
736 top: 24.0,
737 right: 24.0,
738 bottom: 0.0,
739 left: 24.0,
740};
741const SIMPLE_CONTENT_PADDING: Insets = Insets {
745 top: 12.0,
746 right: 0.0,
747 bottom: 16.0,
748 left: 0.0,
749};
750const OPTION_PADDING: Insets = Insets {
752 top: 8.0,
753 right: 24.0,
754 bottom: 8.0,
755 left: 24.0,
756};
757
758pub struct SimpleDialogOption<Msg = crate::callback::Callback> {
765 child: Option<Box<dyn Widget<Msg>>>,
766 on_press: Option<Msg>,
767 padding: Option<Insets>,
768}
769
770impl<Msg: Clone + 'static> SimpleDialogOption<Msg> {
771 pub fn new(label: impl Into<String>, message: Msg) -> Self {
773 Self {
774 child: Some(Box::new(crate::Text::new(label))),
775 on_press: Some(message),
776 padding: None,
777 }
778 }
779
780 pub fn with_child(child: impl Widget<Msg> + 'static) -> Self {
782 Self {
783 child: Some(Box::new(child)),
784 on_press: None,
785 padding: None,
786 }
787 }
788
789 #[must_use]
792 pub fn on_press(mut self, message: impl Into<Msg>) -> Self {
793 self.on_press = Some(message.into());
794 self
795 }
796
797 #[must_use]
799 pub fn padding(mut self, padding: Insets) -> Self {
800 self.padding = Some(padding);
801 self
802 }
803
804 pub fn build(self) -> Box<dyn Widget<Msg>> {
806 let padding = self.padding.unwrap_or(OPTION_PADDING);
807 let mut row = crate::Flex::row().align(Align::Center).padding_each(
808 padding.top,
809 padding.right,
810 padding.bottom,
811 padding.left,
812 );
813 if let Some(child) = self.child {
814 row = row.child_boxed(child);
815 }
816 let mut ink = crate::InkWell::new();
817 if let Some(message) = self.on_press {
818 ink = ink.on_click(message);
819 }
820 Box::new(ink.child(row))
821 }
822}
823
824pub struct SimpleDialog<Msg = crate::callback::Callback> {
830 dialog: Dialog<Msg>,
831 title: Option<Box<dyn Widget<Msg>>>,
832 title_text_style: Option<TextStyle>,
833 title_padding: Option<Insets>,
834 content_padding: Option<Insets>,
835 content_text_style: Option<TextStyle>,
836 options: Vec<Box<dyn Widget<Msg>>>,
837}
838
839impl<Msg: Clone + 'static> SimpleDialog<Msg> {
840 pub fn new(open: bool) -> Self {
842 Self {
843 dialog: Dialog::new(open),
844 title: None,
845 title_text_style: None,
846 title_padding: None,
847 content_padding: None,
848 content_text_style: None,
849 options: Vec::new(),
850 }
851 }
852
853 #[must_use]
855 pub fn on_dismiss(mut self, message: impl Into<Msg>) -> Self {
856 self.dialog = self.dialog.on_dismiss(message);
857 self
858 }
859
860 #[must_use]
862 pub fn title(self, title: impl Into<String>) -> Self {
863 let text = crate::Text::new(title);
864 self.title_widget(text)
865 }
866
867 #[must_use]
869 pub fn title_widget(mut self, title: impl Widget<Msg> + 'static) -> Self {
870 self.title = Some(Box::new(title));
871 self
872 }
873
874 #[must_use]
876 pub fn title_text_style(mut self, style: TextStyle) -> Self {
877 self.title_text_style = Some(style);
878 self
879 }
880
881 #[must_use]
883 pub fn title_padding(mut self, padding: Insets) -> Self {
884 self.title_padding = Some(padding);
885 self
886 }
887
888 #[must_use]
891 pub fn content_padding(mut self, padding: Insets) -> Self {
892 self.content_padding = Some(padding);
893 self
894 }
895
896 #[must_use]
898 pub fn content_text_style(mut self, style: TextStyle) -> Self {
899 self.content_text_style = Some(style);
900 self
901 }
902
903 #[must_use]
905 pub fn option(mut self, option: impl Widget<Msg> + 'static) -> Self {
906 self.options.push(Box::new(option));
907 self
908 }
909
910 #[must_use]
912 pub fn background(mut self, color: Color) -> Self {
913 self.dialog = self.dialog.background(color);
914 self
915 }
916
917 #[must_use]
919 pub fn elevation(mut self, elevation: f32) -> Self {
920 self.dialog = self.dialog.elevation(elevation);
921 self
922 }
923
924 #[must_use]
926 pub fn shape(mut self, shape: ShapeBorder) -> Self {
927 self.dialog = self.dialog.shape(shape);
928 self
929 }
930
931 #[must_use]
933 pub fn inset_padding(mut self, padding: Insets) -> Self {
934 self.dialog = self.dialog.inset_padding(padding);
935 self
936 }
937
938 #[must_use]
940 pub fn body(self, body: impl Widget<Msg> + 'static) -> Dialog<Msg> {
941 let SimpleDialog {
942 dialog,
943 title,
944 title_text_style,
945 title_padding,
946 content_padding,
947 content_text_style,
948 options,
949 } = self;
950 let has_options = !options.is_empty();
951 let column = crate::ThemeBuilder::boxed(move |theme| {
952 let mut column = crate::Flex::column().align(Align::Stretch);
953 if let Some(title) = title {
954 let pad = title_padding.unwrap_or(SIMPLE_TITLE_PADDING);
955 let bottom = if has_options { 0.0 } else { pad.bottom };
959 column = column.child(
960 crate::Flex::column()
961 .padding_each(pad.top, pad.right, bottom, pad.left)
962 .child(texted(
963 title_style(title_text_style, Some(theme)),
964 None,
965 title,
966 )),
967 );
968 }
969 if has_options {
970 let pad = content_padding.unwrap_or(SIMPLE_CONTENT_PADDING);
971 let mut list = crate::Flex::column().align(Align::Stretch);
972 for option in options {
973 list = list.child_boxed(option);
974 }
975 column = column.child(
976 crate::Flex::column()
977 .padding_each(pad.top, pad.right, pad.bottom, pad.left)
978 .child(texted(
979 content_style(content_text_style, Some(theme)),
980 None,
981 Box::new(list),
982 )),
983 );
984 }
985 Box::new(column) as Box<dyn Widget<Msg>>
986 });
987 dialog.child(column).body(body)
988 }
989}
990
991#[cfg(test)]
992mod tests {
993 use super::*;
994 use crate::{build_ui, Container, Runtime, Size, Text};
995
996 #[derive(Clone, Debug, PartialEq)]
997 enum Msg {
998 Close,
999 Delete,
1000 }
1001
1002 fn texts(dialog: &Dialog<Msg>) -> Vec<String> {
1003 build_ui(
1004 dialog,
1005 Size::new(400.0, 800.0),
1006 &Runtime::default(),
1007 &Theme::default(),
1008 )
1009 .scene()
1010 .primitives()
1011 .iter()
1012 .filter_map(|p| match p {
1013 frus_core::Primitive::Text { text, .. } => Some(text.clone()),
1014 _ => None,
1015 })
1016 .collect()
1017 }
1018
1019 #[test]
1022 fn a_closed_dialog_is_an_overlay_at_rest() {
1023 let dialog = Dialog::<Msg>::new(false)
1024 .child(Text::new("Question"))
1025 .body(Container::new());
1026 assert_eq!(Widget::<Msg>::anim_target(&dialog), Some(0.0));
1027 }
1028
1029 #[test]
1030 fn an_open_dialog_is_a_centred_overlay_that_the_scrim_closes() {
1031 let dialog = Dialog::<Msg>::new(true)
1032 .on_dismiss(Msg::Close)
1033 .child(Text::new("Question"))
1034 .body(Container::new());
1035 let (_, placement) = Widget::<Msg>::overlay(&dialog).expect("an open dialog is shown");
1036 assert_eq!(placement, Placement::Center);
1037 assert_eq!(Widget::<Msg>::overlay_dismiss(&dialog), Some(Msg::Close));
1038 assert_eq!(Widget::<Msg>::anim_target(&dialog), Some(1.0));
1039 }
1040
1041 #[test]
1046 fn a_dialog_that_must_be_answered_has_an_inert_scrim() {
1047 let dialog = Dialog::<Msg>::new(true)
1048 .child(Text::new("Question"))
1049 .body(Container::new());
1050 assert_eq!(Widget::<Msg>::overlay_dismiss(&dialog), None);
1051 }
1052
1053 #[test]
1056 fn the_surface_is_the_reference_s_box() {
1057 let dialog = Dialog::<Msg>::new(true)
1058 .child(Text::new("."))
1059 .body(Container::new());
1060 let (panel, _) = Widget::<Msg>::overlay(&dialog).expect("shown");
1061 assert_eq!(panel.style().min_width, Dimension::Length(DIALOG_MIN_WIDTH));
1062 assert_eq!(panel.style().margin, DIALOG_INSET_PADDING);
1063 }
1064
1065 #[test]
1072 fn long_actions_at_a_large_text_scale_stack_inside_the_dialog() {
1073 let window = Size::new(400.0, 800.0);
1074 let _surface = crate::MediaQuery::new(window)
1075 .with_text_scaler(2.0)
1076 .install();
1077 let dialog = AlertDialog::<Msg>::new(true)
1078 .title("Delete this task?")
1079 .content("This cannot be undone.")
1080 .action(crate::dsl::button("Cancel", Msg::Close))
1081 .action(crate::dsl::button("Delete permanently", Msg::Delete))
1082 .body(Container::new());
1083 let ui = build_ui(&dialog, window, &Runtime::default(), &Theme::default());
1084 let theme = Theme::default();
1085 let mut surface = None;
1086 let mut buttons = Vec::new();
1087 for p in ui.scene().primitives() {
1088 if let frus_core::Primitive::Rect { rect, color, .. } = p {
1089 if color.r == theme.scheme.primary.r && color.g == theme.scheme.primary.g {
1090 buttons.push(*rect);
1091 } else if rect.width > 200.0 && rect.height > 100.0 && color.a == 1.0 {
1092 surface.get_or_insert(*rect);
1093 }
1094 }
1095 }
1096 let surface = surface.expect("the dialog's surface");
1097 assert!(
1098 surface.x >= 0.0 && surface.x + surface.width <= window.width,
1099 "the surface itself is on the screen: {surface:?}"
1100 );
1101 assert_eq!(buttons.len(), 2, "both buttons: {buttons:?}");
1102 for button in &buttons {
1103 assert!(
1104 button.x >= surface.x && button.x + button.width <= surface.x + surface.width,
1105 "{button:?} is inside {surface:?}"
1106 );
1107 }
1108 assert_ne!(
1109 buttons[0].y, buttons[1].y,
1110 "and they stacked rather than squashed: {buttons:?}"
1111 );
1112 }
1113
1114 #[test]
1116 fn an_alert_dialog_shows_its_title_its_content_and_its_actions() {
1117 let dialog = AlertDialog::<Msg>::new(true)
1118 .title("Delete this task?")
1119 .content("This cannot be undone.")
1120 .action(crate::dsl::button("Cancel", Msg::Close))
1121 .action(crate::dsl::button("Delete", Msg::Delete))
1122 .body(Container::new());
1123 let found = texts(&dialog);
1124 for wanted in [
1125 "Delete this task?",
1126 "This cannot be undone.",
1127 "Cancel",
1128 "Delete",
1129 ] {
1130 assert!(
1131 found.iter().any(|t| t == wanted),
1132 "{wanted:?} is missing from {found:?}"
1133 );
1134 }
1135 }
1136
1137 #[test]
1142 fn the_title_and_the_content_take_the_reference_s_two_roles() {
1143 let dialog = AlertDialog::<Msg>::new(true)
1144 .title("Heading")
1145 .content("Body")
1146 .max_width(340.0)
1147 .body(Container::new());
1148 let ui = build_ui(
1149 &dialog,
1150 Size::new(400.0, 800.0),
1151 &Runtime::default(),
1152 &Theme::default(),
1153 );
1154 let size_of = |wanted: &str| {
1155 ui.scene()
1156 .primitives()
1157 .iter()
1158 .find_map(|p| match p {
1159 frus_core::Primitive::Text { text, size, .. } if text == wanted => Some(*size),
1160 _ => None,
1161 })
1162 .expect("drawn")
1163 };
1164 let theme = Theme::default();
1165 assert_eq!(
1166 size_of("Heading"),
1167 theme.text.headline_small.resolved().size
1168 );
1169 assert_eq!(size_of("Body"), theme.text.body_medium.resolved().size);
1170 assert!(size_of("Heading") > size_of("Body"));
1171 }
1172
1173 #[test]
1176 fn a_simple_dialog_lists_options_that_answer_it() {
1177 let dialog = SimpleDialog::<Msg>::new(true)
1178 .title("Set backup account")
1179 .option(SimpleDialogOption::new("someone@example.com", Msg::Close).build())
1180 .option(SimpleDialogOption::new("Add account", Msg::Delete).build())
1181 .body(Container::new());
1182 let found = texts(&dialog);
1183 for wanted in ["Set backup account", "someone@example.com", "Add account"] {
1184 assert!(
1185 found.iter().any(|t| t == wanted),
1186 "{wanted:?} is missing from {found:?}"
1187 );
1188 }
1189 fn walk(widget: &dyn Widget<Msg>, out: &mut Vec<Msg>) {
1193 if let Some(message) = widget.on_click() {
1194 out.push(message);
1195 }
1196 for child in widget.children() {
1197 walk(child.as_ref(), out);
1198 }
1199 }
1200 let (panel, _) = Widget::<Msg>::overlay(&dialog).expect("an open dialog is shown");
1201 let mut clicks = Vec::new();
1202 walk(panel, &mut clicks);
1203 assert!(
1204 clicks.contains(&Msg::Close) && clicks.contains(&Msg::Delete),
1205 "an option that cannot be chosen is not an option: {clicks:?}"
1206 );
1207 }
1208
1209 #[test]
1213 fn an_option_s_row_reaches_both_edges_of_the_dialog() {
1214 let dialog = SimpleDialog::<Msg>::new(true)
1215 .title("Pick one")
1216 .option(SimpleDialogOption::new("An option", Msg::Close).build())
1217 .body(Container::new());
1218 let ui = build_ui(
1219 &dialog,
1220 Size::new(400.0, 800.0),
1221 &Runtime::default(),
1222 &Theme::default(),
1223 );
1224 let surface = ui
1227 .scene()
1228 .primitives()
1229 .iter()
1230 .find_map(|p| match p {
1231 frus_core::Primitive::Rect { rect, radius, .. } if radius.top_left > 0.0 => {
1232 Some(*rect)
1233 }
1234 _ => None,
1235 })
1236 .expect("the dialog's surface is drawn");
1237 let option = ui
1238 .scene()
1239 .primitives()
1240 .iter()
1241 .find_map(|p| match p {
1242 frus_core::Primitive::Text { text, bounds, .. } if text == "An option" => {
1243 Some(*bounds)
1244 }
1245 _ => None,
1246 })
1247 .expect("the option is drawn");
1248 assert!(
1249 (option.x - (surface.x + OPTION_PADDING.left)).abs() < 0.5,
1250 "the option is inset twice: surface {surface:?}, option {option:?}"
1251 );
1252 }
1253
1254 #[test]
1258 fn an_icon_centres_the_title_under_it() {
1259 let title_of = |icon: bool| {
1260 let mut dialog = AlertDialog::<Msg>::new(true).title("Heading");
1261 if icon {
1262 dialog = dialog.icon(Text::new("!"));
1263 }
1264 let dialog = dialog.body(Container::new());
1265 build_ui(
1266 &dialog,
1267 Size::new(400.0, 800.0),
1268 &Runtime::default(),
1269 &Theme::default(),
1270 )
1271 .scene()
1272 .primitives()
1273 .iter()
1274 .find_map(|p| match p {
1275 frus_core::Primitive::Text {
1276 text,
1277 align,
1278 bounds,
1279 ..
1280 } if text == "Heading" => Some((*align, *bounds)),
1281 _ => None,
1282 })
1283 .expect("the title is drawn")
1284 };
1285 let (with, with_box) = title_of(true);
1286 let (without, without_box) = title_of(false);
1287 assert_eq!(with, TextAlign::Center, "an icon must centre the title");
1288 assert_eq!(
1289 without,
1290 TextAlign::Start,
1291 "without one it starts at the leading edge"
1292 );
1293 let centre = |b: frus_core::Rect| b.x + b.width / 2.0;
1297 assert!(
1298 (centre(with_box) - 200.0).abs() < 1.0,
1299 "the title is not on the dialog's centre line: {with_box:?}"
1300 );
1301 assert!(
1302 centre(without_box) < centre(with_box) - 10.0,
1303 "the title did not move when an icon was put above it: {without_box:?}"
1304 );
1305 }
1306}