ratatui_toolkit/master_layout/
pane_container.rs

1//! Pane container with selection and focus management
2
3use super::{InteractionMode, Pane, PaneId, PaneLayout};
4use crate::resizable_split::{ResizableSplit, SplitDirection};
5use ratatui::layout::Rect;
6
7/// Container for managing panes within a tab
8pub struct PaneContainer {
9    pub(crate) panes: Vec<Pane>,
10    layout: PaneLayout,
11    /// Resizable splits for horizontal/vertical layouts (one per divider)
12    /// For 2 panes there's 1 divider, for 3 panes there's 2 dividers, etc.
13    resizable_splits: Vec<ResizableSplit>,
14    /// The area where panes are rendered (updated each frame)
15    container_area: Rect,
16}
17
18impl PaneContainer {
19    /// Create a new pane container with the given layout
20    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    /// Initialize resizable splits based on current layout and pane count
30    fn init_resizable_splits(&mut self) {
31        self.resizable_splits.clear();
32
33        match &self.layout {
34            PaneLayout::Horizontal(percentages) => {
35                // For N panes, we need N-1 dividers
36                let divider_count = self.panes.len().saturating_sub(1);
37
38                for i in 0..divider_count {
39                    // Calculate the cumulative percentage up to this divider
40                    let split_percent = if i < percentages.len() {
41                        // Sum up percentages up to and including this pane
42                        percentages.iter().take(i + 1).copied().sum::<u16>()
43                    } else {
44                        // Equal distribution
45                        (((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                // Grid and Custom layouts don't support resizing (for now)
74            }
75        }
76    }
77
78    /// Add a pane to the container
79    pub fn add_pane(&mut self, pane: Pane) {
80        self.panes.push(pane);
81        // Reinitialize splits when panes change
82        self.init_resizable_splits();
83    }
84
85    /// Get number of panes
86    pub fn pane_count(&self) -> usize {
87        self.panes.len()
88    }
89
90    /// Get pane by ID
91    pub fn get_pane(&self, id: PaneId) -> Option<&Pane> {
92        self.panes.iter().find(|p| p.id() == id)
93    }
94
95    /// Get mutable pane by ID
96    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    /// Get pane by index
101    pub fn get_pane_by_index(&self, index: usize) -> Option<&Pane> {
102        self.panes.get(index)
103    }
104
105    /// Get mutable pane by index
106    pub fn get_pane_by_index_mut(&mut self, index: usize) -> Option<&mut Pane> {
107        self.panes.get_mut(index)
108    }
109
110    /// Find pane at the given coordinates
111    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    /// Calculate and update pane areas based on layout
119    pub fn update_layout(&mut self, available_area: Rect) {
120        self.container_area = available_area;
121
122        // Update divider positions
123        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            // No resizable splits - use standard layout calculation
129            self.layout
130                .calculate_areas(available_area, self.panes.len())
131        } else {
132            // Calculate areas from resizable splits
133            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    /// Calculate pane areas from resizable splits
142    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                // Calculate areas for horizontal layout with vertical dividers
152                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                        // Use the split's divider position
157                        self.resizable_splits[i].divider_pos
158                    } else {
159                        // Last pane - extend to the end
160                        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                // Calculate areas for vertical layout with horizontal dividers
176                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                        // Use the split's divider position
181                        self.resizable_splits[i].divider_pos
182                    } else {
183                        // Last pane - extend to the end
184                        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                // Fallback to standard calculation
200                return self
201                    .layout
202                    .calculate_areas(available_area, self.panes.len());
203            }
204        }
205
206        areas
207    }
208
209    /// Select next pane (cycle forward)
210    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    /// Select previous pane (cycle backward)
238    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    /// Select pane to the left (h key)
270    pub fn select_left(&self, current: PaneId) -> Option<PaneId> {
271        self.select_directional(current, Direction::Left)
272    }
273
274    /// Select pane to the right (l key)
275    pub fn select_right(&self, current: PaneId) -> Option<PaneId> {
276        self.select_directional(current, Direction::Right)
277    }
278
279    /// Select pane above (k key)
280    pub fn select_up(&self, current: PaneId) -> Option<PaneId> {
281        self.select_directional(current, Direction::Up)
282    }
283
284    /// Select pane below (j key)
285    pub fn select_down(&self, current: PaneId) -> Option<PaneId> {
286        self.select_directional(current, Direction::Down)
287    }
288
289    /// Spatial navigation in a direction
290    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        // Find the closest focusable pane in the given direction
296        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            // Check if pane is in the correct direction
308            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            // Calculate distance
320            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    /// Render all panes
332    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            // Notify pane content about focus state
341            pane.set_focused(is_focused);
342
343            pane.render(frame, is_selected, is_focused);
344        }
345    }
346
347    /// Check if mouse is on any divider and return the split index
348    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    /// Start dragging a divider
358    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    /// Update drag position
365    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    /// Stop dragging all dividers
374    pub fn stop_drag(&mut self) {
375        for split in &mut self.resizable_splits {
376            split.stop_drag();
377        }
378    }
379
380    /// Check if any divider is currently being dragged
381    pub fn is_dragging(&self) -> bool {
382        self.resizable_splits.iter().any(|s| s.is_dragging)
383    }
384
385    /// Update hover state for dividers
386    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    /// Clear hover state for all dividers
393    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
414/// Get center point of a rectangle
415fn center_point(rect: Rect) -> (u16, u16) {
416    (rect.x + rect.width / 2, rect.y + rect.height / 2)
417}
418
419/// Calculate Euclidean distance between two points
420fn 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    // Mock PaneContent for testing
433    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            // Mock implementation - do nothing
470        }
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        // Cycling from id1 should return to id1
518        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        // Cycle back to first
543        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)); // Start from last
560
561        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        // Cycle back to last
568        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        // Should skip id2 (non-focusable)
587        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        // From left, go right
630        assert_eq!(container.select_right(id1), Some(id2));
631        // From right, go left
632        assert_eq!(container.select_left(id2), Some(id1));
633        // Can't go further right
634        assert_eq!(container.select_right(id2), None);
635        // Can't go further left
636        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        // From top, go down
673        assert_eq!(container.select_down(id1), Some(id2));
674        // From bottom, go up
675        assert_eq!(container.select_up(id2), Some(id1));
676        // Can't go further down
677        assert_eq!(container.select_down(id2), None);
678        // Can't go further up
679        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        // 2 panes should have 1 divider
720        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        // Update layout to calculate divider positions
736        container.update_layout(Rect::new(0, 0, 100, 50));
737
738        // Divider should be at column 50 (50% of 100)
739        // With 3-column hit area: 49, 50, 51
740        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        // Far from divider
745        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        // Start dragging
786        container.start_drag(0);
787
788        // Drag to 70% position (column 70)
789        container.update_drag(70, 25);
790
791        // Update layout to reflect new position
792        container.update_layout(area);
793
794        // Check that split percent changed
795        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        // 2 panes should have 1 divider
812        assert_eq!(container.resizable_splits.len(), 1);
813
814        // Update layout
815        container.update_layout(Rect::new(0, 0, 100, 100));
816
817        // Divider should be at row 50 (50% of 100)
818        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        // 3 panes should have 2 dividers
838        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        // Grid layout doesn't support resizing - no dividers
854        assert_eq!(container.resizable_splits.len(), 0);
855    }
856}