1use super::Window;
2use crate::{layout::LayoutTree, PaneGeometry};
3
4impl Window {
5 #[must_use]
7 pub const fn is_zoomed(&self) -> bool {
8 self.zoomed
9 }
10
11 #[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 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 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}