1use gpui::Pixels;
2
3use super::node::{NodeKind, PaneNode};
4use super::tree::{PaneTree, RootKind};
5
6const MAX_NORMALIZE_PASSES: u32 = 64;
12
13impl PaneTree {
14 pub fn normalize(&mut self) {
34 self.normalize_reporting();
35 }
36
37 pub(crate) fn normalize_reporting(&mut self) -> bool {
44 let changed = self.run_normalize_passes().1;
45 debug_assert!(self.is_normalized(), "normalize did not reach a fixpoint");
46 changed
47 }
48
49 fn run_normalize_passes(&mut self) -> (u32, bool) {
57 let mut passes = 0;
58 let mut changed = true;
59 let mut any_change = false;
60 while changed && passes < MAX_NORMALIZE_PASSES {
63 changed = false;
64 normalize_node(self.root_mut(), &mut changed);
65 collapse_root(self, &mut changed);
66 any_change |= changed;
67 passes += 1;
68 }
69
70 if changed {
76 tracing::warn!(
77 passes,
78 "PaneTree::normalize exhausted {MAX_NORMALIZE_PASSES} passes without reaching \
79 a fixpoint; the tree may still contain an empty container, a single-child \
80 split, same-axis split nesting, or an unclamped Tabs active_ix"
81 );
82 }
83
84 (passes, any_change)
85 }
86
87 #[cfg(test)]
90 pub(crate) fn normalize_pass_count_for_test(&mut self) -> u32 {
91 self.run_normalize_passes().0
92 }
93
94 pub(crate) fn is_normalized(&self) -> bool {
96 let mut ok = true;
97 let root_id = self.root().id();
98 self.root().walk(&mut |node| match node.kind_ref() {
99 NodeKind::Split {
100 children,
101 sizes,
102 axis,
103 } => {
104 ok &= children.len() == sizes.len();
105 if node.id() != root_id {
107 ok &= children.len() > 1;
108 }
109 ok &= !children.iter().any(|child| {
110 matches!(child.kind_ref(), NodeKind::Split { axis: inner, .. } if inner == axis)
111 });
112 }
113 NodeKind::Tabs { panels, active_ix } => {
114 ok &= panels.is_empty() || *active_ix < panels.len();
115 if node.id() != root_id {
116 ok &= !panels.is_empty();
117 }
118 }
119 NodeKind::Tiles { panels } => {
120 if node.id() != root_id {
121 ok &= !panels.is_empty();
122 }
123 }
124 });
125 ok
126 }
127}
128
129fn normalize_node(node: &mut PaneNode, changed: &mut bool) {
130 match node.kind_mut() {
131 NodeKind::Tabs { panels, active_ix } => {
132 let clamped = (*active_ix).min(panels.len().saturating_sub(1));
133 if *active_ix != clamped {
134 *active_ix = clamped;
135 *changed = true;
136 }
137 }
138 NodeKind::Tiles { .. } => {}
139 NodeKind::Split {
140 axis,
141 children,
142 sizes,
143 } => {
144 let axis = *axis;
145
146 for child in children.iter_mut() {
147 normalize_node(child, changed);
148 }
149
150 let mut ix = 0;
152 while ix < children.len() {
153 if is_empty_container(&children[ix]) {
154 children.remove(ix);
155 sizes.remove(ix);
156 *changed = true;
157 } else {
158 ix += 1;
159 }
160 }
161
162 for ix in 0..children.len() {
167 let is_single = matches!(
168 children[ix].kind_ref(),
169 NodeKind::Split { children: inner, .. } if inner.len() == 1
170 );
171 if !is_single {
172 continue;
173 }
174 let NodeKind::Split {
175 children: inner, ..
176 } = children[ix].kind_mut()
177 else {
178 continue;
179 };
180 let replacement = inner.remove(0);
181 children[ix] = replacement;
182 *changed = true;
183 }
184
185 let mut ix = 0;
187 while ix < children.len() {
188 let same_axis = matches!(
189 children[ix].kind_ref(),
190 NodeKind::Split { axis: inner, .. } if *inner == axis
191 );
192 if !same_axis {
193 ix += 1;
194 continue;
195 }
196
197 let NodeKind::Split {
200 children: inner,
201 sizes: inner_sizes,
202 ..
203 } = children[ix].kind_mut()
204 else {
205 ix += 1;
206 continue;
207 };
208 let inner = std::mem::take(inner);
209 let inner_sizes = std::mem::take(inner_sizes);
210
211 let slot = sizes[ix];
212 let inner_sizes = distribute_slot(slot, inner_sizes);
213 let count = inner.len();
214 children.splice(ix..=ix, inner);
215 sizes.splice(ix..=ix, inner_sizes);
216 ix += count;
217 *changed = true;
218 }
219 }
220 }
221}
222
223fn distribute_slot(slot: Option<Pixels>, inner: Vec<Option<Pixels>>) -> Vec<Option<Pixels>> {
229 let Some(slot) = slot else { return inner };
230 let total = inner
233 .iter()
234 .try_fold(Pixels::ZERO, |acc, size| size.map(|size| acc + size));
235 match total {
236 Some(total) if total > Pixels::ZERO => inner
237 .into_iter()
238 .map(|size| size.map(|size| size * (slot / total)))
239 .collect(),
240 _ => inner,
241 }
242}
243
244fn is_empty_container(node: &PaneNode) -> bool {
245 match node.kind_ref() {
246 NodeKind::Split { children, .. } => children.is_empty(),
247 NodeKind::Tabs { panels, .. } => panels.is_empty(),
248 NodeKind::Tiles { panels } => panels.is_empty(),
249 }
250}
251
252fn collapse_root(tree: &mut PaneTree, changed: &mut bool) {
256 if tree.root_kind() == RootKind::Split {
257 return;
258 }
259
260 let replacement = match tree.root().kind_ref() {
261 NodeKind::Split { children, .. } if children.len() == 1 => Some(children[0].clone()),
262 _ => None,
263 };
264
265 if let Some(replacement) = replacement {
266 tree.replace_root(replacement);
267 *changed = true;
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::super::*;
274 use gpui::{Axis, Pixels, px};
275
276 fn panel(n: u64) -> PanelId {
277 PanelId::from_u64(n)
278 }
279
280 #[test]
281 fn empty_tab_groups_are_dropped() {
282 let mut tree = PaneTree::new(RootKind::Split);
283 let root = tree.root().id();
284 tree.push_tabs_for_test(root, vec![]);
285 tree.push_tabs_for_test(root, vec![panel(1)]);
286
287 tree.normalize();
288
289 assert!(
292 matches!(tree.root().kind(), PaneRef::Split { children, .. } if children.len() == 1)
293 );
294 assert_eq!(tree.panels().collect::<Vec<_>>(), vec![panel(1)]);
295 }
296
297 #[test]
298 fn a_single_child_split_is_replaced_by_its_child_keeping_the_child_id() {
299 let mut tree = PaneTree::new(RootKind::Any);
300 let outer = tree.set_root_split_for_test(Axis::Horizontal);
301 let inner = tree.push_split_for_test(outer, Axis::Vertical, Some(px(120.)));
302 let tabs = tree.push_tabs_for_test(inner, vec![panel(1)]);
303
304 tree.normalize();
305
306 assert_eq!(tree.root().id(), tabs, "child keeps its own NodeId");
307 assert!(tree.find_node(inner).is_none());
308 }
309
310 #[test]
311 fn a_collapsing_split_hands_its_slot_size_to_the_child() {
312 let mut tree = PaneTree::new(RootKind::Split);
313 let root = tree.root().id();
314 let inner = tree.push_split_for_test(root, Axis::Vertical, Some(px(300.)));
315 tree.push_tabs_for_test(inner, vec![panel(1)]);
316 tree.push_tabs_for_test(root, vec![panel(2)]);
317
318 tree.normalize();
319
320 let PaneRef::Split { sizes, .. } = tree.root().kind() else {
321 panic!()
322 };
323 assert_eq!(
324 sizes[0],
325 Some(px(300.)),
326 "the child inherits the collapsed split's slot"
327 );
328 }
329
330 #[test]
331 fn same_axis_nesting_is_spliced_into_the_parent() {
332 let mut tree = PaneTree::new(RootKind::Split);
333 tree.set_root_axis_for_test(Axis::Horizontal);
334 let root = tree.root().id();
335 tree.push_tabs_for_test(root, vec![panel(1)]);
336 let inner = tree.push_split_for_test(root, Axis::Horizontal, None);
337 tree.push_tabs_for_test(inner, vec![panel(2)]);
338 tree.push_tabs_for_test(inner, vec![panel(3)]);
339
340 tree.normalize();
341
342 let PaneRef::Split { children, axis, .. } = tree.root().kind() else {
343 panic!()
344 };
345 assert_eq!(axis, Axis::Horizontal);
346 assert_eq!(
347 children.len(),
348 3,
349 "the inner split's children are spliced in"
350 );
351 assert_eq!(
352 tree.panels().collect::<Vec<_>>(),
353 vec![panel(1), panel(2), panel(3)],
354 "order is preserved"
355 );
356 }
357
358 #[test]
359 fn active_index_is_clamped_to_the_panel_count() {
360 let mut tree = PaneTree::new(RootKind::Any);
361 let tabs = tree.set_root_tabs_for_test(vec![panel(1), panel(2)], 9);
362
363 tree.normalize();
364
365 let PaneRef::Tabs { active_ix, .. } = tree.find_node(tabs).unwrap().kind() else {
366 panic!()
367 };
368 assert_eq!(active_ix, 1);
369 }
370
371 #[test]
372 fn a_split_root_survives_being_emptied() {
373 let mut tree = PaneTree::new(RootKind::Split);
374 let root = tree.root().id();
375 tree.push_tabs_for_test(root, vec![]);
376
377 tree.normalize();
378
379 assert!(
380 matches!(tree.root().kind(), PaneRef::Split { children, .. } if children.is_empty()),
381 "the center must still serialize as a StackPanel when empty"
382 );
383 }
384
385 #[test]
392 fn a_same_axis_splice_with_one_unknown_inner_size_passes_them_through() {
393 let mut tree = PaneTree::new(RootKind::Split);
394 let root = tree.root().id();
395 let inner = tree.push_split_for_test(root, Axis::Horizontal, Some(px(400.)));
396 tree.push_sized_tabs_for_test(inner, vec![panel(1)], Some(px(100.)));
397 tree.push_sized_tabs_for_test(inner, vec![panel(2)], None);
398
399 tree.normalize();
400
401 let PaneRef::Split { sizes, .. } = tree.root().kind() else {
402 panic!()
403 };
404 assert_eq!(
405 sizes,
406 &[Some(px(100.)), None],
407 "an unknown inner size leaves every sibling unscaled; the 400px \
408 slot they replaced constrains nothing"
409 );
410 }
411
412 #[test]
417 fn removing_a_middle_container_leaves_its_siblings_untouched() {
418 let mut tree = PaneTree::new(RootKind::Split);
419 let root = tree.root().id();
420 tree.push_sized_tabs_for_test(root, vec![panel(1)], Some(px(400.)));
421 tree.push_sized_tabs_for_test(root, vec![], Some(px(800.)));
422 tree.push_sized_tabs_for_test(root, vec![panel(3)], Some(px(400.)));
423
424 tree.normalize();
425
426 let PaneRef::Split {
427 sizes, children, ..
428 } = tree.root().kind()
429 else {
430 panic!()
431 };
432 assert_eq!(children.len(), 2);
433 assert_eq!(
434 sizes,
435 &[Some(px(400.)), Some(px(400.))],
436 "the survivors keep their own sizes; the 800px the empty group \
437 held is not handed to either of them here"
438 );
439 }
440
441 #[test]
442 fn normalize_is_idempotent() {
443 let mut tree = PaneTree::new(RootKind::Split);
444 let root = tree.root().id();
445 let inner = tree.push_split_for_test(root, Axis::Horizontal, None);
446 tree.push_tabs_for_test(inner, vec![panel(1)]);
447 tree.push_tabs_for_test(inner, vec![]);
448 tree.push_tabs_for_test(root, vec![panel(2)]);
449
450 tree.normalize();
451 let once = tree.clone();
452 tree.normalize();
453
454 assert_eq!(once, tree);
455 }
456
457 #[test]
458 fn same_axis_splice_scales_inner_sizes_to_fill_the_outer_slot() {
459 let mut tree = PaneTree::new(RootKind::Split);
464 let root = tree.root().id();
465 let inner = tree.push_split_for_test(root, Axis::Horizontal, Some(px(400.)));
466 tree.push_sized_tabs_for_test(inner, vec![panel(1)], Some(px(50.)));
467 tree.push_sized_tabs_for_test(inner, vec![panel(2)], Some(px(150.)));
468
469 tree.normalize();
470
471 let PaneRef::Split { sizes, .. } = tree.root().kind() else {
472 panic!()
473 };
474 assert_eq!(
475 sizes,
476 &[Some(px(100.)), Some(px(300.))],
477 "sizes scale by the outer/inner ratio (400/200 = 2x), not by its reverse"
478 );
479 let total: Pixels = sizes.iter().flatten().copied().sum();
480 assert_eq!(
481 total,
482 px(400.),
483 "the scaled sizes sum back to the outer slot"
484 );
485 }
486
487 #[test]
488 fn normalize_converges_within_two_passes_on_an_adversarial_tree() {
489 let mut tree = PaneTree::new(RootKind::Any);
500 let root = tree.root().id();
501 let d = tree.push_split_for_test(root, Axis::Vertical, None);
502 let a = tree.push_split_for_test(d, Axis::Horizontal, None);
503 tree.push_tabs_for_test(a, vec![]);
504 let b = tree.push_split_for_test(a, Axis::Vertical, None);
505 let c = tree.push_split_for_test(b, Axis::Vertical, None);
506 tree.push_tabs_for_test(c, vec![panel(1)]);
507 tree.push_tabs_for_test(c, vec![panel(2)]);
508
509 let passes = tree.normalize_pass_count_for_test();
510
511 assert!(
512 passes <= 2,
513 "expected the fixpoint within 2 passes, took {passes}"
514 );
515 assert!(tree.is_normalized());
516 assert_eq!(
517 tree.panels().collect::<Vec<_>>(),
518 vec![panel(1), panel(2)],
519 "every panel survives the collapse, in order"
520 );
521 }
522}