Skip to main content

ftui_widgets/modal/
dialog.rs

1#![forbid(unsafe_code)]
2
3//! Dialog presets built on the Modal container.
4//!
5//! Provides common dialog patterns:
6//! - Alert: Message with OK button
7//! - Confirm: Message with OK/Cancel
8//! - Prompt: Message with text input + OK/Cancel
9//! - Custom: Builder for custom dialogs
10//!
11//! # Example
12//!
13//! ```ignore
14//! let dialog = Dialog::alert("Operation complete", "File saved successfully.");
15//! let dialog = Dialog::confirm("Delete file?", "This action cannot be undone.");
16//! let dialog = Dialog::prompt("Enter name", "Please enter your username:");
17//! ```
18
19use crate::block::{Alignment, Block};
20use crate::borders::Borders;
21use crate::modal::{Modal, ModalConfig, ModalPosition, ModalSizeConstraints};
22use crate::{StatefulWidget, Widget, draw_text_span, set_style_area};
23use ftui_core::event::{
24    Event, KeyCode, KeyEvent, KeyEventKind, Modifiers, MouseButton, MouseEvent, MouseEventKind,
25};
26use ftui_core::geometry::Rect;
27use ftui_render::cell::Cell;
28use ftui_render::frame::{Frame, HitData, HitId, HitRegion};
29use ftui_style::{Style, StyleFlags};
30use ftui_text::display_width;
31use unicode_segmentation::UnicodeSegmentation;
32
33/// Hit region for dialog buttons.
34pub const DIALOG_HIT_BUTTON: HitRegion = HitRegion::Button;
35/// Hit region for prompt input.
36pub const DIALOG_HIT_INPUT: HitRegion = HitRegion::Custom(1);
37
38/// Result from a dialog interaction.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum DialogResult {
41    /// Dialog was dismissed without action.
42    Dismissed,
43    /// OK / primary button pressed.
44    Ok,
45    /// Cancel / secondary button pressed.
46    Cancel,
47    /// Custom button pressed with its ID.
48    Custom(String),
49    /// Prompt dialog submitted with input value.
50    Input(String),
51}
52
53/// A button in a dialog.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct DialogButton {
56    /// Display label.
57    pub label: String,
58    /// Unique identifier.
59    pub id: String,
60    /// Whether this is the primary/default button.
61    pub primary: bool,
62}
63
64impl DialogButton {
65    /// Create a new dialog button.
66    pub fn new(label: impl Into<String>, id: impl Into<String>) -> Self {
67        Self {
68            label: label.into(),
69            id: id.into(),
70            primary: false,
71        }
72    }
73
74    /// Mark as primary button.
75    #[must_use]
76    pub fn primary(mut self) -> Self {
77        self.primary = true;
78        self
79    }
80
81    /// Display width including brackets.
82    pub fn display_width(&self) -> usize {
83        // [ label ] = display_width(label) + 4
84        display_width(self.label.as_str()) + 4
85    }
86}
87
88/// Dialog type variants.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum DialogKind {
91    /// Alert: single OK button.
92    Alert,
93    /// Confirm: OK + Cancel buttons.
94    Confirm,
95    /// Prompt: input field + OK + Cancel.
96    Prompt,
97    /// Custom dialog.
98    Custom,
99}
100
101/// Dialog state for handling input and button focus.
102#[derive(Debug, Clone, Default)]
103pub struct DialogState {
104    /// Currently focused button index.
105    pub focused_button: Option<usize>,
106    /// Button index currently pressed by mouse (Down without matching Up yet).
107    pressed_button: Option<usize>,
108    /// Input field value (for Prompt dialogs).
109    pub input_value: String,
110    /// Whether the input field is focused.
111    pub input_focused: bool,
112    /// Whether the dialog is open.
113    pub open: bool,
114    /// Result after interaction.
115    pub result: Option<DialogResult>,
116}
117
118impl DialogState {
119    /// Create a new open dialog state.
120    pub fn new() -> Self {
121        Self {
122            open: true,
123            input_focused: true, // Start with input focused for prompts
124            ..Default::default()
125        }
126    }
127
128    /// Check if dialog is open.
129    #[inline]
130    pub fn is_open(&self) -> bool {
131        self.open
132    }
133
134    /// Close the dialog with a result.
135    pub fn close(&mut self, result: DialogResult) {
136        self.open = false;
137        self.pressed_button = None;
138        self.result = Some(result);
139    }
140
141    /// Reset the dialog state to open.
142    pub fn reset(&mut self) {
143        self.open = true;
144        self.result = None;
145        self.input_value.clear();
146        self.focused_button = None;
147        self.pressed_button = None;
148        self.input_focused = true;
149    }
150
151    /// Get the result if closed.
152    pub fn take_result(&mut self) -> Option<DialogResult> {
153        self.result.take()
154    }
155}
156
157/// Dialog configuration.
158#[derive(Debug, Clone)]
159pub struct DialogConfig {
160    /// Modal configuration.
161    pub modal_config: ModalConfig,
162    /// Dialog kind.
163    pub kind: DialogKind,
164    /// Button style.
165    pub button_style: Style,
166    /// Primary button style.
167    pub primary_button_style: Style,
168    /// Focused button style.
169    pub focused_button_style: Style,
170    /// Title style.
171    pub title_style: Style,
172    /// Message style.
173    pub message_style: Style,
174    /// Input style (for Prompt).
175    pub input_style: Style,
176}
177
178impl Default for DialogConfig {
179    fn default() -> Self {
180        Self {
181            modal_config: ModalConfig::default()
182                .position(ModalPosition::Center)
183                .size(ModalSizeConstraints::new().min_width(30).max_width(60)),
184            kind: DialogKind::Alert,
185            button_style: Style::new(),
186            primary_button_style: Style::new().bold(),
187            focused_button_style: Style::new().reverse(),
188            title_style: Style::new().bold(),
189            message_style: Style::new(),
190            input_style: Style::new(),
191        }
192    }
193}
194
195/// A dialog widget built on Modal.
196///
197/// Invariants:
198/// - At least one button is always present.
199/// - Button focus wraps around (modular arithmetic).
200/// - For Prompt dialogs, Tab cycles: input -> buttons -> input.
201///
202/// Failure modes:
203/// - If area is too small, content may be truncated but dialog never panics.
204/// - Empty title/message is allowed (renders nothing for that row).
205#[derive(Debug, Clone)]
206pub struct Dialog {
207    /// Dialog title.
208    title: String,
209    /// Dialog message.
210    message: String,
211    /// Buttons.
212    buttons: Vec<DialogButton>,
213    /// Configuration.
214    config: DialogConfig,
215    /// Hit ID for mouse interaction.
216    hit_id: Option<HitId>,
217}
218
219impl Dialog {
220    /// Create an alert dialog (message + OK).
221    pub fn alert(title: impl Into<String>, message: impl Into<String>) -> Self {
222        Self {
223            title: title.into(),
224            message: message.into(),
225            buttons: vec![DialogButton::new("OK", "ok").primary()],
226            config: DialogConfig {
227                kind: DialogKind::Alert,
228                ..Default::default()
229            },
230            hit_id: None,
231        }
232    }
233
234    /// Create a confirm dialog (message + OK/Cancel).
235    pub fn confirm(title: impl Into<String>, message: impl Into<String>) -> Self {
236        Self {
237            title: title.into(),
238            message: message.into(),
239            buttons: vec![
240                DialogButton::new("OK", "ok").primary(),
241                DialogButton::new("Cancel", "cancel"),
242            ],
243            config: DialogConfig {
244                kind: DialogKind::Confirm,
245                ..Default::default()
246            },
247            hit_id: None,
248        }
249    }
250
251    /// Create a prompt dialog (message + input + OK/Cancel).
252    pub fn prompt(title: impl Into<String>, message: impl Into<String>) -> Self {
253        Self {
254            title: title.into(),
255            message: message.into(),
256            buttons: vec![
257                DialogButton::new("OK", "ok").primary(),
258                DialogButton::new("Cancel", "cancel"),
259            ],
260            config: DialogConfig {
261                kind: DialogKind::Prompt,
262                ..Default::default()
263            },
264            hit_id: None,
265        }
266    }
267
268    /// Create a custom dialog with a builder.
269    pub fn custom(title: impl Into<String>, message: impl Into<String>) -> DialogBuilder {
270        DialogBuilder {
271            title: title.into(),
272            message: message.into(),
273            buttons: Vec::new(),
274            config: DialogConfig {
275                kind: DialogKind::Custom,
276                ..Default::default()
277            },
278            hit_id: None,
279        }
280    }
281
282    /// Set the hit ID for mouse interaction.
283    #[must_use]
284    pub fn hit_id(mut self, id: HitId) -> Self {
285        self.hit_id = Some(id);
286        self.config.modal_config = self.config.modal_config.hit_id(id);
287        self
288    }
289
290    /// Set the modal configuration.
291    #[must_use]
292    pub fn modal_config(mut self, config: ModalConfig) -> Self {
293        self.hit_id = config.hit_id;
294        self.config.modal_config = config;
295        self
296    }
297
298    /// Set button style.
299    #[must_use]
300    pub fn button_style(mut self, style: Style) -> Self {
301        self.config.button_style = style;
302        self
303    }
304
305    /// Set primary button style.
306    #[must_use]
307    pub fn primary_button_style(mut self, style: Style) -> Self {
308        self.config.primary_button_style = style;
309        self
310    }
311
312    /// Set focused button style.
313    #[must_use]
314    pub fn focused_button_style(mut self, style: Style) -> Self {
315        self.config.focused_button_style = style;
316        self
317    }
318
319    /// Handle an event and potentially update state.
320    pub fn handle_event(
321        &self,
322        event: &Event,
323        state: &mut DialogState,
324        hit: Option<(HitId, HitRegion, HitData)>,
325    ) -> Option<DialogResult> {
326        if !state.open {
327            return None;
328        }
329
330        if self.config.kind != DialogKind::Prompt && state.input_focused {
331            state.input_focused = false;
332        }
333
334        match event {
335            // Escape closes with Dismissed
336            Event::Key(KeyEvent {
337                code: KeyCode::Escape,
338                kind: KeyEventKind::Press,
339                ..
340            }) if self.config.modal_config.close_on_escape => {
341                state.close(DialogResult::Dismissed);
342                return Some(DialogResult::Dismissed);
343            }
344
345            // Tab cycles focus
346            Event::Key(KeyEvent {
347                code: KeyCode::Tab,
348                kind: KeyEventKind::Press,
349                modifiers,
350                ..
351            }) => {
352                let shift = modifiers.contains(Modifiers::SHIFT);
353                self.cycle_focus(state, shift);
354            }
355
356            // Enter activates focused button
357            Event::Key(KeyEvent {
358                code: KeyCode::Enter,
359                kind: KeyEventKind::Press,
360                ..
361            }) => {
362                return self.activate_button(state);
363            }
364
365            // Arrow keys navigate buttons
366            Event::Key(KeyEvent {
367                code: KeyCode::Left | KeyCode::Right,
368                kind: KeyEventKind::Press,
369                ..
370            }) if !state.input_focused => {
371                let forward = matches!(
372                    event,
373                    Event::Key(KeyEvent {
374                        code: KeyCode::Right,
375                        ..
376                    })
377                );
378                self.navigate_buttons(state, forward);
379            }
380
381            // Mouse down on button (press only; activate on mouse up).
382            Event::Mouse(MouseEvent {
383                kind: MouseEventKind::Down(MouseButton::Left),
384                ..
385            }) => {
386                state.pressed_button = None;
387                if self.config.kind == DialogKind::Prompt
388                    && let (Some((id, region, _)), Some(expected)) = (hit, self.hit_id)
389                    && id == expected
390                    && region == DIALOG_HIT_INPUT
391                {
392                    state.input_focused = true;
393                    state.focused_button = None;
394                    state.pressed_button = None;
395                } else if let (Some((id, region, data)), Some(expected)) = (hit, self.hit_id)
396                    && id == expected
397                    && region == DIALOG_HIT_BUTTON
398                    && let Ok(idx) = usize::try_from(data)
399                    && idx < self.buttons.len()
400                {
401                    state.input_focused = false;
402                    state.focused_button = Some(idx);
403                    state.pressed_button = Some(idx);
404                }
405            }
406
407            // Mouse up on button activates if it matches the pressed target.
408            Event::Mouse(MouseEvent {
409                kind: MouseEventKind::Up(MouseButton::Left),
410                ..
411            }) => {
412                let pressed = state.pressed_button.take();
413                if let (Some(pressed), Some((id, region, data)), Some(expected)) =
414                    (pressed, hit, self.hit_id)
415                    && id == expected
416                    && region == DIALOG_HIT_BUTTON
417                    && let Ok(idx) = usize::try_from(data)
418                    && idx == pressed
419                {
420                    state.input_focused = false;
421                    state.focused_button = Some(idx);
422                    return self.activate_button(state);
423                }
424            }
425
426            Event::Paste(paste)
427                if self.config.kind == DialogKind::Prompt && state.input_focused =>
428            {
429                self.handle_input_paste(state, &paste.text);
430            }
431
432            // For prompt dialogs, handle text input
433            Event::Key(key_event)
434                if self.config.kind == DialogKind::Prompt && state.input_focused =>
435            {
436                self.handle_input_key(state, key_event);
437            }
438
439            _ => {}
440        }
441
442        None
443    }
444
445    fn cycle_focus(&self, state: &mut DialogState, reverse: bool) {
446        let has_input = self.config.kind == DialogKind::Prompt;
447        let button_count = self.buttons.len();
448        state.pressed_button = None;
449
450        if has_input {
451            // Cycle: input -> button 0 -> button 1 -> ... -> input
452            if state.input_focused {
453                state.input_focused = false;
454                state.focused_button = if reverse {
455                    Some(button_count.saturating_sub(1))
456                } else {
457                    Some(0)
458                };
459            } else if let Some(idx) = state.focused_button {
460                if reverse {
461                    if idx == 0 {
462                        state.input_focused = true;
463                        state.focused_button = None;
464                    } else {
465                        state.focused_button = Some(idx - 1);
466                    }
467                } else if idx + 1 >= button_count {
468                    state.input_focused = true;
469                    state.focused_button = None;
470                } else {
471                    state.focused_button = Some(idx + 1);
472                }
473            } else {
474                state.focused_button = if reverse {
475                    Some(button_count.saturating_sub(1))
476                } else {
477                    Some(0)
478                };
479            }
480        } else {
481            // Just cycle buttons
482            state.focused_button = if reverse {
483                Some(match state.focused_button {
484                    Some(0) => button_count - 1,
485                    Some(current) => current - 1,
486                    None => button_count - 1,
487                })
488            } else {
489                Some(match state.focused_button {
490                    Some(current) => (current + 1) % button_count,
491                    None => 0,
492                })
493            };
494        }
495    }
496
497    fn navigate_buttons(&self, state: &mut DialogState, forward: bool) {
498        let count = self.buttons.len();
499        if count == 0 {
500            return;
501        }
502        state.pressed_button = None;
503        state.focused_button = if forward {
504            Some(match state.focused_button {
505                Some(current) => (current + 1) % count,
506                None => 0,
507            })
508        } else {
509            Some(match state.focused_button {
510                Some(0) => count - 1,
511                Some(current) => current - 1,
512                None => count - 1,
513            })
514        };
515    }
516
517    fn activate_button(&self, state: &mut DialogState) -> Option<DialogResult> {
518        let idx = state.focused_button.or_else(|| {
519            // Default to primary button
520            self.buttons.iter().position(|b| b.primary)
521        })?;
522
523        let button = self.buttons.get(idx)?;
524        let result = match button.id.as_str() {
525            "ok" => {
526                if self.config.kind == DialogKind::Prompt {
527                    DialogResult::Input(state.input_value.clone())
528                } else {
529                    DialogResult::Ok
530                }
531            }
532            "cancel" => DialogResult::Cancel,
533            id => DialogResult::Custom(id.to_string()),
534        };
535
536        state.close(result.clone());
537        Some(result)
538    }
539
540    fn handle_input_key(&self, state: &mut DialogState, key: &KeyEvent) {
541        if key.kind != KeyEventKind::Press {
542            return;
543        }
544
545        match key.code {
546            KeyCode::Char(c) => {
547                state.input_value.push(c);
548            }
549            KeyCode::Backspace => {
550                if let Some((grapheme_start, _)) =
551                    state.input_value.grapheme_indices(true).next_back()
552                {
553                    state.input_value.truncate(grapheme_start);
554                }
555            }
556            KeyCode::Delete => {
557                state.input_value.clear();
558            }
559            _ => {}
560        }
561    }
562
563    fn handle_input_paste(&self, state: &mut DialogState, text: &str) {
564        let sanitized: String = text
565            .chars()
566            .map(|c| match c {
567                '\n' | '\r' | '\t' => ' ',
568                other => other,
569            })
570            .filter(|c| !c.is_control())
571            .collect();
572
573        if !sanitized.is_empty() {
574            state.input_value.push_str(&sanitized);
575        }
576    }
577
578    /// Calculate content height.
579    fn content_height(&self) -> u16 {
580        let mut height: u16 = 2; // Top and bottom border
581
582        // The title is rendered into the top border, so it does not consume an
583        // extra interior row.
584        // Message row(s) - simplified: 1 row
585        if !self.message.is_empty() {
586            height += 1;
587        }
588
589        // Spacing
590        height += 1;
591
592        // Input row (for Prompt)
593        if self.config.kind == DialogKind::Prompt {
594            height += 1;
595            height += 1; // Spacing
596        }
597
598        // Button row
599        height += 1;
600
601        height
602    }
603
604    fn effective_size_constraints(&self, content_height: u16) -> ModalSizeConstraints {
605        let mut size = self.config.modal_config.size;
606        if size.min_width.is_none() && size.max_width.is_none() {
607            size.min_width = Some(30);
608            size.max_width = Some(60);
609        }
610        if size.min_height.is_none() && size.max_height.is_none() {
611            size.min_height = Some(content_height);
612            size.max_height = Some(content_height + 4);
613        }
614        size
615    }
616
617    /// Render the dialog content.
618    fn render_content(&self, area: Rect, frame: &mut Frame, state: &DialogState) {
619        if area.is_empty() {
620            return;
621        }
622
623        // Draw border
624        let block = Block::default()
625            .borders(Borders::ALL)
626            .title(&self.title)
627            .title_alignment(Alignment::Center);
628        block.render(area, frame);
629
630        let inner = block.inner(area);
631        if inner.is_empty() {
632            return;
633        }
634
635        // Dialog content owns the inner pane. Clear it before redraw while preserving the
636        // backdrop-tinted background already applied by `Modal`.
637        for y in inner.y..inner.bottom() {
638            for x in inner.x..inner.right() {
639                if let Some(cell) = frame.buffer.get_mut(x, y) {
640                    let bg = cell.bg;
641                    *cell = Cell::from_char(' ');
642                    cell.bg = bg;
643                }
644            }
645        }
646
647        let mut y = inner.y;
648
649        // Message
650        if !self.message.is_empty() && y < inner.bottom() {
651            self.draw_centered_text(
652                frame,
653                inner.x,
654                y,
655                inner.width,
656                &self.message,
657                self.config.message_style,
658            );
659            y += 1;
660        }
661
662        // Spacing
663        y += 1;
664
665        // Input field (for Prompt)
666        if self.config.kind == DialogKind::Prompt && y < inner.bottom() {
667            self.render_input(frame, inner.x, y, inner.width, state);
668            y += 2; // Input + spacing
669        }
670
671        // Buttons
672        if y < inner.bottom() {
673            self.render_buttons(frame, inner.x, y, inner.width, state);
674        }
675    }
676
677    fn draw_centered_text(
678        &self,
679        frame: &mut Frame,
680        x: u16,
681        y: u16,
682        width: u16,
683        text: &str,
684        style: Style,
685    ) {
686        let text_width = display_width(text).min(width as usize);
687        let offset = (width as usize - text_width) / 2;
688        let start_x = x.saturating_add(offset as u16);
689        draw_text_span(frame, start_x, y, text, style, x.saturating_add(width));
690    }
691
692    fn render_input(&self, frame: &mut Frame, x: u16, y: u16, width: u16, state: &DialogState) {
693        // Draw input background
694        let input_area = Rect::new(x + 1, y, width.saturating_sub(2), 1);
695        let input_style = self.config.input_style;
696        set_style_area(&mut frame.buffer, input_area, input_style);
697
698        if let Some(hit_id) = self.hit_id
699            && !input_area.is_empty()
700        {
701            frame.register_hit(input_area, hit_id, DIALOG_HIT_INPUT, 0);
702        }
703
704        // Draw input value or placeholder
705        let display_text = if state.input_value.is_empty() {
706            " "
707        } else {
708            &state.input_value
709        };
710
711        draw_text_span(
712            frame,
713            input_area.x,
714            y,
715            display_text,
716            input_style,
717            input_area.right(),
718        );
719
720        // Draw cursor if focused
721        if state.input_focused {
722            let input_width = display_width(state.input_value.as_str());
723            let cursor_x = input_area.x + input_width.min(input_area.width as usize) as u16;
724            if cursor_x < input_area.right() {
725                frame.cursor_position = Some((cursor_x, y));
726                frame.cursor_visible = true;
727            }
728        }
729    }
730
731    fn render_buttons(&self, frame: &mut Frame, x: u16, y: u16, width: u16, state: &DialogState) {
732        if self.buttons.is_empty() {
733            return;
734        }
735
736        // Calculate total button width
737        let total_width: usize = self
738            .buttons
739            .iter()
740            .map(|b| b.display_width())
741            .sum::<usize>()
742            + self.buttons.len().saturating_sub(1) * 2; // Spacing between buttons
743
744        // Center the buttons
745        let start_x = x + (width as usize - total_width.min(width as usize)) as u16 / 2;
746        let mut bx = start_x;
747
748        for (i, button) in self.buttons.iter().enumerate() {
749            let is_focused = state.focused_button == Some(i);
750
751            // Select style
752            let mut style = if is_focused {
753                self.config.focused_button_style
754            } else if button.primary {
755                self.config.primary_button_style
756            } else {
757                self.config.button_style
758            };
759            if is_focused {
760                let has_reverse = style
761                    .attrs
762                    .is_some_and(|attrs| attrs.contains(StyleFlags::REVERSE));
763                if !has_reverse {
764                    style = style.reverse();
765                }
766            }
767
768            // Draw button: [ label ]
769            let btn_text = format!("[ {} ]", button.label);
770            let btn_width = display_width(btn_text.as_str());
771            draw_text_span(frame, bx, y, &btn_text, style, x.saturating_add(width));
772
773            // Register hit region for button
774            if let Some(hit_id) = self.hit_id {
775                let max_btn_width = width.saturating_sub(bx.saturating_sub(x));
776                let btn_area_width = btn_width.min(max_btn_width as usize) as u16;
777                if btn_area_width > 0 {
778                    let btn_area = Rect::new(bx, y, btn_area_width, 1);
779                    frame.register_hit(btn_area, hit_id, DIALOG_HIT_BUTTON, i as u64);
780                }
781            }
782
783            bx = bx.saturating_add(btn_width as u16 + 2); // Button + spacing
784        }
785    }
786}
787
788impl StatefulWidget for Dialog {
789    type State = DialogState;
790
791    fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
792        if !state.open || area.is_empty() {
793            return;
794        }
795
796        // Calculate content area
797        let content_height = self.content_height();
798        let config = self
799            .config
800            .modal_config
801            .clone()
802            .size(self.effective_size_constraints(content_height));
803
804        // Create a wrapper widget for the dialog content
805        let content = DialogContent {
806            dialog: self,
807            state,
808        };
809
810        // Render via Modal
811        let modal = Modal::new(content).config(config);
812        modal.render(area, frame);
813    }
814}
815
816/// Internal wrapper for rendering dialog content.
817struct DialogContent<'a> {
818    dialog: &'a Dialog,
819    state: &'a DialogState,
820}
821
822impl Widget for DialogContent<'_> {
823    fn render(&self, area: Rect, frame: &mut Frame) {
824        self.dialog.render_content(area, frame, self.state);
825    }
826}
827
828/// Builder for custom dialogs.
829#[derive(Debug, Clone)]
830#[must_use]
831pub struct DialogBuilder {
832    title: String,
833    message: String,
834    buttons: Vec<DialogButton>,
835    config: DialogConfig,
836    hit_id: Option<HitId>,
837}
838
839impl DialogBuilder {
840    /// Add a button.
841    pub fn button(mut self, button: DialogButton) -> Self {
842        self.buttons.push(button);
843        self
844    }
845
846    /// Add an OK button.
847    pub fn ok_button(self) -> Self {
848        self.button(DialogButton::new("OK", "ok").primary())
849    }
850
851    /// Add a Cancel button.
852    pub fn cancel_button(self) -> Self {
853        self.button(DialogButton::new("Cancel", "cancel"))
854    }
855
856    /// Add a custom button.
857    pub fn custom_button(self, label: impl Into<String>, id: impl Into<String>) -> Self {
858        self.button(DialogButton::new(label, id))
859    }
860
861    /// Set modal configuration.
862    pub fn modal_config(mut self, config: ModalConfig) -> Self {
863        self.hit_id = config.hit_id;
864        self.config.modal_config = config;
865        self
866    }
867
868    /// Set hit ID for mouse interaction.
869    pub fn hit_id(mut self, id: HitId) -> Self {
870        self.hit_id = Some(id);
871        self.config.modal_config = self.config.modal_config.hit_id(id);
872        self
873    }
874
875    /// Build the dialog.
876    pub fn build(self) -> Dialog {
877        let mut buttons = self.buttons;
878        if buttons.is_empty() {
879            buttons.push(DialogButton::new("OK", "ok").primary());
880        }
881
882        Dialog {
883            title: self.title,
884            message: self.message,
885            buttons,
886            config: self.config,
887            hit_id: self.hit_id,
888        }
889    }
890}
891
892#[cfg(test)]
893mod tests {
894    use super::*;
895    use ftui_render::grapheme_pool::GraphemePool;
896
897    fn row_text(frame: &Frame, y: u16) -> String {
898        (0..frame.buffer.width())
899            .map(|x| {
900                frame
901                    .buffer
902                    .get(x, y)
903                    .unwrap()
904                    .content
905                    .as_char()
906                    .unwrap_or(' ')
907            })
908            .collect()
909    }
910
911    fn hit_bounds(frame: &Frame, expected: (HitId, HitRegion, u64)) -> Option<Rect> {
912        let mut min_x = u16::MAX;
913        let mut min_y = u16::MAX;
914        let mut max_x = 0;
915        let mut max_y = 0;
916        let mut found = false;
917
918        for y in 0..frame.buffer.height() {
919            for x in 0..frame.buffer.width() {
920                if frame.hit_test(x, y) == Some(expected) {
921                    min_x = min_x.min(x);
922                    min_y = min_y.min(y);
923                    max_x = max_x.max(x);
924                    max_y = max_y.max(y);
925                    found = true;
926                }
927            }
928        }
929
930        found.then(|| {
931            Rect::new(
932                min_x,
933                min_y,
934                max_x.saturating_sub(min_x) + 1,
935                max_y.saturating_sub(min_y) + 1,
936            )
937        })
938    }
939
940    #[test]
941    fn alert_dialog_single_button() {
942        let dialog = Dialog::alert("Title", "Message");
943        assert_eq!(dialog.buttons.len(), 1);
944        assert_eq!(dialog.buttons[0].label, "OK");
945        assert!(dialog.buttons[0].primary);
946    }
947
948    #[test]
949    fn confirm_dialog_two_buttons() {
950        let dialog = Dialog::confirm("Title", "Message");
951        assert_eq!(dialog.buttons.len(), 2);
952        assert_eq!(dialog.buttons[0].label, "OK");
953        assert_eq!(dialog.buttons[1].label, "Cancel");
954    }
955
956    #[test]
957    fn prompt_dialog_has_input() {
958        let dialog = Dialog::prompt("Title", "Message");
959        assert_eq!(dialog.config.kind, DialogKind::Prompt);
960        assert_eq!(dialog.buttons.len(), 2);
961    }
962
963    #[test]
964    fn custom_dialog_builder() {
965        let dialog = Dialog::custom("Custom", "Message")
966            .ok_button()
967            .cancel_button()
968            .custom_button("Help", "help")
969            .build();
970        assert_eq!(dialog.buttons.len(), 3);
971    }
972
973    #[test]
974    fn dialog_state_starts_open() {
975        let state = DialogState::new();
976        assert!(state.is_open());
977        assert!(state.result.is_none());
978    }
979
980    #[test]
981    fn dialog_state_close_sets_result() {
982        let mut state = DialogState::new();
983        state.close(DialogResult::Ok);
984        assert!(!state.is_open());
985        assert_eq!(state.result, Some(DialogResult::Ok));
986    }
987
988    #[test]
989    fn dialog_escape_closes() {
990        let dialog = Dialog::alert("Test", "Msg");
991        let mut state = DialogState::new();
992        let event = Event::Key(KeyEvent {
993            code: KeyCode::Escape,
994            modifiers: Modifiers::empty(),
995            kind: KeyEventKind::Press,
996        });
997        let result = dialog.handle_event(&event, &mut state, None);
998        assert_eq!(result, Some(DialogResult::Dismissed));
999        assert!(!state.is_open());
1000    }
1001
1002    #[test]
1003    fn dialog_enter_activates_primary() {
1004        let dialog = Dialog::alert("Test", "Msg");
1005        let mut state = DialogState::new();
1006        state.input_focused = false; // Not on input
1007        let event = Event::Key(KeyEvent {
1008            code: KeyCode::Enter,
1009            modifiers: Modifiers::empty(),
1010            kind: KeyEventKind::Press,
1011        });
1012        let result = dialog.handle_event(&event, &mut state, None);
1013        assert_eq!(result, Some(DialogResult::Ok));
1014    }
1015
1016    #[test]
1017    fn dialog_mouse_up_activates_pressed_button() {
1018        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
1019        let mut state = DialogState::new();
1020
1021        let down = Event::Mouse(MouseEvent::new(
1022            MouseEventKind::Down(MouseButton::Left),
1023            0,
1024            0,
1025        ));
1026        let hit = Some((HitId::new(1), HitRegion::Button, 0u64));
1027        let result = dialog.handle_event(&down, &mut state, hit);
1028        assert_eq!(result, None);
1029        assert_eq!(state.focused_button, Some(0));
1030        assert_eq!(state.pressed_button, Some(0));
1031
1032        let up = Event::Mouse(MouseEvent::new(MouseEventKind::Up(MouseButton::Left), 0, 0));
1033        let result = dialog.handle_event(&up, &mut state, hit);
1034        assert_eq!(result, Some(DialogResult::Ok));
1035        assert!(!state.is_open());
1036    }
1037
1038    #[test]
1039    fn prompt_mouse_down_on_button_transfers_focus_from_input() {
1040        let dialog = Dialog::prompt("Test", "Msg").hit_id(HitId::new(1));
1041        let mut state = DialogState::new();
1042        assert!(state.input_focused);
1043        assert_eq!(state.focused_button, None);
1044
1045        let down = Event::Mouse(MouseEvent::new(
1046            MouseEventKind::Down(MouseButton::Left),
1047            0,
1048            0,
1049        ));
1050        let hit = Some((HitId::new(1), HitRegion::Button, 1u64));
1051        let result = dialog.handle_event(&down, &mut state, hit);
1052
1053        assert_eq!(result, None);
1054        assert!(!state.input_focused);
1055        assert_eq!(state.focused_button, Some(1));
1056        assert_eq!(state.pressed_button, Some(1));
1057    }
1058
1059    #[test]
1060    fn prompt_mouse_button_focus_allows_arrow_navigation_after_missed_click() {
1061        let dialog = Dialog::prompt("Test", "Msg").hit_id(HitId::new(1));
1062        let mut state = DialogState::new();
1063
1064        let down = Event::Mouse(MouseEvent::new(
1065            MouseEventKind::Down(MouseButton::Left),
1066            0,
1067            0,
1068        ));
1069        let hit = Some((HitId::new(1), HitRegion::Button, 0u64));
1070        dialog.handle_event(&down, &mut state, hit);
1071
1072        let up_outside = Event::Mouse(MouseEvent::new(MouseEventKind::Up(MouseButton::Left), 0, 0));
1073        dialog.handle_event(&up_outside, &mut state, None);
1074        assert!(!state.input_focused);
1075        assert_eq!(state.focused_button, Some(0));
1076
1077        let right = Event::Key(KeyEvent {
1078            code: KeyCode::Right,
1079            modifiers: Modifiers::empty(),
1080            kind: KeyEventKind::Press,
1081        });
1082        dialog.handle_event(&right, &mut state, None);
1083        assert_eq!(state.focused_button, Some(1));
1084    }
1085
1086    #[test]
1087    fn prompt_mouse_down_on_input_restores_input_focus() {
1088        let dialog = Dialog::prompt("Test", "Msg").hit_id(HitId::new(1));
1089        let mut state = DialogState::new();
1090        state.input_focused = false;
1091        state.focused_button = Some(1);
1092        state.pressed_button = Some(1);
1093
1094        let down = Event::Mouse(MouseEvent::new(
1095            MouseEventKind::Down(MouseButton::Left),
1096            0,
1097            0,
1098        ));
1099        let hit = Some((HitId::new(1), DIALOG_HIT_INPUT, 0u64));
1100        let result = dialog.handle_event(&down, &mut state, hit);
1101
1102        assert_eq!(result, None);
1103        assert!(state.input_focused);
1104        assert_eq!(state.focused_button, None);
1105        assert_eq!(state.pressed_button, None);
1106    }
1107
1108    #[test]
1109    fn render_prompt_registers_input_hit_region() {
1110        let dialog = Dialog::prompt("Prompt", "Enter:").hit_id(HitId::new(7));
1111        let mut state = DialogState::new();
1112        let mut pool = GraphemePool::new();
1113        let mut frame = Frame::with_hit_grid(40, 10, &mut pool);
1114
1115        dialog.render(Rect::new(0, 0, 40, 10), &mut frame, &mut state);
1116
1117        let found = (0..frame.buffer.height()).any(|y| {
1118            (0..frame.buffer.width())
1119                .any(|x| frame.hit_test(x, y) == Some((HitId::new(7), DIALOG_HIT_INPUT, 0)))
1120        });
1121        assert!(found);
1122    }
1123
1124    #[test]
1125    fn render_prompt_registers_input_hit_region_from_modal_config_hit_id() {
1126        let dialog = Dialog::prompt("Prompt", "Enter:")
1127            .modal_config(ModalConfig::default().hit_id(HitId::new(7)));
1128        let mut state = DialogState::new();
1129        let mut pool = GraphemePool::new();
1130        let mut frame = Frame::with_hit_grid(40, 10, &mut pool);
1131
1132        dialog.render(Rect::new(0, 0, 40, 10), &mut frame, &mut state);
1133
1134        let found = (0..frame.buffer.height()).any(|y| {
1135            (0..frame.buffer.width())
1136                .any(|x| frame.hit_test(x, y) == Some((HitId::new(7), DIALOG_HIT_INPUT, 0)))
1137        });
1138        assert!(found);
1139    }
1140
1141    #[test]
1142    fn render_respects_modal_config_size_constraints() {
1143        let dialog = Dialog::alert("Prompt", "Enter:").modal_config(
1144            ModalConfig::default()
1145                .hit_id(HitId::new(11))
1146                .size(ModalSizeConstraints::new().max_width(10).max_height(5)),
1147        );
1148        let mut state = DialogState::new();
1149        let mut pool = GraphemePool::new();
1150        let mut frame = Frame::with_hit_grid(40, 20, &mut pool);
1151
1152        dialog.render(Rect::new(0, 0, 40, 20), &mut frame, &mut state);
1153
1154        let content = hit_bounds(&frame, (HitId::new(11), crate::modal::MODAL_HIT_CONTENT, 0))
1155            .expect("dialog content hit region should exist");
1156        assert_eq!(content.width, 10);
1157        assert_eq!(content.height, 5);
1158    }
1159
1160    #[test]
1161    fn dialog_mouse_up_outside_does_not_activate() {
1162        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
1163        let mut state = DialogState::new();
1164
1165        let down = Event::Mouse(MouseEvent::new(
1166            MouseEventKind::Down(MouseButton::Left),
1167            0,
1168            0,
1169        ));
1170        let hit = Some((HitId::new(1), HitRegion::Button, 0u64));
1171        let result = dialog.handle_event(&down, &mut state, hit);
1172        assert_eq!(result, None);
1173        assert_eq!(state.pressed_button, Some(0));
1174
1175        let up = Event::Mouse(MouseEvent::new(MouseEventKind::Up(MouseButton::Left), 0, 0));
1176        let result = dialog.handle_event(&up, &mut state, None);
1177        assert_eq!(result, None);
1178        assert!(state.is_open());
1179        assert_eq!(state.pressed_button, None);
1180    }
1181
1182    #[test]
1183    fn dialog_tab_cycles_focus() {
1184        let dialog = Dialog::confirm("Test", "Msg");
1185        let mut state = DialogState::new();
1186        state.input_focused = false;
1187        state.focused_button = Some(0);
1188
1189        let tab = Event::Key(KeyEvent {
1190            code: KeyCode::Tab,
1191            modifiers: Modifiers::empty(),
1192            kind: KeyEventKind::Press,
1193        });
1194
1195        dialog.handle_event(&tab, &mut state, None);
1196        assert_eq!(state.focused_button, Some(1));
1197
1198        dialog.handle_event(&tab, &mut state, None);
1199        assert_eq!(state.focused_button, Some(0)); // Wraps around
1200    }
1201
1202    #[test]
1203    fn fresh_non_prompt_tab_starts_on_primary_button() {
1204        let dialog = Dialog::confirm("Test", "Msg");
1205        let mut state = DialogState::new();
1206        assert_eq!(state.focused_button, None);
1207        assert!(state.input_focused);
1208
1209        let tab = Event::Key(KeyEvent {
1210            code: KeyCode::Tab,
1211            modifiers: Modifiers::empty(),
1212            kind: KeyEventKind::Press,
1213        });
1214
1215        dialog.handle_event(&tab, &mut state, None);
1216        assert!(!state.input_focused);
1217        assert_eq!(state.focused_button, Some(0));
1218    }
1219
1220    #[test]
1221    fn fresh_non_prompt_right_arrow_starts_on_primary_button() {
1222        let dialog = Dialog::confirm("Test", "Msg");
1223        let mut state = DialogState::new();
1224        assert_eq!(state.focused_button, None);
1225        assert!(state.input_focused);
1226
1227        let right = Event::Key(KeyEvent {
1228            code: KeyCode::Right,
1229            modifiers: Modifiers::empty(),
1230            kind: KeyEventKind::Press,
1231        });
1232
1233        dialog.handle_event(&right, &mut state, None);
1234        assert!(!state.input_focused);
1235        assert_eq!(state.focused_button, Some(0));
1236    }
1237
1238    #[test]
1239    fn tab_navigation_cancels_pressed_button() {
1240        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
1241        let mut state = DialogState::new();
1242
1243        let down = Event::Mouse(MouseEvent::new(
1244            MouseEventKind::Down(MouseButton::Left),
1245            0,
1246            0,
1247        ));
1248        let hit0 = Some((HitId::new(1), HitRegion::Button, 0u64));
1249        dialog.handle_event(&down, &mut state, hit0);
1250        assert_eq!(state.pressed_button, Some(0));
1251
1252        let tab = Event::Key(KeyEvent {
1253            code: KeyCode::Tab,
1254            modifiers: Modifiers::empty(),
1255            kind: KeyEventKind::Press,
1256        });
1257        dialog.handle_event(&tab, &mut state, None);
1258        assert_eq!(state.focused_button, Some(1));
1259        assert_eq!(state.pressed_button, None);
1260
1261        let up = Event::Mouse(MouseEvent::new(MouseEventKind::Up(MouseButton::Left), 0, 0));
1262        let result = dialog.handle_event(&up, &mut state, hit0);
1263        assert_eq!(result, None);
1264        assert!(state.is_open());
1265    }
1266
1267    #[test]
1268    fn prompt_enter_returns_input() {
1269        let dialog = Dialog::prompt("Test", "Enter:");
1270        let mut state = DialogState::new();
1271        state.input_value = "hello".to_string();
1272        state.input_focused = false;
1273        state.focused_button = Some(0); // OK button
1274
1275        let enter = Event::Key(KeyEvent {
1276            code: KeyCode::Enter,
1277            modifiers: Modifiers::empty(),
1278            kind: KeyEventKind::Press,
1279        });
1280
1281        let result = dialog.handle_event(&enter, &mut state, None);
1282        assert_eq!(result, Some(DialogResult::Input("hello".to_string())));
1283    }
1284
1285    #[test]
1286    fn button_display_width() {
1287        let button = DialogButton::new("OK", "ok");
1288        assert_eq!(button.display_width(), 6); // [ OK ]
1289    }
1290
1291    #[test]
1292    fn render_alert_does_not_panic() {
1293        let dialog = Dialog::alert("Alert", "This is an alert message.");
1294        let mut state = DialogState::new();
1295        let mut pool = GraphemePool::new();
1296        let mut frame = Frame::new(80, 24, &mut pool);
1297        dialog.render(Rect::new(0, 0, 80, 24), &mut frame, &mut state);
1298    }
1299
1300    #[test]
1301    fn render_confirm_does_not_panic() {
1302        let dialog = Dialog::confirm("Confirm", "Are you sure?");
1303        let mut state = DialogState::new();
1304        let mut pool = GraphemePool::new();
1305        let mut frame = Frame::new(80, 24, &mut pool);
1306        dialog.render(Rect::new(0, 0, 80, 24), &mut frame, &mut state);
1307    }
1308
1309    #[test]
1310    fn render_prompt_does_not_panic() {
1311        let dialog = Dialog::prompt("Prompt", "Enter your name:");
1312        let mut state = DialogState::new();
1313        state.input_value = "Test User".to_string();
1314        let mut pool = GraphemePool::new();
1315        let mut frame = Frame::new(80, 24, &mut pool);
1316        dialog.render(Rect::new(0, 0, 80, 24), &mut frame, &mut state);
1317    }
1318
1319    #[test]
1320    fn render_content_shorter_message_and_buttons_clear_stale_inner_rows() {
1321        let dialog_long = Dialog::custom("Title", "LLLLLLLLLLLLLLLLLLLL")
1322            .custom_button("Alpha", "alpha")
1323            .custom_button("Beta", "beta")
1324            .custom_button("Gamma", "gamma")
1325            .build();
1326        let dialog_short = Dialog::custom("Title", "S").ok_button().build();
1327        let state = DialogState::new();
1328        let area = Rect::new(10, 5, 40, 8);
1329        let mut pool = GraphemePool::new();
1330        let mut frame = Frame::new(80, 24, &mut pool);
1331
1332        dialog_long.render_content(area, &mut frame, &state);
1333        dialog_short.render_content(area, &mut frame, &state);
1334
1335        let inner = Block::default()
1336            .borders(Borders::ALL)
1337            .title("Title")
1338            .title_alignment(Alignment::Center)
1339            .inner(area);
1340        let message_row = row_text(&frame, inner.y);
1341        let button_row = row_text(&frame, inner.y + 2);
1342
1343        assert!(message_row.contains('S'));
1344        assert!(!message_row.contains('L'));
1345        assert!(button_row.contains("[ OK ]"));
1346        assert!(!button_row.contains("Alpha"));
1347        assert!(!button_row.contains("Beta"));
1348        assert!(!button_row.contains("Gamma"));
1349    }
1350
1351    #[test]
1352    fn render_prompt_shorter_input_clears_stale_suffix() {
1353        let dialog = Dialog::prompt("Prompt", "Enter:");
1354        let area = Rect::new(10, 5, 40, 8);
1355        let mut long_state = DialogState::new();
1356        long_state.input_value = "LongInputValue".to_string();
1357        let mut short_state = DialogState::new();
1358        short_state.input_value = "Hi".to_string();
1359        let mut pool = GraphemePool::new();
1360        let mut frame = Frame::new(80, 24, &mut pool);
1361
1362        dialog.render_content(area, &mut frame, &long_state);
1363        dialog.render_content(area, &mut frame, &short_state);
1364
1365        let inner = Block::default()
1366            .borders(Borders::ALL)
1367            .title("Prompt")
1368            .title_alignment(Alignment::Center)
1369            .inner(area);
1370        let input_row = row_text(&frame, inner.y + 2);
1371
1372        assert!(input_row.contains("Hi"));
1373        assert!(!input_row.contains("LongInputValue"));
1374        assert!(!input_row.contains("ngInputValue"));
1375    }
1376
1377    #[test]
1378    fn render_tiny_area_does_not_panic() {
1379        let dialog = Dialog::alert("T", "M");
1380        let mut state = DialogState::new();
1381        let mut pool = GraphemePool::new();
1382        let mut frame = Frame::new(10, 5, &mut pool);
1383        dialog.render(Rect::new(0, 0, 10, 5), &mut frame, &mut state);
1384    }
1385
1386    #[test]
1387    fn custom_dialog_empty_buttons_gets_default() {
1388        let dialog = Dialog::custom("Custom", "No buttons").build();
1389        assert_eq!(dialog.buttons.len(), 1);
1390        assert_eq!(dialog.buttons[0].label, "OK");
1391    }
1392
1393    #[test]
1394    fn render_unicode_message_does_not_panic() {
1395        // CJK characters are 2 columns wide each
1396        let dialog = Dialog::alert("你好", "这是一条消息 🎉");
1397        let mut state = DialogState::new();
1398        let mut pool = GraphemePool::new();
1399        let mut frame = Frame::new(80, 24, &mut pool);
1400        dialog.render(Rect::new(0, 0, 80, 24), &mut frame, &mut state);
1401    }
1402
1403    #[test]
1404    fn prompt_with_unicode_input_renders_correctly() {
1405        let dialog = Dialog::prompt("入力", "名前を入力:");
1406        let mut state = DialogState::new();
1407        state.input_value = "田中太郎".to_string(); // CJK input
1408        let mut pool = GraphemePool::new();
1409        let mut frame = Frame::new(80, 24, &mut pool);
1410        dialog.render(Rect::new(0, 0, 80, 24), &mut frame, &mut state);
1411    }
1412
1413    // ---- Edge-case tests (bd-1is2p) ----
1414
1415    #[test]
1416    fn edge_state_default_vs_new() {
1417        let default = DialogState::default();
1418        let new = DialogState::new();
1419        // Default: open=false, input_focused=false
1420        assert!(!default.open);
1421        assert!(!default.input_focused);
1422        // New: open=true, input_focused=true
1423        assert!(new.open);
1424        assert!(new.input_focused);
1425    }
1426
1427    #[test]
1428    fn edge_state_reset_then_reuse() {
1429        let mut state = DialogState::new();
1430        state.input_value = "typed".to_string();
1431        state.focused_button = Some(1);
1432        state.close(DialogResult::Cancel);
1433
1434        assert!(!state.is_open());
1435        assert!(state.result.is_some());
1436
1437        state.reset();
1438        assert!(state.is_open());
1439        assert!(state.result.is_none());
1440        assert!(state.input_value.is_empty());
1441        assert_eq!(state.focused_button, None);
1442        assert!(state.input_focused);
1443    }
1444
1445    #[test]
1446    fn edge_take_result_when_none() {
1447        let mut state = DialogState::new();
1448        assert_eq!(state.take_result(), None);
1449        // Calling again still returns None
1450        assert_eq!(state.take_result(), None);
1451    }
1452
1453    #[test]
1454    fn edge_take_result_consumes() {
1455        let mut state = DialogState::new();
1456        state.close(DialogResult::Ok);
1457        assert_eq!(state.take_result(), Some(DialogResult::Ok));
1458        // Second call returns None — consumed
1459        assert_eq!(state.take_result(), None);
1460    }
1461
1462    #[test]
1463    fn edge_handle_event_when_closed() {
1464        let dialog = Dialog::alert("Test", "Msg");
1465        let mut state = DialogState::new();
1466        state.close(DialogResult::Dismissed);
1467
1468        let enter = Event::Key(KeyEvent {
1469            code: KeyCode::Enter,
1470            modifiers: Modifiers::empty(),
1471            kind: KeyEventKind::Press,
1472        });
1473        // Events on a closed dialog return None immediately
1474        let result = dialog.handle_event(&enter, &mut state, None);
1475        assert_eq!(result, None);
1476    }
1477
1478    #[test]
1479    fn edge_prompt_tab_full_cycle() {
1480        let dialog = Dialog::prompt("Test", "Enter:");
1481        let mut state = DialogState::new();
1482        // Prompt starts with input_focused=true
1483        assert!(state.input_focused);
1484        assert_eq!(state.focused_button, None);
1485
1486        let tab = Event::Key(KeyEvent {
1487            code: KeyCode::Tab,
1488            modifiers: Modifiers::empty(),
1489            kind: KeyEventKind::Press,
1490        });
1491
1492        // Tab 1: input -> button 0 (OK)
1493        dialog.handle_event(&tab, &mut state, None);
1494        assert!(!state.input_focused);
1495        assert_eq!(state.focused_button, Some(0));
1496
1497        // Tab 2: button 0 -> button 1 (Cancel)
1498        dialog.handle_event(&tab, &mut state, None);
1499        assert!(!state.input_focused);
1500        assert_eq!(state.focused_button, Some(1));
1501
1502        // Tab 3: button 1 -> back to input
1503        dialog.handle_event(&tab, &mut state, None);
1504        assert!(state.input_focused);
1505        assert_eq!(state.focused_button, None);
1506    }
1507
1508    #[test]
1509    fn edge_prompt_shift_tab_reverse_cycle() {
1510        let dialog = Dialog::prompt("Test", "Enter:");
1511        let mut state = DialogState::new();
1512
1513        let shift_tab = Event::Key(KeyEvent {
1514            code: KeyCode::Tab,
1515            modifiers: Modifiers::SHIFT,
1516            kind: KeyEventKind::Press,
1517        });
1518
1519        // Shift+Tab from input -> last button (Cancel, index 1)
1520        dialog.handle_event(&shift_tab, &mut state, None);
1521        assert!(!state.input_focused);
1522        assert_eq!(state.focused_button, Some(1));
1523
1524        // Shift+Tab from button 1 -> button 0
1525        dialog.handle_event(&shift_tab, &mut state, None);
1526        assert!(!state.input_focused);
1527        assert_eq!(state.focused_button, Some(0));
1528
1529        // Shift+Tab from button 0 -> back to input
1530        dialog.handle_event(&shift_tab, &mut state, None);
1531        assert!(state.input_focused);
1532        assert_eq!(state.focused_button, None);
1533    }
1534
1535    #[test]
1536    fn prompt_tab_recovers_when_button_focus_is_missing() {
1537        let dialog = Dialog::prompt("Test", "Enter:");
1538        let mut state = DialogState::new();
1539        state.input_focused = false;
1540        state.focused_button = None;
1541
1542        let tab = Event::Key(KeyEvent {
1543            code: KeyCode::Tab,
1544            modifiers: Modifiers::empty(),
1545            kind: KeyEventKind::Press,
1546        });
1547
1548        dialog.handle_event(&tab, &mut state, None);
1549        assert!(!state.input_focused);
1550        assert_eq!(state.focused_button, Some(0));
1551    }
1552
1553    #[test]
1554    fn edge_arrow_key_navigation() {
1555        let dialog = Dialog::confirm("Test", "Msg");
1556        let mut state = DialogState::new();
1557        state.input_focused = false;
1558        state.focused_button = Some(0);
1559
1560        let right = Event::Key(KeyEvent {
1561            code: KeyCode::Right,
1562            modifiers: Modifiers::empty(),
1563            kind: KeyEventKind::Press,
1564        });
1565        let left = Event::Key(KeyEvent {
1566            code: KeyCode::Left,
1567            modifiers: Modifiers::empty(),
1568            kind: KeyEventKind::Press,
1569        });
1570
1571        // Right: 0 -> 1
1572        dialog.handle_event(&right, &mut state, None);
1573        assert_eq!(state.focused_button, Some(1));
1574
1575        // Right: 1 -> 0 (wrap)
1576        dialog.handle_event(&right, &mut state, None);
1577        assert_eq!(state.focused_button, Some(0));
1578
1579        // Left: 0 -> 1 (wrap backwards)
1580        dialog.handle_event(&left, &mut state, None);
1581        assert_eq!(state.focused_button, Some(1));
1582
1583        // Left: 1 -> 0
1584        dialog.handle_event(&left, &mut state, None);
1585        assert_eq!(state.focused_button, Some(0));
1586    }
1587
1588    #[test]
1589    fn edge_arrow_keys_ignored_when_input_focused() {
1590        let dialog = Dialog::prompt("Test", "Enter:");
1591        let mut state = DialogState::new();
1592        // input_focused=true by default for prompt
1593        assert!(state.input_focused);
1594        state.focused_button = None;
1595
1596        let right = Event::Key(KeyEvent {
1597            code: KeyCode::Right,
1598            modifiers: Modifiers::empty(),
1599            kind: KeyEventKind::Press,
1600        });
1601
1602        dialog.handle_event(&right, &mut state, None);
1603        // Arrow keys should NOT navigate buttons when input is focused
1604        assert!(state.input_focused);
1605        assert_eq!(state.focused_button, None);
1606    }
1607
1608    #[test]
1609    fn prompt_arrow_navigation_cancels_pressed_button() {
1610        let dialog = Dialog::prompt("Test", "Enter:").hit_id(HitId::new(1));
1611        let mut state = DialogState::new();
1612
1613        let down = Event::Mouse(MouseEvent::new(
1614            MouseEventKind::Down(MouseButton::Left),
1615            0,
1616            0,
1617        ));
1618        let hit0 = Some((HitId::new(1), HitRegion::Button, 0u64));
1619        dialog.handle_event(&down, &mut state, hit0);
1620        assert_eq!(state.focused_button, Some(0));
1621        assert_eq!(state.pressed_button, Some(0));
1622
1623        let right = Event::Key(KeyEvent {
1624            code: KeyCode::Right,
1625            modifiers: Modifiers::empty(),
1626            kind: KeyEventKind::Press,
1627        });
1628        dialog.handle_event(&right, &mut state, None);
1629        assert_eq!(state.focused_button, Some(1));
1630        assert_eq!(state.pressed_button, None);
1631
1632        let up = Event::Mouse(MouseEvent::new(MouseEventKind::Up(MouseButton::Left), 0, 0));
1633        let result = dialog.handle_event(&up, &mut state, hit0);
1634        assert_eq!(result, None);
1635        assert!(state.is_open());
1636    }
1637
1638    #[test]
1639    fn edge_input_backspace_on_empty() {
1640        let dialog = Dialog::prompt("Test", "Enter:");
1641        let mut state = DialogState::new();
1642        assert!(state.input_value.is_empty());
1643
1644        let backspace = Event::Key(KeyEvent {
1645            code: KeyCode::Backspace,
1646            modifiers: Modifiers::empty(),
1647            kind: KeyEventKind::Press,
1648        });
1649
1650        // Backspace on empty input should not panic
1651        dialog.handle_event(&backspace, &mut state, None);
1652        assert!(state.input_value.is_empty());
1653    }
1654
1655    #[test]
1656    fn edge_input_backspace_removes_whole_grapheme_cluster() {
1657        let dialog = Dialog::prompt("Test", "Enter:");
1658        let mut state = DialogState::new();
1659        state.input_value = "e\u{301}".to_string();
1660
1661        let backspace = Event::Key(KeyEvent {
1662            code: KeyCode::Backspace,
1663            modifiers: Modifiers::empty(),
1664            kind: KeyEventKind::Press,
1665        });
1666
1667        dialog.handle_event(&backspace, &mut state, None);
1668        assert!(state.input_value.is_empty());
1669    }
1670
1671    #[test]
1672    fn edge_input_delete_clears_all() {
1673        let dialog = Dialog::prompt("Test", "Enter:");
1674        let mut state = DialogState::new();
1675        state.input_value = "hello world".to_string();
1676
1677        let delete = Event::Key(KeyEvent {
1678            code: KeyCode::Delete,
1679            modifiers: Modifiers::empty(),
1680            kind: KeyEventKind::Press,
1681        });
1682
1683        dialog.handle_event(&delete, &mut state, None);
1684        assert!(state.input_value.is_empty());
1685    }
1686
1687    #[test]
1688    fn edge_input_char_accumulation() {
1689        let dialog = Dialog::prompt("Test", "Enter:");
1690        let mut state = DialogState::new();
1691
1692        for c in ['h', 'e', 'l', 'l', 'o'] {
1693            let event = Event::Key(KeyEvent {
1694                code: KeyCode::Char(c),
1695                modifiers: Modifiers::empty(),
1696                kind: KeyEventKind::Press,
1697            });
1698            dialog.handle_event(&event, &mut state, None);
1699        }
1700        assert_eq!(state.input_value, "hello");
1701    }
1702
1703    #[test]
1704    fn edge_prompt_paste_appends_sanitized_single_line_text() {
1705        let dialog = Dialog::prompt("Test", "Enter:");
1706        let mut state = DialogState::new();
1707        state.input_value = "hello".to_string();
1708
1709        let paste = Event::Paste(ftui_core::event::PasteEvent::bracketed(
1710            " world\nnext\tline\u{0007}",
1711        ));
1712
1713        dialog.handle_event(&paste, &mut state, None);
1714        assert_eq!(state.input_value, "hello world next line");
1715    }
1716
1717    #[test]
1718    fn edge_prompt_paste_ignored_when_input_not_focused() {
1719        let dialog = Dialog::prompt("Test", "Enter:");
1720        let mut state = DialogState::new();
1721        state.input_focused = false;
1722        state.focused_button = Some(0);
1723
1724        let paste = Event::Paste(ftui_core::event::PasteEvent::bracketed("ignored"));
1725
1726        dialog.handle_event(&paste, &mut state, None);
1727        assert!(state.input_value.is_empty());
1728    }
1729
1730    #[test]
1731    fn edge_prompt_cancel_returns_cancel() {
1732        let dialog = Dialog::prompt("Test", "Enter:");
1733        let mut state = DialogState::new();
1734        state.input_value = "typed something".to_string();
1735        state.input_focused = false;
1736        state.focused_button = Some(1); // Cancel button
1737
1738        let enter = Event::Key(KeyEvent {
1739            code: KeyCode::Enter,
1740            modifiers: Modifiers::empty(),
1741            kind: KeyEventKind::Press,
1742        });
1743
1744        let result = dialog.handle_event(&enter, &mut state, None);
1745        assert_eq!(result, Some(DialogResult::Cancel));
1746        assert!(!state.is_open());
1747    }
1748
1749    #[test]
1750    fn edge_custom_button_activation() {
1751        let dialog = Dialog::custom("Test", "Msg")
1752            .custom_button("Save", "save")
1753            .custom_button("Delete", "delete")
1754            .build();
1755        let mut state = DialogState::new();
1756        state.input_focused = false;
1757        state.focused_button = Some(1); // "Delete" button
1758
1759        let enter = Event::Key(KeyEvent {
1760            code: KeyCode::Enter,
1761            modifiers: Modifiers::empty(),
1762            kind: KeyEventKind::Press,
1763        });
1764
1765        let result = dialog.handle_event(&enter, &mut state, None);
1766        assert_eq!(result, Some(DialogResult::Custom("delete".to_string())));
1767    }
1768
1769    #[test]
1770    fn edge_render_zero_size_area() {
1771        let dialog = Dialog::alert("T", "M");
1772        let mut state = DialogState::new();
1773        let mut pool = GraphemePool::new();
1774        let mut frame = Frame::new(80, 24, &mut pool);
1775        // Zero-width area
1776        dialog.render(Rect::new(0, 0, 0, 0), &mut frame, &mut state);
1777        // Zero-height area
1778        dialog.render(Rect::new(0, 0, 80, 0), &mut frame, &mut state);
1779        // Zero-width nonzero-height
1780        dialog.render(Rect::new(0, 0, 0, 24), &mut frame, &mut state);
1781    }
1782
1783    #[test]
1784    fn edge_render_closed_dialog_is_noop() {
1785        let dialog = Dialog::alert("Test", "Msg");
1786        let mut state = DialogState::new();
1787        state.close(DialogResult::Dismissed);
1788
1789        let mut pool = GraphemePool::new();
1790        let mut frame = Frame::new(80, 24, &mut pool);
1791
1792        // Rendering a closed dialog should not panic or alter frame
1793        dialog.render(Rect::new(0, 0, 80, 24), &mut frame, &mut state);
1794    }
1795
1796    #[test]
1797    fn edge_builder_hit_id() {
1798        let dialog = Dialog::custom("T", "M")
1799            .ok_button()
1800            .hit_id(HitId::new(42))
1801            .build();
1802        assert_eq!(dialog.hit_id, Some(HitId::new(42)));
1803    }
1804
1805    #[test]
1806    fn edge_builder_modal_config() {
1807        let config = ModalConfig::default().position(ModalPosition::TopCenter { margin: 5 });
1808        let dialog = Dialog::custom("T", "M")
1809            .ok_button()
1810            .modal_config(config)
1811            .build();
1812        assert_eq!(
1813            dialog.config.modal_config.position,
1814            ModalPosition::TopCenter { margin: 5 }
1815        );
1816    }
1817
1818    #[test]
1819    fn edge_builder_modal_config_syncs_hit_id() {
1820        let dialog = Dialog::custom("T", "M")
1821            .ok_button()
1822            .modal_config(ModalConfig::default().hit_id(HitId::new(42)))
1823            .build();
1824        assert_eq!(dialog.hit_id, Some(HitId::new(42)));
1825    }
1826
1827    #[test]
1828    fn edge_content_height_alert() {
1829        let dialog = Dialog::alert("Title", "Message");
1830        let h = dialog.content_height();
1831        // 2 (borders) + 1 (message) + 1 (spacing) + 1 (buttons) = 5
1832        assert_eq!(h, 5);
1833    }
1834
1835    #[test]
1836    fn edge_content_height_prompt() {
1837        let dialog = Dialog::prompt("Title", "Message");
1838        let h = dialog.content_height();
1839        // 2 (borders) + 1 (message) + 1 (spacing) + 1 (input) + 1 (input spacing) + 1 (buttons) = 7
1840        assert_eq!(h, 7);
1841    }
1842
1843    #[test]
1844    fn edge_content_height_empty_title_and_message() {
1845        let dialog = Dialog::alert("", "");
1846        let h = dialog.content_height();
1847        // 2 (borders) + 0 (no title) + 0 (no message) + 1 (spacing) + 1 (buttons) = 4
1848        assert_eq!(h, 4);
1849    }
1850
1851    #[test]
1852    fn edge_button_display_width_unicode() {
1853        let button = DialogButton::new("保存", "save");
1854        // "保存" is 4 display columns + 4 for brackets = 8
1855        assert_eq!(button.display_width(), 8);
1856    }
1857
1858    #[test]
1859    fn edge_dialog_result_equality() {
1860        assert_eq!(DialogResult::Ok, DialogResult::Ok);
1861        assert_eq!(DialogResult::Cancel, DialogResult::Cancel);
1862        assert_eq!(DialogResult::Dismissed, DialogResult::Dismissed);
1863        assert_eq!(
1864            DialogResult::Custom("a".into()),
1865            DialogResult::Custom("a".into())
1866        );
1867        assert_ne!(
1868            DialogResult::Custom("a".into()),
1869            DialogResult::Custom("b".into())
1870        );
1871        assert_eq!(
1872            DialogResult::Input("x".into()),
1873            DialogResult::Input("x".into())
1874        );
1875        assert_ne!(DialogResult::Ok, DialogResult::Cancel);
1876    }
1877
1878    #[test]
1879    fn edge_mouse_down_mismatched_hit_id() {
1880        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
1881        let mut state = DialogState::new();
1882
1883        let down = Event::Mouse(MouseEvent::new(
1884            MouseEventKind::Down(MouseButton::Left),
1885            0,
1886            0,
1887        ));
1888        // Hit with different ID should not register
1889        let hit = Some((HitId::new(99), HitRegion::Button, 0u64));
1890        dialog.handle_event(&down, &mut state, hit);
1891        assert_eq!(state.pressed_button, None);
1892        assert_eq!(state.focused_button, None);
1893    }
1894
1895    #[test]
1896    fn mouse_down_outside_cancels_existing_pressed_button() {
1897        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
1898        let mut state = DialogState::new();
1899
1900        let down = Event::Mouse(MouseEvent::new(
1901            MouseEventKind::Down(MouseButton::Left),
1902            0,
1903            0,
1904        ));
1905        let hit0 = Some((HitId::new(1), HitRegion::Button, 0u64));
1906        dialog.handle_event(&down, &mut state, hit0);
1907        assert_eq!(state.pressed_button, Some(0));
1908
1909        dialog.handle_event(&down, &mut state, None);
1910        assert_eq!(state.pressed_button, None);
1911
1912        let up = Event::Mouse(MouseEvent::new(MouseEventKind::Up(MouseButton::Left), 0, 0));
1913        let result = dialog.handle_event(&up, &mut state, hit0);
1914        assert_eq!(result, None);
1915        assert!(state.is_open());
1916    }
1917
1918    #[test]
1919    fn mouse_down_mismatched_hit_id_cancels_existing_pressed_button() {
1920        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
1921        let mut state = DialogState::new();
1922
1923        let down = Event::Mouse(MouseEvent::new(
1924            MouseEventKind::Down(MouseButton::Left),
1925            0,
1926            0,
1927        ));
1928        let hit0 = Some((HitId::new(1), HitRegion::Button, 0u64));
1929        dialog.handle_event(&down, &mut state, hit0);
1930        assert_eq!(state.pressed_button, Some(0));
1931
1932        let wrong_hit = Some((HitId::new(99), HitRegion::Button, 1u64));
1933        dialog.handle_event(&down, &mut state, wrong_hit);
1934        assert_eq!(state.pressed_button, None);
1935        assert_eq!(state.focused_button, Some(0));
1936
1937        let up = Event::Mouse(MouseEvent::new(MouseEventKind::Up(MouseButton::Left), 0, 0));
1938        let result = dialog.handle_event(&up, &mut state, hit0);
1939        assert_eq!(result, None);
1940        assert!(state.is_open());
1941    }
1942
1943    #[test]
1944    fn edge_mouse_down_out_of_bounds_index() {
1945        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
1946        let mut state = DialogState::new();
1947
1948        let down = Event::Mouse(MouseEvent::new(
1949            MouseEventKind::Down(MouseButton::Left),
1950            0,
1951            0,
1952        ));
1953        // Button index beyond button count
1954        let hit = Some((HitId::new(1), HitRegion::Button, 99u64));
1955        dialog.handle_event(&down, &mut state, hit);
1956        assert_eq!(state.pressed_button, None);
1957    }
1958
1959    #[test]
1960    fn edge_mouse_up_different_button_from_pressed() {
1961        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
1962        let mut state = DialogState::new();
1963
1964        // Press button 0
1965        let down = Event::Mouse(MouseEvent::new(
1966            MouseEventKind::Down(MouseButton::Left),
1967            0,
1968            0,
1969        ));
1970        let hit0 = Some((HitId::new(1), HitRegion::Button, 0u64));
1971        dialog.handle_event(&down, &mut state, hit0);
1972        assert_eq!(state.pressed_button, Some(0));
1973
1974        // Release on button 1 — should NOT activate
1975        let up = Event::Mouse(MouseEvent::new(MouseEventKind::Up(MouseButton::Left), 0, 0));
1976        let hit1 = Some((HitId::new(1), HitRegion::Button, 1u64));
1977        let result = dialog.handle_event(&up, &mut state, hit1);
1978        assert_eq!(result, None);
1979        assert!(state.is_open());
1980        // pressed_button cleared by take()
1981        assert_eq!(state.pressed_button, None);
1982    }
1983
1984    #[test]
1985    fn edge_non_prompt_clears_input_focused() {
1986        let dialog = Dialog::alert("Test", "Msg");
1987        let mut state = DialogState::new();
1988        // Manually set input_focused (e.g. leftover from state reuse)
1989        state.input_focused = true;
1990
1991        let tab = Event::Key(KeyEvent {
1992            code: KeyCode::Tab,
1993            modifiers: Modifiers::empty(),
1994            kind: KeyEventKind::Press,
1995        });
1996        dialog.handle_event(&tab, &mut state, None);
1997        // Non-prompt dialog should clear input_focused
1998        assert!(!state.input_focused);
1999    }
2000
2001    #[test]
2002    fn edge_key_release_ignored() {
2003        let dialog = Dialog::prompt("Test", "Enter:");
2004        let mut state = DialogState::new();
2005        state.input_value.clear();
2006
2007        // Key release event should be ignored by input handler
2008        let release = Event::Key(KeyEvent {
2009            code: KeyCode::Char('x'),
2010            modifiers: Modifiers::empty(),
2011            kind: KeyEventKind::Release,
2012        });
2013        dialog.handle_event(&release, &mut state, None);
2014        assert!(state.input_value.is_empty());
2015    }
2016
2017    #[test]
2018    fn edge_enter_no_focused_no_primary_does_nothing() {
2019        // Build a dialog with no primary button
2020        let dialog = Dialog::custom("Test", "Msg")
2021            .custom_button("A", "a")
2022            .custom_button("B", "b")
2023            .build();
2024        let mut state = DialogState::new();
2025        state.input_focused = false;
2026        state.focused_button = None;
2027
2028        let enter = Event::Key(KeyEvent {
2029            code: KeyCode::Enter,
2030            modifiers: Modifiers::empty(),
2031            kind: KeyEventKind::Press,
2032        });
2033        // No focused button and no primary → activate_button returns None
2034        let result = dialog.handle_event(&enter, &mut state, None);
2035        assert_eq!(result, None);
2036        assert!(state.is_open());
2037    }
2038
2039    #[test]
2040    fn edge_dialog_style_setters() {
2041        let style = Style::new().bold();
2042        let dialog = Dialog::alert("T", "M")
2043            .button_style(style)
2044            .primary_button_style(style)
2045            .focused_button_style(style);
2046        assert_eq!(dialog.config.button_style, style);
2047        assert_eq!(dialog.config.primary_button_style, style);
2048        assert_eq!(dialog.config.focused_button_style, style);
2049    }
2050
2051    #[test]
2052    fn edge_dialog_modal_config_setter() {
2053        let mc = ModalConfig::default().position(ModalPosition::Custom { x: 10, y: 20 });
2054        let dialog = Dialog::alert("T", "M").modal_config(mc);
2055        assert_eq!(
2056            dialog.config.modal_config.position,
2057            ModalPosition::Custom { x: 10, y: 20 }
2058        );
2059    }
2060
2061    #[test]
2062    fn edge_dialog_modal_config_setter_syncs_hit_id() {
2063        let dialog =
2064            Dialog::alert("T", "M").modal_config(ModalConfig::default().hit_id(HitId::new(9)));
2065        assert_eq!(dialog.hit_id, Some(HitId::new(9)));
2066    }
2067
2068    #[test]
2069    fn edge_dialog_clone_debug() {
2070        let dialog = Dialog::alert("T", "M");
2071        let cloned = dialog.clone();
2072        assert_eq!(cloned.title, dialog.title);
2073        assert_eq!(cloned.message, dialog.message);
2074        let _ = format!("{:?}", dialog);
2075    }
2076
2077    #[test]
2078    fn edge_dialog_builder_clone_debug() {
2079        let builder = Dialog::custom("T", "M").ok_button();
2080        let cloned = builder.clone();
2081        assert_eq!(cloned.title, builder.title);
2082        let _ = format!("{:?}", builder);
2083    }
2084
2085    #[test]
2086    fn edge_dialog_config_clone_debug() {
2087        let config = DialogConfig::default();
2088        let cloned = config.clone();
2089        assert_eq!(cloned.kind, config.kind);
2090        let _ = format!("{:?}", config);
2091    }
2092
2093    #[test]
2094    fn edge_dialog_state_clone_debug() {
2095        let mut state = DialogState::new();
2096        state.input_value = "test".to_string();
2097        state.focused_button = Some(1);
2098        let cloned = state.clone();
2099        assert_eq!(cloned.input_value, "test");
2100        assert_eq!(cloned.focused_button, Some(1));
2101        assert_eq!(cloned.open, state.open);
2102        let _ = format!("{:?}", state);
2103    }
2104
2105    #[test]
2106    fn edge_dialog_button_clone_debug() {
2107        let button = DialogButton::new("Save", "save").primary();
2108        let cloned = button.clone();
2109        assert_eq!(cloned.label, "Save");
2110        assert_eq!(cloned.id, "save");
2111        assert!(cloned.primary);
2112        let _ = format!("{:?}", button);
2113    }
2114
2115    #[test]
2116    fn edge_dialog_result_clone_debug() {
2117        let results = [
2118            DialogResult::Ok,
2119            DialogResult::Cancel,
2120            DialogResult::Dismissed,
2121            DialogResult::Custom("x".into()),
2122            DialogResult::Input("y".into()),
2123        ];
2124        for r in &results {
2125            let cloned = r.clone();
2126            assert_eq!(&cloned, r);
2127            let _ = format!("{:?}", r);
2128        }
2129    }
2130
2131    #[test]
2132    fn edge_dialog_kind_clone_debug_eq() {
2133        let kinds = [
2134            DialogKind::Alert,
2135            DialogKind::Confirm,
2136            DialogKind::Prompt,
2137            DialogKind::Custom,
2138        ];
2139        for k in &kinds {
2140            let cloned = *k;
2141            assert_eq!(cloned, *k);
2142            let _ = format!("{:?}", k);
2143        }
2144        assert_ne!(DialogKind::Alert, DialogKind::Confirm);
2145    }
2146}