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