1#![warn(clippy::pedantic)]
10#![forbid(unsafe_code)]
11#![deny(missing_docs)]
12
13use ratatui_core::layout::Rect;
14
15use crate::editor::SplitDir;
16
17pub const MAX_LEAVES: usize = 4;
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum Pane {
23 Leaf(usize),
25 Split {
27 dir: SplitDir,
29 ratio: u16,
31 first: Box<Pane>,
33 second: Box<Pane>,
35 },
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub struct LeafBox {
41 pub leaf: usize,
43 pub tab: usize,
45 pub rect: Rect,
47}
48
49impl Pane {
50 #[must_use]
52 pub fn leaf_count(&self) -> usize {
53 match self {
54 Pane::Leaf(_) => 1,
55 Pane::Split { first, second, .. } => first.leaf_count() + second.leaf_count(),
56 }
57 }
58
59 #[must_use]
61 pub fn leaf_tabs(&self) -> Vec<usize> {
62 let mut out = Vec::new();
63 self.collect_tabs(&mut out);
64 out
65 }
66
67 fn collect_tabs(&self, out: &mut Vec<usize>) {
68 match self {
69 Pane::Leaf(t) => out.push(*t),
70 Pane::Split { first, second, .. } => {
71 first.collect_tabs(out);
72 second.collect_tabs(out);
73 }
74 }
75 }
76
77 pub fn clamp_tabs(&mut self, tab_count: usize) {
79 match self {
80 Pane::Leaf(t) => *t = (*t).min(tab_count.saturating_sub(1)),
81 Pane::Split { first, second, .. } => {
82 first.clamp_tabs(tab_count);
83 second.clamp_tabs(tab_count);
84 }
85 }
86 }
87
88 pub fn set_leaf_tab(&mut self, n: usize, tab: usize) {
90 let mut i = 0;
91 self.visit_leaf_mut(&mut i, n, &mut |t| *t = tab);
92 }
93
94 #[must_use]
96 pub fn leaf_tab(&self, n: usize) -> Option<usize> {
97 self.leaf_tabs().get(n).copied()
98 }
99
100 fn visit_leaf_mut(&mut self, i: &mut usize, target: usize, f: &mut impl FnMut(&mut usize)) {
101 match self {
102 Pane::Leaf(t) => {
103 if *i == target {
104 f(t);
105 }
106 *i += 1;
107 }
108 Pane::Split { first, second, .. } => {
109 first.visit_leaf_mut(i, target, f);
110 second.visit_leaf_mut(i, target, f);
111 }
112 }
113 }
114
115 pub fn split_leaf(&mut self, n: usize, dir: SplitDir, new_tab: usize) {
118 if self.leaf_count() >= MAX_LEAVES {
119 return;
120 }
121 let mut i = 0;
122 self.split_leaf_inner(&mut i, n, dir, new_tab);
123 }
124
125 fn split_leaf_inner(&mut self, i: &mut usize, target: usize, dir: SplitDir, new_tab: usize) {
126 match self {
127 Pane::Leaf(t) => {
128 if *i == target {
129 let old = *t;
130 *self = Pane::Split {
131 dir,
132 ratio: 50,
133 first: Box::new(Pane::Leaf(old)),
134 second: Box::new(Pane::Leaf(new_tab)),
135 };
136 }
137 *i += 1;
138 }
139 Pane::Split { first, second, .. } => {
140 first.split_leaf_inner(i, target, dir, new_tab);
141 second.split_leaf_inner(i, target, dir, new_tab);
142 }
143 }
144 }
145
146 #[must_use]
150 pub fn remove_leaf(self, n: usize) -> Option<Pane> {
151 let mut i = 0;
152 self.remove_leaf_inner(&mut i, n)
153 }
154
155 fn remove_leaf_inner(self, i: &mut usize, target: usize) -> Option<Pane> {
156 match self {
157 Pane::Leaf(_) => {
158 let here = *i;
159 *i += 1;
160 if here == target { None } else { Some(self) }
161 }
162 Pane::Split {
163 dir,
164 ratio,
165 first,
166 second,
167 } => {
168 let first = first.remove_leaf_inner(i, target);
169 let second = second.remove_leaf_inner(i, target);
170 match (first, second) {
171 (Some(a), Some(b)) => Some(Pane::Split {
172 dir,
173 ratio,
174 first: Box::new(a),
175 second: Box::new(b),
176 }),
177 (Some(only), None) | (None, Some(only)) => Some(only),
178 (None, None) => None,
179 }
180 }
181 }
182 }
183
184 #[must_use]
186 pub fn layout(&self, area: Rect) -> Vec<LeafBox> {
187 let mut out = Vec::new();
188 let mut i = 0;
189 self.layout_inner(area, &mut i, &mut out);
190 out
191 }
192
193 fn layout_inner(&self, area: Rect, i: &mut usize, out: &mut Vec<LeafBox>) {
194 match self {
195 Pane::Leaf(tab) => {
196 out.push(LeafBox {
197 leaf: *i,
198 tab: *tab,
199 rect: area,
200 });
201 *i += 1;
202 }
203 Pane::Split {
204 dir,
205 ratio,
206 first,
207 second,
208 } => {
209 let (a, _div, b) = split_rects(area, *dir, *ratio);
210 first.layout_inner(a, i, out);
211 second.layout_inner(b, i, out);
212 }
213 }
214 }
215
216 #[must_use]
219 pub fn dividers(&self, area: Rect) -> Vec<(SplitDir, Rect)> {
220 let mut out = Vec::new();
221 self.dividers_inner(area, &mut out);
222 out
223 }
224
225 fn dividers_inner(&self, area: Rect, out: &mut Vec<(SplitDir, Rect)>) {
226 if let Pane::Split {
227 dir,
228 ratio,
229 first,
230 second,
231 } = self
232 {
233 let (a, div, b) = split_rects(area, *dir, *ratio);
234 out.push((*dir, div));
235 first.dividers_inner(a, out);
236 second.dividers_inner(b, out);
237 }
238 }
239
240 #[must_use]
242 pub fn leaf_at(&self, area: Rect, col: u16, row: u16) -> Option<usize> {
243 self.layout(area)
244 .into_iter()
245 .find(|b| contains(b.rect, col, row))
246 .map(|b| b.leaf)
247 }
248
249 pub fn resize_at(&mut self, area: Rect, col: u16, row: u16) -> bool {
252 if let Pane::Split {
253 dir,
254 ratio,
255 first,
256 second,
257 } = self
258 {
259 let (a, div, b) = split_rects(area, *dir, *ratio);
260 if contains(div, col, row) {
261 *ratio = ratio_from_pointer(area, *dir, col, row);
262 return true;
263 }
264 return first.resize_at(a, col, row) || second.resize_at(b, col, row);
265 }
266 false
267 }
268}
269
270fn split_rects(area: Rect, dir: SplitDir, ratio: u16) -> (Rect, Rect, Rect) {
272 let ratio = ratio.clamp(10, 90);
273 match dir {
274 SplitDir::Vertical => {
275 let total = area.width;
276 let w0 = u16::try_from(u32::from(total) * u32::from(ratio) / 100).unwrap_or(u16::MAX);
277 let w0 = w0.clamp(1, total.saturating_sub(2).max(1));
278 (
279 Rect { width: w0, ..area },
280 Rect {
281 x: area.x + w0,
282 y: area.y,
283 width: 1,
284 height: area.height,
285 },
286 Rect {
287 x: area.x + w0 + 1,
288 y: area.y,
289 width: total.saturating_sub(w0 + 1),
290 height: area.height,
291 },
292 )
293 }
294 SplitDir::Horizontal => {
295 let total = area.height;
296 let h0 = u16::try_from(u32::from(total) * u32::from(ratio) / 100).unwrap_or(u16::MAX);
297 let h0 = h0.clamp(1, total.saturating_sub(2).max(1));
298 (
299 Rect { height: h0, ..area },
300 Rect {
301 x: area.x,
302 y: area.y + h0,
303 width: area.width,
304 height: 1,
305 },
306 Rect {
307 x: area.x,
308 y: area.y + h0 + 1,
309 width: area.width,
310 height: total.saturating_sub(h0 + 1),
311 },
312 )
313 }
314 }
315}
316
317fn ratio_from_pointer(area: Rect, dir: SplitDir, col: u16, row: u16) -> u16 {
319 let pct = match dir {
320 SplitDir::Vertical if area.width > 1 => {
321 u32::from(col.saturating_sub(area.x)) * 100 / u32::from(area.width)
322 }
323 SplitDir::Horizontal if area.height > 1 => {
324 u32::from(row.saturating_sub(area.y)) * 100 / u32::from(area.height)
325 }
326 _ => 50,
327 };
328 u16::try_from(pct).unwrap_or(50).clamp(10, 90)
329}
330
331fn contains(rect: Rect, col: u16, row: u16) -> bool {
333 col >= rect.x && col < rect.x + rect.width && row >= rect.y && row < rect.y + rect.height
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 fn area() -> Rect {
341 Rect {
342 x: 0,
343 y: 0,
344 width: 80,
345 height: 24,
346 }
347 }
348
349 #[test]
350 fn split_and_count_and_tabs() {
351 let mut p = Pane::Leaf(0);
352 p.split_leaf(0, SplitDir::Vertical, 1);
353 assert_eq!(p.leaf_count(), 2);
354 assert_eq!(p.leaf_tabs(), vec![0, 1]);
355 p.split_leaf(1, SplitDir::Horizontal, 2);
357 assert_eq!(p.leaf_tabs(), vec![0, 1, 2]);
358 }
359
360 #[test]
361 fn split_caps_at_max_leaves() {
362 let mut p = Pane::Leaf(0);
363 for t in 1..10 {
364 p.split_leaf(0, SplitDir::Vertical, t);
365 }
366 assert_eq!(p.leaf_count(), MAX_LEAVES);
367 }
368
369 #[test]
370 fn remove_collapses_parent() {
371 let mut p = Pane::Leaf(0);
372 p.split_leaf(0, SplitDir::Vertical, 1);
373 p.split_leaf(1, SplitDir::Horizontal, 2); let p = p.remove_leaf(1).unwrap();
375 assert_eq!(p.leaf_tabs(), vec![0, 2]);
376 let single = p.remove_leaf(0).unwrap();
377 assert_eq!(single.leaf_tabs(), vec![2]);
378 assert!(single.remove_leaf(0).is_none());
379 }
380
381 #[test]
382 fn layout_partitions_the_area() {
383 let mut p = Pane::Leaf(0);
384 p.split_leaf(0, SplitDir::Vertical, 1);
385 let boxes = p.layout(area());
386 assert_eq!(boxes.len(), 2);
387 assert_eq!(boxes[0].rect.x, 0);
388 assert!(boxes[1].rect.x > boxes[0].rect.width); assert_eq!(p.leaf_at(area(), 5, 5), Some(0));
391 }
392
393 #[test]
394 fn resize_changes_ratio_on_divider() {
395 let mut p = Pane::Leaf(0);
396 p.split_leaf(0, SplitDir::Vertical, 1);
397 let div = p.dividers(area())[0].1;
398 assert!(
399 p.resize_at(area(), div.x, div.y),
400 "pointer on divider resizes"
401 );
402 assert!(!p.resize_at(area(), 1, 1));
404 }
405}