Skip to main content

fresh/view/
split.rs

1/// Split view system for displaying multiple buffers simultaneously
2///
3/// Design Philosophy (following Emacs model):
4/// - A split is a tree structure: either a leaf (single buffer) or a node (horizontal/vertical split)
5/// - Each split has a fixed size (in percentage or absolute lines/columns)
6/// - Splits can be nested arbitrarily deep
7/// - Only one split is "active" at a time (receives input)
8/// - Splits can display the same buffer multiple times (useful for viewing different parts)
9///
10/// Example split layouts:
11/// ```text
12/// ┌────────────────────┐      ┌──────────┬─────────┐
13/// │                    │      │          │         │
14/// │   Single buffer    │      │  Buffer  │ Buffer  │
15/// │                    │      │    A     │    B    │
16/// └────────────────────┘      └──────────┴─────────┘
17///   (no split)                  (vertical split)
18///
19/// ┌────────────────────┐      ┌──────────┬─────────┐
20/// │     Buffer A       │      │          │ Buffer C│
21/// ├────────────────────┤      │  Buffer  ├─────────┤
22/// │     Buffer B       │      │    A     │ Buffer D│
23/// └────────────────────┘      └──────────┴─────────┘
24///  (horizontal split)          (mixed splits)
25/// ```
26use crate::model::cursor::Cursors;
27use crate::model::event::{BufferId, SplitDirection, SplitId};
28use crate::view::ui::view_pipeline::Layout;
29use crate::view::viewport::Viewport;
30use crate::{services::plugins::api::ViewTransformPayload, state::ViewMode};
31use ratatui::layout::Rect;
32use serde::{Deserialize, Serialize};
33
34/// A node in the split tree
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub enum SplitNode {
37    /// Leaf node: displays a single buffer
38    Leaf {
39        /// Which buffer to display
40        buffer_id: BufferId,
41        /// Unique ID for this split pane
42        split_id: SplitId,
43    },
44    /// Internal node: contains two child splits
45    Split {
46        /// Direction of the split
47        direction: SplitDirection,
48        /// First child (top or left)
49        first: Box<Self>,
50        /// Second child (bottom or right)
51        second: Box<Self>,
52        /// Size ratio (0.0 to 1.0) - how much space the first child gets
53        /// 0.5 = equal split, 0.3 = first gets 30%, etc.
54        ratio: f32,
55        /// Unique ID for this split container
56        split_id: SplitId,
57    },
58}
59
60/// Per-split view state (independent of buffer content)
61///
62/// Following the Emacs model where each window (split) has its own:
63/// - Point (cursor position) - independent per split
64/// - Window-start (scroll position) - independent per split
65/// - Tabs (open buffers) - independent per split
66///
67/// This allows multiple splits to display the same buffer at different positions
68/// with independent cursor and scroll positions, and each split has its own set of tabs.
69#[derive(Debug, Clone)]
70pub struct SplitViewState {
71    /// Independent cursor set for this split (supports multi-cursor)
72    pub cursors: Cursors,
73
74    /// Independent scroll position for this split
75    pub viewport: Viewport,
76
77    /// List of buffer IDs open in this split's tab bar (in order)
78    /// The currently displayed buffer is tracked in the SplitNode::Leaf
79    pub open_buffers: Vec<BufferId>,
80
81    /// Horizontal scroll offset for the tabs in this split
82    pub tab_scroll_offset: usize,
83
84    /// View mode (Source/Compose) per split
85    pub view_mode: ViewMode,
86
87    /// Optional compose width for centering/wrapping in this split
88    pub compose_width: Option<u16>,
89
90    /// Column guides for this split (e.g., tables)
91    pub compose_column_guides: Option<Vec<u16>>,
92
93    /// Previously configured line number visibility (restored when leaving Compose)
94    pub compose_prev_line_numbers: Option<bool>,
95
96    /// Optional view transform payload for this split/viewport
97    pub view_transform: Option<ViewTransformPayload>,
98
99    /// Computed layout for this view (from view_transform or base tokens)
100    /// This is View state - each split has its own Layout
101    pub layout: Option<Layout>,
102
103    /// Whether the layout needs to be rebuilt (buffer changed, transform changed, etc.)
104    pub layout_dirty: bool,
105
106    /// Focus history stack for this split (most recent at end)
107    /// Used for "Switch to Previous Tab" and for returning to previous buffer when closing
108    pub focus_history: Vec<BufferId>,
109
110    /// Sync group ID for synchronized scrolling
111    /// Splits with the same sync_group will scroll together
112    pub sync_group: Option<u32>,
113
114    /// When set, this split renders a composite view (e.g., side-by-side diff).
115    /// The split's buffer_id is the focused source buffer, but rendering uses
116    /// the composite layout. This makes the source buffer the "active buffer"
117    /// so normal keybindings work directly.
118    pub composite_view: Option<BufferId>,
119}
120
121impl SplitViewState {
122    /// Create a new split view state with default cursor at position 0
123    pub fn new(width: u16, height: u16) -> Self {
124        Self {
125            cursors: Cursors::new(),
126            viewport: Viewport::new(width, height),
127            open_buffers: Vec::new(),
128            tab_scroll_offset: 0,
129            view_mode: ViewMode::Source,
130            compose_width: None,
131            compose_column_guides: None,
132            compose_prev_line_numbers: None,
133            view_transform: None,
134            layout: None,
135            layout_dirty: true, // Start dirty so first operation builds layout
136            focus_history: Vec::new(),
137            sync_group: None,
138            composite_view: None,
139        }
140    }
141
142    /// Create a new split view state with an initial buffer open
143    pub fn with_buffer(width: u16, height: u16, buffer_id: BufferId) -> Self {
144        Self {
145            cursors: Cursors::new(),
146            viewport: Viewport::new(width, height),
147            open_buffers: vec![buffer_id],
148            tab_scroll_offset: 0,
149            view_mode: ViewMode::Source,
150            compose_width: None,
151            compose_column_guides: None,
152            compose_prev_line_numbers: None,
153            view_transform: None,
154            layout: None,
155            layout_dirty: true, // Start dirty so first operation builds layout
156            focus_history: Vec::new(),
157            sync_group: None,
158            composite_view: None,
159        }
160    }
161
162    /// Mark layout as needing rebuild (call after buffer changes)
163    pub fn invalidate_layout(&mut self) {
164        self.layout_dirty = true;
165    }
166
167    /// Ensure layout is valid, rebuilding if needed.
168    /// Returns the Layout - never returns None. Following VSCode's ViewModel pattern.
169    ///
170    /// # Arguments
171    /// * `tokens` - ViewTokenWire array (from view_transform or built from buffer)
172    /// * `source_range` - The byte range this layout covers
173    /// * `tab_size` - Tab width for rendering
174    pub fn ensure_layout(
175        &mut self,
176        tokens: &[fresh_core::api::ViewTokenWire],
177        source_range: std::ops::Range<usize>,
178        tab_size: usize,
179    ) -> &Layout {
180        if self.layout.is_none() || self.layout_dirty {
181            self.layout = Some(Layout::from_tokens(tokens, source_range, tab_size));
182            self.layout_dirty = false;
183        }
184        self.layout.as_ref().unwrap()
185    }
186
187    /// Get the current layout if it exists and is valid
188    pub fn get_layout(&self) -> Option<&Layout> {
189        if self.layout_dirty {
190            None
191        } else {
192            self.layout.as_ref()
193        }
194    }
195
196    /// Add a buffer to this split's tabs (if not already present)
197    pub fn add_buffer(&mut self, buffer_id: BufferId) {
198        if !self.open_buffers.contains(&buffer_id) {
199            self.open_buffers.push(buffer_id);
200        }
201    }
202
203    /// Remove a buffer from this split's tabs
204    pub fn remove_buffer(&mut self, buffer_id: BufferId) {
205        self.open_buffers.retain(|&id| id != buffer_id);
206    }
207
208    /// Check if a buffer is open in this split
209    pub fn has_buffer(&self, buffer_id: BufferId) -> bool {
210        self.open_buffers.contains(&buffer_id)
211    }
212
213    /// Push a buffer to the focus history (LRU-style)
214    /// If the buffer is already in history, it's moved to the end
215    pub fn push_focus(&mut self, buffer_id: BufferId) {
216        // Remove if already in history (LRU-style)
217        self.focus_history.retain(|&id| id != buffer_id);
218        self.focus_history.push(buffer_id);
219        // Limit to 50 entries
220        if self.focus_history.len() > 50 {
221            self.focus_history.remove(0);
222        }
223    }
224
225    /// Get the most recently focused buffer (without removing it)
226    pub fn previous_buffer(&self) -> Option<BufferId> {
227        self.focus_history.last().copied()
228    }
229
230    /// Pop the most recent buffer from focus history
231    pub fn pop_focus(&mut self) -> Option<BufferId> {
232        self.focus_history.pop()
233    }
234
235    /// Remove a buffer from the focus history (called when buffer is closed)
236    pub fn remove_from_history(&mut self, buffer_id: BufferId) {
237        self.focus_history.retain(|&id| id != buffer_id);
238    }
239}
240
241impl SplitNode {
242    /// Create a new leaf node
243    pub fn leaf(buffer_id: BufferId, split_id: SplitId) -> Self {
244        Self::Leaf {
245            buffer_id,
246            split_id,
247        }
248    }
249
250    /// Create a new split node with two children
251    pub fn split(
252        direction: SplitDirection,
253        first: SplitNode,
254        second: SplitNode,
255        ratio: f32,
256        split_id: SplitId,
257    ) -> Self {
258        SplitNode::Split {
259            direction,
260            first: Box::new(first),
261            second: Box::new(second),
262            ratio: ratio.clamp(0.1, 0.9), // Prevent extreme ratios
263            split_id,
264        }
265    }
266
267    /// Get the split ID for this node
268    pub fn id(&self) -> SplitId {
269        match self {
270            Self::Leaf { split_id, .. } | Self::Split { split_id, .. } => *split_id,
271        }
272    }
273
274    /// Get the buffer ID if this is a leaf node
275    pub fn buffer_id(&self) -> Option<BufferId> {
276        match self {
277            Self::Leaf { buffer_id, .. } => Some(*buffer_id),
278            Self::Split { .. } => None,
279        }
280    }
281
282    /// Find a split by ID (returns mutable reference)
283    pub fn find_mut(&mut self, target_id: SplitId) -> Option<&mut Self> {
284        if self.id() == target_id {
285            return Some(self);
286        }
287
288        match self {
289            Self::Leaf { .. } => None,
290            Self::Split { first, second, .. } => first
291                .find_mut(target_id)
292                .or_else(|| second.find_mut(target_id)),
293        }
294    }
295
296    /// Find a split by ID (returns immutable reference)
297    pub fn find(&self, target_id: SplitId) -> Option<&Self> {
298        if self.id() == target_id {
299            return Some(self);
300        }
301
302        match self {
303            Self::Leaf { .. } => None,
304            Self::Split { first, second, .. } => {
305                first.find(target_id).or_else(|| second.find(target_id))
306            }
307        }
308    }
309
310    /// Get all leaf nodes (buffer views) with their rectangles
311    pub fn get_leaves_with_rects(&self, rect: Rect) -> Vec<(SplitId, BufferId, Rect)> {
312        match self {
313            Self::Leaf {
314                buffer_id,
315                split_id,
316            } => {
317                vec![(*split_id, *buffer_id, rect)]
318            }
319            Self::Split {
320                direction,
321                first,
322                second,
323                ratio,
324                ..
325            } => {
326                let (first_rect, second_rect) = split_rect(rect, *direction, *ratio);
327                let mut leaves = first.get_leaves_with_rects(first_rect);
328                leaves.extend(second.get_leaves_with_rects(second_rect));
329                leaves
330            }
331        }
332    }
333
334    /// Get all split separator lines (for rendering borders)
335    /// Returns (direction, x, y, length) tuples
336    pub fn get_separators(&self, rect: Rect) -> Vec<(SplitDirection, u16, u16, u16)> {
337        self.get_separators_with_ids(rect)
338            .into_iter()
339            .map(|(_, dir, x, y, len)| (dir, x, y, len))
340            .collect()
341    }
342
343    /// Get all split separator lines with their split IDs (for mouse hit testing)
344    /// Returns (split_id, direction, x, y, length) tuples
345    pub fn get_separators_with_ids(
346        &self,
347        rect: Rect,
348    ) -> Vec<(SplitId, SplitDirection, u16, u16, u16)> {
349        match self {
350            Self::Leaf { .. } => vec![],
351            Self::Split {
352                direction,
353                first,
354                second,
355                ratio,
356                split_id,
357            } => {
358                let (first_rect, second_rect) = split_rect(rect, *direction, *ratio);
359                let mut separators = Vec::new();
360
361                // Add separator for this split (in the 1-char gap between first and second)
362                match direction {
363                    SplitDirection::Horizontal => {
364                        // Horizontal split: separator line is between first and second
365                        // y position is at the end of first rect (the gap line)
366                        separators.push((
367                            *split_id,
368                            SplitDirection::Horizontal,
369                            rect.x,
370                            first_rect.y + first_rect.height,
371                            rect.width,
372                        ));
373                    }
374                    SplitDirection::Vertical => {
375                        // Vertical split: separator line is between first and second
376                        // x position is at the end of first rect (the gap column)
377                        separators.push((
378                            *split_id,
379                            SplitDirection::Vertical,
380                            first_rect.x + first_rect.width,
381                            rect.y,
382                            rect.height,
383                        ));
384                    }
385                }
386
387                // Recursively get separators from children
388                separators.extend(first.get_separators_with_ids(first_rect));
389                separators.extend(second.get_separators_with_ids(second_rect));
390                separators
391            }
392        }
393    }
394
395    /// Collect all split IDs in the tree
396    pub fn all_split_ids(&self) -> Vec<SplitId> {
397        let mut ids = vec![self.id()];
398        match self {
399            Self::Leaf { .. } => ids,
400            Self::Split { first, second, .. } => {
401                ids.extend(first.all_split_ids());
402                ids.extend(second.all_split_ids());
403                ids
404            }
405        }
406    }
407
408    /// Collect only leaf split IDs (visible buffer splits, not container nodes)
409    pub fn leaf_split_ids(&self) -> Vec<SplitId> {
410        match self {
411            Self::Leaf { split_id, .. } => vec![*split_id],
412            Self::Split { first, second, .. } => {
413                let mut ids = first.leaf_split_ids();
414                ids.extend(second.leaf_split_ids());
415                ids
416            }
417        }
418    }
419
420    /// Count the number of leaf nodes (visible buffers)
421    pub fn count_leaves(&self) -> usize {
422        match self {
423            Self::Leaf { .. } => 1,
424            Self::Split { first, second, .. } => first.count_leaves() + second.count_leaves(),
425        }
426    }
427}
428
429/// Split a rectangle into two parts based on direction and ratio
430/// Leaves 1 character space for the separator line between splits
431fn split_rect(rect: Rect, direction: SplitDirection, ratio: f32) -> (Rect, Rect) {
432    match direction {
433        SplitDirection::Horizontal => {
434            // Split into top and bottom, with 1 line for separator
435            let total_height = rect.height.saturating_sub(1); // Reserve 1 line for separator
436            let first_height = (total_height as f32 * ratio).round() as u16;
437            let second_height = total_height.saturating_sub(first_height);
438
439            let first = Rect {
440                x: rect.x,
441                y: rect.y,
442                width: rect.width,
443                height: first_height,
444            };
445
446            let second = Rect {
447                x: rect.x,
448                y: rect.y + first_height + 1, // +1 for separator
449                width: rect.width,
450                height: second_height,
451            };
452
453            (first, second)
454        }
455        SplitDirection::Vertical => {
456            // Split into left and right, with 1 column for separator
457            let total_width = rect.width.saturating_sub(1); // Reserve 1 column for separator
458            let first_width = (total_width as f32 * ratio).round() as u16;
459            let second_width = total_width.saturating_sub(first_width);
460
461            let first = Rect {
462                x: rect.x,
463                y: rect.y,
464                width: first_width,
465                height: rect.height,
466            };
467
468            let second = Rect {
469                x: rect.x + first_width + 1, // +1 for separator
470                y: rect.y,
471                width: second_width,
472                height: rect.height,
473            };
474
475            (first, second)
476        }
477    }
478}
479
480/// Manager for the split view system
481#[derive(Debug)]
482pub struct SplitManager {
483    /// Root of the split tree
484    root: SplitNode,
485
486    /// Currently active split (receives input)
487    active_split: SplitId,
488
489    /// Next split ID to assign
490    next_split_id: usize,
491
492    /// Currently maximized split (if any). When set, only this split is visible.
493    maximized_split: Option<SplitId>,
494}
495
496impl SplitManager {
497    /// Create a new split manager with a single buffer
498    pub fn new(buffer_id: BufferId) -> Self {
499        let split_id = SplitId(0);
500        Self {
501            root: SplitNode::leaf(buffer_id, split_id),
502            active_split: split_id,
503            next_split_id: 1,
504            maximized_split: None,
505        }
506    }
507
508    /// Get the root split node
509    pub fn root(&self) -> &SplitNode {
510        &self.root
511    }
512
513    /// Get the currently active split ID
514    pub fn active_split(&self) -> SplitId {
515        self.active_split
516    }
517
518    /// Set the active split
519    pub fn set_active_split(&mut self, split_id: SplitId) -> bool {
520        // Verify the split exists
521        if self.root.find(split_id).is_some() {
522            self.active_split = split_id;
523            true
524        } else {
525            false
526        }
527    }
528
529    /// Get the buffer ID of the active split (if it's a leaf)
530    pub fn active_buffer_id(&self) -> Option<BufferId> {
531        self.root
532            .find(self.active_split)
533            .and_then(|node| node.buffer_id())
534    }
535
536    /// Get the buffer ID for a specific split (if it's a leaf)
537    pub fn get_buffer_id(&self, split_id: SplitId) -> Option<BufferId> {
538        self.root.find(split_id).and_then(|node| node.buffer_id())
539    }
540
541    /// Update the buffer ID of the active split
542    /// Returns true if successful (active split is a leaf), false otherwise
543    pub fn set_active_buffer_id(&mut self, new_buffer_id: BufferId) -> bool {
544        if let Some(SplitNode::Leaf { buffer_id, .. }) = self.root.find_mut(self.active_split) {
545            *buffer_id = new_buffer_id;
546            return true;
547        }
548        false
549    }
550
551    /// Update the buffer ID of a specific split
552    /// Returns Ok(()) if successful, Err with message if split not found or not a leaf
553    pub fn set_split_buffer(
554        &mut self,
555        split_id: SplitId,
556        new_buffer_id: BufferId,
557    ) -> Result<(), String> {
558        if let Some(node) = self.root.find_mut(split_id) {
559            if let SplitNode::Leaf { buffer_id, .. } = node {
560                *buffer_id = new_buffer_id;
561                return Ok(());
562            }
563            return Err(format!("Split {:?} is not a leaf", split_id));
564        }
565        Err(format!("Split {:?} not found", split_id))
566    }
567
568    /// Allocate a new split ID
569    fn allocate_split_id(&mut self) -> SplitId {
570        let id = SplitId(self.next_split_id);
571        self.next_split_id += 1;
572        id
573    }
574
575    /// Split the currently active pane
576    pub fn split_active(
577        &mut self,
578        direction: SplitDirection,
579        new_buffer_id: BufferId,
580        ratio: f32,
581    ) -> Result<SplitId, String> {
582        let active_id = self.active_split;
583
584        // Find the parent of the active split
585        let result = self.replace_split_with_split(active_id, direction, new_buffer_id, ratio);
586
587        if let Ok(new_split_id) = result {
588            // Set the new split as active
589            self.active_split = new_split_id;
590            Ok(new_split_id)
591        } else {
592            result
593        }
594    }
595
596    /// Replace a split with a new split container
597    fn replace_split_with_split(
598        &mut self,
599        target_id: SplitId,
600        direction: SplitDirection,
601        new_buffer_id: BufferId,
602        ratio: f32,
603    ) -> Result<SplitId, String> {
604        // Pre-allocate all IDs before any borrowing
605        let temp_id = self.allocate_split_id();
606        let new_split_id = self.allocate_split_id();
607        let new_leaf_id = self.allocate_split_id();
608
609        // Special case: if target is root, replace root
610        if self.root.id() == target_id {
611            let old_root =
612                std::mem::replace(&mut self.root, SplitNode::leaf(new_buffer_id, temp_id));
613
614            self.root = SplitNode::split(
615                direction,
616                old_root,
617                SplitNode::leaf(new_buffer_id, new_leaf_id),
618                ratio,
619                new_split_id,
620            );
621
622            return Ok(new_leaf_id);
623        }
624
625        // Find and replace the target node
626        if let Some(node) = self.root.find_mut(target_id) {
627            let old_node = std::mem::replace(node, SplitNode::leaf(new_buffer_id, temp_id));
628
629            *node = SplitNode::split(
630                direction,
631                old_node,
632                SplitNode::leaf(new_buffer_id, new_leaf_id),
633                ratio,
634                new_split_id,
635            );
636
637            Ok(new_leaf_id)
638        } else {
639            Err(format!("Split {:?} not found", target_id))
640        }
641    }
642
643    /// Close a split pane (if not the last one)
644    pub fn close_split(&mut self, split_id: SplitId) -> Result<(), String> {
645        // Can't close if it's the only split
646        if self.root.count_leaves() <= 1 {
647            return Err("Cannot close the last split".to_string());
648        }
649
650        // Can't close if it's the root and root is a leaf
651        if self.root.id() == split_id && self.root.buffer_id().is_some() {
652            return Err("Cannot close the only split".to_string());
653        }
654
655        // If the split being closed is maximized, unmaximize first
656        if self.maximized_split == Some(split_id) {
657            self.maximized_split = None;
658        }
659
660        // Find the parent of the split to close
661        // This requires a parent-tracking traversal
662        let result = self.remove_split_node(split_id);
663
664        // If we closed the active split, update active_split to another split
665        if result.is_ok() && self.active_split == split_id {
666            let leaf_ids = self.root.leaf_split_ids();
667            if let Some(&first_leaf) = leaf_ids.first() {
668                self.active_split = first_leaf;
669            }
670        }
671
672        result
673    }
674
675    /// Remove a split node from the tree
676    fn remove_split_node(&mut self, target_id: SplitId) -> Result<(), String> {
677        // Special case: removing root
678        if self.root.id() == target_id {
679            if let SplitNode::Split { first, .. } = &self.root {
680                // Replace root with the other child
681                // Choose first child arbitrarily
682                self.root = (**first).clone();
683                return Ok(());
684            }
685        }
686
687        // Recursively find and remove
688        Self::remove_child_static(&mut self.root, target_id)
689    }
690
691    /// Helper to remove a child from a split node (static to avoid borrow issues)
692    fn remove_child_static(node: &mut SplitNode, target_id: SplitId) -> Result<(), String> {
693        match node {
694            SplitNode::Leaf { .. } => Err("Target not found".to_string()),
695            SplitNode::Split { first, second, .. } => {
696                // Check if either child is the target
697                if first.id() == target_id {
698                    // Replace this node with the second child
699                    *node = (**second).clone();
700                    Ok(())
701                } else if second.id() == target_id {
702                    // Replace this node with the first child
703                    *node = (**first).clone();
704                    Ok(())
705                } else {
706                    // Recurse into children
707                    Self::remove_child_static(first, target_id)
708                        .or_else(|_| Self::remove_child_static(second, target_id))
709                }
710            }
711        }
712    }
713
714    /// Adjust the split ratio of a container
715    pub fn adjust_ratio(&mut self, split_id: SplitId, delta: f32) -> Result<(), String> {
716        if let Some(node) = self.root.find_mut(split_id) {
717            if let SplitNode::Split { ratio, .. } = node {
718                *ratio = (*ratio + delta).clamp(0.1, 0.9);
719                Ok(())
720            } else {
721                Err("Target is not a split container".to_string())
722            }
723        } else {
724            Err("Split not found".to_string())
725        }
726    }
727
728    /// Get all visible buffer views with their rectangles
729    pub fn get_visible_buffers(&self, viewport_rect: Rect) -> Vec<(SplitId, BufferId, Rect)> {
730        // If a split is maximized, only show that split taking up the full viewport
731        if let Some(maximized_id) = self.maximized_split {
732            if let Some(node) = self.root.find(maximized_id) {
733                if let Some(buffer_id) = node.buffer_id() {
734                    return vec![(maximized_id, buffer_id, viewport_rect)];
735                }
736            }
737            // Maximized split no longer exists, clear it and fall through
738        }
739        self.root.get_leaves_with_rects(viewport_rect)
740    }
741
742    /// Get all split separator positions for rendering borders
743    /// Returns (direction, x, y, length) tuples
744    pub fn get_separators(&self, viewport_rect: Rect) -> Vec<(SplitDirection, u16, u16, u16)> {
745        // No separators when a split is maximized
746        if self.maximized_split.is_some() {
747            return vec![];
748        }
749        self.root.get_separators(viewport_rect)
750    }
751
752    /// Get all split separator positions with their split IDs (for mouse hit testing)
753    /// Returns (split_id, direction, x, y, length) tuples
754    pub fn get_separators_with_ids(
755        &self,
756        viewport_rect: Rect,
757    ) -> Vec<(SplitId, SplitDirection, u16, u16, u16)> {
758        // No separators when a split is maximized
759        if self.maximized_split.is_some() {
760            return vec![];
761        }
762        self.root.get_separators_with_ids(viewport_rect)
763    }
764
765    /// Get the current ratio of a split container
766    pub fn get_ratio(&self, split_id: SplitId) -> Option<f32> {
767        if let Some(SplitNode::Split { ratio, .. }) = self.root.find(split_id) {
768            Some(*ratio)
769        } else {
770            None
771        }
772    }
773
774    /// Set the exact ratio of a split container
775    pub fn set_ratio(&mut self, split_id: SplitId, new_ratio: f32) -> Result<(), String> {
776        if let Some(node) = self.root.find_mut(split_id) {
777            if let SplitNode::Split { ratio, .. } = node {
778                *ratio = new_ratio.clamp(0.1, 0.9);
779                Ok(())
780            } else {
781                Err("Target is not a split container".to_string())
782            }
783        } else {
784            Err("Split not found".to_string())
785        }
786    }
787
788    /// Distribute all visible splits evenly
789    /// This sets the ratios of all container splits so that leaf splits get equal space
790    pub fn distribute_splits_evenly(&mut self) {
791        Self::distribute_node_evenly(&mut self.root);
792    }
793
794    /// Recursively distribute a node's splits evenly
795    /// Returns the number of leaves in this subtree
796    fn distribute_node_evenly(node: &mut SplitNode) -> usize {
797        match node {
798            SplitNode::Leaf { .. } => 1,
799            SplitNode::Split {
800                first,
801                second,
802                ratio,
803                ..
804            } => {
805                let first_leaves = Self::distribute_node_evenly(first);
806                let second_leaves = Self::distribute_node_evenly(second);
807                let total_leaves = first_leaves + second_leaves;
808
809                // Set ratio so each leaf gets equal space
810                // ratio = proportion for first pane
811                *ratio = (first_leaves as f32 / total_leaves as f32).clamp(0.1, 0.9);
812
813                total_leaves
814            }
815        }
816    }
817
818    /// Navigate to the next split (circular)
819    pub fn next_split(&mut self) {
820        let leaf_ids = self.root.leaf_split_ids();
821        if let Some(pos) = leaf_ids.iter().position(|id| *id == self.active_split) {
822            let next_pos = (pos + 1) % leaf_ids.len();
823            self.active_split = leaf_ids[next_pos];
824        }
825    }
826
827    /// Navigate to the previous split (circular)
828    pub fn prev_split(&mut self) {
829        let leaf_ids = self.root.leaf_split_ids();
830        if let Some(pos) = leaf_ids.iter().position(|id| *id == self.active_split) {
831            let prev_pos = if pos == 0 { leaf_ids.len() } else { pos } - 1;
832            self.active_split = leaf_ids[prev_pos];
833        }
834    }
835
836    /// Get all split IDs that display a specific buffer
837    pub fn splits_for_buffer(&self, target_buffer_id: BufferId) -> Vec<SplitId> {
838        self.root
839            .get_leaves_with_rects(Rect {
840                x: 0,
841                y: 0,
842                width: 1,
843                height: 1,
844            })
845            .into_iter()
846            .filter(|(_, buffer_id, _)| *buffer_id == target_buffer_id)
847            .map(|(split_id, _, _)| split_id)
848            .collect()
849    }
850
851    /// Get the buffer ID for a specific split
852    pub fn buffer_for_split(&self, target_split_id: SplitId) -> Option<BufferId> {
853        self.root
854            .get_leaves_with_rects(Rect {
855                x: 0,
856                y: 0,
857                width: 1,
858                height: 1,
859            })
860            .into_iter()
861            .find(|(split_id, _, _)| *split_id == target_split_id)
862            .map(|(_, buffer_id, _)| buffer_id)
863    }
864
865    /// Maximize the active split (hide all other splits temporarily)
866    /// Returns Ok(()) if successful, Err if there's only one split
867    pub fn maximize_split(&mut self) -> Result<(), String> {
868        // Can't maximize if there's only one split
869        if self.root.count_leaves() <= 1 {
870            return Err("Cannot maximize: only one split exists".to_string());
871        }
872
873        // Can't maximize if already maximized
874        if self.maximized_split.is_some() {
875            return Err("A split is already maximized".to_string());
876        }
877
878        // Maximize the active split
879        self.maximized_split = Some(self.active_split);
880        Ok(())
881    }
882
883    /// Unmaximize the currently maximized split (restore all splits)
884    /// Returns Ok(()) if successful, Err if no split is maximized
885    pub fn unmaximize_split(&mut self) -> Result<(), String> {
886        if self.maximized_split.is_none() {
887            return Err("No split is maximized".to_string());
888        }
889
890        self.maximized_split = None;
891        Ok(())
892    }
893
894    /// Check if a split is currently maximized
895    pub fn is_maximized(&self) -> bool {
896        self.maximized_split.is_some()
897    }
898
899    /// Get the currently maximized split ID (if any)
900    pub fn maximized_split(&self) -> Option<SplitId> {
901        self.maximized_split
902    }
903
904    /// Toggle maximize state for the active split
905    /// If maximized, unmaximize. If not maximized, maximize.
906    /// Returns true if maximized, false if ununmaximized.
907    pub fn toggle_maximize(&mut self) -> Result<bool, String> {
908        if self.is_maximized() {
909            self.unmaximize_split()?;
910            Ok(false)
911        } else {
912            self.maximize_split()?;
913            Ok(true)
914        }
915    }
916
917    /// Get all leaf split IDs that belong to a specific sync group
918    pub fn get_splits_in_group(
919        &self,
920        group_id: u32,
921        view_states: &std::collections::HashMap<SplitId, SplitViewState>,
922    ) -> Vec<SplitId> {
923        self.root
924            .leaf_split_ids()
925            .into_iter()
926            .filter(|id| {
927                view_states
928                    .get(id)
929                    .and_then(|vs| vs.sync_group)
930                    .is_some_and(|g| g == group_id)
931            })
932            .collect()
933    }
934}
935
936#[cfg(test)]
937mod tests {
938    use super::*;
939
940    #[test]
941    fn test_create_split_manager() {
942        let buffer_id = BufferId(0);
943        let manager = SplitManager::new(buffer_id);
944
945        assert_eq!(manager.active_buffer_id(), Some(buffer_id));
946        assert_eq!(manager.root().count_leaves(), 1);
947    }
948
949    #[test]
950    fn test_horizontal_split() {
951        let buffer_a = BufferId(0);
952        let buffer_b = BufferId(1);
953
954        let mut manager = SplitManager::new(buffer_a);
955        let result = manager.split_active(SplitDirection::Horizontal, buffer_b, 0.5);
956
957        assert!(result.is_ok());
958        assert_eq!(manager.root().count_leaves(), 2);
959    }
960
961    #[test]
962    fn test_vertical_split() {
963        let buffer_a = BufferId(0);
964        let buffer_b = BufferId(1);
965
966        let mut manager = SplitManager::new(buffer_a);
967        let result = manager.split_active(SplitDirection::Vertical, buffer_b, 0.5);
968
969        assert!(result.is_ok());
970        assert_eq!(manager.root().count_leaves(), 2);
971    }
972
973    #[test]
974    fn test_nested_splits() {
975        let buffer_a = BufferId(0);
976        let buffer_b = BufferId(1);
977        let buffer_c = BufferId(2);
978
979        let mut manager = SplitManager::new(buffer_a);
980
981        // Split horizontally
982        manager
983            .split_active(SplitDirection::Horizontal, buffer_b, 0.5)
984            .unwrap();
985
986        // Split the second pane vertically
987        manager
988            .split_active(SplitDirection::Vertical, buffer_c, 0.5)
989            .unwrap();
990
991        assert_eq!(manager.root().count_leaves(), 3);
992    }
993
994    #[test]
995    fn test_close_split() {
996        let buffer_a = BufferId(0);
997        let buffer_b = BufferId(1);
998
999        let mut manager = SplitManager::new(buffer_a);
1000        let new_split = manager
1001            .split_active(SplitDirection::Horizontal, buffer_b, 0.5)
1002            .unwrap();
1003
1004        assert_eq!(manager.root().count_leaves(), 2);
1005
1006        // Close the new split
1007        let result = manager.close_split(new_split);
1008        assert!(result.is_ok());
1009        assert_eq!(manager.root().count_leaves(), 1);
1010    }
1011
1012    #[test]
1013    fn test_cannot_close_last_split() {
1014        let buffer_a = BufferId(0);
1015        let mut manager = SplitManager::new(buffer_a);
1016
1017        let result = manager.close_split(manager.active_split());
1018        assert!(result.is_err());
1019    }
1020
1021    #[test]
1022    fn test_split_rect_horizontal() {
1023        let rect = Rect {
1024            x: 0,
1025            y: 0,
1026            width: 100,
1027            height: 100,
1028        };
1029
1030        let (first, second) = split_rect(rect, SplitDirection::Horizontal, 0.5);
1031
1032        // With 1 line reserved for separator: (100-1)/2 = 49.5 rounds to 50 and 49
1033        assert_eq!(first.height, 50);
1034        assert_eq!(second.height, 49);
1035        assert_eq!(first.width, 100);
1036        assert_eq!(second.width, 100);
1037        assert_eq!(first.y, 0);
1038        assert_eq!(second.y, 51); // first.y + first.height + 1 (separator)
1039    }
1040
1041    #[test]
1042    fn test_split_rect_vertical() {
1043        let rect = Rect {
1044            x: 0,
1045            y: 0,
1046            width: 100,
1047            height: 100,
1048        };
1049
1050        let (first, second) = split_rect(rect, SplitDirection::Vertical, 0.5);
1051
1052        // With 1 column reserved for separator: (100-1)/2 = 49.5 rounds to 50 and 49
1053        assert_eq!(first.width, 50);
1054        assert_eq!(second.width, 49);
1055        assert_eq!(first.height, 100);
1056        assert_eq!(second.height, 100);
1057        assert_eq!(first.x, 0);
1058        assert_eq!(second.x, 51); // first.x + first.width + 1 (separator)
1059    }
1060}