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::frame::{Frame, HitData, HitId, HitRegion};
28use ftui_style::{Style, StyleFlags};
29use ftui_text::display_width;
30
31/// Hit region for dialog buttons.
32pub const DIALOG_HIT_BUTTON: HitRegion = HitRegion::Button;
33
34/// Result from a dialog interaction.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum DialogResult {
37    /// Dialog was dismissed without action.
38    Dismissed,
39    /// OK / primary button pressed.
40    Ok,
41    /// Cancel / secondary button pressed.
42    Cancel,
43    /// Custom button pressed with its ID.
44    Custom(String),
45    /// Prompt dialog submitted with input value.
46    Input(String),
47}
48
49/// A button in a dialog.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct DialogButton {
52    /// Display label.
53    pub label: String,
54    /// Unique identifier.
55    pub id: String,
56    /// Whether this is the primary/default button.
57    pub primary: bool,
58}
59
60impl DialogButton {
61    /// Create a new dialog button.
62    pub fn new(label: impl Into<String>, id: impl Into<String>) -> Self {
63        Self {
64            label: label.into(),
65            id: id.into(),
66            primary: false,
67        }
68    }
69
70    /// Mark as primary button.
71    #[must_use]
72    pub fn primary(mut self) -> Self {
73        self.primary = true;
74        self
75    }
76
77    /// Display width including brackets.
78    pub fn display_width(&self) -> usize {
79        // [ label ] = display_width(label) + 4
80        display_width(self.label.as_str()) + 4
81    }
82}
83
84/// Dialog type variants.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum DialogKind {
87    /// Alert: single OK button.
88    Alert,
89    /// Confirm: OK + Cancel buttons.
90    Confirm,
91    /// Prompt: input field + OK + Cancel.
92    Prompt,
93    /// Custom dialog.
94    Custom,
95}
96
97/// Dialog state for handling input and button focus.
98#[derive(Debug, Clone, Default)]
99pub struct DialogState {
100    /// Currently focused button index.
101    pub focused_button: Option<usize>,
102    /// Button index currently pressed by mouse (Down without matching Up yet).
103    pressed_button: Option<usize>,
104    /// Input field value (for Prompt dialogs).
105    pub input_value: String,
106    /// Whether the input field is focused.
107    pub input_focused: bool,
108    /// Whether the dialog is open.
109    pub open: bool,
110    /// Result after interaction.
111    pub result: Option<DialogResult>,
112}
113
114impl DialogState {
115    /// Create a new open dialog state.
116    pub fn new() -> Self {
117        Self {
118            open: true,
119            input_focused: true, // Start with input focused for prompts
120            ..Default::default()
121        }
122    }
123
124    /// Check if dialog is open.
125    #[inline]
126    pub fn is_open(&self) -> bool {
127        self.open
128    }
129
130    /// Close the dialog with a result.
131    pub fn close(&mut self, result: DialogResult) {
132        self.open = false;
133        self.pressed_button = None;
134        self.result = Some(result);
135    }
136
137    /// Reset the dialog state to open.
138    pub fn reset(&mut self) {
139        self.open = true;
140        self.result = None;
141        self.input_value.clear();
142        self.focused_button = None;
143        self.pressed_button = None;
144        self.input_focused = true;
145    }
146
147    /// Get the result if closed.
148    pub fn take_result(&mut self) -> Option<DialogResult> {
149        self.result.take()
150    }
151}
152
153/// Dialog configuration.
154#[derive(Debug, Clone)]
155pub struct DialogConfig {
156    /// Modal configuration.
157    pub modal_config: ModalConfig,
158    /// Dialog kind.
159    pub kind: DialogKind,
160    /// Button style.
161    pub button_style: Style,
162    /// Primary button style.
163    pub primary_button_style: Style,
164    /// Focused button style.
165    pub focused_button_style: Style,
166    /// Title style.
167    pub title_style: Style,
168    /// Message style.
169    pub message_style: Style,
170    /// Input style (for Prompt).
171    pub input_style: Style,
172}
173
174impl Default for DialogConfig {
175    fn default() -> Self {
176        Self {
177            modal_config: ModalConfig::default()
178                .position(ModalPosition::Center)
179                .size(ModalSizeConstraints::new().min_width(30).max_width(60)),
180            kind: DialogKind::Alert,
181            button_style: Style::new(),
182            primary_button_style: Style::new().bold(),
183            focused_button_style: Style::new().reverse(),
184            title_style: Style::new().bold(),
185            message_style: Style::new(),
186            input_style: Style::new(),
187        }
188    }
189}
190
191/// A dialog widget built on Modal.
192///
193/// Invariants:
194/// - At least one button is always present.
195/// - Button focus wraps around (modular arithmetic).
196/// - For Prompt dialogs, Tab cycles: input -> buttons -> input.
197///
198/// Failure modes:
199/// - If area is too small, content may be truncated but dialog never panics.
200/// - Empty title/message is allowed (renders nothing for that row).
201#[derive(Debug, Clone)]
202pub struct Dialog {
203    /// Dialog title.
204    title: String,
205    /// Dialog message.
206    message: String,
207    /// Buttons.
208    buttons: Vec<DialogButton>,
209    /// Configuration.
210    config: DialogConfig,
211    /// Hit ID for mouse interaction.
212    hit_id: Option<HitId>,
213}
214
215impl Dialog {
216    /// Create an alert dialog (message + OK).
217    pub fn alert(title: impl Into<String>, message: impl Into<String>) -> Self {
218        Self {
219            title: title.into(),
220            message: message.into(),
221            buttons: vec![DialogButton::new("OK", "ok").primary()],
222            config: DialogConfig {
223                kind: DialogKind::Alert,
224                ..Default::default()
225            },
226            hit_id: None,
227        }
228    }
229
230    /// Create a confirm dialog (message + OK/Cancel).
231    pub fn confirm(title: impl Into<String>, message: impl Into<String>) -> Self {
232        Self {
233            title: title.into(),
234            message: message.into(),
235            buttons: vec![
236                DialogButton::new("OK", "ok").primary(),
237                DialogButton::new("Cancel", "cancel"),
238            ],
239            config: DialogConfig {
240                kind: DialogKind::Confirm,
241                ..Default::default()
242            },
243            hit_id: None,
244        }
245    }
246
247    /// Create a prompt dialog (message + input + OK/Cancel).
248    pub fn prompt(title: impl Into<String>, message: impl Into<String>) -> Self {
249        Self {
250            title: title.into(),
251            message: message.into(),
252            buttons: vec![
253                DialogButton::new("OK", "ok").primary(),
254                DialogButton::new("Cancel", "cancel"),
255            ],
256            config: DialogConfig {
257                kind: DialogKind::Prompt,
258                ..Default::default()
259            },
260            hit_id: None,
261        }
262    }
263
264    /// Create a custom dialog with a builder.
265    pub fn custom(title: impl Into<String>, message: impl Into<String>) -> DialogBuilder {
266        DialogBuilder {
267            title: title.into(),
268            message: message.into(),
269            buttons: Vec::new(),
270            config: DialogConfig {
271                kind: DialogKind::Custom,
272                ..Default::default()
273            },
274            hit_id: None,
275        }
276    }
277
278    /// Set the hit ID for mouse interaction.
279    #[must_use]
280    pub fn hit_id(mut self, id: HitId) -> Self {
281        self.hit_id = Some(id);
282        self.config.modal_config = self.config.modal_config.hit_id(id);
283        self
284    }
285
286    /// Set the modal configuration.
287    #[must_use]
288    pub fn modal_config(mut self, config: ModalConfig) -> Self {
289        self.config.modal_config = config;
290        self
291    }
292
293    /// Set button style.
294    #[must_use]
295    pub fn button_style(mut self, style: Style) -> Self {
296        self.config.button_style = style;
297        self
298    }
299
300    /// Set primary button style.
301    #[must_use]
302    pub fn primary_button_style(mut self, style: Style) -> Self {
303        self.config.primary_button_style = style;
304        self
305    }
306
307    /// Set focused button style.
308    #[must_use]
309    pub fn focused_button_style(mut self, style: Style) -> Self {
310        self.config.focused_button_style = style;
311        self
312    }
313
314    /// Handle an event and potentially update state.
315    pub fn handle_event(
316        &self,
317        event: &Event,
318        state: &mut DialogState,
319        hit: Option<(HitId, HitRegion, HitData)>,
320    ) -> Option<DialogResult> {
321        if !state.open {
322            return None;
323        }
324
325        if self.config.kind != DialogKind::Prompt && state.input_focused {
326            state.input_focused = false;
327        }
328
329        match event {
330            // Escape closes with Dismissed
331            Event::Key(KeyEvent {
332                code: KeyCode::Escape,
333                kind: KeyEventKind::Press,
334                ..
335            }) if self.config.modal_config.close_on_escape => {
336                state.close(DialogResult::Dismissed);
337                return Some(DialogResult::Dismissed);
338            }
339
340            // Tab cycles focus
341            Event::Key(KeyEvent {
342                code: KeyCode::Tab,
343                kind: KeyEventKind::Press,
344                modifiers,
345                ..
346            }) => {
347                let shift = modifiers.contains(Modifiers::SHIFT);
348                self.cycle_focus(state, shift);
349            }
350
351            // Enter activates focused button
352            Event::Key(KeyEvent {
353                code: KeyCode::Enter,
354                kind: KeyEventKind::Press,
355                ..
356            }) => {
357                return self.activate_button(state);
358            }
359
360            // Arrow keys navigate buttons
361            Event::Key(KeyEvent {
362                code: KeyCode::Left | KeyCode::Right,
363                kind: KeyEventKind::Press,
364                ..
365            }) if !state.input_focused => {
366                let forward = matches!(
367                    event,
368                    Event::Key(KeyEvent {
369                        code: KeyCode::Right,
370                        ..
371                    })
372                );
373                self.navigate_buttons(state, forward);
374            }
375
376            // Mouse down on button (press only; activate on mouse up).
377            Event::Mouse(MouseEvent {
378                kind: MouseEventKind::Down(MouseButton::Left),
379                ..
380            }) => {
381                if let (Some((id, region, data)), Some(expected)) = (hit, self.hit_id)
382                    && id == expected
383                    && region == DIALOG_HIT_BUTTON
384                    && let Ok(idx) = usize::try_from(data)
385                    && idx < self.buttons.len()
386                {
387                    state.focused_button = Some(idx);
388                    state.pressed_button = Some(idx);
389                }
390            }
391
392            // Mouse up on button activates if it matches the pressed target.
393            Event::Mouse(MouseEvent {
394                kind: MouseEventKind::Up(MouseButton::Left),
395                ..
396            }) => {
397                let pressed = state.pressed_button.take();
398                if let (Some(pressed), Some((id, region, data)), Some(expected)) =
399                    (pressed, hit, self.hit_id)
400                    && id == expected
401                    && region == DIALOG_HIT_BUTTON
402                    && let Ok(idx) = usize::try_from(data)
403                    && idx == pressed
404                {
405                    state.focused_button = Some(idx);
406                    return self.activate_button(state);
407                }
408            }
409
410            // For prompt dialogs, handle text input
411            Event::Key(key_event)
412                if self.config.kind == DialogKind::Prompt && state.input_focused =>
413            {
414                self.handle_input_key(state, key_event);
415            }
416
417            _ => {}
418        }
419
420        None
421    }
422
423    fn cycle_focus(&self, state: &mut DialogState, reverse: bool) {
424        let has_input = self.config.kind == DialogKind::Prompt;
425        let button_count = self.buttons.len();
426
427        if has_input {
428            // Cycle: input -> button 0 -> button 1 -> ... -> input
429            if state.input_focused {
430                state.input_focused = false;
431                state.focused_button = if reverse {
432                    Some(button_count.saturating_sub(1))
433                } else {
434                    Some(0)
435                };
436            } else if let Some(idx) = state.focused_button {
437                if reverse {
438                    if idx == 0 {
439                        state.input_focused = true;
440                        state.focused_button = None;
441                    } else {
442                        state.focused_button = Some(idx - 1);
443                    }
444                } else if idx + 1 >= button_count {
445                    state.input_focused = true;
446                    state.focused_button = None;
447                } else {
448                    state.focused_button = Some(idx + 1);
449                }
450            }
451        } else {
452            // Just cycle buttons
453            let current = state.focused_button.unwrap_or(0);
454            state.focused_button = if reverse {
455                Some(if current == 0 {
456                    button_count - 1
457                } else {
458                    current - 1
459                })
460            } else {
461                Some((current + 1) % button_count)
462            };
463        }
464    }
465
466    fn navigate_buttons(&self, state: &mut DialogState, forward: bool) {
467        let count = self.buttons.len();
468        if count == 0 {
469            return;
470        }
471        let current = state.focused_button.unwrap_or(0);
472        state.focused_button = if forward {
473            Some((current + 1) % count)
474        } else {
475            Some(if current == 0 { count - 1 } else { current - 1 })
476        };
477    }
478
479    fn activate_button(&self, state: &mut DialogState) -> Option<DialogResult> {
480        let idx = state.focused_button.or_else(|| {
481            // Default to primary button
482            self.buttons.iter().position(|b| b.primary)
483        })?;
484
485        let button = self.buttons.get(idx)?;
486        let result = match button.id.as_str() {
487            "ok" => {
488                if self.config.kind == DialogKind::Prompt {
489                    DialogResult::Input(state.input_value.clone())
490                } else {
491                    DialogResult::Ok
492                }
493            }
494            "cancel" => DialogResult::Cancel,
495            id => DialogResult::Custom(id.to_string()),
496        };
497
498        state.close(result.clone());
499        Some(result)
500    }
501
502    fn handle_input_key(&self, state: &mut DialogState, key: &KeyEvent) {
503        if key.kind != KeyEventKind::Press {
504            return;
505        }
506
507        match key.code {
508            KeyCode::Char(c) => {
509                state.input_value.push(c);
510            }
511            KeyCode::Backspace => {
512                state.input_value.pop();
513            }
514            KeyCode::Delete => {
515                state.input_value.clear();
516            }
517            _ => {}
518        }
519    }
520
521    /// Calculate content height.
522    fn content_height(&self) -> u16 {
523        let mut height: u16 = 2; // Top and bottom border
524
525        // Title row
526        if !self.title.is_empty() {
527            height += 1;
528        }
529
530        // Message row(s) - simplified: 1 row
531        if !self.message.is_empty() {
532            height += 1;
533        }
534
535        // Spacing
536        height += 1;
537
538        // Input row (for Prompt)
539        if self.config.kind == DialogKind::Prompt {
540            height += 1;
541            height += 1; // Spacing
542        }
543
544        // Button row
545        height += 1;
546
547        height
548    }
549
550    /// Render the dialog content.
551    fn render_content(&self, area: Rect, frame: &mut Frame, state: &DialogState) {
552        if area.is_empty() {
553            return;
554        }
555
556        // Draw border
557        let block = Block::default()
558            .borders(Borders::ALL)
559            .title(&self.title)
560            .title_alignment(Alignment::Center);
561        block.render(area, frame);
562
563        let inner = block.inner(area);
564        if inner.is_empty() {
565            return;
566        }
567
568        let mut y = inner.y;
569
570        // Message
571        if !self.message.is_empty() && y < inner.bottom() {
572            self.draw_centered_text(
573                frame,
574                inner.x,
575                y,
576                inner.width,
577                &self.message,
578                self.config.message_style,
579            );
580            y += 1;
581        }
582
583        // Spacing
584        y += 1;
585
586        // Input field (for Prompt)
587        if self.config.kind == DialogKind::Prompt && y < inner.bottom() {
588            self.render_input(frame, inner.x, y, inner.width, state);
589            y += 2; // Input + spacing
590        }
591
592        // Buttons
593        if y < inner.bottom() {
594            self.render_buttons(frame, inner.x, y, inner.width, state);
595        }
596    }
597
598    fn draw_centered_text(
599        &self,
600        frame: &mut Frame,
601        x: u16,
602        y: u16,
603        width: u16,
604        text: &str,
605        style: Style,
606    ) {
607        let text_width = display_width(text).min(width as usize);
608        let offset = (width as usize - text_width) / 2;
609        let start_x = x.saturating_add(offset as u16);
610        draw_text_span(frame, start_x, y, text, style, x.saturating_add(width));
611    }
612
613    fn render_input(&self, frame: &mut Frame, x: u16, y: u16, width: u16, state: &DialogState) {
614        // Draw input background
615        let input_area = Rect::new(x + 1, y, width.saturating_sub(2), 1);
616        let input_style = self.config.input_style;
617        set_style_area(&mut frame.buffer, input_area, input_style);
618
619        // Draw input value or placeholder
620        let display_text = if state.input_value.is_empty() {
621            " "
622        } else {
623            &state.input_value
624        };
625
626        draw_text_span(
627            frame,
628            input_area.x,
629            y,
630            display_text,
631            input_style,
632            input_area.right(),
633        );
634
635        // Draw cursor if focused
636        if state.input_focused {
637            let input_width = display_width(state.input_value.as_str());
638            let cursor_x = input_area.x + input_width.min(input_area.width as usize) as u16;
639            if cursor_x < input_area.right() {
640                frame.cursor_position = Some((cursor_x, y));
641                frame.cursor_visible = true;
642            }
643        }
644    }
645
646    fn render_buttons(&self, frame: &mut Frame, x: u16, y: u16, width: u16, state: &DialogState) {
647        if self.buttons.is_empty() {
648            return;
649        }
650
651        // Calculate total button width
652        let total_width: usize = self
653            .buttons
654            .iter()
655            .map(|b| b.display_width())
656            .sum::<usize>()
657            + self.buttons.len().saturating_sub(1) * 2; // Spacing between buttons
658
659        // Center the buttons
660        let start_x = x + (width as usize - total_width.min(width as usize)) as u16 / 2;
661        let mut bx = start_x;
662
663        for (i, button) in self.buttons.iter().enumerate() {
664            let is_focused = state.focused_button == Some(i);
665
666            // Select style
667            let mut style = if is_focused {
668                self.config.focused_button_style
669            } else if button.primary {
670                self.config.primary_button_style
671            } else {
672                self.config.button_style
673            };
674            if is_focused {
675                let has_reverse = style
676                    .attrs
677                    .is_some_and(|attrs| attrs.contains(StyleFlags::REVERSE));
678                if !has_reverse {
679                    style = style.reverse();
680                }
681            }
682
683            // Draw button: [ label ]
684            let btn_text = format!("[ {} ]", button.label);
685            let btn_width = display_width(btn_text.as_str());
686            draw_text_span(frame, bx, y, &btn_text, style, x.saturating_add(width));
687
688            // Register hit region for button
689            if let Some(hit_id) = self.hit_id {
690                let max_btn_width = width.saturating_sub(bx.saturating_sub(x));
691                let btn_area_width = btn_width.min(max_btn_width as usize) as u16;
692                if btn_area_width > 0 {
693                    let btn_area = Rect::new(bx, y, btn_area_width, 1);
694                    frame.register_hit(btn_area, hit_id, DIALOG_HIT_BUTTON, i as u64);
695                }
696            }
697
698            bx = bx.saturating_add(btn_width as u16 + 2); // Button + spacing
699        }
700    }
701}
702
703impl StatefulWidget for Dialog {
704    type State = DialogState;
705
706    fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
707        if !state.open || area.is_empty() {
708            return;
709        }
710
711        // Calculate content area
712        let content_height = self.content_height();
713        let config = self.config.modal_config.clone().size(
714            ModalSizeConstraints::new()
715                .min_width(30)
716                .max_width(60)
717                .min_height(content_height)
718                .max_height(content_height + 4),
719        );
720
721        // Create a wrapper widget for the dialog content
722        let content = DialogContent {
723            dialog: self,
724            state,
725        };
726
727        // Render via Modal
728        let modal = Modal::new(content).config(config);
729        modal.render(area, frame);
730    }
731}
732
733/// Internal wrapper for rendering dialog content.
734struct DialogContent<'a> {
735    dialog: &'a Dialog,
736    state: &'a DialogState,
737}
738
739impl Widget for DialogContent<'_> {
740    fn render(&self, area: Rect, frame: &mut Frame) {
741        self.dialog.render_content(area, frame, self.state);
742    }
743}
744
745/// Builder for custom dialogs.
746#[derive(Debug, Clone)]
747#[must_use]
748pub struct DialogBuilder {
749    title: String,
750    message: String,
751    buttons: Vec<DialogButton>,
752    config: DialogConfig,
753    hit_id: Option<HitId>,
754}
755
756impl DialogBuilder {
757    /// Add a button.
758    pub fn button(mut self, button: DialogButton) -> Self {
759        self.buttons.push(button);
760        self
761    }
762
763    /// Add an OK button.
764    pub fn ok_button(self) -> Self {
765        self.button(DialogButton::new("OK", "ok").primary())
766    }
767
768    /// Add a Cancel button.
769    pub fn cancel_button(self) -> Self {
770        self.button(DialogButton::new("Cancel", "cancel"))
771    }
772
773    /// Add a custom button.
774    pub fn custom_button(self, label: impl Into<String>, id: impl Into<String>) -> Self {
775        self.button(DialogButton::new(label, id))
776    }
777
778    /// Set modal configuration.
779    pub fn modal_config(mut self, config: ModalConfig) -> Self {
780        self.config.modal_config = config;
781        self
782    }
783
784    /// Set hit ID for mouse interaction.
785    pub fn hit_id(mut self, id: HitId) -> Self {
786        self.hit_id = Some(id);
787        self
788    }
789
790    /// Build the dialog.
791    pub fn build(self) -> Dialog {
792        let mut buttons = self.buttons;
793        if buttons.is_empty() {
794            buttons.push(DialogButton::new("OK", "ok").primary());
795        }
796
797        Dialog {
798            title: self.title,
799            message: self.message,
800            buttons,
801            config: self.config,
802            hit_id: self.hit_id,
803        }
804    }
805}
806
807#[cfg(test)]
808mod tests {
809    use super::*;
810    use ftui_render::grapheme_pool::GraphemePool;
811
812    #[test]
813    fn alert_dialog_single_button() {
814        let dialog = Dialog::alert("Title", "Message");
815        assert_eq!(dialog.buttons.len(), 1);
816        assert_eq!(dialog.buttons[0].label, "OK");
817        assert!(dialog.buttons[0].primary);
818    }
819
820    #[test]
821    fn confirm_dialog_two_buttons() {
822        let dialog = Dialog::confirm("Title", "Message");
823        assert_eq!(dialog.buttons.len(), 2);
824        assert_eq!(dialog.buttons[0].label, "OK");
825        assert_eq!(dialog.buttons[1].label, "Cancel");
826    }
827
828    #[test]
829    fn prompt_dialog_has_input() {
830        let dialog = Dialog::prompt("Title", "Message");
831        assert_eq!(dialog.config.kind, DialogKind::Prompt);
832        assert_eq!(dialog.buttons.len(), 2);
833    }
834
835    #[test]
836    fn custom_dialog_builder() {
837        let dialog = Dialog::custom("Custom", "Message")
838            .ok_button()
839            .cancel_button()
840            .custom_button("Help", "help")
841            .build();
842        assert_eq!(dialog.buttons.len(), 3);
843    }
844
845    #[test]
846    fn dialog_state_starts_open() {
847        let state = DialogState::new();
848        assert!(state.is_open());
849        assert!(state.result.is_none());
850    }
851
852    #[test]
853    fn dialog_state_close_sets_result() {
854        let mut state = DialogState::new();
855        state.close(DialogResult::Ok);
856        assert!(!state.is_open());
857        assert_eq!(state.result, Some(DialogResult::Ok));
858    }
859
860    #[test]
861    fn dialog_escape_closes() {
862        let dialog = Dialog::alert("Test", "Msg");
863        let mut state = DialogState::new();
864        let event = Event::Key(KeyEvent {
865            code: KeyCode::Escape,
866            modifiers: Modifiers::empty(),
867            kind: KeyEventKind::Press,
868        });
869        let result = dialog.handle_event(&event, &mut state, None);
870        assert_eq!(result, Some(DialogResult::Dismissed));
871        assert!(!state.is_open());
872    }
873
874    #[test]
875    fn dialog_enter_activates_primary() {
876        let dialog = Dialog::alert("Test", "Msg");
877        let mut state = DialogState::new();
878        state.input_focused = false; // Not on input
879        let event = Event::Key(KeyEvent {
880            code: KeyCode::Enter,
881            modifiers: Modifiers::empty(),
882            kind: KeyEventKind::Press,
883        });
884        let result = dialog.handle_event(&event, &mut state, None);
885        assert_eq!(result, Some(DialogResult::Ok));
886    }
887
888    #[test]
889    fn dialog_mouse_up_activates_pressed_button() {
890        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
891        let mut state = DialogState::new();
892
893        let down = Event::Mouse(MouseEvent::new(
894            MouseEventKind::Down(MouseButton::Left),
895            0,
896            0,
897        ));
898        let hit = Some((HitId::new(1), HitRegion::Button, 0u64));
899        let result = dialog.handle_event(&down, &mut state, hit);
900        assert_eq!(result, None);
901        assert_eq!(state.focused_button, Some(0));
902        assert_eq!(state.pressed_button, Some(0));
903
904        let up = Event::Mouse(MouseEvent::new(MouseEventKind::Up(MouseButton::Left), 0, 0));
905        let result = dialog.handle_event(&up, &mut state, hit);
906        assert_eq!(result, Some(DialogResult::Ok));
907        assert!(!state.is_open());
908    }
909
910    #[test]
911    fn dialog_mouse_up_outside_does_not_activate() {
912        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
913        let mut state = DialogState::new();
914
915        let down = Event::Mouse(MouseEvent::new(
916            MouseEventKind::Down(MouseButton::Left),
917            0,
918            0,
919        ));
920        let hit = Some((HitId::new(1), HitRegion::Button, 0u64));
921        let result = dialog.handle_event(&down, &mut state, hit);
922        assert_eq!(result, None);
923        assert_eq!(state.pressed_button, Some(0));
924
925        let up = Event::Mouse(MouseEvent::new(MouseEventKind::Up(MouseButton::Left), 0, 0));
926        let result = dialog.handle_event(&up, &mut state, None);
927        assert_eq!(result, None);
928        assert!(state.is_open());
929        assert_eq!(state.pressed_button, None);
930    }
931
932    #[test]
933    fn dialog_tab_cycles_focus() {
934        let dialog = Dialog::confirm("Test", "Msg");
935        let mut state = DialogState::new();
936        state.input_focused = false;
937        state.focused_button = Some(0);
938
939        let tab = Event::Key(KeyEvent {
940            code: KeyCode::Tab,
941            modifiers: Modifiers::empty(),
942            kind: KeyEventKind::Press,
943        });
944
945        dialog.handle_event(&tab, &mut state, None);
946        assert_eq!(state.focused_button, Some(1));
947
948        dialog.handle_event(&tab, &mut state, None);
949        assert_eq!(state.focused_button, Some(0)); // Wraps around
950    }
951
952    #[test]
953    fn prompt_enter_returns_input() {
954        let dialog = Dialog::prompt("Test", "Enter:");
955        let mut state = DialogState::new();
956        state.input_value = "hello".to_string();
957        state.input_focused = false;
958        state.focused_button = Some(0); // OK button
959
960        let enter = Event::Key(KeyEvent {
961            code: KeyCode::Enter,
962            modifiers: Modifiers::empty(),
963            kind: KeyEventKind::Press,
964        });
965
966        let result = dialog.handle_event(&enter, &mut state, None);
967        assert_eq!(result, Some(DialogResult::Input("hello".to_string())));
968    }
969
970    #[test]
971    fn button_display_width() {
972        let button = DialogButton::new("OK", "ok");
973        assert_eq!(button.display_width(), 6); // [ OK ]
974    }
975
976    #[test]
977    fn render_alert_does_not_panic() {
978        let dialog = Dialog::alert("Alert", "This is an alert message.");
979        let mut state = DialogState::new();
980        let mut pool = GraphemePool::new();
981        let mut frame = Frame::new(80, 24, &mut pool);
982        dialog.render(Rect::new(0, 0, 80, 24), &mut frame, &mut state);
983    }
984
985    #[test]
986    fn render_confirm_does_not_panic() {
987        let dialog = Dialog::confirm("Confirm", "Are you sure?");
988        let mut state = DialogState::new();
989        let mut pool = GraphemePool::new();
990        let mut frame = Frame::new(80, 24, &mut pool);
991        dialog.render(Rect::new(0, 0, 80, 24), &mut frame, &mut state);
992    }
993
994    #[test]
995    fn render_prompt_does_not_panic() {
996        let dialog = Dialog::prompt("Prompt", "Enter your name:");
997        let mut state = DialogState::new();
998        state.input_value = "Test User".to_string();
999        let mut pool = GraphemePool::new();
1000        let mut frame = Frame::new(80, 24, &mut pool);
1001        dialog.render(Rect::new(0, 0, 80, 24), &mut frame, &mut state);
1002    }
1003
1004    #[test]
1005    fn render_tiny_area_does_not_panic() {
1006        let dialog = Dialog::alert("T", "M");
1007        let mut state = DialogState::new();
1008        let mut pool = GraphemePool::new();
1009        let mut frame = Frame::new(10, 5, &mut pool);
1010        dialog.render(Rect::new(0, 0, 10, 5), &mut frame, &mut state);
1011    }
1012
1013    #[test]
1014    fn custom_dialog_empty_buttons_gets_default() {
1015        let dialog = Dialog::custom("Custom", "No buttons").build();
1016        assert_eq!(dialog.buttons.len(), 1);
1017        assert_eq!(dialog.buttons[0].label, "OK");
1018    }
1019
1020    #[test]
1021    fn render_unicode_message_does_not_panic() {
1022        // CJK characters are 2 columns wide each
1023        let dialog = Dialog::alert("你好", "这是一条消息 🎉");
1024        let mut state = DialogState::new();
1025        let mut pool = GraphemePool::new();
1026        let mut frame = Frame::new(80, 24, &mut pool);
1027        dialog.render(Rect::new(0, 0, 80, 24), &mut frame, &mut state);
1028    }
1029
1030    #[test]
1031    fn prompt_with_unicode_input_renders_correctly() {
1032        let dialog = Dialog::prompt("入力", "名前を入力:");
1033        let mut state = DialogState::new();
1034        state.input_value = "田中太郎".to_string(); // CJK input
1035        let mut pool = GraphemePool::new();
1036        let mut frame = Frame::new(80, 24, &mut pool);
1037        dialog.render(Rect::new(0, 0, 80, 24), &mut frame, &mut state);
1038    }
1039
1040    // ---- Edge-case tests (bd-1is2p) ----
1041
1042    #[test]
1043    fn edge_state_default_vs_new() {
1044        let default = DialogState::default();
1045        let new = DialogState::new();
1046        // Default: open=false, input_focused=false
1047        assert!(!default.open);
1048        assert!(!default.input_focused);
1049        // New: open=true, input_focused=true
1050        assert!(new.open);
1051        assert!(new.input_focused);
1052    }
1053
1054    #[test]
1055    fn edge_state_reset_then_reuse() {
1056        let mut state = DialogState::new();
1057        state.input_value = "typed".to_string();
1058        state.focused_button = Some(1);
1059        state.close(DialogResult::Cancel);
1060
1061        assert!(!state.is_open());
1062        assert!(state.result.is_some());
1063
1064        state.reset();
1065        assert!(state.is_open());
1066        assert!(state.result.is_none());
1067        assert!(state.input_value.is_empty());
1068        assert_eq!(state.focused_button, None);
1069        assert!(state.input_focused);
1070    }
1071
1072    #[test]
1073    fn edge_take_result_when_none() {
1074        let mut state = DialogState::new();
1075        assert_eq!(state.take_result(), None);
1076        // Calling again still returns None
1077        assert_eq!(state.take_result(), None);
1078    }
1079
1080    #[test]
1081    fn edge_take_result_consumes() {
1082        let mut state = DialogState::new();
1083        state.close(DialogResult::Ok);
1084        assert_eq!(state.take_result(), Some(DialogResult::Ok));
1085        // Second call returns None — consumed
1086        assert_eq!(state.take_result(), None);
1087    }
1088
1089    #[test]
1090    fn edge_handle_event_when_closed() {
1091        let dialog = Dialog::alert("Test", "Msg");
1092        let mut state = DialogState::new();
1093        state.close(DialogResult::Dismissed);
1094
1095        let enter = Event::Key(KeyEvent {
1096            code: KeyCode::Enter,
1097            modifiers: Modifiers::empty(),
1098            kind: KeyEventKind::Press,
1099        });
1100        // Events on a closed dialog return None immediately
1101        let result = dialog.handle_event(&enter, &mut state, None);
1102        assert_eq!(result, None);
1103    }
1104
1105    #[test]
1106    fn edge_prompt_tab_full_cycle() {
1107        let dialog = Dialog::prompt("Test", "Enter:");
1108        let mut state = DialogState::new();
1109        // Prompt starts with input_focused=true
1110        assert!(state.input_focused);
1111        assert_eq!(state.focused_button, None);
1112
1113        let tab = Event::Key(KeyEvent {
1114            code: KeyCode::Tab,
1115            modifiers: Modifiers::empty(),
1116            kind: KeyEventKind::Press,
1117        });
1118
1119        // Tab 1: input -> button 0 (OK)
1120        dialog.handle_event(&tab, &mut state, None);
1121        assert!(!state.input_focused);
1122        assert_eq!(state.focused_button, Some(0));
1123
1124        // Tab 2: button 0 -> button 1 (Cancel)
1125        dialog.handle_event(&tab, &mut state, None);
1126        assert!(!state.input_focused);
1127        assert_eq!(state.focused_button, Some(1));
1128
1129        // Tab 3: button 1 -> back to input
1130        dialog.handle_event(&tab, &mut state, None);
1131        assert!(state.input_focused);
1132        assert_eq!(state.focused_button, None);
1133    }
1134
1135    #[test]
1136    fn edge_prompt_shift_tab_reverse_cycle() {
1137        let dialog = Dialog::prompt("Test", "Enter:");
1138        let mut state = DialogState::new();
1139
1140        let shift_tab = Event::Key(KeyEvent {
1141            code: KeyCode::Tab,
1142            modifiers: Modifiers::SHIFT,
1143            kind: KeyEventKind::Press,
1144        });
1145
1146        // Shift+Tab from input -> last button (Cancel, index 1)
1147        dialog.handle_event(&shift_tab, &mut state, None);
1148        assert!(!state.input_focused);
1149        assert_eq!(state.focused_button, Some(1));
1150
1151        // Shift+Tab from button 1 -> button 0
1152        dialog.handle_event(&shift_tab, &mut state, None);
1153        assert!(!state.input_focused);
1154        assert_eq!(state.focused_button, Some(0));
1155
1156        // Shift+Tab from button 0 -> back to input
1157        dialog.handle_event(&shift_tab, &mut state, None);
1158        assert!(state.input_focused);
1159        assert_eq!(state.focused_button, None);
1160    }
1161
1162    #[test]
1163    fn edge_arrow_key_navigation() {
1164        let dialog = Dialog::confirm("Test", "Msg");
1165        let mut state = DialogState::new();
1166        state.input_focused = false;
1167        state.focused_button = Some(0);
1168
1169        let right = Event::Key(KeyEvent {
1170            code: KeyCode::Right,
1171            modifiers: Modifiers::empty(),
1172            kind: KeyEventKind::Press,
1173        });
1174        let left = Event::Key(KeyEvent {
1175            code: KeyCode::Left,
1176            modifiers: Modifiers::empty(),
1177            kind: KeyEventKind::Press,
1178        });
1179
1180        // Right: 0 -> 1
1181        dialog.handle_event(&right, &mut state, None);
1182        assert_eq!(state.focused_button, Some(1));
1183
1184        // Right: 1 -> 0 (wrap)
1185        dialog.handle_event(&right, &mut state, None);
1186        assert_eq!(state.focused_button, Some(0));
1187
1188        // Left: 0 -> 1 (wrap backwards)
1189        dialog.handle_event(&left, &mut state, None);
1190        assert_eq!(state.focused_button, Some(1));
1191
1192        // Left: 1 -> 0
1193        dialog.handle_event(&left, &mut state, None);
1194        assert_eq!(state.focused_button, Some(0));
1195    }
1196
1197    #[test]
1198    fn edge_arrow_keys_ignored_when_input_focused() {
1199        let dialog = Dialog::prompt("Test", "Enter:");
1200        let mut state = DialogState::new();
1201        // input_focused=true by default for prompt
1202        assert!(state.input_focused);
1203        state.focused_button = None;
1204
1205        let right = Event::Key(KeyEvent {
1206            code: KeyCode::Right,
1207            modifiers: Modifiers::empty(),
1208            kind: KeyEventKind::Press,
1209        });
1210
1211        dialog.handle_event(&right, &mut state, None);
1212        // Arrow keys should NOT navigate buttons when input is focused
1213        assert!(state.input_focused);
1214        assert_eq!(state.focused_button, None);
1215    }
1216
1217    #[test]
1218    fn edge_input_backspace_on_empty() {
1219        let dialog = Dialog::prompt("Test", "Enter:");
1220        let mut state = DialogState::new();
1221        assert!(state.input_value.is_empty());
1222
1223        let backspace = Event::Key(KeyEvent {
1224            code: KeyCode::Backspace,
1225            modifiers: Modifiers::empty(),
1226            kind: KeyEventKind::Press,
1227        });
1228
1229        // Backspace on empty input should not panic
1230        dialog.handle_event(&backspace, &mut state, None);
1231        assert!(state.input_value.is_empty());
1232    }
1233
1234    #[test]
1235    fn edge_input_delete_clears_all() {
1236        let dialog = Dialog::prompt("Test", "Enter:");
1237        let mut state = DialogState::new();
1238        state.input_value = "hello world".to_string();
1239
1240        let delete = Event::Key(KeyEvent {
1241            code: KeyCode::Delete,
1242            modifiers: Modifiers::empty(),
1243            kind: KeyEventKind::Press,
1244        });
1245
1246        dialog.handle_event(&delete, &mut state, None);
1247        assert!(state.input_value.is_empty());
1248    }
1249
1250    #[test]
1251    fn edge_input_char_accumulation() {
1252        let dialog = Dialog::prompt("Test", "Enter:");
1253        let mut state = DialogState::new();
1254
1255        for c in ['h', 'e', 'l', 'l', 'o'] {
1256            let event = Event::Key(KeyEvent {
1257                code: KeyCode::Char(c),
1258                modifiers: Modifiers::empty(),
1259                kind: KeyEventKind::Press,
1260            });
1261            dialog.handle_event(&event, &mut state, None);
1262        }
1263        assert_eq!(state.input_value, "hello");
1264    }
1265
1266    #[test]
1267    fn edge_prompt_cancel_returns_cancel() {
1268        let dialog = Dialog::prompt("Test", "Enter:");
1269        let mut state = DialogState::new();
1270        state.input_value = "typed something".to_string();
1271        state.input_focused = false;
1272        state.focused_button = Some(1); // Cancel button
1273
1274        let enter = Event::Key(KeyEvent {
1275            code: KeyCode::Enter,
1276            modifiers: Modifiers::empty(),
1277            kind: KeyEventKind::Press,
1278        });
1279
1280        let result = dialog.handle_event(&enter, &mut state, None);
1281        assert_eq!(result, Some(DialogResult::Cancel));
1282        assert!(!state.is_open());
1283    }
1284
1285    #[test]
1286    fn edge_custom_button_activation() {
1287        let dialog = Dialog::custom("Test", "Msg")
1288            .custom_button("Save", "save")
1289            .custom_button("Delete", "delete")
1290            .build();
1291        let mut state = DialogState::new();
1292        state.input_focused = false;
1293        state.focused_button = Some(1); // "Delete" button
1294
1295        let enter = Event::Key(KeyEvent {
1296            code: KeyCode::Enter,
1297            modifiers: Modifiers::empty(),
1298            kind: KeyEventKind::Press,
1299        });
1300
1301        let result = dialog.handle_event(&enter, &mut state, None);
1302        assert_eq!(result, Some(DialogResult::Custom("delete".to_string())));
1303    }
1304
1305    #[test]
1306    fn edge_render_zero_size_area() {
1307        let dialog = Dialog::alert("T", "M");
1308        let mut state = DialogState::new();
1309        let mut pool = GraphemePool::new();
1310        let mut frame = Frame::new(80, 24, &mut pool);
1311        // Zero-width area
1312        dialog.render(Rect::new(0, 0, 0, 0), &mut frame, &mut state);
1313        // Zero-height area
1314        dialog.render(Rect::new(0, 0, 80, 0), &mut frame, &mut state);
1315        // Zero-width nonzero-height
1316        dialog.render(Rect::new(0, 0, 0, 24), &mut frame, &mut state);
1317    }
1318
1319    #[test]
1320    fn edge_render_closed_dialog_is_noop() {
1321        let dialog = Dialog::alert("Test", "Msg");
1322        let mut state = DialogState::new();
1323        state.close(DialogResult::Dismissed);
1324
1325        let mut pool = GraphemePool::new();
1326        let mut frame = Frame::new(80, 24, &mut pool);
1327
1328        // Rendering a closed dialog should not panic or alter frame
1329        dialog.render(Rect::new(0, 0, 80, 24), &mut frame, &mut state);
1330    }
1331
1332    #[test]
1333    fn edge_builder_hit_id() {
1334        let dialog = Dialog::custom("T", "M")
1335            .ok_button()
1336            .hit_id(HitId::new(42))
1337            .build();
1338        assert_eq!(dialog.hit_id, Some(HitId::new(42)));
1339    }
1340
1341    #[test]
1342    fn edge_builder_modal_config() {
1343        let config = ModalConfig::default().position(ModalPosition::TopCenter { margin: 5 });
1344        let dialog = Dialog::custom("T", "M")
1345            .ok_button()
1346            .modal_config(config)
1347            .build();
1348        assert_eq!(
1349            dialog.config.modal_config.position,
1350            ModalPosition::TopCenter { margin: 5 }
1351        );
1352    }
1353
1354    #[test]
1355    fn edge_content_height_alert() {
1356        let dialog = Dialog::alert("Title", "Message");
1357        let h = dialog.content_height();
1358        // 2 (borders) + 1 (title) + 1 (message) + 1 (spacing) + 1 (buttons) = 6
1359        assert_eq!(h, 6);
1360    }
1361
1362    #[test]
1363    fn edge_content_height_prompt() {
1364        let dialog = Dialog::prompt("Title", "Message");
1365        let h = dialog.content_height();
1366        // 2 (borders) + 1 (title) + 1 (message) + 1 (spacing) + 1 (input) + 1 (input spacing) + 1 (buttons) = 8
1367        assert_eq!(h, 8);
1368    }
1369
1370    #[test]
1371    fn edge_content_height_empty_title_and_message() {
1372        let dialog = Dialog::alert("", "");
1373        let h = dialog.content_height();
1374        // 2 (borders) + 0 (no title) + 0 (no message) + 1 (spacing) + 1 (buttons) = 4
1375        assert_eq!(h, 4);
1376    }
1377
1378    #[test]
1379    fn edge_button_display_width_unicode() {
1380        let button = DialogButton::new("保存", "save");
1381        // "保存" is 4 display columns + 4 for brackets = 8
1382        assert_eq!(button.display_width(), 8);
1383    }
1384
1385    #[test]
1386    fn edge_dialog_result_equality() {
1387        assert_eq!(DialogResult::Ok, DialogResult::Ok);
1388        assert_eq!(DialogResult::Cancel, DialogResult::Cancel);
1389        assert_eq!(DialogResult::Dismissed, DialogResult::Dismissed);
1390        assert_eq!(
1391            DialogResult::Custom("a".into()),
1392            DialogResult::Custom("a".into())
1393        );
1394        assert_ne!(
1395            DialogResult::Custom("a".into()),
1396            DialogResult::Custom("b".into())
1397        );
1398        assert_eq!(
1399            DialogResult::Input("x".into()),
1400            DialogResult::Input("x".into())
1401        );
1402        assert_ne!(DialogResult::Ok, DialogResult::Cancel);
1403    }
1404
1405    #[test]
1406    fn edge_mouse_down_mismatched_hit_id() {
1407        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
1408        let mut state = DialogState::new();
1409
1410        let down = Event::Mouse(MouseEvent::new(
1411            MouseEventKind::Down(MouseButton::Left),
1412            0,
1413            0,
1414        ));
1415        // Hit with different ID should not register
1416        let hit = Some((HitId::new(99), HitRegion::Button, 0u64));
1417        dialog.handle_event(&down, &mut state, hit);
1418        assert_eq!(state.pressed_button, None);
1419        assert_eq!(state.focused_button, None);
1420    }
1421
1422    #[test]
1423    fn edge_mouse_down_out_of_bounds_index() {
1424        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
1425        let mut state = DialogState::new();
1426
1427        let down = Event::Mouse(MouseEvent::new(
1428            MouseEventKind::Down(MouseButton::Left),
1429            0,
1430            0,
1431        ));
1432        // Button index beyond button count
1433        let hit = Some((HitId::new(1), HitRegion::Button, 99u64));
1434        dialog.handle_event(&down, &mut state, hit);
1435        assert_eq!(state.pressed_button, None);
1436    }
1437
1438    #[test]
1439    fn edge_mouse_up_different_button_from_pressed() {
1440        let dialog = Dialog::confirm("Test", "Msg").hit_id(HitId::new(1));
1441        let mut state = DialogState::new();
1442
1443        // Press button 0
1444        let down = Event::Mouse(MouseEvent::new(
1445            MouseEventKind::Down(MouseButton::Left),
1446            0,
1447            0,
1448        ));
1449        let hit0 = Some((HitId::new(1), HitRegion::Button, 0u64));
1450        dialog.handle_event(&down, &mut state, hit0);
1451        assert_eq!(state.pressed_button, Some(0));
1452
1453        // Release on button 1 — should NOT activate
1454        let up = Event::Mouse(MouseEvent::new(MouseEventKind::Up(MouseButton::Left), 0, 0));
1455        let hit1 = Some((HitId::new(1), HitRegion::Button, 1u64));
1456        let result = dialog.handle_event(&up, &mut state, hit1);
1457        assert_eq!(result, None);
1458        assert!(state.is_open());
1459        // pressed_button cleared by take()
1460        assert_eq!(state.pressed_button, None);
1461    }
1462
1463    #[test]
1464    fn edge_non_prompt_clears_input_focused() {
1465        let dialog = Dialog::alert("Test", "Msg");
1466        let mut state = DialogState::new();
1467        // Manually set input_focused (e.g. leftover from state reuse)
1468        state.input_focused = true;
1469
1470        let tab = Event::Key(KeyEvent {
1471            code: KeyCode::Tab,
1472            modifiers: Modifiers::empty(),
1473            kind: KeyEventKind::Press,
1474        });
1475        dialog.handle_event(&tab, &mut state, None);
1476        // Non-prompt dialog should clear input_focused
1477        assert!(!state.input_focused);
1478    }
1479
1480    #[test]
1481    fn edge_key_release_ignored() {
1482        let dialog = Dialog::prompt("Test", "Enter:");
1483        let mut state = DialogState::new();
1484        state.input_value.clear();
1485
1486        // Key release event should be ignored by input handler
1487        let release = Event::Key(KeyEvent {
1488            code: KeyCode::Char('x'),
1489            modifiers: Modifiers::empty(),
1490            kind: KeyEventKind::Release,
1491        });
1492        dialog.handle_event(&release, &mut state, None);
1493        assert!(state.input_value.is_empty());
1494    }
1495
1496    #[test]
1497    fn edge_enter_no_focused_no_primary_does_nothing() {
1498        // Build a dialog with no primary button
1499        let dialog = Dialog::custom("Test", "Msg")
1500            .custom_button("A", "a")
1501            .custom_button("B", "b")
1502            .build();
1503        let mut state = DialogState::new();
1504        state.input_focused = false;
1505        state.focused_button = None;
1506
1507        let enter = Event::Key(KeyEvent {
1508            code: KeyCode::Enter,
1509            modifiers: Modifiers::empty(),
1510            kind: KeyEventKind::Press,
1511        });
1512        // No focused button and no primary → activate_button returns None
1513        let result = dialog.handle_event(&enter, &mut state, None);
1514        assert_eq!(result, None);
1515        assert!(state.is_open());
1516    }
1517
1518    #[test]
1519    fn edge_dialog_style_setters() {
1520        let style = Style::new().bold();
1521        let dialog = Dialog::alert("T", "M")
1522            .button_style(style)
1523            .primary_button_style(style)
1524            .focused_button_style(style);
1525        assert_eq!(dialog.config.button_style, style);
1526        assert_eq!(dialog.config.primary_button_style, style);
1527        assert_eq!(dialog.config.focused_button_style, style);
1528    }
1529
1530    #[test]
1531    fn edge_dialog_modal_config_setter() {
1532        let mc = ModalConfig::default().position(ModalPosition::Custom { x: 10, y: 20 });
1533        let dialog = Dialog::alert("T", "M").modal_config(mc);
1534        assert_eq!(
1535            dialog.config.modal_config.position,
1536            ModalPosition::Custom { x: 10, y: 20 }
1537        );
1538    }
1539
1540    #[test]
1541    fn edge_dialog_clone_debug() {
1542        let dialog = Dialog::alert("T", "M");
1543        let cloned = dialog.clone();
1544        assert_eq!(cloned.title, dialog.title);
1545        assert_eq!(cloned.message, dialog.message);
1546        let _ = format!("{:?}", dialog);
1547    }
1548
1549    #[test]
1550    fn edge_dialog_builder_clone_debug() {
1551        let builder = Dialog::custom("T", "M").ok_button();
1552        let cloned = builder.clone();
1553        assert_eq!(cloned.title, builder.title);
1554        let _ = format!("{:?}", builder);
1555    }
1556
1557    #[test]
1558    fn edge_dialog_config_clone_debug() {
1559        let config = DialogConfig::default();
1560        let cloned = config.clone();
1561        assert_eq!(cloned.kind, config.kind);
1562        let _ = format!("{:?}", config);
1563    }
1564
1565    #[test]
1566    fn edge_dialog_state_clone_debug() {
1567        let mut state = DialogState::new();
1568        state.input_value = "test".to_string();
1569        state.focused_button = Some(1);
1570        let cloned = state.clone();
1571        assert_eq!(cloned.input_value, "test");
1572        assert_eq!(cloned.focused_button, Some(1));
1573        assert_eq!(cloned.open, state.open);
1574        let _ = format!("{:?}", state);
1575    }
1576
1577    #[test]
1578    fn edge_dialog_button_clone_debug() {
1579        let button = DialogButton::new("Save", "save").primary();
1580        let cloned = button.clone();
1581        assert_eq!(cloned.label, "Save");
1582        assert_eq!(cloned.id, "save");
1583        assert!(cloned.primary);
1584        let _ = format!("{:?}", button);
1585    }
1586
1587    #[test]
1588    fn edge_dialog_result_clone_debug() {
1589        let results = [
1590            DialogResult::Ok,
1591            DialogResult::Cancel,
1592            DialogResult::Dismissed,
1593            DialogResult::Custom("x".into()),
1594            DialogResult::Input("y".into()),
1595        ];
1596        for r in &results {
1597            let cloned = r.clone();
1598            assert_eq!(&cloned, r);
1599            let _ = format!("{:?}", r);
1600        }
1601    }
1602
1603    #[test]
1604    fn edge_dialog_kind_clone_debug_eq() {
1605        let kinds = [
1606            DialogKind::Alert,
1607            DialogKind::Confirm,
1608            DialogKind::Prompt,
1609            DialogKind::Custom,
1610        ];
1611        for k in &kinds {
1612            let cloned = *k;
1613            assert_eq!(cloned, *k);
1614            let _ = format!("{:?}", k);
1615        }
1616        assert_ne!(DialogKind::Alert, DialogKind::Confirm);
1617    }
1618}