Skip to main content

iced_cube/overlay/
dialog.rs

1//! A modal window over a dimmed scrim, with a title, a description, body
2//! content and a footer of actions.
3//!
4//! The app owns whether the dialog is open. [`dialog`] wraps the app's
5//! content (the base) and, while open, draws a scrim over it that blocks
6//! every click and key press from reaching the base.
7//!
8//! ```no_run
9//! use iced::Element;
10//! use iced::widget::center;
11//! use iced_cube::button;
12//! use iced_cube::overlay::dialog::dialog;
13//!
14//! #[derive(Debug, Clone)]
15//! enum Message {
16//!     Open,
17//!     Close,
18//! }
19//!
20//! struct App {
21//!     open: bool,
22//! }
23//!
24//! impl App {
25//!     fn update(&mut self, message: Message) {
26//!         self.open = matches!(message, Message::Open);
27//!     }
28//!
29//!     fn view(&self) -> Element<'_, Message> {
30//!         dialog(center(button("Open").on_press(Message::Open)))
31//!             .open(self.open)
32//!             .title("Share link")
33//!             .description("Anyone with the link can view this document.")
34//!             .action(button("Done").on_press(Message::Close))
35//!             .on_dismiss(Message::Close)
36//!             .into()
37//!     }
38//! }
39//! ```
40//!
41//! The dialog handles its own keys through a [`Keymap`], so an app needs no
42//! subscription for it: Escape dismisses, Enter sends the
43//! [`on_confirm`](Dialog::on_confirm) message, and Tab and Shift+Tab move
44//! focus between the text fields inside the dialog without leaving it.
45//!
46//! While it is open the dialog captures every key press, after the content
47//! inside it has had its turn, so app-wide shortcuts from
48//! [`keys::subscription`] never reach the components underneath. Let
49//! chosen chords through with [`Dialog::pass_through`].
50
51use 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
68/// The default widget id of the dialog surface, which scopes focus.
69pub const DEFAULT_ID: widget::Id = widget::Id::new("iced-cube-dialog");
70
71/// The widget id of the close button, for clicking it in tests.
72pub const CLOSE_BUTTON_ID: widget::Id = widget::Id::new("iced-cube-dialog-close");
73
74/// Maximum width of the dialog surface. It shrinks to fit smaller windows.
75#[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    /// The maximum width in logical pixels.
87    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/// What a dialog keyboard shortcut does.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub enum Action {
99    Close,
100    Confirm,
101    FocusNext,
102    FocusPrevious,
103}
104
105/// The direction focus moves in.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub enum Direction {
108    Next,
109    Previous,
110}
111
112/// What an [`Action`] does to an open dialog.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub enum Effect {
115    /// Emit the `on_dismiss` message.
116    Dismiss,
117    /// Emit the `on_confirm` message.
118    Confirm,
119    /// Move focus to another text field inside the dialog.
120    Focus(Direction),
121}
122
123impl Action {
124    /// The effect of this action on an open dialog, or `None` when Close
125    /// has nothing to do because Escape does not dismiss the dialog.
126    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
173/// The default dialog shortcuts:
174///
175/// | Keys | Action |
176/// | --- | --- |
177/// | `Escape` | [`Action::Close`] |
178/// | `Enter` | [`Action::Confirm`] |
179/// | `Tab` | [`Action::FocusNext`] |
180/// | `Shift+Tab` | [`Action::FocusPrevious`] |
181///
182/// The dialog resolves these itself while it is open, after the content
183/// inside it, so a menu or combobox in the dialog closes on Escape before
184/// the dialog does, and a field that submits on Enter keeps it. A focused
185/// text field takes the first Escape to lose focus, so it takes a second
186/// Escape to dismiss the dialog. Enter does nothing in a dialog without
187/// [`Dialog::on_confirm`].
188pub 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
196/// A dialog builder. Convert it into an [`Element`] to render.
197pub 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
232/// Wraps `base`, the content the dialog opens over. The dialog is closed
233/// until [`open`](Dialog::open) is `true`.
234pub 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    /// A line of muted text under the title.
266    pub fn description(mut self, description: impl text::IntoFragment<'a>) -> Self {
267        self.description = Some(description.into_fragment());
268        self
269    }
270
271    /// Content between the header and the footer, such as a form.
272    pub fn body(mut self, body: impl Into<Element<'a, Message>>) -> Self {
273        self.body = Some(body.into());
274        self
275    }
276
277    /// Adds a button or other element to the footer, which lines them up
278    /// on the right. Add the main action last.
279    pub fn action(mut self, action: impl Into<Element<'a, Message>>) -> Self {
280        self.actions.push(action.into());
281        self
282    }
283
284    /// The message emitted when Escape, a click on the scrim or the close
285    /// button dismisses the dialog. Without it, only the footer actions
286    /// can close the dialog and the close button renders disabled.
287    pub fn on_dismiss(mut self, message: Message) -> Self {
288        self.on_dismiss = Some(message);
289        self
290    }
291
292    /// Whether Escape dismisses the dialog. Defaults to `true`.
293    pub fn dismiss_on_escape(mut self, dismiss: bool) -> Self {
294        self.dismiss_on_escape = dismiss;
295        self
296    }
297
298    /// The message [`Action::Confirm`] (Enter) emits, usually the same as
299    /// the main footer action. Without it Enter does nothing. A field
300    /// inside the dialog that submits on Enter keeps the key.
301    pub fn on_confirm(mut self, message: Message) -> Self {
302        self.on_confirm = Some(message);
303        self
304    }
305
306    /// Whether a click on the scrim dismisses the dialog. Defaults to
307    /// `true`; turn it off for confirmations that need an explicit answer.
308    pub fn dismiss_on_scrim(mut self, dismiss: bool) -> Self {
309        self.dismiss_on_scrim = dismiss;
310        self
311    }
312
313    /// Whether to show a close button in the top right corner. Defaults to
314    /// `true`.
315    pub fn close_button(mut self, show: bool) -> Self {
316        self.close_button = show;
317        self
318    }
319
320    /// Sets the maximum width from a [`Size`].
321    pub fn size(mut self, size: Size) -> Self {
322        self.width = size.width();
323        self
324    }
325
326    /// Sets the maximum width in logical pixels.
327    pub fn width(mut self, width: f32) -> Self {
328        self.width = width;
329        self
330    }
331
332    /// Sets the surface's widget id, which scopes focus. Give nested
333    /// dialogs different ids.
334    pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
335        self.id = id.into();
336        self
337    }
338
339    /// Replaces the default shortcuts.
340    pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
341        self.keymap = keymap;
342        self
343    }
344
345    /// Chords the open dialog lets through to the app, such as the shortcut
346    /// that toggles it. Every other key press stops at the dialog.
347    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        // The base is always the first child, so it keeps its widget state
383        // when the dialog opens and closes.
384        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    // Keeps long titles clear of the close button.
446    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
487/// The dialog surface: the floating surface every overlay shares, with a
488/// larger radius.
489pub 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
500/// The scrim: black at half opacity in light themes and more in dark ones,
501/// where half would barely show.
502pub 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
512/// Draws the base content, and while `blocked` keeps input and hover state
513/// away from it.
514struct 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        // Window events still arrive so animations underneath keep running.
603        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
651/// Resolves the dialog's keymap, keeps focus inside the surface and
652/// captures every other key press while the dialog is open.
653struct 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
663/// The key and modifiers of a key press or release.
664fn 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/// Whether focus has moved into the dialog since it opened.
677#[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/// Where focus goes inside the dialog.
880#[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/// A focusable widget, in tree order.
897#[derive(Debug, Clone, Copy, PartialEq, Eq)]
898struct Slot {
899    inside: bool,
900    focused: bool,
901}
902
903/// The index of the slot to focus. Only slots inside the dialog are
904/// candidates, and focus wraps at either end. Every other slot loses focus.
905fn 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
919/// Records every focusable widget and whether it sits inside the surface.
920struct 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    // A container reports itself and then traverses its children, so the
946    // traversal right after the scope's container is the inside of it.
947    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
967/// Focuses the target slot and unfocuses every other one.
968struct 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
994/// A confirmation that needs an explicit answer: a clicked scrim does
995/// nothing, there is no close button, and the footer holds Cancel and a
996/// destructive action.
997pub 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
1025/// Wraps `base` in a confirmation dialog. The buttons read "Cancel" and
1026/// "Continue" until you change them.
1027pub 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    /// The label of the cancel button.
1054    pub fn cancel(mut self, label: impl text::IntoFragment<'a>) -> Self {
1055        self.cancel = label.into_fragment();
1056        self
1057    }
1058
1059    /// The label of the destructive button, such as "Delete".
1060    pub fn confirm(mut self, label: impl text::IntoFragment<'a>) -> Self {
1061        self.confirm = label.into_fragment();
1062        self
1063    }
1064
1065    /// Emitted by the cancel button and by Escape.
1066    pub fn on_cancel(mut self, message: Message) -> Self {
1067        self.on_cancel = Some(message);
1068        self
1069    }
1070
1071    /// Emitted by the destructive button and by Enter.
1072    pub fn on_confirm(mut self, message: Message) -> Self {
1073        self.on_confirm = Some(message);
1074        self
1075    }
1076
1077    /// Whether Escape cancels. Defaults to `true`.
1078    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    /// Replaces the default shortcuts.
1089    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}