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