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