1#![forbid(unsafe_code)]
2
3use 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
31pub const DIALOG_HIT_BUTTON: HitRegion = HitRegion::Button;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum DialogResult {
37 Dismissed,
39 Ok,
41 Cancel,
43 Custom(String),
45 Input(String),
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct DialogButton {
52 pub label: String,
54 pub id: String,
56 pub primary: bool,
58}
59
60impl DialogButton {
61 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 #[must_use]
72 pub fn primary(mut self) -> Self {
73 self.primary = true;
74 self
75 }
76
77 pub fn display_width(&self) -> usize {
79 display_width(self.label.as_str()) + 4
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum DialogKind {
87 Alert,
89 Confirm,
91 Prompt,
93 Custom,
95}
96
97#[derive(Debug, Clone, Default)]
99pub struct DialogState {
100 pub focused_button: Option<usize>,
102 pressed_button: Option<usize>,
104 pub input_value: String,
106 pub input_focused: bool,
108 pub open: bool,
110 pub result: Option<DialogResult>,
112}
113
114impl DialogState {
115 pub fn new() -> Self {
117 Self {
118 open: true,
119 input_focused: true, ..Default::default()
121 }
122 }
123
124 #[inline]
126 pub fn is_open(&self) -> bool {
127 self.open
128 }
129
130 pub fn close(&mut self, result: DialogResult) {
132 self.open = false;
133 self.pressed_button = None;
134 self.result = Some(result);
135 }
136
137 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 pub fn take_result(&mut self) -> Option<DialogResult> {
149 self.result.take()
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct DialogConfig {
156 pub modal_config: ModalConfig,
158 pub kind: DialogKind,
160 pub button_style: Style,
162 pub primary_button_style: Style,
164 pub focused_button_style: Style,
166 pub title_style: Style,
168 pub message_style: Style,
170 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#[derive(Debug, Clone)]
202pub struct Dialog {
203 title: String,
205 message: String,
207 buttons: Vec<DialogButton>,
209 config: DialogConfig,
211 hit_id: Option<HitId>,
213}
214
215impl Dialog {
216 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 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 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 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 #[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 #[must_use]
288 pub fn modal_config(mut self, config: ModalConfig) -> Self {
289 self.config.modal_config = config;
290 self
291 }
292
293 #[must_use]
295 pub fn button_style(mut self, style: Style) -> Self {
296 self.config.button_style = style;
297 self
298 }
299
300 #[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 #[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 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 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 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 Event::Key(KeyEvent {
353 code: KeyCode::Enter,
354 kind: KeyEventKind::Press,
355 ..
356 }) => {
357 return self.activate_button(state);
358 }
359
360 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 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 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 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 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 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 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 fn content_height(&self) -> u16 {
523 let mut height: u16 = 2; if !self.title.is_empty() {
527 height += 1;
528 }
529
530 if !self.message.is_empty() {
532 height += 1;
533 }
534
535 height += 1;
537
538 if self.config.kind == DialogKind::Prompt {
540 height += 1;
541 height += 1; }
543
544 height += 1;
546
547 height
548 }
549
550 fn render_content(&self, area: Rect, frame: &mut Frame, state: &DialogState) {
552 if area.is_empty() {
553 return;
554 }
555
556 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 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 y += 1;
585
586 if self.config.kind == DialogKind::Prompt && y < inner.bottom() {
588 self.render_input(frame, inner.x, y, inner.width, state);
589 y += 2; }
591
592 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 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 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 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 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; 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 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 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 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); }
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 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 let content = DialogContent {
723 dialog: self,
724 state,
725 };
726
727 let modal = Modal::new(content).config(config);
729 modal.render(area, frame);
730 }
731}
732
733struct 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#[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 pub fn button(mut self, button: DialogButton) -> Self {
759 self.buttons.push(button);
760 self
761 }
762
763 pub fn ok_button(self) -> Self {
765 self.button(DialogButton::new("OK", "ok").primary())
766 }
767
768 pub fn cancel_button(self) -> Self {
770 self.button(DialogButton::new("Cancel", "cancel"))
771 }
772
773 pub fn custom_button(self, label: impl Into<String>, id: impl Into<String>) -> Self {
775 self.button(DialogButton::new(label, id))
776 }
777
778 pub fn modal_config(mut self, config: ModalConfig) -> Self {
780 self.config.modal_config = config;
781 self
782 }
783
784 pub fn hit_id(mut self, id: HitId) -> Self {
786 self.hit_id = Some(id);
787 self
788 }
789
790 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; 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)); }
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); 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); }
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 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(); 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 #[test]
1043 fn edge_state_default_vs_new() {
1044 let default = DialogState::default();
1045 let new = DialogState::new();
1046 assert!(!default.open);
1048 assert!(!default.input_focused);
1049 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 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 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 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 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 dialog.handle_event(&tab, &mut state, None);
1121 assert!(!state.input_focused);
1122 assert_eq!(state.focused_button, Some(0));
1123
1124 dialog.handle_event(&tab, &mut state, None);
1126 assert!(!state.input_focused);
1127 assert_eq!(state.focused_button, Some(1));
1128
1129 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 dialog.handle_event(&shift_tab, &mut state, None);
1148 assert!(!state.input_focused);
1149 assert_eq!(state.focused_button, Some(1));
1150
1151 dialog.handle_event(&shift_tab, &mut state, None);
1153 assert!(!state.input_focused);
1154 assert_eq!(state.focused_button, Some(0));
1155
1156 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 dialog.handle_event(&right, &mut state, None);
1182 assert_eq!(state.focused_button, Some(1));
1183
1184 dialog.handle_event(&right, &mut state, None);
1186 assert_eq!(state.focused_button, Some(0));
1187
1188 dialog.handle_event(&left, &mut state, None);
1190 assert_eq!(state.focused_button, Some(1));
1191
1192 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 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 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 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); 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); 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 dialog.render(Rect::new(0, 0, 0, 0), &mut frame, &mut state);
1313 dialog.render(Rect::new(0, 0, 80, 0), &mut frame, &mut state);
1315 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 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 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 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 assert_eq!(h, 4);
1376 }
1377
1378 #[test]
1379 fn edge_button_display_width_unicode() {
1380 let button = DialogButton::new("保存", "save");
1381 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 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 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 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 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 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 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 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 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 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 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}