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 });
120 ok
121 }
122}
123
124fn normalize_node(node: &mut PaneNode, changed: &mut bool) {
125 match node.kind_mut() {
126 NodeKind::Tabs { panels, active_ix } => {
127 let clamped = (*active_ix).min(panels.len().saturating_sub(1));
128 if *active_ix != clamped {
129 *active_ix = clamped;
130 *changed = true;
131 }
132 }
133 NodeKind::Split {
134 axis,
135 children,
136 sizes,
137 } => {
138 let axis = *axis;
139
140 for child in children.iter_mut() {
141 normalize_node(child, changed);
142 }
143
144 let mut ix = 0;
146 while ix < children.len() {
147 if is_empty_container(&children[ix]) {
148 children.remove(ix);
149 sizes.remove(ix);
150 *changed = true;
151 } else {
152 ix += 1;
153 }
154 }
155
156 for ix in 0..children.len() {
161 let is_single = matches!(
162 children[ix].kind_ref(),
163 NodeKind::Split { children: inner, .. } if inner.len() == 1
164 );
165 if !is_single {
166 continue;
167 }
168 let NodeKind::Split {
169 children: inner, ..
170 } = children[ix].kind_mut()
171 else {
172 continue;
173 };
174 let replacement = inner.remove(0);
175 children[ix] = replacement;
176 *changed = true;
177 }
178
179 let mut ix = 0;
181 while ix < children.len() {
182 let same_axis = matches!(
183 children[ix].kind_ref(),
184 NodeKind::Split { axis: inner, .. } if *inner == axis
185 );
186 if !same_axis {
187 ix += 1;
188 continue;
189 }
190
191 let NodeKind::Split {
194 children: inner,
195 sizes: inner_sizes,
196 ..
197 } = children[ix].kind_mut()
198 else {
199 ix += 1;
200 continue;
201 };
202 let inner = std::mem::take(inner);
203 let inner_sizes = std::mem::take(inner_sizes);
204
205 let slot = sizes[ix];
206 let inner_sizes = distribute_slot(slot, inner_sizes);
207 let count = inner.len();
208 children.splice(ix..=ix, inner);
209 sizes.splice(ix..=ix, inner_sizes);
210 ix += count;
211 *changed = true;
212 }
213 }
214 }
215}
216
217fn distribute_slot(slot: Option<Pixels>, inner: Vec<Option<Pixels>>) -> Vec<Option<Pixels>> {
223 let Some(slot) = slot else { return inner };
224 let total = inner
227 .iter()
228 .try_fold(Pixels::ZERO, |acc, size| size.map(|size| acc + size));
229 match total {
230 Some(total) if total > Pixels::ZERO => inner
231 .into_iter()
232 .map(|size| size.map(|size| size * (slot / total)))
233 .collect(),
234 _ => inner,
235 }
236}
237
238fn is_empty_container(node: &PaneNode) -> bool {
239 match node.kind_ref() {
240 NodeKind::Split { children, .. } => children.is_empty(),
241 NodeKind::Tabs { panels, .. } => panels.is_empty(),
242 }
243}
244
245fn collapse_root(tree: &mut PaneTree, changed: &mut bool) {
249 if tree.root_kind() == RootKind::Split {
250 return;
251 }
252
253 let replacement = match tree.root().kind_ref() {
254 NodeKind::Split { children, .. } if children.len() == 1 => Some(children[0].clone()),
255 _ => None,
256 };
257
258 if let Some(replacement) = replacement {
259 tree.replace_root(replacement);
260 *changed = true;
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::super::*;
267 use gpui::{Axis, Pixels, px};
268
269 fn panel(n: u64) -> PanelId {
270 PanelId::from_u64(n)
271 }
272
273 #[test]
274 fn empty_tab_groups_are_dropped() {
275 let mut tree = PaneTree::new(RootKind::Split);
276 let root = tree.root().id();
277 tree.push_tabs_for_test(root, vec![]);
278 tree.push_tabs_for_test(root, vec![panel(1)]);
279
280 tree.normalize();
281
282 assert!(
285 matches!(tree.root().kind(), PaneRef::Split { children, .. } if children.len() == 1)
286 );
287 assert_eq!(tree.panels().collect::<Vec<_>>(), vec![panel(1)]);
288 }
289
290 #[test]
291 fn a_single_child_split_is_replaced_by_its_child_keeping_the_child_id() {
292 let mut tree = PaneTree::new(RootKind::Any);
293 let outer = tree.set_root_split_for_test(Axis::Horizontal);
294 let inner = tree.push_split_for_test(outer, Axis::Vertical, Some(px(120.)));
295 let tabs = tree.push_tabs_for_test(inner, vec![panel(1)]);
296
297 tree.normalize();
298
299 assert_eq!(tree.root().id(), tabs, "child keeps its own NodeId");
300 assert!(tree.find_node(inner).is_none());
301 }
302
303 #[test]
304 fn a_collapsing_split_hands_its_slot_size_to_the_child() {
305 let mut tree = PaneTree::new(RootKind::Split);
306 let root = tree.root().id();
307 let inner = tree.push_split_for_test(root, Axis::Vertical, Some(px(300.)));
308 tree.push_tabs_for_test(inner, vec![panel(1)]);
309 tree.push_tabs_for_test(root, vec![panel(2)]);
310
311 tree.normalize();
312
313 let PaneRef::Split { sizes, .. } = tree.root().kind() else {
314 panic!()
315 };
316 assert_eq!(
317 sizes[0],
318 Some(px(300.)),
319 "the child inherits the collapsed split's slot"
320 );
321 }
322
323 #[test]
324 fn same_axis_nesting_is_spliced_into_the_parent() {
325 let mut tree = PaneTree::new(RootKind::Split);
326 tree.set_root_axis_for_test(Axis::Horizontal);
327 let root = tree.root().id();
328 tree.push_tabs_for_test(root, vec![panel(1)]);
329 let inner = tree.push_split_for_test(root, Axis::Horizontal, None);
330 tree.push_tabs_for_test(inner, vec![panel(2)]);
331 tree.push_tabs_for_test(inner, vec![panel(3)]);
332
333 tree.normalize();
334
335 let PaneRef::Split { children, axis, .. } = tree.root().kind() else {
336 panic!()
337 };
338 assert_eq!(axis, Axis::Horizontal);
339 assert_eq!(
340 children.len(),
341 3,
342 "the inner split's children are spliced in"
343 );
344 assert_eq!(
345 tree.panels().collect::<Vec<_>>(),
346 vec![panel(1), panel(2), panel(3)],
347 "order is preserved"
348 );
349 }
350
351 #[test]
352 fn active_index_is_clamped_to_the_panel_count() {
353 let mut tree = PaneTree::new(RootKind::Any);
354 let tabs = tree.set_root_tabs_for_test(vec![panel(1), panel(2)], 9);
355
356 tree.normalize();
357
358 let PaneRef::Tabs { active_ix, .. } = tree.find_node(tabs).unwrap().kind() else {
359 panic!()
360 };
361 assert_eq!(active_ix, 1);
362 }
363
364 #[test]
365 fn a_split_root_survives_being_emptied() {
366 let mut tree = PaneTree::new(RootKind::Split);
367 let root = tree.root().id();
368 tree.push_tabs_for_test(root, vec![]);
369
370 tree.normalize();
371
372 assert!(
373 matches!(tree.root().kind(), PaneRef::Split { children, .. } if children.is_empty()),
374 "the center must still serialize as a StackPanel when empty"
375 );
376 }
377
378 #[test]
385 fn a_same_axis_splice_with_one_unknown_inner_size_passes_them_through() {
386 let mut tree = PaneTree::new(RootKind::Split);
387 let root = tree.root().id();
388 let inner = tree.push_split_for_test(root, Axis::Horizontal, Some(px(400.)));
389 tree.push_sized_tabs_for_test(inner, vec![panel(1)], Some(px(100.)));
390 tree.push_sized_tabs_for_test(inner, vec![panel(2)], None);
391
392 tree.normalize();
393
394 let PaneRef::Split { sizes, .. } = tree.root().kind() else {
395 panic!()
396 };
397 assert_eq!(
398 sizes,
399 &[Some(px(100.)), None],
400 "an unknown inner size leaves every sibling unscaled; the 400px \
401 slot they replaced constrains nothing"
402 );
403 }
404
405 #[test]
410 fn removing_a_middle_container_leaves_its_siblings_untouched() {
411 let mut tree = PaneTree::new(RootKind::Split);
412 let root = tree.root().id();
413 tree.push_sized_tabs_for_test(root, vec![panel(1)], Some(px(400.)));
414 tree.push_sized_tabs_for_test(root, vec![], Some(px(800.)));
415 tree.push_sized_tabs_for_test(root, vec![panel(3)], Some(px(400.)));
416
417 tree.normalize();
418
419 let PaneRef::Split {
420 sizes, children, ..
421 } = tree.root().kind()
422 else {
423 panic!()
424 };
425 assert_eq!(children.len(), 2);
426 assert_eq!(
427 sizes,
428 &[Some(px(400.)), Some(px(400.))],
429 "the survivors keep their own sizes; the 800px the empty group \
430 held is not handed to either of them here"
431 );
432 }
433
434 #[test]
435 fn normalize_is_idempotent() {
436 let mut tree = PaneTree::new(RootKind::Split);
437 let root = tree.root().id();
438 let inner = tree.push_split_for_test(root, Axis::Horizontal, None);
439 tree.push_tabs_for_test(inner, vec![panel(1)]);
440 tree.push_tabs_for_test(inner, vec![]);
441 tree.push_tabs_for_test(root, vec![panel(2)]);
442
443 tree.normalize();
444 let once = tree.clone();
445 tree.normalize();
446
447 assert_eq!(once, tree);
448 }
449
450 #[test]
451 fn same_axis_splice_scales_inner_sizes_to_fill_the_outer_slot() {
452 let mut tree = PaneTree::new(RootKind::Split);
457 let root = tree.root().id();
458 let inner = tree.push_split_for_test(root, Axis::Horizontal, Some(px(400.)));
459 tree.push_sized_tabs_for_test(inner, vec![panel(1)], Some(px(50.)));
460 tree.push_sized_tabs_for_test(inner, vec![panel(2)], Some(px(150.)));
461
462 tree.normalize();
463
464 let PaneRef::Split { sizes, .. } = tree.root().kind() else {
465 panic!()
466 };
467 assert_eq!(
468 sizes,
469 &[Some(px(100.)), Some(px(300.))],
470 "sizes scale by the outer/inner ratio (400/200 = 2x), not by its reverse"
471 );
472 let total: Pixels = sizes.iter().flatten().copied().sum();
473 assert_eq!(
474 total,
475 px(400.),
476 "the scaled sizes sum back to the outer slot"
477 );
478 }
479
480 #[test]
481 fn normalize_converges_within_two_passes_on_an_adversarial_tree() {
482 let mut tree = PaneTree::new(RootKind::Any);
493 let root = tree.root().id();
494 let d = tree.push_split_for_test(root, Axis::Vertical, None);
495 let a = tree.push_split_for_test(d, Axis::Horizontal, None);
496 tree.push_tabs_for_test(a, vec![]);
497 let b = tree.push_split_for_test(a, Axis::Vertical, None);
498 let c = tree.push_split_for_test(b, Axis::Vertical, None);
499 tree.push_tabs_for_test(c, vec![panel(1)]);
500 tree.push_tabs_for_test(c, vec![panel(2)]);
501
502 let passes = tree.normalize_pass_count_for_test();
503
504 assert!(
505 passes <= 2,
506 "expected the fixpoint within 2 passes, took {passes}"
507 );
508 assert!(tree.is_normalized());
509 assert_eq!(
510 tree.panels().collect::<Vec<_>>(),
511 vec![panel(1), panel(2)],
512 "every panel survives the collapse, in order"
513 );
514 }
515}