1use crate::TestSupportExt as _;
5use std::{
6 collections::{HashMap, HashSet},
7 rc::Rc,
8 sync::Arc,
9};
10
11use anyhow::Result;
12use gpui::{
13 AnyElement, AnyView, App, AppContext as _, Axis, Bounds, Context, Div, Empty, Entity,
14 EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, ParentElement,
15 Pixels, Point, Render, SharedString, Stateful, Styled as _, Subscription, WeakEntity, Window,
16 div, prelude::FluentBuilder as _, px,
17};
18
19use crate::{
20 ElementExt as _, Placement, ResizablePanelEvent, ResizableState, ResizeHandleContext,
21 h_resizable, resizable::PANEL_MIN_SIZE, resizable_panel, v_resizable,
22};
23
24use super::{
25 dock_placement::{Dock, DockSizing},
26 drag::{AnyDrag, DropTarget},
27 layout::{
28 DockLayout, EditResult, InsertTarget, NodeId, NodeKind, PaneNode, PaneRef, PaneTree,
29 PanelId, RootKind,
30 },
31 panel::{LivePanels, Panel, PanelEvent, PanelView},
32 registry::{PanelBuildContext, PanelRegistry},
33 state::{DockAreaState, DockPlacement, DockState, PanelInfo, PanelState},
34 state_convert::{PanelBuilder, PanelSource as _},
35 tab_group::{BareTabGroup, TabGroup, TabGroupConstraints, TabGroupEvent, TabGroupRenderer},
36};
37
38pub enum DockEvent {
40 LayoutChanged,
43 DragDrop { item: AnyDrag, target: DropTarget },
45}
46
47#[derive(Clone, Copy, PartialEq, Eq, Debug)]
57enum Zoomed {
58 Group(NodeId),
60}
61
62struct DockRegion {
64 tree: PaneTree,
65 dock: Dock,
66}
67
68struct Cached<T> {
72 entity: Entity<T>,
73 _subscription: Subscription,
74}
75
76struct CachedSplit {
85 entity: Entity<ResizableState>,
86 children: Vec<NodeId>,
87 sizes: Vec<Option<Pixels>>,
92 _subscription: Subscription,
93}
94
95pub struct DockArea {
100 id: SharedString,
101 version: Option<usize>,
102 bounds: Bounds<Pixels>,
103 this: WeakEntity<Self>,
104
105 center: PaneTree,
106 docks: HashMap<DockPlacement, DockRegion>,
107
108 groups: HashMap<NodeId, Cached<TabGroup>>,
109 splits: HashMap<NodeId, CachedSplit>,
110 panels: HashMap<PanelId, Arc<dyn PanelView>>,
111
112 locked: bool,
113 zoomed: Option<Zoomed>,
114 focus_handle: FocusHandle,
115 renderer: Rc<dyn DockAreaRenderer>,
116}
117
118impl DockArea {
119 pub fn new(
127 id: impl Into<SharedString>,
128 version: Option<usize>,
129 _window: &mut Window,
130 cx: &mut Context<Self>,
131 ) -> Self {
132 PanelRegistry::init(cx);
133
134 Self {
135 id: id.into(),
136 version,
137 bounds: Bounds::default(),
138 this: cx.weak_entity(),
139 center: PaneTree::new(RootKind::Split),
140 docks: HashMap::new(),
141 groups: HashMap::new(),
142 splits: HashMap::new(),
143 panels: HashMap::new(),
144 locked: false,
145 zoomed: None,
146 focus_handle: cx.focus_handle(),
147 renderer: Rc::new(BareDockArea),
148 }
149 }
150
151 pub fn with_renderer(mut self, renderer: Rc<dyn DockAreaRenderer>) -> Self {
155 self.renderer = renderer;
156 self
157 }
158
159 pub fn id(&self) -> SharedString {
160 self.id.clone()
161 }
162
163 pub fn version(&self) -> Option<usize> {
164 self.version
165 }
166
167 pub fn set_version(&mut self, version: Option<usize>, cx: &mut Context<Self>) {
173 self.version = version;
174 cx.notify();
175 }
176
177 pub fn bounds(&self) -> Bounds<Pixels> {
180 self.bounds
181 }
182
183 pub fn layout(&self, placement: DockPlacement) -> Option<&PaneTree> {
190 match placement {
191 DockPlacement::Center => Some(&self.center),
192 _ => self.docks.get(&placement).map(|pane| &pane.tree),
193 }
194 }
195
196 pub fn panel(&self, panel: PanelId) -> Option<&Arc<dyn PanelView>> {
198 self.panels.get(&panel)
199 }
200
201 pub fn is_locked(&self) -> bool {
202 self.locked
203 }
204
205 pub fn is_empty(&self, placement: DockPlacement, cx: &App) -> bool {
212 self.layout(placement)
213 .is_none_or(|tree| !self.is_node_visible(tree.root(), cx))
214 }
215
216 pub fn set_locked(&mut self, locked: bool, window: &mut Window, cx: &mut Context<Self>) {
218 if self.locked == locked {
219 return;
220 }
221 self.locked = locked;
222 self.reconcile(window, cx);
225 }
226}
227
228impl DockArea {
230 pub fn set_center(&mut self, layout: DockLayout, window: &mut Window, cx: &mut Context<Self>) {
233 let (tree, panels) = PaneTree::from_layout(layout, RootKind::Split);
234 self.center = tree;
235 self.panels.extend(panels);
236 self.reconcile(window, cx);
237 cx.emit(DockEvent::LayoutChanged);
238 }
239
240 pub fn set_dock(
247 &mut self,
248 placement: DockPlacement,
249 layout: DockLayout,
250 window: &mut Window,
251 cx: &mut Context<Self>,
252 ) {
253 if placement == DockPlacement::Center {
254 return self.set_center(layout, window, cx);
255 }
256
257 let (tree, panels) = PaneTree::from_layout(layout, RootKind::Any);
258 let dock = self
259 .docks
260 .get(&placement)
261 .map(|pane| pane.dock)
262 .unwrap_or_else(|| Dock::new(PANEL_MIN_SIZE * 2.));
263 self.docks.insert(placement, DockRegion { tree, dock });
264 self.panels.extend(panels);
265 self.reconcile(window, cx);
266 cx.emit(DockEvent::LayoutChanged);
267 }
268
269 pub fn remove_dock(
272 &mut self,
273 placement: DockPlacement,
274 window: &mut Window,
275 cx: &mut Context<Self>,
276 ) {
277 if self.docks.remove(&placement).is_none() {
278 return;
279 }
280 self.reconcile(window, cx);
283 cx.emit(DockEvent::LayoutChanged);
284 }
285
286 pub fn has_dock(&self, placement: DockPlacement) -> bool {
287 self.docks.contains_key(&placement)
288 }
289
290 pub fn is_dock_open(&self, placement: DockPlacement) -> bool {
294 self.docks
295 .get(&placement)
296 .is_some_and(|pane| pane.dock.is_open())
297 }
298
299 pub fn toggle_dock(
302 &mut self,
303 placement: DockPlacement,
304 window: &mut Window,
305 cx: &mut Context<Self>,
306 ) {
307 let Some(pane) = self.docks.get_mut(&placement) else {
308 return;
309 };
310 if !pane.dock.is_collapsible() && pane.dock.is_open() {
311 return;
312 }
313 let open = pane.dock.is_open();
314 pane.dock.set_open(!open);
315 self.reconcile(window, cx);
319 cx.emit(DockEvent::LayoutChanged);
320 }
321
322 pub fn is_dock_collapsible(&self, placement: DockPlacement) -> bool {
325 self.docks
326 .get(&placement)
327 .is_some_and(|pane| pane.dock.is_collapsible())
328 }
329
330 pub fn set_dock_collapsible(
331 &mut self,
332 placement: DockPlacement,
333 collapsible: bool,
334 _window: &mut Window,
335 cx: &mut Context<Self>,
336 ) {
337 if let Some(pane) = self.docks.get_mut(&placement) {
338 pane.dock.set_collapsible(collapsible);
339 cx.notify();
340 }
341 }
342
343 pub fn dock_size(&self, placement: DockPlacement) -> Option<Pixels> {
345 self.docks.get(&placement).map(|pane| pane.dock.size())
346 }
347
348 pub fn set_dock_size(
349 &mut self,
350 placement: DockPlacement,
351 size: Pixels,
352 _window: &mut Window,
353 cx: &mut Context<Self>,
354 ) {
355 if let Some(pane) = self.docks.get_mut(&placement) {
356 let previous = pane.dock.size();
357 pane.dock.set_size(size);
358 if pane.dock.size() == previous {
359 return;
360 }
361 cx.notify();
362 cx.emit(DockEvent::LayoutChanged);
363 }
364 }
365}
366
367impl DockArea {
369 pub fn add_panel<P: Panel>(
372 &mut self,
373 panel: Entity<P>,
374 placement: DockPlacement,
375 size: Option<Pixels>,
376 window: &mut Window,
377 cx: &mut Context<Self>,
378 ) {
379 let id = PanelId::from(panel.entity_id());
380 self.add_panel_inner(id, Arc::new(panel), placement, size, window, cx);
381 }
382
383 pub fn add_panel_view(
390 &mut self,
391 panel: Arc<dyn PanelView>,
392 placement: DockPlacement,
393 size: Option<Pixels>,
394 window: &mut Window,
395 cx: &mut Context<Self>,
396 ) {
397 let id = panel.panel_id(cx);
398 self.add_panel_inner(id, panel, placement, size, window, cx);
399 }
400
401 fn add_panel_inner(
402 &mut self,
403 id: PanelId,
404 panel: Arc<dyn PanelView>,
405 placement: DockPlacement,
406 size: Option<Pixels>,
407 window: &mut Window,
408 cx: &mut Context<Self>,
409 ) {
410 let previous = self.panels.insert(id, panel);
417
418 if placement != DockPlacement::Center && !self.docks.contains_key(&placement) {
420 self.docks.insert(
421 placement,
422 DockRegion {
423 tree: PaneTree::new(RootKind::Any),
424 dock: Dock::new(size.unwrap_or(PANEL_MIN_SIZE * 2.)),
425 },
426 );
427 }
428
429 let Some(tree) = self.tree_mut(placement) else {
430 self.restore_registration(id, previous);
431 return;
432 };
433 let target = match first_tab_group(tree.root()) {
434 Some(node) => InsertTarget::Tabs {
435 node,
436 ix: None,
437 activate: true,
438 },
439 None => InsertTarget::Split {
443 node: tree.root().id(),
444 placement: Placement::Right,
445 size,
446 },
447 };
448 let result = tree.insert_panel(id, target);
449 if !result.changed() {
450 self.restore_registration(id, previous);
454 return;
455 }
456 self.commit(result, window, cx);
457 }
458
459 fn restore_registration(&mut self, id: PanelId, previous: Option<Arc<dyn PanelView>>) {
462 match previous {
463 Some(view) => self.panels.insert(id, view),
464 None => self.panels.remove(&id),
465 };
466 }
467
468 pub fn remove_panel<P: Panel>(
470 &mut self,
471 panel: Entity<P>,
472 window: &mut Window,
473 cx: &mut Context<Self>,
474 ) {
475 self.remove_panel_id(PanelId::from(panel.entity_id()), window, cx);
476 }
477
478 pub fn move_panel(
481 &mut self,
482 panel: PanelId,
483 target: InsertTarget,
484 window: &mut Window,
485 cx: &mut Context<Self>,
486 ) {
487 if self.panel(panel).is_none() {
490 return;
491 }
492 let Some(destination) = self.placement_of_node(target_node(&target)) else {
493 return;
494 };
495 let source = self.placement_of_panel(panel);
496
497 if matches!(target, InsertTarget::Split { .. }) {
500 self.adopt_measured_sizes(destination, cx);
501 }
502
503 let was_active = self
507 .layout(source.unwrap_or(destination))
508 .and_then(|tree| tree.find_panel_node(panel))
509 .and_then(|node| self.groups.get(&node))
510 .and_then(|cached| cached.entity.read(cx).last_notified_active(panel));
511
512 let changed = match source {
513 Some(source) if source == destination => {
514 let Some(tree) = self.tree_mut(destination) else {
515 return;
516 };
517 tree.move_panel(panel, target).changed()
518 }
519 source => {
520 let detached = source
531 .and_then(|source| self.tree_mut(source))
532 .is_some_and(|tree| tree.remove_panel(panel).changed());
533 let Some(tree) = self.tree_mut(destination) else {
534 return;
535 };
536 let inserted = tree.insert_panel(panel, target).changed();
537 detached || inserted
538 }
539 };
540
541 self.commit_changed(changed, window, cx);
542
543 if let Some(active) = was_active {
544 if let Some(cached) = self
545 .layout(destination)
546 .and_then(|tree| tree.find_panel_node(panel))
547 .and_then(|node| self.groups.get(&node))
548 {
549 let group = cached.entity.clone();
550 group.update(cx, |group, _| group.seed_active(panel, active));
551 }
552 }
553 }
554
555 pub fn select_panel(&mut self, panel: PanelId, window: &mut Window, cx: &mut Context<Self>) {
564 let Some(placement) = self.placement_of_panel(panel) else {
565 return;
566 };
567 let Some(tree) = self.tree_mut(placement) else {
568 return;
569 };
570 let Some(node) = tree.find_panel_node(panel) else {
571 return;
572 };
573 let ix = tree.find_node(node).and_then(|node| match node.kind() {
574 PaneRef::Tabs { panels, .. } => panels.iter().position(|held| *held == panel),
575 PaneRef::Split { .. } => None,
576 });
577 let Some(ix) = ix else {
578 return;
579 };
580 let result = tree.set_active(node, ix);
581 self.commit(result, window, cx);
582 }
583
584 pub fn set_split_sizes(
586 &mut self,
587 node: NodeId,
588 sizes: Vec<Pixels>,
589 window: &mut Window,
590 cx: &mut Context<Self>,
591 ) {
592 let Some(placement) = self.placement_of_node(node) else {
593 return;
594 };
595 let Some(tree) = self.tree_mut(placement) else {
596 return;
597 };
598 let result = tree.set_sizes(node, sizes.into_iter().map(Some).collect());
599 self.commit(result, window, cx);
600 }
601
602 pub fn split_at(
604 &mut self,
605 node: NodeId,
606 panel: PanelId,
607 placement: Placement,
608 window: &mut Window,
609 cx: &mut Context<Self>,
610 ) {
611 let Some(region) = self.placement_of_node(node) else {
612 return;
613 };
614 self.adopt_measured_sizes(region, cx);
615 let Some(tree) = self.tree_mut(region) else {
616 return;
617 };
618 let result = tree.split(node, panel, placement, None);
619 self.commit(result, window, cx);
620 }
621
622 fn remove_panel_id(&mut self, panel: PanelId, window: &mut Window, cx: &mut Context<Self>) {
623 let Some(region) = self.placement_of_panel(panel) else {
624 return;
625 };
626 let focused = window.focused(cx);
627 let had_focus = self
628 .panel(panel)
629 .is_some_and(|view| view.focus_handle(cx).contains_focused(window, cx));
630 let mut candidates = Vec::new();
634 if had_focus && let Some(tree) = self.layout(region) {
635 tree.root().walk(&mut |node| {
636 if matches!(node.kind(), PaneRef::Tabs { .. }) {
637 candidates.push(node.id());
638 }
639 });
640 if let Some(ix) = tree
641 .find_panel_node(panel)
642 .and_then(|node| candidates.iter().position(|candidate| *candidate == node))
643 {
644 candidates = candidates[ix..]
645 .iter()
646 .chain(candidates[..ix].iter().rev())
647 .copied()
648 .collect();
649 }
650 }
651 let Some(tree) = self.tree_mut(region) else {
652 return;
653 };
654 let result = tree.remove_panel(panel);
655 self.commit(result, window, cx);
656
657 if !had_focus || window.focused(cx) != focused {
660 return;
661 }
662 let next = candidates
663 .into_iter()
664 .find_map(|node| self.focus_target_in_group(node, cx))
665 .or_else(|| {
666 [
667 DockPlacement::Center,
668 DockPlacement::Left,
669 DockPlacement::Right,
670 DockPlacement::Bottom,
671 ]
672 .into_iter()
673 .filter(|placement| *placement != region)
674 .filter(|placement| {
675 *placement == DockPlacement::Center
676 || self
677 .docks
678 .get(placement)
679 .is_some_and(|pane| pane.dock.is_open())
680 })
681 .find_map(|placement| {
682 self.layout(placement)
683 .and_then(|tree| self.focus_target_in_node(tree.root(), cx))
684 })
685 });
686 if let Some(next) = next {
687 next.focus_handle(cx).focus(window, cx);
688 }
689 }
690
691 fn focus_target_in_group(&self, node: NodeId, cx: &App) -> Option<Arc<dyn PanelView>> {
692 if self.zoomed.is_some_and(|zoom| zoom != Zoomed::Group(node)) {
693 return None;
694 }
695 let group = self.groups.get(&node)?.entity.read(cx);
696 if group.is_collapsed() {
697 return None;
698 }
699 group.active_panel(cx)
700 }
701
702 fn focus_target_in_node(&self, node: &PaneNode, cx: &App) -> Option<Arc<dyn PanelView>> {
703 match node.kind() {
704 PaneRef::Tabs { .. } => self.focus_target_in_group(node.id(), cx),
705 PaneRef::Split { children, .. } => children
706 .iter()
707 .find_map(|child| self.focus_target_in_node(child, cx)),
708 }
709 }
710}
711
712impl DockArea {
722 pub fn set_zoomed_in(&mut self, node: NodeId, window: &mut Window, cx: &mut Context<Self>) {
728 self.set_zoom(Some(Zoomed::Group(node)), window, cx);
729 }
730
731 pub fn set_zoomed_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
737 self.set_zoom(None, window, cx);
738 }
739
740 pub fn is_zoomed(&self) -> bool {
741 self.zoomed.is_some()
742 }
743
744 pub fn zoomed_group(&self) -> Option<NodeId> {
746 match self.zoomed {
747 Some(Zoomed::Group(node)) => Some(node),
748 _ => None,
749 }
750 }
751
752 fn set_zoom(&mut self, zoomed: Option<Zoomed>, window: &mut Window, cx: &mut Context<Self>) {
760 if self.zoomed == zoomed {
761 return;
762 }
763
764 if let Some(previous) = self.zoomed {
765 self.drive_zoom(previous, false, window, cx);
766 }
767 let accepted = match zoomed {
768 Some(next) => self.drive_zoom(next, true, window, cx).then_some(next),
769 None => None,
770 };
771 self.zoomed = accepted;
772 cx.notify();
773 }
774
775 fn drive_zoom(
781 &mut self,
782 zoomed: Zoomed,
783 zoom_in: bool,
784 window: &mut Window,
785 cx: &mut Context<Self>,
786 ) -> bool {
787 match zoomed {
788 Zoomed::Group(node) => {
789 let Some(group) = self.groups.get(&node).map(|cached| cached.entity.clone()) else {
790 return false;
791 };
792 group.update(cx, |group, cx| {
793 group.set_zoomed(zoom_in, window, cx);
794 group.is_zoomed() == zoom_in
795 })
796 }
797 }
798 }
799
800 fn zoomed_view(&self) -> Option<AnyView> {
805 match self.zoomed? {
806 Zoomed::Group(node) => Some(self.groups.get(&node)?.entity.clone().into()),
807 }
808 }
809}
810
811impl DockArea {
813 pub fn load(
818 &mut self,
819 state: DockAreaState,
820 window: &mut Window,
821 cx: &mut Context<Self>,
822 ) -> Result<()> {
823 self.version = state.version;
824 self.zoomed = None;
825 self.groups.clear();
829 self.splits.clear();
830 self.docks.clear();
831 let dock_area = self.this.clone();
836 let renderer = self.renderer.clone();
837 let mut built = Vec::new();
838 self.center = {
839 let mut builder = RegistryPanelBuilder {
840 dock_area: dock_area.clone(),
841 renderer: renderer.clone(),
842 built: &mut built,
843 window,
844 cx,
845 };
846 PaneTree::from_state(&state.center, RootKind::Split, &mut builder)
847 };
848
849 for dock_state in [state.left_dock, state.right_dock, state.bottom_dock]
850 .into_iter()
851 .flatten()
852 {
853 let tree = {
854 let mut builder = RegistryPanelBuilder {
855 dock_area: dock_area.clone(),
856 renderer: renderer.clone(),
857 built: &mut built,
858 window,
859 cx,
860 };
861 PaneTree::from_state(dock_state.panel(), RootKind::Any, &mut builder)
862 };
863 let mut dock = Dock::new(dock_state.size());
864 dock.set_open(dock_state.open());
865 self.docks
866 .insert(dock_state.placement(), DockRegion { tree, dock });
867 }
868
869 self.panels.extend(built);
870 self.reconcile(window, cx);
871 cx.emit(DockEvent::LayoutChanged);
872 Ok(())
873 }
874
875 pub fn dump(&self, cx: &App) -> DockAreaState {
896 let source = LivePanels::new(&self.panels, cx);
897
898 DockAreaState {
899 version: self.version,
900 center: self.resolved_tree(&self.center, cx).to_state(&source),
901 left_dock: self.dump_dock(DockPlacement::Left, &source, cx),
902 right_dock: self.dump_dock(DockPlacement::Right, &source, cx),
903 bottom_dock: self.dump_dock(DockPlacement::Bottom, &source, cx),
904 }
905 }
906
907 fn dump_dock(
908 &self,
909 placement: DockPlacement,
910 source: &LivePanels<'_>,
911 cx: &App,
912 ) -> Option<DockState> {
913 let pane = self.docks.get(&placement)?;
914 Some(DockState::new(
915 self.resolved_tree(&pane.tree, cx).to_state(source),
916 placement,
917 pane.dock.size(),
918 pane.dock.is_open(),
919 ))
920 }
921
922 fn resolved_tree(&self, tree: &PaneTree, cx: &App) -> PaneTree {
923 let mut tree = tree.clone();
924 self.resolve_sizes(tree.root_mut(), cx);
925 tree
926 }
927
928 fn resolve_sizes(&self, node: &mut PaneNode, cx: &App) {
929 let measured = self
930 .splits
931 .get(&node.id())
932 .map(|cached| cached.entity.read(cx).sizes().clone())
933 .unwrap_or_default();
934
935 let NodeKind::Split {
936 children, sizes, ..
937 } = node.kind_mut()
938 else {
939 return;
940 };
941
942 for (ix, size) in sizes.iter_mut().enumerate() {
943 let on_screen = measured.get(ix).copied().filter(|size| *size > px(0.));
948 let stored = (*size).filter(|size| *size > px(0.));
949 *size = Some(on_screen.or(stored).unwrap_or(PANEL_MIN_SIZE));
950 }
951
952 for child in children.iter_mut() {
953 self.resolve_sizes(child, cx);
954 }
955 }
956}
957
958impl DockArea {
960 fn commit(&mut self, result: EditResult, window: &mut Window, cx: &mut Context<Self>) {
969 self.commit_changed(result.changed(), window, cx);
970 }
971
972 fn commit_changed(&mut self, changed: bool, window: &mut Window, cx: &mut Context<Self>) {
974 if !changed {
975 return;
976 }
977
978 self.reconcile(window, cx);
979 cx.emit(DockEvent::LayoutChanged);
980 }
981
982 fn reconcile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
989 let mut plans = Vec::new();
992 plan_tree(&self.center, false, self.locked, &mut plans);
993 for pane in self.docks.values() {
994 plan_tree(&pane.tree, !pane.dock.is_open(), self.locked, &mut plans);
995 }
996
997 let mut live_nodes = HashSet::with_capacity(plans.len());
1001 let mut live_panels: HashSet<PanelId> = HashSet::new();
1002
1003 for plan in plans {
1004 live_nodes.insert(plan.node());
1005 match plan {
1006 ContainerPlan::Split {
1007 node,
1008 axis,
1009 children,
1010 sizes,
1011 } => {
1012 let state = self.split_entity(node, cx);
1013 let (previous, adopted) = self
1014 .splits
1015 .get(&node)
1016 .map(|cached| (cached.children.clone(), cached.sizes.clone()))
1017 .unwrap_or_default();
1018 if previous != children || adopted != sizes {
1026 state.update(cx, |state, cx| {
1027 sync_split_panels(state, &previous, &children, &sizes, cx);
1028 state.sync_panels_count(axis, children.len(), cx);
1029 state.adopt_sizes(&scale_sizes_to(state.container_size(), &sizes), cx);
1045 });
1046 }
1047 if let Some(cached) = self.splits.get_mut(&node) {
1048 cached.children = children;
1049 cached.sizes = sizes;
1050 }
1051 }
1052 ContainerPlan::Group {
1053 node,
1054 panels,
1055 active_ix,
1056 constraints,
1057 } => {
1058 live_panels.extend(panels.iter().copied());
1059 let views = self.views_of(&panels);
1060 let group = self.group_entity(node, window, cx);
1061 group.update(cx, |group, cx| {
1062 group.set_constraints(constraints, window, cx);
1066 group.sync_from_tree(views, active_ix, window, cx);
1067 });
1068 }
1069 }
1070 }
1071
1072 self.groups.retain(|node, _| live_nodes.contains(node));
1073 self.splits.retain(|node, _| live_nodes.contains(node));
1074
1075 let departed: Vec<Arc<dyn PanelView>> = self
1076 .panels
1077 .iter()
1078 .filter(|(panel, _)| !live_panels.contains(panel))
1079 .map(|(_, view)| view.clone())
1080 .collect();
1081 self.panels.retain(|panel, _| live_panels.contains(panel));
1082
1083 let zoom_survives = match self.zoomed {
1093 Some(Zoomed::Group(node)) => self.groups.contains_key(&node),
1094 None => true,
1095 };
1096 if !zoom_survives {
1097 self.set_zoom(None, window, cx);
1098 }
1099
1100 cx.notify();
1101 for view in departed {
1104 view.on_removed(window, cx);
1105 }
1106 }
1107
1108 fn views_of(&self, panels: &[PanelId]) -> Vec<Arc<dyn PanelView>> {
1110 debug_assert!(
1111 panels.iter().all(|panel| self.panels.contains_key(panel)),
1112 "every panel in a tree must have a live view; a missing one would \
1113 silently shift the group's active index"
1114 );
1115 panels
1116 .iter()
1117 .filter_map(|panel| self.panels.get(panel).cloned())
1118 .collect()
1119 }
1120
1121 fn group_entity(
1122 &mut self,
1123 node: NodeId,
1124 window: &mut Window,
1125 cx: &mut Context<Self>,
1126 ) -> Entity<TabGroup> {
1127 if let Some(cached) = self.groups.get(&node) {
1128 return cached.entity.clone();
1129 }
1130
1131 let renderer = self.renderer.tab_group_renderer();
1132 let entity = cx.new(|cx| TabGroup::new(node, window, cx).with_renderer(renderer));
1133 let subscription = cx.subscribe_in(&entity, window, Self::on_tab_group_event);
1134 self.groups.insert(
1135 node,
1136 Cached {
1137 entity: entity.clone(),
1138 _subscription: subscription,
1139 },
1140 );
1141 entity
1142 }
1143
1144 fn split_entity(&mut self, node: NodeId, cx: &mut Context<Self>) -> Entity<ResizableState> {
1145 if let Some(cached) = self.splits.get(&node) {
1146 return cached.entity.clone();
1147 }
1148
1149 let entity = cx.new(|_| ResizableState::default());
1150 let subscription =
1160 cx.subscribe(&entity, move |this, state, _: &ResizablePanelEvent, cx| {
1161 let sizes: Vec<Option<Pixels>> = state
1162 .read(cx)
1163 .sizes()
1164 .iter()
1165 .map(|size| Some(*size))
1166 .collect();
1167 let Some(region) = this.placement_of_node(node) else {
1168 return;
1169 };
1170 let Some(tree) = this.tree_mut(region) else {
1171 return;
1172 };
1173 if tree.set_sizes(node, sizes.clone()).changed() {
1174 cx.emit(DockEvent::LayoutChanged);
1175 }
1176 if let Some(cached) = this.splits.get_mut(&node) {
1179 cached.sizes = sizes;
1180 }
1181 });
1182 self.splits.insert(
1183 node,
1184 CachedSplit {
1185 entity: entity.clone(),
1186 children: Vec::new(),
1187 sizes: Vec::new(),
1188 _subscription: subscription,
1189 },
1190 );
1191 entity
1192 }
1193}
1194
1195impl DockArea {
1197 fn on_tab_group_event(
1198 &mut self,
1199 group: &Entity<TabGroup>,
1200 event: &TabGroupEvent,
1201 window: &mut Window,
1202 cx: &mut Context<Self>,
1203 ) {
1204 match event {
1205 TabGroupEvent::Drop { panel, target, .. } => {
1206 self.move_panel(*panel, *target, window, cx)
1207 }
1208 TabGroupEvent::DragDrop { item, target } => cx.emit(DockEvent::DragDrop {
1209 item: item.clone(),
1210 target: *target,
1211 }),
1212 TabGroupEvent::ClosePanel { panel } => self.remove_panel_id(*panel, window, cx),
1213 TabGroupEvent::ActiveChanged { ix } => {
1214 let node = group.read(cx).node();
1215 let Some(region) = self.placement_of_node(node) else {
1216 return;
1217 };
1218 let Some(tree) = self.tree_mut(region) else {
1219 return;
1220 };
1221 let result = tree.set_active(node, *ix);
1222 self.commit(result, window, cx);
1223 }
1224 TabGroupEvent::ZoomIn => {
1225 let node = group.read(cx).node();
1226 self.set_zoom(Some(Zoomed::Group(node)), window, cx);
1227 }
1228 TabGroupEvent::ZoomOut => {
1233 let node = group.read(cx).node();
1234 if self.zoomed == Some(Zoomed::Group(node)) {
1235 self.set_zoom(None, window, cx);
1236 }
1237 }
1238 }
1239 }
1240}
1241
1242impl DockArea {
1244 fn adopt_measured_sizes(&mut self, placement: DockPlacement, cx: &App) {
1251 let measured: HashMap<NodeId, Vec<Pixels>> = self
1252 .splits
1253 .iter()
1254 .filter(|(_, cached)| cached.entity.read(cx).container_size() > Pixels::ZERO)
1260 .map(|(node, cached)| (*node, cached.entity.read(cx).sizes().clone()))
1261 .collect();
1262
1263 if let Some(tree) = self.tree_mut(placement) {
1264 tree.adopt_measured_sizes(&measured);
1265 }
1266 }
1267
1268 fn tree_mut(&mut self, placement: DockPlacement) -> Option<&mut PaneTree> {
1269 match placement {
1270 DockPlacement::Center => Some(&mut self.center),
1271 _ => self.docks.get_mut(&placement).map(|pane| &mut pane.tree),
1272 }
1273 }
1274
1275 fn placement_of_node(&self, node: NodeId) -> Option<DockPlacement> {
1278 if self.center.find_node(node).is_some() {
1279 return Some(DockPlacement::Center);
1280 }
1281 self.docks
1282 .iter()
1283 .find(|(_, pane)| pane.tree.find_node(node).is_some())
1284 .map(|(placement, _)| *placement)
1285 }
1286
1287 fn placement_of_panel(&self, panel: PanelId) -> Option<DockPlacement> {
1288 if self.center.find_panel_node(panel).is_some() {
1289 return Some(DockPlacement::Center);
1290 }
1291 self.docks
1292 .iter()
1293 .find(|(_, pane)| pane.tree.find_panel_node(panel).is_some())
1294 .map(|(placement, _)| *placement)
1295 }
1296
1297 fn resize_dock(
1305 &mut self,
1306 placement: DockPlacement,
1307 pointer: Point<Pixels>,
1308 window: &mut Window,
1309 cx: &mut Context<Self>,
1310 ) {
1311 let opposite = match placement {
1312 DockPlacement::Left => self.dock_size(DockPlacement::Right),
1313 DockPlacement::Right => self.dock_size(DockPlacement::Left),
1314 _ => None,
1315 };
1316 let sizing = DockSizing::new(placement)
1317 .with_area_bounds(self.bounds)
1318 .with_opposite_dock_size(opposite.unwrap_or(px(0.)));
1319 let size = sizing
1320 .size_from_pointer(pointer)
1321 .min(sizing.clamp(Pixels::MAX));
1322
1323 let Some(pane) = self.docks.get_mut(&placement) else {
1324 return;
1325 };
1326 let was_open = pane.dock.is_open();
1327 if placement == DockPlacement::Bottom && pane.dock.is_collapsible() && size < PANEL_MIN_SIZE
1328 {
1329 pane.dock.set_open(size > CLOSED_BOTTOM_STRIP);
1330 pane.dock.set_live_size(Some(size.max(CLOSED_BOTTOM_STRIP)));
1331 } else {
1332 pane.dock.set_open(true);
1333 pane.dock.set_live_size(None);
1334 pane.dock.set_size(size);
1335 }
1336 if pane.dock.is_open() != was_open {
1337 self.reconcile(window, cx);
1338 }
1339 cx.notify();
1340 }
1341
1342 fn end_dock_resize(
1345 &mut self,
1346 placement: DockPlacement,
1347 window: &mut Window,
1348 cx: &mut Context<Self>,
1349 ) {
1350 let Some(pane) = self.docks.get_mut(&placement) else {
1351 return;
1352 };
1353 let Some(size) = pane.dock.live_size() else {
1354 return;
1355 };
1356 pane.dock.set_live_size(None);
1357
1358 let open = size >= (CLOSED_BOTTOM_STRIP + PANEL_MIN_SIZE) / 2.;
1359 if open {
1360 pane.dock.set_size(PANEL_MIN_SIZE);
1361 }
1362 if pane.dock.is_open() != open {
1363 pane.dock.set_open(open);
1364 self.reconcile(window, cx);
1365 }
1366 cx.notify();
1367 }
1368}
1369
1370impl DockArea {
1372 fn render_node(&self, node: &PaneNode, window: &mut Window, cx: &mut App) -> AnyElement {
1374 match node.kind() {
1375 PaneRef::Split {
1376 axis,
1377 children,
1378 sizes,
1379 } => {
1380 let group = match axis {
1381 Axis::Horizontal => h_resizable(("dock-split", node.id().as_u64())),
1382 Axis::Vertical => v_resizable(("dock-split", node.id().as_u64())),
1383 };
1384 let shown: Vec<bool> = children
1388 .iter()
1389 .map(|child| self.is_node_visible(child, cx))
1390 .collect();
1391 let grows = shown.iter().rposition(|shown| *shown);
1397 let panels: Vec<_> = children
1398 .iter()
1399 .zip(sizes.iter())
1400 .enumerate()
1401 .map(|(ix, (child, size))| {
1402 resizable_panel()
1403 .visible(shown[ix])
1404 .child(self.render_node(child, window, cx))
1405 .when_some(*size, |panel, size| {
1417 panel
1418 .size(size)
1419 .when(Some(ix) != grows, |panel| panel.flex_none())
1420 })
1421 })
1422 .collect();
1423
1424 let group = group
1425 .when_some(self.splits.get(&node.id()), |group, cached| {
1426 group.with_state(&cached.entity)
1427 })
1428 .with_handle_appearance({
1429 let renderer = self.renderer.clone();
1430 Rc::new(move |handle, window, cx| {
1431 renderer.render_split_handle(handle, window, cx)
1432 })
1433 })
1434 .children(panels);
1435
1436 self.renderer
1437 .split_frame(node.id(), axis, window, cx)
1438 .size_full()
1446 .flex_1()
1447 .min_h(px(0.))
1448 .overflow_hidden()
1449 .child(group)
1450 .into_any_element()
1451 }
1452 PaneRef::Tabs { .. } => match self.groups.get(&node.id()) {
1453 Some(cached) => cached.entity.clone().into_any_element(),
1454 None => Empty.into_any_element(),
1455 },
1456 }
1457 }
1458
1459 fn is_node_visible(&self, node: &PaneNode, cx: &App) -> bool {
1465 let panels = LivePanels::new(&self.panels, cx);
1466 match node.kind() {
1467 PaneRef::Split { children, .. } => {
1468 children.iter().any(|child| self.is_node_visible(child, cx))
1469 }
1470 PaneRef::Tabs { panels: ids, .. } => ids.iter().any(|panel| panels.is_visible(*panel)),
1471 }
1472 }
1473
1474 fn render_dock(
1475 &self,
1476 placement: DockPlacement,
1477 window: &mut Window,
1478 cx: &mut App,
1479 ) -> Option<AnyElement> {
1480 let pane = self.docks.get(&placement)?;
1481 let dock = self.dock_context(placement, &pane.dock);
1482
1483 let size = dock_extent(&dock);
1488 if size <= px(0.) {
1489 return Some(div().into_any_element());
1490 }
1491
1492 let content = self.render_node(pane.tree.root(), window, cx);
1493 let chrome = self.renderer.render_dock(&dock, content, window, cx);
1502 Some(dock_frame(&dock, size).child(chrome).into_any_element())
1503 }
1504
1505 fn dock_context(&self, placement: DockPlacement, dock: &Dock) -> DockContext {
1506 let area = self.this.clone();
1507
1508 DockContext {
1509 placement,
1510 size: dock.live_size().unwrap_or(dock.size()),
1511 open: dock.is_open(),
1512 collapsible: dock.is_collapsible(),
1513 on_toggle: {
1514 let area = area.clone();
1515 Rc::new(move |window, cx| {
1516 _ = area.update(cx, |area, cx| area.toggle_dock(placement, window, cx));
1517 })
1518 },
1519 on_resize: {
1520 let area = area.clone();
1521 Rc::new(move |pointer, window, cx| {
1522 _ = area.update(cx, |area, cx| {
1523 area.resize_dock(placement, pointer, window, cx)
1524 });
1525 })
1526 },
1527 on_resize_end: Rc::new(move |window, cx| {
1528 _ = area.update(cx, |area, cx| area.end_dock_resize(placement, window, cx));
1529 }),
1530 }
1531 }
1532}
1533
1534impl EventEmitter<DockEvent> for DockArea {}
1535
1536impl Focusable for DockArea {
1537 fn focus_handle(&self, _: &App) -> FocusHandle {
1538 self.focus_handle.clone()
1539 }
1540}
1541
1542impl Render for DockArea {
1543 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1544 let area = cx.entity();
1545 let renderer = self.renderer.clone();
1546
1547 renderer
1548 .frame(window, cx)
1549 .test_support()
1550 .relative()
1557 .size_full()
1558 .overflow_hidden()
1559 .flex()
1560 .flex_row()
1561 .on_prepaint(move |bounds, _, cx| {
1562 area.update(cx, |area, _| area.bounds = bounds);
1563 })
1564 .track_focus(&self.focus_handle)
1565 .map(|frame| match self.zoomed_view() {
1566 Some(view) => frame.child(view),
1567 None => frame
1568 .when_some(
1569 self.render_dock(DockPlacement::Left, window, cx),
1570 ParentElement::child,
1571 )
1572 .child(
1573 renderer
1574 .center_frame(window, cx)
1575 .flex()
1580 .flex_1()
1581 .flex_col()
1582 .overflow_hidden()
1583 .child(self.render_node(self.center.root(), window, cx))
1584 .when_some(
1585 self.render_dock(DockPlacement::Bottom, window, cx),
1586 ParentElement::child,
1587 ),
1588 )
1589 .when_some(
1590 self.render_dock(DockPlacement::Right, window, cx),
1591 ParentElement::child,
1592 ),
1593 })
1594 }
1595}
1596
1597enum ContainerPlan {
1599 Split {
1600 node: NodeId,
1601 axis: Axis,
1602 children: Vec<NodeId>,
1603 sizes: Vec<Option<Pixels>>,
1604 },
1605 Group {
1606 node: NodeId,
1607 panels: Vec<PanelId>,
1608 active_ix: usize,
1609 constraints: TabGroupConstraints,
1610 },
1611}
1612
1613impl ContainerPlan {
1614 fn node(&self) -> NodeId {
1615 match self {
1616 Self::Split { node, .. } | Self::Group { node, .. } => *node,
1617 }
1618 }
1619}
1620
1621fn scale_sizes_to(container: Pixels, sizes: &[Option<Pixels>]) -> Vec<Option<Pixels>> {
1628 let total: f32 = sizes.iter().flatten().map(|size| size.as_f32()).sum();
1629 if container <= px(0.) || total <= 0. || sizes.iter().any(Option::is_none) {
1630 return sizes.to_vec();
1631 }
1632
1633 let scale = container.as_f32() / total;
1634 sizes
1635 .iter()
1636 .map(|size| size.map(|size| px(size.as_f32() * scale)))
1637 .collect()
1638}
1639
1640fn sync_split_panels(
1650 state: &mut ResizableState,
1651 previous: &[NodeId],
1652 next: &[NodeId],
1653 sizes: &[Option<Pixels>],
1654 cx: &mut Context<ResizableState>,
1655) {
1656 let mut current = previous.to_vec();
1657
1658 for ix in (0..current.len()).rev() {
1660 if !next.contains(¤t[ix]) {
1661 if ix < state.sizes().len() {
1662 state.remove_panel(ix, cx);
1663 }
1664 current.remove(ix);
1665 }
1666 }
1667
1668 for (ix, node) in next.iter().enumerate() {
1669 if current.get(ix) == Some(node) {
1670 continue;
1671 }
1672 let at = ix.min(state.sizes().len());
1673 state.insert_panel(sizes.get(ix).copied().flatten(), Some(at), cx);
1678 current.insert(at.min(current.len()), *node);
1679 }
1680
1681 debug_assert_eq!(
1682 current, next,
1683 "the split's panel list must end up mirroring its children exactly; \
1684 a reordering edit would need its own case here"
1685 );
1686}
1687
1688fn plan_tree(tree: &PaneTree, collapsed: bool, locked: bool, out: &mut Vec<ContainerPlan>) {
1689 plan_node(tree.root(), true, collapsed, locked, out);
1691}
1692
1693fn plan_node(
1694 node: &PaneNode,
1695 alone: bool,
1696 collapsed: bool,
1697 locked: bool,
1698 out: &mut Vec<ContainerPlan>,
1699) {
1700 match node.kind() {
1701 PaneRef::Split {
1702 axis,
1703 children,
1704 sizes,
1705 } => {
1706 out.push(ContainerPlan::Split {
1707 node: node.id(),
1708 axis,
1709 children: children.iter().map(PaneNode::id).collect(),
1710 sizes: sizes.to_vec(),
1711 });
1712 let children_alone = children.len() <= 1;
1713 for child in children {
1714 plan_node(child, children_alone, collapsed, locked, out);
1715 }
1716 }
1717 PaneRef::Tabs { panels, active_ix } => out.push(ContainerPlan::Group {
1718 node: node.id(),
1719 panels: panels.to_vec(),
1720 active_ix,
1721 constraints: TabGroupConstraints::in_split(alone)
1722 .dock_locked(locked)
1723 .collapsed(collapsed),
1724 }),
1725 }
1726}
1727
1728fn first_tab_group(node: &PaneNode) -> Option<NodeId> {
1729 match node.kind() {
1730 PaneRef::Tabs { .. } => Some(node.id()),
1731 PaneRef::Split { children, .. } => children.iter().find_map(first_tab_group),
1732 }
1733}
1734
1735fn target_node(target: &InsertTarget) -> NodeId {
1736 match target {
1737 InsertTarget::Tabs { node, .. } | InsertTarget::Split { node, .. } => *node,
1738 }
1739}
1740
1741struct RegistryPanelBuilder<'a, 'w, 'c> {
1743 dock_area: WeakEntity<DockArea>,
1744 renderer: Rc<dyn DockAreaRenderer>,
1745 built: &'a mut Vec<(PanelId, Arc<dyn PanelView>)>,
1746 window: &'w mut Window,
1747 cx: &'c mut App,
1748}
1749
1750impl PanelBuilder for RegistryPanelBuilder<'_, '_, '_> {
1751 fn build(&mut self, state: &PanelState, info: &PanelInfo) -> PanelId {
1752 let context = PanelBuildContext::new(self.dock_area.clone(), state, info);
1753 let view =
1754 match PanelRegistry::build_panel(&state.panel_name, context, self.window, self.cx) {
1755 Some(view) => view,
1756 None => self
1757 .renderer
1758 .build_placeholder(state, self.window, self.cx)
1759 .unwrap_or_else(|| {
1760 Arc::new(self.cx.new(|cx| PlaceholderPanel::new(state.clone(), cx)))
1761 as Arc<dyn PanelView>
1762 }),
1763 };
1764
1765 let id = view.panel_id(self.cx);
1766 self.built.push((id, view));
1767 id
1768 }
1769}
1770
1771struct PlaceholderPanel {
1778 state: PanelState,
1779 focus_handle: FocusHandle,
1780}
1781
1782impl PlaceholderPanel {
1783 fn new(state: PanelState, cx: &mut Context<Self>) -> Self {
1784 Self {
1785 state,
1786 focus_handle: cx.focus_handle(),
1787 }
1788 }
1789}
1790
1791impl Panel for PlaceholderPanel {
1792 fn panel_name(&self) -> &'static str {
1793 "InvalidPanel"
1794 }
1795
1796 fn dump(&self, _: &App) -> PanelState {
1797 self.state.clone()
1798 }
1799}
1800
1801impl EventEmitter<PanelEvent> for PlaceholderPanel {}
1802
1803impl Focusable for PlaceholderPanel {
1804 fn focus_handle(&self, _: &App) -> FocusHandle {
1805 self.focus_handle.clone()
1806 }
1807}
1808
1809impl Render for PlaceholderPanel {
1810 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1811 Empty
1812 }
1813}
1814
1815type DockToggleHandler = Rc<dyn Fn(&mut Window, &mut App)>;
1816type DockResizeHandler = Rc<dyn Fn(Point<Pixels>, &mut Window, &mut App)>;
1817
1818#[derive(Clone)]
1821pub struct DockContext {
1822 placement: DockPlacement,
1823 size: Pixels,
1824 open: bool,
1825 collapsible: bool,
1826 on_toggle: DockToggleHandler,
1827 on_resize: DockResizeHandler,
1828 on_resize_end: DockToggleHandler,
1829}
1830
1831impl DockContext {
1832 pub fn placement(&self) -> DockPlacement {
1833 self.placement
1834 }
1835
1836 pub fn size(&self) -> Pixels {
1839 self.size
1840 }
1841
1842 pub fn is_open(&self) -> bool {
1843 self.open
1844 }
1845
1846 pub fn is_collapsible(&self) -> bool {
1847 self.collapsible
1848 }
1849
1850 pub fn toggle(&self, window: &mut Window, cx: &mut App) {
1851 (self.on_toggle)(window, cx);
1852 }
1853
1854 pub fn resize_to(&self, pointer: Point<Pixels>, window: &mut Window, cx: &mut App) {
1857 (self.on_resize)(pointer, window, cx);
1858 }
1859
1860 pub fn end_resize(&self, window: &mut Window, cx: &mut App) {
1863 (self.on_resize_end)(window, cx);
1864 }
1865}
1866
1867pub const CLOSED_BOTTOM_STRIP: Pixels = px(29.);
1871
1872pub fn dock_extent(dock: &DockContext) -> Pixels {
1874 match (dock.is_open(), dock.placement()) {
1875 (true, _) => dock.size(),
1876 (false, DockPlacement::Bottom) => CLOSED_BOTTOM_STRIP,
1877 (false, _) => px(0.),
1878 }
1879}
1880
1881pub fn dock_frame(dock: &DockContext, size: Pixels) -> Div {
1887 div()
1888 .flex()
1889 .flex_none()
1890 .relative()
1891 .overflow_hidden()
1892 .map(|this| match dock.placement() {
1893 DockPlacement::Left | DockPlacement::Right => this.flex_row().h_full().w(size),
1894 DockPlacement::Bottom => this.w_full().h(size),
1895 DockPlacement::Center => this,
1897 })
1898}
1899
1900#[allow(unused_variables)]
1911pub trait DockAreaRenderer: 'static {
1912 fn frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
1918 div().id("dock-area")
1919 }
1920
1921 fn split_frame(
1931 &self,
1932 node: NodeId,
1933 axis: Axis,
1934 window: &mut Window,
1935 cx: &mut App,
1936 ) -> Stateful<Div> {
1937 div().id(("dock-split-frame", node.as_u64()))
1938 }
1939
1940 fn center_frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
1944 div().id("dock-area-center")
1945 }
1946
1947 fn render_split_handle(
1954 &self,
1955 handle: &ResizeHandleContext,
1956 window: &mut Window,
1957 cx: &mut App,
1958 ) -> Option<AnyElement> {
1959 None
1960 }
1961
1962 fn render_dock(
1971 &self,
1972 dock: &DockContext,
1973 content: AnyElement,
1974 window: &mut Window,
1975 cx: &mut App,
1976 ) -> AnyElement {
1977 content
1978 }
1979
1980 fn build_placeholder(
1995 &self,
1996 state: &PanelState,
1997 window: &mut Window,
1998 cx: &mut App,
1999 ) -> Option<Arc<dyn PanelView>> {
2000 None
2001 }
2002
2003 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer>;
2004}
2005
2006struct BareDockArea;
2008
2009impl DockAreaRenderer for BareDockArea {
2010 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
2011 Rc::new(BareTabGroup)
2012 }
2013}
2014
2015#[cfg(test)]
2016impl DockArea {
2017 pub(crate) fn container_entity_ids(&self) -> Vec<(NodeId, gpui::EntityId)> {
2024 let mut ids: Vec<(NodeId, gpui::EntityId)> = self
2025 .groups
2026 .iter()
2027 .map(|(node, cached)| (*node, cached.entity.entity_id()))
2028 .chain(
2029 self.splits
2030 .iter()
2031 .map(|(node, cached)| (*node, cached.entity.entity_id())),
2032 )
2033 .collect();
2034 ids.sort();
2035 ids
2036 }
2037}
2038
2039#[cfg(test)]
2040mod tests {
2041 use gpui::{TestAppContext, VisualTestContext};
2042
2043 use std::{
2044 cell::{Cell, RefCell},
2045 rc::Rc,
2046 };
2047
2048 use super::*;
2049 use crate::dock::TabGroupContext;
2050 use crate::dock::test_support::{Log, PanelSignal, TestPanel, drain, drain_active, log_of};
2051
2052 #[test]
2055 fn slot_sizes_are_re_expressed_as_shares_of_the_container() {
2056 let scaled = scale_sizes_to(px(800.), &[Some(px(300.)), Some(px(100.))]);
2057
2058 assert_eq!(scaled, vec![Some(px(600.)), Some(px(200.))]);
2059 }
2060
2061 #[test]
2064 fn an_unconstrained_slot_leaves_every_size_alone() {
2065 let sizes = [Some(px(300.)), None];
2066
2067 assert_eq!(scale_sizes_to(px(800.), &sizes), sizes.to_vec());
2068 }
2069
2070 #[test]
2073 fn an_unusable_container_or_total_leaves_every_size_alone() {
2074 let sizes = [Some(px(300.)), Some(px(100.))];
2075 assert_eq!(scale_sizes_to(px(0.), &sizes), sizes.to_vec());
2076
2077 let zeroed = [Some(px(0.)), Some(px(0.))];
2078 assert_eq!(scale_sizes_to(px(800.), &zeroed), zeroed.to_vec());
2079 }
2080
2081 fn setup(cx: &mut TestAppContext) -> (Entity<DockArea>, &mut VisualTestContext) {
2082 cx.update(|cx| {
2083 let _ = crate::Theme::global_mut(cx);
2084 });
2085 cx.add_window_view(|window, cx| DockArea::new("test-dock", None, window, cx))
2086 }
2087
2088 #[gpui::test]
2089 fn dock_size_change_emits_one_layout_event(cx: &mut TestAppContext) {
2090 let (area, cx) = setup(cx);
2091 cx.update(|window, cx| {
2092 area.update(cx, |area, cx| {
2093 area.set_dock(
2094 DockPlacement::Left,
2095 DockLayout::tabs().panel(TestPanel::new("Left", cx)),
2096 window,
2097 cx,
2098 );
2099 });
2100 });
2101
2102 let events = Rc::new(Cell::new(0));
2103 let observed = events.clone();
2104 let _subscription = cx.update(|window, cx| {
2105 window.subscribe(&area, cx, move |_, event: &DockEvent, _, _| {
2106 if matches!(event, DockEvent::LayoutChanged) {
2107 observed.set(observed.get() + 1);
2108 }
2109 })
2110 });
2111
2112 cx.update(|window, cx| {
2113 area.update(cx, |area, cx| {
2114 area.set_dock_size(DockPlacement::Left, px(320.), window, cx);
2115 area.set_dock_size(DockPlacement::Left, px(320.), window, cx);
2116 });
2117 });
2118 assert_eq!(
2119 events.get(),
2120 1,
2121 "only an effective size change is persisted"
2122 );
2123 }
2124
2125 fn two_groups<'a>(
2127 log: &Log,
2128 cx: &'a mut TestAppContext,
2129 ) -> (
2130 Entity<DockArea>,
2131 Entity<TestPanel>,
2132 &'a mut VisualTestContext,
2133 ) {
2134 let (area, cx) = setup(cx);
2135 let log = log.clone();
2136 let alpha = cx.update(|window, cx| {
2137 let alpha = TestPanel::logging("Alpha", &log, cx);
2138 let beta = TestPanel::logging("Beta", &log, cx);
2139 area.update(cx, |area, cx| {
2140 area.set_center(
2141 DockLayout::h_split()
2142 .child(DockLayout::tabs().panel(alpha.clone()), None)
2143 .child(DockLayout::tabs().panel(beta), None),
2144 window,
2145 cx,
2146 );
2147 });
2148 alpha
2149 });
2150 (area, alpha, cx)
2151 }
2152
2153 fn child_node(area: &Entity<DockArea>, ix: usize, cx: &mut VisualTestContext) -> NodeId {
2155 cx.read(|cx| {
2156 let PaneRef::Split { children, .. } = area
2157 .read(cx)
2158 .layout(DockPlacement::Center)
2159 .unwrap()
2160 .root()
2161 .kind()
2162 else {
2163 panic!("the center root is a split");
2164 };
2165 children[ix].id()
2166 })
2167 }
2168
2169 fn panel_id_of(panel: &Entity<TestPanel>) -> PanelId {
2170 PanelId::from(panel.entity_id())
2171 }
2172
2173 fn move_alpha_into_the_other_group(
2174 area: &Entity<DockArea>,
2175 alpha: &Entity<TestPanel>,
2176 cx: &mut VisualTestContext,
2177 ) {
2178 let target = child_node(area, 1, cx);
2179 let alpha_id = panel_id_of(alpha);
2180 cx.update(|window, cx| {
2181 area.update(cx, |area, cx| {
2182 area.move_panel(
2183 alpha_id,
2184 InsertTarget::Tabs {
2185 node: target,
2186 ix: None,
2187 activate: true,
2188 },
2189 window,
2190 cx,
2191 );
2192 });
2193 });
2194 cx.run_until_parked();
2195 }
2196
2197 fn collect_sizes(state: &PanelState, out: &mut Vec<Pixels>) {
2198 if let PanelInfo::Stack { sizes, .. } = &state.info {
2199 out.extend(sizes.iter().copied());
2200 }
2201 for child in &state.children {
2202 collect_sizes(child, out);
2203 }
2204 }
2205
2206 fn register_test_panels(cx: &mut App) {
2207 for name in ["Alpha", "Beta", "Gamma"] {
2208 crate::dock::registry::register_panel(cx, name, move |_, _, cx| {
2209 Arc::new(TestPanel::new(name, cx)) as Arc<dyn PanelView>
2210 });
2211 }
2212 }
2213
2214 fn one_group<'a>(
2220 log: &Log,
2221 names: &[&'static str],
2222 active_ix: Option<usize>,
2223 cx: &'a mut TestAppContext,
2224 ) -> (
2225 Entity<DockArea>,
2226 Vec<Entity<TestPanel>>,
2227 &'a mut VisualTestContext,
2228 ) {
2229 let (area, cx) = setup(cx);
2230 let log = log.clone();
2231 let names = names.to_vec();
2232 let panels = cx.update(|window, cx| {
2233 let panels: Vec<_> = names
2234 .iter()
2235 .map(|name| TestPanel::logging(name, &log, cx))
2236 .collect();
2237 let layout = panels
2238 .iter()
2239 .fold(DockLayout::tabs(), |layout, panel| {
2240 layout.panel(panel.clone())
2241 })
2242 .active_index(active_ix.unwrap_or(0));
2243 area.update(cx, |area, cx| area.set_center(layout, window, cx));
2244 panels
2245 });
2246 (area, panels, cx)
2247 }
2248
2249 fn group_of(
2251 area: &Entity<DockArea>,
2252 ix: usize,
2253 cx: &mut VisualTestContext,
2254 ) -> Entity<TabGroup> {
2255 let node = child_node(area, ix, cx);
2256 cx.read(|cx| area.read(cx).groups.get(&node).unwrap().entity.clone())
2257 }
2258
2259 fn move_panel_into(
2260 area: &Entity<DockArea>,
2261 panel: PanelId,
2262 node: NodeId,
2263 ix: Option<usize>,
2264 activate: bool,
2265 cx: &mut VisualTestContext,
2266 ) {
2267 cx.update(|window, cx| {
2268 area.update(cx, |area, cx| {
2269 area.move_panel(panel, InsertTarget::Tabs { node, ix, activate }, window, cx);
2270 });
2271 });
2272 cx.run_until_parked();
2273 }
2274
2275 fn is_center_empty(area: &Entity<DockArea>, cx: &mut VisualTestContext) -> bool {
2276 cx.read(|cx| area.read(cx).is_empty(DockPlacement::Center, cx))
2277 }
2278
2279 #[gpui::test]
2280 fn a_layout_installs_and_dumps_back_to_the_same_state(cx: &mut TestAppContext) {
2281 let (area, cx) = setup(cx);
2282 cx.update(|window, cx| {
2283 let alpha = TestPanel::new("Alpha", cx);
2284 let beta = TestPanel::new("Beta", cx);
2285 area.update(cx, |area, cx| {
2286 area.set_center(
2287 DockLayout::h_split()
2288 .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2289 .child(DockLayout::tabs().panel(beta), None),
2290 window,
2291 cx,
2292 );
2293 });
2294 });
2295
2296 let state = cx.read(|cx| area.read(cx).dump(cx));
2297 assert_eq!(state.center.panel_name, "StackPanel");
2298 assert_eq!(state.center.children.len(), 2);
2299 assert_eq!(state.center.children[0].children[0].panel_name, "Alpha");
2300 assert_eq!(state.center.children[1].children[0].panel_name, "Beta");
2301 }
2302
2303 #[gpui::test]
2304 fn moving_a_panel_reuses_its_entity(cx: &mut TestAppContext) {
2305 let log = log_of();
2306 let (area, alpha, cx) = two_groups(&log, cx);
2307 cx.run_until_parked();
2308 drain(&log);
2309
2310 let destination = child_node(&area, 1, cx);
2311 let destination_entity = cx.read(|cx| {
2312 area.read(cx)
2313 .groups
2314 .get(&destination)
2315 .unwrap()
2316 .entity
2317 .entity_id()
2318 });
2319
2320 move_alpha_into_the_other_group(&area, &alpha, cx);
2321
2322 assert_eq!(
2323 cx.read(|cx| area
2324 .read(cx)
2325 .groups
2326 .get(&destination)
2327 .unwrap()
2328 .entity
2329 .entity_id()),
2330 destination_entity,
2331 "the group the panel arrived in was reused, not rebuilt"
2332 );
2333
2334 let alpha_id = panel_id_of(&alpha);
2339 assert!(
2340 cx.read(|cx| area
2341 .read(cx)
2342 .layout(DockPlacement::Center)
2343 .unwrap()
2344 .find_panel_node(alpha_id))
2345 .is_some(),
2346 "the moved panel is still in the tree"
2347 );
2348
2349 let state = cx.read(|cx| area.read(cx).dump(cx));
2350 assert_eq!(
2354 state.center.children.len(),
2355 1,
2356 "the emptied group collapsed out of the split"
2357 );
2358 assert_eq!(
2359 state.center.children[0].children.len(),
2360 2,
2361 "both panels now share the surviving group"
2362 );
2363 }
2364
2365 #[gpui::test]
2366 fn a_moved_panel_is_not_told_it_was_removed(cx: &mut TestAppContext) {
2367 let log = log_of();
2368 let (area, alpha, cx) = two_groups(&log, cx);
2369 cx.run_until_parked();
2370 drain(&log);
2371
2372 move_alpha_into_the_other_group(&area, &alpha, cx);
2373
2374 assert!(
2375 !drain(&log).contains(&("Alpha", PanelSignal::Removed)),
2376 "moving a panel between groups must never deliver on_removed"
2377 );
2378 }
2379
2380 #[gpui::test]
2381 fn removing_a_panel_does_tell_it_it_was_removed(cx: &mut TestAppContext) {
2382 let log = log_of();
2386 let (area, alpha, cx) = two_groups(&log, cx);
2387 cx.run_until_parked();
2388 drain(&log);
2389
2390 cx.update(|window, cx| {
2391 area.update(cx, |area, cx| area.remove_panel(alpha.clone(), window, cx));
2392 });
2393 cx.run_until_parked();
2394
2395 assert!(
2396 drain(&log).contains(&("Alpha", PanelSignal::Removed)),
2397 "a genuine removal must deliver on_removed"
2398 );
2399 }
2400
2401 #[gpui::test]
2402 fn reconciling_an_unchanged_tree_creates_no_entities(cx: &mut TestAppContext) {
2403 let log = log_of();
2404 let (area, _alpha, cx) = two_groups(&log, cx);
2405 cx.run_until_parked();
2406 drain(&log);
2407
2408 let before = cx.read(|cx| area.read(cx).container_entity_ids());
2409 cx.update(|window, cx| area.update(cx, |area, cx| area.reconcile(window, cx)));
2410 let after = cx.read(|cx| area.read(cx).container_entity_ids());
2411
2412 assert!(!before.is_empty(), "there were containers to preserve");
2413 assert_eq!(
2414 before, after,
2415 "a steady-state pass creates and drops nothing"
2416 );
2417 cx.run_until_parked();
2418 assert_eq!(
2419 drain(&log),
2420 vec![],
2421 "and no panel was re-added or re-activated by it"
2422 );
2423 }
2424
2425 #[gpui::test]
2426 fn a_loaded_layout_round_trips_through_dump(cx: &mut TestAppContext) {
2427 let (area, cx) = setup(cx);
2428 cx.update(|_, cx| register_test_panels(cx));
2429
2430 let json = include_str!("fixtures/nested_splits.json");
2431 let state: DockAreaState = serde_json::from_str(json).unwrap();
2432
2433 cx.update(|window, cx| {
2434 area.update(cx, |area, cx| area.load(state.clone(), window, cx).unwrap())
2435 });
2436 let dumped = cx.read(|cx| area.read(cx).dump(cx));
2437
2438 cx.update(|window, cx| {
2439 area.update(cx, |area, cx| {
2440 area.load(dumped.clone(), window, cx).unwrap()
2441 })
2442 });
2443 let again = cx.read(|cx| area.read(cx).dump(cx));
2444
2445 assert_eq!(dumped, again, "load/dump must reach a fixpoint");
2446 assert_eq!(
2447 dumped.center.children.len(),
2448 3,
2449 "the fixture's nesting is flattened, as the state layer already pins"
2450 );
2451 assert_eq!(dumped.center.children[0].children[0].panel_name, "Alpha");
2452 }
2453
2454 #[gpui::test]
2455 fn a_dumped_live_layout_has_no_zero_sizes(cx: &mut TestAppContext) {
2456 let (area, cx) = setup(cx);
2457 cx.update(|window, cx| {
2458 let alpha = TestPanel::new("Alpha", cx);
2459 let beta = TestPanel::new("Beta", cx);
2460 let gamma = TestPanel::new("Gamma", cx);
2461 area.update(cx, |area, cx| {
2462 area.set_center(
2463 DockLayout::v_split()
2464 .child(DockLayout::tabs().panel(alpha), None)
2467 .child(DockLayout::tabs().panel(beta), Some(px(0.)))
2470 .child(DockLayout::tabs().panel(gamma), Some(px(240.))),
2471 window,
2472 cx,
2473 );
2474 });
2475 });
2476
2477 let state = cx.read(|cx| area.read(cx).dump(cx));
2478 let mut sizes = Vec::new();
2479 collect_sizes(&state.center, &mut sizes);
2480
2481 assert!(!sizes.is_empty(), "the layout has slots to check");
2482 assert!(
2483 sizes.iter().all(|size| *size > px(0.)),
2484 "an older build reads a persisted 0.0 back as a real zero-pixel panel: {sizes:?}"
2485 );
2486 }
2487
2488 #[gpui::test]
2495 fn a_dumped_split_writes_the_sizes_it_is_actually_drawn_at(cx: &mut TestAppContext) {
2496 let (area, cx) = setup(cx);
2497 cx.update(|window, cx| {
2498 let alpha = TestPanel::new("Alpha", cx);
2499 let beta = TestPanel::new("Beta", cx);
2500 area.update(cx, |area, cx| {
2501 area.set_center(
2502 DockLayout::h_split()
2503 .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2504 .child(DockLayout::tabs().panel(beta), Some(px(300.))),
2505 window,
2506 cx,
2507 );
2508 });
2509 });
2510 cx.run_until_parked();
2511
2512 let root = cx.read(|cx| {
2513 area.read(cx)
2514 .layout(DockPlacement::Center)
2515 .unwrap()
2516 .root()
2517 .id()
2518 });
2519 let measured = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2520 assert_ne!(
2521 measured,
2522 vec![px(300.), px(300.)],
2523 "the split has to have been rescaled by a layout pass, or this \
2524 test cannot tell the two preferences apart"
2525 );
2526
2527 let state = cx.read(|cx| area.read(cx).dump(cx));
2528 let PanelInfo::Stack { sizes, .. } = &state.center.info else {
2529 panic!("the center writes a stack");
2530 };
2531 assert_eq!(
2532 sizes, &measured,
2533 "the written sizes are the ones on screen, not the ones the tree \
2534 was built from"
2535 );
2536 }
2537
2538 #[gpui::test]
2539 fn set_split_sizes_restores_a_share_and_reports_it(cx: &mut TestAppContext) {
2540 let (area, cx) = setup(cx);
2541 cx.update(|window, cx| {
2542 let alpha = TestPanel::new("Alpha", cx);
2543 let beta = TestPanel::new("Beta", cx);
2544 area.update(cx, |area, cx| {
2545 area.set_center(
2546 DockLayout::h_split()
2547 .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2548 .child(DockLayout::tabs().panel(beta), Some(px(300.))),
2549 window,
2550 cx,
2551 );
2552 });
2553 });
2554 cx.run_until_parked();
2555
2556 let root = cx.read(|cx| {
2557 area.read(cx)
2558 .layout(DockPlacement::Center)
2559 .unwrap()
2560 .root()
2561 .id()
2562 });
2563 let events = Rc::new(Cell::new(0));
2564 let observed = events.clone();
2565 let _subscription = cx.update(|window, cx| {
2566 window.subscribe(&area, cx, move |_, event: &DockEvent, _, _| {
2567 if matches!(event, DockEvent::LayoutChanged) {
2568 observed.set(observed.get() + 1);
2569 }
2570 })
2571 });
2572
2573 cx.update(|window, cx| {
2574 area.update(cx, |area, cx| {
2575 area.set_split_sizes(root, vec![px(100.), px(300.)], window, cx);
2576 });
2577 });
2578 cx.run_until_parked();
2579 assert_eq!(events.get(), 1);
2580 let measured = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2581 let share = measured[0].as_f32() / (measured[0].as_f32() + measured[1].as_f32());
2582 assert!((share - 0.25).abs() < 0.01, "measured {measured:?}");
2583
2584 cx.update(|window, cx| {
2585 area.update(cx, |area, cx| {
2586 area.set_split_sizes(root, vec![px(10.)], window, cx);
2587 });
2588 });
2589 cx.run_until_parked();
2590 assert_eq!(events.get(), 1);
2591 let after = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2592 assert_eq!(after, measured);
2593 }
2594
2595 #[gpui::test]
2601 fn a_panel_dropped_beside_another_takes_half_the_split(cx: &mut TestAppContext) {
2602 let log = Log::default();
2603 let (area, panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2604 let group = child_node(&area, 0, cx);
2605 let beta = panel_id_of(&panels[1]);
2606
2607 cx.update(|window, cx| {
2608 area.update(cx, |area, cx| {
2609 area.move_panel(
2610 beta,
2611 InsertTarget::Split {
2612 node: group,
2613 placement: Placement::Right,
2614 size: None,
2615 },
2616 window,
2617 cx,
2618 );
2619 });
2620 });
2621 cx.run_until_parked();
2622
2623 let root = cx.read(|cx| {
2624 area.read(cx)
2625 .layout(DockPlacement::Center)
2626 .unwrap()
2627 .root()
2628 .id()
2629 });
2630 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2631
2632 assert_eq!(sizes.len(), 2, "the drop splits the center in two");
2633 let (left, right) = (sizes[0].as_f32(), sizes[1].as_f32());
2634 assert!(
2635 (left - right).abs() <= (left + right) * 0.02,
2636 "the two halves must be within 2% of each other, got {left} and {right}"
2637 );
2638 }
2639
2640 #[gpui::test]
2642 fn a_move_of_an_unowned_panel_is_ignored(cx: &mut TestAppContext) {
2643 let log = Log::default();
2644 let (area, _panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2645 let group = child_node(&area, 0, cx);
2646 let before = cx.read(|cx| area.read(cx).dump(cx));
2647
2648 move_panel_into(&area, PanelId::from_u64(9_999_999), group, None, true, cx);
2650
2651 let after = cx.read(|cx| area.read(cx).dump(cx));
2652 assert_eq!(
2653 before, after,
2654 "an unowned panel move must not touch the tree"
2655 );
2656 }
2657
2658 #[gpui::test]
2662 fn a_panel_dropped_across_the_axis_still_takes_half(cx: &mut TestAppContext) {
2663 let log = Log::default();
2664 let (area, panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2665 let group = child_node(&area, 0, cx);
2666 let beta = panel_id_of(&panels[1]);
2667
2668 cx.update(|window, cx| {
2669 area.update(cx, |area, cx| {
2670 area.move_panel(
2671 beta,
2672 InsertTarget::Split {
2673 node: group,
2674 placement: Placement::Bottom,
2675 size: None,
2676 },
2677 window,
2678 cx,
2679 );
2680 });
2681 });
2682 cx.run_until_parked();
2683
2684 let wrapper = child_node(&area, 0, cx);
2688 let sizes = cx.read(|cx| {
2689 area.read(cx).splits[&wrapper]
2690 .entity
2691 .read(cx)
2692 .sizes()
2693 .clone()
2694 });
2695 assert_eq!(sizes.len(), 2, "the drop splits the group in two");
2696 let (top, bottom) = (sizes[0].as_f32(), sizes[1].as_f32());
2697 assert!(
2698 (top - bottom).abs() <= (top + bottom) * 0.02,
2699 "the two halves must be within 2% of each other, got {top} and {bottom}"
2700 );
2701 }
2702
2703 #[gpui::test]
2706 fn a_panel_dropped_into_a_populated_split_takes_an_even_share(cx: &mut TestAppContext) {
2707 let (area, cx) = setup(cx);
2708 let panels = cx.update(|window, cx| {
2709 let alpha = TestPanel::new("Alpha", cx);
2710 let beta = TestPanel::new("Beta", cx);
2711 let gamma = TestPanel::new("Gamma", cx);
2712 area.update(cx, |area, cx| {
2713 area.set_center(
2714 DockLayout::h_split()
2715 .child(DockLayout::tabs().panel(alpha.clone()), Some(px(240.)))
2716 .child(
2717 DockLayout::tabs().panel(beta.clone()).panel(gamma.clone()),
2718 None,
2719 ),
2720 window,
2721 cx,
2722 );
2723 });
2724 vec![alpha, beta, gamma]
2725 });
2726 cx.run_until_parked();
2727
2728 let right = child_node(&area, 1, cx);
2729 let gamma = panel_id_of(&panels[2]);
2730 cx.update(|window, cx| {
2731 area.update(cx, |area, cx| {
2732 area.move_panel(
2733 gamma,
2734 InsertTarget::Split {
2735 node: right,
2736 placement: Placement::Right,
2737 size: None,
2738 },
2739 window,
2740 cx,
2741 );
2742 });
2743 });
2744 cx.run_until_parked();
2745
2746 let root = cx.read(|cx| {
2747 area.read(cx)
2748 .layout(DockPlacement::Center)
2749 .unwrap()
2750 .root()
2751 .id()
2752 });
2753 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2754 assert_eq!(sizes.len(), 3, "three slots side by side");
2755 let dropped = sizes[2].as_f32();
2756 let neighbour = sizes[1].as_f32();
2757 assert!(
2758 (dropped - neighbour).abs() <= (dropped + neighbour) * 0.02,
2759 "the dropped panel splits its neighbour evenly, got neighbour {neighbour} and dropped {dropped}"
2760 );
2761 }
2762
2763 #[gpui::test]
2767 fn a_panel_dropped_beside_a_dock_takes_half(cx: &mut TestAppContext) {
2768 for placement in [DockPlacement::Bottom, DockPlacement::Left] {
2769 let (area, cx) = setup(cx);
2770 let dropped = cx.update(|window, cx| {
2771 let resident = TestPanel::new("Resident", cx);
2772 let dropped = TestPanel::new("Dropped", cx);
2773 area.update(cx, |area, cx| {
2774 area.set_center(
2775 DockLayout::tabs().panel(TestPanel::new("Center", cx)),
2776 window,
2777 cx,
2778 );
2779 area.set_dock(
2780 placement,
2781 DockLayout::tabs().panel(resident).panel(dropped.clone()),
2782 window,
2783 cx,
2784 );
2785 area.set_dock_size(placement, px(400.), window, cx);
2786 });
2787 dropped
2788 });
2789 cx.run_until_parked();
2790
2791 let group = cx.read(|cx| {
2792 area.read(cx)
2793 .layout(placement)
2794 .unwrap()
2795 .find_panel_node(panel_id_of(&dropped))
2796 .expect("both panels start in the dock's only group")
2797 });
2798
2799 cx.update(|window, cx| {
2800 area.update(cx, |area, cx| {
2801 area.move_panel(
2802 panel_id_of(&dropped),
2803 InsertTarget::Split {
2804 node: group,
2805 placement: Placement::Bottom,
2806 size: None,
2807 },
2808 window,
2809 cx,
2810 );
2811 });
2812 });
2813 cx.run_until_parked();
2814
2815 let split = cx.read(|cx| {
2816 let tree = area.read(cx).layout(placement).unwrap();
2817 let root = tree.root();
2818 match root.kind() {
2819 PaneRef::Split { .. } => root.id(),
2820 _ => panic!("the drop must have produced a split"),
2821 }
2822 });
2823 let sizes = cx.read(|cx| area.read(cx).splits[&split].entity.read(cx).sizes().clone());
2824
2825 assert_eq!(sizes.len(), 2, "{placement:?}: the drop splits in two");
2826 let (first, second) = (sizes[0].as_f32(), sizes[1].as_f32());
2827 assert!(
2828 (first - second).abs() <= (first + second) * 0.02,
2829 "{placement:?}: expected halves, got {first} and {second}"
2830 );
2831 }
2832 }
2833
2834 #[gpui::test]
2841 fn an_explicit_slot_size_survives_the_first_layout_pass(cx: &mut TestAppContext) {
2842 let (area, cx) = setup(cx);
2843 cx.update(|window, cx| {
2844 let sidebar = TestPanel::new("Sidebar", cx);
2845 let content = TestPanel::new("Content", cx);
2846 area.update(cx, |area, cx| {
2847 area.set_center(
2848 DockLayout::h_split()
2849 .child(DockLayout::tabs().panel(sidebar), Some(px(200.)))
2850 .child(DockLayout::tabs().panel(content), None),
2851 window,
2852 cx,
2853 );
2854 });
2855 });
2856 cx.run_until_parked();
2857
2858 let root = cx.read(|cx| {
2859 area.read(cx)
2860 .layout(DockPlacement::Center)
2861 .unwrap()
2862 .root()
2863 .id()
2864 });
2865 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2866
2867 let fixed = sizes
2871 .first()
2872 .copied()
2873 .expect("the split has slots")
2874 .as_f32();
2875 assert!(
2876 (fixed - 200.).abs() <= 4.,
2877 "the fixed slot keeps its 200px instead of being rescaled by the \
2878 flexible sibling's placeholder, got {fixed}"
2879 );
2880 }
2881
2882 struct MeasuredPanel {
2885 name: &'static str,
2886 focus_handle: FocusHandle,
2887 }
2888
2889 impl MeasuredPanel {
2890 fn new(name: &'static str, cx: &mut App) -> Entity<Self> {
2891 cx.new(|cx| Self {
2892 name,
2893 focus_handle: cx.focus_handle(),
2894 })
2895 }
2896 }
2897
2898 impl Panel for MeasuredPanel {
2899 fn panel_name(&self) -> &'static str {
2900 self.name
2901 }
2902 }
2903
2904 impl EventEmitter<PanelEvent> for MeasuredPanel {}
2905
2906 impl Focusable for MeasuredPanel {
2907 fn focus_handle(&self, _: &App) -> FocusHandle {
2908 self.focus_handle.clone()
2909 }
2910 }
2911
2912 impl Render for MeasuredPanel {
2913 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2914 let name = self.name;
2915 div().size_full().debug_selector(move || name.into())
2916 }
2917 }
2918
2919 fn draw_frames(cx: &mut VisualTestContext, frames: usize) {
2920 for _ in 0..frames {
2921 cx.update(|window, cx| window.draw(cx).clear(cx));
2922 }
2923 }
2924
2925 #[gpui::test]
2932 fn switching_a_tab_leaves_an_untouched_split_where_it_was_drawn(cx: &mut TestAppContext) {
2933 let (area, cx) = setup(cx);
2934 cx.update(|window, cx| {
2935 area.update(cx, |area, cx| {
2936 area.set_center(
2937 DockLayout::tabs().panel(TestPanel::new("Center", cx)),
2938 window,
2939 cx,
2940 );
2941 area.set_dock(
2942 DockPlacement::Left,
2943 DockLayout::v_split()
2944 .child(
2945 DockLayout::tabs().panel(MeasuredPanel::new("upper-left", cx)),
2946 None,
2947 )
2948 .child(
2949 DockLayout::tabs().panel(MeasuredPanel::new("lower-left", cx)),
2950 Some(px(360.)),
2951 ),
2952 window,
2953 cx,
2954 );
2955 area.set_dock_size(DockPlacement::Left, px(350.), window, cx);
2956 area.set_dock(
2957 DockPlacement::Bottom,
2958 DockLayout::tabs()
2959 .panel(TestPanel::new("Tooltip", cx))
2960 .panel(TestPanel::new("Icon", cx)),
2961 window,
2962 cx,
2963 );
2964 area.set_dock_size(DockPlacement::Bottom, px(200.), window, cx);
2965 });
2966 });
2967 cx.run_until_parked();
2968 draw_frames(cx, 3);
2969 let before = (
2970 cx.debug_bounds("upper-left").unwrap(),
2971 cx.debug_bounds("lower-left").unwrap(),
2972 );
2973
2974 let bottom = cx.read(|cx| {
2975 area.read(cx)
2976 .layout(DockPlacement::Bottom)
2977 .unwrap()
2978 .root()
2979 .id()
2980 });
2981 let group = cx.read(|cx| area.read(cx).groups[&bottom].entity.clone());
2982 cx.update(|window, cx| {
2983 group.update(cx, |group, cx| group.select_tab(1, window, cx));
2984 });
2985 cx.run_until_parked();
2986 draw_frames(cx, 3);
2987 let after = (
2988 cx.debug_bounds("upper-left").unwrap(),
2989 cx.debug_bounds("lower-left").unwrap(),
2990 );
2991
2992 assert_eq!(
2993 before, after,
2994 "a tab change in the bottom dock must not move the left split"
2995 );
2996 }
2997
2998 #[gpui::test]
3003 fn switching_a_tab_keeps_a_restored_split_at_its_rescaled_share(cx: &mut TestAppContext) {
3004 let (area, cx) = setup(cx);
3005 cx.update(|window, cx| {
3006 area.update(cx, |area, cx| {
3007 area.set_center(
3008 DockLayout::h_split()
3009 .child(
3010 DockLayout::tabs()
3011 .panel(TestPanel::new("Alpha", cx))
3012 .panel(TestPanel::new("Beta", cx)),
3013 Some(px(620.)),
3014 )
3015 .child(
3016 DockLayout::tabs().panel(MeasuredPanel::new("second", cx)),
3017 Some(px(350.)),
3018 ),
3019 window,
3020 cx,
3021 );
3022 });
3023 });
3024 cx.run_until_parked();
3025 draw_frames(cx, 3);
3026 let before = cx.debug_bounds("second").unwrap();
3029 assert_ne!(
3030 before.right(),
3031 px(970.),
3032 "the window must not match the recorded total, or this test cannot \
3033 tell a rescaled split from the file's pixels"
3034 );
3035
3036 let group = group_of(&area, 0, cx);
3037 cx.update(|window, cx| {
3038 group.update(cx, |group, cx| group.select_tab(1, window, cx));
3039 });
3040 cx.run_until_parked();
3041 draw_frames(cx, 3);
3042 let after = cx.debug_bounds("second").unwrap();
3043
3044 assert_eq!(
3045 before, after,
3046 "a tab change must not hand the file's pixels back to the split"
3047 );
3048 }
3049
3050 #[gpui::test]
3061 fn the_shipped_fixture_survives_a_load_dump_load_round_trip(cx: &mut TestAppContext) {
3062 let (area, cx) = setup(cx);
3063 let fixture: DockAreaState =
3064 serde_json::from_str(include_str!("fixtures/layout.json")).unwrap();
3065
3066 cx.update(|window, cx| area.update(cx, |area, cx| area.load(fixture, window, cx).unwrap()));
3067 cx.run_until_parked();
3068 let first = cx.read(|cx| area.read(cx).dump(cx));
3069
3070 assert_eq!(first.center.children.len(), 2, "the center's two groups");
3071 assert_eq!(first.center.children[0].children.len(), 15);
3072 assert_eq!(first.center.children[1].children.len(), 1);
3073 for dock in [&first.left_dock, &first.bottom_dock, &first.right_dock] {
3074 let dock = dock.as_ref().expect("all three docks are attached");
3075 assert!(dock.open());
3076 assert!(
3077 !dock.panel().children.is_empty(),
3078 "a dock that loaded empty would round-trip just as stably"
3079 );
3080 }
3081 assert_eq!(first.left_dock.as_ref().unwrap().size(), px(350.));
3082 assert_eq!(first.bottom_dock.as_ref().unwrap().size(), px(200.));
3083 assert_eq!(first.right_dock.as_ref().unwrap().size(), px(320.));
3084
3085 cx.update(|window, cx| {
3086 area.update(cx, |area, cx| area.load(first.clone(), window, cx).unwrap())
3087 });
3088 cx.run_until_parked();
3089 let second = cx.read(|cx| area.read(cx).dump(cx));
3090
3091 assert_eq!(second, first, "dump == dump(load(dump))");
3092 }
3093
3094 #[gpui::test]
3095 fn an_unregistered_panel_survives_a_load_and_save_round_trip(cx: &mut TestAppContext) {
3096 let (area, cx) = setup(cx);
3097 cx.update(|_, cx| register_test_panels(cx));
3098
3099 let json = include_str!("fixtures/unregistered_panel.json");
3100 let state: DockAreaState = serde_json::from_str(json).unwrap();
3101
3102 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
3103 let dumped = cx.read(|cx| area.read(cx).dump(cx));
3104
3105 let leaf = &dumped.center.children[0].children[0];
3106 assert_eq!(leaf.panel_name, "PanelFromTheFuture");
3107 assert_eq!(
3108 leaf.info,
3109 PanelInfo::panel(serde_json::json!({"keep": "me"})),
3110 "a panel this build cannot construct keeps its payload"
3111 );
3112 }
3113
3114 #[gpui::test]
3115 fn a_dock_carries_its_own_tree_and_survives_a_round_trip(cx: &mut TestAppContext) {
3116 let (area, cx) = setup(cx);
3117 cx.update(|window, cx| {
3118 let alpha = TestPanel::new("Alpha", cx);
3119 let beta = TestPanel::new("Beta", cx);
3120 area.update(cx, |area, cx| {
3121 area.set_center(DockLayout::tabs().panel(alpha), window, cx);
3122 area.set_dock(
3123 DockPlacement::Left,
3124 DockLayout::tabs().panel(beta),
3125 window,
3126 cx,
3127 );
3128 });
3129 });
3130
3131 let center_ids = cx.read(|cx| {
3140 area.read(cx)
3141 .layout(DockPlacement::Center)
3142 .unwrap()
3143 .node_ids()
3144 });
3145 let dock_ids = cx.read(|cx| {
3146 area.read(cx)
3147 .layout(DockPlacement::Left)
3148 .unwrap()
3149 .node_ids()
3150 });
3151 assert!(!center_ids.is_empty() && !dock_ids.is_empty());
3152 assert!(
3153 center_ids.iter().all(|id| !dock_ids.contains(id)),
3154 "every tree in one area must draw from one id space: \
3155 center {center_ids:?} vs left {dock_ids:?}"
3156 );
3157
3158 let state = cx.read(|cx| area.read(cx).dump(cx));
3159 let left = state.left_dock.clone().expect("the left dock is written");
3160 assert_eq!(left.placement(), DockPlacement::Left);
3161 assert!(left.open());
3162 assert_eq!(left.panel().children[0].panel_name, "Beta");
3163 assert!(state.right_dock.is_none());
3164 }
3165
3166 #[gpui::test]
3167 fn a_panel_moved_between_regions_keeps_its_active_state(cx: &mut TestAppContext) {
3168 let log = log_of();
3169 let (area, alpha, cx) = two_groups(&log, cx);
3170 cx.run_until_parked();
3171 assert!(drain(&log).contains(&("Alpha", PanelSignal::Active(true))));
3174
3175 move_alpha_into_the_other_group(&area, &alpha, cx);
3176
3177 assert!(
3178 !drain(&log).contains(&("Alpha", PanelSignal::Active(true))),
3179 "a displayed panel dragged to another group must not be told `true` twice"
3180 );
3181 }
3182
3183 #[gpui::test]
3184 fn a_groups_close_intent_reaches_the_tree(cx: &mut TestAppContext) {
3185 let log = log_of();
3189 let (area, alpha, cx) = two_groups(&log, cx);
3190 cx.run_until_parked();
3191 drain(&log);
3192
3193 let node = child_node(&area, 0, cx);
3194 let alpha_id = panel_id_of(&alpha);
3195 cx.update(|_, cx| {
3196 let group = area.read(cx).groups.get(&node).unwrap().entity.clone();
3197 group.update(cx, |group, cx| group.close_panel(alpha_id, cx));
3198 });
3199 cx.run_until_parked();
3200
3201 assert!(
3202 cx.read(|cx| area
3203 .read(cx)
3204 .layout(DockPlacement::Center)
3205 .unwrap()
3206 .find_panel_node(alpha_id))
3207 .is_none(),
3208 "the close intent was applied to the tree"
3209 );
3210 assert!(drain(&log).contains(&("Alpha", PanelSignal::Removed)));
3211 }
3212
3213 #[gpui::test]
3214 fn replacing_the_center_tells_the_panels_that_left(cx: &mut TestAppContext) {
3215 let log = log_of();
3216 let (area, _alpha, cx) = two_groups(&log, cx);
3217 cx.run_until_parked();
3218 drain(&log);
3219
3220 cx.update(|window, cx| {
3221 let gamma = TestPanel::new("Gamma", cx);
3222 area.update(cx, |area, cx| {
3223 area.set_center(DockLayout::tabs().panel(gamma), window, cx)
3224 });
3225 });
3226 cx.run_until_parked();
3227
3228 let seen = drain(&log);
3229 assert!(seen.contains(&("Alpha", PanelSignal::Removed)));
3230 assert!(seen.contains(&("Beta", PanelSignal::Removed)));
3231 }
3232
3233 #[gpui::test]
3234 fn closing_a_zoomed_panel_clears_the_zoom(cx: &mut TestAppContext) {
3235 let log = log_of();
3236 let (area, alpha, cx) = two_groups(&log, cx);
3237 cx.run_until_parked();
3238
3239 let node = child_node(&area, 0, cx);
3240 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_in(node, window, cx)));
3241 assert!(cx.read(|cx| area.read(cx).is_zoomed()));
3242
3243 cx.update(|window, cx| {
3244 area.update(cx, |area, cx| area.remove_panel(alpha.clone(), window, cx))
3245 });
3246
3247 assert!(
3248 !cx.read(|cx| area.read(cx).is_zoomed()),
3249 "a zoomed panel that left the dock must not keep filling it"
3250 );
3251 }
3252
3253 struct SkinPlaceholder {
3257 state: PanelState,
3258 focus_handle: FocusHandle,
3259 }
3260
3261 impl Panel for SkinPlaceholder {
3262 fn panel_name(&self) -> &'static str {
3263 "SkinPlaceholder"
3264 }
3265
3266 fn dump(&self, _: &App) -> PanelState {
3267 self.state.clone()
3268 }
3269 }
3270
3271 impl EventEmitter<PanelEvent> for SkinPlaceholder {}
3272
3273 impl Focusable for SkinPlaceholder {
3274 fn focus_handle(&self, _: &App) -> FocusHandle {
3275 self.focus_handle.clone()
3276 }
3277 }
3278
3279 impl Render for SkinPlaceholder {
3280 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3281 Empty
3282 }
3283 }
3284
3285 struct PlaceholderSkin {
3286 asked: Rc<std::cell::RefCell<Vec<String>>>,
3287 }
3288
3289 impl DockAreaRenderer for PlaceholderSkin {
3290 fn build_placeholder(
3291 &self,
3292 state: &PanelState,
3293 _: &mut Window,
3294 cx: &mut App,
3295 ) -> Option<Arc<dyn PanelView>> {
3296 self.asked.borrow_mut().push(state.panel_name.clone());
3297 let state = state.clone();
3298 Some(Arc::new(cx.new(|cx| SkinPlaceholder {
3299 state,
3300 focus_handle: cx.focus_handle(),
3301 })))
3302 }
3303
3304 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
3305 Rc::new(BareTabGroup)
3306 }
3307 }
3308
3309 #[gpui::test]
3313 fn an_unbuildable_panel_becomes_the_skins_placeholder(cx: &mut TestAppContext) {
3314 cx.update(|cx| {
3315 let _ = crate::Theme::global_mut(cx);
3316 });
3317 let asked: Rc<std::cell::RefCell<Vec<String>>> = Rc::default();
3318 let skin = Rc::new(PlaceholderSkin {
3319 asked: asked.clone(),
3320 });
3321 let (area, cx) = cx.add_window_view(|window, cx| {
3322 DockArea::new("test-dock", None, window, cx).with_renderer(skin)
3323 });
3324
3325 cx.update(|window, cx| {
3327 let ghost = TestPanel::new("Ghost", cx);
3328 area.update(cx, |area, cx| {
3329 area.set_center(DockLayout::tabs().panel(ghost), window, cx)
3330 });
3331 });
3332 let state = cx.read(|cx| area.read(cx).dump(cx));
3333 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
3334 cx.run_until_parked();
3335
3336 assert_eq!(*asked.borrow(), vec!["Ghost".to_string()]);
3337 assert_eq!(
3338 cx.read(|cx| area
3339 .read(cx)
3340 .panels
3341 .values()
3342 .map(|panel| panel.panel_name(cx))
3343 .collect::<Vec<_>>()),
3344 vec!["SkinPlaceholder"],
3345 "base installed the skin's placeholder, not its own"
3346 );
3347 assert_eq!(
3348 cx.read(|cx| area.read(cx).dump(cx)).center.children[0].children[0].panel_name,
3349 "Ghost",
3350 "and the unknown panel still survives the next save"
3351 );
3352 }
3353
3354 #[gpui::test]
3355 fn removing_a_non_tail_child_shifts_the_split_sizes_with_it(cx: &mut TestAppContext) {
3356 let log = log_of();
3360 let (area, cx) = setup(cx);
3361 let alpha = cx.update(|window, cx| {
3362 let alpha = TestPanel::logging("Alpha", &log, cx);
3363 let beta = TestPanel::logging("Beta", &log, cx);
3364 let gamma = TestPanel::logging("Gamma", &log, cx);
3365 area.update(cx, |area, cx| {
3366 area.set_center(
3367 DockLayout::h_split()
3368 .child(DockLayout::tabs().panel(alpha.clone()), Some(px(100.)))
3369 .child(DockLayout::tabs().panel(beta), Some(px(200.)))
3370 .child(DockLayout::tabs().panel(gamma), Some(px(300.))),
3371 window,
3372 cx,
3373 );
3374 });
3375 alpha
3376 });
3377
3378 let root = cx.read(|cx| {
3379 area.read(cx)
3380 .layout(DockPlacement::Center)
3381 .unwrap()
3382 .root()
3383 .id()
3384 });
3385 let split = cx.read(|cx| area.read(cx).splits.get(&root).unwrap().entity.clone());
3386 let sizes = |cx: &mut VisualTestContext| cx.read(|cx| split.read(cx).sizes().clone());
3387
3388 let before = sizes(cx);
3392 assert_eq!(before.len(), 3);
3393 let kept_if_correct = before[1] / before[2];
3394 let kept_if_truncated = before[0] / before[1];
3395 assert!(
3396 (kept_if_correct - kept_if_truncated).abs() > 0.1,
3397 "the fixture must be able to tell the two outcomes apart"
3398 );
3399
3400 cx.update(|window, cx| area.update(cx, |area, cx| area.remove_panel(alpha, window, cx)));
3401
3402 let after = sizes(cx);
3403 assert_eq!(after.len(), 2);
3404 assert!(
3405 (after[0] / after[1] - kept_if_correct).abs() < 0.01,
3406 "the survivors kept their own proportions: slot 0 was removed, not \
3407 the tail — got {after:?} from {before:?}"
3408 );
3409 }
3410
3411 #[gpui::test]
3412 fn a_panel_moves_between_the_center_and_a_dock(cx: &mut TestAppContext) {
3413 let log = log_of();
3414 let (area, alpha, cx) = two_groups(&log, cx);
3415 cx.update(|window, cx| {
3416 let gamma = TestPanel::logging("Gamma", &log, cx);
3417 area.update(cx, |area, cx| {
3418 area.set_dock(
3419 DockPlacement::Left,
3420 DockLayout::tabs().panel(gamma),
3421 window,
3422 cx,
3423 );
3424 });
3425 });
3426 cx.run_until_parked();
3427 drain(&log);
3428
3429 let alpha_id = panel_id_of(&alpha);
3430 let dock_group = cx.read(|cx| {
3431 area.read(cx)
3432 .layout(DockPlacement::Left)
3433 .unwrap()
3434 .root()
3435 .id()
3436 });
3437
3438 cx.update(|window, cx| {
3439 area.update(cx, |area, cx| {
3440 area.move_panel(
3441 alpha_id,
3442 InsertTarget::Tabs {
3443 node: dock_group,
3444 ix: None,
3445 activate: true,
3446 },
3447 window,
3448 cx,
3449 );
3450 });
3451 });
3452 cx.run_until_parked();
3453
3454 assert!(
3455 cx.read(|cx| area
3456 .read(cx)
3457 .layout(DockPlacement::Center)
3458 .unwrap()
3459 .find_panel_node(alpha_id))
3460 .is_none(),
3461 "the panel left the center"
3462 );
3463 assert_eq!(
3464 cx.read(|cx| area
3465 .read(cx)
3466 .layout(DockPlacement::Left)
3467 .unwrap()
3468 .find_panel_node(alpha_id)),
3469 Some(dock_group),
3470 "and arrived in the dock's group"
3471 );
3472
3473 let seen = drain(&log);
3474 assert!(
3475 !seen.contains(&("Alpha", PanelSignal::Removed)),
3476 "crossing regions is still a move, not a removal"
3477 );
3478 assert!(
3479 !seen.contains(&("Alpha", PanelSignal::Active(true))),
3480 "and it was displayed in both, so it is not told `true` twice"
3481 );
3482 }
3483
3484 #[gpui::test]
3485 fn a_move_onto_an_unusable_target_leaves_no_stranded_panel(cx: &mut TestAppContext) {
3486 let log = log_of();
3491 let (area, _alpha, cx) = two_groups(&log, cx);
3492 let gamma = cx.update(|window, cx| {
3493 let gamma = TestPanel::logging("Gamma", &log, cx);
3494 area.update(cx, |area, cx| {
3495 area.set_dock(
3496 DockPlacement::Left,
3497 DockLayout::tabs().panel(gamma.clone()),
3498 window,
3499 cx,
3500 );
3501 });
3502 gamma
3503 });
3504 cx.run_until_parked();
3505 drain(&log);
3506
3507 let gamma_id = panel_id_of(&gamma);
3508 let center_root = cx.read(|cx| {
3511 area.read(cx)
3512 .layout(DockPlacement::Center)
3513 .unwrap()
3514 .root()
3515 .id()
3516 });
3517
3518 cx.update(|window, cx| {
3519 area.update(cx, |area, cx| {
3520 area.move_panel(
3521 gamma_id,
3522 InsertTarget::Tabs {
3523 node: center_root,
3524 ix: None,
3525 activate: true,
3526 },
3527 window,
3528 cx,
3529 );
3530 });
3531 });
3532 cx.run_until_parked();
3533
3534 assert!(
3535 cx.read(|cx| area.read(cx).panel(gamma_id).is_none()),
3536 "the view map agrees with the trees straight away, rather than \
3537 carrying a panel that belongs to no tree"
3538 );
3539 assert!(
3540 drain(&log).contains(&("Gamma", PanelSignal::Removed)),
3541 "and the panel was told so at the point of the call"
3542 );
3543 }
3544
3545 #[gpui::test]
3546 fn an_all_hidden_container_reports_itself_invisible(cx: &mut TestAppContext) {
3547 let (area, cx) = setup(cx);
3550 let beta = cx.update(|window, cx| {
3551 let alpha = TestPanel::new("Alpha", cx);
3552 let beta = TestPanel::new("Beta", cx);
3553 area.update(cx, |area, cx| {
3554 area.set_center(
3555 DockLayout::h_split()
3556 .child(DockLayout::tabs().panel(alpha), None)
3557 .child(DockLayout::tabs().panel(beta.clone()), None),
3558 window,
3559 cx,
3560 );
3561 });
3562 beta
3563 });
3564
3565 let visible = |ix: usize, cx: &mut VisualTestContext| {
3566 let node = child_node(&area, ix, cx);
3567 cx.read(|cx| {
3568 let area = area.read(cx);
3569 let tree = area.layout(DockPlacement::Center).unwrap();
3570 area.is_node_visible(tree.find_node(node).unwrap(), cx)
3571 })
3572 };
3573
3574 assert!(visible(0, cx) && visible(1, cx));
3575
3576 cx.update(|_, cx| beta.update(cx, |beta, cx| beta.set_visible(false, cx)));
3577
3578 assert!(visible(0, cx), "the visible group still holds its slot");
3579 assert!(
3580 !visible(1, cx),
3581 "a group whose every panel is hidden must give its slot up"
3582 );
3583 }
3584
3585 #[gpui::test]
3586 fn a_locked_area_seals_its_groups(cx: &mut TestAppContext) {
3587 let log = log_of();
3588 let (area, _alpha, cx) = two_groups(&log, cx);
3589 cx.run_until_parked();
3590
3591 let node = child_node(&area, 0, cx);
3592 let group = cx.read(|cx| area.read(cx).groups.get(&node).unwrap().entity.clone());
3593 assert!(
3594 cx.read(|cx| group.read(cx).is_closable(cx)),
3595 "an unlocked group's panel can be closed"
3596 );
3597
3598 cx.update(|window, cx| area.update(cx, |area, cx| area.set_locked(true, window, cx)));
3599
3600 assert!(
3601 !cx.read(|cx| group.read(cx).is_closable(cx)),
3602 "the lock reaches every group through the constraints push"
3603 );
3604 }
3605
3606 #[gpui::test]
3614 fn empty_center_round_trips_as_a_stack(cx: &mut TestAppContext) {
3615 let (area, cx) = setup(cx);
3616 let center = cx.read(|cx| area.read(cx).dump(cx).center);
3617
3618 assert_eq!(center.panel_name, "StackPanel");
3619 assert!(
3620 matches!(center.info, PanelInfo::Stack { .. }),
3621 "got {:?}",
3622 center.info
3623 );
3624 }
3625
3626 #[gpui::test]
3627 fn fresh_center_is_empty(cx: &mut TestAppContext) {
3628 let (area, cx) = setup(cx);
3629
3630 assert!(
3631 is_center_empty(&area, cx),
3632 "DockArea::new starts with an empty split centre"
3633 );
3634 }
3635
3636 #[gpui::test]
3637 fn center_holding_a_tab_group_is_not_empty(cx: &mut TestAppContext) {
3638 let log = log_of();
3639 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3640 cx.run_until_parked();
3641
3642 assert!(!is_center_empty(&area, cx));
3643 }
3644
3645 #[gpui::test]
3649 fn center_is_empty_again_once_every_panel_is_removed(cx: &mut TestAppContext) {
3650 let log = log_of();
3651 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3652 cx.run_until_parked();
3653
3654 for panel in panels {
3655 cx.update(|window, cx| {
3656 area.update(cx, |area, cx| area.remove_panel(panel.clone(), window, cx))
3657 });
3658 }
3659 cx.run_until_parked();
3660
3661 assert!(is_center_empty(&area, cx));
3662 }
3663
3664 #[gpui::test]
3665 fn center_is_not_empty_after_adding_to_a_tab_group(cx: &mut TestAppContext) {
3666 let (area, cx) = setup(cx);
3667 assert!(is_center_empty(&area, cx));
3668
3669 cx.update(|window, cx| {
3670 let alpha = TestPanel::new("Alpha", cx);
3671 area.update(cx, |area, cx| {
3672 area.add_panel(alpha, DockPlacement::Center, None, window, cx)
3673 });
3674 });
3675 cx.run_until_parked();
3676
3677 assert!(!is_center_empty(&area, cx));
3678 }
3679
3680 #[gpui::test]
3684 fn add_panel_view_registers_the_handle_it_was_given(cx: &mut TestAppContext) {
3685 let (area, cx) = setup(cx);
3686
3687 let view = cx.update(|window, cx| {
3688 let view: Arc<dyn PanelView> = Arc::new(TestPanel::new("Alpha", cx));
3689 area.update(cx, |area, cx| {
3690 area.add_panel_view(view.clone(), DockPlacement::Center, None, window, cx)
3691 });
3692 view
3693 });
3694 cx.run_until_parked();
3695
3696 let id = cx.read(|cx| view.panel_id(cx));
3697 assert!(
3698 cx.read(|cx| area
3699 .read(cx)
3700 .panel(id)
3701 .is_some_and(|stored| Arc::ptr_eq(stored, &view))),
3702 "the stored handle is the one that was handed over, under its own id"
3703 );
3704 assert!(!is_center_empty(&area, cx));
3705 }
3706
3707 #[gpui::test]
3710 fn center_holding_only_hidden_panels_is_empty(cx: &mut TestAppContext) {
3711 let log = log_of();
3712 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3713 cx.run_until_parked();
3714 assert!(!is_center_empty(&area, cx));
3715
3716 cx.update(|_, cx| {
3717 for panel in &panels {
3718 panel.update(cx, |panel, cx| panel.set_visible(false, cx));
3719 }
3720 });
3721 cx.run_until_parked();
3722
3723 assert_eq!(
3724 cx.read(|cx| area
3725 .read(cx)
3726 .layout(DockPlacement::Center)
3727 .unwrap()
3728 .panels()
3729 .count()),
3730 2,
3731 "hiding a panel does not remove it from the tab group"
3732 );
3733 assert!(is_center_empty(&area, cx));
3734 }
3735
3736 #[gpui::test]
3737 fn single_panel_group_receives_initial_active(cx: &mut TestAppContext) {
3738 let log = log_of();
3739 let (_area, _panels, cx) = one_group(&log, &["A"], None, cx);
3740 cx.run_until_parked();
3741
3742 assert_eq!(drain_active(&log), [("A", true)]);
3743 }
3744
3745 #[gpui::test]
3746 fn multi_tab_construction_notifies_only_displayed_panel(cx: &mut TestAppContext) {
3747 let log = log_of();
3748 let (_area, _panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
3749 cx.run_until_parked();
3750
3751 assert_eq!(drain_active(&log), [("A", true)]);
3753 }
3754
3755 #[gpui::test]
3756 fn active_index_restore_notifies_that_panel_only(cx: &mut TestAppContext) {
3757 let log = log_of();
3758 let (_area, _panels, cx) = one_group(&log, &["A", "B", "C"], Some(2), cx);
3759 cx.run_until_parked();
3760
3761 assert_eq!(drain_active(&log), [("C", true)]);
3762 }
3763
3764 #[gpui::test]
3765 fn switching_tabs_sends_false_then_true(cx: &mut TestAppContext) {
3766 let log = log_of();
3767 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3768 cx.run_until_parked();
3769 drain(&log);
3770
3771 let group = group_of(&area, 0, cx);
3772 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(1, window, cx)));
3773 cx.run_until_parked();
3774
3775 assert_eq!(drain_active(&log), [("A", false), ("B", true)]);
3776 }
3777
3778 #[gpui::test]
3779 fn select_panel_displays_that_tab_where_it_sits(cx: &mut TestAppContext) {
3780 let log = log_of();
3781 let (area, panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
3782 cx.run_until_parked();
3783 drain(&log);
3784
3785 let b = panel_id_of(&panels[1]);
3786 cx.update(|window, cx| area.update(cx, |area, cx| area.select_panel(b, window, cx)));
3787 cx.run_until_parked();
3788
3789 assert_eq!(drain_active(&log), [("A", false), ("B", true)]);
3790 let (order, active_ix) = cx.read(|cx| {
3791 let tree = area.read(cx).layout(DockPlacement::Center).unwrap();
3792 let node = tree.find_panel_node(b).unwrap();
3793 match tree.find_node(node).unwrap().kind() {
3794 PaneRef::Tabs { panels, active_ix } => (panels.to_vec(), active_ix),
3795 PaneRef::Split { .. } => panic!("a tab group"),
3796 }
3797 });
3798 assert_eq!(active_ix, 1, "the selected tab is displayed");
3799 assert_eq!(
3800 order,
3801 panels.iter().map(panel_id_of).collect::<Vec<_>>(),
3802 "selecting a tab does not move it"
3803 );
3804 }
3805
3806 #[gpui::test]
3807 fn select_panel_is_silent_for_the_displayed_or_an_unknown_panel(cx: &mut TestAppContext) {
3808 let log = log_of();
3809 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3810 cx.run_until_parked();
3811 drain(&log);
3812
3813 let a = panel_id_of(&panels[0]);
3814 let stranger = cx.update(|_, cx| panel_id_of(&TestPanel::logging("Z", &log, cx)));
3815 cx.update(|window, cx| {
3816 area.update(cx, |area, cx| {
3817 area.select_panel(a, window, cx);
3818 area.select_panel(stranger, window, cx);
3819 })
3820 });
3821 cx.run_until_parked();
3822
3823 assert_eq!(drain_active(&log), []);
3824 }
3825
3826 #[gpui::test]
3827 fn reselecting_active_tab_stays_silent(cx: &mut TestAppContext) {
3828 let log = log_of();
3829 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3830 cx.run_until_parked();
3831 drain(&log);
3832
3833 let group = group_of(&area, 0, cx);
3834 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(0, window, cx)));
3835 cx.run_until_parked();
3836
3837 assert_eq!(drain_active(&log), []);
3838 }
3839
3840 #[gpui::test]
3846 fn inserting_at_active_ix_swaps_notifications(cx: &mut TestAppContext) {
3847 let log = log_of();
3848 let (area, cx) = setup(cx);
3849 let c = cx.update(|window, cx| {
3850 let a = TestPanel::logging("A", &log, cx);
3851 let b = TestPanel::logging("B", &log, cx);
3852 let x = TestPanel::logging("X", &log, cx);
3853 let c = TestPanel::logging("C", &log, cx);
3854 area.update(cx, |area, cx| {
3855 area.set_center(
3856 DockLayout::h_split()
3857 .child(DockLayout::tabs().panel(a).panel(b), None)
3858 .child(DockLayout::tabs().panel(x).panel(c.clone()), None),
3859 window,
3860 cx,
3861 );
3862 });
3863 c
3864 });
3865 cx.run_until_parked();
3866 drain(&log);
3867
3868 let destination = child_node(&area, 0, cx);
3869 let c_id = panel_id_of(&c);
3870 move_panel_into(&area, c_id, destination, Some(0), true, cx);
3871
3872 assert_eq!(drain_active(&log), [("A", false), ("C", true)]);
3873 let group = group_of(&area, 0, cx);
3874 assert_eq!(cx.read(|cx| group.read(cx).active_ix()), 0);
3875 assert_eq!(
3876 cx.read(|cx| group.read(cx).panels()[0].panel_id(cx)),
3877 c_id,
3878 "the arriving panel took the slot it named"
3879 );
3880 }
3881
3882 #[gpui::test]
3883 fn removing_before_active_keeps_displayed_panel(cx: &mut TestAppContext) {
3884 let log = log_of();
3885 let (area, panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
3886 let group = group_of(&area, 0, cx);
3887 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(1, window, cx)));
3888 cx.run_until_parked();
3889 drain(&log);
3890
3891 cx.update(|window, cx| {
3892 area.update(cx, |area, cx| {
3893 area.remove_panel(panels[0].clone(), window, cx)
3894 })
3895 });
3896 cx.run_until_parked();
3897
3898 assert_eq!(drain_active(&log), []);
3899 assert_eq!(cx.read(|cx| group.read(cx).active_ix()), 0);
3900 assert_eq!(
3901 cx.read(|cx| group.read(cx).panels()[0].panel_id(cx)),
3902 panel_id_of(&panels[1]),
3903 "the same panel is still displayed, at its new index"
3904 );
3905 }
3906
3907 #[gpui::test]
3910 fn collapse_and_expand_notify_active_panel(cx: &mut TestAppContext) {
3911 let log = log_of();
3912 let (area, cx) = setup(cx);
3913 cx.update(|window, cx| {
3914 let a = TestPanel::logging("A", &log, cx);
3915 let b = TestPanel::logging("B", &log, cx);
3916 area.update(cx, |area, cx| {
3917 area.set_dock(
3918 DockPlacement::Left,
3919 DockLayout::tabs().panel(a).panel(b),
3920 window,
3921 cx,
3922 );
3923 });
3924 });
3925 cx.run_until_parked();
3926 drain(&log);
3927
3928 cx.update(|window, cx| {
3929 area.update(cx, |area, cx| {
3930 area.toggle_dock(DockPlacement::Left, window, cx)
3931 })
3932 });
3933 cx.run_until_parked();
3934 assert_eq!(drain_active(&log), [("A", false)]);
3935
3936 cx.update(|window, cx| {
3937 area.update(cx, |area, cx| {
3938 area.toggle_dock(DockPlacement::Left, window, cx)
3939 })
3940 });
3941 cx.run_until_parked();
3942 assert_eq!(drain_active(&log), [("A", true)]);
3943 }
3944
3945 #[gpui::test]
3946 fn background_add_is_silent_but_first_panel_is_not(cx: &mut TestAppContext) {
3947 let log = log_of();
3948 let (area, cx) = setup(cx);
3949 let d = cx.update(|window, cx| {
3950 let a = TestPanel::logging("A", &log, cx);
3951 let b = TestPanel::logging("B", &log, cx);
3952 let c = TestPanel::logging("C", &log, cx);
3953 let d = TestPanel::logging("D", &log, cx);
3954 area.update(cx, |area, cx| {
3955 area.set_center(
3956 DockLayout::h_split()
3957 .child(DockLayout::tabs().panel(a).panel(b), None)
3958 .child(DockLayout::tabs().panel(c).panel(d.clone()), None),
3959 window,
3960 cx,
3961 );
3962 });
3963 d
3964 });
3965 cx.run_until_parked();
3966 drain(&log);
3967
3968 let destination = child_node(&area, 0, cx);
3971 move_panel_into(&area, panel_id_of(&d), destination, None, false, cx);
3972 assert_eq!(drain_active(&log), []);
3973
3974 cx.update(|window, cx| {
3977 let e = TestPanel::logging("E", &log, cx);
3978 area.update(cx, |area, cx| {
3979 area.add_panel(e, DockPlacement::Left, None, window, cx)
3980 });
3981 });
3982 cx.run_until_parked();
3983 assert_eq!(drain_active(&log), [("E", true)]);
3984 }
3985
3986 #[gpui::test]
3987 fn drag_active_panel_to_other_group_stays_silent_for_it(cx: &mut TestAppContext) {
3988 let log = log_of();
3989 let (area, cx) = setup(cx);
3990 let a = cx.update(|window, cx| {
3991 let a = TestPanel::logging("A", &log, cx);
3992 let b = TestPanel::logging("B", &log, cx);
3993 let c = TestPanel::logging("C", &log, cx);
3994 area.update(cx, |area, cx| {
3995 area.set_center(
3996 DockLayout::h_split()
3997 .child(DockLayout::tabs().panel(a.clone()).panel(b), None)
3998 .child(DockLayout::tabs().panel(c), None),
3999 window,
4000 cx,
4001 );
4002 });
4003 a
4004 });
4005 cx.run_until_parked();
4006 drain(&log);
4007
4008 let destination = child_node(&area, 1, cx);
4011 move_panel_into(&area, panel_id_of(&a), destination, None, true, cx);
4012
4013 let seen = drain_active(&log);
4016 assert!(seen.contains(&("B", true)), "got {seen:?}");
4017 assert!(seen.contains(&("C", false)), "got {seen:?}");
4018 assert!(
4019 !seen.iter().any(|(name, _)| *name == "A"),
4020 "the moved panel was displayed before and after: {seen:?}"
4021 );
4022 }
4023
4024 #[gpui::test]
4025 fn drag_active_panel_to_background_slot_deactivates_it(cx: &mut TestAppContext) {
4026 let log = log_of();
4027 let (area, cx) = setup(cx);
4028 let a = cx.update(|window, cx| {
4029 let a = TestPanel::logging("A", &log, cx);
4030 let c = TestPanel::logging("C", &log, cx);
4031 let d = TestPanel::logging("D", &log, cx);
4032 area.update(cx, |area, cx| {
4033 area.set_center(
4034 DockLayout::h_split()
4035 .child(DockLayout::tabs().panel(a.clone()), None)
4036 .child(DockLayout::tabs().panel(c).panel(d), None),
4037 window,
4038 cx,
4039 );
4040 });
4041 a
4042 });
4043 cx.run_until_parked();
4044 drain(&log);
4045
4046 let destination = child_node(&area, 1, cx);
4049 move_panel_into(&area, panel_id_of(&a), destination, None, false, cx);
4050
4051 assert_eq!(drain_active(&log), [("A", false)]);
4052 }
4053
4054 struct RecordingSkin {
4061 tab_bars: Rc<RefCell<Vec<NodeId>>>,
4062 }
4063
4064 struct RecordingTabGroup {
4065 drawn: Rc<RefCell<Vec<NodeId>>>,
4066 }
4067
4068 impl DockAreaRenderer for RecordingSkin {
4069 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
4070 Rc::new(RecordingTabGroup {
4071 drawn: self.tab_bars.clone(),
4072 })
4073 }
4074 }
4075
4076 impl TabGroupRenderer for RecordingTabGroup {
4077 fn render_tab_bar(
4078 &self,
4079 group: &TabGroupContext,
4080 _: &mut Window,
4081 _: &mut App,
4082 ) -> AnyElement {
4083 self.drawn.borrow_mut().push(group.node());
4084 Empty.into_any_element()
4085 }
4086 }
4087
4088 type DrawLog = Rc<RefCell<Vec<NodeId>>>;
4089
4090 fn setup_recording(
4092 cx: &mut TestAppContext,
4093 ) -> (Entity<DockArea>, DrawLog, &mut VisualTestContext) {
4094 cx.update(|cx| {
4095 let _ = crate::Theme::global_mut(cx);
4096 });
4097 let tab_bars: DrawLog = Rc::default();
4098 let skin = Rc::new(RecordingSkin {
4099 tab_bars: tab_bars.clone(),
4100 });
4101 let (area, cx) = cx.add_window_view(|window, cx| {
4102 DockArea::new("test-dock", None, window, cx).with_renderer(skin)
4103 });
4104 (area, tab_bars, cx)
4105 }
4106
4107 fn zoom_signals(log: &Log) -> Vec<(&'static str, PanelSignal)> {
4108 drain(log)
4109 .into_iter()
4110 .filter(|(_, signal)| matches!(signal, PanelSignal::Zoomed(_)))
4111 .collect()
4112 }
4113
4114 #[gpui::test]
4123 fn a_zoomed_group_is_drawn_whole_rather_than_as_its_bare_panel(cx: &mut TestAppContext) {
4124 let log = log_of();
4125 let (area, tab_bars, cx) = setup_recording(cx);
4126 cx.update(|window, cx| {
4127 let alpha = TestPanel::logging("Alpha", &log, cx);
4128 let beta = TestPanel::logging("Beta", &log, cx);
4129 area.update(cx, |area, cx| {
4130 area.set_center(
4131 DockLayout::h_split()
4132 .child(DockLayout::tabs().panel(alpha), None)
4133 .child(DockLayout::tabs().panel(beta), None),
4134 window,
4135 cx,
4136 );
4137 });
4138 });
4139 cx.run_until_parked();
4140
4141 let zoomed = child_node(&area, 0, cx);
4142 let other = child_node(&area, 1, cx);
4143 assert!(
4144 tab_bars.borrow().contains(&zoomed) && tab_bars.borrow().contains(&other),
4145 "both groups draw their own tab bar while nothing is zoomed"
4146 );
4147
4148 tab_bars.borrow_mut().clear();
4149 let group = group_of(&area, 0, cx);
4150 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
4151 cx.run_until_parked();
4152
4153 assert!(
4154 tab_bars.borrow().contains(&zoomed),
4155 "a zoomed group is rendered whole: its own tab bar is still drawn, \
4156 which is exactly what the bare panel does not carry"
4157 );
4158 assert!(
4159 !tab_bars.borrow().contains(&other),
4160 "and it is the only thing on screen"
4161 );
4162 }
4163
4164 #[gpui::test]
4170 fn clearing_the_zoom_from_outside_puts_the_groups_own_flag_back(cx: &mut TestAppContext) {
4171 let log = log_of();
4172 let (area, _alpha, cx) = two_groups(&log, cx);
4173 cx.run_until_parked();
4174 drain(&log);
4175
4176 let node = child_node(&area, 0, cx);
4177 let group = group_of(&area, 0, cx);
4178 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
4179 cx.run_until_parked();
4180 assert_eq!(cx.read(|cx| area.read(cx).zoomed_group()), Some(node));
4181 assert!(cx.read(|cx| group.read(cx).is_zoomed()));
4182 assert_eq!(
4183 zoom_signals(&log),
4184 vec![("Alpha", PanelSignal::Zoomed(true))]
4185 );
4186
4187 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_out(window, cx)));
4188 cx.run_until_parked();
4189
4190 assert!(!cx.read(|cx| area.read(cx).is_zoomed()));
4191 assert!(
4192 !cx.read(|cx| group.read(cx).is_zoomed()),
4193 "a group left flagged zoomed would stay locked and refuse every drop"
4194 );
4195 assert!(
4196 cx.read(|cx| group.read(cx).context(cx).is_droppable()),
4197 "and the lock the zoom imposed is lifted with it"
4198 );
4199 assert_eq!(
4200 zoom_signals(&log),
4201 vec![("Alpha", PanelSignal::Zoomed(false))],
4202 "the panel hears the zoom end too, not just the group"
4203 );
4204 }
4205
4206 #[gpui::test]
4209 fn a_group_that_refuses_to_zoom_leaves_the_area_unzoomed(cx: &mut TestAppContext) {
4210 let log = log_of();
4211 let (area, alpha, cx) = two_groups(&log, cx);
4212 cx.run_until_parked();
4213 cx.update(|_, cx| alpha.update(cx, |panel, cx| panel.set_zoomable(false, cx)));
4214
4215 let node = child_node(&area, 0, cx);
4216 let group = group_of(&area, 0, cx);
4217 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_in(node, window, cx)));
4218 cx.run_until_parked();
4219
4220 assert!(!cx.read(|cx| group.read(cx).is_zoomed()));
4221 assert!(
4222 !cx.read(|cx| area.read(cx).is_zoomed()),
4223 "the area must not fill itself with a group that never zoomed"
4224 );
4225 }
4226}