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