1use iced::advanced::layout::{self, Layout};
52use iced::advanced::widget::operation::Focusable;
53use iced::advanced::widget::{Operation, Tree, Widget, tree};
54use iced::advanced::{Clipboard, Shell, overlay, renderer};
55use iced::keyboard::key::Named;
56use iced::widget::{self, center, column, container, mouse_area, opaque, row, stack, text};
57use iced::{
58 Alignment, Background, Border, Color, Element, Event, Length, Padding, Rectangle, Renderer,
59 Size as Bounds, Theme, Vector, keyboard, mouse,
60};
61
62use crate::keys::{self, Chord, Keymap};
63use crate::overlay::anchored;
64use crate::primitives::button::{self, Size as ButtonSize};
65use crate::primitives::icon_button::icon_button;
66use crate::theme::{Tokens, fade, radius, space, text_size};
67
68pub const DEFAULT_ID: widget::Id = widget::Id::new("iced-cube-dialog");
70
71pub const CLOSE_BUTTON_ID: widget::Id = widget::Id::new("iced-cube-dialog-close");
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
76pub enum Size {
77 Sm,
78 #[default]
79 Md,
80 Lg,
81}
82
83impl Size {
84 pub const ALL: [Size; 3] = [Size::Sm, Size::Md, Size::Lg];
85
86 pub const fn width(self) -> f32 {
88 match self {
89 Size::Sm => 384.0,
90 Size::Md => 512.0,
91 Size::Lg => 640.0,
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub enum Action {
99 Close,
100 Confirm,
101 FocusNext,
102 FocusPrevious,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub enum Direction {
108 Next,
109 Previous,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub enum Effect {
115 Dismiss,
117 Confirm,
119 Focus(Direction),
121}
122
123impl Action {
124 pub fn effect(self, dismiss_on_escape: bool) -> Option<Effect> {
127 match self {
128 Action::Close => dismiss_on_escape.then_some(Effect::Dismiss),
129 Action::Confirm => Some(Effect::Confirm),
130 Action::FocusNext => Some(Effect::Focus(Direction::Next)),
131 Action::FocusPrevious => Some(Effect::Focus(Direction::Previous)),
132 }
133 }
134}
135
136impl keys::Action for Action {
137 const ALL: &'static [Self] = &[
138 Action::Close,
139 Action::Confirm,
140 Action::FocusNext,
141 Action::FocusPrevious,
142 ];
143
144 fn defaults() -> Keymap<Self> {
145 default_keymap()
146 }
147
148 fn name(self) -> &'static str {
149 match self {
150 Action::Close => "Close",
151 Action::Confirm => "Confirm",
152 Action::FocusNext => "FocusNext",
153 Action::FocusPrevious => "FocusPrevious",
154 }
155 }
156
157 fn description(self) -> &'static str {
158 match self {
159 Action::Close => "Dismisses the dialog, unless Escape dismissal is turned off.",
160 Action::Confirm => {
161 "Sends the dialog's confirm message, such as the alert dialog's destructive action."
162 }
163 Action::FocusNext => {
164 "Focuses the next text field in the dialog, wrapping from the last to the first."
165 }
166 Action::FocusPrevious => {
167 "Focuses the previous text field in the dialog, wrapping from the first to the last."
168 }
169 }
170 }
171}
172
173pub fn default_keymap() -> Keymap<Action> {
189 Keymap::new()
190 .bind(Chord::named(Named::Escape), Action::Close)
191 .bind(Chord::named(Named::Enter), Action::Confirm)
192 .bind(Chord::named(Named::Tab), Action::FocusNext)
193 .bind(Chord::named(Named::Tab).shift(), Action::FocusPrevious)
194}
195
196pub struct Dialog<'a, Message> {
198 base: Element<'a, Message>,
199 open: bool,
200 title: Option<text::Fragment<'a>>,
201 description: Option<text::Fragment<'a>>,
202 body: Option<Element<'a, Message>>,
203 actions: Vec<Element<'a, Message>>,
204 on_dismiss: Option<Message>,
205 on_confirm: Option<Message>,
206 dismiss_on_escape: bool,
207 dismiss_on_scrim: bool,
208 close_button: bool,
209 width: f32,
210 id: widget::Id,
211 keymap: Keymap<Action>,
212 pass_through: Vec<Chord>,
213}
214
215impl<Message> std::fmt::Debug for Dialog<'_, Message> {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 f.debug_struct("Dialog")
218 .field("open", &self.open)
219 .field("title", &self.title)
220 .field("description", &self.description)
221 .field("actions", &self.actions.len())
222 .field("dismiss_on_escape", &self.dismiss_on_escape)
223 .field("dismiss_on_scrim", &self.dismiss_on_scrim)
224 .field("close_button", &self.close_button)
225 .field("width", &self.width)
226 .field("id", &self.id)
227 .field("pass_through", &self.pass_through)
228 .finish_non_exhaustive()
229 }
230}
231
232pub fn dialog<'a, Message>(base: impl Into<Element<'a, Message>>) -> Dialog<'a, Message> {
235 Dialog {
236 base: base.into(),
237 open: false,
238 title: None,
239 description: None,
240 body: None,
241 actions: Vec::new(),
242 on_dismiss: None,
243 on_confirm: None,
244 dismiss_on_escape: true,
245 dismiss_on_scrim: true,
246 close_button: true,
247 width: Size::default().width(),
248 id: DEFAULT_ID,
249 keymap: default_keymap(),
250 pass_through: Vec::new(),
251 }
252}
253
254impl<'a, Message> Dialog<'a, Message> {
255 pub fn open(mut self, open: bool) -> Self {
256 self.open = open;
257 self
258 }
259
260 pub fn title(mut self, title: impl text::IntoFragment<'a>) -> Self {
261 self.title = Some(title.into_fragment());
262 self
263 }
264
265 pub fn description(mut self, description: impl text::IntoFragment<'a>) -> Self {
267 self.description = Some(description.into_fragment());
268 self
269 }
270
271 pub fn body(mut self, body: impl Into<Element<'a, Message>>) -> Self {
273 self.body = Some(body.into());
274 self
275 }
276
277 pub fn action(mut self, action: impl Into<Element<'a, Message>>) -> Self {
280 self.actions.push(action.into());
281 self
282 }
283
284 pub fn on_dismiss(mut self, message: Message) -> Self {
288 self.on_dismiss = Some(message);
289 self
290 }
291
292 pub fn dismiss_on_escape(mut self, dismiss: bool) -> Self {
294 self.dismiss_on_escape = dismiss;
295 self
296 }
297
298 pub fn on_confirm(mut self, message: Message) -> Self {
302 self.on_confirm = Some(message);
303 self
304 }
305
306 pub fn dismiss_on_scrim(mut self, dismiss: bool) -> Self {
309 self.dismiss_on_scrim = dismiss;
310 self
311 }
312
313 pub fn close_button(mut self, show: bool) -> Self {
316 self.close_button = show;
317 self
318 }
319
320 pub fn size(mut self, size: Size) -> Self {
322 self.width = size.width();
323 self
324 }
325
326 pub fn width(mut self, width: f32) -> Self {
328 self.width = width;
329 self
330 }
331
332 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
335 self.id = id.into();
336 self
337 }
338
339 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
341 self.keymap = keymap;
342 self
343 }
344
345 pub fn pass_through(mut self, chords: impl IntoIterator<Item = Chord>) -> Self {
348 self.pass_through = chords.into_iter().collect();
349 self
350 }
351
352 pub fn is_open(&self) -> bool {
353 self.open
354 }
355}
356
357impl<'a, Message: Clone + 'a> From<Dialog<'a, Message>> for Element<'a, Message> {
358 fn from(dialog: Dialog<'a, Message>) -> Self {
359 let Dialog {
360 base,
361 open,
362 title,
363 description,
364 body,
365 actions,
366 on_dismiss,
367 on_confirm,
368 dismiss_on_escape,
369 dismiss_on_scrim,
370 close_button,
371 width,
372 id,
373 keymap,
374 pass_through,
375 } = dialog;
376
377 let guarded = Element::new(Guard {
378 content: base,
379 blocked: open,
380 });
381
382 let mut layers = stack![guarded].width(Length::Fill).height(Length::Fill);
385
386 if open {
387 let surface = surface(
388 title,
389 description,
390 body,
391 actions,
392 close_button.then(|| on_dismiss.clone()),
393 width,
394 id.clone(),
395 );
396 let scrim = center(opaque(surface))
397 .padding(space::LG)
398 .style(|theme| scrim_style(&Tokens::of(theme)));
399 let layer: Element<'a, Message> = match on_dismiss.clone().filter(|_| dismiss_on_scrim)
400 {
401 Some(message) => opaque(mouse_area(scrim).on_press(message)),
402 None => opaque(scrim),
403 };
404 layers = layers.push(layer);
405 }
406
407 Element::new(Scope {
408 content: layers.into(),
409 open,
410 surface: id,
411 keymap,
412 pass_through,
413 on_escape: on_dismiss.filter(|_| dismiss_on_escape),
414 on_confirm,
415 })
416 }
417}
418
419fn surface<'a, Message: Clone + 'a>(
420 title: Option<text::Fragment<'a>>,
421 description: Option<text::Fragment<'a>>,
422 body: Option<Element<'a, Message>>,
423 actions: Vec<Element<'a, Message>>,
424 close: Option<Option<Message>>,
425 width: f32,
426 id: widget::Id,
427) -> Element<'a, Message> {
428 let mut header = column![].spacing(space::XS);
429 if let Some(title) = title {
430 header = header.push(
431 text(title)
432 .size(text_size::LG)
433 .font(crate::theme::semibold()),
434 );
435 }
436 if let Some(description) = description {
437 header = header.push(
438 text(description)
439 .size(text_size::SM)
440 .style(|theme: &Theme| text::Style {
441 color: Some(Tokens::of(theme).muted_foreground),
442 }),
443 );
444 }
445 if close.is_some() {
447 header = header.padding(Padding::ZERO.right(space::XL));
448 }
449
450 let mut content = column![header].spacing(space::LG).width(Length::Fill);
451 if let Some(body) = body {
452 content = content.push(body);
453 }
454 if !actions.is_empty() {
455 content = content.push(
456 container(row(actions).spacing(space::SM))
457 .width(Length::Fill)
458 .align_x(Alignment::End),
459 );
460 }
461
462 let mut layers = stack![container(content).padding(space::XL)];
463 if let Some(on_press) = close {
464 layers = layers.push(
465 container(
466 icon_button(crate::lucide!(X))
467 .label("Close")
468 .tooltip(None)
469 .size(ButtonSize::Sm)
470 .id(CLOSE_BUTTON_ID)
471 .on_press_maybe(on_press),
472 )
473 .padding(space::MD)
474 .width(Length::Fill)
475 .align_x(Alignment::End),
476 );
477 }
478
479 container(layers)
480 .id(id)
481 .width(Length::Fill)
482 .max_width(width)
483 .style(|theme| surface_style(&Tokens::of(theme)))
484 .into()
485}
486
487pub fn surface_style(tokens: &Tokens) -> container::Style {
490 let style = anchored::surface_style(tokens);
491 container::Style {
492 border: Border {
493 radius: radius::LG.into(),
494 ..style.border
495 },
496 ..style
497 }
498}
499
500pub fn scrim_style(tokens: &Tokens) -> container::Style {
503 container::Style {
504 background: Some(Background::Color(fade(
505 Color::BLACK,
506 if tokens.is_dark { 0.7 } else { 0.5 },
507 ))),
508 ..container::Style::default()
509 }
510}
511
512struct Guard<'a, Message> {
515 content: Element<'a, Message>,
516 blocked: bool,
517}
518
519impl<Message> Widget<Message, Theme, Renderer> for Guard<'_, Message> {
520 fn size(&self) -> Bounds<Length> {
521 self.content.as_widget().size()
522 }
523
524 fn size_hint(&self) -> Bounds<Length> {
525 self.content.as_widget().size_hint()
526 }
527
528 fn tag(&self) -> tree::Tag {
529 self.content.as_widget().tag()
530 }
531
532 fn state(&self) -> tree::State {
533 self.content.as_widget().state()
534 }
535
536 fn children(&self) -> Vec<Tree> {
537 self.content.as_widget().children()
538 }
539
540 fn diff(&self, tree: &mut Tree) {
541 self.content.as_widget().diff(tree);
542 }
543
544 fn layout(
545 &mut self,
546 tree: &mut Tree,
547 renderer: &Renderer,
548 limits: &layout::Limits,
549 ) -> layout::Node {
550 self.content.as_widget_mut().layout(tree, renderer, limits)
551 }
552
553 fn draw(
554 &self,
555 tree: &Tree,
556 renderer: &mut Renderer,
557 theme: &Theme,
558 style: &renderer::Style,
559 layout: Layout<'_>,
560 cursor: mouse::Cursor,
561 viewport: &Rectangle,
562 ) {
563 let cursor = if self.blocked {
564 mouse::Cursor::Unavailable
565 } else {
566 cursor
567 };
568 self.content
569 .as_widget()
570 .draw(tree, renderer, theme, style, layout, cursor, viewport);
571 }
572
573 fn operate(
574 &mut self,
575 tree: &mut Tree,
576 layout: Layout<'_>,
577 renderer: &Renderer,
578 operation: &mut dyn Operation,
579 ) {
580 self.content
581 .as_widget_mut()
582 .operate(tree, layout, renderer, operation);
583 }
584
585 fn update(
586 &mut self,
587 tree: &mut Tree,
588 event: &Event,
589 layout: Layout<'_>,
590 cursor: mouse::Cursor,
591 renderer: &Renderer,
592 clipboard: &mut dyn Clipboard,
593 shell: &mut Shell<'_, Message>,
594 viewport: &Rectangle,
595 ) {
596 if !self.blocked {
597 self.content.as_widget_mut().update(
598 tree, event, layout, cursor, renderer, clipboard, shell, viewport,
599 );
600 return;
601 }
602 if !matches!(event, Event::Window(_)) {
604 return;
605 }
606 self.content.as_widget_mut().update(
607 tree,
608 event,
609 layout,
610 mouse::Cursor::Unavailable,
611 renderer,
612 clipboard,
613 shell,
614 viewport,
615 );
616 }
617
618 fn mouse_interaction(
619 &self,
620 tree: &Tree,
621 layout: Layout<'_>,
622 cursor: mouse::Cursor,
623 viewport: &Rectangle,
624 renderer: &Renderer,
625 ) -> mouse::Interaction {
626 if self.blocked {
627 return mouse::Interaction::None;
628 }
629 self.content
630 .as_widget()
631 .mouse_interaction(tree, layout, cursor, viewport, renderer)
632 }
633
634 fn overlay<'b>(
635 &'b mut self,
636 tree: &'b mut Tree,
637 layout: Layout<'b>,
638 renderer: &Renderer,
639 viewport: &Rectangle,
640 translation: Vector,
641 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
642 if self.blocked {
643 return None;
644 }
645 self.content
646 .as_widget_mut()
647 .overlay(tree, layout, renderer, viewport, translation)
648 }
649}
650
651struct Scope<'a, Message> {
654 content: Element<'a, Message>,
655 open: bool,
656 surface: widget::Id,
657 keymap: Keymap<Action>,
658 pass_through: Vec<Chord>,
659 on_escape: Option<Message>,
660 on_confirm: Option<Message>,
661}
662
663fn key_of(event: &Event) -> Option<(&keyboard::Key, keyboard::Modifiers, bool)> {
665 match event {
666 Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => {
667 Some((key, *modifiers, true))
668 }
669 Event::Keyboard(keyboard::Event::KeyReleased { key, modifiers, .. }) => {
670 Some((key, *modifiers, false))
671 }
672 _ => None,
673 }
674}
675
676#[derive(Debug, Default)]
678struct ScopeState {
679 entered: bool,
680}
681
682impl<Message> Scope<'_, Message> {
683 fn move_focus(
684 &mut self,
685 tree: &mut Tree,
686 layout: Layout<'_>,
687 renderer: &Renderer,
688 movement: Movement,
689 ) {
690 let mut survey = Survey::new(self.surface.clone());
691 self.content
692 .as_widget_mut()
693 .operate(tree, layout, renderer, &mut survey);
694
695 let mut apply = Apply {
696 target: target(&survey.slots, movement),
697 index: 0,
698 };
699 self.content
700 .as_widget_mut()
701 .operate(tree, layout, renderer, &mut apply);
702 }
703}
704
705impl<Message: Clone> Widget<Message, Theme, Renderer> for Scope<'_, Message> {
706 fn size(&self) -> Bounds<Length> {
707 self.content.as_widget().size()
708 }
709
710 fn size_hint(&self) -> Bounds<Length> {
711 self.content.as_widget().size_hint()
712 }
713
714 fn tag(&self) -> tree::Tag {
715 tree::Tag::of::<ScopeState>()
716 }
717
718 fn state(&self) -> tree::State {
719 tree::State::new(ScopeState::default())
720 }
721
722 fn children(&self) -> Vec<Tree> {
723 vec![Tree::new(&self.content)]
724 }
725
726 fn diff(&self, tree: &mut Tree) {
727 tree.diff_children(std::slice::from_ref(&self.content));
728 }
729
730 fn layout(
731 &mut self,
732 tree: &mut Tree,
733 renderer: &Renderer,
734 limits: &layout::Limits,
735 ) -> layout::Node {
736 self.content
737 .as_widget_mut()
738 .layout(&mut tree.children[0], renderer, limits)
739 }
740
741 fn draw(
742 &self,
743 tree: &Tree,
744 renderer: &mut Renderer,
745 theme: &Theme,
746 style: &renderer::Style,
747 layout: Layout<'_>,
748 cursor: mouse::Cursor,
749 viewport: &Rectangle,
750 ) {
751 self.content.as_widget().draw(
752 &tree.children[0],
753 renderer,
754 theme,
755 style,
756 layout,
757 cursor,
758 viewport,
759 );
760 }
761
762 fn operate(
763 &mut self,
764 tree: &mut Tree,
765 layout: Layout<'_>,
766 renderer: &Renderer,
767 operation: &mut dyn Operation,
768 ) {
769 self.content
770 .as_widget_mut()
771 .operate(&mut tree.children[0], layout, renderer, operation);
772 }
773
774 fn update(
775 &mut self,
776 tree: &mut Tree,
777 event: &Event,
778 layout: Layout<'_>,
779 cursor: mouse::Cursor,
780 renderer: &Renderer,
781 clipboard: &mut dyn Clipboard,
782 shell: &mut Shell<'_, Message>,
783 viewport: &Rectangle,
784 ) {
785 let state = tree.state.downcast_mut::<ScopeState>();
786 let entering = self.open && !state.entered;
787 state.entered = self.open;
788
789 let content = &mut tree.children[0];
790 if entering {
791 self.move_focus(content, layout, renderer, Movement::First);
792 shell.request_redraw();
793 }
794
795 self.content.as_widget_mut().update(
796 content, event, layout, cursor, renderer, clipboard, shell, viewport,
797 );
798 if !self.open || shell.is_event_captured() {
799 return;
800 }
801
802 let Some((key, modifiers, pressed)) = key_of(event) else {
803 return;
804 };
805 if self
806 .pass_through
807 .iter()
808 .any(|chord| chord.matches(key, modifiers))
809 {
810 return;
811 }
812 shell.capture_event();
813 if !pressed {
814 return;
815 }
816 let Some(effect) = self
817 .keymap
818 .resolve(key, modifiers)
819 .and_then(|action| action.effect(self.on_escape.is_some()))
820 else {
821 return;
822 };
823
824 match effect {
825 Effect::Dismiss => {
826 let Some(message) = self.on_escape.clone() else {
827 return;
828 };
829 shell.publish(message);
830 }
831 Effect::Confirm => {
832 let Some(message) = self.on_confirm.clone() else {
833 return;
834 };
835 shell.publish(message);
836 }
837 Effect::Focus(direction) => {
838 self.move_focus(content, layout, renderer, Movement::from(direction));
839 shell.request_redraw();
840 }
841 }
842 }
843
844 fn mouse_interaction(
845 &self,
846 tree: &Tree,
847 layout: Layout<'_>,
848 cursor: mouse::Cursor,
849 viewport: &Rectangle,
850 renderer: &Renderer,
851 ) -> mouse::Interaction {
852 self.content.as_widget().mouse_interaction(
853 &tree.children[0],
854 layout,
855 cursor,
856 viewport,
857 renderer,
858 )
859 }
860
861 fn overlay<'b>(
862 &'b mut self,
863 tree: &'b mut Tree,
864 layout: Layout<'b>,
865 renderer: &Renderer,
866 viewport: &Rectangle,
867 translation: Vector,
868 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
869 self.content.as_widget_mut().overlay(
870 &mut tree.children[0],
871 layout,
872 renderer,
873 viewport,
874 translation,
875 )
876 }
877}
878
879#[derive(Debug, Clone, Copy, PartialEq, Eq)]
881enum Movement {
882 First,
883 Next,
884 Previous,
885}
886
887impl From<Direction> for Movement {
888 fn from(direction: Direction) -> Self {
889 match direction {
890 Direction::Next => Movement::Next,
891 Direction::Previous => Movement::Previous,
892 }
893 }
894}
895
896#[derive(Debug, Clone, Copy, PartialEq, Eq)]
898struct Slot {
899 inside: bool,
900 focused: bool,
901}
902
903fn target(slots: &[Slot], movement: Movement) -> Option<usize> {
906 let inside: Vec<usize> = (0..slots.len()).filter(|&i| slots[i].inside).collect();
907 let (first, last) = (inside.first().copied()?, inside.last().copied()?);
908 let current = inside.iter().position(|&i| slots[i].focused);
909 let count = inside.len();
910
911 match (movement, current) {
912 (Movement::First, _) | (Movement::Next, None) => Some(first),
913 (Movement::Previous, None) => Some(last),
914 (Movement::Next, Some(at)) => inside.get((at + 1) % count).copied(),
915 (Movement::Previous, Some(at)) => inside.get((at + count - 1) % count).copied(),
916 }
917}
918
919struct Survey {
921 scope: widget::Id,
922 entering: bool,
923 depth: usize,
924 slots: Vec<Slot>,
925}
926
927impl Survey {
928 fn new(scope: widget::Id) -> Self {
929 Self {
930 scope,
931 entering: false,
932 depth: 0,
933 slots: Vec::new(),
934 }
935 }
936}
937
938impl Operation for Survey {
939 fn container(&mut self, id: Option<&widget::Id>, _bounds: Rectangle) {
940 if id == Some(&self.scope) {
941 self.entering = true;
942 }
943 }
944
945 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) {
948 let entered = std::mem::take(&mut self.entering);
949 self.depth += usize::from(entered);
950 operate(self);
951 self.depth -= usize::from(entered);
952 }
953
954 fn focusable(
955 &mut self,
956 _id: Option<&widget::Id>,
957 _bounds: Rectangle,
958 state: &mut dyn Focusable,
959 ) {
960 self.slots.push(Slot {
961 inside: self.depth > 0,
962 focused: state.is_focused(),
963 });
964 }
965}
966
967struct Apply {
969 target: Option<usize>,
970 index: usize,
971}
972
973impl Operation for Apply {
974 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) {
975 operate(self);
976 }
977
978 fn focusable(
979 &mut self,
980 _id: Option<&widget::Id>,
981 _bounds: Rectangle,
982 state: &mut dyn Focusable,
983 ) {
984 let is_target = self.target == Some(self.index);
985 if is_target && !state.is_focused() {
986 state.focus();
987 } else if !is_target && state.is_focused() {
988 state.unfocus();
989 }
990 self.index += 1;
991 }
992}
993
994pub struct AlertDialog<'a, Message> {
998 base: Element<'a, Message>,
999 open: bool,
1000 title: text::Fragment<'a>,
1001 description: text::Fragment<'a>,
1002 cancel: text::Fragment<'a>,
1003 confirm: text::Fragment<'a>,
1004 on_cancel: Option<Message>,
1005 on_confirm: Option<Message>,
1006 dismiss_on_escape: bool,
1007 size: Size,
1008 keymap: Keymap<Action>,
1009}
1010
1011impl<Message> std::fmt::Debug for AlertDialog<'_, Message> {
1012 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1013 f.debug_struct("AlertDialog")
1014 .field("open", &self.open)
1015 .field("title", &self.title)
1016 .field("description", &self.description)
1017 .field("cancel", &self.cancel)
1018 .field("confirm", &self.confirm)
1019 .field("dismiss_on_escape", &self.dismiss_on_escape)
1020 .field("size", &self.size)
1021 .finish_non_exhaustive()
1022 }
1023}
1024
1025pub fn alert_dialog<'a, Message>(
1028 base: impl Into<Element<'a, Message>>,
1029 title: impl text::IntoFragment<'a>,
1030 description: impl text::IntoFragment<'a>,
1031) -> AlertDialog<'a, Message> {
1032 AlertDialog {
1033 base: base.into(),
1034 open: false,
1035 title: title.into_fragment(),
1036 description: description.into_fragment(),
1037 cancel: "Cancel".into(),
1038 confirm: "Continue".into(),
1039 on_cancel: None,
1040 on_confirm: None,
1041 dismiss_on_escape: true,
1042 size: Size::default(),
1043 keymap: default_keymap(),
1044 }
1045}
1046
1047impl<'a, Message> AlertDialog<'a, Message> {
1048 pub fn open(mut self, open: bool) -> Self {
1049 self.open = open;
1050 self
1051 }
1052
1053 pub fn cancel(mut self, label: impl text::IntoFragment<'a>) -> Self {
1055 self.cancel = label.into_fragment();
1056 self
1057 }
1058
1059 pub fn confirm(mut self, label: impl text::IntoFragment<'a>) -> Self {
1061 self.confirm = label.into_fragment();
1062 self
1063 }
1064
1065 pub fn on_cancel(mut self, message: Message) -> Self {
1067 self.on_cancel = Some(message);
1068 self
1069 }
1070
1071 pub fn on_confirm(mut self, message: Message) -> Self {
1073 self.on_confirm = Some(message);
1074 self
1075 }
1076
1077 pub fn dismiss_on_escape(mut self, dismiss: bool) -> Self {
1079 self.dismiss_on_escape = dismiss;
1080 self
1081 }
1082
1083 pub fn size(mut self, size: Size) -> Self {
1084 self.size = size;
1085 self
1086 }
1087
1088 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
1090 self.keymap = keymap;
1091 self
1092 }
1093}
1094
1095impl<'a, Message: Clone + 'a> From<AlertDialog<'a, Message>> for Dialog<'a, Message> {
1096 fn from(alert: AlertDialog<'a, Message>) -> Self {
1097 let mut dialog = dialog(alert.base)
1098 .open(alert.open)
1099 .title(alert.title)
1100 .description(alert.description)
1101 .action(
1102 button::button(alert.cancel)
1103 .variant(button::Variant::Outline)
1104 .on_press_maybe(alert.on_cancel.clone()),
1105 )
1106 .action(
1107 button::button(alert.confirm)
1108 .variant(button::Variant::Destructive)
1109 .on_press_maybe(alert.on_confirm.clone()),
1110 )
1111 .dismiss_on_escape(alert.dismiss_on_escape)
1112 .dismiss_on_scrim(false)
1113 .close_button(false)
1114 .size(alert.size)
1115 .keymap(alert.keymap);
1116 if let Some(message) = alert.on_cancel {
1117 dialog = dialog.on_dismiss(message);
1118 }
1119 if let Some(message) = alert.on_confirm {
1120 dialog = dialog.on_confirm(message);
1121 }
1122 dialog
1123 }
1124}
1125
1126impl<'a, Message: Clone + 'a> From<AlertDialog<'a, Message>> for Element<'a, Message> {
1127 fn from(alert: AlertDialog<'a, Message>) -> Self {
1128 Dialog::from(alert).into()
1129 }
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use super::*;
1135 use crate::theme::{dark, light};
1136
1137 fn slots(layout: &[(bool, bool)]) -> Vec<Slot> {
1138 layout
1139 .iter()
1140 .map(|&(inside, focused)| Slot { inside, focused })
1141 .collect()
1142 }
1143
1144 #[test]
1145 fn builder_defaults() {
1146 let d: Dialog<'_, ()> = dialog(text("Base"));
1147 assert!(!d.is_open());
1148 assert!(d.dismiss_on_escape);
1149 assert!(d.dismiss_on_scrim);
1150 assert!(d.close_button);
1151 assert!(d.on_dismiss.is_none());
1152 assert_eq!(d.width, Size::Md.width());
1153 assert_eq!(d.id, DEFAULT_ID);
1154 assert_eq!(d.keymap, default_keymap());
1155 assert!(d.pass_through.is_empty());
1156 }
1157
1158 #[test]
1159 fn builder_sets_every_option() {
1160 let d = dialog::<u8>(text("Base"))
1161 .open(true)
1162 .title("Title")
1163 .description("Description")
1164 .body(text("Body"))
1165 .action(text("One"))
1166 .action(text("Two"))
1167 .on_dismiss(1)
1168 .dismiss_on_escape(false)
1169 .dismiss_on_scrim(false)
1170 .close_button(false)
1171 .size(Size::Lg)
1172 .id("custom")
1173 .keymap(Keymap::new())
1174 .pass_through([Chord::character('k').command()]);
1175 assert_eq!(d.pass_through, vec![Chord::character('k').command()]);
1176 assert!(d.is_open());
1177 assert_eq!(d.title.as_deref(), Some("Title"));
1178 assert_eq!(d.description.as_deref(), Some("Description"));
1179 assert!(d.body.is_some());
1180 assert_eq!(d.actions.len(), 2);
1181 assert_eq!(d.on_dismiss, Some(1));
1182 assert!(!d.dismiss_on_escape && !d.dismiss_on_scrim && !d.close_button);
1183 assert_eq!(d.width, 640.0);
1184 assert_eq!(d.id, widget::Id::new("custom"));
1185 assert!(d.keymap.is_empty());
1186 assert_eq!(dialog::<u8>(text("Base")).width(300.0).width, 300.0);
1187 }
1188
1189 #[test]
1190 fn sizes_grow_monotonically() {
1191 let widths = Size::ALL.map(Size::width);
1192 assert!(widths.windows(2).all(|w| w[0] < w[1]));
1193 assert_eq!(Size::default(), Size::Md);
1194 }
1195
1196 #[test]
1197 fn alert_dialog_needs_an_explicit_answer() {
1198 let alert = alert_dialog::<u8>(text("Base"), "Delete file?", "This cannot be undone.")
1199 .open(true)
1200 .confirm("Delete")
1201 .on_cancel(1)
1202 .on_confirm(2);
1203 assert_eq!(alert.cancel.as_ref(), "Cancel");
1204 assert_eq!(alert.confirm.as_ref(), "Delete");
1205
1206 let d = Dialog::from(alert);
1207 assert!(d.is_open());
1208 assert!(!d.dismiss_on_scrim);
1209 assert!(!d.close_button);
1210 assert!(d.dismiss_on_escape);
1211 assert_eq!(d.on_dismiss, Some(1));
1212 assert_eq!(d.on_confirm, Some(2), "Enter confirms");
1213 assert_eq!(d.actions.len(), 2);
1214 }
1215
1216 #[test]
1217 fn alert_dialog_without_cancel_has_no_dismiss() {
1218 let d = Dialog::from(
1219 alert_dialog::<u8>(text("Base"), "Title", "Description").dismiss_on_escape(false),
1220 );
1221 assert!(d.on_dismiss.is_none());
1222 assert!(d.on_confirm.is_none());
1223 assert!(!d.dismiss_on_escape);
1224 }
1225
1226 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
1227 let chord: Chord = chord.parse().ok()?;
1228 keymap.resolve(chord.key(), chord.modifiers())
1229 }
1230
1231 #[test]
1232 fn default_keymap_and_overrides() {
1233 let keymap = default_keymap();
1234 assert_eq!(press(&keymap, "Escape"), Some(Action::Close));
1235 assert_eq!(press(&keymap, "Enter"), Some(Action::Confirm));
1236 assert_eq!(press(&keymap, "Shift+Enter"), None);
1237 assert_eq!(press(&keymap, "Tab"), Some(Action::FocusNext));
1238 assert_eq!(press(&keymap, "Shift+Tab"), Some(Action::FocusPrevious));
1239 assert_eq!(press(&keymap, "Ctrl+Tab"), None);
1240
1241 let custom = keymap
1242 .unbind(&Chord::named(Named::Escape))
1243 .bind(Chord::character('w').command(), Action::Close);
1244 assert_eq!(press(&custom, "Escape"), None);
1245 assert_eq!(press(&custom, "Mod+W"), Some(Action::Close));
1246 assert!(
1247 custom
1248 .unbind_action(&Action::Close)
1249 .chords(&Action::Close)
1250 .is_empty()
1251 );
1252 }
1253
1254 #[test]
1255 fn close_only_dismisses_when_escape_is_allowed() {
1256 assert_eq!(Action::Close.effect(true), Some(Effect::Dismiss));
1257 assert_eq!(Action::Close.effect(false), None);
1258 for allowed in [true, false] {
1259 assert_eq!(Action::Confirm.effect(allowed), Some(Effect::Confirm));
1260 assert_eq!(
1261 Action::FocusNext.effect(allowed),
1262 Some(Effect::Focus(Direction::Next))
1263 );
1264 assert_eq!(
1265 Action::FocusPrevious.effect(allowed),
1266 Some(Effect::Focus(Direction::Previous))
1267 );
1268 }
1269 }
1270
1271 #[test]
1272 fn every_action_has_a_name_and_a_sentence() {
1273 use keys::Action as _;
1274 for action in Action::ALL {
1275 assert!(!action.name().is_empty());
1276 assert!(action.description().ends_with('.'));
1277 }
1278 }
1279
1280 #[test]
1281 fn first_focuses_the_first_field_inside() {
1282 let layout = slots(&[(false, true), (true, false), (true, false)]);
1283 assert_eq!(target(&layout, Movement::First), Some(1));
1284 }
1285
1286 #[test]
1287 fn next_and_previous_wrap_inside_the_dialog() {
1288 let last = slots(&[(false, false), (true, false), (true, true), (false, false)]);
1289 assert_eq!(target(&last, Movement::Next), Some(1));
1290 assert_eq!(target(&last, Movement::Previous), Some(1));
1291
1292 let first = slots(&[(true, true), (true, false), (true, false)]);
1293 assert_eq!(target(&first, Movement::Previous), Some(2));
1294 assert_eq!(target(&first, Movement::Next), Some(1));
1295 }
1296
1297 #[test]
1298 fn focus_outside_moves_to_either_end_inside() {
1299 let layout = slots(&[(false, true), (true, false), (true, false), (false, false)]);
1300 assert_eq!(target(&layout, Movement::Next), Some(1));
1301 assert_eq!(target(&layout, Movement::Previous), Some(2));
1302 }
1303
1304 #[test]
1305 fn nothing_to_focus_without_fields_inside() {
1306 let layout = slots(&[(false, true), (false, false)]);
1307 for movement in [Movement::First, Movement::Next, Movement::Previous] {
1308 assert_eq!(target(&layout, movement), None);
1309 }
1310 assert_eq!(target(&[], Movement::Next), None);
1311 }
1312
1313 #[test]
1314 fn a_single_field_keeps_focus() {
1315 let layout = slots(&[(true, true)]);
1316 assert_eq!(target(&layout, Movement::Next), Some(0));
1317 assert_eq!(target(&layout, Movement::Previous), Some(0));
1318 }
1319
1320 #[test]
1321 fn surface_uses_the_shared_floating_style() {
1322 for theme in [light(), dark()] {
1323 let tokens = Tokens::of(&theme);
1324 let style = surface_style(&tokens);
1325 let shared = anchored::surface_style(&tokens);
1326 assert_eq!(style.background, Some(Background::Color(tokens.popover)));
1327 assert_eq!(style.text_color, Some(tokens.foreground));
1328 assert_eq!(style.border.color, tokens.border);
1329 assert_eq!(style.border.width, 1.0);
1330 assert_eq!(style.border.radius, radius::LG.into());
1331 assert_eq!(style.shadow, shared.shadow);
1332 }
1333 }
1334
1335 #[test]
1336 fn dark_theme_has_a_stronger_shadow_and_scrim() {
1337 let light = Tokens::of(&light());
1338 let dark = Tokens::of(&dark());
1339 assert!(surface_style(&dark).shadow.color.a > surface_style(&light).shadow.color.a);
1340
1341 let alpha = |tokens: &Tokens| match scrim_style(tokens).background {
1342 Some(Background::Color(colour)) => colour.a,
1343 _ => 0.0,
1344 };
1345 assert!((alpha(&light) - 0.5).abs() < 1e-6);
1346 assert!((alpha(&dark) - 0.7).abs() < 1e-6);
1347 }
1348}