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>>) -> impl View<State> {
1349 icon_button(icon(crate::icons::CLOSE), move |state: &mut State| {
1350 if let Some(on_dismiss) = &on_dismiss {
1351 on_dismiss(state);
1352 }
1353 })
1354 .semantic_label(CLOSE_SEMANTIC_LABEL)
1355}
1356
1357fn header_title_view<State: 'static>(title: &str) -> AnyView<State> {
1360 any::<State, _>(
1361 text(title.to_string())
1362 .size(HEADER_TITLE_SIZE)
1363 .line_height(LineHeight::Absolute(HEADER_TITLE_LINE_HEIGHT))
1364 .max_lines(1)
1365 .overflow(TextOverflow::Ellipsis)
1366 .themed_family(ThemeTextType::TitleLarge),
1367 )
1368}
1369
1370impl<State: 'static> View<State> for FullScreenPanelView<State> {
1371 type Element = FullScreenPanelWidget;
1372
1373 fn build(&self, ctx: &mut BuildCtx<'_>) -> FullScreenPanelWidget {
1374 FullScreenPanelWidget {
1375 close: build_child(&close_view::<State>(self.on_dismiss.clone()), ctx),
1376 title: build_child(&header_title_view::<State>(&self.title), ctx),
1377 action: self.action.as_ref().map(|a| build_child(a.as_ref(), ctx)),
1378 divider: build_child(÷r_view::<State>(), ctx),
1379 body: build_child(self.body.as_ref(), ctx),
1380 }
1381 }
1382
1383 fn rebuild(
1384 &self,
1385 prev: &Self,
1386 element: &mut FullScreenPanelWidget,
1387 ctx: &mut BuildCtx<'_>,
1388 ) -> ChangeFlags {
1389 let mut flags = rebuild_child(
1390 &close_view::<State>(prev.on_dismiss.clone()),
1391 &close_view::<State>(self.on_dismiss.clone()),
1392 &mut element.close,
1393 ctx,
1394 );
1395 flags |= rebuild_child(
1396 &header_title_view::<State>(&prev.title),
1397 &header_title_view::<State>(&self.title),
1398 &mut element.title,
1399 ctx,
1400 );
1401 flags |= match (prev.action.as_ref(), self.action.as_ref()) {
1402 (None, Some(v)) => {
1403 element.action = Some(build_child(v.as_ref(), ctx));
1404 ChangeFlags::LAYOUT | ChangeFlags::PAINT
1405 }
1406 (Some(v), None) => {
1407 if let Some(p) = element.action.as_mut() {
1408 teardown_child(v.as_ref(), p, ctx);
1409 }
1410 element.action = None;
1411 ChangeFlags::LAYOUT | ChangeFlags::PAINT
1412 }
1413 (Some(a), Some(b)) => match element.action.as_mut() {
1414 Some(p) => rebuild_child(a.as_ref(), b.as_ref(), p, ctx),
1415 None => ChangeFlags::NONE,
1416 },
1417 (None, None) => ChangeFlags::NONE,
1418 };
1419 flags |= rebuild_child(
1420 ÷r_view::<State>(),
1421 ÷r_view::<State>(),
1422 &mut element.divider,
1423 ctx,
1424 );
1425 flags |= rebuild_child(
1426 prev.body.as_ref(),
1427 self.body.as_ref(),
1428 &mut element.body,
1429 ctx,
1430 );
1431 flags
1432 }
1433
1434 fn teardown(&self, element: &mut FullScreenPanelWidget, ctx: &mut BuildCtx<'_>) {
1435 teardown_child(
1436 &close_view::<State>(self.on_dismiss.clone()),
1437 &mut element.close,
1438 ctx,
1439 );
1440 teardown_child(
1441 &header_title_view::<State>(&self.title),
1442 &mut element.title,
1443 ctx,
1444 );
1445 if let (Some(v), Some(p)) = (&self.action, element.action.as_mut()) {
1446 teardown_child(v.as_ref(), p, ctx);
1447 }
1448 teardown_child(÷r_view::<State>(), &mut element.divider, ctx);
1449 teardown_child(self.body.as_ref(), &mut element.body, ctx);
1450 }
1451}
1452
1453impl Widget for FullScreenPanelWidget {
1454 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1455 let size = bc.max();
1458 let (width, height) = (size.width, size.height);
1459
1460 let loose = BoxConstraints::loose(Size::new(width, FULL_SCREEN_HEADER_HEIGHT));
1461 let close_size = self.close.layout_child(ctx, &loose);
1462 let action_size = self.action.as_mut().map(|a| a.layout_child(ctx, &loose));
1463
1464 let close_x = HEADER_EDGE_GAP;
1465 self.close.set_origin(Point::new(
1466 close_x,
1467 (FULL_SCREEN_HEADER_HEIGHT - close_size.height) / 2.0,
1468 ));
1469
1470 let title_leading = close_x + close_size.width + HEADER_EDGE_GAP;
1471 let trailing_reserve = action_size
1472 .map(|s| s.width + HEADER_ACTION_GAP)
1473 .unwrap_or(0.0);
1474 let title_w = (width - title_leading - trailing_reserve).max(0.0);
1475 let title_bc = BoxConstraints::new(
1476 Size::new(title_w, 0.0),
1477 Size::new(title_w, FULL_SCREEN_HEADER_HEIGHT),
1478 );
1479 let title_size = self.title.layout_child(ctx, &title_bc);
1480 self.title.set_origin(Point::new(
1481 title_leading,
1482 (FULL_SCREEN_HEADER_HEIGHT - title_size.height) / 2.0,
1483 ));
1484
1485 if let (Some(action), Some(s)) = (self.action.as_mut(), action_size) {
1486 action.set_origin(Point::new(
1487 width - HEADER_ACTION_GAP - s.width,
1488 (FULL_SCREEN_HEADER_HEIGHT - s.height) / 2.0,
1489 ));
1490 }
1491
1492 self.divider
1493 .layout_child(ctx, &BoxConstraints::tight(Size::new(width, 1.0)));
1494 self.divider
1495 .set_origin(Point::new(0.0, FULL_SCREEN_HEADER_HEIGHT));
1496 let divider_h = self.divider.size().height;
1497
1498 let body_y = FULL_SCREEN_HEADER_HEIGHT + divider_h;
1499 let body_h = (height - body_y).max(0.0);
1500 self.body
1501 .layout_child(ctx, &BoxConstraints::tight(Size::new(width, body_h)));
1502 self.body.set_origin(Point::new(0.0, body_y));
1503
1504 bc.constrain(size)
1505 }
1506
1507 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1508 let theme = Theme::from_paint_ctx(ctx);
1512 scene.fill_rect(ctx.origin(), ctx.size(), resolve_surface(theme));
1513
1514 self.close.paint_child(ctx, scene);
1515 self.title.paint_child(ctx, scene);
1516 if let Some(action) = self.action.as_mut() {
1517 action.paint_child(ctx, scene);
1518 }
1519 self.divider.paint_child(ctx, scene);
1520 self.body.paint_child(ctx, scene);
1521 }
1522
1523 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1524 if route_event_single(&mut self.close, ctx, event) == EventResult::Handled {
1525 return EventResult::Handled;
1526 }
1527 if let Some(action) = self.action.as_mut()
1528 && route_event_single(action, ctx, event) == EventResult::Handled
1529 {
1530 return EventResult::Handled;
1531 }
1532 if route_event_single(&mut self.body, ctx, event) == EventResult::Handled {
1533 return EventResult::Handled;
1534 }
1535 EventResult::Ignored
1536 }
1537
1538 fn semantics(&self, ctx: &mut SemanticsCtx) {
1539 self.close.semantics_child(ctx);
1540 self.title.semantics_child(ctx);
1541 if let Some(action) = &self.action {
1542 action.semantics_child(ctx);
1543 }
1544 self.body.semantics_child(ctx);
1545 }
1546
1547 visit_children!(close, title, action, divider, body);
1548}
1549
1550#[cfg(test)]
1551mod tests {
1552 use super::*;
1553 use frust::authoring::scene::GlyphRun;
1554 use frust::authoring::{
1555 CornerRadii, Key, KeyEvent, Modifiers, NamedKey, PointerButton, PointerEvent, PointerPhase,
1556 text::TextContext,
1557 };
1558 use frust::{FrameTime, NavigatorView};
1559 use frust_core::RenderRoot;
1560 use frust_widgets::navigator;
1561 use std::any::Any;
1562
1563 const WINDOW: Size = Size::new(400.0, 600.0);
1565
1566 #[derive(Default)]
1569 struct Recorder {
1570 rects: Vec<(Point, Size, Color)>,
1571 rrects: Vec<(Point, Size, CornerRadii, Color)>,
1572 glyphs: usize,
1573 }
1574 impl PaintScene for Recorder {
1575 fn fill_rect(&mut self, o: Point, s: Size, c: Color) {
1576 self.rects.push((o, s, c));
1577 }
1578 fn draw_text(&mut self, _o: Point, _t: &str) {}
1579 fn draw_glyph_run(&mut self, _run: GlyphRun) {
1580 self.glyphs += 1;
1581 }
1582 fn fill_rounded_rect(&mut self, _o: Point, _s: Size, _r: f64, _c: Color) {}
1583 fn fill_rounded_rect_radii(&mut self, o: Point, s: Size, radii: CornerRadii, c: Color) {
1584 self.rrects.push((o, s, radii, c));
1585 }
1586 }
1587
1588 fn ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
1589 InputEvent::Pointer(PointerEvent {
1590 phase,
1591 position: Point::new(x, y),
1592 button: PointerButton::Primary,
1593 })
1594 }
1595
1596 fn escape_event() -> InputEvent {
1597 InputEvent::Key(KeyEvent {
1598 key: Key::Named(NamedKey::Escape),
1599 modifiers: Modifiers::default(),
1600 repeat: false,
1601 })
1602 }
1603
1604 #[derive(Default)]
1605 struct NavState {
1606 results: Vec<Option<bool>>,
1607 toggled: Vec<String>,
1608 confirmed: bool,
1609 }
1610
1611 struct Sized {
1612 size: Size,
1613 }
1614 struct SizedW {
1615 size: Size,
1616 }
1617 impl View<NavState> for Sized {
1618 type Element = SizedW;
1619 fn build(&self, _c: &mut BuildCtx<'_>) -> SizedW {
1620 SizedW { size: self.size }
1621 }
1622 fn rebuild(&self, _p: &Self, _e: &mut SizedW, _c: &mut BuildCtx<'_>) -> ChangeFlags {
1623 ChangeFlags::NONE
1624 }
1625 }
1626 impl Widget for SizedW {
1627 fn layout(&mut self, _c: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1628 bc.constrain(self.size)
1629 }
1630 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1631 scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
1632 }
1633 }
1634 fn sized(w: f64, h: f64) -> Sized {
1635 Sized {
1636 size: Size::new(w, h),
1637 }
1638 }
1639
1640 struct TapView {
1644 size: Size,
1645 on_tap: Rc<dyn Fn(&mut NavState)>,
1646 }
1647 struct TapWidget {
1648 size: Size,
1649 on_tap: frust::authoring::ErasedCallback,
1650 captured: bool,
1651 }
1652 impl View<NavState> for TapView {
1653 type Element = TapWidget;
1654 fn build(&self, _ctx: &mut BuildCtx<'_>) -> TapWidget {
1655 TapWidget {
1656 size: self.size,
1657 on_tap: frust::authoring::erase_callback(&self.on_tap),
1658 captured: false,
1659 }
1660 }
1661 fn rebuild(
1662 &self,
1663 _prev: &Self,
1664 element: &mut TapWidget,
1665 _ctx: &mut BuildCtx<'_>,
1666 ) -> ChangeFlags {
1667 element.on_tap = frust::authoring::erase_callback(&self.on_tap);
1668 ChangeFlags::NONE
1669 }
1670 }
1671 impl Widget for TapWidget {
1672 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1673 bc.constrain(self.size)
1674 }
1675 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1676 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1677 let InputEvent::Pointer(p) = event else {
1678 return EventResult::Ignored;
1679 };
1680 match p.phase {
1681 PointerPhase::Down => {
1682 self.captured = true;
1683 ctx.capture_pointer();
1684 EventResult::Handled
1685 }
1686 PointerPhase::Up => {
1687 if self.captured
1688 && p.position.x >= 0.0
1689 && p.position.y >= 0.0
1690 && p.position.x < self.size.width
1691 && p.position.y < self.size.height
1692 {
1693 (self.on_tap)(ctx);
1694 }
1695 self.captured = false;
1696 EventResult::Handled
1697 }
1698 PointerPhase::Move => EventResult::Handled,
1699 PointerPhase::Cancel => {
1700 self.captured = false;
1701 EventResult::Handled
1702 }
1703 }
1704 }
1705 }
1706 fn tap_action(w: f64, h: f64, on_tap: impl Fn(&mut NavState) + 'static) -> impl View<NavState> {
1707 TapView {
1708 size: Size::new(w, h),
1709 on_tap: Rc::new(on_tap),
1710 }
1711 }
1712
1713 type NavLogic = Box<dyn FnMut(&mut NavState) -> NavigatorView<NavState>>;
1715
1716 struct Harness {
1717 root: RenderRoot<NavState, NavigatorView<NavState>>,
1718 state: NavState,
1719 tcx: TextContext,
1720 controller: NavigatorController<NavState>,
1721 logic: NavLogic,
1722 }
1723
1724 impl Harness {
1725 fn new() -> Self {
1726 let controller: NavigatorController<NavState> = NavigatorController::new();
1727 let ctrl = controller.clone();
1728 let mut h = Harness {
1729 root: RenderRoot::new(),
1730 state: NavState::default(),
1731 tcx: TextContext::new(),
1732 controller,
1733 logic: Box::new(move |_: &mut NavState| {
1734 navigator(&ctrl, || {
1735 any::<NavState, _>(sized(WINDOW.width, WINDOW.height))
1736 })
1737 }),
1738 };
1739 h.pass();
1740 h
1741 }
1742
1743 fn pass(&mut self) {
1744 self.root.rebuild(&mut self.logic, &mut self.state);
1745 self.root
1746 .layout_with_text(WINDOW, &mut self.tcx as &mut dyn Any);
1747 }
1748
1749 fn settle(&mut self) {
1751 self.root.paint(&mut Recorder::default(), FrameTime::ZERO);
1752 self.root.paint(
1753 &mut Recorder::default(),
1754 FrameTime::from_nanos(1_000_000_000),
1755 );
1756 self.pass();
1757 }
1758
1759 fn settle_exit(&mut self) {
1761 self.root.paint(
1762 &mut Recorder::default(),
1763 FrameTime::from_nanos(2_000_000_000),
1764 );
1765 self.root.paint(
1766 &mut Recorder::default(),
1767 FrameTime::from_nanos(3_000_000_000),
1768 );
1769 self.pass();
1770 }
1771
1772 fn event(&mut self, event: &InputEvent) {
1773 self.root.event(&mut self.state, event);
1774 }
1775
1776 fn flush(&mut self) {
1777 self.pass();
1778 self.event(&ev(PointerPhase::Move, 2.0, 2.0));
1779 }
1780
1781 fn paint(&mut self) -> Recorder {
1782 let mut rec = Recorder::default();
1783 self.root
1784 .paint(&mut rec, FrameTime::from_nanos(4_000_000_000));
1785 rec
1786 }
1787 }
1788
1789 #[test]
1792 fn the_basic_dialog_renders_title_body_and_actions() {
1793 let mut h = Harness::new();
1794 show_dialog(
1795 &h.controller,
1796 || {
1797 dialog()
1798 .title("Confirm")
1799 .body("Are you sure?")
1800 .action(text_button("OK", |_s: &mut NavState| {}))
1801 },
1802 |state: &mut NavState, result: PopResult| {
1803 state.results.push(result.take::<bool>());
1804 },
1805 );
1806 h.pass();
1807 h.settle();
1808 let rec = h.paint();
1809 assert!(
1810 rec.glyphs >= 3,
1811 "title, body, and the action label all shape"
1812 );
1813 }
1814
1815 #[test]
1816 fn the_selection_dialog_renders_options_and_actions() {
1817 let mut h = Harness::new();
1818 show_selection_dialog(
1819 &h.controller,
1820 || {
1821 selection_dialog("Pick one", vec!["A".into(), "B".into(), "C".into()])
1822 .selected(vec!["B".into()])
1823 },
1824 |state: &mut NavState, result: PopResult| {
1825 state.results.push(result.take::<bool>());
1826 },
1827 );
1828 h.pass();
1829 h.settle();
1830 let rec = h.paint();
1831 assert!(rec.glyphs >= 6);
1833 }
1834
1835 #[test]
1836 fn the_full_screen_dialog_renders_header_and_body() {
1837 let mut h = Harness::new();
1838 show_full_screen_dialog(
1839 &h.controller,
1840 || full_screen_dialog("Settings", sized(200.0, 200.0)),
1841 |state: &mut NavState, result: PopResult| {
1842 state.results.push(result.take::<bool>());
1843 },
1844 );
1845 h.pass();
1846 h.settle();
1847 let rec = h.paint();
1848 assert!(rec.glyphs >= 1, "the header title shapes");
1849 assert!(rec.rects.len() >= 2);
1851 }
1852
1853 #[test]
1856 fn scrim_tap_stages_an_exit_then_pops_with_an_empty_result() {
1857 let mut h = Harness::new();
1858 show_dialog(
1859 &h.controller,
1860 || dialog().title("Confirm"),
1861 |state: &mut NavState, result: PopResult| {
1862 state.results.push(result.take::<bool>());
1863 },
1864 );
1865 h.pass();
1866 h.settle();
1867 h.event(&ev(PointerPhase::Down, 5.0, 5.0));
1868 h.event(&ev(PointerPhase::Up, 5.0, 5.0));
1869 h.flush();
1870 assert!(
1871 h.state.results.is_empty(),
1872 "still animating out — no local scrim/pop code fired it directly"
1873 );
1874 h.settle_exit();
1875 h.flush();
1876 assert_eq!(h.state.results, vec![None]);
1877 }
1878
1879 #[test]
1880 fn escape_stages_the_same_exit_once_the_dialog_has_focus() {
1881 let mut h = Harness::new();
1882 show_dialog(
1883 &h.controller,
1884 || dialog().title("Confirm"),
1885 |state: &mut NavState, result: PopResult| {
1886 state.results.push(result.take::<bool>());
1887 },
1888 );
1889 h.pass();
1890 h.settle();
1891 h.event(&ev(PointerPhase::Down, 200.0, 300.0));
1894 h.event(&ev(PointerPhase::Up, 200.0, 300.0));
1895 h.flush();
1896 assert!(h.state.results.is_empty());
1897 h.event(&escape_event());
1898 h.flush();
1899 assert!(h.state.results.is_empty(), "still animating out");
1900 h.settle_exit();
1901 h.flush();
1902 assert_eq!(h.state.results, vec![None]);
1903 }
1904
1905 #[test]
1906 fn a_dismissable_false_dialog_ignores_escape() {
1907 let mut h = Harness::new();
1908 show_dialog(
1909 &h.controller,
1910 || dialog().title("Pinned").dismissable(false),
1911 |state: &mut NavState, result: PopResult| {
1912 state.results.push(result.take::<bool>());
1913 },
1914 );
1915 h.pass();
1916 h.settle();
1917 h.event(&ev(PointerPhase::Down, 200.0, 300.0));
1918 h.event(&ev(PointerPhase::Up, 200.0, 300.0));
1919 h.event(&escape_event());
1920 h.flush();
1921 h.settle_exit();
1922 h.flush();
1923 assert!(
1924 h.state.results.is_empty(),
1925 "a non-dismissable dialog never pops via Escape"
1926 );
1927 }
1928
1929 #[test]
1930 fn an_action_pops_with_a_value_via_the_navigator() {
1931 const ACTION: Size = Size::new(80.0, 36.0);
1938 const PANEL_W: f64 = 368.0;
1939 const PANEL_MARGIN: f64 = 16.0;
1940
1941 let mut h = Harness::new();
1942 h.controller.push_transparent_for_result(
1943 {
1944 let ctrl = h.controller.clone();
1945 move || {
1946 let c = ctrl.clone();
1947 any::<NavState, _>(dialog().action(tap_action(
1948 ACTION.width,
1949 ACTION.height,
1950 move |_s: &mut NavState| c.pop_with_result(PopResult::of(true)),
1951 )))
1952 }
1953 },
1954 frust::TransitionSpec::NONE,
1955 |state: &mut NavState, result: PopResult| {
1956 state.results.push(result.take::<bool>());
1957 },
1958 );
1959 h.pass();
1960 h.settle();
1961
1962 let panel_h = 2.0 * DIALOG_PADDING + ACTION.height;
1966 let panel_y = (WINDOW.height - panel_h) / 2.0;
1967 let action_x = PANEL_MARGIN + PANEL_W - DIALOG_PADDING - ACTION.width;
1968 let action_y = panel_y + DIALOG_PADDING;
1969 let (cx, cy) = (
1970 action_x + ACTION.width / 2.0,
1971 action_y + ACTION.height / 2.0,
1972 );
1973 h.event(&ev(PointerPhase::Down, cx, cy));
1974 h.event(&ev(PointerPhase::Up, cx, cy));
1975 h.flush();
1976 assert_eq!(h.state.results, vec![Some(true)]);
1977 }
1978
1979 #[test]
1980 fn selection_dialog_confirm_reports_the_controlled_selection() {
1981 let mut h = Harness::new();
1982 show_selection_dialog(
1983 &h.controller,
1984 || {
1985 selection_dialog("Pick", vec!["A".into(), "B".into()])
1986 .selected(vec!["A".into()])
1987 .on_toggle(|s: &mut NavState, v: String| s.toggled.push(v))
1988 .on_confirm(|s: &mut NavState| {
1989 s.confirmed = true;
1990 })
1991 },
1992 |state: &mut NavState, result: PopResult| {
1993 state.results.push(result.take::<bool>());
1994 },
1995 );
1996 h.pass();
1997 h.settle();
1998 assert!(!h.state.confirmed);
1999 }
2000
2001 #[test]
2002 fn every_variant_forwards_an_installed_dismiss_handle_to_the_host() {
2003 let dismiss = ModalDismiss::new();
2008 assert!(
2009 dialog::<NavState>()
2010 .dismiss_handle(dismiss.clone())
2011 .compose()
2012 .dismiss_handle
2013 .is_some()
2014 );
2015 assert!(
2016 selection_dialog::<NavState>("Pick", vec!["A".into()])
2017 .dismiss_handle(dismiss.clone())
2018 .compose()
2019 .dismiss_handle
2020 .is_some()
2021 );
2022 assert!(
2023 full_screen_dialog::<NavState, _>("Edit", body_view::<NavState>("body"))
2024 .dismiss_handle(dismiss)
2025 .compose()
2026 .dismiss_handle
2027 .is_some()
2028 );
2029 assert!(
2030 dialog::<NavState>().compose().dismiss_handle.is_none(),
2031 "and none is invented for a variant that was handed none"
2032 );
2033 }
2034
2035 #[test]
2038 fn the_old_builder_shape_still_compiles_and_runs() {
2039 let mut h = Harness::new();
2040 show_dialog(
2041 &h.controller,
2042 || {
2043 dialog()
2044 .title("Invite people")
2045 .body("Send invites to this workspace? (mock)")
2046 .action(text_button("Send", |_s: &mut NavState| {}))
2047 },
2048 |state: &mut NavState, result: PopResult| {
2049 state.results.push(result.take::<bool>());
2050 },
2051 );
2052 h.pass();
2053 h.settle();
2054 assert!(h.state.results.is_empty());
2055 }
2056}