Skip to main content

tmprl_ui/
tree.rs

1//! One tab's window tree.
2
3use crate::{Axis, Direction, Rect, ViewId};
4
5/// A window, laid out.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct Pane {
8    pub view: ViewId,
9    pub rect: Rect,
10    pub focused: bool,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14enum Node {
15    Leaf(ViewId),
16    Split {
17        axis: Axis,
18        children: Vec<Node>,
19        /// Relative sizes, not absolute cells. A tree laid out at one terminal size and then
20        /// at another keeps its proportions, which is what makes a resize of the terminal
21        /// not scramble a layout the reader arranged.
22        weights: Vec<u16>,
23    },
24}
25
26/// A tree of windows, with one of them focused.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Tree {
29    root: Node,
30    /// Child indices from the root down to the focused leaf. Empty means the root is a leaf.
31    focus: Vec<usize>,
32}
33
34/// The weight a freshly split window gets. Any constant works (only ratios matter) but a
35/// round number keeps `resize` arithmetic legible when debugging a layout.
36const DEFAULT_WEIGHT: u16 = 100;
37
38impl Tree {
39    pub fn new(view: ViewId) -> Self {
40        Self {
41            root: Node::Leaf(view),
42            focus: Vec::new(),
43        }
44    }
45
46    pub fn focused(&self) -> ViewId {
47        match Self::at(&self.root, &self.focus) {
48            Some(Node::Leaf(v)) => *v,
49            // Unreachable while `focus` always points at a leaf, which every mutation
50            // maintains. Falling back to the first view beats panicking in a renderer.
51            _ => self.views().first().copied().unwrap_or(ViewId(0)),
52        }
53    }
54
55    /// Focus a named window, wherever it sits in the tree. `false` if it is not in this one.
56    ///
57    /// Distinct from [`Tree::focus_direction`], which asks "what is to the left of here"
58    /// and needs a geometry to answer. A picker already knows exactly which window it
59    /// means, so making it aim with direction keys would be the picker's whole point
60    /// thrown away.
61    pub fn focus_view(&mut self, view: ViewId) -> bool {
62        let mut path = Vec::new();
63        if Self::path_to(&self.root, view, &mut path) {
64            self.focus = path;
65            return true;
66        }
67        false
68    }
69
70    /// Depth-first walk for the child indices leading to `view`.
71    ///
72    /// `path` is left holding the route on success. On failure every frame pops what it
73    /// pushed, so a caller's vector is untouched by a search that found nothing.
74    fn path_to(node: &Node, view: ViewId, path: &mut Vec<usize>) -> bool {
75        match node {
76            Node::Leaf(v) => *v == view,
77            Node::Split { children, .. } => {
78                for (i, child) in children.iter().enumerate() {
79                    path.push(i);
80                    if Self::path_to(child, view, path) {
81                        return true;
82                    }
83                    path.pop();
84                }
85                false
86            }
87        }
88    }
89
90    /// Every view in the tree, left to right, top to bottom.
91    pub fn views(&self) -> Vec<ViewId> {
92        let mut out = Vec::new();
93        Self::walk(&self.root, &mut |v| out.push(v));
94        out
95    }
96
97    pub fn len(&self) -> usize {
98        self.views().len()
99    }
100
101    pub fn is_empty(&self) -> bool {
102        false // a tree always has at least one leaf
103    }
104
105    /// Split the focused window, and focus the new one, as vim does.
106    pub fn split(&mut self, axis: Axis, view: ViewId) {
107        let path = self.focus.clone();
108        let Some(node) = Self::at_mut(&mut self.root, &path) else {
109            return;
110        };
111
112        match node {
113            // Splitting a leaf turns it into a two-child split.
114            Node::Leaf(existing) => {
115                *node = Node::Split {
116                    axis,
117                    children: vec![Node::Leaf(*existing), Node::Leaf(view)],
118                    weights: vec![DEFAULT_WEIGHT, DEFAULT_WEIGHT],
119                };
120                self.focus.push(1);
121            }
122            Node::Split { .. } => {}
123        }
124
125        // If the new window's parent splits on the same axis as its grandparent, the nesting
126        // is redundant, vim flattens it, and so does this, or `<C-w>l` would have to step
127        // through invisible levels.
128        self.flatten();
129    }
130
131    /// Close the focused window. Returns false when it is the only one, since a tab with no
132    /// windows has nothing to draw.
133    pub fn close(&mut self) -> bool {
134        if self.focus.is_empty() {
135            return false;
136        }
137        let (parent_path, index) = {
138            let mut p = self.focus.clone();
139            let i = p.pop().expect("focus is non-empty");
140            (p, i)
141        };
142
143        let Some(Node::Split {
144            children, weights, ..
145        }) = Self::at_mut(&mut self.root, &parent_path)
146        else {
147            return false;
148        };
149        children.remove(index);
150        weights.remove(index);
151
152        // A split with one child left is not a split any more.
153        if children.len() == 1 {
154            let only = children.remove(0);
155            let Some(parent) = Self::at_mut(&mut self.root, &parent_path) else {
156                return false;
157            };
158            *parent = only;
159            self.focus = parent_path;
160            // Focus must land on a leaf, not on whatever the collapsed child happened to be.
161            self.descend_to_leaf();
162        } else {
163            // Focus the neighbour that took its place, or the last one if it was the last.
164            self.focus = parent_path;
165            self.focus.push(index.min(children.len() - 1));
166            self.descend_to_leaf();
167        }
168        self.flatten();
169        true
170    }
171
172    /// Move focus geometrically, the way `<C-w>hjkl` does.
173    ///
174    /// Geometric rather than tree-structural: the window to the right is the one that *looks*
175    /// to the right, which is not always a sibling. Candidates must overlap this window
176    /// across the direction's axis, so pressing `<C-w>j` in a tall left-hand pane does not
177    /// jump to something in a different column that happens to sit lower.
178    ///
179    /// Returns false when there is nothing that way.
180    pub fn focus_direction(&mut self, dir: Direction, area: Rect) -> bool {
181        let panes = self.layout_with_paths(area);
182        let Some((_, from)) = panes.iter().find(|(_, p)| p.focused).map(|(a, b)| (a, *b)) else {
183            return false;
184        };
185
186        let axis = dir.axis();
187        let best = panes
188            .iter()
189            .filter(|(_, p)| !p.focused)
190            .filter(|(_, p)| from.rect.overlaps_across(p.rect, axis))
191            .filter(|(_, p)| match dir {
192                Direction::Left => p.rect.right() <= from.rect.x,
193                Direction::Right => p.rect.x >= from.rect.right(),
194                Direction::Up => p.rect.bottom() <= from.rect.y,
195                Direction::Down => p.rect.y >= from.rect.bottom(),
196            })
197            // Nearest edge first, then nearest along the other axis, so a column of
198            // candidates resolves to the one closest to where the cursor already is.
199            .min_by_key(|(_, p)| {
200                let gap = match dir {
201                    Direction::Left => from.rect.x.saturating_sub(p.rect.right()),
202                    Direction::Right => p.rect.x.saturating_sub(from.rect.right()),
203                    Direction::Up => from.rect.y.saturating_sub(p.rect.bottom()),
204                    Direction::Down => p.rect.y.saturating_sub(from.rect.bottom()),
205                };
206                let offset = match axis {
207                    Axis::Columns => p.rect.y.abs_diff(from.rect.y),
208                    Axis::Rows => p.rect.x.abs_diff(from.rect.x),
209                };
210                (gap, offset)
211            });
212
213        match best {
214            Some((path, _)) => {
215                self.focus = path.clone();
216                true
217            }
218            None => false,
219        }
220    }
221
222    /// Grow or shrink the focused window along `dir` by `cells` worth of weight.
223    ///
224    /// Applied to the nearest ancestor that actually splits on that axis: asking a
225    /// side-by-side split to get taller is meaningless, and silently doing nothing there
226    /// would look like a broken key.
227    pub fn resize(&mut self, dir: Direction, delta: i32) -> bool {
228        let axis = dir.axis();
229        let mut path = self.focus.clone();
230
231        while !path.is_empty() {
232            let index = *path.last().expect("non-empty");
233            let parent_path = &path[..path.len() - 1];
234            let is_match = matches!(
235                Self::at(&self.root, parent_path),
236                Some(Node::Split { axis: a, .. }) if *a == axis
237            );
238            if is_match {
239                let Some(Node::Split { weights, .. }) = Self::at_mut(&mut self.root, parent_path)
240                else {
241                    return false;
242                };
243                if weights.len() < 2 {
244                    return false;
245                }
246                // Growing one window shrinks its neighbour: the parent's total is fixed, so
247                // weight has to come from somewhere rather than being conjured.
248                let neighbour = if index + 1 < weights.len() {
249                    index + 1
250                } else {
251                    index - 1
252                };
253                let signed = if dir.is_forward() { delta } else { -delta };
254                let taken =
255                    signed.clamp(-(weights[index] as i32 - 1), weights[neighbour] as i32 - 1);
256                if taken == 0 {
257                    return false;
258                }
259                weights[index] = (weights[index] as i32 + taken) as u16;
260                weights[neighbour] = (weights[neighbour] as i32 - taken) as u16;
261                return true;
262            }
263            path.pop();
264        }
265        false
266    }
267
268    /// Give every window in every split the same share, as `<C-w>=` does.
269    pub fn equalize(&mut self) {
270        Self::equalize_node(&mut self.root);
271    }
272
273    /// Where everything goes, given the space available.
274    pub fn layout(&self, area: Rect) -> Vec<Pane> {
275        self.layout_with_paths(area)
276            .into_iter()
277            .map(|(_, pane)| pane)
278            .collect()
279    }
280
281    fn layout_with_paths(&self, area: Rect) -> Vec<(Vec<usize>, Pane)> {
282        let mut out = Vec::new();
283        Self::place(&self.root, area, &mut Vec::new(), &self.focus, &mut out);
284        out
285    }
286
287    fn place(
288        node: &Node,
289        area: Rect,
290        path: &mut Vec<usize>,
291        focus: &[usize],
292        out: &mut Vec<(Vec<usize>, Pane)>,
293    ) {
294        match node {
295            Node::Leaf(view) => out.push((
296                path.clone(),
297                Pane {
298                    view: *view,
299                    rect: area,
300                    focused: path.as_slice() == focus,
301                },
302            )),
303            Node::Split {
304                axis,
305                children,
306                weights,
307            } => {
308                for (i, (child, rect)) in children
309                    .iter()
310                    .zip(divide(area, *axis, weights))
311                    .enumerate()
312                {
313                    path.push(i);
314                    Self::place(child, rect, path, focus, out);
315                    path.pop();
316                }
317            }
318        }
319    }
320
321    fn at<'a>(node: &'a Node, path: &[usize]) -> Option<&'a Node> {
322        match path.split_first() {
323            None => Some(node),
324            Some((i, rest)) => match node {
325                Node::Split { children, .. } => Self::at(children.get(*i)?, rest),
326                Node::Leaf(_) => None,
327            },
328        }
329    }
330
331    fn at_mut<'a>(node: &'a mut Node, path: &[usize]) -> Option<&'a mut Node> {
332        match path.split_first() {
333            None => Some(node),
334            Some((i, rest)) => match node {
335                Node::Split { children, .. } => Self::at_mut(children.get_mut(*i)?, rest),
336                Node::Leaf(_) => None,
337            },
338        }
339    }
340
341    fn walk(node: &Node, f: &mut impl FnMut(ViewId)) {
342        match node {
343            Node::Leaf(v) => f(*v),
344            Node::Split { children, .. } => {
345                for c in children {
346                    Self::walk(c, f);
347                }
348            }
349        }
350    }
351
352    /// Push focus down to a leaf, taking the first child at each level.
353    fn descend_to_leaf(&mut self) {
354        loop {
355            match Self::at(&self.root, &self.focus) {
356                Some(Node::Split { children, .. }) if !children.is_empty() => self.focus.push(0),
357                _ => return,
358            }
359        }
360    }
361
362    /// Merge a split into its parent when both divide on the same axis.
363    fn flatten(&mut self) {
364        let focused = self.focused();
365        Self::flatten_node(&mut self.root);
366        // Flattening renumbers children, so the old path may point elsewhere. Re-find the
367        // view that was focused rather than trusting the indices.
368        if let Some(path) = Self::path_of(&self.root, focused, &mut Vec::new()) {
369            self.focus = path;
370        }
371    }
372
373    fn flatten_node(node: &mut Node) {
374        let Node::Split {
375            axis,
376            children,
377            weights,
378        } = node
379        else {
380            return;
381        };
382        for c in children.iter_mut() {
383            Self::flatten_node(c);
384        }
385
386        let mut new_children = Vec::new();
387        let mut new_weights = Vec::new();
388        for (child, weight) in std::mem::take(children)
389            .into_iter()
390            .zip(std::mem::take(weights))
391        {
392            match child {
393                Node::Split {
394                    axis: inner_axis,
395                    children: inner,
396                    weights: inner_weights,
397                } if inner_axis == *axis => {
398                    // Redistribute the parent's share of this slot across the children that
399                    // are being promoted into it, so the layout does not visibly jump.
400                    let total: u32 = inner_weights.iter().map(|w| *w as u32).sum::<u32>().max(1);
401                    for (c, w) in inner.into_iter().zip(inner_weights) {
402                        new_children.push(c);
403                        new_weights.push(
404                            ((w as u32 * weight as u32) / total)
405                                .max(1)
406                                .min(u16::MAX as u32) as u16,
407                        );
408                    }
409                }
410                other => {
411                    new_children.push(other);
412                    new_weights.push(weight);
413                }
414            }
415        }
416        *children = new_children;
417        *weights = new_weights;
418    }
419
420    fn path_of(node: &Node, view: ViewId, path: &mut Vec<usize>) -> Option<Vec<usize>> {
421        match node {
422            Node::Leaf(v) if *v == view => Some(path.clone()),
423            Node::Leaf(_) => None,
424            Node::Split { children, .. } => {
425                for (i, c) in children.iter().enumerate() {
426                    path.push(i);
427                    if let Some(found) = Self::path_of(c, view, path) {
428                        return Some(found);
429                    }
430                    path.pop();
431                }
432                None
433            }
434        }
435    }
436
437    fn equalize_node(node: &mut Node) {
438        if let Node::Split {
439            children, weights, ..
440        } = node
441        {
442            for w in weights.iter_mut() {
443                *w = DEFAULT_WEIGHT;
444            }
445            for c in children.iter_mut() {
446                Self::equalize_node(c);
447            }
448        }
449    }
450}
451
452/// Divide `area` along `axis` in proportion to `weights`.
453///
454/// Every pane gets at least one cell, and the remainder from integer division goes to the
455/// earliest panes. Without the floor, a heavily lopsided split renders a zero-width window
456/// that cannot be seen or focused out of.
457fn divide(area: Rect, axis: Axis, weights: &[u16]) -> Vec<Rect> {
458    let n = weights.len();
459    if n == 0 {
460        return Vec::new();
461    }
462    let total_extent = area.extent(axis);
463    // Not enough room for one cell each: hand out what there is and let the rest be empty,
464    // rather than overlapping panes on top of each other.
465    if (total_extent as usize) < n {
466        return (0..n)
467            .map(|i| {
468                let mut r = area;
469                let at = area_start(area, axis) + i as u16;
470                set_span(
471                    &mut r,
472                    axis,
473                    at,
474                    if (i as u16) < total_extent { 1 } else { 0 },
475                );
476                r
477            })
478            .collect();
479    }
480
481    let sum: u32 = weights
482        .iter()
483        .map(|w| (*w).max(1) as u32)
484        .sum::<u32>()
485        .max(1);
486    let spare = total_extent as u32 - n as u32; // one cell already reserved per pane
487    let mut spans: Vec<u16> = weights
488        .iter()
489        .map(|w| 1 + ((*w).max(1) as u32 * spare / sum) as u16)
490        .collect();
491
492    // Integer division leaves cells over; give them to the earliest panes so the total is
493    // exactly the space available and no column goes unpainted.
494    let assigned: u32 = spans.iter().map(|s| *s as u32).sum();
495    let mut leftover = total_extent as u32 - assigned;
496    let mut i = 0;
497    while leftover > 0 {
498        spans[i % n] += 1;
499        leftover -= 1;
500        i += 1;
501    }
502
503    let mut out = Vec::with_capacity(n);
504    let mut at = area_start(area, axis);
505    for span in spans {
506        let mut r = area;
507        set_span(&mut r, axis, at, span);
508        out.push(r);
509        at += span;
510    }
511    out
512}
513
514fn area_start(area: Rect, axis: Axis) -> u16 {
515    match axis {
516        Axis::Columns => area.x,
517        Axis::Rows => area.y,
518    }
519}
520
521fn set_span(r: &mut Rect, axis: Axis, at: u16, span: u16) {
522    match axis {
523        Axis::Columns => {
524            r.x = at;
525            r.width = span;
526        }
527        Axis::Rows => {
528            r.y = at;
529            r.height = span;
530        }
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    fn v(n: u64) -> ViewId {
539        ViewId(n)
540    }
541
542    /// A generous area so proportional division lands on round numbers.
543    const AREA: Rect = Rect {
544        x: 0,
545        y: 0,
546        width: 100,
547        height: 40,
548    };
549
550    fn rects(tree: &Tree) -> Vec<(u64, Rect)> {
551        tree.layout(AREA)
552            .into_iter()
553            .map(|p| (p.view.0, p.rect))
554            .collect()
555    }
556
557    #[test]
558    fn one_window_fills_the_area() {
559        let tree = Tree::new(v(1));
560        assert_eq!(rects(&tree), [(1, AREA)]);
561        assert_eq!(tree.focused(), v(1));
562        assert_eq!(tree.len(), 1);
563    }
564
565    #[test]
566    fn a_column_split_divides_the_width_and_focuses_the_new_window() {
567        let mut tree = Tree::new(v(1));
568        tree.split(Axis::Columns, v(2));
569
570        assert_eq!(tree.focused(), v(2), "vim focuses the window it just made");
571        assert_eq!(
572            rects(&tree),
573            [(1, Rect::new(0, 0, 50, 40)), (2, Rect::new(50, 0, 50, 40))]
574        );
575    }
576
577    #[test]
578    fn a_row_split_divides_the_height() {
579        let mut tree = Tree::new(v(1));
580        tree.split(Axis::Rows, v(2));
581        assert_eq!(
582            rects(&tree),
583            [
584                (1, Rect::new(0, 0, 100, 20)),
585                (2, Rect::new(0, 20, 100, 20))
586            ]
587        );
588    }
589
590    #[test]
591    fn panes_tile_the_area_exactly_with_no_gap_or_overlap() {
592        // Integer division leaves cells over; unassigned they show as an unpainted column.
593        let mut tree = Tree::new(v(1));
594        tree.split(Axis::Columns, v(2));
595        tree.split(Axis::Columns, v(3));
596
597        let area = Rect::new(0, 0, 100, 40); // 100 / 3 does not divide
598        let mut panes = tree.layout(area);
599        panes.sort_by_key(|p| p.rect.x);
600
601        assert_eq!(panes[0].rect.x, 0);
602        for pair in panes.windows(2) {
603            assert_eq!(
604                pair[0].rect.right(),
605                pair[1].rect.x,
606                "panes must abut exactly"
607            );
608        }
609        assert_eq!(panes.last().unwrap().rect.right(), area.right());
610    }
611
612    #[test]
613    fn splitting_on_the_same_axis_flattens_instead_of_nesting() {
614        // Nested same-axis splits look identical but make focus movement step through
615        // invisible levels.
616        let mut tree = Tree::new(v(1));
617        tree.split(Axis::Columns, v(2));
618        tree.split(Axis::Columns, v(3));
619
620        assert_eq!(tree.len(), 3);
621        assert_eq!(tree.views(), [v(1), v(2), v(3)]);
622        assert_eq!(tree.focused(), v(3), "focus survives the flattening");
623
624        // Flattening must not move anything on screen. vim's `:vsplit` halves the *current*
625        // window, so splitting twice gives one half and two quarters, not three thirds.
626        // The nesting is what goes away, not the proportions.
627        let widths: Vec<u16> = tree.layout(AREA).iter().map(|p| p.rect.width).collect();
628        assert_eq!(widths, [50, 25, 25], "flattening must preserve the layout");
629
630        // Evening them up is `<C-w>=`, a separate decision.
631        tree.equalize();
632        let widths: Vec<u16> = tree.layout(AREA).iter().map(|p| p.rect.width).collect();
633        assert!(
634            widths.iter().max().unwrap() - widths.iter().min().unwrap() <= 1,
635            "equalize should even them out, got {widths:?}"
636        );
637    }
638
639    #[test]
640    fn splitting_on_the_other_axis_does_nest() {
641        let mut tree = Tree::new(v(1));
642        tree.split(Axis::Columns, v(2));
643        tree.split(Axis::Rows, v(3));
644
645        // 2 was the right half; it is now split top and bottom.
646        let layout = rects(&tree);
647        assert_eq!(layout.len(), 3);
648        assert!(layout.contains(&(1, Rect::new(0, 0, 50, 40))));
649        assert!(layout.contains(&(2, Rect::new(50, 0, 50, 20))));
650        assert!(layout.contains(&(3, Rect::new(50, 20, 50, 20))));
651    }
652
653    #[test]
654    fn focus_moves_to_what_looks_that_way() {
655        let mut tree = Tree::new(v(1));
656        tree.split(Axis::Columns, v(2)); // 1 | 2, focus on 2
657
658        assert!(tree.focus_direction(Direction::Left, AREA));
659        assert_eq!(tree.focused(), v(1));
660        assert!(tree.focus_direction(Direction::Right, AREA));
661        assert_eq!(tree.focused(), v(2));
662    }
663
664    #[test]
665    fn focus_does_not_move_off_the_edge() {
666        let mut tree = Tree::new(v(1));
667        tree.split(Axis::Columns, v(2));
668        tree.focus_direction(Direction::Left, AREA); // on 1, the leftmost
669
670        assert!(
671            !tree.focus_direction(Direction::Left, AREA),
672            "nothing there"
673        );
674        assert_eq!(tree.focused(), v(1), "and focus stays put");
675        assert!(!tree.focus_direction(Direction::Up, AREA));
676    }
677
678    #[test]
679    fn focus_skips_windows_that_do_not_share_any_rows() {
680        // 1 fills the left; 2 over 3 on the right. From 1, `l` must reach 2, the one it
681        // shares rows with, not 3.
682
683        let mut tree = Tree::new(v(1));
684        tree.split(Axis::Columns, v(2));
685        tree.split(Axis::Rows, v(3));
686        tree.focus_direction(Direction::Left, AREA);
687        assert_eq!(tree.focused(), v(1));
688
689        assert!(tree.focus_direction(Direction::Right, AREA));
690        assert_eq!(tree.focused(), v(2), "the top-right shares row 0 with 1");
691
692        // And from 2, down reaches 3 but right reaches nothing.
693        assert!(tree.focus_direction(Direction::Down, AREA));
694        assert_eq!(tree.focused(), v(3));
695        assert!(!tree.focus_direction(Direction::Right, AREA));
696    }
697
698    #[test]
699    fn closing_a_window_gives_its_space_to_the_survivor() {
700        let mut tree = Tree::new(v(1));
701        tree.split(Axis::Columns, v(2));
702
703        assert!(tree.close());
704        assert_eq!(tree.len(), 1);
705        assert_eq!(tree.focused(), v(1));
706        assert_eq!(rects(&tree), [(1, AREA)], "the split collapsed entirely");
707    }
708
709    #[test]
710    fn closing_the_last_window_is_refused() {
711        // A tab with no windows has nothing to draw; quitting is a separate decision.
712        let mut tree = Tree::new(v(1));
713        assert!(!tree.close());
714        assert_eq!(tree.len(), 1);
715    }
716
717    #[test]
718    fn closing_focuses_a_neighbour_and_never_a_split() {
719        let mut tree = Tree::new(v(1));
720        tree.split(Axis::Columns, v(2));
721        tree.split(Axis::Columns, v(3)); // 1 | 2 | 3, focus 3
722
723        assert!(tree.close());
724        assert_eq!(tree.views(), [v(1), v(2)]);
725        assert_eq!(tree.focused(), v(2), "focus falls back to the neighbour");
726
727        // Focus must be a real window afterwards, not an interior node.
728        assert!(tree.layout(AREA).iter().any(|p| p.focused));
729    }
730
731    #[test]
732    fn closing_a_nested_window_collapses_its_parent_onto_a_leaf() {
733        let mut tree = Tree::new(v(1));
734        tree.split(Axis::Columns, v(2));
735        tree.split(Axis::Rows, v(3)); // right half split; focus 3
736
737        assert!(tree.close());
738        assert_eq!(tree.len(), 2);
739        assert_eq!(
740            rects(&tree),
741            [(1, Rect::new(0, 0, 50, 40)), (2, Rect::new(50, 0, 50, 40))],
742            "2 should reclaim the whole right half"
743        );
744        assert_eq!(tree.focused(), v(2));
745        assert_eq!(
746            tree.layout(AREA).iter().filter(|p| p.focused).count(),
747            1,
748            "exactly one window is focused"
749        );
750    }
751
752    #[test]
753    fn resizing_takes_from_the_neighbour_rather_than_conjuring_space() {
754        let mut tree = Tree::new(v(1));
755        tree.split(Axis::Columns, v(2)); // focus 2, the right half
756
757        let before: u16 = tree.layout(AREA)[0].rect.width;
758        assert!(tree.resize(Direction::Right, 50));
759
760        let after = tree.layout(AREA);
761        assert!(after[1].rect.width > before, "the focused window grew");
762        assert!(after[0].rect.width < before, "its neighbour gave the space");
763        assert_eq!(
764            after[0].rect.width + after[1].rect.width,
765            AREA.width,
766            "the total is unchanged"
767        );
768    }
769
770    #[test]
771    fn resizing_along_an_axis_with_no_split_does_nothing() {
772        // Asking a side-by-side split to get taller is meaningless.
773        let mut tree = Tree::new(v(1));
774        tree.split(Axis::Columns, v(2));
775        assert!(!tree.resize(Direction::Down, 10));
776        assert!(
777            !Tree::new(v(1)).resize(Direction::Right, 10),
778            "one window alone"
779        );
780    }
781
782    #[test]
783    fn a_window_can_never_be_resized_out_of_existence() {
784        let mut tree = Tree::new(v(1));
785        tree.split(Axis::Columns, v(2));
786        for _ in 0..50 {
787            tree.resize(Direction::Right, 1_000);
788        }
789        for pane in tree.layout(AREA) {
790            assert!(pane.rect.width >= 1, "a zero-width window cannot be seen");
791        }
792    }
793
794    #[test]
795    fn equalize_undoes_a_resize() {
796        let mut tree = Tree::new(v(1));
797        tree.split(Axis::Columns, v(2));
798        tree.resize(Direction::Right, 60);
799        tree.equalize();
800
801        let widths: Vec<u16> = tree.layout(AREA).iter().map(|p| p.rect.width).collect();
802        assert!(
803            widths[0].abs_diff(widths[1]) <= 1,
804            "expected even columns, got {widths:?}"
805        );
806    }
807
808    #[test]
809    fn a_layout_keeps_its_proportions_at_a_different_terminal_size() {
810        // Weights are relative, so resizing the terminal must not scramble an arrangement
811        // the reader set up.
812        let mut tree = Tree::new(v(1));
813        tree.split(Axis::Columns, v(2));
814        tree.resize(Direction::Right, 50);
815
816        let wide = tree.layout(Rect::new(0, 0, 200, 40));
817        let narrow = tree.layout(Rect::new(0, 0, 100, 40));
818        let ratio = |p: &[Pane]| p[1].rect.width as f32 / p[0].rect.width as f32;
819        assert!(
820            (ratio(&wide) - ratio(&narrow)).abs() < 0.15,
821            "proportions drifted: {} vs {}",
822            ratio(&wide),
823            ratio(&narrow)
824        );
825    }
826
827    #[test]
828    fn a_tiny_area_does_not_produce_overlapping_windows() {
829        // Three windows in two columns cannot all be seen; they must still not be drawn on
830        // top of each other.
831        let mut tree = Tree::new(v(1));
832        tree.split(Axis::Columns, v(2));
833        tree.split(Axis::Columns, v(3));
834
835        let panes = tree.layout(Rect::new(0, 0, 2, 1));
836        for pair in panes.windows(2) {
837            assert!(
838                pair[0].rect.right() <= pair[1].rect.x,
839                "windows overlap: {:?}",
840                panes.iter().map(|p| p.rect).collect::<Vec<_>>()
841            );
842        }
843    }
844
845    #[test]
846    fn a_zero_sized_area_does_not_panic() {
847        let mut tree = Tree::new(v(1));
848        tree.split(Axis::Rows, v(2));
849        let panes = tree.layout(Rect::new(0, 0, 0, 0));
850        assert_eq!(panes.len(), 2);
851        assert!(panes.iter().all(|p| p.rect.is_empty()));
852    }
853
854    #[test]
855    fn exactly_one_window_is_focused_however_the_tree_was_built() {
856        let mut tree = Tree::new(v(1));
857        tree.split(Axis::Columns, v(2));
858        tree.split(Axis::Rows, v(3));
859        tree.split(Axis::Columns, v(4));
860        tree.focus_direction(Direction::Left, AREA);
861        tree.close();
862
863        assert_eq!(
864            tree.layout(AREA).iter().filter(|p| p.focused).count(),
865            1,
866            "focus is a single window at all times"
867        );
868    }
869
870    #[test]
871    fn a_window_can_be_focused_by_name_from_anywhere_in_the_tree() {
872        let mut t = Tree::new(ViewId(1));
873        t.split(Axis::Columns, ViewId(2));
874        t.split(Axis::Rows, ViewId(3));
875        assert_eq!(t.focused(), ViewId(3));
876
877        assert!(t.focus_view(ViewId(1)), "view 1 is in this tree");
878        assert_eq!(t.focused(), ViewId(1));
879        assert!(t.focus_view(ViewId(2)));
880        assert_eq!(t.focused(), ViewId(2));
881    }
882
883    #[test]
884    fn focusing_a_window_that_is_not_here_changes_nothing() {
885        // A pane picker spans every tab; asking the wrong tree must be a clean miss rather
886        // than a focus pointing at a leaf that does not exist.
887        let mut t = Tree::new(ViewId(1));
888        t.split(Axis::Columns, ViewId(2));
889        let before = t.focused();
890        assert!(!t.focus_view(ViewId(99)));
891        assert_eq!(t.focused(), before);
892    }
893}