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::buffer::Buffer;
27use crate::model::cursor::Cursors;
28use crate::model::event::{BufferId, ContainerId, LeafId, SplitDirection, SplitId};
29use crate::model::marker::MarkerList;
30use crate::view::folding::FoldManager;
31use crate::view::ui::view_pipeline::Layout;
32use crate::view::viewport::Viewport;
33use crate::{services::plugins::api::ViewTransformPayload, state::ViewMode};
34use ratatui::layout::Rect;
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37
38/// A tab target — what a tab entry in a split's tab bar points to.
39///
40/// The tab bar contains a mix of regular buffer tabs and group tabs.
41/// Group tabs point to a `SplitNode::Grouped` node by its `LeafId`.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub enum TabTarget {
44    /// A regular buffer tab
45    Buffer(BufferId),
46    /// A buffer group tab — points to a `SplitNode::Grouped` node's `split_id`
47    Group(LeafId),
48}
49
50impl TabTarget {
51    pub fn as_buffer(self) -> Option<BufferId> {
52        match self {
53            Self::Buffer(id) => Some(id),
54            Self::Group(_) => None,
55        }
56    }
57
58    pub fn as_group(self) -> Option<LeafId> {
59        match self {
60            Self::Buffer(_) => None,
61            Self::Group(id) => Some(id),
62        }
63    }
64}
65
66/// A node in the split tree
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub enum SplitNode {
69    /// Leaf node: displays a single buffer
70    Leaf {
71        /// Which buffer to display
72        buffer_id: BufferId,
73        /// Unique ID for this split pane
74        split_id: LeafId,
75    },
76    /// Internal node: contains two child splits
77    Split {
78        /// Direction of the split
79        direction: SplitDirection,
80        /// First child (top or left)
81        first: Box<Self>,
82        /// Second child (bottom or right)
83        second: Box<Self>,
84        /// Size ratio (0.0 to 1.0) - how much space the first child gets
85        /// 0.5 = equal split, 0.3 = first gets 30%, etc.
86        ratio: f32,
87        /// Unique ID for this split container
88        split_id: ContainerId,
89        /// If set, first child gets exactly this many rows/cols instead of using ratio
90        #[serde(default)]
91        fixed_first: Option<u16>,
92        /// If set, second child gets exactly this many rows/cols instead of using ratio
93        #[serde(default)]
94        fixed_second: Option<u16>,
95    },
96    /// A grouped subtree that appears as a single tab entry in its parent
97    /// split's tab bar. When that tab is active, the subtree is expanded
98    /// and rendered inside the parent split's content area. When inactive,
99    /// the node is skipped during rect computation.
100    Grouped {
101        /// Unique ID used as a tab target (see `TabTarget::Group`).
102        /// Behaves like a `LeafId` — identifies this node uniquely.
103        split_id: LeafId,
104        /// Display name shown in the tab bar
105        name: String,
106        /// The nested layout to render when this tab is active
107        layout: Box<Self>,
108        /// The preferred active leaf within the layout (for focus when activating)
109        active_inner_leaf: LeafId,
110    },
111}
112
113/// Per-buffer view state within a split.
114///
115/// Each buffer opened in a split gets its own `BufferViewState` stored in the
116/// split's `keyed_states` map. This ensures that switching buffers within a split
117/// preserves cursor position, scroll state, view mode, and compose settings
118/// independently for each buffer.
119#[derive(Debug)]
120pub struct BufferViewState {
121    /// Independent cursor set (supports multi-cursor)
122    pub cursors: Cursors,
123
124    /// Independent scroll position
125    pub viewport: Viewport,
126
127    /// View mode (Source/Compose) for this buffer in this split
128    pub view_mode: ViewMode,
129
130    /// Optional compose width for centering/wrapping
131    pub compose_width: Option<u16>,
132
133    /// Column guides (e.g., tables)
134    pub compose_column_guides: Option<Vec<u16>>,
135
136    /// Vertical ruler positions (initialized from config, mutable per-buffer)
137    pub rulers: Vec<usize>,
138
139    /// Per-split line number visibility.
140    /// This is the single source of truth for whether line numbers are shown
141    /// in this split. Initialized from config when the split is created.
142    /// Compose mode forces this to false; leaving compose restores from config.
143    pub show_line_numbers: bool,
144
145    /// Per-split current line highlight visibility.
146    /// When true, the line containing the cursor gets a distinct background color.
147    /// Initialized from config when the split is created.
148    pub highlight_current_line: bool,
149
150    /// Optional view transform payload
151    pub view_transform: Option<ViewTransformPayload>,
152
153    /// True when the buffer was edited since the last view_transform_request hook fired.
154    /// While true, incoming SubmitViewTransform commands are rejected as stale
155    /// (their tokens have source_offsets from before the edit).
156    pub view_transform_stale: bool,
157
158    /// Plugin-managed state (arbitrary key-value pairs).
159    /// Plugins can store per-buffer-per-split state here via the `setViewState`/`getViewState` API.
160    /// Persisted across sessions via workspace serialization.
161    pub plugin_state: std::collections::HashMap<String, serde_json::Value>,
162
163    /// Collapsed folding ranges for this buffer/view.
164    pub folds: FoldManager,
165}
166
167impl BufferViewState {
168    /// Resolve fold ranges and ensure the primary cursor is visible.
169    ///
170    /// This is the preferred entry point for all non-rendering callers — it
171    /// resolves hidden fold byte ranges from the marker list and passes them
172    /// to `viewport.ensure_visible` so that line counting skips folded lines.
173    pub fn ensure_cursor_visible(&mut self, buffer: &mut Buffer, marker_list: &MarkerList) {
174        let hidden: Vec<(usize, usize)> = self
175            .folds
176            .resolved_ranges(buffer, marker_list)
177            .into_iter()
178            .map(|r| (r.start_byte, r.end_byte))
179            .collect();
180        let cursor = *self.cursors.primary();
181        self.viewport.ensure_visible(buffer, &cursor, &hidden);
182    }
183
184    /// Create a new buffer view state with defaults
185    pub fn new(width: u16, height: u16) -> Self {
186        Self {
187            cursors: Cursors::new(),
188            viewport: Viewport::new(width, height),
189            view_mode: ViewMode::Source,
190            compose_width: None,
191            compose_column_guides: None,
192            rulers: Vec::new(),
193            show_line_numbers: true,
194            highlight_current_line: true,
195            view_transform: None,
196            view_transform_stale: false,
197            plugin_state: std::collections::HashMap::new(),
198            folds: FoldManager::new(),
199        }
200    }
201
202    /// Apply editor config defaults for display settings.
203    ///
204    /// Sets `show_line_numbers`, `highlight_current_line`, `line_wrap`,
205    /// `wrap_column`, and `rulers` from the given config values. Call this after
206    /// creating a new `BufferViewState` (via `new()` or `ensure_buffer_state()`)
207    /// to ensure the view respects the user's settings.
208    pub fn apply_config_defaults(
209        &mut self,
210        line_numbers: bool,
211        highlight_current_line: bool,
212        line_wrap: bool,
213        wrap_indent: bool,
214        wrap_column: Option<usize>,
215        rulers: Vec<usize>,
216    ) {
217        self.show_line_numbers = line_numbers;
218        self.highlight_current_line = highlight_current_line;
219        self.viewport.line_wrap_enabled = line_wrap;
220        self.viewport.wrap_indent = wrap_indent;
221        self.viewport.wrap_column = wrap_column;
222        self.rulers = rulers;
223    }
224
225    /// Activate page view (compose mode) with an optional page width.
226    ///
227    /// This sets the view mode to Compose, disables builtin line wrap
228    /// (the compose plugin handles wrapping), hides line numbers,
229    /// and optionally sets the compose width for centering.
230    pub fn activate_page_view(&mut self, page_width: Option<usize>) {
231        self.view_mode = ViewMode::PageView;
232        self.show_line_numbers = false;
233        self.viewport.line_wrap_enabled = false;
234        if let Some(width) = page_width {
235            self.compose_width = Some(width as u16);
236        }
237    }
238}
239
240impl Clone for BufferViewState {
241    fn clone(&self) -> Self {
242        Self {
243            cursors: self.cursors.clone(),
244            viewport: self.viewport.clone(),
245            view_mode: self.view_mode.clone(),
246            compose_width: self.compose_width,
247            compose_column_guides: self.compose_column_guides.clone(),
248            rulers: self.rulers.clone(),
249            show_line_numbers: self.show_line_numbers,
250            highlight_current_line: self.highlight_current_line,
251            view_transform: self.view_transform.clone(),
252            view_transform_stale: self.view_transform_stale,
253            plugin_state: self.plugin_state.clone(),
254            // Fold markers are per-view; clones start with no folded ranges.
255            folds: FoldManager::new(),
256        }
257    }
258}
259
260/// Per-split view state (independent of buffer content)
261///
262/// Following the Emacs model where each window (split) has its own:
263/// - Point (cursor position) - independent per split
264/// - Window-start (scroll position) - independent per split
265/// - Tabs (open buffers) - independent per split
266///
267/// Buffer-specific state (cursors, viewport, view_mode, compose settings) is stored
268/// in the `keyed_states` map, keyed by `BufferId`. The active buffer's state is
269/// accessible via `Deref`/`DerefMut` (so `vs.cursors` transparently accesses the
270/// active buffer's cursors), or explicitly via `active_state()`/`active_state_mut()`.
271#[derive(Debug, Clone)]
272pub struct SplitViewState {
273    /// Which buffer is currently active in this split
274    pub active_buffer: BufferId,
275
276    /// Per-buffer view state map. The active buffer always has an entry.
277    pub keyed_states: HashMap<BufferId, BufferViewState>,
278
279    /// List of tab targets open in this split's tab bar (in order).
280    /// Each entry is either a regular buffer or a grouped subtree.
281    /// The currently displayed target is tracked by `active_buffer`
282    /// (for buffer tabs) or by walking the tree for the active leaf
283    /// (for group tabs).
284    pub open_buffers: Vec<TabTarget>,
285
286    /// Horizontal scroll offset for the tabs in this split
287    pub tab_scroll_offset: usize,
288
289    /// Computed layout for this view (from view_transform or base tokens)
290    /// This is View state - each split has its own Layout
291    pub layout: Option<Layout>,
292
293    /// Whether the layout needs to be rebuilt (buffer changed, transform changed, etc.)
294    pub layout_dirty: bool,
295
296    /// Focus history stack for this split (most recent at end).
297    /// Tracks both buffer tabs and group tabs so that "Switch to Previous
298    /// Tab" and close-buffer replacement both work across tab types.
299    pub focus_history: Vec<TabTarget>,
300
301    /// Sync group ID for synchronized scrolling
302    /// Splits with the same sync_group will scroll together
303    pub sync_group: Option<u32>,
304
305    /// When set, this split renders a composite view (e.g., side-by-side diff).
306    /// The split's buffer_id is the focused source buffer, but rendering uses
307    /// the composite layout. This makes the source buffer the "active buffer"
308    /// so normal keybindings work directly.
309    pub composite_view: Option<BufferId>,
310
311    /// When true, suppress per-split chrome (tab bar, close/maximize buttons).
312    /// Used for splits within a buffer group where the group provides its own tab.
313    pub suppress_chrome: bool,
314
315    /// When true, hide tilde markers (~) for empty rows in this split.
316    /// Used for panels where empty space should be blank, not marked.
317    pub hide_tilde: bool,
318
319    /// When `Some(leaf_id)`, the currently "active tab" of this split is the
320    /// buffer group identified by `leaf_id` (i.e., `TabTarget::Group(leaf_id)`).
321    /// When `None`, the active tab is a regular buffer (`TabTarget::Buffer(active_buffer)`).
322    pub active_group_tab: Option<LeafId>,
323
324    /// When a group tab is active, this tracks which inner leaf inside the
325    /// group's subtree has keyboard focus.
326    pub focused_group_leaf: Option<LeafId>,
327}
328
329impl std::ops::Deref for SplitViewState {
330    type Target = BufferViewState;
331
332    fn deref(&self) -> &BufferViewState {
333        self.active_state()
334    }
335}
336
337impl std::ops::DerefMut for SplitViewState {
338    fn deref_mut(&mut self) -> &mut BufferViewState {
339        self.active_state_mut()
340    }
341}
342
343impl SplitViewState {
344    /// Create a new split view state with an initial buffer open
345    pub fn with_buffer(width: u16, height: u16, buffer_id: BufferId) -> Self {
346        let buf_state = BufferViewState::new(width, height);
347        let mut keyed_states = HashMap::new();
348        keyed_states.insert(buffer_id, buf_state);
349        Self {
350            active_buffer: buffer_id,
351            keyed_states,
352            open_buffers: vec![TabTarget::Buffer(buffer_id)],
353            tab_scroll_offset: 0,
354            layout: None,
355            layout_dirty: true,
356            focus_history: Vec::new(),
357            sync_group: None,
358            composite_view: None,
359            suppress_chrome: false,
360            hide_tilde: false,
361            active_group_tab: None,
362            focused_group_leaf: None,
363        }
364    }
365
366    /// Get the active buffer's view state
367    pub fn active_state(&self) -> &BufferViewState {
368        self.keyed_states
369            .get(&self.active_buffer)
370            .expect("active_buffer must always have an entry in keyed_states")
371    }
372
373    /// Get a mutable reference to the active buffer's view state
374    pub fn active_state_mut(&mut self) -> &mut BufferViewState {
375        self.keyed_states
376            .get_mut(&self.active_buffer)
377            .expect("active_buffer must always have an entry in keyed_states")
378    }
379
380    /// Switch the active buffer in this split.
381    ///
382    /// If the new buffer has a saved state in `keyed_states`, it is restored.
383    /// Otherwise a default `BufferViewState` is created with the split's current
384    /// viewport dimensions.
385    pub fn switch_buffer(&mut self, new_buffer_id: BufferId) {
386        if new_buffer_id == self.active_buffer {
387            return;
388        }
389        // Ensure the new buffer has keyed state (create default if first time)
390        if !self.keyed_states.contains_key(&new_buffer_id) {
391            let active = self.active_state();
392            let width = active.viewport.width;
393            let height = active.viewport.height;
394            self.keyed_states
395                .insert(new_buffer_id, BufferViewState::new(width, height));
396        }
397        self.active_buffer = new_buffer_id;
398        // Invalidate layout since we're now showing different buffer content
399        self.layout_dirty = true;
400    }
401
402    /// Get the view state for a specific buffer (if it exists)
403    pub fn buffer_state(&self, buffer_id: BufferId) -> Option<&BufferViewState> {
404        self.keyed_states.get(&buffer_id)
405    }
406
407    /// Get a mutable reference to the view state for a specific buffer (if it exists)
408    pub fn buffer_state_mut(&mut self, buffer_id: BufferId) -> Option<&mut BufferViewState> {
409        self.keyed_states.get_mut(&buffer_id)
410    }
411
412    /// Ensure a buffer has keyed state, creating a default if needed.
413    /// Returns a mutable reference to the buffer's view state.
414    pub fn ensure_buffer_state(&mut self, buffer_id: BufferId) -> &mut BufferViewState {
415        let (width, height) = {
416            let active = self.active_state();
417            (active.viewport.width, active.viewport.height)
418        };
419        self.keyed_states
420            .entry(buffer_id)
421            .or_insert_with(|| BufferViewState::new(width, height))
422    }
423
424    /// Remove keyed state for a buffer (when buffer is closed from this split)
425    pub fn remove_buffer_state(&mut self, buffer_id: BufferId) {
426        if buffer_id != self.active_buffer {
427            self.keyed_states.remove(&buffer_id);
428        }
429    }
430
431    /// Mark layout as needing rebuild (call after buffer changes)
432    pub fn invalidate_layout(&mut self) {
433        self.layout_dirty = true;
434    }
435
436    /// Ensure layout is valid, rebuilding if needed.
437    /// Returns the Layout - never returns None. Following VSCode's ViewModel pattern.
438    ///
439    /// # Arguments
440    /// * `tokens` - ViewTokenWire array (from view_transform or built from buffer)
441    /// * `source_range` - The byte range this layout covers
442    /// * `tab_size` - Tab width for rendering
443    pub fn ensure_layout(
444        &mut self,
445        tokens: &[fresh_core::api::ViewTokenWire],
446        source_range: std::ops::Range<usize>,
447        tab_size: usize,
448    ) -> &Layout {
449        if self.layout.is_none() || self.layout_dirty {
450            self.layout = Some(Layout::from_tokens(tokens, source_range, tab_size));
451            self.layout_dirty = false;
452        }
453        self.layout.as_ref().unwrap()
454    }
455
456    /// Get the current layout if it exists and is valid
457    pub fn get_layout(&self) -> Option<&Layout> {
458        if self.layout_dirty {
459            None
460        } else {
461            self.layout.as_ref()
462        }
463    }
464
465    /// Add a buffer to this split's tabs (if not already present)
466    pub fn add_buffer(&mut self, buffer_id: BufferId) {
467        if !self.has_buffer(buffer_id) {
468            self.open_buffers.push(TabTarget::Buffer(buffer_id));
469        }
470    }
471
472    /// Remove a buffer from this split's tabs and clean up its keyed state
473    pub fn remove_buffer(&mut self, buffer_id: BufferId) {
474        self.open_buffers
475            .retain(|t| *t != TabTarget::Buffer(buffer_id));
476        // Clean up keyed state (but never remove the active buffer's state)
477        if buffer_id != self.active_buffer {
478            self.keyed_states.remove(&buffer_id);
479        }
480    }
481
482    /// Check if a buffer is open in this split
483    pub fn has_buffer(&self, buffer_id: BufferId) -> bool {
484        self.open_buffers.contains(&TabTarget::Buffer(buffer_id))
485    }
486
487    /// Add a group tab to this split's tabs (if not already present)
488    pub fn add_group(&mut self, leaf_id: LeafId) {
489        if !self.has_group(leaf_id) {
490            self.open_buffers.push(TabTarget::Group(leaf_id));
491        }
492    }
493
494    /// Remove a group tab from this split's tabs
495    pub fn remove_group(&mut self, leaf_id: LeafId) {
496        self.open_buffers
497            .retain(|t| *t != TabTarget::Group(leaf_id));
498    }
499
500    /// Check if a group tab is open in this split
501    pub fn has_group(&self, leaf_id: LeafId) -> bool {
502        self.open_buffers.contains(&TabTarget::Group(leaf_id))
503    }
504
505    /// Iterate over only the buffer-tab ids in open_buffers (skipping groups).
506    pub fn buffer_tab_ids(&self) -> impl Iterator<Item = BufferId> + '_ {
507        self.open_buffers.iter().filter_map(|t| t.as_buffer())
508    }
509
510    /// Collect buffer-tab ids as a Vec<BufferId> (skipping groups).
511    /// Convenience for call sites that need ownership / indexing.
512    pub fn buffer_tab_ids_vec(&self) -> Vec<BufferId> {
513        self.buffer_tab_ids().collect()
514    }
515
516    /// Count only buffer tabs (ignoring group tabs).
517    pub fn buffer_tab_count(&self) -> usize {
518        self.open_buffers
519            .iter()
520            .filter(|t| matches!(t, TabTarget::Buffer(_)))
521            .count()
522    }
523
524    /// Return the effective active tab target for this split.
525    /// If a group tab is marked active, returns `TabTarget::Group`. Otherwise
526    /// returns `TabTarget::Buffer(active_buffer)`.
527    pub fn active_target(&self) -> TabTarget {
528        match self.active_group_tab {
529            Some(leaf_id) => TabTarget::Group(leaf_id),
530            None => TabTarget::Buffer(self.active_buffer),
531        }
532    }
533
534    /// Switch the active tab to a regular buffer target. Clears any
535    /// active group tab marker.
536    pub fn set_active_buffer_tab(&mut self, buffer_id: BufferId) {
537        self.active_group_tab = None;
538        self.focused_group_leaf = None;
539        self.switch_buffer(buffer_id);
540    }
541
542    /// Switch the active tab to a group target.
543    pub fn set_active_group_tab(&mut self, leaf_id: LeafId) {
544        self.active_group_tab = Some(leaf_id);
545    }
546
547    /// Push a tab target to the focus history (LRU-style).
548    /// If the target is already in history, it's moved to the end.
549    pub fn push_focus(&mut self, target: TabTarget) {
550        self.focus_history.retain(|t| *t != target);
551        self.focus_history.push(target);
552        if self.focus_history.len() > 50 {
553            self.focus_history.remove(0);
554        }
555    }
556
557    /// Get the most recently focused tab target (without removing it)
558    pub fn previous_tab(&self) -> Option<TabTarget> {
559        self.focus_history.last().copied()
560    }
561
562    /// Remove a buffer from the focus history (called when buffer is closed)
563    pub fn remove_from_history(&mut self, buffer_id: BufferId) {
564        self.focus_history
565            .retain(|t| *t != TabTarget::Buffer(buffer_id));
566    }
567
568    /// Remove a group from the focus history (called when group is closed)
569    pub fn remove_group_from_history(&mut self, leaf_id: LeafId) {
570        self.focus_history
571            .retain(|t| *t != TabTarget::Group(leaf_id));
572    }
573}
574
575impl SplitNode {
576    /// Create a new leaf node
577    pub fn leaf(buffer_id: BufferId, split_id: SplitId) -> Self {
578        Self::Leaf {
579            buffer_id,
580            split_id: LeafId(split_id),
581        }
582    }
583
584    /// Create a new split node with two children
585    pub fn split(
586        direction: SplitDirection,
587        first: SplitNode,
588        second: SplitNode,
589        ratio: f32,
590        split_id: SplitId,
591    ) -> Self {
592        SplitNode::Split {
593            direction,
594            first: Box::new(first),
595            second: Box::new(second),
596            ratio: ratio.clamp(0.1, 0.9), // Prevent extreme ratios
597            split_id: ContainerId(split_id),
598            fixed_first: None,
599            fixed_second: None,
600        }
601    }
602
603    /// Get the split ID for this node
604    pub fn id(&self) -> SplitId {
605        match self {
606            Self::Leaf { split_id, .. } => split_id.0,
607            Self::Split { split_id, .. } => split_id.0,
608            Self::Grouped { split_id, .. } => split_id.0,
609        }
610    }
611
612    /// Get the buffer ID if this is a leaf node
613    pub fn buffer_id(&self) -> Option<BufferId> {
614        match self {
615            Self::Leaf { buffer_id, .. } => Some(*buffer_id),
616            Self::Split { .. } | Self::Grouped { .. } => None,
617        }
618    }
619
620    /// Find a split by ID (returns mutable reference).
621    /// Grouped nodes are found by their `split_id`, and their inner
622    /// layout is searched as well.
623    pub fn find_mut(&mut self, target_id: SplitId) -> Option<&mut Self> {
624        if self.id() == target_id {
625            return Some(self);
626        }
627
628        match self {
629            Self::Leaf { .. } => None,
630            Self::Split { first, second, .. } => first
631                .find_mut(target_id)
632                .or_else(|| second.find_mut(target_id)),
633            Self::Grouped { layout, .. } => layout.find_mut(target_id),
634        }
635    }
636
637    /// Find a split by ID (returns immutable reference).
638    /// Grouped nodes are found by their `split_id`, and their inner
639    /// layout is searched as well.
640    pub fn find(&self, target_id: SplitId) -> Option<&Self> {
641        if self.id() == target_id {
642            return Some(self);
643        }
644
645        match self {
646            Self::Leaf { .. } => None,
647            Self::Split { first, second, .. } => {
648                first.find(target_id).or_else(|| second.find(target_id))
649            }
650            Self::Grouped { layout, .. } => layout.find(target_id),
651        }
652    }
653
654    /// Find the parent container of a given split node.
655    /// For a node inside a Grouped subtree, returns the container within
656    /// the subtree (not the Grouped node itself).
657    pub fn parent_container_of(&self, target_id: SplitId) -> Option<ContainerId> {
658        match self {
659            Self::Leaf { .. } => None,
660            Self::Split {
661                split_id,
662                first,
663                second,
664                ..
665            } => {
666                if first.id() == target_id || second.id() == target_id {
667                    Some(*split_id)
668                } else {
669                    first
670                        .parent_container_of(target_id)
671                        .or_else(|| second.parent_container_of(target_id))
672                }
673            }
674            Self::Grouped { layout, .. } => layout.parent_container_of(target_id),
675        }
676    }
677
678    /// Find the Grouped ancestor node that contains a given target id (by walking
679    /// into Grouped subtrees). Returns the Grouped node's own `split_id` if found.
680    pub fn grouped_ancestor_of(&self, target_id: SplitId) -> Option<LeafId> {
681        match self {
682            Self::Leaf { .. } => None,
683            Self::Split { first, second, .. } => first
684                .grouped_ancestor_of(target_id)
685                .or_else(|| second.grouped_ancestor_of(target_id)),
686            Self::Grouped {
687                split_id, layout, ..
688            } => {
689                if layout.find(target_id).is_some() {
690                    Some(*split_id)
691                } else {
692                    layout.grouped_ancestor_of(target_id)
693                }
694            }
695        }
696    }
697
698    /// Find the Grouped node whose `split_id` matches `target`. Returns
699    /// a reference to the Grouped node (or None).
700    pub fn find_grouped(&self, target: LeafId) -> Option<&Self> {
701        match self {
702            Self::Leaf { .. } => None,
703            Self::Split { first, second, .. } => first
704                .find_grouped(target)
705                .or_else(|| second.find_grouped(target)),
706            Self::Grouped {
707                split_id, layout, ..
708            } => {
709                if *split_id == target {
710                    Some(self)
711                } else {
712                    layout.find_grouped(target)
713                }
714            }
715        }
716    }
717
718    /// Get all leaf nodes (buffer views) with their rectangles.
719    ///
720    /// Grouped nodes always recurse into their inner layout — the layout's
721    /// leaves get the full rect that would have been given to the Grouped
722    /// node. Visibility (which group is "active") is applied elsewhere.
723    pub fn get_leaves_with_rects(&self, rect: Rect) -> Vec<(LeafId, BufferId, Rect)> {
724        match self {
725            Self::Leaf {
726                buffer_id,
727                split_id,
728            } => {
729                vec![(*split_id, *buffer_id, rect)]
730            }
731            Self::Split {
732                direction,
733                first,
734                second,
735                ratio,
736                fixed_first,
737                fixed_second,
738                ..
739            } => {
740                let (first_rect, second_rect) =
741                    split_rect_ext(rect, *direction, *ratio, *fixed_first, *fixed_second);
742                let mut leaves = first.get_leaves_with_rects(first_rect);
743                leaves.extend(second.get_leaves_with_rects(second_rect));
744                leaves
745            }
746            Self::Grouped { layout, .. } => layout.get_leaves_with_rects(rect),
747        }
748    }
749
750    /// Walk the tree using an "active group" predicate. For each Grouped node
751    /// encountered, the predicate is called with the Grouped node's split_id;
752    /// if it returns `true`, the node's layout is recursed into (with the
753    /// Grouped node's rect). If `false`, the Grouped node and its subtree are
754    /// skipped entirely (not rendered).
755    pub fn get_visible_leaves_with_rects<F>(
756        &self,
757        rect: Rect,
758        is_group_active: &F,
759    ) -> Vec<(LeafId, BufferId, Rect)>
760    where
761        F: Fn(LeafId) -> bool,
762    {
763        match self {
764            Self::Leaf {
765                buffer_id,
766                split_id,
767            } => {
768                vec![(*split_id, *buffer_id, rect)]
769            }
770            Self::Split {
771                direction,
772                first,
773                second,
774                ratio,
775                fixed_first,
776                fixed_second,
777                ..
778            } => {
779                let (first_rect, second_rect) =
780                    split_rect_ext(rect, *direction, *ratio, *fixed_first, *fixed_second);
781                let mut leaves = first.get_visible_leaves_with_rects(first_rect, is_group_active);
782                leaves.extend(second.get_visible_leaves_with_rects(second_rect, is_group_active));
783                leaves
784            }
785            Self::Grouped {
786                split_id, layout, ..
787            } => {
788                if is_group_active(*split_id) {
789                    layout.get_visible_leaves_with_rects(rect, is_group_active)
790                } else {
791                    Vec::new()
792                }
793            }
794        }
795    }
796
797    /// Get all split separator lines (for rendering borders)
798    /// Returns (direction, x, y, length) tuples
799    pub fn get_separators(&self, rect: Rect) -> Vec<(SplitDirection, u16, u16, u16)> {
800        self.get_separators_with_ids(rect)
801            .into_iter()
802            .map(|(_, dir, x, y, len)| (dir, x, y, len))
803            .collect()
804    }
805
806    /// Get all split separator lines with their split IDs (for mouse hit testing)
807    /// Returns (split_id, direction, x, y, length) tuples
808    pub fn get_separators_with_ids(
809        &self,
810        rect: Rect,
811    ) -> Vec<(ContainerId, SplitDirection, u16, u16, u16)> {
812        match self {
813            Self::Leaf { .. } => vec![],
814            Self::Grouped { layout, .. } => layout.get_separators_with_ids(rect),
815            Self::Split {
816                direction,
817                first,
818                second,
819                ratio,
820                split_id,
821                fixed_first,
822                fixed_second,
823            } => {
824                let (first_rect, second_rect) =
825                    split_rect_ext(rect, *direction, *ratio, *fixed_first, *fixed_second);
826                let mut separators = Vec::new();
827
828                // Add separator for this split (in the 1-char gap between first and second)
829                match direction {
830                    SplitDirection::Horizontal => {
831                        // Horizontal split: separator line is between first and second
832                        // y position is at the end of first rect (the gap line)
833                        separators.push((
834                            *split_id,
835                            SplitDirection::Horizontal,
836                            rect.x,
837                            first_rect.y + first_rect.height,
838                            rect.width,
839                        ));
840                    }
841                    SplitDirection::Vertical => {
842                        // Vertical split: separator line is between first and second
843                        // x position is at the end of first rect (the gap column)
844                        separators.push((
845                            *split_id,
846                            SplitDirection::Vertical,
847                            first_rect.x + first_rect.width,
848                            rect.y,
849                            rect.height,
850                        ));
851                    }
852                }
853
854                // Recursively get separators from children
855                separators.extend(first.get_separators_with_ids(first_rect));
856                separators.extend(second.get_separators_with_ids(second_rect));
857                separators
858            }
859        }
860    }
861
862    /// Collect all split IDs in the tree
863    pub fn all_split_ids(&self) -> Vec<SplitId> {
864        let mut ids = vec![self.id()];
865        match self {
866            Self::Leaf { .. } => ids,
867            Self::Split { first, second, .. } => {
868                ids.extend(first.all_split_ids());
869                ids.extend(second.all_split_ids());
870                ids
871            }
872            Self::Grouped { layout, .. } => {
873                ids.extend(layout.all_split_ids());
874                ids
875            }
876        }
877    }
878
879    /// Collect only leaf split IDs (visible buffer splits, not container nodes).
880    /// For Grouped nodes, returns the inner layout's leaves.
881    pub fn leaf_split_ids(&self) -> Vec<LeafId> {
882        match self {
883            Self::Leaf { split_id, .. } => vec![*split_id],
884            Self::Split { first, second, .. } => {
885                let mut ids = first.leaf_split_ids();
886                ids.extend(second.leaf_split_ids());
887                ids
888            }
889            Self::Grouped { layout, .. } => layout.leaf_split_ids(),
890        }
891    }
892
893    /// Count the number of leaf nodes (visible buffers).
894    /// Grouped subtrees count their inner leaves.
895    pub fn count_leaves(&self) -> usize {
896        match self {
897            Self::Leaf { .. } => 1,
898            Self::Split { first, second, .. } => first.count_leaves() + second.count_leaves(),
899            Self::Grouped { layout, .. } => layout.count_leaves(),
900        }
901    }
902
903    /// Collect display names for all Grouped nodes in the tree, keyed by
904    /// their LeafId (which is what `TabTarget::Group` points to).
905    pub fn collect_group_names(&self) -> HashMap<LeafId, String> {
906        let mut map = HashMap::new();
907        self.collect_group_names_into(&mut map);
908        map
909    }
910
911    fn collect_group_names_into(&self, map: &mut HashMap<LeafId, String>) {
912        match self {
913            Self::Leaf { .. } => {}
914            Self::Split { first, second, .. } => {
915                first.collect_group_names_into(map);
916                second.collect_group_names_into(map);
917            }
918            Self::Grouped {
919                split_id,
920                name,
921                layout,
922                ..
923            } => {
924                map.insert(*split_id, name.clone());
925                layout.collect_group_names_into(map);
926            }
927        }
928    }
929}
930
931/// Split a rectangle into two parts based on direction and ratio
932/// Leaves 1 character space for the separator line between splits
933#[cfg(test)]
934fn split_rect(rect: Rect, direction: SplitDirection, ratio: f32) -> (Rect, Rect) {
935    split_rect_ext(rect, direction, ratio, None, None)
936}
937
938fn split_rect_ext(
939    rect: Rect,
940    direction: SplitDirection,
941    ratio: f32,
942    fixed_first: Option<u16>,
943    fixed_second: Option<u16>,
944) -> (Rect, Rect) {
945    match direction {
946        SplitDirection::Horizontal => {
947            // Split into top and bottom, with 1 line for separator
948            let total_height = rect.height.saturating_sub(1); // Reserve 1 line for separator
949            let first_height = if let Some(f) = fixed_first {
950                f.min(total_height)
951            } else if let Some(s) = fixed_second {
952                total_height.saturating_sub(s.min(total_height))
953            } else {
954                (total_height as f32 * ratio).round() as u16
955            };
956            let second_height = total_height.saturating_sub(first_height);
957
958            let first = Rect {
959                x: rect.x,
960                y: rect.y,
961                width: rect.width,
962                height: first_height,
963            };
964
965            let second = Rect {
966                x: rect.x,
967                y: rect.y + first_height + 1, // +1 for separator
968                width: rect.width,
969                height: second_height,
970            };
971
972            (first, second)
973        }
974        SplitDirection::Vertical => {
975            // Split into left and right, with 1 column for separator
976            let total_width = rect.width.saturating_sub(1); // Reserve 1 column for separator
977            let first_width = if let Some(f) = fixed_first {
978                f.min(total_width)
979            } else if let Some(s) = fixed_second {
980                total_width.saturating_sub(s.min(total_width))
981            } else {
982                (total_width as f32 * ratio).round() as u16
983            };
984            let second_width = total_width.saturating_sub(first_width);
985
986            let first = Rect {
987                x: rect.x,
988                y: rect.y,
989                width: first_width,
990                height: rect.height,
991            };
992
993            let second = Rect {
994                x: rect.x + first_width + 1, // +1 for separator
995                y: rect.y,
996                width: second_width,
997                height: rect.height,
998            };
999
1000            (first, second)
1001        }
1002    }
1003}
1004
1005/// Manager for the split view system
1006#[derive(Debug)]
1007pub struct SplitManager {
1008    /// Root of the split tree
1009    root: SplitNode,
1010
1011    /// Currently active split (receives input) — always a leaf
1012    active_split: LeafId,
1013
1014    /// Next split ID to assign
1015    next_split_id: usize,
1016
1017    /// Currently maximized split (if any). When set, only this split is visible.
1018    maximized_split: Option<SplitId>,
1019
1020    /// Labels for leaf splits (e.g., "sidebar" to mark managed splits)
1021    labels: HashMap<SplitId, String>,
1022}
1023
1024impl SplitManager {
1025    /// Create a new split manager with a single buffer
1026    pub fn new(buffer_id: BufferId) -> Self {
1027        let split_id = SplitId(0);
1028        Self {
1029            root: SplitNode::leaf(buffer_id, split_id),
1030            active_split: LeafId(split_id),
1031            next_split_id: 1,
1032            maximized_split: None,
1033            labels: HashMap::new(),
1034        }
1035    }
1036
1037    /// Get the root split node
1038    pub fn root(&self) -> &SplitNode {
1039        &self.root
1040    }
1041
1042    /// Allocate a new unique split ID
1043    pub fn allocate_split_id(&mut self) -> SplitId {
1044        let id = SplitId(self.next_split_id);
1045        self.next_split_id += 1;
1046        id
1047    }
1048
1049    /// Replace the root split tree. The new tree must have unique IDs
1050    /// (allocated via `allocate_split_id`). The caller must also provide
1051    /// the new active leaf ID.
1052    pub fn replace_root(&mut self, new_root: SplitNode, new_active: LeafId) {
1053        self.root = new_root;
1054        self.active_split = new_active;
1055    }
1056
1057    /// Get the currently active split ID
1058    pub fn active_split(&self) -> LeafId {
1059        self.active_split
1060    }
1061
1062    /// Set the active split (must be a leaf)
1063    pub fn set_active_split(&mut self, split_id: LeafId) -> bool {
1064        // Verify the split exists
1065        if self.root.find(split_id.into()).is_some() {
1066            self.active_split = split_id;
1067            true
1068        } else {
1069            false
1070        }
1071    }
1072
1073    /// Get the buffer ID of the active split (if it's a leaf)
1074    pub fn active_buffer_id(&self) -> Option<BufferId> {
1075        self.root
1076            .find(self.active_split.into())
1077            .and_then(|node| node.buffer_id())
1078    }
1079
1080    /// Get the buffer ID for a specific split (if it's a leaf)
1081    pub fn get_buffer_id(&self, split_id: SplitId) -> Option<BufferId> {
1082        self.root.find(split_id).and_then(|node| node.buffer_id())
1083    }
1084
1085    /// Update the buffer ID of the active split
1086    pub fn set_active_buffer_id(&mut self, new_buffer_id: BufferId) -> bool {
1087        if let Some(SplitNode::Leaf { buffer_id, .. }) =
1088            self.root.find_mut(self.active_split.into())
1089        {
1090            *buffer_id = new_buffer_id;
1091            return true;
1092        }
1093        false
1094    }
1095
1096    /// Update the buffer ID of a specific leaf split
1097    pub fn set_split_buffer(&mut self, leaf_id: LeafId, new_buffer_id: BufferId) {
1098        match self.root.find_mut(leaf_id.into()) {
1099            Some(SplitNode::Leaf { buffer_id, .. }) => {
1100                *buffer_id = new_buffer_id;
1101            }
1102            Some(SplitNode::Split { .. }) => {
1103                unreachable!("LeafId {:?} points to a container", leaf_id)
1104            }
1105            Some(SplitNode::Grouped { .. }) => {
1106                unreachable!("LeafId {:?} points to a Grouped node", leaf_id)
1107            }
1108            None => {
1109                unreachable!("LeafId {:?} not found in split tree", leaf_id)
1110            }
1111        }
1112    }
1113
1114    // allocate_split_id is defined as pub earlier in this impl block
1115
1116    /// Split the currently active pane
1117    pub fn split_active(
1118        &mut self,
1119        direction: SplitDirection,
1120        new_buffer_id: BufferId,
1121        ratio: f32,
1122    ) -> Result<LeafId, String> {
1123        self.split_active_positioned(direction, new_buffer_id, ratio, false)
1124    }
1125
1126    /// Split the active pane, placing the new buffer before (left/top) the existing content.
1127    /// `ratio` still controls the first child's proportion of space.
1128    pub fn split_active_before(
1129        &mut self,
1130        direction: SplitDirection,
1131        new_buffer_id: BufferId,
1132        ratio: f32,
1133    ) -> Result<LeafId, String> {
1134        self.split_active_positioned(direction, new_buffer_id, ratio, true)
1135    }
1136
1137    pub fn split_active_positioned(
1138        &mut self,
1139        direction: SplitDirection,
1140        new_buffer_id: BufferId,
1141        ratio: f32,
1142        before: bool,
1143    ) -> Result<LeafId, String> {
1144        let active_id: SplitId = self.active_split.into();
1145
1146        // Find the parent of the active split
1147        let result =
1148            self.replace_split_with_split(active_id, direction, new_buffer_id, ratio, before);
1149
1150        if let Ok(new_split_id) = &result {
1151            // Set the new split as active
1152            self.active_split = *new_split_id;
1153        }
1154        result
1155    }
1156
1157    /// Replace a split with a new split container.
1158    /// When `before` is true, the new buffer is placed as the first child (left/top).
1159    fn replace_split_with_split(
1160        &mut self,
1161        target_id: SplitId,
1162        direction: SplitDirection,
1163        new_buffer_id: BufferId,
1164        ratio: f32,
1165        before: bool,
1166    ) -> Result<LeafId, String> {
1167        // Pre-allocate all IDs before any borrowing
1168        let temp_id = self.allocate_split_id();
1169        let new_split_id = self.allocate_split_id();
1170        let new_leaf_id = self.allocate_split_id();
1171
1172        // Special case: if target is root, replace root
1173        if self.root.id() == target_id {
1174            let old_root =
1175                std::mem::replace(&mut self.root, SplitNode::leaf(new_buffer_id, temp_id));
1176            let new_leaf = SplitNode::leaf(new_buffer_id, new_leaf_id);
1177
1178            let (first, second) = if before {
1179                (new_leaf, old_root)
1180            } else {
1181                (old_root, new_leaf)
1182            };
1183
1184            self.root = SplitNode::split(direction, first, second, ratio, new_split_id);
1185
1186            return Ok(LeafId(new_leaf_id));
1187        }
1188
1189        // Find and replace the target node
1190        if let Some(node) = self.root.find_mut(target_id) {
1191            let old_node = std::mem::replace(node, SplitNode::leaf(new_buffer_id, temp_id));
1192            let new_leaf = SplitNode::leaf(new_buffer_id, new_leaf_id);
1193
1194            let (first, second) = if before {
1195                (new_leaf, old_node)
1196            } else {
1197                (old_node, new_leaf)
1198            };
1199
1200            *node = SplitNode::split(direction, first, second, ratio, new_split_id);
1201
1202            Ok(LeafId(new_leaf_id))
1203        } else {
1204            Err(format!("Split {:?} not found", target_id))
1205        }
1206    }
1207
1208    /// Close a split pane (if not the last one)
1209    pub fn close_split(&mut self, split_id: LeafId) -> Result<(), String> {
1210        // Can't close if it's the only split
1211        if self.root.count_leaves() <= 1 {
1212            return Err("Cannot close the last split".to_string());
1213        }
1214
1215        // Can't close if it's the root and root is a leaf
1216        if self.root.id() == split_id.into() && self.root.buffer_id().is_some() {
1217            return Err("Cannot close the only split".to_string());
1218        }
1219
1220        // If the split being closed is maximized, unmaximize first
1221        if self.maximized_split == Some(split_id.into()) {
1222            self.maximized_split = None;
1223        }
1224
1225        // Collect all split IDs that will be removed (the target and its children)
1226        let removed_ids: Vec<SplitId> = self
1227            .root
1228            .find(split_id.into())
1229            .map(|node| node.all_split_ids())
1230            .unwrap_or_default();
1231
1232        // Find the parent of the split to close
1233        // This requires a parent-tracking traversal
1234        let result = self.remove_split_node(split_id.into());
1235
1236        if result.is_ok() {
1237            // Clean up labels for all removed splits
1238            for id in &removed_ids {
1239                self.labels.remove(id);
1240            }
1241
1242            // If we closed the active split, update active_split to another split
1243            if self.active_split == split_id {
1244                let leaf_ids = self.root.leaf_split_ids();
1245                if let Some(&first_leaf) = leaf_ids.first() {
1246                    self.active_split = first_leaf;
1247                }
1248            }
1249        }
1250
1251        result
1252    }
1253
1254    /// Remove a split node from the tree
1255    fn remove_split_node(&mut self, target_id: SplitId) -> Result<(), String> {
1256        // Special case: removing root
1257        if self.root.id() == target_id {
1258            if let SplitNode::Split { first, .. } = &self.root {
1259                // Replace root with the other child
1260                // Choose first child arbitrarily
1261                self.root = (**first).clone();
1262                return Ok(());
1263            }
1264        }
1265
1266        // Recursively find and remove
1267        Self::remove_child_static(&mut self.root, target_id)
1268    }
1269
1270    /// Helper to remove a child from a split node (static to avoid borrow issues)
1271    fn remove_child_static(node: &mut SplitNode, target_id: SplitId) -> Result<(), String> {
1272        match node {
1273            SplitNode::Leaf { .. } => Err("Target not found".to_string()),
1274            SplitNode::Grouped { layout, .. } => Self::remove_child_static(layout, target_id),
1275            SplitNode::Split { first, second, .. } => {
1276                // Check if either child is the target
1277                if first.id() == target_id {
1278                    // Replace this node with the second child
1279                    *node = (**second).clone();
1280                    Ok(())
1281                } else if second.id() == target_id {
1282                    // Replace this node with the first child
1283                    *node = (**first).clone();
1284                    Ok(())
1285                } else {
1286                    // Recurse into children
1287                    Self::remove_child_static(first, target_id)
1288                        .or_else(|_| Self::remove_child_static(second, target_id))
1289                }
1290            }
1291        }
1292    }
1293
1294    /// Remove a Grouped node from the tree by its split_id. Unlike
1295    /// `close_split` which requires a leaf, this removes a whole Grouped
1296    /// subtree (tab) from the split structure. The Grouped node is
1297    /// replaced with... well, nothing — so this can only succeed if the
1298    /// Grouped is inside a Split (so we can replace the Split with its
1299    /// sibling) or if the root itself is the Grouped (which we can't
1300    /// remove without a replacement).
1301    pub fn remove_grouped(&mut self, target: LeafId) -> Result<(), String> {
1302        let target_id: SplitId = target.into();
1303        if self.root.id() == target_id {
1304            return Err("Cannot remove root Grouped node".to_string());
1305        }
1306        Self::remove_child_static(&mut self.root, target_id)
1307    }
1308
1309    /// Adjust the split ratio of a container
1310    pub fn adjust_ratio(&mut self, container_id: ContainerId, delta: f32) {
1311        match self.root.find_mut(container_id.into()) {
1312            Some(SplitNode::Split { ratio, .. }) => {
1313                *ratio = (*ratio + delta).clamp(0.1, 0.9);
1314            }
1315            Some(SplitNode::Leaf { .. }) => {
1316                unreachable!("ContainerId {:?} points to a leaf", container_id)
1317            }
1318            Some(SplitNode::Grouped { .. }) => {
1319                unreachable!("ContainerId {:?} points to a Grouped node", container_id)
1320            }
1321            None => {
1322                unreachable!("ContainerId {:?} not found in split tree", container_id)
1323            }
1324        }
1325    }
1326
1327    /// Find the parent container of a leaf
1328    pub fn parent_container_of(&self, leaf_id: LeafId) -> Option<ContainerId> {
1329        self.root.parent_container_of(leaf_id.into())
1330    }
1331
1332    /// Get all visible buffer views with their rectangles
1333    pub fn get_visible_buffers(&self, viewport_rect: Rect) -> Vec<(LeafId, BufferId, Rect)> {
1334        // If a split is maximized, only show that split taking up the full viewport
1335        if let Some(maximized_id) = self.maximized_split {
1336            if let Some(SplitNode::Leaf {
1337                buffer_id,
1338                split_id,
1339            }) = self.root.find(maximized_id)
1340            {
1341                return vec![(*split_id, *buffer_id, viewport_rect)];
1342            }
1343            // Maximized split no longer exists, clear it and fall through
1344        }
1345        self.root.get_leaves_with_rects(viewport_rect)
1346    }
1347
1348    /// Get all split separator positions for rendering borders
1349    /// Returns (direction, x, y, length) tuples
1350    pub fn get_separators(&self, viewport_rect: Rect) -> Vec<(SplitDirection, u16, u16, u16)> {
1351        // No separators when a split is maximized
1352        if self.maximized_split.is_some() {
1353            return vec![];
1354        }
1355        self.root.get_separators(viewport_rect)
1356    }
1357
1358    /// Get all split separator positions with their split IDs (for mouse hit testing)
1359    /// Returns (container_id, direction, x, y, length) tuples
1360    pub fn get_separators_with_ids(
1361        &self,
1362        viewport_rect: Rect,
1363    ) -> Vec<(ContainerId, SplitDirection, u16, u16, u16)> {
1364        // No separators when a split is maximized
1365        if self.maximized_split.is_some() {
1366            return vec![];
1367        }
1368        self.root.get_separators_with_ids(viewport_rect)
1369    }
1370
1371    /// Get the current ratio of a split container
1372    pub fn get_ratio(&self, split_id: SplitId) -> Option<f32> {
1373        if let Some(SplitNode::Split { ratio, .. }) = self.root.find(split_id) {
1374            Some(*ratio)
1375        } else {
1376            None
1377        }
1378    }
1379
1380    /// Set the exact ratio of a split container
1381    pub fn set_ratio(&mut self, container_id: ContainerId, new_ratio: f32) {
1382        match self.root.find_mut(container_id.into()) {
1383            Some(SplitNode::Split { ratio, .. }) => {
1384                *ratio = new_ratio.clamp(0.1, 0.9);
1385            }
1386            Some(SplitNode::Leaf { .. }) => {
1387                unreachable!("ContainerId {:?} points to a leaf", container_id)
1388            }
1389            Some(SplitNode::Grouped { .. }) => {
1390                unreachable!("ContainerId {:?} points to a Grouped node", container_id)
1391            }
1392            None => {
1393                unreachable!("ContainerId {:?} not found in split tree", container_id)
1394            }
1395        }
1396    }
1397
1398    /// Set a fixed size on a split container's first or second child.
1399    /// When set, the child gets exactly this many rows/cols instead of using the ratio.
1400    pub fn set_fixed_size(
1401        &mut self,
1402        container_id: ContainerId,
1403        first: Option<u16>,
1404        second: Option<u16>,
1405    ) {
1406        match self.root.find_mut(container_id.into()) {
1407            Some(SplitNode::Split {
1408                fixed_first,
1409                fixed_second,
1410                ..
1411            }) => {
1412                *fixed_first = first;
1413                *fixed_second = second;
1414            }
1415            _ => {}
1416        }
1417    }
1418
1419    /// Distribute all visible splits evenly
1420    /// This sets the ratios of all container splits so that leaf splits get equal space
1421    pub fn distribute_splits_evenly(&mut self) {
1422        Self::distribute_node_evenly(&mut self.root);
1423    }
1424
1425    /// Recursively distribute a node's splits evenly
1426    /// Returns the number of leaves in this subtree
1427    fn distribute_node_evenly(node: &mut SplitNode) -> usize {
1428        match node {
1429            SplitNode::Leaf { .. } => 1,
1430            SplitNode::Grouped { layout, .. } => Self::distribute_node_evenly(layout),
1431            SplitNode::Split {
1432                first,
1433                second,
1434                ratio,
1435                ..
1436            } => {
1437                let first_leaves = Self::distribute_node_evenly(first);
1438                let second_leaves = Self::distribute_node_evenly(second);
1439                let total_leaves = first_leaves + second_leaves;
1440
1441                // Set ratio so each leaf gets equal space
1442                // ratio = proportion for first pane
1443                *ratio = (first_leaves as f32 / total_leaves as f32).clamp(0.1, 0.9);
1444
1445                total_leaves
1446            }
1447        }
1448    }
1449
1450    /// Navigate to the next split (circular)
1451    pub fn next_split(&mut self) {
1452        let leaf_ids = self.root.leaf_split_ids();
1453        if let Some(pos) = leaf_ids.iter().position(|id| *id == self.active_split) {
1454            let next_pos = (pos + 1) % leaf_ids.len();
1455            self.active_split = leaf_ids[next_pos];
1456        }
1457    }
1458
1459    /// Navigate to the previous split (circular)
1460    pub fn prev_split(&mut self) {
1461        let leaf_ids = self.root.leaf_split_ids();
1462        if let Some(pos) = leaf_ids.iter().position(|id| *id == self.active_split) {
1463            let prev_pos = if pos == 0 { leaf_ids.len() } else { pos } - 1;
1464            self.active_split = leaf_ids[prev_pos];
1465        }
1466    }
1467
1468    /// Get all split IDs that display a specific buffer
1469    pub fn splits_for_buffer(&self, target_buffer_id: BufferId) -> Vec<LeafId> {
1470        self.root
1471            .get_leaves_with_rects(Rect {
1472                x: 0,
1473                y: 0,
1474                width: 1,
1475                height: 1,
1476            })
1477            .into_iter()
1478            .filter(|(_, buffer_id, _)| *buffer_id == target_buffer_id)
1479            .map(|(split_id, _, _)| split_id)
1480            .collect()
1481    }
1482
1483    /// Get the buffer ID for a specific leaf split
1484    pub fn buffer_for_split(&self, target_split_id: LeafId) -> Option<BufferId> {
1485        self.root
1486            .get_leaves_with_rects(Rect {
1487                x: 0,
1488                y: 0,
1489                width: 1,
1490                height: 1,
1491            })
1492            .into_iter()
1493            .find(|(split_id, _, _)| *split_id == target_split_id)
1494            .map(|(_, buffer_id, _)| buffer_id)
1495    }
1496
1497    /// Maximize the active split (hide all other splits temporarily)
1498    /// Returns Ok(()) if successful, Err if there's only one split
1499    pub fn maximize_split(&mut self) -> Result<(), String> {
1500        // Can't maximize if there's only one split
1501        if self.root.count_leaves() <= 1 {
1502            return Err("Cannot maximize: only one split exists".to_string());
1503        }
1504
1505        // Can't maximize if already maximized
1506        if self.maximized_split.is_some() {
1507            return Err("A split is already maximized".to_string());
1508        }
1509
1510        // Maximize the active split
1511        self.maximized_split = Some(self.active_split.into());
1512        Ok(())
1513    }
1514
1515    /// Unmaximize the currently maximized split (restore all splits)
1516    /// Returns Ok(()) if successful, Err if no split is maximized
1517    pub fn unmaximize_split(&mut self) -> Result<(), String> {
1518        if self.maximized_split.is_none() {
1519            return Err("No split is maximized".to_string());
1520        }
1521
1522        self.maximized_split = None;
1523        Ok(())
1524    }
1525
1526    /// Check if a split is currently maximized
1527    pub fn is_maximized(&self) -> bool {
1528        self.maximized_split.is_some()
1529    }
1530
1531    /// Get the currently maximized split ID (if any)
1532    pub fn maximized_split(&self) -> Option<SplitId> {
1533        self.maximized_split
1534    }
1535
1536    /// Toggle maximize state for the active split
1537    /// If maximized, unmaximize. If not maximized, maximize.
1538    /// Returns true if maximized, false if ununmaximized.
1539    pub fn toggle_maximize(&mut self) -> Result<bool, String> {
1540        if self.is_maximized() {
1541            self.unmaximize_split()?;
1542            Ok(false)
1543        } else {
1544            self.maximize_split()?;
1545            Ok(true)
1546        }
1547    }
1548
1549    /// Get all leaf split IDs that belong to a specific sync group
1550    pub fn get_splits_in_group(
1551        &self,
1552        group_id: u32,
1553        view_states: &std::collections::HashMap<LeafId, SplitViewState>,
1554    ) -> Vec<LeafId> {
1555        self.root
1556            .leaf_split_ids()
1557            .into_iter()
1558            .filter(|id| {
1559                view_states
1560                    .get(id)
1561                    .and_then(|vs| vs.sync_group)
1562                    .is_some_and(|g| g == group_id)
1563            })
1564            .collect()
1565    }
1566
1567    // === Split labels ===
1568
1569    /// Set a label on a leaf split (e.g., "sidebar")
1570    pub fn set_label(&mut self, split_id: LeafId, label: String) {
1571        self.labels.insert(split_id.into(), label);
1572    }
1573
1574    /// Remove a label from a split
1575    pub fn clear_label(&mut self, split_id: SplitId) {
1576        self.labels.remove(&split_id);
1577    }
1578
1579    /// Get the label for a split (if any)
1580    pub fn get_label(&self, split_id: SplitId) -> Option<&str> {
1581        self.labels.get(&split_id).map(|s| s.as_str())
1582    }
1583
1584    /// Get all split labels (for workspace serialization)
1585    pub fn labels(&self) -> &HashMap<SplitId, String> {
1586        &self.labels
1587    }
1588
1589    /// Find the first leaf split with the given label
1590    pub fn find_split_by_label(&self, label: &str) -> Option<LeafId> {
1591        self.root
1592            .leaf_split_ids()
1593            .into_iter()
1594            .find(|id| self.labels.get(&(*id).into()).is_some_and(|l| l == label))
1595    }
1596
1597    /// Find the first leaf split without a label
1598    pub fn find_unlabeled_leaf(&self) -> Option<LeafId> {
1599        self.root
1600            .leaf_split_ids()
1601            .into_iter()
1602            .find(|id| !self.labels.contains_key(&(*id).into()))
1603    }
1604}
1605
1606#[cfg(test)]
1607mod tests {
1608    use super::*;
1609
1610    #[test]
1611    fn test_create_split_manager() {
1612        let buffer_id = BufferId(0);
1613        let manager = SplitManager::new(buffer_id);
1614
1615        assert_eq!(manager.active_buffer_id(), Some(buffer_id));
1616        assert_eq!(manager.root().count_leaves(), 1);
1617    }
1618
1619    #[test]
1620    fn test_horizontal_split() {
1621        let buffer_a = BufferId(0);
1622        let buffer_b = BufferId(1);
1623
1624        let mut manager = SplitManager::new(buffer_a);
1625        let result = manager.split_active(SplitDirection::Horizontal, buffer_b, 0.5);
1626
1627        assert!(result.is_ok());
1628        assert_eq!(manager.root().count_leaves(), 2);
1629    }
1630
1631    #[test]
1632    fn test_vertical_split() {
1633        let buffer_a = BufferId(0);
1634        let buffer_b = BufferId(1);
1635
1636        let mut manager = SplitManager::new(buffer_a);
1637        let result = manager.split_active(SplitDirection::Vertical, buffer_b, 0.5);
1638
1639        assert!(result.is_ok());
1640        assert_eq!(manager.root().count_leaves(), 2);
1641    }
1642
1643    #[test]
1644    fn test_nested_splits() {
1645        let buffer_a = BufferId(0);
1646        let buffer_b = BufferId(1);
1647        let buffer_c = BufferId(2);
1648
1649        let mut manager = SplitManager::new(buffer_a);
1650
1651        // Split horizontally
1652        manager
1653            .split_active(SplitDirection::Horizontal, buffer_b, 0.5)
1654            .unwrap();
1655
1656        // Split the second pane vertically
1657        manager
1658            .split_active(SplitDirection::Vertical, buffer_c, 0.5)
1659            .unwrap();
1660
1661        assert_eq!(manager.root().count_leaves(), 3);
1662    }
1663
1664    #[test]
1665    fn test_close_split() {
1666        let buffer_a = BufferId(0);
1667        let buffer_b = BufferId(1);
1668
1669        let mut manager = SplitManager::new(buffer_a);
1670        let new_split = manager
1671            .split_active(SplitDirection::Horizontal, buffer_b, 0.5)
1672            .unwrap();
1673
1674        assert_eq!(manager.root().count_leaves(), 2);
1675
1676        // Close the new split
1677        let result = manager.close_split(new_split);
1678        assert!(result.is_ok());
1679        assert_eq!(manager.root().count_leaves(), 1);
1680    }
1681
1682    #[test]
1683    fn test_cannot_close_last_split() {
1684        let buffer_a = BufferId(0);
1685        let mut manager = SplitManager::new(buffer_a);
1686
1687        let result = manager.close_split(manager.active_split());
1688        assert!(result.is_err());
1689    }
1690
1691    #[test]
1692    fn test_split_rect_horizontal() {
1693        let rect = Rect {
1694            x: 0,
1695            y: 0,
1696            width: 100,
1697            height: 100,
1698        };
1699
1700        let (first, second) = split_rect(rect, SplitDirection::Horizontal, 0.5);
1701
1702        // With 1 line reserved for separator: (100-1)/2 = 49.5 rounds to 50 and 49
1703        assert_eq!(first.height, 50);
1704        assert_eq!(second.height, 49);
1705        assert_eq!(first.width, 100);
1706        assert_eq!(second.width, 100);
1707        assert_eq!(first.y, 0);
1708        assert_eq!(second.y, 51); // first.y + first.height + 1 (separator)
1709    }
1710
1711    #[test]
1712    fn test_split_rect_vertical() {
1713        let rect = Rect {
1714            x: 0,
1715            y: 0,
1716            width: 100,
1717            height: 100,
1718        };
1719
1720        let (first, second) = split_rect(rect, SplitDirection::Vertical, 0.5);
1721
1722        // With 1 column reserved for separator: (100-1)/2 = 49.5 rounds to 50 and 49
1723        assert_eq!(first.width, 50);
1724        assert_eq!(second.width, 49);
1725        assert_eq!(first.height, 100);
1726        assert_eq!(second.height, 100);
1727        assert_eq!(first.x, 0);
1728        assert_eq!(second.x, 51); // first.x + first.width + 1 (separator)
1729    }
1730
1731    // === Split label tests ===
1732
1733    #[test]
1734    fn test_set_and_get_label() {
1735        let mut manager = SplitManager::new(BufferId(0));
1736        let split = manager.active_split();
1737
1738        assert_eq!(manager.get_label(split.into()), None);
1739
1740        manager.set_label(split, "sidebar".to_string());
1741        assert_eq!(manager.get_label(split.into()), Some("sidebar"));
1742    }
1743
1744    #[test]
1745    fn test_clear_label() {
1746        let mut manager = SplitManager::new(BufferId(0));
1747        let split = manager.active_split();
1748
1749        manager.set_label(split, "sidebar".to_string());
1750        assert!(manager.get_label(split.into()).is_some());
1751
1752        manager.clear_label(split.into());
1753        assert_eq!(manager.get_label(split.into()), None);
1754    }
1755
1756    #[test]
1757    fn test_find_split_by_label() {
1758        let mut manager = SplitManager::new(BufferId(0));
1759        let first_split = manager.active_split();
1760
1761        let second_split = manager
1762            .split_active(SplitDirection::Vertical, BufferId(1), 0.5)
1763            .unwrap();
1764
1765        manager.set_label(first_split, "sidebar".to_string());
1766
1767        assert_eq!(manager.find_split_by_label("sidebar"), Some(first_split));
1768        assert_eq!(manager.find_split_by_label("terminal"), None);
1769
1770        // The second split has no label
1771        assert_ne!(manager.find_split_by_label("sidebar"), Some(second_split));
1772    }
1773
1774    #[test]
1775    fn test_find_unlabeled_leaf() {
1776        let mut manager = SplitManager::new(BufferId(0));
1777        let first_split = manager.active_split();
1778
1779        let second_split = manager
1780            .split_active(SplitDirection::Vertical, BufferId(1), 0.5)
1781            .unwrap();
1782
1783        // No labels — first leaf returned
1784        assert!(manager.find_unlabeled_leaf().is_some());
1785
1786        // Label the first split — unlabeled should return the second
1787        manager.set_label(first_split, "sidebar".to_string());
1788        assert_eq!(manager.find_unlabeled_leaf(), Some(second_split));
1789
1790        // Label both — no unlabeled leaf
1791        manager.set_label(second_split, "terminal".to_string());
1792        assert_eq!(manager.find_unlabeled_leaf(), None);
1793    }
1794
1795    #[test]
1796    fn test_close_split_cleans_up_label() {
1797        let mut manager = SplitManager::new(BufferId(0));
1798        let _first_split = manager.active_split();
1799
1800        let second_split = manager
1801            .split_active(SplitDirection::Vertical, BufferId(1), 0.5)
1802            .unwrap();
1803
1804        manager.set_label(second_split, "sidebar".to_string());
1805        assert_eq!(manager.find_split_by_label("sidebar"), Some(second_split));
1806
1807        manager.close_split(second_split).unwrap();
1808
1809        // Label should be cleaned up
1810        assert_eq!(manager.find_split_by_label("sidebar"), None);
1811        assert_eq!(manager.get_label(second_split.into()), None);
1812    }
1813
1814    #[test]
1815    fn test_label_overwrite() {
1816        let mut manager = SplitManager::new(BufferId(0));
1817        let split = manager.active_split();
1818
1819        manager.set_label(split, "sidebar".to_string());
1820        assert_eq!(manager.get_label(split.into()), Some("sidebar"));
1821
1822        manager.set_label(split, "terminal".to_string());
1823        assert_eq!(manager.get_label(split.into()), Some("terminal"));
1824        assert_eq!(manager.find_split_by_label("sidebar"), None);
1825        assert_eq!(manager.find_split_by_label("terminal"), Some(split));
1826    }
1827
1828    #[test]
1829    fn test_find_unlabeled_leaf_single_split_no_label() {
1830        let manager = SplitManager::new(BufferId(0));
1831        // Single unlabeled split — should return it
1832        assert_eq!(manager.find_unlabeled_leaf(), Some(manager.active_split()));
1833    }
1834
1835    #[test]
1836    fn test_find_unlabeled_leaf_single_split_labeled() {
1837        let mut manager = SplitManager::new(BufferId(0));
1838        let split = manager.active_split();
1839        manager.set_label(split, "only".to_string());
1840        // Only split is labeled — returns None
1841        assert_eq!(manager.find_unlabeled_leaf(), None);
1842    }
1843}