Skip to main content

tmprl_ui/
tabs.rs

1//! Tabs: a list of window trees, one of them current.
2
3use crate::{Rect, Tree, ViewId};
4
5/// Every tab, with one current. There is always at least one.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Tabs {
8    tabs: Vec<Tree>,
9    current: usize,
10}
11
12impl Tabs {
13    pub fn new(view: ViewId) -> Self {
14        Self {
15            tabs: vec![Tree::new(view)],
16            current: 0,
17        }
18    }
19
20    pub fn current(&self) -> &Tree {
21        &self.tabs[self.current]
22    }
23
24    pub fn current_mut(&mut self) -> &mut Tree {
25        &mut self.tabs[self.current]
26    }
27
28    pub fn index(&self) -> usize {
29        self.current
30    }
31
32    pub fn len(&self) -> usize {
33        self.tabs.len()
34    }
35
36    pub fn is_empty(&self) -> bool {
37        false // there is always one tab
38    }
39
40    /// Open a tab after the current one and switch to it, as `:tabnew` does.
41    pub fn open(&mut self, view: ViewId) {
42        self.current += 1;
43        self.tabs.insert(self.current, Tree::new(view));
44    }
45
46    /// Close the current tab. Returns false when it is the last one, a session with no tabs
47    /// has nothing to draw, and quitting is a separate decision from closing.
48    pub fn close(&mut self) -> bool {
49        if self.tabs.len() == 1 {
50            return false;
51        }
52        self.tabs.remove(self.current);
53        // Land on the tab that slid into this slot, or the new last one.
54        self.current = self.current.min(self.tabs.len() - 1);
55        true
56    }
57
58    /// Wrapping, like vim's `gt` / `gT`.
59    pub fn next(&mut self) {
60        self.current = (self.current + 1) % self.tabs.len();
61    }
62
63    pub fn previous(&mut self) {
64        self.current = (self.current + self.tabs.len() - 1) % self.tabs.len();
65    }
66
67    /// Every view across every tab, for a caller that owns the view state.
68    pub fn views(&self) -> Vec<ViewId> {
69        self.tabs.iter().flat_map(|t| t.views()).collect()
70    }
71
72    /// Lay out the current tab.
73    pub fn layout(&self, area: Rect) -> Vec<crate::Pane> {
74        self.current().layout(area)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::Axis;
82
83    fn v(n: u64) -> ViewId {
84        ViewId(n)
85    }
86
87    #[test]
88    fn a_session_starts_with_one_tab() {
89        let tabs = Tabs::new(v(1));
90        assert_eq!(tabs.len(), 1);
91        assert_eq!(tabs.index(), 0);
92        assert_eq!(tabs.current().focused(), v(1));
93    }
94
95    #[test]
96    fn opening_a_tab_switches_to_it_and_inserts_after_the_current_one() {
97        let mut tabs = Tabs::new(v(1));
98        tabs.open(v(2));
99        assert_eq!(tabs.len(), 2);
100        assert_eq!(tabs.index(), 1);
101        assert_eq!(tabs.current().focused(), v(2));
102
103        // From the middle, a new tab lands next, not at the end.
104        tabs.previous();
105        tabs.open(v(3));
106        assert_eq!(tabs.index(), 1);
107        assert_eq!(tabs.views(), [v(1), v(3), v(2)]);
108    }
109
110    #[test]
111    fn tabs_wrap_in_both_directions() {
112        let mut tabs = Tabs::new(v(1));
113        tabs.open(v(2));
114        tabs.open(v(3));
115        assert_eq!(tabs.index(), 2);
116
117        tabs.next();
118        assert_eq!(tabs.index(), 0, "past the end comes back to the first");
119        tabs.previous();
120        assert_eq!(tabs.index(), 2, "and before the first is the last");
121    }
122
123    #[test]
124    fn closing_a_tab_lands_on_the_one_that_took_its_place() {
125        let mut tabs = Tabs::new(v(1));
126        tabs.open(v(2));
127        tabs.open(v(3));
128        tabs.previous(); // on tab 1, holding view 2
129
130        assert!(tabs.close());
131        assert_eq!(tabs.len(), 2);
132        assert_eq!(tabs.index(), 1);
133        assert_eq!(tabs.current().focused(), v(3));
134    }
135
136    #[test]
137    fn closing_the_last_tab_lands_on_the_new_last() {
138        let mut tabs = Tabs::new(v(1));
139        tabs.open(v(2));
140        assert!(tabs.close());
141        assert_eq!(tabs.index(), 0);
142        assert_eq!(tabs.current().focused(), v(1));
143    }
144
145    #[test]
146    fn the_final_tab_cannot_be_closed() {
147        // Quitting is a separate decision from closing a tab, and a session with no tabs has
148        // nothing to draw.
149        let mut tabs = Tabs::new(v(1));
150        assert!(!tabs.close());
151        assert_eq!(tabs.len(), 1);
152    }
153
154    #[test]
155    fn each_tab_keeps_its_own_layout() {
156        let mut tabs = Tabs::new(v(1));
157        tabs.current_mut().split(Axis::Columns, v(2));
158        assert_eq!(tabs.current().len(), 2);
159
160        tabs.open(v(3));
161        assert_eq!(tabs.current().len(), 1, "a new tab starts with one window");
162
163        tabs.previous();
164        assert_eq!(tabs.current().len(), 2, "the split is still there");
165    }
166}