1use iced::advanced::layout::{self, Layout};
54use iced::advanced::widget::operation::Focusable;
55use iced::advanced::widget::{Operation, Tree, Widget, tree};
56use iced::advanced::{Clipboard, Renderer as _, Shell, overlay, renderer};
57use iced::keyboard::key::Named;
58use iced::widget::{self, center, column, container, mouse_area, opaque, row, stack, text};
59use iced::{
60 Alignment, Background, Border, Color, Element, Event, Length, Padding, Rectangle, Renderer,
61 Size as Bounds, Theme, Vector, keyboard, mouse,
62};
63
64use crate::keys::{self, Chord, Keymap};
65use crate::overlay::anchored;
66use crate::primitives::button::{self, Size as ButtonSize};
67use crate::primitives::icon_button::icon_button;
68use crate::theme::{Tokens, fade, radius, space, text_size};
69
70pub const DEFAULT_ID: widget::Id = widget::Id::new("iced-cube-dialog");
72
73pub const CLOSE_BUTTON_ID: widget::Id = widget::Id::new("iced-cube-dialog-close");
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
78pub enum Size {
79 Sm,
80 #[default]
81 Md,
82 Lg,
83}
84
85impl Size {
86 pub const ALL: [Size; 3] = [Size::Sm, Size::Md, Size::Lg];
87
88 pub const fn width(self) -> f32 {
90 match self {
91 Size::Sm => 384.0,
92 Size::Md => 512.0,
93 Size::Lg => 640.0,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100pub enum Action {
101 Close,
102 Confirm,
103 FocusNext,
104 FocusPrevious,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109pub enum Direction {
110 Next,
111 Previous,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
116pub enum Effect {
117 Dismiss,
119 Confirm,
121 Focus(Direction),
123}
124
125impl Action {
126 pub fn effect(self, dismiss_on_escape: bool) -> Option<Effect> {
129 match self {
130 Action::Close => dismiss_on_escape.then_some(Effect::Dismiss),
131 Action::Confirm => Some(Effect::Confirm),
132 Action::FocusNext => Some(Effect::Focus(Direction::Next)),
133 Action::FocusPrevious => Some(Effect::Focus(Direction::Previous)),
134 }
135 }
136}
137
138impl keys::Action for Action {
139 const ALL: &'static [Self] = &[
140 Action::Close,
141 Action::Confirm,
142 Action::FocusNext,
143 Action::FocusPrevious,
144 ];
145
146 fn defaults() -> Keymap<Self> {
147 default_keymap()
148 }
149
150 fn name(self) -> &'static str {
151 match self {
152 Action::Close => "Close",
153 Action::Confirm => "Confirm",
154 Action::FocusNext => "FocusNext",
155 Action::FocusPrevious => "FocusPrevious",
156 }
157 }
158
159 fn description(self) -> &'static str {
160 match self {
161 Action::Close => "Dismisses the dialog, unless Escape dismissal is turned off.",
162 Action::Confirm => {
163 "Sends the dialog's confirm message. An alert dialog's destructive action only runs on Enter when it opts in."
164 }
165 Action::FocusNext => {
166 "Focuses the next text field, or alert dialog button, wrapping from the last to the first."
167 }
168 Action::FocusPrevious => {
169 "Focuses the previous text field, or alert dialog button, wrapping from the first to the last."
170 }
171 }
172 }
173}
174
175pub fn default_keymap() -> Keymap<Action> {
192 Keymap::new()
193 .bind(Chord::named(Named::Escape), Action::Close)
194 .bind(Chord::named(Named::Enter), Action::Confirm)
195 .bind(Chord::named(Named::Tab), Action::FocusNext)
196 .bind(Chord::named(Named::Tab).shift(), Action::FocusPrevious)
197}
198
199pub struct Dialog<'a, Message> {
201 base: Element<'a, Message>,
202 open: bool,
203 title: Option<text::Fragment<'a>>,
204 description: Option<text::Fragment<'a>>,
205 body: Option<Element<'a, Message>>,
206 actions: Vec<Element<'a, Message>>,
207 on_dismiss: Option<Message>,
208 on_confirm: Option<Message>,
209 dismiss_on_escape: bool,
210 dismiss_on_scrim: bool,
211 close_button: bool,
212 width: f32,
213 id: widget::Id,
214 keymap: Keymap<Action>,
215 pass_through: Vec<Chord>,
216}
217
218impl<Message> std::fmt::Debug for Dialog<'_, Message> {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 f.debug_struct("Dialog")
221 .field("open", &self.open)
222 .field("title", &self.title)
223 .field("description", &self.description)
224 .field("actions", &self.actions.len())
225 .field("dismiss_on_escape", &self.dismiss_on_escape)
226 .field("dismiss_on_scrim", &self.dismiss_on_scrim)
227 .field("close_button", &self.close_button)
228 .field("width", &self.width)
229 .field("id", &self.id)
230 .field("pass_through", &self.pass_through)
231 .finish_non_exhaustive()
232 }
233}
234
235pub fn dialog<'a, Message>(base: impl Into<Element<'a, Message>>) -> Dialog<'a, Message> {
238 Dialog {
239 base: base.into(),
240 open: false,
241 title: None,
242 description: None,
243 body: None,
244 actions: Vec::new(),
245 on_dismiss: None,
246 on_confirm: None,
247 dismiss_on_escape: true,
248 dismiss_on_scrim: true,
249 close_button: true,
250 width: Size::default().width(),
251 id: DEFAULT_ID,
252 keymap: default_keymap(),
253 pass_through: Vec::new(),
254 }
255}
256
257impl<'a, Message> Dialog<'a, Message> {
258 pub fn open(mut self, open: bool) -> Self {
259 self.open = open;
260 self
261 }
262
263 pub fn title(mut self, title: impl text::IntoFragment<'a>) -> Self {
264 self.title = Some(title.into_fragment());
265 self
266 }
267
268 pub fn description(mut self, description: impl text::IntoFragment<'a>) -> Self {
270 self.description = Some(description.into_fragment());
271 self
272 }
273
274 pub fn body(mut self, body: impl Into<Element<'a, Message>>) -> Self {
276 self.body = Some(body.into());
277 self
278 }
279
280 pub fn action(mut self, action: impl Into<Element<'a, Message>>) -> Self {
283 self.actions.push(action.into());
284 self
285 }
286
287 pub fn on_dismiss(mut self, message: Message) -> Self {
291 self.on_dismiss = Some(message);
292 self
293 }
294
295 pub fn dismiss_on_escape(mut self, dismiss: bool) -> Self {
297 self.dismiss_on_escape = dismiss;
298 self
299 }
300
301 pub fn on_confirm(mut self, message: Message) -> Self {
305 self.on_confirm = Some(message);
306 self
307 }
308
309 pub fn dismiss_on_scrim(mut self, dismiss: bool) -> Self {
312 self.dismiss_on_scrim = dismiss;
313 self
314 }
315
316 pub fn close_button(mut self, show: bool) -> Self {
319 self.close_button = show;
320 self
321 }
322
323 pub fn size(mut self, size: Size) -> Self {
325 self.width = size.width();
326 self
327 }
328
329 pub fn width(mut self, width: f32) -> Self {
331 self.width = width;
332 self
333 }
334
335 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
338 self.id = id.into();
339 self
340 }
341
342 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
344 self.keymap = keymap;
345 self
346 }
347
348 pub fn pass_through(mut self, chords: impl IntoIterator<Item = Chord>) -> Self {
351 self.pass_through = chords.into_iter().collect();
352 self
353 }
354
355 pub fn is_open(&self) -> bool {
356 self.open
357 }
358}
359
360impl<'a, Message: Clone + 'a> From<Dialog<'a, Message>> for Element<'a, Message> {
361 fn from(dialog: Dialog<'a, Message>) -> Self {
362 let Dialog {
363 base,
364 open,
365 title,
366 description,
367 body,
368 actions,
369 on_dismiss,
370 on_confirm,
371 dismiss_on_escape,
372 dismiss_on_scrim,
373 close_button,
374 width,
375 id,
376 keymap,
377 pass_through,
378 } = dialog;
379
380 let guarded = Element::new(Guard {
381 content: base,
382 blocked: open,
383 });
384
385 let mut layers = stack![guarded].width(Length::Fill).height(Length::Fill);
388
389 if open {
390 let surface = surface(
391 title,
392 description,
393 body,
394 actions,
395 close_button.then(|| on_dismiss.clone()),
396 width,
397 id.clone(),
398 );
399 let scrim = center(opaque(surface))
400 .padding(space::LG)
401 .style(|theme| scrim_style(&Tokens::of(theme)));
402 let layer: Element<'a, Message> = match on_dismiss.clone().filter(|_| dismiss_on_scrim)
403 {
404 Some(message) => opaque(mouse_area(scrim).on_press(message)),
405 None => opaque(scrim),
406 };
407 layers = layers.push(layer);
408 }
409
410 Element::new(Scope {
411 content: layers.into(),
412 open,
413 surface: id,
414 keymap,
415 pass_through,
416 on_escape: on_dismiss.filter(|_| dismiss_on_escape),
417 on_confirm,
418 })
419 }
420}
421
422fn surface<'a, Message: Clone + 'a>(
423 title: Option<text::Fragment<'a>>,
424 description: Option<text::Fragment<'a>>,
425 body: Option<Element<'a, Message>>,
426 actions: Vec<Element<'a, Message>>,
427 close: Option<Option<Message>>,
428 width: f32,
429 id: widget::Id,
430) -> Element<'a, Message> {
431 let mut header = column![].spacing(space::XS);
432 if let Some(title) = title {
433 header = header.push(
434 text(title)
435 .size(text_size::LG)
436 .font(crate::theme::semibold()),
437 );
438 }
439 if let Some(description) = description {
440 header = header.push(
441 text(description)
442 .size(text_size::SM)
443 .style(|theme: &Theme| text::Style {
444 color: Some(Tokens::of(theme).muted_foreground),
445 }),
446 );
447 }
448 if close.is_some() {
450 header = header.padding(Padding::ZERO.right(space::XL));
451 }
452
453 let mut content = column![header].spacing(space::LG).width(Length::Fill);
454 if let Some(body) = body {
455 content = content.push(body);
456 }
457 if !actions.is_empty() {
458 content = content.push(
459 container(row(actions).spacing(space::SM))
460 .width(Length::Fill)
461 .align_x(Alignment::End),
462 );
463 }
464
465 let mut layers = stack![container(content).padding(space::XL)];
466 if let Some(on_press) = close {
467 layers = layers.push(
468 container(
469 icon_button(crate::lucide!(X))
470 .label("Close")
471 .tooltip(None)
472 .size(ButtonSize::Sm)
473 .id(CLOSE_BUTTON_ID)
474 .on_press_maybe(on_press),
475 )
476 .padding(space::MD)
477 .width(Length::Fill)
478 .align_x(Alignment::End),
479 );
480 }
481
482 container(layers)
483 .id(id)
484 .width(Length::Fill)
485 .max_width(width)
486 .style(|theme| surface_style(&Tokens::of(theme)))
487 .into()
488}
489
490pub fn surface_style(tokens: &Tokens) -> container::Style {
493 let style = anchored::surface_style(tokens);
494 container::Style {
495 border: Border {
496 radius: radius::LG.into(),
497 ..style.border
498 },
499 ..style
500 }
501}
502
503pub fn scrim_style(tokens: &Tokens) -> container::Style {
506 container::Style {
507 background: Some(Background::Color(fade(
508 Color::BLACK,
509 if tokens.is_dark { 0.7 } else { 0.5 },
510 ))),
511 ..container::Style::default()
512 }
513}
514
515struct Guard<'a, Message> {
518 content: Element<'a, Message>,
519 blocked: bool,
520}
521
522impl<Message> Widget<Message, Theme, Renderer> for Guard<'_, Message> {
523 fn size(&self) -> Bounds<Length> {
524 self.content.as_widget().size()
525 }
526
527 fn size_hint(&self) -> Bounds<Length> {
528 self.content.as_widget().size_hint()
529 }
530
531 fn tag(&self) -> tree::Tag {
532 self.content.as_widget().tag()
533 }
534
535 fn state(&self) -> tree::State {
536 self.content.as_widget().state()
537 }
538
539 fn children(&self) -> Vec<Tree> {
540 self.content.as_widget().children()
541 }
542
543 fn diff(&self, tree: &mut Tree) {
544 self.content.as_widget().diff(tree);
545 }
546
547 fn layout(
548 &mut self,
549 tree: &mut Tree,
550 renderer: &Renderer,
551 limits: &layout::Limits,
552 ) -> layout::Node {
553 self.content.as_widget_mut().layout(tree, renderer, limits)
554 }
555
556 fn draw(
557 &self,
558 tree: &Tree,
559 renderer: &mut Renderer,
560 theme: &Theme,
561 style: &renderer::Style,
562 layout: Layout<'_>,
563 cursor: mouse::Cursor,
564 viewport: &Rectangle,
565 ) {
566 let cursor = if self.blocked {
567 mouse::Cursor::Unavailable
568 } else {
569 cursor
570 };
571 self.content
572 .as_widget()
573 .draw(tree, renderer, theme, style, layout, cursor, viewport);
574 }
575
576 fn operate(
577 &mut self,
578 tree: &mut Tree,
579 layout: Layout<'_>,
580 renderer: &Renderer,
581 operation: &mut dyn Operation,
582 ) {
583 self.content
584 .as_widget_mut()
585 .operate(tree, layout, renderer, operation);
586 }
587
588 fn update(
589 &mut self,
590 tree: &mut Tree,
591 event: &Event,
592 layout: Layout<'_>,
593 cursor: mouse::Cursor,
594 renderer: &Renderer,
595 clipboard: &mut dyn Clipboard,
596 shell: &mut Shell<'_, Message>,
597 viewport: &Rectangle,
598 ) {
599 if !self.blocked {
600 self.content.as_widget_mut().update(
601 tree, event, layout, cursor, renderer, clipboard, shell, viewport,
602 );
603 return;
604 }
605 if !matches!(event, Event::Window(_)) {
607 return;
608 }
609 self.content.as_widget_mut().update(
610 tree,
611 event,
612 layout,
613 mouse::Cursor::Unavailable,
614 renderer,
615 clipboard,
616 shell,
617 viewport,
618 );
619 }
620
621 fn mouse_interaction(
622 &self,
623 tree: &Tree,
624 layout: Layout<'_>,
625 cursor: mouse::Cursor,
626 viewport: &Rectangle,
627 renderer: &Renderer,
628 ) -> mouse::Interaction {
629 if self.blocked {
630 return mouse::Interaction::None;
631 }
632 self.content
633 .as_widget()
634 .mouse_interaction(tree, layout, cursor, viewport, renderer)
635 }
636
637 fn overlay<'b>(
638 &'b mut self,
639 tree: &'b mut Tree,
640 layout: Layout<'b>,
641 renderer: &Renderer,
642 viewport: &Rectangle,
643 translation: Vector,
644 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
645 if self.blocked {
646 return None;
647 }
648 self.content
649 .as_widget_mut()
650 .overlay(tree, layout, renderer, viewport, translation)
651 }
652}
653
654struct Scope<'a, Message> {
657 content: Element<'a, Message>,
658 open: bool,
659 surface: widget::Id,
660 keymap: Keymap<Action>,
661 pass_through: Vec<Chord>,
662 on_escape: Option<Message>,
663 on_confirm: Option<Message>,
664}
665
666fn key_of(event: &Event) -> Option<(&keyboard::Key, keyboard::Modifiers, bool)> {
668 match event {
669 Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => {
670 Some((key, *modifiers, true))
671 }
672 Event::Keyboard(keyboard::Event::KeyReleased { key, modifiers, .. }) => {
673 Some((key, *modifiers, false))
674 }
675 _ => None,
676 }
677}
678
679#[derive(Debug, Default)]
681struct ScopeState {
682 entered: bool,
683}
684
685impl<Message> Scope<'_, Message> {
686 fn move_focus(
687 &mut self,
688 tree: &mut Tree,
689 layout: Layout<'_>,
690 renderer: &Renderer,
691 movement: Movement,
692 ) {
693 let mut survey = Survey::new(self.surface.clone());
694 self.content
695 .as_widget_mut()
696 .operate(tree, layout, renderer, &mut survey);
697
698 let mut apply = Apply {
699 target: target(&survey.slots, movement),
700 index: 0,
701 };
702 self.content
703 .as_widget_mut()
704 .operate(tree, layout, renderer, &mut apply);
705 }
706}
707
708impl<Message: Clone> Widget<Message, Theme, Renderer> for Scope<'_, Message> {
709 fn size(&self) -> Bounds<Length> {
710 self.content.as_widget().size()
711 }
712
713 fn size_hint(&self) -> Bounds<Length> {
714 self.content.as_widget().size_hint()
715 }
716
717 fn tag(&self) -> tree::Tag {
718 tree::Tag::of::<ScopeState>()
719 }
720
721 fn state(&self) -> tree::State {
722 tree::State::new(ScopeState::default())
723 }
724
725 fn children(&self) -> Vec<Tree> {
726 vec![Tree::new(&self.content)]
727 }
728
729 fn diff(&self, tree: &mut Tree) {
730 tree.diff_children(std::slice::from_ref(&self.content));
731 }
732
733 fn layout(
734 &mut self,
735 tree: &mut Tree,
736 renderer: &Renderer,
737 limits: &layout::Limits,
738 ) -> layout::Node {
739 self.content
740 .as_widget_mut()
741 .layout(&mut tree.children[0], renderer, limits)
742 }
743
744 fn draw(
745 &self,
746 tree: &Tree,
747 renderer: &mut Renderer,
748 theme: &Theme,
749 style: &renderer::Style,
750 layout: Layout<'_>,
751 cursor: mouse::Cursor,
752 viewport: &Rectangle,
753 ) {
754 self.content.as_widget().draw(
755 &tree.children[0],
756 renderer,
757 theme,
758 style,
759 layout,
760 cursor,
761 viewport,
762 );
763 }
764
765 fn operate(
766 &mut self,
767 tree: &mut Tree,
768 layout: Layout<'_>,
769 renderer: &Renderer,
770 operation: &mut dyn Operation,
771 ) {
772 self.content
773 .as_widget_mut()
774 .operate(&mut tree.children[0], layout, renderer, operation);
775 }
776
777 fn update(
778 &mut self,
779 tree: &mut Tree,
780 event: &Event,
781 layout: Layout<'_>,
782 cursor: mouse::Cursor,
783 renderer: &Renderer,
784 clipboard: &mut dyn Clipboard,
785 shell: &mut Shell<'_, Message>,
786 viewport: &Rectangle,
787 ) {
788 let state = tree.state.downcast_mut::<ScopeState>();
789 let entering = self.open && !state.entered;
790 state.entered = self.open;
791
792 let content = &mut tree.children[0];
793 if entering {
794 self.move_focus(content, layout, renderer, Movement::First);
795 shell.request_redraw();
796 }
797
798 self.content.as_widget_mut().update(
799 content, event, layout, cursor, renderer, clipboard, shell, viewport,
800 );
801 if !self.open || shell.is_event_captured() {
802 return;
803 }
804
805 let Some((key, modifiers, pressed)) = key_of(event) else {
806 return;
807 };
808 if self
809 .pass_through
810 .iter()
811 .any(|chord| chord.matches(key, modifiers))
812 {
813 return;
814 }
815 shell.capture_event();
816 if !pressed {
817 return;
818 }
819 let Some(effect) = self
820 .keymap
821 .resolve(key, modifiers)
822 .and_then(|action| action.effect(self.on_escape.is_some()))
823 else {
824 return;
825 };
826
827 match effect {
828 Effect::Dismiss => {
829 let Some(message) = self.on_escape.clone() else {
830 return;
831 };
832 shell.publish(message);
833 }
834 Effect::Confirm => {
835 let Some(message) = self.on_confirm.clone() else {
836 return;
837 };
838 shell.publish(message);
839 }
840 Effect::Focus(direction) => {
841 self.move_focus(content, layout, renderer, Movement::from(direction));
842 shell.request_redraw();
843 }
844 }
845 }
846
847 fn mouse_interaction(
848 &self,
849 tree: &Tree,
850 layout: Layout<'_>,
851 cursor: mouse::Cursor,
852 viewport: &Rectangle,
853 renderer: &Renderer,
854 ) -> mouse::Interaction {
855 self.content.as_widget().mouse_interaction(
856 &tree.children[0],
857 layout,
858 cursor,
859 viewport,
860 renderer,
861 )
862 }
863
864 fn overlay<'b>(
865 &'b mut self,
866 tree: &'b mut Tree,
867 layout: Layout<'b>,
868 renderer: &Renderer,
869 viewport: &Rectangle,
870 translation: Vector,
871 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
872 self.content.as_widget_mut().overlay(
873 &mut tree.children[0],
874 layout,
875 renderer,
876 viewport,
877 translation,
878 )
879 }
880}
881
882#[derive(Debug, Clone, Copy, PartialEq, Eq)]
884enum Movement {
885 First,
886 Next,
887 Previous,
888}
889
890impl From<Direction> for Movement {
891 fn from(direction: Direction) -> Self {
892 match direction {
893 Direction::Next => Movement::Next,
894 Direction::Previous => Movement::Previous,
895 }
896 }
897}
898
899#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901struct Slot {
902 inside: bool,
903 focused: bool,
904 button: bool,
906}
907
908fn target(slots: &[Slot], movement: Movement) -> Option<usize> {
912 let inside: Vec<usize> = (0..slots.len()).filter(|&i| slots[i].inside).collect();
913 if movement == Movement::First {
914 return inside.into_iter().find(|&i| !slots[i].button);
915 }
916 let (first, last) = (inside.first().copied()?, inside.last().copied()?);
917 let current = inside.iter().position(|&i| slots[i].focused);
918 let count = inside.len();
919
920 match (movement, current) {
921 (Movement::First, _) | (Movement::Next, None) => Some(first),
922 (Movement::Previous, None) => Some(last),
923 (Movement::Next, Some(at)) => inside.get((at + 1) % count).copied(),
924 (Movement::Previous, Some(at)) => inside.get((at + count - 1) % count).copied(),
925 }
926}
927
928struct Survey {
930 scope: widget::Id,
931 entering: bool,
932 depth: usize,
933 slots: Vec<Slot>,
934}
935
936impl Survey {
937 fn new(scope: widget::Id) -> Self {
938 Self {
939 scope,
940 entering: false,
941 depth: 0,
942 slots: Vec::new(),
943 }
944 }
945}
946
947impl Operation for Survey {
948 fn container(&mut self, id: Option<&widget::Id>, _bounds: Rectangle) {
949 if id == Some(&self.scope) {
950 self.entering = true;
951 }
952 }
953
954 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) {
957 let entered = std::mem::take(&mut self.entering);
958 self.depth += usize::from(entered);
959 operate(self);
960 self.depth -= usize::from(entered);
961 }
962
963 fn focusable(
964 &mut self,
965 id: Option<&widget::Id>,
966 _bounds: Rectangle,
967 state: &mut dyn Focusable,
968 ) {
969 self.slots.push(Slot {
970 inside: self.depth > 0,
971 focused: state.is_focused(),
972 button: id == Some(&ALERT_BUTTON_ID),
973 });
974 }
975}
976
977struct Apply {
979 target: Option<usize>,
980 index: usize,
981}
982
983impl Operation for Apply {
984 fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation)) {
985 operate(self);
986 }
987
988 fn focusable(
989 &mut self,
990 _id: Option<&widget::Id>,
991 _bounds: Rectangle,
992 state: &mut dyn Focusable,
993 ) {
994 let is_target = self.target == Some(self.index);
995 if is_target && !state.is_focused() {
996 state.focus();
997 } else if !is_target && state.is_focused() {
998 state.unfocus();
999 }
1000 self.index += 1;
1001 }
1002}
1003
1004const ALERT_BUTTON_ID: widget::Id = widget::Id::new("iced-cube-dialog-alert-button");
1007
1008const RING_WIDTH: f32 = 2.0;
1010
1011struct FocusButton<'a, Message> {
1014 content: Element<'a, Message>,
1015 on_press: Option<Message>,
1016}
1017
1018#[derive(Debug, Default)]
1019struct FocusState {
1020 focused: bool,
1021}
1022
1023impl Focusable for FocusState {
1024 fn is_focused(&self) -> bool {
1025 self.focused
1026 }
1027
1028 fn focus(&mut self) {
1029 self.focused = true;
1030 }
1031
1032 fn unfocus(&mut self) {
1033 self.focused = false;
1034 }
1035}
1036
1037fn presses_button(event: &Event) -> bool {
1039 let Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event else {
1040 return false;
1041 };
1042 modifiers.is_empty()
1043 && matches!(
1044 key,
1045 keyboard::Key::Named(Named::Enter) | keyboard::Key::Named(Named::Space)
1046 )
1047}
1048
1049fn focus_button<'a, Message: Clone + 'a>(
1050 content: impl Into<Element<'a, Message>>,
1051 on_press: Option<Message>,
1052) -> Element<'a, Message> {
1053 Element::new(FocusButton {
1054 content: content.into(),
1055 on_press,
1056 })
1057}
1058
1059impl<Message: Clone> Widget<Message, Theme, Renderer> for FocusButton<'_, Message> {
1060 fn size(&self) -> Bounds<Length> {
1061 self.content.as_widget().size()
1062 }
1063
1064 fn size_hint(&self) -> Bounds<Length> {
1065 self.content.as_widget().size_hint()
1066 }
1067
1068 fn tag(&self) -> tree::Tag {
1069 tree::Tag::of::<FocusState>()
1070 }
1071
1072 fn state(&self) -> tree::State {
1073 tree::State::new(FocusState::default())
1074 }
1075
1076 fn children(&self) -> Vec<Tree> {
1077 vec![Tree::new(&self.content)]
1078 }
1079
1080 fn diff(&self, tree: &mut Tree) {
1081 tree.diff_children(std::slice::from_ref(&self.content));
1082 }
1083
1084 fn layout(
1085 &mut self,
1086 tree: &mut Tree,
1087 renderer: &Renderer,
1088 limits: &layout::Limits,
1089 ) -> layout::Node {
1090 self.content
1091 .as_widget_mut()
1092 .layout(&mut tree.children[0], renderer, limits)
1093 }
1094
1095 fn draw(
1096 &self,
1097 tree: &Tree,
1098 renderer: &mut Renderer,
1099 theme: &Theme,
1100 style: &renderer::Style,
1101 layout: Layout<'_>,
1102 cursor: mouse::Cursor,
1103 viewport: &Rectangle,
1104 ) {
1105 self.content.as_widget().draw(
1106 &tree.children[0],
1107 renderer,
1108 theme,
1109 style,
1110 layout,
1111 cursor,
1112 viewport,
1113 );
1114 if !tree.state.downcast_ref::<FocusState>().focused {
1115 return;
1116 }
1117 let tokens = Tokens::of(theme);
1118 let outset = RING_WIDTH + 1.0;
1120 renderer.fill_quad(
1121 renderer::Quad {
1122 bounds: layout.bounds().expand(outset),
1123 border: Border {
1124 color: tokens.ring,
1125 width: RING_WIDTH,
1126 radius: (radius::MD + outset).into(),
1127 },
1128 ..renderer::Quad::default()
1129 },
1130 Background::Color(Color::TRANSPARENT),
1131 );
1132 }
1133
1134 fn operate(
1135 &mut self,
1136 tree: &mut Tree,
1137 layout: Layout<'_>,
1138 renderer: &Renderer,
1139 operation: &mut dyn Operation,
1140 ) {
1141 let state = tree.state.downcast_mut::<FocusState>();
1142 operation.focusable(Some(&ALERT_BUTTON_ID), layout.bounds(), state);
1143 self.content
1144 .as_widget_mut()
1145 .operate(&mut tree.children[0], layout, renderer, operation);
1146 }
1147
1148 fn update(
1149 &mut self,
1150 tree: &mut Tree,
1151 event: &Event,
1152 layout: Layout<'_>,
1153 cursor: mouse::Cursor,
1154 renderer: &Renderer,
1155 clipboard: &mut dyn Clipboard,
1156 shell: &mut Shell<'_, Message>,
1157 viewport: &Rectangle,
1158 ) {
1159 let state = tree.state.downcast_mut::<FocusState>();
1160 if state.focused && is_pointer_press(event) {
1161 state.focused = false;
1162 shell.request_redraw();
1163 }
1164 if state.focused && presses_button(event) {
1165 if let Some(message) = &self.on_press {
1166 shell.publish(message.clone());
1167 }
1168 shell.capture_event();
1169 return;
1170 }
1171 self.content.as_widget_mut().update(
1172 &mut tree.children[0],
1173 event,
1174 layout,
1175 cursor,
1176 renderer,
1177 clipboard,
1178 shell,
1179 viewport,
1180 );
1181 }
1182
1183 fn mouse_interaction(
1184 &self,
1185 tree: &Tree,
1186 layout: Layout<'_>,
1187 cursor: mouse::Cursor,
1188 viewport: &Rectangle,
1189 renderer: &Renderer,
1190 ) -> mouse::Interaction {
1191 self.content.as_widget().mouse_interaction(
1192 &tree.children[0],
1193 layout,
1194 cursor,
1195 viewport,
1196 renderer,
1197 )
1198 }
1199}
1200
1201fn is_pointer_press(event: &Event) -> bool {
1202 matches!(
1203 event,
1204 Event::Mouse(mouse::Event::ButtonPressed(_))
1205 | Event::Touch(iced::touch::Event::FingerPressed { .. })
1206 )
1207}
1208
1209pub struct AlertDialog<'a, Message> {
1218 base: Element<'a, Message>,
1219 open: bool,
1220 title: text::Fragment<'a>,
1221 description: text::Fragment<'a>,
1222 cancel: text::Fragment<'a>,
1223 confirm: text::Fragment<'a>,
1224 on_cancel: Option<Message>,
1225 on_confirm: Option<Message>,
1226 confirm_on_enter: bool,
1227 dismiss_on_escape: bool,
1228 size: Size,
1229 keymap: Keymap<Action>,
1230}
1231
1232impl<Message> std::fmt::Debug for AlertDialog<'_, Message> {
1233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1234 f.debug_struct("AlertDialog")
1235 .field("open", &self.open)
1236 .field("title", &self.title)
1237 .field("description", &self.description)
1238 .field("cancel", &self.cancel)
1239 .field("confirm", &self.confirm)
1240 .field("confirm_on_enter", &self.confirm_on_enter)
1241 .field("dismiss_on_escape", &self.dismiss_on_escape)
1242 .field("size", &self.size)
1243 .finish_non_exhaustive()
1244 }
1245}
1246
1247pub fn alert_dialog<'a, Message>(
1250 base: impl Into<Element<'a, Message>>,
1251 title: impl text::IntoFragment<'a>,
1252 description: impl text::IntoFragment<'a>,
1253) -> AlertDialog<'a, Message> {
1254 AlertDialog {
1255 base: base.into(),
1256 open: false,
1257 title: title.into_fragment(),
1258 description: description.into_fragment(),
1259 cancel: "Cancel".into(),
1260 confirm: "Continue".into(),
1261 on_cancel: None,
1262 on_confirm: None,
1263 confirm_on_enter: false,
1264 dismiss_on_escape: true,
1265 size: Size::default(),
1266 keymap: default_keymap(),
1267 }
1268}
1269
1270impl<'a, Message> AlertDialog<'a, Message> {
1271 pub fn open(mut self, open: bool) -> Self {
1272 self.open = open;
1273 self
1274 }
1275
1276 pub fn cancel(mut self, label: impl text::IntoFragment<'a>) -> Self {
1278 self.cancel = label.into_fragment();
1279 self
1280 }
1281
1282 pub fn confirm(mut self, label: impl text::IntoFragment<'a>) -> Self {
1284 self.confirm = label.into_fragment();
1285 self
1286 }
1287
1288 pub fn on_cancel(mut self, message: Message) -> Self {
1290 self.on_cancel = Some(message);
1291 self
1292 }
1293
1294 pub fn on_confirm(mut self, message: Message) -> Self {
1297 self.on_confirm = Some(message);
1298 self
1299 }
1300
1301 pub fn confirm_on_enter(mut self, confirm: bool) -> Self {
1306 self.confirm_on_enter = confirm;
1307 self
1308 }
1309
1310 pub fn dismiss_on_escape(mut self, dismiss: bool) -> Self {
1312 self.dismiss_on_escape = dismiss;
1313 self
1314 }
1315
1316 pub fn size(mut self, size: Size) -> Self {
1317 self.size = size;
1318 self
1319 }
1320
1321 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
1323 self.keymap = keymap;
1324 self
1325 }
1326}
1327
1328impl<'a, Message: Clone + 'a> From<AlertDialog<'a, Message>> for Dialog<'a, Message> {
1329 fn from(alert: AlertDialog<'a, Message>) -> Self {
1330 let mut dialog = dialog(alert.base)
1331 .open(alert.open)
1332 .title(alert.title)
1333 .description(alert.description)
1334 .action(focus_button(
1335 button::button(alert.cancel)
1336 .variant(button::Variant::Outline)
1337 .on_press_maybe(alert.on_cancel.clone()),
1338 alert.on_cancel.clone(),
1339 ))
1340 .action(focus_button(
1341 button::button(alert.confirm)
1342 .variant(button::Variant::Destructive)
1343 .on_press_maybe(alert.on_confirm.clone()),
1344 alert.on_confirm.clone(),
1345 ))
1346 .dismiss_on_escape(alert.dismiss_on_escape)
1347 .dismiss_on_scrim(false)
1348 .close_button(false)
1349 .size(alert.size)
1350 .keymap(alert.keymap);
1351 if let Some(message) = alert.on_cancel {
1352 dialog = dialog.on_dismiss(message);
1353 }
1354 let Some(message) = alert.on_confirm.filter(|_| alert.confirm_on_enter) else {
1355 return dialog;
1356 };
1357 dialog.on_confirm(message)
1358 }
1359}
1360
1361impl<'a, Message: Clone + 'a> From<AlertDialog<'a, Message>> for Element<'a, Message> {
1362 fn from(alert: AlertDialog<'a, Message>) -> Self {
1363 Dialog::from(alert).into()
1364 }
1365}
1366
1367#[cfg(test)]
1368mod tests {
1369 use super::*;
1370 use crate::theme::{dark, light};
1371
1372 fn slots(layout: &[(bool, bool)]) -> Vec<Slot> {
1373 layout
1374 .iter()
1375 .map(|&(inside, focused)| Slot {
1376 inside,
1377 focused,
1378 button: false,
1379 })
1380 .collect()
1381 }
1382
1383 #[test]
1384 fn builder_defaults() {
1385 let d: Dialog<'_, ()> = dialog(text("Base"));
1386 assert!(!d.is_open());
1387 assert!(d.dismiss_on_escape);
1388 assert!(d.dismiss_on_scrim);
1389 assert!(d.close_button);
1390 assert!(d.on_dismiss.is_none());
1391 assert_eq!(d.width, Size::Md.width());
1392 assert_eq!(d.id, DEFAULT_ID);
1393 assert_eq!(d.keymap, default_keymap());
1394 assert!(d.pass_through.is_empty());
1395 }
1396
1397 #[test]
1398 fn builder_sets_every_option() {
1399 let d = dialog::<u8>(text("Base"))
1400 .open(true)
1401 .title("Title")
1402 .description("Description")
1403 .body(text("Body"))
1404 .action(text("One"))
1405 .action(text("Two"))
1406 .on_dismiss(1)
1407 .dismiss_on_escape(false)
1408 .dismiss_on_scrim(false)
1409 .close_button(false)
1410 .size(Size::Lg)
1411 .id("custom")
1412 .keymap(Keymap::new())
1413 .pass_through([Chord::character('k').command()]);
1414 assert_eq!(d.pass_through, vec![Chord::character('k').command()]);
1415 assert!(d.is_open());
1416 assert_eq!(d.title.as_deref(), Some("Title"));
1417 assert_eq!(d.description.as_deref(), Some("Description"));
1418 assert!(d.body.is_some());
1419 assert_eq!(d.actions.len(), 2);
1420 assert_eq!(d.on_dismiss, Some(1));
1421 assert!(!d.dismiss_on_escape && !d.dismiss_on_scrim && !d.close_button);
1422 assert_eq!(d.width, 640.0);
1423 assert_eq!(d.id, widget::Id::new("custom"));
1424 assert!(d.keymap.is_empty());
1425 assert_eq!(dialog::<u8>(text("Base")).width(300.0).width, 300.0);
1426 }
1427
1428 #[test]
1429 fn sizes_grow_monotonically() {
1430 let widths = Size::ALL.map(Size::width);
1431 assert!(widths.windows(2).all(|w| w[0] < w[1]));
1432 assert_eq!(Size::default(), Size::Md);
1433 }
1434
1435 #[test]
1436 fn alert_dialog_needs_an_explicit_answer() {
1437 let alert = alert_dialog::<u8>(text("Base"), "Delete file?", "This cannot be undone.")
1438 .open(true)
1439 .confirm("Delete")
1440 .on_cancel(1)
1441 .on_confirm(2);
1442 assert_eq!(alert.cancel.as_ref(), "Cancel");
1443 assert_eq!(alert.confirm.as_ref(), "Delete");
1444
1445 let d = Dialog::from(alert);
1446 assert!(d.is_open());
1447 assert!(!d.dismiss_on_scrim);
1448 assert!(!d.close_button);
1449 assert!(d.dismiss_on_escape);
1450 assert_eq!(d.on_dismiss, Some(1));
1451 assert_eq!(d.on_confirm, None, "Enter does not destroy by default");
1452 assert_eq!(d.actions.len(), 2);
1453 }
1454
1455 #[test]
1456 fn alert_dialog_confirms_on_enter_only_when_asked() {
1457 let alert = alert_dialog::<u8>(text("Base"), "Delete file?", "This cannot be undone.")
1458 .on_confirm(2)
1459 .confirm_on_enter(true);
1460 assert_eq!(Dialog::from(alert).on_confirm, Some(2));
1461 }
1462
1463 #[test]
1464 fn alert_dialog_without_cancel_has_no_dismiss() {
1465 let d = Dialog::from(
1466 alert_dialog::<u8>(text("Base"), "Title", "Description").dismiss_on_escape(false),
1467 );
1468 assert!(d.on_dismiss.is_none());
1469 assert!(d.on_confirm.is_none());
1470 assert!(!d.dismiss_on_escape);
1471 }
1472
1473 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
1474 let chord: Chord = chord.parse().ok()?;
1475 keymap.resolve(chord.key(), chord.modifiers())
1476 }
1477
1478 #[test]
1479 fn default_keymap_and_overrides() {
1480 let keymap = default_keymap();
1481 assert_eq!(press(&keymap, "Escape"), Some(Action::Close));
1482 assert_eq!(press(&keymap, "Enter"), Some(Action::Confirm));
1483 assert_eq!(press(&keymap, "Shift+Enter"), None);
1484 assert_eq!(press(&keymap, "Tab"), Some(Action::FocusNext));
1485 assert_eq!(press(&keymap, "Shift+Tab"), Some(Action::FocusPrevious));
1486 assert_eq!(press(&keymap, "Ctrl+Tab"), None);
1487
1488 let custom = keymap
1489 .unbind(&Chord::named(Named::Escape))
1490 .bind(Chord::character('w').command(), Action::Close);
1491 assert_eq!(press(&custom, "Escape"), None);
1492 assert_eq!(press(&custom, "Mod+W"), Some(Action::Close));
1493 assert!(
1494 custom
1495 .unbind_action(&Action::Close)
1496 .chords(&Action::Close)
1497 .is_empty()
1498 );
1499 }
1500
1501 #[test]
1502 fn close_only_dismisses_when_escape_is_allowed() {
1503 assert_eq!(Action::Close.effect(true), Some(Effect::Dismiss));
1504 assert_eq!(Action::Close.effect(false), None);
1505 for allowed in [true, false] {
1506 assert_eq!(Action::Confirm.effect(allowed), Some(Effect::Confirm));
1507 assert_eq!(
1508 Action::FocusNext.effect(allowed),
1509 Some(Effect::Focus(Direction::Next))
1510 );
1511 assert_eq!(
1512 Action::FocusPrevious.effect(allowed),
1513 Some(Effect::Focus(Direction::Previous))
1514 );
1515 }
1516 }
1517
1518 #[test]
1519 fn every_action_has_a_name_and_a_sentence() {
1520 use keys::Action as _;
1521 for action in Action::ALL {
1522 assert!(!action.name().is_empty());
1523 assert!(action.description().ends_with('.'));
1524 }
1525 }
1526
1527 #[test]
1528 fn first_focuses_the_first_field_inside() {
1529 let layout = slots(&[(false, true), (true, false), (true, false)]);
1530 assert_eq!(target(&layout, Movement::First), Some(1));
1531 }
1532
1533 #[test]
1534 fn opening_skips_alert_buttons_but_tab_reaches_them() {
1535 let mut layout = slots(&[(true, false), (true, false)]);
1536 for slot in &mut layout {
1537 slot.button = true;
1538 }
1539 assert_eq!(target(&layout, Movement::First), None);
1540 assert_eq!(target(&layout, Movement::Next), Some(0));
1541 assert_eq!(target(&layout, Movement::Previous), Some(1));
1542
1543 layout[1].button = false;
1544 assert_eq!(target(&layout, Movement::First), Some(1));
1545 }
1546
1547 #[test]
1548 fn enter_and_space_press_a_focused_button_but_not_with_modifiers() {
1549 let key = |named: Named, modifiers: keyboard::Modifiers| {
1550 Event::Keyboard(keyboard::Event::KeyPressed {
1551 key: keyboard::Key::Named(named),
1552 modified_key: keyboard::Key::Named(named),
1553 physical_key: keyboard::key::Physical::Unidentified(
1554 keyboard::key::NativeCode::Unidentified,
1555 ),
1556 location: keyboard::Location::Standard,
1557 modifiers,
1558 text: None,
1559 repeat: false,
1560 })
1561 };
1562 let none = keyboard::Modifiers::empty();
1563 assert!(presses_button(&key(Named::Enter, none)));
1564 assert!(presses_button(&key(Named::Space, none)));
1565 assert!(!presses_button(&key(
1566 Named::Enter,
1567 keyboard::Modifiers::CTRL
1568 )));
1569 assert!(!presses_button(&key(Named::Tab, none)));
1570 }
1571
1572 #[test]
1573 fn next_and_previous_wrap_inside_the_dialog() {
1574 let last = slots(&[(false, false), (true, false), (true, true), (false, false)]);
1575 assert_eq!(target(&last, Movement::Next), Some(1));
1576 assert_eq!(target(&last, Movement::Previous), Some(1));
1577
1578 let first = slots(&[(true, true), (true, false), (true, false)]);
1579 assert_eq!(target(&first, Movement::Previous), Some(2));
1580 assert_eq!(target(&first, Movement::Next), Some(1));
1581 }
1582
1583 #[test]
1584 fn focus_outside_moves_to_either_end_inside() {
1585 let layout = slots(&[(false, true), (true, false), (true, false), (false, false)]);
1586 assert_eq!(target(&layout, Movement::Next), Some(1));
1587 assert_eq!(target(&layout, Movement::Previous), Some(2));
1588 }
1589
1590 #[test]
1591 fn nothing_to_focus_without_fields_inside() {
1592 let layout = slots(&[(false, true), (false, false)]);
1593 for movement in [Movement::First, Movement::Next, Movement::Previous] {
1594 assert_eq!(target(&layout, movement), None);
1595 }
1596 assert_eq!(target(&[], Movement::Next), None);
1597 }
1598
1599 #[test]
1600 fn a_single_field_keeps_focus() {
1601 let layout = slots(&[(true, true)]);
1602 assert_eq!(target(&layout, Movement::Next), Some(0));
1603 assert_eq!(target(&layout, Movement::Previous), Some(0));
1604 }
1605
1606 #[test]
1607 fn surface_uses_the_shared_floating_style() {
1608 for theme in [light(), dark()] {
1609 let tokens = Tokens::of(&theme);
1610 let style = surface_style(&tokens);
1611 let shared = anchored::surface_style(&tokens);
1612 assert_eq!(style.background, Some(Background::Color(tokens.popover)));
1613 assert_eq!(style.text_color, Some(tokens.foreground));
1614 assert_eq!(style.border.color, tokens.border);
1615 assert_eq!(style.border.width, 1.0);
1616 assert_eq!(style.border.radius, radius::LG.into());
1617 assert_eq!(style.shadow, shared.shadow);
1618 }
1619 }
1620
1621 #[test]
1622 fn dark_theme_has_a_stronger_shadow_and_scrim() {
1623 let light = Tokens::of(&light());
1624 let dark = Tokens::of(&dark());
1625 assert!(surface_style(&dark).shadow.color.a > surface_style(&light).shadow.color.a);
1626
1627 let alpha = |tokens: &Tokens| match scrim_style(tokens).background {
1628 Some(Background::Color(colour)) => colour.a,
1629 _ => 0.0,
1630 };
1631 assert!((alpha(&light) - 0.5).abs() < 1e-6);
1632 assert!((alpha(&dark) - 0.7).abs() < 1e-6);
1633 }
1634}