1use std::rc::Rc;
103
104use frust::authoring::text::{FontWeight, LineHeight, TextOverflow};
105use frust::authoring::{
106 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
107 LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, ThemeTextColor, ThemeTextType, View, ViewSeq,
108 Widget, any, build_child, rebuild_child, rebuild_children, route_event, route_event_single,
109 teardown_child, visit_children,
110};
111use frust::{Color, NavigatorController, PopResult, Theme, icon, text};
112use kurbo::{Point, Size};
113
114use crate::button::{button, text_button};
115use crate::checkbox::checkbox;
116use crate::divider::divider;
117use crate::icon_button::icon_button;
118use crate::overlay::{
119 ModalDismiss, OVERLAY_DIALOG_MAX_WIDTH, OverlayCorners, OverlayElevation, OverlayExtent,
120 OverlayLimit, OverlayModalConfig, OverlayModalContent, OverlayModalView, OverlayModalWidget,
121 OverlaySide, overlay_modal, show_overlay_modal,
122};
123use crate::radio::radio;
124
125const DIALOG_PADDING: f64 = 24.0;
127const ACTION_GAP: f64 = 8.0;
129const SELECTION_ITEM_HEIGHT: f64 = 56.0;
132const SELECTION_LIST_INSET: f64 = DIALOG_PADDING / 2.0;
136const CHECKBOX_LABEL_GAP: f64 = 8.0;
140const FULL_SCREEN_HEADER_HEIGHT: f64 = 64.0;
143const HEADER_EDGE_GAP: f64 = 4.0;
146const HEADER_ACTION_GAP: f64 = 16.0;
149const DEFAULT_CANCEL_LABEL: &str = "Cancel";
151const DEFAULT_CONFIRM_LABEL: &str = "OK";
153const CLOSE_SEMANTIC_LABEL: &str = "Close";
155
156const TITLE_SIZE: f32 = 24.0;
164const TITLE_LINE_HEIGHT: f32 = 32.0;
165const TITLE_WEIGHT: FontWeight = FontWeight::MEDIUM;
166const BODY_SIZE: f32 = 14.0;
168const BODY_LINE_HEIGHT: f32 = 20.0;
169const HEADER_TITLE_SIZE: f32 = 22.0;
174const HEADER_TITLE_LINE_HEIGHT: f32 = 28.0;
175
176const FALLBACK_SURFACE: Color = Color::from_rgb8(0xFE, 0xF7, 0xFF);
179
180fn title_view<State: 'static>(s: &str) -> AnyView<State> {
184 any::<State, _>(
185 text(s.to_string())
186 .size(TITLE_SIZE)
187 .weight(TITLE_WEIGHT)
188 .line_height(LineHeight::Absolute(TITLE_LINE_HEIGHT))
189 .themed_family(ThemeTextType::HeadlineSmallEmphasized),
190 )
191}
192
193fn body_view<State: 'static>(s: &str) -> AnyView<State> {
196 any::<State, _>(
197 text(s.to_string())
198 .size(BODY_SIZE)
199 .line_height(LineHeight::Absolute(BODY_LINE_HEIGHT))
200 .themed_role(ThemeTextColor::OnSurfaceVariant)
201 .themed_family(ThemeTextType::BodyMedium),
202 )
203}
204
205fn divider_view<State: 'static>() -> AnyView<State> {
207 any::<State, _>(divider())
208}
209
210fn resolve_surface(theme: Option<&Theme>) -> Color {
213 theme.map_or(FALLBACK_SURFACE, |t| t.scheme().surface)
214}
215
216type SharedAction<State> = Rc<AnyView<State>>;
229
230struct DialogPanelView<State: 'static> {
232 title: Option<String>,
233 content: Option<SharedAction<State>>,
234 actions: Vec<SharedAction<State>>,
235 top_divider: bool,
236 bottom_divider: bool,
237 content_full_bleed: bool,
242}
243
244struct DialogPanelWidget {
246 title: Option<ChildPod>,
247 content: Option<ChildPod>,
248 actions: Vec<ChildPod>,
249 top_divider_pod: ChildPod,
250 bottom_divider_pod: ChildPod,
251 top_divider: bool,
252 bottom_divider: bool,
253 has_content: bool,
254 has_actions: bool,
255 content_full_bleed: bool,
256}
257
258fn reconcile_text<State: 'static>(
260 prev: Option<&String>,
261 next: Option<&String>,
262 pod: &mut Option<ChildPod>,
263 make: impl Fn(&str) -> AnyView<State>,
264 ctx: &mut BuildCtx<'_>,
265) -> ChangeFlags {
266 match (prev, next) {
267 (None, Some(s)) => {
268 *pod = Some(build_child(&make(s.as_str()), ctx));
269 ChangeFlags::LAYOUT | ChangeFlags::PAINT
270 }
271 (Some(s), None) => {
272 if let Some(p) = pod.as_mut() {
273 teardown_child(&make(s.as_str()), p, ctx);
274 }
275 *pod = None;
276 ChangeFlags::LAYOUT | ChangeFlags::PAINT
277 }
278 (Some(a), Some(b)) => match pod.as_mut() {
279 Some(p) => rebuild_child(&make(a.as_str()), &make(b.as_str()), p, ctx),
280 None => ChangeFlags::NONE,
281 },
282 (None, None) => ChangeFlags::NONE,
283 }
284}
285
286fn reconcile_any<State: 'static>(
289 prev: Option<&SharedAction<State>>,
290 next: Option<&SharedAction<State>>,
291 pod: &mut Option<ChildPod>,
292 ctx: &mut BuildCtx<'_>,
293) -> ChangeFlags {
294 match (prev, next) {
295 (None, Some(v)) => {
296 *pod = Some(build_child(v.as_ref(), ctx));
297 ChangeFlags::LAYOUT | ChangeFlags::PAINT
298 }
299 (Some(v), None) => {
300 if let Some(p) = pod.as_mut() {
301 teardown_child(v.as_ref(), p, ctx);
302 }
303 *pod = None;
304 ChangeFlags::LAYOUT | ChangeFlags::PAINT
305 }
306 (Some(a), Some(b)) => match pod.as_mut() {
307 Some(p) => rebuild_child(a.as_ref(), b.as_ref(), p, ctx),
308 None => ChangeFlags::NONE,
309 },
310 (None, None) => ChangeFlags::NONE,
311 }
312}
313
314impl<State: 'static> View<State> for DialogPanelView<State> {
315 type Element = DialogPanelWidget;
316
317 fn build(&self, ctx: &mut BuildCtx<'_>) -> DialogPanelWidget {
318 DialogPanelWidget {
319 title: self
320 .title
321 .as_ref()
322 .map(|s| build_child(&title_view::<State>(s), ctx)),
323 content: self.content.as_ref().map(|c| build_child(c.as_ref(), ctx)),
324 actions: self
325 .actions
326 .iter()
327 .map(|a| build_child(a.as_ref(), ctx))
328 .collect(),
329 top_divider_pod: build_child(÷r_view::<State>(), ctx),
330 bottom_divider_pod: build_child(÷r_view::<State>(), ctx),
331 top_divider: self.top_divider,
332 bottom_divider: self.bottom_divider,
333 has_content: self.content.is_some(),
334 has_actions: !self.actions.is_empty(),
335 content_full_bleed: self.content_full_bleed,
336 }
337 }
338
339 fn rebuild(
340 &self,
341 prev: &Self,
342 element: &mut DialogPanelWidget,
343 ctx: &mut BuildCtx<'_>,
344 ) -> ChangeFlags {
345 let mut flags = reconcile_text(
346 prev.title.as_ref(),
347 self.title.as_ref(),
348 &mut element.title,
349 title_view::<State>,
350 ctx,
351 );
352 flags |= reconcile_any(
353 prev.content.as_ref(),
354 self.content.as_ref(),
355 &mut element.content,
356 ctx,
357 );
358
359 let prev_actions: Vec<&AnyView<State>> = prev.actions.iter().map(|a| a.as_ref()).collect();
360 let next_actions: Vec<&AnyView<State>> = self.actions.iter().map(|a| a.as_ref()).collect();
361 flags |= rebuild_children(
362 &prev_actions,
363 &next_actions,
364 &mut element.actions,
365 ctx,
366 |view: &&AnyView<State>| *view,
367 |_| None,
368 );
369
370 flags |= rebuild_child(
371 ÷r_view::<State>(),
372 ÷r_view::<State>(),
373 &mut element.top_divider_pod,
374 ctx,
375 );
376 flags |= rebuild_child(
377 ÷r_view::<State>(),
378 ÷r_view::<State>(),
379 &mut element.bottom_divider_pod,
380 ctx,
381 );
382
383 if element.top_divider != self.top_divider {
384 element.top_divider = self.top_divider;
385 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
386 }
387 if element.bottom_divider != self.bottom_divider {
388 element.bottom_divider = self.bottom_divider;
389 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
390 }
391 if element.content_full_bleed != self.content_full_bleed {
392 element.content_full_bleed = self.content_full_bleed;
393 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
394 }
395 element.has_content = self.content.is_some();
396 element.has_actions = !self.actions.is_empty();
397 flags
398 }
399
400 fn teardown(&self, element: &mut DialogPanelWidget, ctx: &mut BuildCtx<'_>) {
401 if let (Some(s), Some(p)) = (&self.title, element.title.as_mut()) {
402 teardown_child(&title_view::<State>(s), p, ctx);
403 }
404 if let (Some(v), Some(p)) = (&self.content, element.content.as_mut()) {
405 teardown_child(v.as_ref(), p, ctx);
406 }
407 for (view, pod) in self.actions.iter().zip(element.actions.iter_mut()) {
408 teardown_child(view.as_ref(), pod, ctx);
409 }
410 teardown_child(÷r_view::<State>(), &mut element.top_divider_pod, ctx);
411 teardown_child(
412 ÷r_view::<State>(),
413 &mut element.bottom_divider_pod,
414 ctx,
415 );
416 }
417}
418
419impl Widget for DialogPanelWidget {
420 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
421 let width = bc.max().width.max(bc.min().width);
424 let inner_w = (width - 2.0 * DIALOG_PADDING).max(0.0);
425 let loose = BoxConstraints::loose(Size::new(inner_w, f64::INFINITY));
426
427 let mut y = if self.title.is_some() {
431 0.0
432 } else {
433 DIALOG_PADDING
434 };
435
436 if let Some(p) = self.title.as_mut() {
437 y += DIALOG_PADDING;
438 let s = p.layout_child(ctx, &loose);
439 p.set_origin(Point::new(DIALOG_PADDING, y));
440 y += s.height;
441 y += DIALOG_PADDING;
442 }
443
444 let has_below = self.has_content || self.has_actions;
445 if self.top_divider && has_below {
446 self.top_divider_pod
447 .layout_child(ctx, &BoxConstraints::tight(Size::new(width, 1.0)));
448 self.top_divider_pod.set_origin(Point::new(0.0, y));
449 y += self.top_divider_pod.size().height;
450 }
451
452 if let Some(p) = self.content.as_mut() {
453 let (pad_lr, pad_tb) = if self.content_full_bleed {
454 (0.0, 0.0)
455 } else if self.top_divider && self.bottom_divider {
456 (DIALOG_PADDING, DIALOG_PADDING)
457 } else {
458 (DIALOG_PADDING, 0.0)
459 };
460 let content_w = (width - 2.0 * pad_lr).max(0.0);
461 y += pad_tb;
462 let s = p.layout_child(
463 ctx,
464 &BoxConstraints::new(
465 Size::new(content_w, 0.0),
466 Size::new(content_w, f64::INFINITY),
467 ),
468 );
469 p.set_origin(Point::new(pad_lr, y));
470 y += s.height;
471 y += pad_tb;
472 }
473
474 if self.bottom_divider && self.has_actions {
475 self.bottom_divider_pod
476 .layout_child(ctx, &BoxConstraints::tight(Size::new(width, 1.0)));
477 self.bottom_divider_pod.set_origin(Point::new(0.0, y));
478 y += self.bottom_divider_pod.size().height;
479 }
480
481 if self.has_actions {
482 y += DIALOG_PADDING;
483 let mut actions_w = 0.0;
484 let mut actions_h: f64 = 0.0;
485 for (i, pod) in self.actions.iter_mut().enumerate() {
486 let s = pod.layout_child(ctx, &loose);
487 if i > 0 {
488 actions_w += ACTION_GAP;
489 }
490 actions_w += s.width;
491 actions_h = actions_h.max(s.height);
492 }
493 let actions_y = y;
494 let mut ax = width - DIALOG_PADDING;
495 for pod in self.actions.iter_mut().rev() {
496 let w = pod.size().width;
497 ax -= w;
498 pod.set_origin(Point::new(ax, actions_y));
499 ax -= ACTION_GAP;
500 }
501 let _ = actions_w;
502 y += actions_h;
503 y += DIALOG_PADDING;
504 }
505
506 bc.constrain(Size::new(width, y))
507 }
508
509 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
510 if let Some(p) = self.title.as_mut() {
511 p.paint_child(ctx, scene);
512 }
513 let has_below = self.has_content || self.has_actions;
514 if self.top_divider && has_below {
515 self.top_divider_pod.paint_child(ctx, scene);
516 }
517 if let Some(p) = self.content.as_mut() {
518 p.paint_child(ctx, scene);
519 }
520 if self.bottom_divider && self.has_actions {
521 self.bottom_divider_pod.paint_child(ctx, scene);
522 }
523 for pod in &mut self.actions {
524 pod.paint_child(ctx, scene);
525 }
526 }
527
528 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
529 if let Some(content) = self.content.as_mut()
530 && route_event_single(content, ctx, event) == EventResult::Handled
531 {
532 return EventResult::Handled;
533 }
534 if !self.actions.is_empty()
535 && route_event(&mut self.actions, ctx, event) == EventResult::Handled
536 {
537 return EventResult::Handled;
538 }
539 EventResult::Ignored
540 }
541
542 fn semantics(&self, ctx: &mut SemanticsCtx) {
543 if let Some(p) = &self.title {
544 p.semantics_child(ctx);
545 }
546 if let Some(p) = &self.content {
547 p.semantics_child(ctx);
548 }
549 for pod in &self.actions {
550 pod.semantics_child(ctx);
551 }
552 }
553
554 visit_children!(title, content, actions, top_divider_pod, bottom_divider_pod);
555}
556
557type OnDismiss<State> = Rc<dyn Fn(&mut State)>;
563
564pub struct DialogView<State: 'static> {
567 title: Option<String>,
568 body: Option<String>,
569 actions: Vec<SharedAction<State>>,
570 dismissable: bool,
571 on_dismiss: Option<OnDismiss<State>>,
572 dismiss_handle: Option<ModalDismiss>,
573}
574
575pub fn dialog<State: 'static>() -> DialogView<State> {
580 DialogView {
581 title: None,
582 body: None,
583 actions: Vec::new(),
584 dismissable: true,
585 on_dismiss: None,
586 dismiss_handle: None,
587 }
588}
589
590impl<State: 'static> DialogView<State> {
591 pub fn title(mut self, title: impl Into<String>) -> Self {
593 self.title = Some(title.into());
594 self
595 }
596
597 pub fn body(mut self, body: impl Into<String>) -> Self {
599 self.body = Some(body.into());
600 self
601 }
602
603 pub fn actions<M>(mut self, actions: impl ViewSeq<State, M>) -> Self {
606 let mut erased = Vec::new();
607 actions.extend_views(&mut erased);
608 self.actions = erased.into_iter().map(Rc::new).collect();
609 self
610 }
611
612 pub fn action(mut self, action: impl View<State>) -> Self {
614 self.actions.push(Rc::new(AnyView::new(action)));
615 self
616 }
617
618 pub fn dismissable(mut self, dismissable: bool) -> Self {
623 self.dismissable = dismissable;
624 self
625 }
626
627 pub fn on_dismiss<F: Fn(&mut State) + 'static>(mut self, on_dismiss: F) -> Self {
632 self.on_dismiss = Some(Rc::new(on_dismiss));
633 self
634 }
635
636 pub fn dismiss_handle(mut self, dismiss: ModalDismiss) -> Self {
643 self.dismiss_handle = Some(dismiss);
644 self
645 }
646
647 fn compose(&self) -> OverlayModalView<State> {
651 let panel = DialogPanelView {
652 title: self.title.clone(),
653 content: self.body.as_ref().map(|s| Rc::new(body_view::<State>(s))),
654 actions: self.actions.clone(),
655 top_divider: false,
656 bottom_divider: false,
657 content_full_bleed: false,
658 };
659 let mut view = overlay_modal(
660 panel,
661 OverlayModalConfig::centered(OVERLAY_DIALOG_MAX_WIDTH),
662 )
663 .dismissable(self.dismissable);
664 if let Some(title) = &self.title {
665 view = view.label(title.clone());
666 }
667 if let Some(on_dismiss) = &self.on_dismiss {
668 let on_dismiss = on_dismiss.clone();
669 view = view.on_dismiss(move |state: &mut State| on_dismiss(state));
670 }
671 if let Some(dismiss) = &self.dismiss_handle {
672 view = view.dismiss_handle(dismiss.clone());
673 }
674 view
675 }
676}
677
678impl<State: 'static> View<State> for DialogView<State> {
679 type Element = OverlayModalWidget;
680
681 fn build(&self, ctx: &mut BuildCtx<'_>) -> OverlayModalWidget {
682 View::build(&self.compose(), ctx)
683 }
684
685 fn rebuild(
686 &self,
687 prev: &Self,
688 element: &mut OverlayModalWidget,
689 ctx: &mut BuildCtx<'_>,
690 ) -> ChangeFlags {
691 View::rebuild(&self.compose(), &prev.compose(), element, ctx)
692 }
693
694 fn teardown(&self, element: &mut OverlayModalWidget, ctx: &mut BuildCtx<'_>) {
695 View::teardown(&self.compose(), element, ctx);
696 }
697}
698
699impl<State: 'static> OverlayModalContent<State> for DialogView<State> {
700 fn on_modal_dismiss(mut self, on_dismiss: Rc<dyn Fn(&mut State)>) -> Self {
701 self.on_dismiss = Some(on_dismiss);
702 self
703 }
704
705 fn modal_dismissable(&self) -> bool {
706 self.dismissable
707 }
708}
709
710pub fn show_dialog<State, B, R>(controller: &NavigatorController<State>, build: B, on_result: R)
728where
729 State: 'static,
730 B: Fn() -> DialogView<State> + 'static,
731 R: Fn(&mut State, PopResult) + 'static,
732{
733 show_overlay_modal(controller, build, on_result);
734}
735
736type OnToggle<State> = Rc<dyn Fn(&mut State, String)>;
743
744pub struct SelectionDialogView<State: 'static> {
748 title: String,
749 options: Vec<String>,
750 multi_select: bool,
751 selected: Vec<String>,
752 on_toggle: Option<OnToggle<State>>,
753 cancel_label: String,
754 confirm_label: String,
755 on_confirm: Option<OnDismiss<State>>,
756 dismissable: bool,
757 on_dismiss: Option<OnDismiss<State>>,
758 dismiss_handle: Option<ModalDismiss>,
759}
760
761pub fn selection_dialog<State: 'static>(
764 title: impl Into<String>,
765 options: Vec<String>,
766) -> SelectionDialogView<State> {
767 SelectionDialogView {
768 title: title.into(),
769 options,
770 multi_select: false,
771 selected: Vec::new(),
772 on_toggle: None,
773 cancel_label: DEFAULT_CANCEL_LABEL.to_string(),
774 confirm_label: DEFAULT_CONFIRM_LABEL.to_string(),
775 on_confirm: None,
776 dismissable: true,
777 on_dismiss: None,
778 dismiss_handle: None,
779 }
780}
781
782impl<State: 'static> SelectionDialogView<State> {
783 pub fn multi_select(mut self, multi_select: bool) -> Self {
786 self.multi_select = multi_select;
787 self
788 }
789
790 pub fn selected(mut self, selected: Vec<String>) -> Self {
793 self.selected = selected;
794 self
795 }
796
797 pub fn on_toggle<F: Fn(&mut State, String) + 'static>(mut self, on_toggle: F) -> Self {
802 self.on_toggle = Some(Rc::new(on_toggle));
803 self
804 }
805
806 pub fn cancel_label(mut self, label: impl Into<String>) -> Self {
808 self.cancel_label = label.into();
809 self
810 }
811
812 pub fn confirm_label(mut self, label: impl Into<String>) -> Self {
814 self.confirm_label = label.into();
815 self
816 }
817
818 pub fn on_confirm<F: Fn(&mut State) + 'static>(mut self, on_confirm: F) -> Self {
823 self.on_confirm = Some(Rc::new(on_confirm));
824 self
825 }
826
827 pub fn dismissable(mut self, dismissable: bool) -> Self {
831 self.dismissable = dismissable;
832 self
833 }
834
835 pub fn on_dismiss<F: Fn(&mut State) + 'static>(mut self, on_dismiss: F) -> Self {
838 self.on_dismiss = Some(Rc::new(on_dismiss));
839 self
840 }
841
842 pub fn dismiss_handle(mut self, dismiss: ModalDismiss) -> Self {
846 self.dismiss_handle = Some(dismiss);
847 self
848 }
849
850 fn compose(&self) -> OverlayModalView<State> {
851 let list = SelectionListView {
852 options: self.options.clone(),
853 multi_select: self.multi_select,
854 selected: self.selected.clone(),
855 on_toggle: self.on_toggle.clone().unwrap_or_else(|| Rc::new(|_, _| {})),
856 };
857
858 let on_dismiss = self.on_dismiss.clone();
859 let cancel_action: AnyView<State> = any(text_button(
860 self.cancel_label.clone(),
861 move |state: &mut State| {
862 if let Some(on_dismiss) = &on_dismiss {
863 on_dismiss(state);
864 }
865 },
866 ));
867 let on_confirm = self.on_confirm.clone();
868 let confirm_action: AnyView<State> = any(button(
869 self.confirm_label.clone(),
870 move |state: &mut State| {
871 if let Some(on_confirm) = &on_confirm {
872 on_confirm(state);
873 }
874 },
875 )
876 .enabled(!self.selected.is_empty()));
877
878 let panel = DialogPanelView {
879 title: Some(self.title.clone()),
880 content: Some(Rc::new(any(list))),
881 actions: vec![Rc::new(cancel_action), Rc::new(confirm_action)],
882 top_divider: true,
883 bottom_divider: true,
884 content_full_bleed: true,
885 };
886 let mut view = overlay_modal(
887 panel,
888 OverlayModalConfig::centered(OVERLAY_DIALOG_MAX_WIDTH),
889 )
890 .dismissable(self.dismissable)
891 .label(self.title.clone());
892 if let Some(on_dismiss) = &self.on_dismiss {
893 let on_dismiss = on_dismiss.clone();
894 view = view.on_dismiss(move |state: &mut State| on_dismiss(state));
895 }
896 if let Some(dismiss) = &self.dismiss_handle {
897 view = view.dismiss_handle(dismiss.clone());
898 }
899 view
900 }
901}
902
903impl<State: 'static> View<State> for SelectionDialogView<State> {
904 type Element = OverlayModalWidget;
905
906 fn build(&self, ctx: &mut BuildCtx<'_>) -> OverlayModalWidget {
907 View::build(&self.compose(), ctx)
908 }
909
910 fn rebuild(
911 &self,
912 prev: &Self,
913 element: &mut OverlayModalWidget,
914 ctx: &mut BuildCtx<'_>,
915 ) -> ChangeFlags {
916 View::rebuild(&self.compose(), &prev.compose(), element, ctx)
917 }
918
919 fn teardown(&self, element: &mut OverlayModalWidget, ctx: &mut BuildCtx<'_>) {
920 View::teardown(&self.compose(), element, ctx);
921 }
922}
923
924impl<State: 'static> OverlayModalContent<State> for SelectionDialogView<State> {
925 fn on_modal_dismiss(mut self, on_dismiss: Rc<dyn Fn(&mut State)>) -> Self {
926 self.on_dismiss = Some(on_dismiss);
927 self
928 }
929
930 fn modal_dismissable(&self) -> bool {
931 self.dismissable
932 }
933}
934
935pub fn show_selection_dialog<State, B, R>(
938 controller: &NavigatorController<State>,
939 build: B,
940 on_result: R,
941) where
942 State: 'static,
943 B: Fn() -> SelectionDialogView<State> + 'static,
944 R: Fn(&mut State, PopResult) + 'static,
945{
946 show_overlay_modal(controller, build, on_result);
947}
948
949struct SelectionListView<State: 'static> {
952 options: Vec<String>,
953 multi_select: bool,
954 selected: Vec<String>,
955 on_toggle: OnToggle<State>,
956}
957
958struct SelectionRow {
962 control: ChildPod,
963 label: Option<ChildPod>,
964}
965
966struct SelectionListWidget {
968 rows: Vec<SelectionRow>,
969}
970
971fn selection_row_views<State: 'static>(
973 option: &str,
974 multi_select: bool,
975 selected: &[String],
976 on_toggle: &OnToggle<State>,
977) -> (AnyView<State>, Option<AnyView<State>>) {
978 if multi_select {
979 let checked = selected.iter().any(|s| s == option);
980 let toggle = on_toggle.clone();
981 let value = option.to_string();
982 let control = any(checkbox(checked, move |state: &mut State, _next: bool| {
983 toggle(state, value.clone())
984 }));
985 let label = any(text(option.to_string())
986 .themed_role(ThemeTextColor::OnSurface)
987 .themed_family(ThemeTextType::BodyLarge));
988 (control, Some(label))
989 } else {
990 let group_value = selected.first().cloned();
991 let toggle = on_toggle.clone();
992 let control = any(
993 radio::<State, Option<String>>(Some(option.to_string()), group_value)
994 .on_changed(move |state: &mut State, value: Option<String>| {
995 if let Some(value) = value {
996 toggle(state, value);
997 }
998 })
999 .label(option.to_string()),
1000 );
1001 (control, None)
1002 }
1003}
1004
1005impl<State: 'static> View<State> for SelectionListView<State> {
1006 type Element = SelectionListWidget;
1007
1008 fn build(&self, ctx: &mut BuildCtx<'_>) -> SelectionListWidget {
1009 let rows = self
1010 .options
1011 .iter()
1012 .map(|option| {
1013 let (control, label) = selection_row_views::<State>(
1014 option,
1015 self.multi_select,
1016 &self.selected,
1017 &self.on_toggle,
1018 );
1019 SelectionRow {
1020 control: build_child(&control, ctx),
1021 label: label.map(|l| build_child(&l, ctx)),
1022 }
1023 })
1024 .collect();
1025 SelectionListWidget { rows }
1026 }
1027
1028 fn rebuild(
1029 &self,
1030 prev: &Self,
1031 element: &mut SelectionListWidget,
1032 ctx: &mut BuildCtx<'_>,
1033 ) -> ChangeFlags {
1034 let shape_changed = prev.options != self.options
1035 || prev.multi_select != self.multi_select
1036 || element.rows.len() != self.options.len();
1037 if shape_changed {
1038 for (view, row) in prev.options.iter().zip(element.rows.iter_mut()) {
1039 let (control, label) = selection_row_views::<State>(
1040 view,
1041 prev.multi_select,
1042 &prev.selected,
1043 &prev.on_toggle,
1044 );
1045 teardown_child(&control, &mut row.control, ctx);
1046 if let (Some(label_view), Some(pod)) = (label, row.label.as_mut()) {
1047 teardown_child(&label_view, pod, ctx);
1048 }
1049 }
1050 element.rows = self
1051 .options
1052 .iter()
1053 .map(|option| {
1054 let (control, label) = selection_row_views::<State>(
1055 option,
1056 self.multi_select,
1057 &self.selected,
1058 &self.on_toggle,
1059 );
1060 SelectionRow {
1061 control: build_child(&control, ctx),
1062 label: label.map(|l| build_child(&l, ctx)),
1063 }
1064 })
1065 .collect();
1066 return ChangeFlags::LAYOUT | ChangeFlags::PAINT;
1067 }
1068
1069 let mut flags = ChangeFlags::NONE;
1070 for (i, (option, row)) in self.options.iter().zip(element.rows.iter_mut()).enumerate() {
1071 let (prev_control, prev_label) = selection_row_views::<State>(
1072 &prev.options[i],
1073 prev.multi_select,
1074 &prev.selected,
1075 &prev.on_toggle,
1076 );
1077 let (next_control, next_label) = selection_row_views::<State>(
1078 option,
1079 self.multi_select,
1080 &self.selected,
1081 &self.on_toggle,
1082 );
1083 flags |= rebuild_child(&prev_control, &next_control, &mut row.control, ctx);
1084 if let (Some(prev_l), Some(next_l), Some(pod)) =
1085 (prev_label, next_label, row.label.as_mut())
1086 {
1087 flags |= rebuild_child(&prev_l, &next_l, pod, ctx);
1088 }
1089 }
1090 flags
1091 }
1092
1093 fn teardown(&self, element: &mut SelectionListWidget, ctx: &mut BuildCtx<'_>) {
1094 for (option, row) in self.options.iter().zip(element.rows.iter_mut()) {
1095 let (control, label) = selection_row_views::<State>(
1096 option,
1097 self.multi_select,
1098 &self.selected,
1099 &self.on_toggle,
1100 );
1101 teardown_child(&control, &mut row.control, ctx);
1102 if let (Some(label_view), Some(pod)) = (label, row.label.as_mut()) {
1103 teardown_child(&label_view, pod, ctx);
1104 }
1105 }
1106 }
1107}
1108
1109impl Widget for SelectionListWidget {
1110 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1111 let width = bc.max().width.max(bc.min().width);
1112 let mut y = SELECTION_LIST_INSET;
1113 let row_w = (width - 2.0 * DIALOG_PADDING).max(0.0);
1114 let row_bc = BoxConstraints::loose(Size::new(row_w, SELECTION_ITEM_HEIGHT));
1115
1116 for row in &mut self.rows {
1117 let control_size = row.control.layout_child(ctx, &row_bc);
1118 let mut row_h = control_size.height;
1119 let label_size = row.label.as_mut().map(|l| l.layout_child(ctx, &row_bc));
1120 if let Some(s) = label_size {
1121 row_h = row_h.max(s.height);
1122 }
1123 let band_h = row_h.max(SELECTION_ITEM_HEIGHT);
1124 let control_y = y + (band_h - control_size.height) / 2.0;
1125 row.control
1126 .set_origin(Point::new(DIALOG_PADDING, control_y));
1127 if let (Some(label), Some(s)) = (row.label.as_mut(), label_size) {
1128 let label_x = DIALOG_PADDING + control_size.width + CHECKBOX_LABEL_GAP;
1129 let label_y = y + (band_h - s.height) / 2.0;
1130 label.set_origin(Point::new(label_x, label_y));
1131 }
1132 y += band_h.max(SELECTION_ITEM_HEIGHT);
1133 }
1134 y += SELECTION_LIST_INSET;
1135
1136 bc.constrain(Size::new(width, y))
1137 }
1138
1139 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1140 for row in &mut self.rows {
1141 row.control.paint_child(ctx, scene);
1142 if let Some(label) = row.label.as_mut() {
1143 label.paint_child(ctx, scene);
1144 }
1145 }
1146 }
1147
1148 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1149 for row in &mut self.rows {
1150 if route_event_single(&mut row.control, ctx, event) == EventResult::Handled {
1151 return EventResult::Handled;
1152 }
1153 }
1154 EventResult::Ignored
1155 }
1156
1157 fn semantics(&self, ctx: &mut SemanticsCtx) {
1158 for row in &self.rows {
1159 row.control.semantics_child(ctx);
1160 if let Some(label) = &row.label {
1161 label.semantics_child(ctx);
1162 }
1163 }
1164 }
1165
1166 fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
1167 for row in &self.rows {
1168 visitor(&row.control);
1169 if let Some(label) = &row.label {
1170 visitor(label);
1171 }
1172 }
1173 }
1174}
1175
1176pub struct FullScreenDialogView<State: 'static> {
1185 title: String,
1186 body: Rc<AnyView<State>>,
1187 action: Option<Rc<AnyView<State>>>,
1188 dismissable: bool,
1189 on_dismiss: Option<OnDismiss<State>>,
1190 dismiss_handle: Option<ModalDismiss>,
1191}
1192
1193pub fn full_screen_dialog<State: 'static, V: View<State>>(
1197 title: impl Into<String>,
1198 body: V,
1199) -> FullScreenDialogView<State> {
1200 FullScreenDialogView {
1201 title: title.into(),
1202 body: Rc::new(any(body)),
1203 action: None,
1204 dismissable: true,
1205 on_dismiss: None,
1206 dismiss_handle: None,
1207 }
1208}
1209
1210impl<State: 'static> FullScreenDialogView<State> {
1211 pub fn action(mut self, action: impl View<State>) -> Self {
1214 self.action = Some(Rc::new(AnyView::new(action)));
1215 self
1216 }
1217
1218 pub fn dismissable(mut self, dismissable: bool) -> Self {
1224 self.dismissable = dismissable;
1225 self
1226 }
1227
1228 pub fn on_dismiss<F: Fn(&mut State) + 'static>(mut self, on_dismiss: F) -> Self {
1231 self.on_dismiss = Some(Rc::new(on_dismiss));
1232 self
1233 }
1234
1235 pub fn dismiss_handle(mut self, dismiss: ModalDismiss) -> Self {
1242 self.dismiss_handle = Some(dismiss);
1243 self
1244 }
1245
1246 fn config() -> OverlayModalConfig {
1247 OverlayModalConfig::edge(OverlaySide::Bottom)
1248 .extent(OverlayExtent::Fraction(1.0), OverlayLimit::None)
1249 .corners(OverlayCorners::None)
1250 .close_button(false)
1251 .scrim_dismiss(false)
1252 .elevation(OverlayElevation::None)
1253 }
1254
1255 fn compose(&self) -> OverlayModalView<State> {
1256 let panel = FullScreenPanelView {
1257 title: self.title.clone(),
1258 body: self.body.clone(),
1259 action: self.action.clone(),
1260 on_dismiss: self.on_dismiss.clone(),
1261 };
1262 let mut view = overlay_modal(panel, Self::config())
1263 .dismissable(self.dismissable)
1264 .label(self.title.clone());
1265 if let Some(on_dismiss) = &self.on_dismiss {
1266 let on_dismiss = on_dismiss.clone();
1267 view = view.on_dismiss(move |state: &mut State| on_dismiss(state));
1268 }
1269 if let Some(dismiss) = &self.dismiss_handle {
1270 view = view.dismiss_handle(dismiss.clone());
1271 }
1272 view
1273 }
1274}
1275
1276impl<State: 'static> View<State> for FullScreenDialogView<State> {
1277 type Element = OverlayModalWidget;
1278
1279 fn build(&self, ctx: &mut BuildCtx<'_>) -> OverlayModalWidget {
1280 View::build(&self.compose(), ctx)
1281 }
1282
1283 fn rebuild(
1284 &self,
1285 prev: &Self,
1286 element: &mut OverlayModalWidget,
1287 ctx: &mut BuildCtx<'_>,
1288 ) -> ChangeFlags {
1289 View::rebuild(&self.compose(), &prev.compose(), element, ctx)
1290 }
1291
1292 fn teardown(&self, element: &mut OverlayModalWidget, ctx: &mut BuildCtx<'_>) {
1293 View::teardown(&self.compose(), element, ctx);
1294 }
1295}
1296
1297impl<State: 'static> OverlayModalContent<State> for FullScreenDialogView<State> {
1298 fn on_modal_dismiss(mut self, on_dismiss: Rc<dyn Fn(&mut State)>) -> Self {
1299 self.on_dismiss = Some(on_dismiss);
1300 self
1301 }
1302
1303 fn modal_dismissable(&self) -> bool {
1304 self.dismissable
1310 }
1311}
1312
1313pub fn show_full_screen_dialog<State, B, R>(
1316 controller: &NavigatorController<State>,
1317 build: B,
1318 on_result: R,
1319) where
1320 State: 'static,
1321 B: Fn() -> FullScreenDialogView<State> + 'static,
1322 R: Fn(&mut State, PopResult) + 'static,
1323{
1324 show_overlay_modal(controller, build, on_result);
1325}
1326
1327struct FullScreenPanelView<State: 'static> {
1330 title: String,
1331 body: Rc<AnyView<State>>,
1332 action: Option<Rc<AnyView<State>>>,
1333 on_dismiss: Option<OnDismiss<State>>,
1334}
1335
1336struct FullScreenPanelWidget {
1338 close: ChildPod,
1339 title: ChildPod,
1340 action: Option<ChildPod>,
1341 divider: ChildPod,
1342 body: ChildPod,
1343}
1344
1345fn close_view<State: 'static>(on_dismiss: Option<OnDismiss<State>>) -> AnyView<State> {
1350 any(
1351 icon_button(icon(crate::icons::CLOSE), move |state: &mut State| {
1352 if let Some(on_dismiss) = &on_dismiss {
1353 on_dismiss(state);
1354 }
1355 })
1356 .semantic_label(CLOSE_SEMANTIC_LABEL),
1357 )
1358}
1359
1360fn header_title_view<State: 'static>(title: &str) -> AnyView<State> {
1363 any::<State, _>(
1364 text(title.to_string())
1365 .size(HEADER_TITLE_SIZE)
1366 .line_height(LineHeight::Absolute(HEADER_TITLE_LINE_HEIGHT))
1367 .max_lines(1)
1368 .overflow(TextOverflow::Ellipsis)
1369 .themed_family(ThemeTextType::TitleLarge),
1370 )
1371}
1372
1373impl<State: 'static> View<State> for FullScreenPanelView<State> {
1374 type Element = FullScreenPanelWidget;
1375
1376 fn build(&self, ctx: &mut BuildCtx<'_>) -> FullScreenPanelWidget {
1377 FullScreenPanelWidget {
1378 close: build_child(&close_view::<State>(self.on_dismiss.clone()), ctx),
1379 title: build_child(&header_title_view::<State>(&self.title), ctx),
1380 action: self.action.as_ref().map(|a| build_child(a.as_ref(), ctx)),
1381 divider: build_child(÷r_view::<State>(), ctx),
1382 body: build_child(self.body.as_ref(), ctx),
1383 }
1384 }
1385
1386 fn rebuild(
1387 &self,
1388 prev: &Self,
1389 element: &mut FullScreenPanelWidget,
1390 ctx: &mut BuildCtx<'_>,
1391 ) -> ChangeFlags {
1392 let mut flags = rebuild_child(
1393 &close_view::<State>(prev.on_dismiss.clone()),
1394 &close_view::<State>(self.on_dismiss.clone()),
1395 &mut element.close,
1396 ctx,
1397 );
1398 flags |= rebuild_child(
1399 &header_title_view::<State>(&prev.title),
1400 &header_title_view::<State>(&self.title),
1401 &mut element.title,
1402 ctx,
1403 );
1404 flags |= match (prev.action.as_ref(), self.action.as_ref()) {
1405 (None, Some(v)) => {
1406 element.action = Some(build_child(v.as_ref(), ctx));
1407 ChangeFlags::LAYOUT | ChangeFlags::PAINT
1408 }
1409 (Some(v), None) => {
1410 if let Some(p) = element.action.as_mut() {
1411 teardown_child(v.as_ref(), p, ctx);
1412 }
1413 element.action = None;
1414 ChangeFlags::LAYOUT | ChangeFlags::PAINT
1415 }
1416 (Some(a), Some(b)) => match element.action.as_mut() {
1417 Some(p) => rebuild_child(a.as_ref(), b.as_ref(), p, ctx),
1418 None => ChangeFlags::NONE,
1419 },
1420 (None, None) => ChangeFlags::NONE,
1421 };
1422 flags |= rebuild_child(
1423 ÷r_view::<State>(),
1424 ÷r_view::<State>(),
1425 &mut element.divider,
1426 ctx,
1427 );
1428 flags |= rebuild_child(
1429 prev.body.as_ref(),
1430 self.body.as_ref(),
1431 &mut element.body,
1432 ctx,
1433 );
1434 flags
1435 }
1436
1437 fn teardown(&self, element: &mut FullScreenPanelWidget, ctx: &mut BuildCtx<'_>) {
1438 teardown_child(
1439 &close_view::<State>(self.on_dismiss.clone()),
1440 &mut element.close,
1441 ctx,
1442 );
1443 teardown_child(
1444 &header_title_view::<State>(&self.title),
1445 &mut element.title,
1446 ctx,
1447 );
1448 if let (Some(v), Some(p)) = (&self.action, element.action.as_mut()) {
1449 teardown_child(v.as_ref(), p, ctx);
1450 }
1451 teardown_child(÷r_view::<State>(), &mut element.divider, ctx);
1452 teardown_child(self.body.as_ref(), &mut element.body, ctx);
1453 }
1454}
1455
1456impl Widget for FullScreenPanelWidget {
1457 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1458 let size = bc.max();
1461 let (width, height) = (size.width, size.height);
1462
1463 let loose = BoxConstraints::loose(Size::new(width, FULL_SCREEN_HEADER_HEIGHT));
1464 let close_size = self.close.layout_child(ctx, &loose);
1465 let action_size = self.action.as_mut().map(|a| a.layout_child(ctx, &loose));
1466
1467 let close_x = HEADER_EDGE_GAP;
1468 self.close.set_origin(Point::new(
1469 close_x,
1470 (FULL_SCREEN_HEADER_HEIGHT - close_size.height) / 2.0,
1471 ));
1472
1473 let title_leading = close_x + close_size.width + HEADER_EDGE_GAP;
1474 let trailing_reserve = action_size
1475 .map(|s| s.width + HEADER_ACTION_GAP)
1476 .unwrap_or(0.0);
1477 let title_w = (width - title_leading - trailing_reserve).max(0.0);
1478 let title_bc = BoxConstraints::new(
1479 Size::new(title_w, 0.0),
1480 Size::new(title_w, FULL_SCREEN_HEADER_HEIGHT),
1481 );
1482 let title_size = self.title.layout_child(ctx, &title_bc);
1483 self.title.set_origin(Point::new(
1484 title_leading,
1485 (FULL_SCREEN_HEADER_HEIGHT - title_size.height) / 2.0,
1486 ));
1487
1488 if let (Some(action), Some(s)) = (self.action.as_mut(), action_size) {
1489 action.set_origin(Point::new(
1490 width - HEADER_ACTION_GAP - s.width,
1491 (FULL_SCREEN_HEADER_HEIGHT - s.height) / 2.0,
1492 ));
1493 }
1494
1495 self.divider
1496 .layout_child(ctx, &BoxConstraints::tight(Size::new(width, 1.0)));
1497 self.divider
1498 .set_origin(Point::new(0.0, FULL_SCREEN_HEADER_HEIGHT));
1499 let divider_h = self.divider.size().height;
1500
1501 let body_y = FULL_SCREEN_HEADER_HEIGHT + divider_h;
1502 let body_h = (height - body_y).max(0.0);
1503 self.body
1504 .layout_child(ctx, &BoxConstraints::tight(Size::new(width, body_h)));
1505 self.body.set_origin(Point::new(0.0, body_y));
1506
1507 bc.constrain(size)
1508 }
1509
1510 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1511 let theme = Theme::from_paint_ctx(ctx);
1515 scene.fill_rect(ctx.origin(), ctx.size(), resolve_surface(theme));
1516
1517 self.close.paint_child(ctx, scene);
1518 self.title.paint_child(ctx, scene);
1519 if let Some(action) = self.action.as_mut() {
1520 action.paint_child(ctx, scene);
1521 }
1522 self.divider.paint_child(ctx, scene);
1523 self.body.paint_child(ctx, scene);
1524 }
1525
1526 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1527 if route_event_single(&mut self.close, ctx, event) == EventResult::Handled {
1528 return EventResult::Handled;
1529 }
1530 if let Some(action) = self.action.as_mut()
1531 && route_event_single(action, ctx, event) == EventResult::Handled
1532 {
1533 return EventResult::Handled;
1534 }
1535 if route_event_single(&mut self.body, ctx, event) == EventResult::Handled {
1536 return EventResult::Handled;
1537 }
1538 EventResult::Ignored
1539 }
1540
1541 fn semantics(&self, ctx: &mut SemanticsCtx) {
1542 self.close.semantics_child(ctx);
1543 self.title.semantics_child(ctx);
1544 if let Some(action) = &self.action {
1545 action.semantics_child(ctx);
1546 }
1547 self.body.semantics_child(ctx);
1548 }
1549
1550 visit_children!(close, title, action, divider, body);
1551}
1552
1553#[cfg(test)]
1554mod tests {
1555 use super::*;
1556 use frust::authoring::scene::GlyphRun;
1557 use frust::authoring::{
1558 CornerRadii, Key, KeyEvent, Modifiers, NamedKey, PointerButton, PointerEvent, PointerPhase,
1559 text::TextContext,
1560 };
1561 use frust::{FrameTime, NavigatorView};
1562 use frust_core::RenderRoot;
1563 use frust_widgets::navigator;
1564 use std::any::Any;
1565
1566 const WINDOW: Size = Size::new(400.0, 600.0);
1568
1569 #[derive(Default)]
1572 struct Recorder {
1573 rects: Vec<(Point, Size, Color)>,
1574 rrects: Vec<(Point, Size, CornerRadii, Color)>,
1575 glyphs: usize,
1576 }
1577 impl PaintScene for Recorder {
1578 fn fill_rect(&mut self, o: Point, s: Size, c: Color) {
1579 self.rects.push((o, s, c));
1580 }
1581 fn draw_text(&mut self, _o: Point, _t: &str) {}
1582 fn draw_glyph_run(&mut self, _run: GlyphRun) {
1583 self.glyphs += 1;
1584 }
1585 fn fill_rounded_rect(&mut self, _o: Point, _s: Size, _r: f64, _c: Color) {}
1586 fn fill_rounded_rect_radii(&mut self, o: Point, s: Size, radii: CornerRadii, c: Color) {
1587 self.rrects.push((o, s, radii, c));
1588 }
1589 }
1590
1591 fn ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
1592 InputEvent::Pointer(PointerEvent {
1593 phase,
1594 position: Point::new(x, y),
1595 button: PointerButton::Primary,
1596 })
1597 }
1598
1599 fn escape_event() -> InputEvent {
1600 InputEvent::Key(KeyEvent {
1601 key: Key::Named(NamedKey::Escape),
1602 modifiers: Modifiers::default(),
1603 repeat: false,
1604 })
1605 }
1606
1607 #[derive(Default)]
1608 struct NavState {
1609 results: Vec<Option<bool>>,
1610 toggled: Vec<String>,
1611 confirmed: bool,
1612 }
1613
1614 struct Sized {
1615 size: Size,
1616 }
1617 struct SizedW {
1618 size: Size,
1619 }
1620 impl View<NavState> for Sized {
1621 type Element = SizedW;
1622 fn build(&self, _c: &mut BuildCtx<'_>) -> SizedW {
1623 SizedW { size: self.size }
1624 }
1625 fn rebuild(&self, _p: &Self, _e: &mut SizedW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
1626 ChangeFlags::NONE
1627 }
1628 }
1629 impl Widget for SizedW {
1630 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1631 bc.constrain(self.size)
1632 }
1633 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1634 scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
1635 }
1636 }
1637 fn sized(w: f64, h: f64) -> Sized {
1638 Sized {
1639 size: Size::new(w, h),
1640 }
1641 }
1642
1643 struct TapView {
1647 size: Size,
1648 on_tap: Rc<dyn Fn(&mut NavState)>,
1649 }
1650 struct TapWidget {
1651 size: Size,
1652 on_tap: frust::authoring::ErasedCallback,
1653 captured: bool,
1654 }
1655 impl View<NavState> for TapView {
1656 type Element = TapWidget;
1657 fn build(&self, _ctx: &mut BuildCtx<'_>) -> TapWidget {
1658 TapWidget {
1659 size: self.size,
1660 on_tap: frust::authoring::erase_callback(&self.on_tap),
1661 captured: false,
1662 }
1663 }
1664 fn rebuild(
1665 &self,
1666 _prev: &Self,
1667 element: &mut TapWidget,
1668 _ctx: &mut BuildCtx<'_>,
1669 ) -> ChangeFlags {
1670 element.on_tap = frust::authoring::erase_callback(&self.on_tap);
1671 ChangeFlags::NONE
1672 }
1673 }
1674 impl Widget for TapWidget {
1675 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1676 bc.constrain(self.size)
1677 }
1678 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1679 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1680 let InputEvent::Pointer(p) = event else {
1681 return EventResult::Ignored;
1682 };
1683 match p.phase {
1684 PointerPhase::Down => {
1685 self.captured = true;
1686 ctx.capture_pointer();
1687 EventResult::Handled
1688 }
1689 PointerPhase::Up => {
1690 if self.captured
1691 && p.position.x >= 0.0
1692 && p.position.y >= 0.0
1693 && p.position.x < self.size.width
1694 && p.position.y < self.size.height
1695 {
1696 (self.on_tap)(ctx);
1697 }
1698 self.captured = false;
1699 EventResult::Handled
1700 }
1701 PointerPhase::Move => EventResult::Handled,
1702 PointerPhase::Cancel => {
1703 self.captured = false;
1704 EventResult::Handled
1705 }
1706 }
1707 }
1708 }
1709 fn tap_action(w: f64, h: f64, on_tap: impl Fn(&mut NavState) + 'static) -> impl View<NavState> {
1710 TapView {
1711 size: Size::new(w, h),
1712 on_tap: Rc::new(on_tap),
1713 }
1714 }
1715
1716 type NavLogic = Box<dyn FnMut(&mut NavState) -> NavigatorView<NavState>>;
1718
1719 struct Harness {
1720 root: RenderRoot<NavState, NavigatorView<NavState>>,
1721 state: NavState,
1722 tcx: TextContext,
1723 controller: NavigatorController<NavState>,
1724 logic: NavLogic,
1725 }
1726
1727 impl Harness {
1728 fn new() -> Self {
1729 let controller: NavigatorController<NavState> = NavigatorController::new();
1730 let ctrl = controller.clone();
1731 let mut h = Harness {
1732 root: RenderRoot::new(),
1733 state: NavState::default(),
1734 tcx: TextContext::new(),
1735 controller,
1736 logic: Box::new(move |_: &mut NavState| {
1737 navigator(&ctrl, || {
1738 any::<NavState, _>(sized(WINDOW.width, WINDOW.height))
1739 })
1740 }),
1741 };
1742 h.pass();
1743 h
1744 }
1745
1746 fn pass(&mut self) {
1747 self.root.rebuild(&mut self.logic, &mut self.state);
1748 self.root
1749 .layout_with_text(WINDOW, &mut self.tcx as &mut dyn Any);
1750 }
1751
1752 fn settle(&mut self) {
1754 self.root.paint(&mut Recorder::default(), FrameTime::ZERO);
1755 self.root.paint(
1756 &mut Recorder::default(),
1757 FrameTime::from_nanos(1_000_000_000),
1758 );
1759 self.pass();
1760 }
1761
1762 fn settle_exit(&mut self) {
1764 self.root.paint(
1765 &mut Recorder::default(),
1766 FrameTime::from_nanos(2_000_000_000),
1767 );
1768 self.root.paint(
1769 &mut Recorder::default(),
1770 FrameTime::from_nanos(3_000_000_000),
1771 );
1772 self.pass();
1773 }
1774
1775 fn event(&mut self, event: &InputEvent) {
1776 self.root.event(&mut self.state, event);
1777 }
1778
1779 fn flush(&mut self) {
1780 self.pass();
1781 self.event(&ev(PointerPhase::Move, 2.0, 2.0));
1782 }
1783
1784 fn paint(&mut self) -> Recorder {
1785 let mut rec = Recorder::default();
1786 self.root
1787 .paint(&mut rec, FrameTime::from_nanos(4_000_000_000));
1788 rec
1789 }
1790 }
1791
1792 #[test]
1795 fn the_basic_dialog_renders_title_body_and_actions() {
1796 let mut h = Harness::new();
1797 show_dialog(
1798 &h.controller,
1799 || {
1800 dialog()
1801 .title("Confirm")
1802 .body("Are you sure?")
1803 .action(text_button("OK", |_s: &mut NavState| {}))
1804 },
1805 |state: &mut NavState, result: PopResult| {
1806 state.results.push(result.take::<bool>());
1807 },
1808 );
1809 h.pass();
1810 h.settle();
1811 let rec = h.paint();
1812 assert!(
1813 rec.glyphs >= 3,
1814 "title, body, and the action label all shape"
1815 );
1816 }
1817
1818 #[test]
1819 fn the_selection_dialog_renders_options_and_actions() {
1820 let mut h = Harness::new();
1821 show_selection_dialog(
1822 &h.controller,
1823 || {
1824 selection_dialog("Pick one", vec!["A".into(), "B".into(), "C".into()])
1825 .selected(vec!["B".into()])
1826 },
1827 |state: &mut NavState, result: PopResult| {
1828 state.results.push(result.take::<bool>());
1829 },
1830 );
1831 h.pass();
1832 h.settle();
1833 let rec = h.paint();
1834 assert!(rec.glyphs >= 6);
1836 }
1837
1838 #[test]
1839 fn the_full_screen_dialog_renders_header_and_body() {
1840 let mut h = Harness::new();
1841 show_full_screen_dialog(
1842 &h.controller,
1843 || full_screen_dialog("Settings", sized(200.0, 200.0)),
1844 |state: &mut NavState, result: PopResult| {
1845 state.results.push(result.take::<bool>());
1846 },
1847 );
1848 h.pass();
1849 h.settle();
1850 let rec = h.paint();
1851 assert!(rec.glyphs >= 1, "the header title shapes");
1852 assert!(rec.rects.len() >= 2);
1854 }
1855
1856 #[test]
1859 fn scrim_tap_stages_an_exit_then_pops_with_an_empty_result() {
1860 let mut h = Harness::new();
1861 show_dialog(
1862 &h.controller,
1863 || dialog().title("Confirm"),
1864 |state: &mut NavState, result: PopResult| {
1865 state.results.push(result.take::<bool>());
1866 },
1867 );
1868 h.pass();
1869 h.settle();
1870 h.event(&ev(PointerPhase::Down, 5.0, 5.0));
1871 h.event(&ev(PointerPhase::Up, 5.0, 5.0));
1872 h.flush();
1873 assert!(
1874 h.state.results.is_empty(),
1875 "still animating out — no local scrim/pop code fired it directly"
1876 );
1877 h.settle_exit();
1878 h.flush();
1879 assert_eq!(h.state.results, vec![None]);
1880 }
1881
1882 #[test]
1883 fn escape_stages_the_same_exit_once_the_dialog_has_focus() {
1884 let mut h = Harness::new();
1885 show_dialog(
1886 &h.controller,
1887 || dialog().title("Confirm"),
1888 |state: &mut NavState, result: PopResult| {
1889 state.results.push(result.take::<bool>());
1890 },
1891 );
1892 h.pass();
1893 h.settle();
1894 h.event(&ev(PointerPhase::Down, 200.0, 300.0));
1897 h.event(&ev(PointerPhase::Up, 200.0, 300.0));
1898 h.flush();
1899 assert!(h.state.results.is_empty());
1900 h.event(&escape_event());
1901 h.flush();
1902 assert!(h.state.results.is_empty(), "still animating out");
1903 h.settle_exit();
1904 h.flush();
1905 assert_eq!(h.state.results, vec![None]);
1906 }
1907
1908 #[test]
1909 fn a_dismissable_false_dialog_ignores_escape() {
1910 let mut h = Harness::new();
1911 show_dialog(
1912 &h.controller,
1913 || dialog().title("Pinned").dismissable(false),
1914 |state: &mut NavState, result: PopResult| {
1915 state.results.push(result.take::<bool>());
1916 },
1917 );
1918 h.pass();
1919 h.settle();
1920 h.event(&ev(PointerPhase::Down, 200.0, 300.0));
1921 h.event(&ev(PointerPhase::Up, 200.0, 300.0));
1922 h.event(&escape_event());
1923 h.flush();
1924 h.settle_exit();
1925 h.flush();
1926 assert!(
1927 h.state.results.is_empty(),
1928 "a non-dismissable dialog never pops via Escape"
1929 );
1930 }
1931
1932 #[test]
1933 fn an_action_pops_with_a_value_via_the_navigator() {
1934 const ACTION: Size = Size::new(80.0, 36.0);
1941 const PANEL_W: f64 = 368.0;
1942 const PANEL_MARGIN: f64 = 16.0;
1943
1944 let mut h = Harness::new();
1945 h.controller.push_transparent_for_result(
1946 {
1947 let ctrl = h.controller.clone();
1948 move || {
1949 let c = ctrl.clone();
1950 any::<NavState, _>(dialog().action(tap_action(
1951 ACTION.width,
1952 ACTION.height,
1953 move |_s: &mut NavState| c.pop_with_result(PopResult::of(true)),
1954 )))
1955 }
1956 },
1957 frust::TransitionSpec::NONE,
1958 |state: &mut NavState, result: PopResult| {
1959 state.results.push(result.take::<bool>());
1960 },
1961 );
1962 h.pass();
1963 h.settle();
1964
1965 let panel_h = 2.0 * DIALOG_PADDING + ACTION.height;
1969 let panel_y = (WINDOW.height - panel_h) / 2.0;
1970 let action_x = PANEL_MARGIN + PANEL_W - DIALOG_PADDING - ACTION.width;
1971 let action_y = panel_y + DIALOG_PADDING;
1972 let (cx, cy) = (
1973 action_x + ACTION.width / 2.0,
1974 action_y + ACTION.height / 2.0,
1975 );
1976 h.event(&ev(PointerPhase::Down, cx, cy));
1977 h.event(&ev(PointerPhase::Up, cx, cy));
1978 h.flush();
1979 assert_eq!(h.state.results, vec![Some(true)]);
1980 }
1981
1982 #[test]
1983 fn selection_dialog_confirm_reports_the_controlled_selection() {
1984 let mut h = Harness::new();
1985 show_selection_dialog(
1986 &h.controller,
1987 || {
1988 selection_dialog("Pick", vec!["A".into(), "B".into()])
1989 .selected(vec!["A".into()])
1990 .on_toggle(|s: &mut NavState, v: String| s.toggled.push(v))
1991 .on_confirm(|s: &mut NavState| {
1992 s.confirmed = true;
1993 })
1994 },
1995 |state: &mut NavState, result: PopResult| {
1996 state.results.push(result.take::<bool>());
1997 },
1998 );
1999 h.pass();
2000 h.settle();
2001 assert!(!h.state.confirmed);
2002 }
2003
2004 #[test]
2005 fn every_variant_forwards_an_installed_dismiss_handle_to_the_host() {
2006 let dismiss = ModalDismiss::new();
2011 assert!(
2012 dialog::<NavState>()
2013 .dismiss_handle(dismiss.clone())
2014 .compose()
2015 .dismiss_handle
2016 .is_some()
2017 );
2018 assert!(
2019 selection_dialog::<NavState>("Pick", vec!["A".into()])
2020 .dismiss_handle(dismiss.clone())
2021 .compose()
2022 .dismiss_handle
2023 .is_some()
2024 );
2025 assert!(
2026 full_screen_dialog::<NavState, _>("Edit", body_view::<NavState>("body"))
2027 .dismiss_handle(dismiss)
2028 .compose()
2029 .dismiss_handle
2030 .is_some()
2031 );
2032 assert!(
2033 dialog::<NavState>().compose().dismiss_handle.is_none(),
2034 "and none is invented for a variant that was handed none"
2035 );
2036 }
2037
2038 #[test]
2041 fn the_old_builder_shape_still_compiles_and_runs() {
2042 let mut h = Harness::new();
2043 show_dialog(
2044 &h.controller,
2045 || {
2046 dialog()
2047 .title("Invite people")
2048 .body("Send invites to this workspace? (mock)")
2049 .action(text_button("Send", |_s: &mut NavState| {}))
2050 },
2051 |state: &mut NavState, result: PopResult| {
2052 state.results.push(result.take::<bool>());
2053 },
2054 );
2055 h.pass();
2056 h.settle();
2057 assert!(h.state.results.is_empty());
2058 }
2059}