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. In
45//! an [`AlertDialog`] they also reach its two buttons; see there for how
46//! keyboard users confirm.
47//!
48//! While it is open the dialog captures every key press, after the content
49//! inside it has had its turn, so app-wide shortcuts from
50//! [`keys::subscription`] never reach the components underneath. Let
51//! chosen chords through with [`Dialog::pass_through`].
52
53use 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
70/// The default widget id of the dialog surface, which scopes focus.
71pub const DEFAULT_ID: widget::Id = widget::Id::new("iced-cube-dialog");
72
73/// The widget id of the close button, for clicking it in tests.
74pub const CLOSE_BUTTON_ID: widget::Id = widget::Id::new("iced-cube-dialog-close");
75
76/// Maximum width of the dialog surface. It shrinks to fit smaller windows.
77#[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    /// The maximum width in logical pixels.
89    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/// What a dialog keyboard shortcut does.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100pub enum Action {
101    Close,
102    Confirm,
103    FocusNext,
104    FocusPrevious,
105}
106
107/// The direction focus moves in.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109pub enum Direction {
110    Next,
111    Previous,
112}
113
114/// What an [`Action`] does to an open dialog.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
116pub enum Effect {
117    /// Emit the `on_dismiss` message.
118    Dismiss,
119    /// Emit the `on_confirm` message.
120    Confirm,
121    /// Move focus to another text field inside the dialog.
122    Focus(Direction),
123}
124
125impl Action {
126    /// The effect of this action on an open dialog, or `None` when Close
127    /// has nothing to do because Escape does not dismiss the dialog.
128    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
175/// The default dialog shortcuts:
176///
177/// | Keys | Action |
178/// | --- | --- |
179/// | `Escape` | [`Action::Close`] |
180/// | `Enter` | [`Action::Confirm`] |
181/// | `Tab` | [`Action::FocusNext`] |
182/// | `Shift+Tab` | [`Action::FocusPrevious`] |
183///
184/// The dialog resolves these itself while it is open, after the content
185/// inside it, so a menu or combobox in the dialog closes on Escape before
186/// the dialog does, and a field that submits on Enter keeps it. A focused
187/// text field takes the first Escape to lose focus, so it takes a second
188/// Escape to dismiss the dialog. Enter does nothing in a dialog without
189/// [`Dialog::on_confirm`], or in an alert dialog without
190/// [`AlertDialog::confirm_on_enter`].
191pub 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
199/// A dialog builder. Convert it into an [`Element`] to render.
200pub 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
235/// Wraps `base`, the content the dialog opens over. The dialog is closed
236/// until [`open`](Dialog::open) is `true`.
237pub 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    /// A line of muted text under the title.
269    pub fn description(mut self, description: impl text::IntoFragment<'a>) -> Self {
270        self.description = Some(description.into_fragment());
271        self
272    }
273
274    /// Content between the header and the footer, such as a form.
275    pub fn body(mut self, body: impl Into<Element<'a, Message>>) -> Self {
276        self.body = Some(body.into());
277        self
278    }
279
280    /// Adds a button or other element to the footer, which lines them up
281    /// on the right. Add the main action last.
282    pub fn action(mut self, action: impl Into<Element<'a, Message>>) -> Self {
283        self.actions.push(action.into());
284        self
285    }
286
287    /// The message emitted when Escape, a click on the scrim or the close
288    /// button dismisses the dialog. Without it, only the footer actions
289    /// can close the dialog and the close button renders disabled.
290    pub fn on_dismiss(mut self, message: Message) -> Self {
291        self.on_dismiss = Some(message);
292        self
293    }
294
295    /// Whether Escape dismisses the dialog. Defaults to `true`.
296    pub fn dismiss_on_escape(mut self, dismiss: bool) -> Self {
297        self.dismiss_on_escape = dismiss;
298        self
299    }
300
301    /// The message [`Action::Confirm`] (Enter) emits, usually the same as
302    /// the main footer action. Without it Enter does nothing. A field
303    /// inside the dialog that submits on Enter keeps the key.
304    pub fn on_confirm(mut self, message: Message) -> Self {
305        self.on_confirm = Some(message);
306        self
307    }
308
309    /// Whether a click on the scrim dismisses the dialog. Defaults to
310    /// `true`; turn it off for confirmations that need an explicit answer.
311    pub fn dismiss_on_scrim(mut self, dismiss: bool) -> Self {
312        self.dismiss_on_scrim = dismiss;
313        self
314    }
315
316    /// Whether to show a close button in the top right corner. Defaults to
317    /// `true`.
318    pub fn close_button(mut self, show: bool) -> Self {
319        self.close_button = show;
320        self
321    }
322
323    /// Sets the maximum width from a [`Size`].
324    pub fn size(mut self, size: Size) -> Self {
325        self.width = size.width();
326        self
327    }
328
329    /// Sets the maximum width in logical pixels.
330    pub fn width(mut self, width: f32) -> Self {
331        self.width = width;
332        self
333    }
334
335    /// Sets the surface's widget id, which scopes focus. Give nested
336    /// dialogs different ids.
337    pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
338        self.id = id.into();
339        self
340    }
341
342    /// Replaces the default shortcuts.
343    pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
344        self.keymap = keymap;
345        self
346    }
347
348    /// Chords the open dialog lets through to the app, such as the shortcut
349    /// that toggles it. Every other key press stops at the dialog.
350    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        // The base is always the first child, so it keeps its widget state
386        // when the dialog opens and closes.
387        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    // Keeps long titles clear of the close button.
449    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
490/// The dialog surface: the floating surface every overlay shares, with a
491/// larger radius.
492pub 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
503/// The scrim: black at half opacity in light themes and more in dark ones,
504/// where half would barely show.
505pub 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
515/// Draws the base content, and while `blocked` keeps input and hover state
516/// away from it.
517struct 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        // Window events still arrive so animations underneath keep running.
606        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
654/// Resolves the dialog's keymap, keeps focus inside the surface and
655/// captures every other key press while the dialog is open.
656struct 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
666/// The key and modifiers of a key press or release.
667fn 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/// Whether focus has moved into the dialog since it opened.
680#[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/// Where focus goes inside the dialog.
883#[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/// A focusable widget, in tree order.
900#[derive(Debug, Clone, Copy, PartialEq, Eq)]
901struct Slot {
902    inside: bool,
903    focused: bool,
904    /// An alert dialog button, which Tab reaches but opening does not.
905    button: bool,
906}
907
908/// The index of the slot to focus. Only slots inside the dialog are
909/// candidates, and focus wraps at either end. Opening focuses the first
910/// one that is not a button. Every other slot loses focus.
911fn 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
928/// Records every focusable widget and whether it sits inside the surface.
929struct 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    // A container reports itself and then traverses its children, so the
955    // traversal right after the scope's container is the inside of it.
956    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
977/// Focuses the target slot and unfocuses every other one.
978struct 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
1004/// The widget id an alert dialog's buttons report focus under, so the
1005/// dialog leaves them unfocused when it opens.
1006const ALERT_BUTTON_ID: widget::Id = widget::Id::new("iced-cube-dialog-alert-button");
1007
1008/// Width of the ring around a focused alert dialog button.
1009const RING_WIDTH: f32 = 2.0;
1010
1011/// Lets Tab focus a button inside an alert dialog. While focused it draws a
1012/// ring and Enter or Space presses it; any pointer press takes focus away.
1013struct 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
1037/// Whether a key press activates a focused button.
1038fn 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        // The ring sits just outside the button, clear of its own border.
1119        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
1209/// A confirmation that needs an explicit answer: a clicked scrim does
1210/// nothing, there is no close button, and the footer holds Cancel and a
1211/// destructive action.
1212///
1213/// Keyboard users reach both buttons with Tab and Shift+Tab, which ring
1214/// the focused one, and press it with Enter or Space. Nothing is focused
1215/// when the dialog opens, so a stray Enter does nothing unless
1216/// [`confirm_on_enter`](Self::confirm_on_enter) is on; Escape cancels.
1217pub 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
1247/// Wraps `base` in a confirmation dialog. The buttons read "Cancel" and
1248/// "Continue" until you change them.
1249pub 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    /// The label of the cancel button.
1277    pub fn cancel(mut self, label: impl text::IntoFragment<'a>) -> Self {
1278        self.cancel = label.into_fragment();
1279        self
1280    }
1281
1282    /// The label of the destructive button, such as "Delete".
1283    pub fn confirm(mut self, label: impl text::IntoFragment<'a>) -> Self {
1284        self.confirm = label.into_fragment();
1285        self
1286    }
1287
1288    /// Emitted by the cancel button and by Escape.
1289    pub fn on_cancel(mut self, message: Message) -> Self {
1290        self.on_cancel = Some(message);
1291        self
1292    }
1293
1294    /// Emitted by the destructive button, and by Enter when
1295    /// [`confirm_on_enter`](Self::confirm_on_enter) is on.
1296    pub fn on_confirm(mut self, message: Message) -> Self {
1297        self.on_confirm = Some(message);
1298        self
1299    }
1300
1301    /// Whether Enter ([`Action::Confirm`]) runs the destructive action
1302    /// while no button is focused. Defaults to `false`, so a stray Enter
1303    /// never destroys anything; keyboard users Tab to the button and press
1304    /// Enter or Space instead.
1305    pub fn confirm_on_enter(mut self, confirm: bool) -> Self {
1306        self.confirm_on_enter = confirm;
1307        self
1308    }
1309
1310    /// Whether Escape cancels. Defaults to `true`.
1311    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    /// Replaces the default shortcuts.
1322    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}