1use super::{InteractionMode, Pane, PaneId, PaneLayout};
4use crate::resizable_split::{ResizableSplit, SplitDirection};
5use ratatui::layout::Rect;
6
7pub struct PaneContainer {
9 pub(crate) panes: Vec<Pane>,
10 layout: PaneLayout,
11 resizable_splits: Vec<ResizableSplit>,
14 container_area: Rect,
16}
17
18impl PaneContainer {
19 pub fn new(layout: PaneLayout) -> Self {
21 Self {
22 panes: Vec::new(),
23 layout,
24 resizable_splits: Vec::new(),
25 container_area: Rect::default(),
26 }
27 }
28
29 fn init_resizable_splits(&mut self) {
31 self.resizable_splits.clear();
32
33 match &self.layout {
34 PaneLayout::Horizontal(percentages) => {
35 let divider_count = self.panes.len().saturating_sub(1);
37
38 for i in 0..divider_count {
39 let split_percent = if i < percentages.len() {
41 percentages.iter().take(i + 1).copied().sum::<u16>()
43 } else {
44 (((i + 1) * 100) / self.panes.len()) as u16
46 };
47
48 self.resizable_splits
49 .push(ResizableSplit::new_with_direction(
50 split_percent,
51 SplitDirection::Vertical,
52 ));
53 }
54 }
55 PaneLayout::Vertical(percentages) => {
56 let divider_count = self.panes.len().saturating_sub(1);
57
58 for i in 0..divider_count {
59 let split_percent = if i < percentages.len() {
60 percentages.iter().take(i + 1).copied().sum::<u16>()
61 } else {
62 (((i + 1) * 100) / self.panes.len()) as u16
63 };
64
65 self.resizable_splits
66 .push(ResizableSplit::new_with_direction(
67 split_percent,
68 SplitDirection::Horizontal,
69 ));
70 }
71 }
72 _ => {
73 }
75 }
76 }
77
78 pub fn add_pane(&mut self, pane: Pane) {
80 self.panes.push(pane);
81 self.init_resizable_splits();
83 }
84
85 pub fn pane_count(&self) -> usize {
87 self.panes.len()
88 }
89
90 pub fn get_pane(&self, id: PaneId) -> Option<&Pane> {
92 self.panes.iter().find(|p| p.id() == id)
93 }
94
95 pub fn get_pane_mut(&mut self, id: PaneId) -> Option<&mut Pane> {
97 self.panes.iter_mut().find(|p| p.id() == id)
98 }
99
100 pub fn get_pane_by_index(&self, index: usize) -> Option<&Pane> {
102 self.panes.get(index)
103 }
104
105 pub fn get_pane_by_index_mut(&mut self, index: usize) -> Option<&mut Pane> {
107 self.panes.get_mut(index)
108 }
109
110 pub fn find_pane_at(&self, x: u16, y: u16) -> Option<PaneId> {
112 self.panes
113 .iter()
114 .find(|pane| pane.contains_point(x, y))
115 .map(|pane| pane.id())
116 }
117
118 pub fn update_layout(&mut self, available_area: Rect) {
120 self.container_area = available_area;
121
122 for split in &mut self.resizable_splits {
124 split.update_divider_position(available_area);
125 }
126
127 let areas = if self.resizable_splits.is_empty() {
128 self.layout
130 .calculate_areas(available_area, self.panes.len())
131 } else {
132 self.calculate_areas_from_splits(available_area)
134 };
135
136 for (pane, area) in self.panes.iter_mut().zip(areas.iter()) {
137 pane.set_area(*area);
138 }
139 }
140
141 fn calculate_areas_from_splits(&self, available_area: Rect) -> Vec<Rect> {
143 if self.panes.is_empty() {
144 return Vec::new();
145 }
146
147 let mut areas = Vec::new();
148
149 match &self.layout {
150 PaneLayout::Horizontal(_) => {
151 let mut last_x = available_area.x;
153
154 for (i, _pane_idx) in (0..self.panes.len()).enumerate() {
155 let next_x = if i < self.resizable_splits.len() {
156 self.resizable_splits[i].divider_pos
158 } else {
159 available_area.x + available_area.width
161 };
162
163 let width = next_x.saturating_sub(last_x);
164 areas.push(Rect::new(
165 last_x,
166 available_area.y,
167 width,
168 available_area.height,
169 ));
170
171 last_x = next_x;
172 }
173 }
174 PaneLayout::Vertical(_) => {
175 let mut last_y = available_area.y;
177
178 for (i, _pane_idx) in (0..self.panes.len()).enumerate() {
179 let next_y = if i < self.resizable_splits.len() {
180 self.resizable_splits[i].divider_pos
182 } else {
183 available_area.y + available_area.height
185 };
186
187 let height = next_y.saturating_sub(last_y);
188 areas.push(Rect::new(
189 available_area.x,
190 last_y,
191 available_area.width,
192 height,
193 ));
194
195 last_y = next_y;
196 }
197 }
198 _ => {
199 return self
201 .layout
202 .calculate_areas(available_area, self.panes.len());
203 }
204 }
205
206 areas
207 }
208
209 pub fn select_next(&self, current: Option<PaneId>) -> Option<PaneId> {
211 if self.panes.is_empty() {
212 return None;
213 }
214
215 let focusable_panes: Vec<_> = self.panes.iter().filter(|p| p.is_focusable()).collect();
216
217 if focusable_panes.is_empty() {
218 return None;
219 }
220
221 match current {
222 None => Some(focusable_panes[0].id()),
223 Some(current_id) => {
224 let current_idx = focusable_panes.iter().position(|p| p.id() == current_id);
225
226 match current_idx {
227 Some(idx) => {
228 let next_idx = (idx + 1) % focusable_panes.len();
229 Some(focusable_panes[next_idx].id())
230 }
231 None => Some(focusable_panes[0].id()),
232 }
233 }
234 }
235 }
236
237 pub fn select_prev(&self, current: Option<PaneId>) -> Option<PaneId> {
239 if self.panes.is_empty() {
240 return None;
241 }
242
243 let focusable_panes: Vec<_> = self.panes.iter().filter(|p| p.is_focusable()).collect();
244
245 if focusable_panes.is_empty() {
246 return None;
247 }
248
249 match current {
250 None => Some(focusable_panes[focusable_panes.len() - 1].id()),
251 Some(current_id) => {
252 let current_idx = focusable_panes.iter().position(|p| p.id() == current_id);
253
254 match current_idx {
255 Some(idx) => {
256 let prev_idx = if idx == 0 {
257 focusable_panes.len() - 1
258 } else {
259 idx - 1
260 };
261 Some(focusable_panes[prev_idx].id())
262 }
263 None => Some(focusable_panes[0].id()),
264 }
265 }
266 }
267 }
268
269 pub fn select_left(&self, current: PaneId) -> Option<PaneId> {
271 self.select_directional(current, Direction::Left)
272 }
273
274 pub fn select_right(&self, current: PaneId) -> Option<PaneId> {
276 self.select_directional(current, Direction::Right)
277 }
278
279 pub fn select_up(&self, current: PaneId) -> Option<PaneId> {
281 self.select_directional(current, Direction::Up)
282 }
283
284 pub fn select_down(&self, current: PaneId) -> Option<PaneId> {
286 self.select_directional(current, Direction::Down)
287 }
288
289 fn select_directional(&self, current: PaneId, direction: Direction) -> Option<PaneId> {
291 let current_pane = self.get_pane(current)?;
292 let current_area = current_pane.area();
293 let current_center = center_point(current_area);
294
295 let mut best_pane: Option<PaneId> = None;
297 let mut best_distance: f64 = f64::MAX;
298
299 for pane in &self.panes {
300 if !pane.is_focusable() || pane.id() == current {
301 continue;
302 }
303
304 let area = pane.area();
305 let center = center_point(area);
306
307 let in_direction = match direction {
309 Direction::Left => center.0 < current_center.0,
310 Direction::Right => center.0 > current_center.0,
311 Direction::Up => center.1 < current_center.1,
312 Direction::Down => center.1 > current_center.1,
313 };
314
315 if !in_direction {
316 continue;
317 }
318
319 let distance = distance_between(current_center, center);
321
322 if distance < best_distance {
323 best_distance = distance;
324 best_pane = Some(pane.id());
325 }
326 }
327
328 best_pane
329 }
330
331 pub fn render(&mut self, frame: &mut ratatui::Frame, mode: &InteractionMode) {
333 let selected_id = mode.selected_pane();
334 let focused_id = mode.focused_pane();
335
336 for pane in &mut self.panes {
337 let is_selected = selected_id == Some(pane.id());
338 let is_focused = focused_id == Some(pane.id());
339
340 pane.set_focused(is_focused);
342
343 pane.render(frame, is_selected, is_focused);
344 }
345 }
346
347 pub fn find_divider_at(&self, mouse_x: u16, mouse_y: u16) -> Option<usize> {
349 for (i, split) in self.resizable_splits.iter().enumerate() {
350 if split.is_on_divider(mouse_x, mouse_y, self.container_area) {
351 return Some(i);
352 }
353 }
354 None
355 }
356
357 pub fn start_drag(&mut self, divider_index: usize) {
359 if let Some(split) = self.resizable_splits.get_mut(divider_index) {
360 split.start_drag();
361 }
362 }
363
364 pub fn update_drag(&mut self, mouse_x: u16, mouse_y: u16) {
366 for split in &mut self.resizable_splits {
367 if split.is_dragging {
368 split.update_from_mouse(mouse_x, mouse_y, self.container_area);
369 }
370 }
371 }
372
373 pub fn stop_drag(&mut self) {
375 for split in &mut self.resizable_splits {
376 split.stop_drag();
377 }
378 }
379
380 pub fn is_dragging(&self) -> bool {
382 self.resizable_splits.iter().any(|s| s.is_dragging)
383 }
384
385 pub fn update_hover(&mut self, mouse_x: u16, mouse_y: u16) {
387 for split in &mut self.resizable_splits {
388 split.is_hovering = split.is_on_divider(mouse_x, mouse_y, self.container_area);
389 }
390 }
391
392 pub fn clear_hover(&mut self) {
394 for split in &mut self.resizable_splits {
395 split.is_hovering = false;
396 }
397 }
398}
399
400impl Default for PaneContainer {
401 fn default() -> Self {
402 Self::new(PaneLayout::default())
403 }
404}
405
406#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407enum Direction {
408 Left,
409 Right,
410 Up,
411 Down,
412}
413
414fn center_point(rect: Rect) -> (u16, u16) {
416 (rect.x + rect.width / 2, rect.y + rect.height / 2)
417}
418
419fn distance_between(p1: (u16, u16), p2: (u16, u16)) -> f64 {
421 let dx = (p2.0 as f64) - (p1.0 as f64);
422 let dy = (p2.1 as f64) - (p1.1 as f64);
423 (dx * dx + dy * dy).sqrt()
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429 use crossterm::event::{KeyEvent, MouseEvent};
430 use ratatui::{buffer::Buffer, widgets::Widget};
431
432 struct MockContent {
434 title: String,
435 focusable: bool,
436 }
437
438 impl MockContent {
439 fn new(title: &str) -> Self {
440 Self {
441 title: title.to_string(),
442 focusable: true,
443 }
444 }
445
446 fn non_focusable(title: &str) -> Self {
447 Self {
448 title: title.to_string(),
449 focusable: false,
450 }
451 }
452 }
453
454 impl Widget for MockContent {
455 fn render(self, _area: Rect, _buf: &mut Buffer) {}
456 }
457
458 impl super::super::pane::PaneContent for MockContent {
459 fn handle_key(&mut self, _key: KeyEvent) -> bool {
460 true
461 }
462 fn handle_mouse(&mut self, _mouse: MouseEvent) -> bool {
463 true
464 }
465 fn title(&self) -> String {
466 self.title.clone()
467 }
468 fn render_content(&mut self, _area: Rect, _frame: &mut ratatui::Frame) {
469 }
471 fn is_focusable(&self) -> bool {
472 self.focusable
473 }
474 }
475
476 #[test]
477 fn test_pane_container_creation() {
478 let container = PaneContainer::new(PaneLayout::default());
479 assert_eq!(container.pane_count(), 0);
480 }
481
482 #[test]
483 fn test_add_pane() {
484 let mut container = PaneContainer::default();
485 let pane = Pane::new(PaneId::new("test"), Box::new(MockContent::new("Test")));
486
487 container.add_pane(pane);
488 assert_eq!(container.pane_count(), 1);
489 }
490
491 #[test]
492 fn test_get_pane_by_id() {
493 let mut container = PaneContainer::default();
494 let pane_id = PaneId::new("test");
495 let pane = Pane::new(pane_id, Box::new(MockContent::new("Test")));
496
497 container.add_pane(pane);
498
499 assert!(container.get_pane(pane_id).is_some());
500 }
501
502 #[test]
503 fn test_select_next_empty() {
504 let container = PaneContainer::default();
505 assert_eq!(container.select_next(None), None);
506 }
507
508 #[test]
509 fn test_select_next_single_pane() {
510 let mut container = PaneContainer::default();
511 let id1 = PaneId::new("pane1");
512 container.add_pane(Pane::new(id1, Box::new(MockContent::new("Pane 1"))));
513
514 let next = container.select_next(None);
515 assert_eq!(next, Some(id1));
516
517 let next = container.select_next(Some(id1));
519 assert_eq!(next, Some(id1));
520 }
521
522 #[test]
523 fn test_select_next_multiple_panes() {
524 let mut container = PaneContainer::default();
525 let id1 = PaneId::new("pane1");
526 let id2 = PaneId::new("pane2");
527 let id3 = PaneId::new("pane3");
528
529 container.add_pane(Pane::new(id1, Box::new(MockContent::new("Pane 1"))));
530 container.add_pane(Pane::new(id2, Box::new(MockContent::new("Pane 2"))));
531 container.add_pane(Pane::new(id3, Box::new(MockContent::new("Pane 3"))));
532
533 let next = container.select_next(None);
534 assert_eq!(next, Some(id1));
535
536 let next = container.select_next(Some(id1));
537 assert_eq!(next, Some(id2));
538
539 let next = container.select_next(Some(id2));
540 assert_eq!(next, Some(id3));
541
542 let next = container.select_next(Some(id3));
544 assert_eq!(next, Some(id1));
545 }
546
547 #[test]
548 fn test_select_prev_multiple_panes() {
549 let mut container = PaneContainer::default();
550 let id1 = PaneId::new("pane1");
551 let id2 = PaneId::new("pane2");
552 let id3 = PaneId::new("pane3");
553
554 container.add_pane(Pane::new(id1, Box::new(MockContent::new("Pane 1"))));
555 container.add_pane(Pane::new(id2, Box::new(MockContent::new("Pane 2"))));
556 container.add_pane(Pane::new(id3, Box::new(MockContent::new("Pane 3"))));
557
558 let prev = container.select_prev(None);
559 assert_eq!(prev, Some(id3)); let prev = container.select_prev(Some(id3));
562 assert_eq!(prev, Some(id2));
563
564 let prev = container.select_prev(Some(id2));
565 assert_eq!(prev, Some(id1));
566
567 let prev = container.select_prev(Some(id1));
569 assert_eq!(prev, Some(id3));
570 }
571
572 #[test]
573 fn test_skip_non_focusable_panes() {
574 let mut container = PaneContainer::default();
575 let id1 = PaneId::new("pane1");
576 let id2 = PaneId::new("pane2");
577 let id3 = PaneId::new("pane3");
578
579 container.add_pane(Pane::new(id1, Box::new(MockContent::new("Pane 1"))));
580 container.add_pane(Pane::new(
581 id2,
582 Box::new(MockContent::non_focusable("Status")),
583 ));
584 container.add_pane(Pane::new(id3, Box::new(MockContent::new("Pane 3"))));
585
586 let next = container.select_next(Some(id1));
588 assert_eq!(next, Some(id3));
589
590 let prev = container.select_prev(Some(id3));
591 assert_eq!(prev, Some(id1));
592 }
593
594 #[test]
595 fn test_find_pane_at() {
596 let mut container = PaneContainer::default();
597 let id1 = PaneId::new("pane1");
598 let id2 = PaneId::new("pane2");
599
600 let mut pane1 = Pane::new(id1, Box::new(MockContent::new("Pane 1")));
601 pane1.set_area(Rect::new(0, 0, 40, 20));
602
603 let mut pane2 = Pane::new(id2, Box::new(MockContent::new("Pane 2")));
604 pane2.set_area(Rect::new(40, 0, 40, 20));
605
606 container.add_pane(pane1);
607 container.add_pane(pane2);
608
609 assert_eq!(container.find_pane_at(20, 10), Some(id1));
610 assert_eq!(container.find_pane_at(60, 10), Some(id2));
611 assert_eq!(container.find_pane_at(100, 10), None);
612 }
613
614 #[test]
615 fn test_directional_navigation_horizontal() {
616 let mut container = PaneContainer::default();
617 let id1 = PaneId::new("left");
618 let id2 = PaneId::new("right");
619
620 let mut pane1 = Pane::new(id1, Box::new(MockContent::new("Left")));
621 pane1.set_area(Rect::new(0, 0, 40, 20));
622
623 let mut pane2 = Pane::new(id2, Box::new(MockContent::new("Right")));
624 pane2.set_area(Rect::new(40, 0, 40, 20));
625
626 container.add_pane(pane1);
627 container.add_pane(pane2);
628
629 assert_eq!(container.select_right(id1), Some(id2));
631 assert_eq!(container.select_left(id2), Some(id1));
633 assert_eq!(container.select_right(id2), None);
635 assert_eq!(container.select_left(id1), None);
637 }
638
639 #[test]
640 fn test_update_layout() {
641 let mut container = PaneContainer::new(PaneLayout::Horizontal(vec![50, 50]));
642 let id1 = PaneId::new("pane1");
643 let id2 = PaneId::new("pane2");
644
645 container.add_pane(Pane::new(id1, Box::new(MockContent::new("Pane 1"))));
646 container.add_pane(Pane::new(id2, Box::new(MockContent::new("Pane 2"))));
647
648 container.update_layout(Rect::new(0, 0, 100, 50));
649
650 let pane1 = container.get_pane(id1).unwrap();
651 let pane2 = container.get_pane(id2).unwrap();
652
653 assert_ne!(pane1.area(), Rect::default());
654 assert_ne!(pane2.area(), Rect::default());
655 }
656
657 #[test]
658 fn test_directional_navigation_vertical() {
659 let mut container = PaneContainer::default();
660 let id1 = PaneId::new("top");
661 let id2 = PaneId::new("bottom");
662
663 let mut pane1 = Pane::new(id1, Box::new(MockContent::new("Top")));
664 pane1.set_area(Rect::new(0, 0, 40, 20));
665
666 let mut pane2 = Pane::new(id2, Box::new(MockContent::new("Bottom")));
667 pane2.set_area(Rect::new(0, 20, 40, 20));
668
669 container.add_pane(pane1);
670 container.add_pane(pane2);
671
672 assert_eq!(container.select_down(id1), Some(id2));
674 assert_eq!(container.select_up(id2), Some(id1));
676 assert_eq!(container.select_down(id2), None);
678 assert_eq!(container.select_up(id1), None);
680 }
681
682 #[test]
683 fn test_get_pane_by_index() {
684 let mut container = PaneContainer::default();
685 let id1 = PaneId::new("pane1");
686 let id2 = PaneId::new("pane2");
687
688 container.add_pane(Pane::new(id1, Box::new(MockContent::new("Pane 1"))));
689 container.add_pane(Pane::new(id2, Box::new(MockContent::new("Pane 2"))));
690
691 assert_eq!(container.get_pane_by_index(0).unwrap().id(), id1);
692 assert_eq!(container.get_pane_by_index(1).unwrap().id(), id2);
693 assert!(container.get_pane_by_index(2).is_none());
694 }
695
696 #[test]
697 fn test_get_pane_mut() {
698 let mut container = PaneContainer::default();
699 let id1 = PaneId::new("pane1");
700
701 container.add_pane(Pane::new(id1, Box::new(MockContent::new("Pane 1"))));
702
703 let pane = container.get_pane_mut(id1);
704 assert!(pane.is_some());
705
706 let non_existent = PaneId::new("nonexistent");
707 assert!(container.get_pane_mut(non_existent).is_none());
708 }
709
710 #[test]
711 fn test_resizable_splits_initialized() {
712 let mut container = PaneContainer::new(PaneLayout::Horizontal(vec![50, 50]));
713 let id1 = PaneId::new("pane1");
714 let id2 = PaneId::new("pane2");
715
716 container.add_pane(Pane::new(id1, Box::new(MockContent::new("Pane 1"))));
717 container.add_pane(Pane::new(id2, Box::new(MockContent::new("Pane 2"))));
718
719 assert_eq!(container.resizable_splits.len(), 1);
721 }
722
723 #[test]
724 fn test_find_divider_at() {
725 let mut container = PaneContainer::new(PaneLayout::Horizontal(vec![50, 50]));
726 container.add_pane(Pane::new(
727 PaneId::new("p1"),
728 Box::new(MockContent::new("P1")),
729 ));
730 container.add_pane(Pane::new(
731 PaneId::new("p2"),
732 Box::new(MockContent::new("P2")),
733 ));
734
735 container.update_layout(Rect::new(0, 0, 100, 50));
737
738 assert!(container.find_divider_at(50, 25).is_some());
741 assert!(container.find_divider_at(49, 25).is_some());
742 assert!(container.find_divider_at(51, 25).is_some());
743
744 assert!(container.find_divider_at(10, 25).is_none());
746 assert!(container.find_divider_at(90, 25).is_none());
747 }
748
749 #[test]
750 fn test_drag_start_stop() {
751 let mut container = PaneContainer::new(PaneLayout::Horizontal(vec![50, 50]));
752 container.add_pane(Pane::new(
753 PaneId::new("p1"),
754 Box::new(MockContent::new("P1")),
755 ));
756 container.add_pane(Pane::new(
757 PaneId::new("p2"),
758 Box::new(MockContent::new("P2")),
759 ));
760
761 assert!(!container.is_dragging());
762
763 container.start_drag(0);
764 assert!(container.is_dragging());
765
766 container.stop_drag();
767 assert!(!container.is_dragging());
768 }
769
770 #[test]
771 fn test_drag_updates_split() {
772 let mut container = PaneContainer::new(PaneLayout::Horizontal(vec![50, 50]));
773 container.add_pane(Pane::new(
774 PaneId::new("p1"),
775 Box::new(MockContent::new("P1")),
776 ));
777 container.add_pane(Pane::new(
778 PaneId::new("p2"),
779 Box::new(MockContent::new("P2")),
780 ));
781
782 let area = Rect::new(0, 0, 100, 50);
783 container.update_layout(area);
784
785 container.start_drag(0);
787
788 container.update_drag(70, 25);
790
791 container.update_layout(area);
793
794 assert!(container.resizable_splits[0].split_percent > 50);
796 assert!(container.resizable_splits[0].split_percent <= 70);
797 }
798
799 #[test]
800 fn test_vertical_layout_resizable() {
801 let mut container = PaneContainer::new(PaneLayout::Vertical(vec![50, 50]));
802 container.add_pane(Pane::new(
803 PaneId::new("p1"),
804 Box::new(MockContent::new("P1")),
805 ));
806 container.add_pane(Pane::new(
807 PaneId::new("p2"),
808 Box::new(MockContent::new("P2")),
809 ));
810
811 assert_eq!(container.resizable_splits.len(), 1);
813
814 container.update_layout(Rect::new(0, 0, 100, 100));
816
817 assert!(container.find_divider_at(50, 50).is_some());
819 }
820
821 #[test]
822 fn test_three_panes_two_dividers() {
823 let mut container = PaneContainer::new(PaneLayout::Horizontal(vec![33, 33, 34]));
824 container.add_pane(Pane::new(
825 PaneId::new("p1"),
826 Box::new(MockContent::new("P1")),
827 ));
828 container.add_pane(Pane::new(
829 PaneId::new("p2"),
830 Box::new(MockContent::new("P2")),
831 ));
832 container.add_pane(Pane::new(
833 PaneId::new("p3"),
834 Box::new(MockContent::new("P3")),
835 ));
836
837 assert_eq!(container.resizable_splits.len(), 2);
839 }
840
841 #[test]
842 fn test_grid_layout_no_resizing() {
843 let mut container = PaneContainer::new(PaneLayout::Grid { rows: 2, cols: 2 });
844 container.add_pane(Pane::new(
845 PaneId::new("p1"),
846 Box::new(MockContent::new("P1")),
847 ));
848 container.add_pane(Pane::new(
849 PaneId::new("p2"),
850 Box::new(MockContent::new("P2")),
851 ));
852
853 assert_eq!(container.resizable_splits.len(), 0);
855 }
856}