1pub const MAX_PANES: usize = 4;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum SplitKind {
11 #[default]
12 None,
13 Vertical,
15 Horizontal,
17}
18
19#[derive(Debug, Clone)]
20pub struct Pane {
21 pub tab_index: usize,
22 pub scroll: usize,
23 pub cursor: (usize, usize),
25}
26
27impl Default for Pane {
28 fn default() -> Self {
29 Self {
30 tab_index: 0,
31 scroll: 0,
32 cursor: (0, 0),
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum SplitAdd {
40 Opened,
41 Added,
42 Full,
43 MixedKind,
45}
46
47#[derive(Debug, Clone)]
48pub struct SplitState {
49 pub kind: SplitKind,
50 pub ratio: f32,
52 pub focus: usize,
54 pub panes: Vec<Pane>,
55 pub pending_chord: bool,
57}
58
59impl Default for SplitState {
60 fn default() -> Self {
61 Self {
62 kind: SplitKind::None,
63 ratio: 0.5,
64 focus: 0,
65 panes: vec![Pane::default(), Pane::default()],
66 pending_chord: false,
67 }
68 }
69}
70
71impl SplitState {
72 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn is_split(&self) -> bool {
77 self.kind != SplitKind::None && self.panes.len() >= 2
78 }
79
80 pub fn pane_count(&self) -> usize {
81 if self.is_split() {
82 self.panes.len()
83 } else {
84 1
85 }
86 }
87
88 fn clamp_focus(&self) -> usize {
89 self.focus.min(self.panes.len().saturating_sub(1))
90 }
91
92 pub fn focused_pane(&self) -> &Pane {
93 &self.panes[self.clamp_focus()]
94 }
95
96 pub fn focused_pane_mut(&mut self) -> &mut Pane {
97 let i = self.clamp_focus();
98 &mut self.panes[i]
99 }
100
101 pub fn open_split(
104 &mut self,
105 kind: SplitKind,
106 tab: usize,
107 scroll: usize,
108 cursor: (usize, usize),
109 ) -> SplitAdd {
110 if kind == SplitKind::None {
111 self.close();
112 return SplitAdd::Opened;
113 }
114 self.pending_chord = false;
115 if self.is_split() {
116 if self.kind != kind {
117 return SplitAdd::MixedKind;
118 }
119 if self.panes.len() >= MAX_PANES {
120 return SplitAdd::Full;
121 }
122 let at = self.clamp_focus() + 1;
124 self.panes.insert(
125 at,
126 Pane {
127 tab_index: tab,
128 scroll,
129 cursor,
130 },
131 );
132 self.focus = at;
133 return SplitAdd::Added;
134 }
135 self.kind = kind;
136 self.ratio = 0.5;
137 self.focus = 0;
138 self.panes = vec![
139 Pane {
140 tab_index: tab,
141 scroll,
142 cursor,
143 },
144 Pane {
146 tab_index: tab,
147 scroll,
148 cursor,
149 },
150 ];
151 SplitAdd::Opened
152 }
153
154 pub fn remove_focused(&mut self) -> Option<Pane> {
157 if !self.is_split() {
158 return None;
159 }
160 let idx = self.clamp_focus();
161 self.panes.remove(idx);
162 self.focus = idx.min(self.panes.len().saturating_sub(1));
163 if self.panes.len() < 2 {
164 let survivor = self.panes.first().cloned();
165 self.close_keep_panes();
166 return survivor;
167 }
168 Some(self.focused_pane().clone())
169 }
170
171 pub fn close(&mut self) {
172 self.kind = SplitKind::None;
173 self.focus = 0;
174 self.pending_chord = false;
175 self.panes = vec![Pane::default(), Pane::default()];
176 }
177
178 fn close_keep_panes(&mut self) {
179 self.kind = SplitKind::None;
180 self.focus = 0;
181 self.pending_chord = false;
182 if self.panes.is_empty() {
183 self.panes = vec![Pane::default()];
184 }
185 while self.panes.len() < 2 {
186 let last = self.panes.last().cloned().unwrap_or_default();
187 self.panes.push(last);
188 }
189 }
190
191 pub fn focus_other(&mut self) {
192 if self.is_split() {
193 self.focus = (self.clamp_focus() + 1) % self.panes.len();
194 }
195 }
196
197 pub fn set_focus(&mut self, idx: usize) {
198 if self.is_split() {
199 self.focus = idx.min(self.panes.len() - 1);
200 }
201 }
202
203 pub fn adjust_ratio(&mut self, delta: f32) {
204 self.ratio = (self.ratio + delta).clamp(0.2, 0.8);
205 }
206
207 pub fn equalize(&mut self) {
208 self.ratio = 0.5;
209 }
210
211 pub fn clamp_tabs(&mut self, n_tabs: usize) {
213 if n_tabs == 0 {
214 return;
215 }
216 for p in &mut self.panes {
217 if p.tab_index >= n_tabs {
218 p.tab_index = n_tabs - 1;
219 }
220 }
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn repeated_splits_add_panes_up_to_cap() {
230 let mut s = SplitState::new();
231 assert_eq!(s.open_split(SplitKind::Vertical, 0, 0, (0, 0)), SplitAdd::Opened);
232 assert_eq!(s.pane_count(), 2);
233 assert_eq!(s.open_split(SplitKind::Vertical, 1, 3, (3, 0)), SplitAdd::Added);
234 assert_eq!(s.pane_count(), 3);
235 assert_eq!(s.focus, 1);
237 assert_eq!(s.focused_pane().tab_index, 1);
238 assert_eq!(s.open_split(SplitKind::Vertical, 0, 0, (0, 0)), SplitAdd::Added);
239 assert_eq!(s.pane_count(), 4);
240 assert_eq!(s.open_split(SplitKind::Vertical, 0, 0, (0, 0)), SplitAdd::Full);
241 assert_eq!(s.open_split(SplitKind::Horizontal, 0, 0, (0, 0)), SplitAdd::MixedKind);
242 }
243
244 #[test]
245 fn remove_focused_collapses_to_single() {
246 let mut s = SplitState::new();
247 s.open_split(SplitKind::Vertical, 0, 0, (0, 0));
248 s.open_split(SplitKind::Vertical, 2, 9, (9, 0)); s.set_focus(1);
250 let survivor = s.remove_focused().expect("still split");
251 assert!(s.is_split());
253 assert_eq!(s.pane_count(), 2);
254 assert_eq!(survivor.tab_index, s.focused_pane().tab_index);
255 let last = s.remove_focused().expect("survivor");
257 assert!(!s.is_split());
258 assert_eq!(last.tab_index, 0);
259 }
260
261 #[test]
262 fn focus_cycles_all_panes() {
263 let mut s = SplitState::new();
264 s.open_split(SplitKind::Horizontal, 0, 0, (0, 0));
265 s.open_split(SplitKind::Horizontal, 0, 0, (0, 0));
266 assert_eq!(s.pane_count(), 3);
267 s.set_focus(0);
268 s.focus_other();
269 s.focus_other();
270 assert_eq!(s.focus, 2);
271 s.focus_other();
272 assert_eq!(s.focus, 0);
273 }
274}