1use super::{InteractionMode, PaneId, Tab};
4use crate::{MenuBar, MenuItem};
5use crossterm::event::{
6 Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
7};
8use ratatui::layout::{Constraint, Direction, Layout, Rect};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum EventResult {
13 Consumed,
15 NotHandled,
17 Quit,
19}
20
21pub struct MasterLayout {
59 nav_bar: MenuBar,
60 tabs: Vec<Tab>,
61 active_tab_index: usize,
62 mode: InteractionMode,
63 global_area: Rect,
64 nav_bar_offset: u16,
65}
66
67impl MasterLayout {
68 pub fn new() -> Self {
70 Self {
71 nav_bar: MenuBar::new(Vec::new()),
72 tabs: Vec::new(),
73 active_tab_index: 0,
74 mode: InteractionMode::default(),
75 global_area: Rect::default(),
76 nav_bar_offset: 0,
77 }
78 }
79
80 pub fn set_nav_bar_offset(&mut self, offset: u16) {
82 self.nav_bar_offset = offset;
83 }
84
85 pub fn add_tab(&mut self, tab: Tab) {
87 self.tabs.push(tab);
88
89 let menu_items: Vec<MenuItem> = self
91 .tabs
92 .iter()
93 .enumerate()
94 .map(|(i, t)| MenuItem::new(t.name(), i))
95 .collect();
96 self.nav_bar = MenuBar::new(menu_items).with_selected(self.active_tab_index);
97
98 if self.tabs.len() == 1 {
100 self.select_first_pane_in_active_tab();
101 }
102 }
103
104 pub fn tab_count(&self) -> usize {
106 self.tabs.len()
107 }
108
109 pub fn active_tab_index(&self) -> usize {
111 self.active_tab_index
112 }
113
114 pub fn set_active_tab(&mut self, index: usize) {
116 if index < self.tabs.len() {
117 self.active_tab_index = index;
118 for (i, item) in self.nav_bar.items.iter_mut().enumerate() {
120 item.selected = i == index;
121 }
122
123 self.select_first_pane_in_active_tab();
125 }
126 }
127
128 pub fn active_tab(&self) -> Option<&Tab> {
130 self.tabs.get(self.active_tab_index)
131 }
132
133 pub fn active_tab_mut(&mut self) -> Option<&mut Tab> {
135 self.tabs.get_mut(self.active_tab_index)
136 }
137
138 pub fn mode(&self) -> &InteractionMode {
140 &self.mode
141 }
142
143 pub fn enter_layout_mode(&mut self) {
145 if let Some(focused_id) = self.mode.focused_pane() {
146 self.mode = InteractionMode::layout_with_selection(focused_id);
148 } else {
149 if !self.mode.is_layout() {
151 self.mode = InteractionMode::layout();
152 }
153 }
154 }
155
156 pub fn enter_focus_mode(&mut self, pane_id: PaneId) {
158 self.mode = InteractionMode::focus(pane_id);
159 }
160
161 pub fn exit_focus_mode(&mut self) {
163 self.mode.exit_focus();
164 }
165
166 fn select_first_pane_in_active_tab(&mut self) {
168 if let Some(tab) = self.active_tab() {
169 let container = tab.pane_container();
170 if let Some(first_pane) = container.select_next(None) {
171 self.mode = InteractionMode::layout_with_selection(first_pane);
172 }
173 }
174 }
175
176 pub fn select_next_pane(&mut self) {
178 if !self.mode.is_layout() {
179 return;
180 }
181
182 if let Some(tab) = self.active_tab() {
183 let current = self.mode.selected_pane();
184 if let Some(next) = tab.pane_container().select_next(current) {
185 self.mode.select_pane(next);
186 }
187 }
188 }
189
190 pub fn select_prev_pane(&mut self) {
192 if !self.mode.is_layout() {
193 return;
194 }
195
196 if let Some(tab) = self.active_tab() {
197 let current = self.mode.selected_pane();
198 if let Some(prev) = tab.pane_container().select_prev(current) {
199 self.mode.select_pane(prev);
200 }
201 }
202 }
203
204 pub fn select_left(&mut self) {
206 if !self.mode.is_layout() {
207 return;
208 }
209
210 if let Some(tab) = self.active_tab() {
211 if let Some(current) = self.mode.selected_pane() {
212 if let Some(left) = tab.pane_container().select_left(current) {
213 self.mode.select_pane(left);
214 }
215 }
216 }
217 }
218
219 pub fn select_right(&mut self) {
221 if !self.mode.is_layout() {
222 return;
223 }
224
225 if let Some(tab) = self.active_tab() {
226 if let Some(current) = self.mode.selected_pane() {
227 if let Some(right) = tab.pane_container().select_right(current) {
228 self.mode.select_pane(right);
229 }
230 }
231 }
232 }
233
234 pub fn select_up(&mut self) {
236 if !self.mode.is_layout() {
237 return;
238 }
239
240 if let Some(tab) = self.active_tab() {
241 if let Some(current) = self.mode.selected_pane() {
242 if let Some(up) = tab.pane_container().select_up(current) {
243 self.mode.select_pane(up);
244 }
245 }
246 }
247 }
248
249 pub fn select_down(&mut self) {
251 if !self.mode.is_layout() {
252 return;
253 }
254
255 if let Some(tab) = self.active_tab() {
256 if let Some(current) = self.mode.selected_pane() {
257 if let Some(down) = tab.pane_container().select_down(current) {
258 self.mode.select_pane(down);
259 }
260 }
261 }
262 }
263
264 pub fn focus_selected(&mut self) {
266 if !self.mode.is_layout() {
267 return;
268 }
269
270 if let Some(selected) = self.mode.selected_pane() {
271 self.enter_focus_mode(selected);
272 }
273 }
274
275 pub fn handle_event(&mut self, event: Event) -> EventResult {
277 match event {
278 Event::Key(key) => self.handle_key_event(key),
279 Event::Mouse(mouse) => self.handle_mouse_event(mouse),
280 Event::Resize(_, _) => EventResult::Consumed,
281 _ => EventResult::NotHandled,
282 }
283 }
284
285 fn handle_key_event(&mut self, key: KeyEvent) -> EventResult {
287 match self.mode.clone() {
289 InteractionMode::Layout { selected_pane } => {
290 if matches!(key.code, KeyCode::Char('q') | KeyCode::Char('Q'))
292 && key.modifiers.is_empty()
293 {
294 return EventResult::Quit;
295 }
296
297 if let Some(pane_id) = selected_pane {
299 if key.code == KeyCode::Char('c')
301 && key.modifiers.contains(KeyModifiers::CONTROL)
302 && key.modifiers.contains(KeyModifiers::SHIFT)
303 {
304 if let Some(tab) = self.active_tab_mut() {
305 if let Some(pane) = tab.pane_container_mut().get_pane_mut(pane_id) {
306 if pane.has_selection() && pane.handle_key(key) {
308 return EventResult::Consumed;
309 }
310 }
311 }
312 }
313
314 if key.code == KeyCode::Esc {
316 if let Some(tab) = self.active_tab_mut() {
317 if let Some(pane) = tab.pane_container_mut().get_pane_mut(pane_id) {
318 if pane.has_selection() && pane.handle_key(key) {
320 return EventResult::Consumed;
321 }
322 }
323 }
324 }
326 }
327
328 self.handle_layout_mode_key(key)
330 }
331 InteractionMode::Focus { focused_pane } => {
332 if key.code == KeyCode::Char('a') && key.modifiers.contains(KeyModifiers::CONTROL) {
335 self.exit_focus_mode();
336 return EventResult::Consumed;
337 }
338
339 if let Some(tab) = self.active_tab_mut() {
341 if let Some(pane) = tab.pane_container_mut().get_pane_mut(focused_pane) {
342 if pane.handle_key(key) {
343 return EventResult::Consumed;
344 }
345 }
346 }
347
348 EventResult::NotHandled
349 }
350 }
351 }
352
353 fn handle_layout_mode_key(&mut self, key: KeyEvent) -> EventResult {
355 match key.code {
356 KeyCode::Esc => {
358 self.mode = InteractionMode::Layout {
359 selected_pane: None,
360 };
361 EventResult::Consumed
362 }
363 KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
365 self.mode = InteractionMode::Layout {
366 selected_pane: None,
367 };
368 EventResult::Consumed
369 }
370 KeyCode::Char(c) if c.is_ascii_digit() => {
372 let digit = c.to_digit(10).unwrap() as usize;
373 if digit >= 1 && digit <= self.tab_count() {
374 self.set_active_tab(digit - 1);
375 return EventResult::Consumed;
376 }
377 EventResult::NotHandled
378 }
379 KeyCode::Char('h') => {
381 self.select_left();
382 EventResult::Consumed
383 }
384 KeyCode::Char('j') => {
385 self.select_down();
386 EventResult::Consumed
387 }
388 KeyCode::Char('k') => {
389 self.select_up();
390 EventResult::Consumed
391 }
392 KeyCode::Char('l') => {
393 self.select_right();
394 EventResult::Consumed
395 }
396 KeyCode::Enter => {
398 self.focus_selected();
399 EventResult::Consumed
400 }
401 _ => EventResult::NotHandled,
402 }
403 }
404
405 fn handle_mouse_event(&mut self, mouse: MouseEvent) -> EventResult {
407 if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
409 if mouse.row < 3 {
411 if let Some(tab_index) = self.nav_bar.handle_click(mouse.column, mouse.row) {
412 self.set_active_tab(tab_index);
413 return EventResult::Consumed;
414 }
415 }
416 }
417
418 if matches!(self.mode, InteractionMode::Layout { .. }) {
420 if let Some(tab) = self.active_tab_mut() {
421 let container = tab.pane_container_mut();
422
423 match mouse.kind {
424 MouseEventKind::Down(MouseButton::Left) => {
425 if let Some(divider_idx) =
427 container.find_divider_at(mouse.column, mouse.row)
428 {
429 container.start_drag(divider_idx);
430 return EventResult::Consumed;
431 }
432 }
433 MouseEventKind::Drag(MouseButton::Left) => {
434 if container.is_dragging() {
436 container.update_drag(mouse.column, mouse.row);
437 return EventResult::Consumed;
438 }
439 }
440 MouseEventKind::Up(MouseButton::Left) => {
441 if container.is_dragging() {
443 container.stop_drag();
444 return EventResult::Consumed;
445 }
446 }
447 MouseEventKind::Moved => {
448 container.update_hover(mouse.column, mouse.row);
450 }
451 _ => {}
452 }
453 }
454 }
455
456 match mouse.kind {
458 MouseEventKind::Down(MouseButton::Left) if mouse.row >= 3 => {
459 let current_pane = match &self.mode {
461 InteractionMode::Layout { selected_pane } => *selected_pane,
462 InteractionMode::Focus { focused_pane } => Some(*focused_pane),
463 };
464
465 if let Some(tab) = self.active_tab_mut() {
467 if let Some(pane_id) =
468 tab.pane_container().find_pane_at(mouse.column, mouse.row)
469 {
470 if Some(pane_id) == current_pane {
472 if let Some(pane) = tab.pane_container_mut().get_pane_mut(pane_id) {
473 if pane.contains_point(mouse.column, mouse.row) {
474 let local_mouse = pane.translate_mouse(mouse);
475 pane.start_selection(local_mouse.column, local_mouse.row);
476 return EventResult::Consumed;
477 }
478 }
479 } else {
480 match &mut self.mode {
482 InteractionMode::Layout { .. } => {
483 self.mode.select_pane(pane_id);
484 }
485 InteractionMode::Focus { focused_pane } => {
486 if *focused_pane != pane_id {
487 self.enter_focus_mode(pane_id);
488 }
489 }
490 }
491 return EventResult::Consumed;
492 }
493 }
494 }
495 }
496 MouseEventKind::Drag(MouseButton::Left) if mouse.row >= 3 => {
497 let current_pane = match &self.mode {
499 InteractionMode::Layout { selected_pane } => *selected_pane,
500 InteractionMode::Focus { focused_pane } => Some(*focused_pane),
501 };
502
503 if let Some(pane_id) = current_pane {
504 if let Some(tab) = self.active_tab_mut() {
505 if let Some(pane) = tab.pane_container_mut().get_pane_mut(pane_id) {
506 if pane.contains_point(mouse.column, mouse.row) {
507 let local_mouse = pane.translate_mouse(mouse);
508 pane.update_selection(local_mouse.column, local_mouse.row);
509 return EventResult::Consumed;
510 }
511 }
512 }
513 }
514 }
515 MouseEventKind::Up(MouseButton::Left) if mouse.row >= 3 => {
516 let current_pane = match &self.mode {
518 InteractionMode::Layout { selected_pane } => *selected_pane,
519 InteractionMode::Focus { focused_pane } => Some(*focused_pane),
520 };
521
522 if let Some(pane_id) = current_pane {
523 if let Some(tab) = self.active_tab_mut() {
524 if let Some(pane) = tab.pane_container_mut().get_pane_mut(pane_id) {
525 pane.end_selection();
526 return EventResult::Consumed;
527 }
528 }
529 }
530 }
531 _ => {}
532 }
533
534 if let InteractionMode::Focus { focused_pane } = self.mode.clone() {
536 if let Some(tab) = self.active_tab_mut() {
537 if let Some(pane) = tab.pane_container_mut().get_pane_mut(focused_pane) {
538 if pane.contains_point(mouse.column, mouse.row) {
540 let local_mouse = pane.translate_mouse(mouse);
542 if pane.handle_mouse(local_mouse) {
543 return EventResult::Consumed;
544 }
545 } else {
546 if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
548 if let Some(new_pane) =
549 tab.pane_container().find_pane_at(mouse.column, mouse.row)
550 {
551 self.enter_focus_mode(new_pane);
552 return EventResult::Consumed;
553 }
554 }
555 }
556 }
557 }
558 }
559
560 EventResult::NotHandled
561 }
562
563 pub fn render(&mut self, frame: &mut ratatui::Frame) {
565 self.global_area = frame.area();
566
567 let chunks = Layout::default()
569 .direction(Direction::Vertical)
570 .constraints([
571 Constraint::Length(3), Constraint::Min(5), ])
574 .split(self.global_area);
575
576 let nav_area = chunks[0];
577 let tab_area = chunks[1];
578
579 self.nav_bar
581 .render_with_offset(frame, nav_area, self.nav_bar_offset);
582
583 let mode = self.mode.clone();
585 if let Some(tab) = self.active_tab_mut() {
586 tab.render(frame, tab_area, &mode);
587 }
588 }
589}
590
591impl Default for MasterLayout {
592 fn default() -> Self {
593 Self::new()
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use crate::master_layout::{Pane, PaneContent};
601 use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
602 use ratatui::{backend::TestBackend, buffer::Buffer, widgets::Widget, Terminal};
603
604 struct MockContent {
606 title: String,
607 key_count: usize,
608 mouse_count: usize,
609 last_key: Option<KeyEvent>,
610 }
611
612 impl MockContent {
613 fn new(title: &str) -> Self {
614 Self {
615 title: title.to_string(),
616 key_count: 0,
617 mouse_count: 0,
618 last_key: None,
619 }
620 }
621 }
622
623 impl Widget for MockContent {
624 fn render(self, _area: Rect, _buf: &mut Buffer) {}
625 }
626
627 impl PaneContent for MockContent {
628 fn handle_key(&mut self, key: KeyEvent) -> bool {
629 self.key_count += 1;
630 self.last_key = Some(key);
631 true
632 }
633
634 fn handle_mouse(&mut self, _mouse: MouseEvent) -> bool {
635 self.mouse_count += 1;
636 true
637 }
638
639 fn title(&self) -> String {
640 self.title.clone()
641 }
642
643 fn render_content(&mut self, _area: Rect, _frame: &mut ratatui::Frame) {
644 }
646 }
647
648 fn create_test_layout() -> MasterLayout {
649 let mut layout = MasterLayout::new();
650
651 let mut tab = Tab::new("Test Tab");
653 let pane1 = Pane::new(PaneId::new("pane1"), Box::new(MockContent::new("Pane 1")));
654 let pane2 = Pane::new(PaneId::new("pane2"), Box::new(MockContent::new("Pane 2")));
655 tab.add_pane(pane1);
656 tab.add_pane(pane2);
657
658 layout.add_tab(tab);
659 layout
660 }
661
662 #[test]
663 fn test_master_layout_creation() {
664 let layout = MasterLayout::new();
665 assert_eq!(layout.tab_count(), 0);
666 assert_eq!(layout.active_tab_index(), 0);
667 assert!(layout.mode().is_layout());
668 }
669
670 #[test]
671 fn test_add_tab() {
672 let mut layout = MasterLayout::new();
673 let tab = Tab::new("Tab 1");
674
675 layout.add_tab(tab);
676
677 assert_eq!(layout.tab_count(), 1);
678 assert!(layout.active_tab().is_some());
679 assert_eq!(layout.active_tab().unwrap().name(), "Tab 1");
680 }
681
682 #[test]
683 fn test_add_multiple_tabs() {
684 let mut layout = MasterLayout::new();
685
686 layout.add_tab(Tab::new("Tab 1"));
687 layout.add_tab(Tab::new("Tab 2"));
688 layout.add_tab(Tab::new("Tab 3"));
689
690 assert_eq!(layout.tab_count(), 3);
691 assert_eq!(layout.active_tab_index(), 0);
692 }
693
694 #[test]
695 fn test_set_active_tab() {
696 let mut layout = MasterLayout::new();
697 layout.add_tab(Tab::new("Tab 1"));
698 layout.add_tab(Tab::new("Tab 2"));
699 layout.add_tab(Tab::new("Tab 3"));
700
701 layout.set_active_tab(1);
702 assert_eq!(layout.active_tab_index(), 1);
703 assert_eq!(layout.active_tab().unwrap().name(), "Tab 2");
704
705 layout.set_active_tab(2);
706 assert_eq!(layout.active_tab_index(), 2);
707 assert_eq!(layout.active_tab().unwrap().name(), "Tab 3");
708 }
709
710 #[test]
711 fn test_set_active_tab_invalid_index() {
712 let mut layout = MasterLayout::new();
713 layout.add_tab(Tab::new("Tab 1"));
714
715 layout.set_active_tab(10);
716 assert_eq!(layout.active_tab_index(), 0);
718 }
719
720 #[test]
721 fn test_mode_transitions() {
722 let mut layout = create_test_layout();
723
724 assert!(layout.mode().is_layout());
726
727 let pane_id = layout
729 .active_tab()
730 .unwrap()
731 .pane_container()
732 .get_pane_by_index(0)
733 .unwrap()
734 .id();
735
736 layout.enter_focus_mode(pane_id);
738 assert!(layout.mode().is_focus());
739 assert_eq!(layout.mode().focused_pane(), Some(pane_id));
740
741 layout.exit_focus_mode();
743 assert!(layout.mode().is_layout());
744 assert_eq!(layout.mode().selected_pane(), Some(pane_id));
745 }
746
747 #[test]
748 fn test_enter_key_focuses_selected_pane() {
749 let mut layout = create_test_layout();
750
751 let selected = layout.mode().selected_pane();
753 assert!(selected.is_some());
754
755 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
757 let result = layout.handle_key_event(key);
758
759 assert_eq!(result, EventResult::Consumed);
760 assert!(layout.mode().is_focus());
761 assert_eq!(layout.mode().focused_pane(), selected);
762 }
763
764 #[test]
765 fn test_ctrl_a_exits_focus_mode() {
766 let mut layout = create_test_layout();
767
768 let pane_id = layout
769 .active_tab()
770 .unwrap()
771 .pane_container()
772 .get_pane_by_index(0)
773 .unwrap()
774 .id();
775
776 layout.enter_focus_mode(pane_id);
778 assert!(layout.mode().is_focus());
779
780 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
782 let result = layout.handle_key_event(key);
783
784 assert_eq!(result, EventResult::Consumed);
785 assert!(layout.mode().is_layout());
786 assert_eq!(layout.mode().selected_pane(), Some(pane_id));
787 }
788
789 #[test]
790 fn test_q_quits() {
791 let mut layout = create_test_layout();
792
793 let key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty());
794 let result = layout.handle_key_event(key);
795
796 assert_eq!(result, EventResult::Quit);
797 }
798
799 #[test]
800 fn test_tab_key_not_handled_in_layout_mode() {
801 let mut layout = create_test_layout();
803
804 let first_selected = layout.mode().selected_pane();
805 assert!(first_selected.is_some());
806
807 let key = KeyEvent::new(KeyCode::Tab, KeyModifiers::empty());
809 let result = layout.handle_key_event(key);
810
811 assert_eq!(result, EventResult::NotHandled);
813
814 let still_selected = layout.mode().selected_pane();
816 assert_eq!(first_selected, still_selected);
817 }
818
819 #[test]
820 fn test_shift_tab_not_handled_in_layout_mode() {
821 let mut layout = create_test_layout();
823
824 let first_selected = layout.mode().selected_pane();
825
826 let key = KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT);
828 let result = layout.handle_key_event(key);
829
830 assert_eq!(result, EventResult::NotHandled);
832
833 let still_selected = layout.mode().selected_pane();
835 assert_eq!(first_selected, still_selected);
836 }
837
838 #[test]
839 fn test_digit_keys_switch_tabs_in_layout_mode() {
840 let mut layout = MasterLayout::new();
842
843 let mut tab1 = Tab::new("Tab 1");
844 tab1.add_pane(Pane::new(
845 PaneId::new("p1"),
846 Box::new(MockContent::new("P1")),
847 ));
848 layout.add_tab(tab1);
849
850 let mut tab2 = Tab::new("Tab 2");
851 tab2.add_pane(Pane::new(
852 PaneId::new("p2"),
853 Box::new(MockContent::new("P2")),
854 ));
855 layout.add_tab(tab2);
856
857 let mut tab3 = Tab::new("Tab 3");
858 tab3.add_pane(Pane::new(
859 PaneId::new("p3"),
860 Box::new(MockContent::new("P3")),
861 ));
862 layout.add_tab(tab3);
863
864 assert!(layout.mode().is_layout());
866
867 let key = KeyEvent::new(KeyCode::Char('2'), KeyModifiers::empty());
869 let result = layout.handle_key_event(key);
870
871 assert_eq!(result, EventResult::Consumed);
872 assert_eq!(layout.active_tab_index(), 1);
873
874 let key = KeyEvent::new(KeyCode::Char('3'), KeyModifiers::empty());
876 layout.handle_key_event(key);
877 assert_eq!(layout.active_tab_index(), 2);
878 }
879
880 #[test]
881 fn test_hjkl_navigation_in_layout_mode() {
882 let mut layout = create_test_layout();
883
884 assert!(layout.mode().is_layout());
886 assert!(layout.mode().selected_pane().is_some());
887
888 let key = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::empty());
890 let result = layout.handle_key_event(key);
891 assert_eq!(result, EventResult::Consumed);
892
893 let key = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::empty());
895 let result = layout.handle_key_event(key);
896 assert_eq!(result, EventResult::Consumed);
897
898 let key = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::empty());
900 let result = layout.handle_key_event(key);
901 assert_eq!(result, EventResult::Consumed);
902
903 let key = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::empty());
905 let result = layout.handle_key_event(key);
906 assert_eq!(result, EventResult::Consumed);
907 }
908
909 #[test]
910 fn test_hjkl_ignored_in_focus_mode() {
911 let mut layout = create_test_layout();
912
913 let pane_id = layout
914 .active_tab()
915 .unwrap()
916 .pane_container()
917 .get_pane_by_index(0)
918 .unwrap()
919 .id();
920
921 layout.enter_focus_mode(pane_id);
923
924 let initial_mode = layout.mode().clone();
925
926 let key = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::empty());
928 layout.handle_key_event(key);
929
930 assert!(layout.mode().is_focus());
932 assert_eq!(layout.mode(), &initial_mode);
933 }
934
935 #[test]
936 fn test_mouse_click_on_nav_bar() {
937 let mut layout = MasterLayout::new();
938 layout.add_tab(Tab::new("Tab 1"));
939 layout.add_tab(Tab::new("Tab 2"));
940
941 assert_eq!(layout.active_tab_index(), 0);
943
944 let backend = TestBackend::new(80, 24);
946 let mut terminal = Terminal::new(backend).unwrap();
947 terminal
948 .draw(|frame| {
949 layout.render(frame);
950 })
951 .unwrap();
952
953 let mouse = MouseEvent {
960 kind: MouseEventKind::Down(MouseButton::Left),
961 column: 5,
962 row: 1,
963 modifiers: KeyModifiers::empty(),
964 };
965
966 let result = layout.handle_mouse_event(mouse);
967 assert!(result == EventResult::Consumed || result == EventResult::NotHandled);
970 }
971
972 #[test]
973 fn test_mouse_click_on_pane_focuses() {
974 let mut layout = create_test_layout();
975
976 let backend = TestBackend::new(80, 24);
978 let mut terminal = Terminal::new(backend).unwrap();
979 terminal
980 .draw(|frame| {
981 layout.render(frame);
982 })
983 .unwrap();
984
985 let mouse = MouseEvent {
987 kind: MouseEventKind::Down(MouseButton::Left),
988 column: 10,
989 row: 5,
990 modifiers: KeyModifiers::empty(),
991 };
992
993 layout.handle_mouse_event(mouse);
994
995 }
998
999 #[test]
1000 fn test_keys_routed_to_focused_pane() {
1001 let mut layout = create_test_layout();
1002
1003 let pane_id = layout
1004 .active_tab()
1005 .unwrap()
1006 .pane_container()
1007 .get_pane_by_index(0)
1008 .unwrap()
1009 .id();
1010
1011 layout.enter_focus_mode(pane_id);
1013
1014 let key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty());
1016 let result = layout.handle_key_event(key);
1017
1018 assert_eq!(result, EventResult::Consumed);
1019 }
1021
1022 #[test]
1023 fn test_tab_key_routed_to_pane_in_focus_mode() {
1024 let mut layout = create_test_layout();
1026
1027 let pane_id = layout
1028 .active_tab()
1029 .unwrap()
1030 .pane_container()
1031 .get_pane_by_index(0)
1032 .unwrap()
1033 .id();
1034
1035 layout.enter_focus_mode(pane_id);
1037 assert!(layout.mode().is_focus());
1038
1039 let key = KeyEvent::new(KeyCode::Tab, KeyModifiers::empty());
1041 let result = layout.handle_key_event(key);
1042
1043 assert_eq!(result, EventResult::Consumed);
1046
1047 assert!(layout.mode().is_focus());
1049 assert_eq!(layout.mode().focused_pane(), Some(pane_id));
1050 }
1051
1052 #[test]
1053 fn test_number_keys_routed_to_pane_in_focus_mode() {
1054 let mut layout = MasterLayout::new();
1056
1057 let mut tab1 = Tab::new("Tab 1");
1058 tab1.add_pane(Pane::new(
1059 PaneId::new("p1"),
1060 Box::new(MockContent::new("P1")),
1061 ));
1062 layout.add_tab(tab1);
1063
1064 let mut tab2 = Tab::new("Tab 2");
1065 tab2.add_pane(Pane::new(
1066 PaneId::new("p2"),
1067 Box::new(MockContent::new("P2")),
1068 ));
1069 layout.add_tab(tab2);
1070
1071 assert_eq!(layout.active_tab_index(), 0);
1073
1074 let pane_id = layout
1075 .active_tab()
1076 .unwrap()
1077 .pane_container()
1078 .get_pane_by_index(0)
1079 .unwrap()
1080 .id();
1081
1082 layout.enter_focus_mode(pane_id);
1084 assert!(layout.mode().is_focus());
1085
1086 let key = KeyEvent::new(KeyCode::Char('2'), KeyModifiers::empty());
1088 let result = layout.handle_key_event(key);
1089
1090 assert_eq!(result, EventResult::Consumed);
1092
1093 assert_eq!(layout.active_tab_index(), 0);
1095
1096 assert!(layout.mode().is_focus());
1098 assert_eq!(layout.mode().focused_pane(), Some(pane_id));
1099 }
1100
1101 #[test]
1102 fn test_only_ctrl_a_exits_focus_mode() {
1103 let mut layout = create_test_layout();
1105
1106 let pane_id = layout
1107 .active_tab()
1108 .unwrap()
1109 .pane_container()
1110 .get_pane_by_index(0)
1111 .unwrap()
1112 .id();
1113
1114 layout.enter_focus_mode(pane_id);
1116 assert!(layout.mode().is_focus());
1117
1118 let test_keys = vec![
1120 KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()),
1121 KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
1122 KeyEvent::new(KeyCode::Char('h'), KeyModifiers::empty()),
1123 KeyEvent::new(KeyCode::Tab, KeyModifiers::empty()),
1124 KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty()),
1125 ];
1126
1127 for key in test_keys {
1128 layout.handle_key_event(key);
1129 assert!(
1131 layout.mode().is_focus(),
1132 "Key {:?} should not exit focus mode",
1133 key
1134 );
1135 }
1136
1137 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
1139 let result = layout.handle_key_event(key);
1140
1141 assert_eq!(result, EventResult::Consumed);
1142 assert!(layout.mode().is_layout());
1143 }
1144
1145 #[test]
1146 fn test_all_keys_passed_to_focused_pane() {
1147 let mut layout = create_test_layout();
1149
1150 let pane_id = layout
1151 .active_tab()
1152 .unwrap()
1153 .pane_container()
1154 .get_pane_by_index(0)
1155 .unwrap()
1156 .id();
1157
1158 layout.enter_focus_mode(pane_id);
1160 assert!(layout.mode().is_focus());
1161
1162 let test_keys = vec![
1164 KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL), KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL), KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()), KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL), ];
1170
1171 for key in test_keys {
1172 let result = layout.handle_key_event(key);
1173
1174 assert_eq!(
1176 result,
1177 EventResult::Consumed,
1178 "Key {:?} should be passed to pane",
1179 key
1180 );
1181
1182 assert!(
1184 layout.mode().is_focus(),
1185 "Key {:?} should not exit focus mode",
1186 key
1187 );
1188
1189 let _pane = layout
1191 .active_tab()
1192 .unwrap()
1193 .pane_container()
1194 .get_pane(pane_id)
1195 .unwrap();
1196
1197 }
1201 }
1202
1203 #[test]
1204 fn test_q_quits_only_in_layout_mode() {
1205 let mut layout = create_test_layout();
1207
1208 assert!(layout.mode().is_layout());
1210 let key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty());
1211 let result = layout.handle_key_event(key);
1212 assert_eq!(result, EventResult::Quit, "'q' should quit in Layout Mode");
1213
1214 let mut layout = create_test_layout();
1216 let pane_id = layout
1217 .active_tab()
1218 .unwrap()
1219 .pane_container()
1220 .get_pane_by_index(0)
1221 .unwrap()
1222 .id();
1223
1224 layout.enter_focus_mode(pane_id);
1225 assert!(layout.mode().is_focus());
1226
1227 let key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty());
1228 let result = layout.handle_key_event(key);
1229 assert_eq!(
1230 result,
1231 EventResult::Consumed,
1232 "'q' should be consumed by pane in Focus Mode, NOT quit app"
1233 );
1234
1235 assert!(
1237 layout.mode().is_focus(),
1238 "Should still be in Focus Mode after 'q'"
1239 );
1240 }
1241
1242 #[test]
1243 fn test_q_in_focus_mode_regression() {
1244 let mut layout = create_test_layout();
1248
1249 let pane_id = layout
1251 .active_tab()
1252 .unwrap()
1253 .pane_container()
1254 .get_pane_by_index(0)
1255 .unwrap()
1256 .id();
1257 layout.enter_focus_mode(pane_id);
1258
1259 assert!(layout.mode().is_focus(), "Should be in Focus Mode");
1261
1262 let q_key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty());
1264 let result = layout.handle_key_event(q_key);
1265
1266 assert_ne!(
1268 result,
1269 EventResult::Quit,
1270 "REGRESSION: 'q' in Focus Mode should NOT quit the application"
1271 );
1272 assert_eq!(
1273 result,
1274 EventResult::Consumed,
1275 "'q' should be consumed by the focused pane"
1276 );
1277
1278 assert!(
1280 layout.mode().is_focus(),
1281 "Should still be in Focus Mode after pressing 'q'"
1282 );
1283
1284 for _ in 0..5 {
1286 let result = layout.handle_key_event(q_key);
1287 assert_eq!(
1288 result,
1289 EventResult::Consumed,
1290 "Multiple 'q' presses should all be consumed"
1291 );
1292 assert!(layout.mode().is_focus(), "Should remain in Focus Mode");
1293 }
1294
1295 assert!(
1297 layout.mode().is_focus(),
1298 "Should remain in Focus Mode after all 'q' presses"
1299 );
1300 }
1301
1302 #[test]
1303 fn test_uppercase_q_in_focus_mode() {
1304 let mut layout = create_test_layout();
1308
1309 let pane_id = layout
1311 .active_tab()
1312 .unwrap()
1313 .pane_container()
1314 .get_pane_by_index(0)
1315 .unwrap()
1316 .id();
1317 layout.enter_focus_mode(pane_id);
1318 assert!(layout.mode().is_focus());
1319
1320 let uppercase_q = KeyEvent::new(KeyCode::Char('Q'), KeyModifiers::empty());
1322 let result = layout.handle_key_event(uppercase_q);
1323
1324 assert_ne!(
1326 result,
1327 EventResult::Quit,
1328 "REGRESSION: Uppercase 'Q' (Shift+Q) in Focus Mode should NOT quit the application"
1329 );
1330 assert_eq!(
1331 result,
1332 EventResult::Consumed,
1333 "Uppercase 'Q' should be consumed by the focused pane"
1334 );
1335
1336 assert!(
1338 layout.mode().is_focus(),
1339 "Should still be in Focus Mode after pressing 'Q'"
1340 );
1341 }
1342
1343 #[test]
1344 fn test_uppercase_q_quits_in_layout_mode() {
1345 let mut layout = create_test_layout();
1347 assert!(layout.mode().is_layout());
1348
1349 let lowercase_q = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty());
1351 let result = layout.handle_key_event(lowercase_q);
1352 assert_eq!(
1353 result,
1354 EventResult::Quit,
1355 "Lowercase 'q' should quit in Layout Mode"
1356 );
1357
1358 let mut layout = create_test_layout();
1360 let uppercase_q = KeyEvent::new(KeyCode::Char('Q'), KeyModifiers::empty());
1361 let result = layout.handle_key_event(uppercase_q);
1362 assert_eq!(
1363 result,
1364 EventResult::Quit,
1365 "Uppercase 'Q' should quit in Layout Mode"
1366 );
1367 }
1368
1369 #[test]
1370 fn test_render_does_not_panic() {
1371 let mut layout = create_test_layout();
1372
1373 let backend = TestBackend::new(80, 24);
1374 let mut terminal = Terminal::new(backend).unwrap();
1375
1376 terminal
1377 .draw(|frame| {
1378 layout.render(frame);
1379 })
1380 .unwrap();
1381 }
1382
1383 #[test]
1384 fn test_empty_layout_renders() {
1385 let mut layout = MasterLayout::new();
1386
1387 let backend = TestBackend::new(80, 24);
1388 let mut terminal = Terminal::new(backend).unwrap();
1389
1390 terminal
1391 .draw(|frame| {
1392 layout.render(frame);
1393 })
1394 .unwrap();
1395 }
1396
1397 #[test]
1398 fn test_tab_switching_preserves_mode() {
1399 let mut layout = MasterLayout::new();
1400
1401 let mut tab1 = Tab::new("Tab 1");
1403 tab1.add_pane(Pane::new(
1404 PaneId::new("t1p1"),
1405 Box::new(MockContent::new("T1P1")),
1406 ));
1407 layout.add_tab(tab1);
1408
1409 let mut tab2 = Tab::new("Tab 2");
1410 tab2.add_pane(Pane::new(
1411 PaneId::new("t2p1"),
1412 Box::new(MockContent::new("T2P1")),
1413 ));
1414 layout.add_tab(tab2);
1415
1416 layout.set_active_tab(1);
1418
1419 assert!(layout.mode().is_layout());
1421 assert!(layout.mode().selected_pane().is_some());
1422 }
1423
1424 #[test]
1425 fn test_no_panes_no_selection() {
1426 let mut layout = MasterLayout::new();
1427 let tab = Tab::new("Empty Tab");
1428 layout.add_tab(tab);
1429
1430 assert!(layout.mode().is_layout());
1432 }
1433
1434 #[test]
1435 fn test_event_handling_resize() {
1436 let mut layout = create_test_layout();
1437
1438 let event = Event::Resize(100, 50);
1439 let result = layout.handle_event(event);
1440
1441 assert_eq!(result, EventResult::Consumed);
1442 }
1443
1444 #[test]
1445 fn test_select_next_with_no_tabs() {
1446 let mut layout = MasterLayout::new();
1447 layout.select_next_pane();
1448 }
1450
1451 #[test]
1452 fn test_focus_selected_with_no_selection() {
1453 let mut layout = MasterLayout::new();
1454 let tab = Tab::new("Empty Tab");
1455 layout.add_tab(tab);
1456
1457 layout.focus_selected();
1459 assert!(layout.mode().is_layout());
1461 }
1462
1463 #[test]
1464 fn test_esc_deselects_in_layout_mode() {
1465 let mut layout = create_test_layout();
1466
1467 assert!(layout.mode().is_layout());
1469 assert!(layout.mode().selected_pane().is_some());
1470
1471 let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
1473 let result = layout.handle_key_event(key);
1474
1475 assert_eq!(result, EventResult::Consumed);
1477
1478 assert!(layout.mode().is_layout());
1480
1481 assert_eq!(layout.mode().selected_pane(), None);
1483 }
1484
1485 #[test]
1486 fn test_mouse_click_selects_pane_in_layout_mode() {
1487 let mut layout = create_test_layout();
1488
1489 assert!(layout.mode().is_layout());
1491 let initial_selection = layout.mode().selected_pane();
1492 assert!(initial_selection.is_some());
1493
1494 let _pane2 = layout
1496 .active_tab()
1497 .unwrap()
1498 .pane_container()
1499 .get_pane_by_index(1)
1500 .unwrap()
1501 .id();
1502
1503 let backend = TestBackend::new(80, 24);
1505 let mut terminal = Terminal::new(backend).unwrap();
1506 terminal
1507 .draw(|frame| {
1508 layout.render(frame);
1509 })
1510 .unwrap();
1511
1512 let mouse = MouseEvent {
1514 kind: MouseEventKind::Down(MouseButton::Left),
1515 column: 10,
1516 row: 5,
1517 modifiers: KeyModifiers::empty(),
1518 };
1519
1520 layout.handle_mouse_event(mouse);
1521
1522 assert!(
1525 layout.mode().is_layout(),
1526 "Mode should still be Layout Mode after mouse click in Layout Mode"
1527 );
1528
1529 assert!(
1531 layout.mode().focused_pane().is_none(),
1532 "No pane should be focused in Layout Mode"
1533 );
1534
1535 let current_selection = layout.mode().selected_pane();
1537 assert!(
1538 current_selection.is_some(),
1539 "A pane should be selected after mouse click"
1540 );
1541 }
1542
1543 #[test]
1544 fn test_keyboard_navigation_then_enter_focuses() {
1545 let mut layout = create_test_layout();
1546
1547 assert!(layout.mode().is_layout());
1549 let first_pane = layout.mode().selected_pane();
1550 assert!(first_pane.is_some());
1551
1552 let backend = TestBackend::new(80, 24);
1554 let mut terminal = Terminal::new(backend).unwrap();
1555 terminal
1556 .draw(|frame| {
1557 layout.render(frame);
1558 })
1559 .unwrap();
1560
1561 let key = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::empty());
1563 let result = layout.handle_key_event(key);
1564 assert_eq!(result, EventResult::Consumed);
1565
1566 let second_pane = layout.mode().selected_pane();
1568 assert!(second_pane.is_some());
1569 assert_ne!(
1570 first_pane, second_pane,
1571 "Second pane should be different from first"
1572 );
1573 assert!(layout.mode().is_layout(), "Should still be in Layout Mode");
1574
1575 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
1577 let result = layout.handle_key_event(key);
1578 assert_eq!(result, EventResult::Consumed);
1579
1580 assert!(
1582 layout.mode().is_focus(),
1583 "Should be in Focus Mode after Enter"
1584 );
1585 assert_eq!(
1586 layout.mode().focused_pane(),
1587 second_pane,
1588 "Second pane should be focused"
1589 );
1590 }
1591
1592 #[test]
1593 fn test_ctrl_a_deselects_in_layout_mode() {
1594 let mut layout = create_test_layout();
1595
1596 assert!(layout.mode().is_layout());
1598 assert!(layout.mode().selected_pane().is_some());
1599
1600 let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
1602 let result = layout.handle_key_event(key);
1603
1604 assert_eq!(result, EventResult::Consumed);
1606
1607 assert!(layout.mode().is_layout());
1609
1610 assert_eq!(layout.mode().selected_pane(), None);
1612 }
1613
1614 #[test]
1615 fn test_click_different_pane_in_focus_mode_switches() {
1616 let mut layout = create_test_layout();
1617
1618 let pane1_id = layout
1620 .active_tab()
1621 .unwrap()
1622 .pane_container()
1623 .get_pane_by_index(0)
1624 .unwrap()
1625 .id();
1626
1627 let pane2_id = layout
1628 .active_tab()
1629 .unwrap()
1630 .pane_container()
1631 .get_pane_by_index(1)
1632 .unwrap()
1633 .id();
1634
1635 layout.enter_focus_mode(pane1_id);
1637 assert!(layout.mode().is_focus());
1638 assert_eq!(layout.mode().focused_pane(), Some(pane1_id));
1639
1640 let backend = TestBackend::new(80, 24);
1642 let mut terminal = Terminal::new(backend).unwrap();
1643 terminal
1644 .draw(|frame| {
1645 layout.render(frame);
1646 })
1647 .unwrap();
1648
1649 let mouse = MouseEvent {
1651 kind: MouseEventKind::Down(MouseButton::Left),
1652 column: 50,
1653 row: 12,
1654 modifiers: KeyModifiers::empty(),
1655 };
1656
1657 let result = layout.handle_mouse_event(mouse);
1658 assert_eq!(result, EventResult::Consumed);
1659
1660 assert!(layout.mode().is_focus());
1662
1663 assert_eq!(layout.mode().focused_pane(), Some(pane2_id));
1665 }
1666
1667 #[test]
1668 fn test_click_same_pane_in_focus_mode_maintains() {
1669 let mut layout = create_test_layout();
1670
1671 let pane1_id = layout
1673 .active_tab()
1674 .unwrap()
1675 .pane_container()
1676 .get_pane_by_index(0)
1677 .unwrap()
1678 .id();
1679
1680 layout.enter_focus_mode(pane1_id);
1682 assert!(layout.mode().is_focus());
1683 assert_eq!(layout.mode().focused_pane(), Some(pane1_id));
1684
1685 let backend = TestBackend::new(80, 24);
1687 let mut terminal = Terminal::new(backend).unwrap();
1688 terminal
1689 .draw(|frame| {
1690 layout.render(frame);
1691 })
1692 .unwrap();
1693
1694 let mouse = MouseEvent {
1696 kind: MouseEventKind::Down(MouseButton::Left),
1697 column: 10,
1698 row: 8,
1699 modifiers: KeyModifiers::empty(),
1700 };
1701
1702 let result = layout.handle_mouse_event(mouse);
1703 assert_eq!(result, EventResult::Consumed);
1704
1705 assert!(layout.mode().is_focus());
1707
1708 assert_eq!(layout.mode().focused_pane(), Some(pane1_id));
1710 }
1711
1712 #[test]
1713 fn test_hjkl_navigation_blocked_in_focus_mode() {
1714 let mut layout = create_test_layout();
1715
1716 let pane1_id = layout
1717 .active_tab()
1718 .unwrap()
1719 .pane_container()
1720 .get_pane_by_index(0)
1721 .unwrap()
1722 .id();
1723
1724 let pane2_id = layout
1725 .active_tab()
1726 .unwrap()
1727 .pane_container()
1728 .get_pane_by_index(1)
1729 .unwrap()
1730 .id();
1731
1732 assert_ne!(pane1_id, pane2_id);
1734
1735 layout.enter_focus_mode(pane1_id);
1737 assert!(layout.mode().is_focus());
1738 assert_eq!(layout.mode().focused_pane(), Some(pane1_id));
1739
1740 let key = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::empty());
1742 let result = layout.handle_key_event(key);
1743
1744 assert_eq!(result, EventResult::Consumed);
1746
1747 assert!(layout.mode().is_focus(), "Should still be in Focus Mode");
1749 assert_eq!(
1750 layout.mode().focused_pane(),
1751 Some(pane1_id),
1752 "Should still be focused on first pane (navigation not allowed in Focus Mode)"
1753 );
1754 }
1755}