Skip to main content

rmux_core/window/
zoom.rs

1use super::Window;
2use crate::{layout::LayoutTree, PaneGeometry};
3
4impl Window {
5    /// Returns whether this window is currently zoomed.
6    #[must_use]
7    pub const fn is_zoomed(&self) -> bool {
8        self.zoomed
9    }
10
11    /// Returns the canonical layout currently visible to a client.
12    ///
13    /// Zoom keeps the saved layout tree intact while presenting the active
14    /// pane as a full-window leaf. Unzoomed windows expose the saved tree.
15    #[must_use]
16    pub fn visible_layout_dump(&self) -> String {
17        if !self.zoomed {
18            return self.layout_dump();
19        }
20
21        self.active_pane().map_or_else(String::new, |pane| {
22            LayoutTree::single(self.size).dump(std::slice::from_ref(pane))
23        })
24    }
25
26    pub(crate) fn toggle_zoom(&mut self, pane_index: u32) -> bool {
27        if self.pane(pane_index).is_none() {
28            return self.zoomed;
29        }
30
31        if self.zoomed {
32            self.auto_unzoom();
33            return false;
34        }
35
36        if self.panes.len() <= 1 {
37            return false;
38        }
39
40        let selected = self.select_pane(pane_index);
41        debug_assert!(selected, "validated zoom target pane must be selectable");
42        self.zoomed = true;
43        self.apply_zoom_geometry();
44        true
45    }
46
47    pub(crate) fn auto_unzoom(&mut self) -> bool {
48        if !self.zoomed {
49            return false;
50        }
51
52        self.zoomed = false;
53        self.recalculate_geometry();
54        true
55    }
56
57    /// Saves the current zoom state and unzooms. If `restore` is true, the
58    /// following `pop_zoom` will re-zoom to the then-active pane.
59    ///
60    /// Matches tmux `window_push_zoom`.
61    pub(crate) fn push_zoom(&mut self, restore: bool) -> bool {
62        let was_zoomed = self.zoomed;
63        if was_zoomed {
64            self.auto_unzoom();
65        }
66        self.zoom_restore_pending = was_zoomed && restore;
67        was_zoomed
68    }
69
70    /// Restores zoom state that was saved by a previous `push_zoom` call.
71    ///
72    /// Matches tmux `window_pop_zoom`.
73    pub(crate) fn pop_zoom(&mut self) {
74        if self.zoom_restore_pending {
75            self.zoom_restore_pending = false;
76            if self.panes.len() > 1 {
77                self.zoomed = true;
78                self.apply_zoom_geometry();
79            }
80        }
81    }
82
83    pub(crate) fn apply_zoom_geometry(&mut self) {
84        let size = self.size;
85        let active_pane = self.active_pane;
86        if let Some(pane) = self.pane_mut(active_pane) {
87            pane.set_geometry(PaneGeometry::new(0, 0, size.cols, size.rows));
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use rmux_proto::{LayoutName, ResizePaneAdjustment, TerminalSize};
96
97    fn window_with_two_panes() -> Window {
98        let mut window = Window::new(TerminalSize { cols: 80, rows: 24 });
99        window.split_after_position(0);
100        window
101    }
102
103    #[test]
104    fn toggle_zoom_expands_the_target_pane_and_auto_unzoom_restores_layout() {
105        let mut window = window_with_two_panes();
106        let pane_zero = window.pane(0).expect("pane 0 exists").geometry();
107        let pane_one = window.pane(1).expect("pane 1 exists").geometry();
108
109        assert!(window.toggle_zoom(1));
110
111        assert!(window.is_zoomed());
112        assert_eq!(window.active_pane_index(), 1);
113        assert_eq!(
114            window.pane(1).expect("pane 1 exists").geometry(),
115            PaneGeometry::new(0, 0, 80, 24)
116        );
117
118        assert!(window.auto_unzoom());
119        assert!(!window.is_zoomed());
120        assert_eq!(window.pane(0).expect("pane 0 exists").geometry(), pane_zero);
121        assert_eq!(window.pane(1).expect("pane 1 exists").geometry(), pane_one);
122    }
123
124    #[test]
125    fn set_size_while_zoomed_reconciles_and_updates_the_saved_layout() {
126        let mut window = Window::new(TerminalSize { cols: 3, rows: 10 });
127        window.split_after_position(0);
128        assert!(window.toggle_zoom(0));
129
130        window.set_size(TerminalSize { cols: 2, rows: 10 });
131
132        assert_eq!(window.size(), TerminalSize { cols: 3, rows: 10 });
133        assert_eq!(
134            window.pane(0).expect("pane 0 exists").geometry(),
135            PaneGeometry::new(0, 0, 3, 10)
136        );
137        assert_eq!(
138            window.pane(1).expect("pane 1 exists").geometry(),
139            PaneGeometry::new(2, 0, 1, 10)
140        );
141
142        window.set_size(TerminalSize { cols: 5, rows: 12 });
143
144        assert_eq!(window.size(), TerminalSize { cols: 5, rows: 12 });
145        assert_eq!(
146            window.pane(0).expect("pane 0 exists").geometry(),
147            PaneGeometry::new(0, 0, 5, 12)
148        );
149        assert_eq!(
150            window.pane(1).expect("pane 1 exists").geometry(),
151            PaneGeometry::new(3, 0, 2, 12)
152        );
153
154        assert!(window.auto_unzoom());
155        assert_eq!(
156            window.pane(0).expect("pane 0 exists").geometry(),
157            PaneGeometry::new(0, 0, 2, 12)
158        );
159        assert_eq!(
160            window.pane(1).expect("pane 1 exists").geometry(),
161            PaneGeometry::new(3, 0, 2, 12)
162        );
163    }
164
165    #[test]
166    fn single_pane_zoom_toggle_is_a_noop() {
167        let mut window = Window::new(TerminalSize { cols: 80, rows: 24 });
168        let geometry = window.pane(0).expect("pane 0 exists").geometry();
169        let layout = window.layout_dump();
170
171        assert!(!window.toggle_zoom(0));
172
173        assert!(!window.is_zoomed());
174        assert_eq!(window.pane(0).expect("pane 0 exists").geometry(), geometry);
175        assert_eq!(window.visible_layout_dump(), layout);
176    }
177
178    #[test]
179    fn visible_layout_projects_zoom_without_mutating_the_saved_layout() {
180        let mut window = window_with_two_panes();
181        window.save_old_layout();
182        let layout = window.layout_dump();
183        let old_layout = window
184            .old_layout()
185            .expect("old layout was saved")
186            .to_owned();
187
188        assert!(window.toggle_zoom(0));
189
190        assert_eq!(window.layout_dump(), layout);
191        assert_eq!(window.old_layout(), Some(old_layout.as_str()));
192        assert_eq!(
193            window.visible_layout_dump(),
194            "b25d,80x24,0,0,0",
195            "tmux 3.7b renders the zoomed pane as a canonical full-window leaf"
196        );
197
198        assert!(window.auto_unzoom());
199        assert_eq!(window.visible_layout_dump(), layout);
200        assert_eq!(window.old_layout(), Some(old_layout.as_str()));
201    }
202
203    #[test]
204    fn select_pane_unzooms_before_switching_active_panes() {
205        let mut window = window_with_two_panes();
206        assert!(window.toggle_zoom(1));
207
208        assert!(window.select_pane(0));
209
210        assert!(!window.is_zoomed());
211        assert_eq!(window.active_pane_index(), 0);
212        assert_ne!(
213            window.pane(1).expect("pane 1 exists").geometry(),
214            PaneGeometry::new(0, 0, 80, 24)
215        );
216    }
217
218    #[test]
219    fn absolute_resize_auto_unzooms_before_recalculating_layout() {
220        let mut window = window_with_two_panes();
221        assert!(window.toggle_zoom(1));
222
223        window.resize_main_pane(ResizePaneAdjustment::AbsoluteWidth { columns: 20 });
224
225        assert!(!window.is_zoomed());
226        assert_eq!(
227            window.pane(0).expect("pane 0 exists").geometry(),
228            PaneGeometry::new(0, 0, 20, 24)
229        );
230    }
231
232    #[test]
233    fn set_layout_auto_unzooms_before_recalculating_layout() {
234        let mut window = window_with_two_panes();
235        assert!(window.toggle_zoom(1));
236
237        window.set_layout(LayoutName::MainHorizontal);
238
239        assert!(!window.is_zoomed());
240        assert_eq!(
241            window.pane(0).expect("pane 0 exists").geometry(),
242            PaneGeometry::new(0, 0, 80, 22)
243        );
244        assert_eq!(
245            window.pane(1).expect("pane 1 exists").geometry(),
246            PaneGeometry::new(0, 23, 80, 1)
247        );
248    }
249
250    #[test]
251    fn set_even_layout_auto_unzooms_before_recalculating_layout() {
252        let mut window = window_with_two_panes();
253        assert!(window.toggle_zoom(1));
254
255        window.set_layout(LayoutName::EvenHorizontal);
256
257        assert!(!window.is_zoomed());
258        assert_eq!(
259            window.pane(0).expect("pane 0 exists").geometry(),
260            PaneGeometry::new(0, 0, 40, 24)
261        );
262        assert_eq!(
263            window.pane(1).expect("pane 1 exists").geometry(),
264            PaneGeometry::new(41, 0, 39, 24)
265        );
266    }
267
268    #[test]
269    fn push_zoom_without_restore_leaves_the_window_unzoomed_after_pop() {
270        let mut window = window_with_two_panes();
271        assert!(window.toggle_zoom(1));
272
273        assert!(window.push_zoom(false));
274        assert!(!window.is_zoomed());
275        assert!(!window.zoom_restore_pending);
276
277        window.select_pane(0);
278        window.pop_zoom();
279
280        assert!(!window.is_zoomed());
281        assert_eq!(window.active_pane_index(), 0);
282        assert_eq!(
283            window.pane(0).expect("pane 0 exists").geometry(),
284            PaneGeometry::new(0, 0, 40, 24)
285        );
286    }
287
288    #[test]
289    fn push_zoom_with_restore_rezooms_the_new_active_pane_on_pop() {
290        let mut window = window_with_two_panes();
291        assert!(window.toggle_zoom(1));
292
293        assert!(window.push_zoom(true));
294        assert!(!window.is_zoomed());
295        assert!(window.zoom_restore_pending);
296
297        assert!(window.select_pane(0));
298        window.pop_zoom();
299
300        assert!(window.is_zoomed());
301        assert!(!window.zoom_restore_pending);
302        assert_eq!(window.active_pane_index(), 0);
303        assert_eq!(
304            window.pane(0).expect("pane 0 exists").geometry(),
305            PaneGeometry::new(0, 0, 80, 24)
306        );
307        assert_eq!(
308            window.pane(1).expect("pane 1 exists").geometry(),
309            PaneGeometry::new(41, 0, 39, 24)
310        );
311    }
312}