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 Some(tree) = self.tree_mut(region) else {
627 return;
628 };
629 let result = tree.remove_panel(panel);
630 self.commit(result, window, cx);
631 }
632}
633
634impl DockArea {
644 pub fn set_zoomed_in(&mut self, node: NodeId, window: &mut Window, cx: &mut Context<Self>) {
650 self.set_zoom(Some(Zoomed::Group(node)), window, cx);
651 }
652
653 pub fn set_zoomed_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
659 self.set_zoom(None, window, cx);
660 }
661
662 pub fn is_zoomed(&self) -> bool {
663 self.zoomed.is_some()
664 }
665
666 pub fn zoomed_group(&self) -> Option<NodeId> {
668 match self.zoomed {
669 Some(Zoomed::Group(node)) => Some(node),
670 _ => None,
671 }
672 }
673
674 fn set_zoom(&mut self, zoomed: Option<Zoomed>, window: &mut Window, cx: &mut Context<Self>) {
682 if self.zoomed == zoomed {
683 return;
684 }
685
686 if let Some(previous) = self.zoomed {
687 self.drive_zoom(previous, false, window, cx);
688 }
689 let accepted = match zoomed {
690 Some(next) => self.drive_zoom(next, true, window, cx).then_some(next),
691 None => None,
692 };
693 self.zoomed = accepted;
694 cx.notify();
695 }
696
697 fn drive_zoom(
703 &mut self,
704 zoomed: Zoomed,
705 zoom_in: bool,
706 window: &mut Window,
707 cx: &mut Context<Self>,
708 ) -> bool {
709 match zoomed {
710 Zoomed::Group(node) => {
711 let Some(group) = self.groups.get(&node).map(|cached| cached.entity.clone()) else {
712 return false;
713 };
714 group.update(cx, |group, cx| {
715 group.set_zoomed(zoom_in, window, cx);
716 group.is_zoomed() == zoom_in
717 })
718 }
719 }
720 }
721
722 fn zoomed_view(&self) -> Option<AnyView> {
727 match self.zoomed? {
728 Zoomed::Group(node) => Some(self.groups.get(&node)?.entity.clone().into()),
729 }
730 }
731}
732
733impl DockArea {
735 pub fn load(
740 &mut self,
741 state: DockAreaState,
742 window: &mut Window,
743 cx: &mut Context<Self>,
744 ) -> Result<()> {
745 self.version = state.version;
746 self.zoomed = None;
747 self.groups.clear();
751 self.splits.clear();
752 self.docks.clear();
753 let dock_area = self.this.clone();
758 let renderer = self.renderer.clone();
759 let mut built = Vec::new();
760 self.center = {
761 let mut builder = RegistryPanelBuilder {
762 dock_area: dock_area.clone(),
763 renderer: renderer.clone(),
764 built: &mut built,
765 window,
766 cx,
767 };
768 PaneTree::from_state(&state.center, RootKind::Split, &mut builder)
769 };
770
771 for dock_state in [state.left_dock, state.right_dock, state.bottom_dock]
772 .into_iter()
773 .flatten()
774 {
775 let tree = {
776 let mut builder = RegistryPanelBuilder {
777 dock_area: dock_area.clone(),
778 renderer: renderer.clone(),
779 built: &mut built,
780 window,
781 cx,
782 };
783 PaneTree::from_state(dock_state.panel(), RootKind::Any, &mut builder)
784 };
785 let mut dock = Dock::new(dock_state.size());
786 dock.set_open(dock_state.open());
787 self.docks
788 .insert(dock_state.placement(), DockRegion { tree, dock });
789 }
790
791 self.panels.extend(built);
792 self.reconcile(window, cx);
793 cx.emit(DockEvent::LayoutChanged);
794 Ok(())
795 }
796
797 pub fn dump(&self, cx: &App) -> DockAreaState {
818 let source = LivePanels::new(&self.panels, cx);
819
820 DockAreaState {
821 version: self.version,
822 center: self.resolved_tree(&self.center, cx).to_state(&source),
823 left_dock: self.dump_dock(DockPlacement::Left, &source, cx),
824 right_dock: self.dump_dock(DockPlacement::Right, &source, cx),
825 bottom_dock: self.dump_dock(DockPlacement::Bottom, &source, cx),
826 }
827 }
828
829 fn dump_dock(
830 &self,
831 placement: DockPlacement,
832 source: &LivePanels<'_>,
833 cx: &App,
834 ) -> Option<DockState> {
835 let pane = self.docks.get(&placement)?;
836 Some(DockState::new(
837 self.resolved_tree(&pane.tree, cx).to_state(source),
838 placement,
839 pane.dock.size(),
840 pane.dock.is_open(),
841 ))
842 }
843
844 fn resolved_tree(&self, tree: &PaneTree, cx: &App) -> PaneTree {
845 let mut tree = tree.clone();
846 self.resolve_sizes(tree.root_mut(), cx);
847 tree
848 }
849
850 fn resolve_sizes(&self, node: &mut PaneNode, cx: &App) {
851 let measured = self
852 .splits
853 .get(&node.id())
854 .map(|cached| cached.entity.read(cx).sizes().clone())
855 .unwrap_or_default();
856
857 let NodeKind::Split {
858 children, sizes, ..
859 } = node.kind_mut()
860 else {
861 return;
862 };
863
864 for (ix, size) in sizes.iter_mut().enumerate() {
865 let on_screen = measured.get(ix).copied().filter(|size| *size > px(0.));
870 let stored = (*size).filter(|size| *size > px(0.));
871 *size = Some(on_screen.or(stored).unwrap_or(PANEL_MIN_SIZE));
872 }
873
874 for child in children.iter_mut() {
875 self.resolve_sizes(child, cx);
876 }
877 }
878}
879
880impl DockArea {
882 fn commit(&mut self, result: EditResult, window: &mut Window, cx: &mut Context<Self>) {
891 self.commit_changed(result.changed(), window, cx);
892 }
893
894 fn commit_changed(&mut self, changed: bool, window: &mut Window, cx: &mut Context<Self>) {
896 if !changed {
897 return;
898 }
899
900 self.reconcile(window, cx);
901 cx.emit(DockEvent::LayoutChanged);
902 }
903
904 fn reconcile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
911 let mut plans = Vec::new();
914 plan_tree(&self.center, false, self.locked, &mut plans);
915 for pane in self.docks.values() {
916 plan_tree(&pane.tree, !pane.dock.is_open(), self.locked, &mut plans);
917 }
918
919 let mut live_nodes = HashSet::with_capacity(plans.len());
923 let mut live_panels: HashSet<PanelId> = HashSet::new();
924
925 for plan in plans {
926 live_nodes.insert(plan.node());
927 match plan {
928 ContainerPlan::Split {
929 node,
930 axis,
931 children,
932 sizes,
933 } => {
934 let state = self.split_entity(node, cx);
935 let (previous, adopted) = self
936 .splits
937 .get(&node)
938 .map(|cached| (cached.children.clone(), cached.sizes.clone()))
939 .unwrap_or_default();
940 if previous != children || adopted != sizes {
948 state.update(cx, |state, cx| {
949 sync_split_panels(state, &previous, &children, &sizes, cx);
950 state.sync_panels_count(axis, children.len(), cx);
951 state.adopt_sizes(&scale_sizes_to(state.container_size(), &sizes), cx);
967 });
968 }
969 if let Some(cached) = self.splits.get_mut(&node) {
970 cached.children = children;
971 cached.sizes = sizes;
972 }
973 }
974 ContainerPlan::Group {
975 node,
976 panels,
977 active_ix,
978 constraints,
979 } => {
980 live_panels.extend(panels.iter().copied());
981 let views = self.views_of(&panels);
982 let group = self.group_entity(node, window, cx);
983 group.update(cx, |group, cx| {
984 group.set_constraints(constraints, window, cx);
988 group.sync_from_tree(views, active_ix, window, cx);
989 });
990 }
991 }
992 }
993
994 self.groups.retain(|node, _| live_nodes.contains(node));
995 self.splits.retain(|node, _| live_nodes.contains(node));
996
997 let departed: Vec<Arc<dyn PanelView>> = self
998 .panels
999 .iter()
1000 .filter(|(panel, _)| !live_panels.contains(panel))
1001 .map(|(_, view)| view.clone())
1002 .collect();
1003 self.panels.retain(|panel, _| live_panels.contains(panel));
1004
1005 let zoom_survives = match self.zoomed {
1015 Some(Zoomed::Group(node)) => self.groups.contains_key(&node),
1016 None => true,
1017 };
1018 if !zoom_survives {
1019 self.set_zoom(None, window, cx);
1020 }
1021
1022 cx.notify();
1023 for view in departed {
1026 view.on_removed(window, cx);
1027 }
1028 }
1029
1030 fn views_of(&self, panels: &[PanelId]) -> Vec<Arc<dyn PanelView>> {
1032 debug_assert!(
1033 panels.iter().all(|panel| self.panels.contains_key(panel)),
1034 "every panel in a tree must have a live view; a missing one would \
1035 silently shift the group's active index"
1036 );
1037 panels
1038 .iter()
1039 .filter_map(|panel| self.panels.get(panel).cloned())
1040 .collect()
1041 }
1042
1043 fn group_entity(
1044 &mut self,
1045 node: NodeId,
1046 window: &mut Window,
1047 cx: &mut Context<Self>,
1048 ) -> Entity<TabGroup> {
1049 if let Some(cached) = self.groups.get(&node) {
1050 return cached.entity.clone();
1051 }
1052
1053 let renderer = self.renderer.tab_group_renderer();
1054 let entity = cx.new(|cx| TabGroup::new(node, window, cx).with_renderer(renderer));
1055 let subscription = cx.subscribe_in(&entity, window, Self::on_tab_group_event);
1056 self.groups.insert(
1057 node,
1058 Cached {
1059 entity: entity.clone(),
1060 _subscription: subscription,
1061 },
1062 );
1063 entity
1064 }
1065
1066 fn split_entity(&mut self, node: NodeId, cx: &mut Context<Self>) -> Entity<ResizableState> {
1067 if let Some(cached) = self.splits.get(&node) {
1068 return cached.entity.clone();
1069 }
1070
1071 let entity = cx.new(|_| ResizableState::default());
1072 let subscription =
1082 cx.subscribe(&entity, move |this, state, _: &ResizablePanelEvent, cx| {
1083 let sizes: Vec<Option<Pixels>> = state
1084 .read(cx)
1085 .sizes()
1086 .iter()
1087 .map(|size| Some(*size))
1088 .collect();
1089 let Some(region) = this.placement_of_node(node) else {
1090 return;
1091 };
1092 let Some(tree) = this.tree_mut(region) else {
1093 return;
1094 };
1095 if tree.set_sizes(node, sizes.clone()).changed() {
1096 cx.emit(DockEvent::LayoutChanged);
1097 }
1098 if let Some(cached) = this.splits.get_mut(&node) {
1101 cached.sizes = sizes;
1102 }
1103 });
1104 self.splits.insert(
1105 node,
1106 CachedSplit {
1107 entity: entity.clone(),
1108 children: Vec::new(),
1109 sizes: Vec::new(),
1110 _subscription: subscription,
1111 },
1112 );
1113 entity
1114 }
1115}
1116
1117impl DockArea {
1119 fn on_tab_group_event(
1120 &mut self,
1121 group: &Entity<TabGroup>,
1122 event: &TabGroupEvent,
1123 window: &mut Window,
1124 cx: &mut Context<Self>,
1125 ) {
1126 match event {
1127 TabGroupEvent::Drop { panel, target, .. } => {
1128 self.move_panel(*panel, *target, window, cx)
1129 }
1130 TabGroupEvent::DragDrop { item, target } => cx.emit(DockEvent::DragDrop {
1131 item: item.clone(),
1132 target: *target,
1133 }),
1134 TabGroupEvent::ClosePanel { panel } => self.remove_panel_id(*panel, window, cx),
1135 TabGroupEvent::ActiveChanged { ix } => {
1136 let node = group.read(cx).node();
1137 let Some(region) = self.placement_of_node(node) else {
1138 return;
1139 };
1140 let Some(tree) = self.tree_mut(region) else {
1141 return;
1142 };
1143 let result = tree.set_active(node, *ix);
1144 self.commit(result, window, cx);
1145 }
1146 TabGroupEvent::ZoomIn => {
1147 let node = group.read(cx).node();
1148 self.set_zoom(Some(Zoomed::Group(node)), window, cx);
1149 }
1150 TabGroupEvent::ZoomOut => {
1155 let node = group.read(cx).node();
1156 if self.zoomed == Some(Zoomed::Group(node)) {
1157 self.set_zoom(None, window, cx);
1158 }
1159 }
1160 }
1161 }
1162}
1163
1164impl DockArea {
1166 fn adopt_measured_sizes(&mut self, placement: DockPlacement, cx: &App) {
1173 let measured: HashMap<NodeId, Vec<Pixels>> = self
1174 .splits
1175 .iter()
1176 .filter(|(_, cached)| cached.entity.read(cx).container_size() > Pixels::ZERO)
1182 .map(|(node, cached)| (*node, cached.entity.read(cx).sizes().clone()))
1183 .collect();
1184
1185 if let Some(tree) = self.tree_mut(placement) {
1186 tree.adopt_measured_sizes(&measured);
1187 }
1188 }
1189
1190 fn tree_mut(&mut self, placement: DockPlacement) -> Option<&mut PaneTree> {
1191 match placement {
1192 DockPlacement::Center => Some(&mut self.center),
1193 _ => self.docks.get_mut(&placement).map(|pane| &mut pane.tree),
1194 }
1195 }
1196
1197 fn placement_of_node(&self, node: NodeId) -> Option<DockPlacement> {
1200 if self.center.find_node(node).is_some() {
1201 return Some(DockPlacement::Center);
1202 }
1203 self.docks
1204 .iter()
1205 .find(|(_, pane)| pane.tree.find_node(node).is_some())
1206 .map(|(placement, _)| *placement)
1207 }
1208
1209 fn placement_of_panel(&self, panel: PanelId) -> Option<DockPlacement> {
1210 if self.center.find_panel_node(panel).is_some() {
1211 return Some(DockPlacement::Center);
1212 }
1213 self.docks
1214 .iter()
1215 .find(|(_, pane)| pane.tree.find_panel_node(panel).is_some())
1216 .map(|(placement, _)| *placement)
1217 }
1218
1219 fn resize_dock(
1227 &mut self,
1228 placement: DockPlacement,
1229 pointer: Point<Pixels>,
1230 window: &mut Window,
1231 cx: &mut Context<Self>,
1232 ) {
1233 let opposite = match placement {
1234 DockPlacement::Left => self.dock_size(DockPlacement::Right),
1235 DockPlacement::Right => self.dock_size(DockPlacement::Left),
1236 _ => None,
1237 };
1238 let sizing = DockSizing::new(placement)
1239 .with_area_bounds(self.bounds)
1240 .with_opposite_dock_size(opposite.unwrap_or(px(0.)));
1241 let size = sizing
1242 .size_from_pointer(pointer)
1243 .min(sizing.clamp(Pixels::MAX));
1244
1245 let Some(pane) = self.docks.get_mut(&placement) else {
1246 return;
1247 };
1248 let was_open = pane.dock.is_open();
1249 if placement == DockPlacement::Bottom && pane.dock.is_collapsible() && size < PANEL_MIN_SIZE
1250 {
1251 pane.dock.set_open(size > CLOSED_BOTTOM_STRIP);
1252 pane.dock.set_live_size(Some(size.max(CLOSED_BOTTOM_STRIP)));
1253 } else {
1254 pane.dock.set_open(true);
1255 pane.dock.set_live_size(None);
1256 pane.dock.set_size(size);
1257 }
1258 if pane.dock.is_open() != was_open {
1259 self.reconcile(window, cx);
1260 }
1261 cx.notify();
1262 }
1263
1264 fn end_dock_resize(
1267 &mut self,
1268 placement: DockPlacement,
1269 window: &mut Window,
1270 cx: &mut Context<Self>,
1271 ) {
1272 let Some(pane) = self.docks.get_mut(&placement) else {
1273 return;
1274 };
1275 let Some(size) = pane.dock.live_size() else {
1276 return;
1277 };
1278 pane.dock.set_live_size(None);
1279
1280 let open = size >= (CLOSED_BOTTOM_STRIP + PANEL_MIN_SIZE) / 2.;
1281 if open {
1282 pane.dock.set_size(PANEL_MIN_SIZE);
1283 }
1284 if pane.dock.is_open() != open {
1285 pane.dock.set_open(open);
1286 self.reconcile(window, cx);
1287 }
1288 cx.notify();
1289 }
1290}
1291
1292impl DockArea {
1294 fn render_node(&self, node: &PaneNode, window: &mut Window, cx: &mut App) -> AnyElement {
1296 match node.kind() {
1297 PaneRef::Split {
1298 axis,
1299 children,
1300 sizes,
1301 } => {
1302 let group = match axis {
1303 Axis::Horizontal => h_resizable(("dock-split", node.id().as_u64())),
1304 Axis::Vertical => v_resizable(("dock-split", node.id().as_u64())),
1305 };
1306 let shown: Vec<bool> = children
1310 .iter()
1311 .map(|child| self.is_node_visible(child, cx))
1312 .collect();
1313 let grows = shown.iter().rposition(|shown| *shown);
1319 let panels: Vec<_> = children
1320 .iter()
1321 .zip(sizes.iter())
1322 .enumerate()
1323 .map(|(ix, (child, size))| {
1324 resizable_panel()
1325 .visible(shown[ix])
1326 .child(self.render_node(child, window, cx))
1327 .when_some(*size, |panel, size| {
1339 panel
1340 .size(size)
1341 .when(Some(ix) != grows, |panel| panel.flex_none())
1342 })
1343 })
1344 .collect();
1345
1346 let group = group
1347 .when_some(self.splits.get(&node.id()), |group, cached| {
1348 group.with_state(&cached.entity)
1349 })
1350 .with_handle_appearance({
1351 let renderer = self.renderer.clone();
1352 Rc::new(move |handle, window, cx| {
1353 renderer.render_split_handle(handle, window, cx)
1354 })
1355 })
1356 .children(panels);
1357
1358 self.renderer
1359 .split_frame(node.id(), axis, window, cx)
1360 .size_full()
1368 .flex_1()
1369 .min_h(px(0.))
1370 .overflow_hidden()
1371 .child(group)
1372 .into_any_element()
1373 }
1374 PaneRef::Tabs { .. } => match self.groups.get(&node.id()) {
1375 Some(cached) => cached.entity.clone().into_any_element(),
1376 None => Empty.into_any_element(),
1377 },
1378 }
1379 }
1380
1381 fn is_node_visible(&self, node: &PaneNode, cx: &App) -> bool {
1387 let panels = LivePanels::new(&self.panels, cx);
1388 match node.kind() {
1389 PaneRef::Split { children, .. } => {
1390 children.iter().any(|child| self.is_node_visible(child, cx))
1391 }
1392 PaneRef::Tabs { panels: ids, .. } => ids.iter().any(|panel| panels.is_visible(*panel)),
1393 }
1394 }
1395
1396 fn render_dock(
1397 &self,
1398 placement: DockPlacement,
1399 window: &mut Window,
1400 cx: &mut App,
1401 ) -> Option<AnyElement> {
1402 let pane = self.docks.get(&placement)?;
1403 let dock = self.dock_context(placement, &pane.dock);
1404
1405 let size = dock_extent(&dock);
1410 if size <= px(0.) {
1411 return Some(div().into_any_element());
1412 }
1413
1414 let content = self.render_node(pane.tree.root(), window, cx);
1415 let chrome = self.renderer.render_dock(&dock, content, window, cx);
1424 Some(dock_frame(&dock, size).child(chrome).into_any_element())
1425 }
1426
1427 fn dock_context(&self, placement: DockPlacement, dock: &Dock) -> DockContext {
1428 let area = self.this.clone();
1429
1430 DockContext {
1431 placement,
1432 size: dock.live_size().unwrap_or(dock.size()),
1433 open: dock.is_open(),
1434 collapsible: dock.is_collapsible(),
1435 on_toggle: {
1436 let area = area.clone();
1437 Rc::new(move |window, cx| {
1438 _ = area.update(cx, |area, cx| area.toggle_dock(placement, window, cx));
1439 })
1440 },
1441 on_resize: {
1442 let area = area.clone();
1443 Rc::new(move |pointer, window, cx| {
1444 _ = area.update(cx, |area, cx| {
1445 area.resize_dock(placement, pointer, window, cx)
1446 });
1447 })
1448 },
1449 on_resize_end: Rc::new(move |window, cx| {
1450 _ = area.update(cx, |area, cx| area.end_dock_resize(placement, window, cx));
1451 }),
1452 }
1453 }
1454}
1455
1456impl EventEmitter<DockEvent> for DockArea {}
1457
1458impl Focusable for DockArea {
1459 fn focus_handle(&self, _: &App) -> FocusHandle {
1460 self.focus_handle.clone()
1461 }
1462}
1463
1464impl Render for DockArea {
1465 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1466 let area = cx.entity();
1467 let renderer = self.renderer.clone();
1468
1469 renderer
1470 .frame(window, cx)
1471 .test_support()
1472 .relative()
1479 .size_full()
1480 .overflow_hidden()
1481 .flex()
1482 .flex_row()
1483 .on_prepaint(move |bounds, _, cx| {
1484 area.update(cx, |area, _| area.bounds = bounds);
1485 })
1486 .track_focus(&self.focus_handle)
1487 .map(|frame| match self.zoomed_view() {
1488 Some(view) => frame.child(view),
1489 None => frame
1490 .when_some(
1491 self.render_dock(DockPlacement::Left, window, cx),
1492 ParentElement::child,
1493 )
1494 .child(
1495 renderer
1496 .center_frame(window, cx)
1497 .flex()
1502 .flex_1()
1503 .flex_col()
1504 .overflow_hidden()
1505 .child(self.render_node(self.center.root(), window, cx))
1506 .when_some(
1507 self.render_dock(DockPlacement::Bottom, window, cx),
1508 ParentElement::child,
1509 ),
1510 )
1511 .when_some(
1512 self.render_dock(DockPlacement::Right, window, cx),
1513 ParentElement::child,
1514 ),
1515 })
1516 }
1517}
1518
1519enum ContainerPlan {
1521 Split {
1522 node: NodeId,
1523 axis: Axis,
1524 children: Vec<NodeId>,
1525 sizes: Vec<Option<Pixels>>,
1526 },
1527 Group {
1528 node: NodeId,
1529 panels: Vec<PanelId>,
1530 active_ix: usize,
1531 constraints: TabGroupConstraints,
1532 },
1533}
1534
1535impl ContainerPlan {
1536 fn node(&self) -> NodeId {
1537 match self {
1538 Self::Split { node, .. } | Self::Group { node, .. } => *node,
1539 }
1540 }
1541}
1542
1543fn scale_sizes_to(container: Pixels, sizes: &[Option<Pixels>]) -> Vec<Option<Pixels>> {
1550 let total: f32 = sizes.iter().flatten().map(|size| size.as_f32()).sum();
1551 if container <= px(0.) || total <= 0. || sizes.iter().any(Option::is_none) {
1552 return sizes.to_vec();
1553 }
1554
1555 let scale = container.as_f32() / total;
1556 sizes
1557 .iter()
1558 .map(|size| size.map(|size| px(size.as_f32() * scale)))
1559 .collect()
1560}
1561
1562fn sync_split_panels(
1572 state: &mut ResizableState,
1573 previous: &[NodeId],
1574 next: &[NodeId],
1575 sizes: &[Option<Pixels>],
1576 cx: &mut Context<ResizableState>,
1577) {
1578 let mut current = previous.to_vec();
1579
1580 for ix in (0..current.len()).rev() {
1582 if !next.contains(¤t[ix]) {
1583 if ix < state.sizes().len() {
1584 state.remove_panel(ix, cx);
1585 }
1586 current.remove(ix);
1587 }
1588 }
1589
1590 for (ix, node) in next.iter().enumerate() {
1591 if current.get(ix) == Some(node) {
1592 continue;
1593 }
1594 let at = ix.min(state.sizes().len());
1595 state.insert_panel(sizes.get(ix).copied().flatten(), Some(at), cx);
1600 current.insert(at.min(current.len()), *node);
1601 }
1602
1603 debug_assert_eq!(
1604 current, next,
1605 "the split's panel list must end up mirroring its children exactly; \
1606 a reordering edit would need its own case here"
1607 );
1608}
1609
1610fn plan_tree(tree: &PaneTree, collapsed: bool, locked: bool, out: &mut Vec<ContainerPlan>) {
1611 plan_node(tree.root(), true, collapsed, locked, out);
1613}
1614
1615fn plan_node(
1616 node: &PaneNode,
1617 alone: bool,
1618 collapsed: bool,
1619 locked: bool,
1620 out: &mut Vec<ContainerPlan>,
1621) {
1622 match node.kind() {
1623 PaneRef::Split {
1624 axis,
1625 children,
1626 sizes,
1627 } => {
1628 out.push(ContainerPlan::Split {
1629 node: node.id(),
1630 axis,
1631 children: children.iter().map(PaneNode::id).collect(),
1632 sizes: sizes.to_vec(),
1633 });
1634 let children_alone = children.len() <= 1;
1635 for child in children {
1636 plan_node(child, children_alone, collapsed, locked, out);
1637 }
1638 }
1639 PaneRef::Tabs { panels, active_ix } => out.push(ContainerPlan::Group {
1640 node: node.id(),
1641 panels: panels.to_vec(),
1642 active_ix,
1643 constraints: TabGroupConstraints::in_split(alone)
1644 .dock_locked(locked)
1645 .collapsed(collapsed),
1646 }),
1647 }
1648}
1649
1650fn first_tab_group(node: &PaneNode) -> Option<NodeId> {
1651 match node.kind() {
1652 PaneRef::Tabs { .. } => Some(node.id()),
1653 PaneRef::Split { children, .. } => children.iter().find_map(first_tab_group),
1654 }
1655}
1656
1657fn target_node(target: &InsertTarget) -> NodeId {
1658 match target {
1659 InsertTarget::Tabs { node, .. } | InsertTarget::Split { node, .. } => *node,
1660 }
1661}
1662
1663struct RegistryPanelBuilder<'a, 'w, 'c> {
1665 dock_area: WeakEntity<DockArea>,
1666 renderer: Rc<dyn DockAreaRenderer>,
1667 built: &'a mut Vec<(PanelId, Arc<dyn PanelView>)>,
1668 window: &'w mut Window,
1669 cx: &'c mut App,
1670}
1671
1672impl PanelBuilder for RegistryPanelBuilder<'_, '_, '_> {
1673 fn build(&mut self, state: &PanelState, info: &PanelInfo) -> PanelId {
1674 let context = PanelBuildContext::new(self.dock_area.clone(), state, info);
1675 let view =
1676 match PanelRegistry::build_panel(&state.panel_name, context, self.window, self.cx) {
1677 Some(view) => view,
1678 None => self
1679 .renderer
1680 .build_placeholder(state, self.window, self.cx)
1681 .unwrap_or_else(|| {
1682 Arc::new(self.cx.new(|cx| PlaceholderPanel::new(state.clone(), cx)))
1683 as Arc<dyn PanelView>
1684 }),
1685 };
1686
1687 let id = view.panel_id(self.cx);
1688 self.built.push((id, view));
1689 id
1690 }
1691}
1692
1693struct PlaceholderPanel {
1700 state: PanelState,
1701 focus_handle: FocusHandle,
1702}
1703
1704impl PlaceholderPanel {
1705 fn new(state: PanelState, cx: &mut Context<Self>) -> Self {
1706 Self {
1707 state,
1708 focus_handle: cx.focus_handle(),
1709 }
1710 }
1711}
1712
1713impl Panel for PlaceholderPanel {
1714 fn panel_name(&self) -> &'static str {
1715 "InvalidPanel"
1716 }
1717
1718 fn dump(&self, _: &App) -> PanelState {
1719 self.state.clone()
1720 }
1721}
1722
1723impl EventEmitter<PanelEvent> for PlaceholderPanel {}
1724
1725impl Focusable for PlaceholderPanel {
1726 fn focus_handle(&self, _: &App) -> FocusHandle {
1727 self.focus_handle.clone()
1728 }
1729}
1730
1731impl Render for PlaceholderPanel {
1732 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1733 Empty
1734 }
1735}
1736
1737type DockToggleHandler = Rc<dyn Fn(&mut Window, &mut App)>;
1738type DockResizeHandler = Rc<dyn Fn(Point<Pixels>, &mut Window, &mut App)>;
1739
1740#[derive(Clone)]
1743pub struct DockContext {
1744 placement: DockPlacement,
1745 size: Pixels,
1746 open: bool,
1747 collapsible: bool,
1748 on_toggle: DockToggleHandler,
1749 on_resize: DockResizeHandler,
1750 on_resize_end: DockToggleHandler,
1751}
1752
1753impl DockContext {
1754 pub fn placement(&self) -> DockPlacement {
1755 self.placement
1756 }
1757
1758 pub fn size(&self) -> Pixels {
1761 self.size
1762 }
1763
1764 pub fn is_open(&self) -> bool {
1765 self.open
1766 }
1767
1768 pub fn is_collapsible(&self) -> bool {
1769 self.collapsible
1770 }
1771
1772 pub fn toggle(&self, window: &mut Window, cx: &mut App) {
1773 (self.on_toggle)(window, cx);
1774 }
1775
1776 pub fn resize_to(&self, pointer: Point<Pixels>, window: &mut Window, cx: &mut App) {
1779 (self.on_resize)(pointer, window, cx);
1780 }
1781
1782 pub fn end_resize(&self, window: &mut Window, cx: &mut App) {
1785 (self.on_resize_end)(window, cx);
1786 }
1787}
1788
1789pub const CLOSED_BOTTOM_STRIP: Pixels = px(29.);
1793
1794pub fn dock_extent(dock: &DockContext) -> Pixels {
1796 match (dock.is_open(), dock.placement()) {
1797 (true, _) => dock.size(),
1798 (false, DockPlacement::Bottom) => CLOSED_BOTTOM_STRIP,
1799 (false, _) => px(0.),
1800 }
1801}
1802
1803pub fn dock_frame(dock: &DockContext, size: Pixels) -> Div {
1809 div()
1810 .flex()
1811 .flex_none()
1812 .relative()
1813 .overflow_hidden()
1814 .map(|this| match dock.placement() {
1815 DockPlacement::Left | DockPlacement::Right => this.flex_row().h_full().w(size),
1816 DockPlacement::Bottom => this.w_full().h(size),
1817 DockPlacement::Center => this,
1819 })
1820}
1821
1822#[allow(unused_variables)]
1833pub trait DockAreaRenderer: 'static {
1834 fn frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
1840 div().id("dock-area")
1841 }
1842
1843 fn split_frame(
1853 &self,
1854 node: NodeId,
1855 axis: Axis,
1856 window: &mut Window,
1857 cx: &mut App,
1858 ) -> Stateful<Div> {
1859 div().id(("dock-split-frame", node.as_u64()))
1860 }
1861
1862 fn center_frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
1866 div().id("dock-area-center")
1867 }
1868
1869 fn render_split_handle(
1876 &self,
1877 handle: &ResizeHandleContext,
1878 window: &mut Window,
1879 cx: &mut App,
1880 ) -> Option<AnyElement> {
1881 None
1882 }
1883
1884 fn render_dock(
1893 &self,
1894 dock: &DockContext,
1895 content: AnyElement,
1896 window: &mut Window,
1897 cx: &mut App,
1898 ) -> AnyElement {
1899 content
1900 }
1901
1902 fn build_placeholder(
1917 &self,
1918 state: &PanelState,
1919 window: &mut Window,
1920 cx: &mut App,
1921 ) -> Option<Arc<dyn PanelView>> {
1922 None
1923 }
1924
1925 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer>;
1926}
1927
1928struct BareDockArea;
1930
1931impl DockAreaRenderer for BareDockArea {
1932 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
1933 Rc::new(BareTabGroup)
1934 }
1935}
1936
1937#[cfg(test)]
1938impl DockArea {
1939 pub(crate) fn container_entity_ids(&self) -> Vec<(NodeId, gpui::EntityId)> {
1946 let mut ids: Vec<(NodeId, gpui::EntityId)> = self
1947 .groups
1948 .iter()
1949 .map(|(node, cached)| (*node, cached.entity.entity_id()))
1950 .chain(
1951 self.splits
1952 .iter()
1953 .map(|(node, cached)| (*node, cached.entity.entity_id())),
1954 )
1955 .collect();
1956 ids.sort();
1957 ids
1958 }
1959}
1960
1961#[cfg(test)]
1962mod tests {
1963 use gpui::{TestAppContext, VisualTestContext};
1964
1965 use std::{
1966 cell::{Cell, RefCell},
1967 rc::Rc,
1968 };
1969
1970 use super::*;
1971 use crate::dock::TabGroupContext;
1972 use crate::dock::test_support::{Log, PanelSignal, TestPanel, drain, drain_active, log_of};
1973
1974 #[test]
1977 fn slot_sizes_are_re_expressed_as_shares_of_the_container() {
1978 let scaled = scale_sizes_to(px(800.), &[Some(px(300.)), Some(px(100.))]);
1979
1980 assert_eq!(scaled, vec![Some(px(600.)), Some(px(200.))]);
1981 }
1982
1983 #[test]
1986 fn an_unconstrained_slot_leaves_every_size_alone() {
1987 let sizes = [Some(px(300.)), None];
1988
1989 assert_eq!(scale_sizes_to(px(800.), &sizes), sizes.to_vec());
1990 }
1991
1992 #[test]
1995 fn an_unusable_container_or_total_leaves_every_size_alone() {
1996 let sizes = [Some(px(300.)), Some(px(100.))];
1997 assert_eq!(scale_sizes_to(px(0.), &sizes), sizes.to_vec());
1998
1999 let zeroed = [Some(px(0.)), Some(px(0.))];
2000 assert_eq!(scale_sizes_to(px(800.), &zeroed), zeroed.to_vec());
2001 }
2002
2003 fn setup(cx: &mut TestAppContext) -> (Entity<DockArea>, &mut VisualTestContext) {
2004 cx.update(|cx| {
2005 let _ = crate::Theme::global_mut(cx);
2006 });
2007 cx.add_window_view(|window, cx| DockArea::new("test-dock", None, window, cx))
2008 }
2009
2010 #[gpui::test]
2011 fn dock_size_change_emits_one_layout_event(cx: &mut TestAppContext) {
2012 let (area, cx) = setup(cx);
2013 cx.update(|window, cx| {
2014 area.update(cx, |area, cx| {
2015 area.set_dock(
2016 DockPlacement::Left,
2017 DockLayout::tabs().panel(TestPanel::new("Left", cx)),
2018 window,
2019 cx,
2020 );
2021 });
2022 });
2023
2024 let events = Rc::new(Cell::new(0));
2025 let observed = events.clone();
2026 let _subscription = cx.update(|window, cx| {
2027 window.subscribe(&area, cx, move |_, event: &DockEvent, _, _| {
2028 if matches!(event, DockEvent::LayoutChanged) {
2029 observed.set(observed.get() + 1);
2030 }
2031 })
2032 });
2033
2034 cx.update(|window, cx| {
2035 area.update(cx, |area, cx| {
2036 area.set_dock_size(DockPlacement::Left, px(320.), window, cx);
2037 area.set_dock_size(DockPlacement::Left, px(320.), window, cx);
2038 });
2039 });
2040 assert_eq!(
2041 events.get(),
2042 1,
2043 "only an effective size change is persisted"
2044 );
2045 }
2046
2047 fn two_groups<'a>(
2049 log: &Log,
2050 cx: &'a mut TestAppContext,
2051 ) -> (
2052 Entity<DockArea>,
2053 Entity<TestPanel>,
2054 &'a mut VisualTestContext,
2055 ) {
2056 let (area, cx) = setup(cx);
2057 let log = log.clone();
2058 let alpha = cx.update(|window, cx| {
2059 let alpha = TestPanel::logging("Alpha", &log, cx);
2060 let beta = TestPanel::logging("Beta", &log, cx);
2061 area.update(cx, |area, cx| {
2062 area.set_center(
2063 DockLayout::h_split()
2064 .child(DockLayout::tabs().panel(alpha.clone()), None)
2065 .child(DockLayout::tabs().panel(beta), None),
2066 window,
2067 cx,
2068 );
2069 });
2070 alpha
2071 });
2072 (area, alpha, cx)
2073 }
2074
2075 fn child_node(area: &Entity<DockArea>, ix: usize, cx: &mut VisualTestContext) -> NodeId {
2077 cx.read(|cx| {
2078 let PaneRef::Split { children, .. } = area
2079 .read(cx)
2080 .layout(DockPlacement::Center)
2081 .unwrap()
2082 .root()
2083 .kind()
2084 else {
2085 panic!("the center root is a split");
2086 };
2087 children[ix].id()
2088 })
2089 }
2090
2091 fn panel_id_of(panel: &Entity<TestPanel>) -> PanelId {
2092 PanelId::from(panel.entity_id())
2093 }
2094
2095 fn move_alpha_into_the_other_group(
2096 area: &Entity<DockArea>,
2097 alpha: &Entity<TestPanel>,
2098 cx: &mut VisualTestContext,
2099 ) {
2100 let target = child_node(area, 1, cx);
2101 let alpha_id = panel_id_of(alpha);
2102 cx.update(|window, cx| {
2103 area.update(cx, |area, cx| {
2104 area.move_panel(
2105 alpha_id,
2106 InsertTarget::Tabs {
2107 node: target,
2108 ix: None,
2109 activate: true,
2110 },
2111 window,
2112 cx,
2113 );
2114 });
2115 });
2116 cx.run_until_parked();
2117 }
2118
2119 fn collect_sizes(state: &PanelState, out: &mut Vec<Pixels>) {
2120 if let PanelInfo::Stack { sizes, .. } = &state.info {
2121 out.extend(sizes.iter().copied());
2122 }
2123 for child in &state.children {
2124 collect_sizes(child, out);
2125 }
2126 }
2127
2128 fn register_test_panels(cx: &mut App) {
2129 for name in ["Alpha", "Beta", "Gamma"] {
2130 crate::dock::registry::register_panel(cx, name, move |_, _, cx| {
2131 Arc::new(TestPanel::new(name, cx)) as Arc<dyn PanelView>
2132 });
2133 }
2134 }
2135
2136 fn one_group<'a>(
2142 log: &Log,
2143 names: &[&'static str],
2144 active_ix: Option<usize>,
2145 cx: &'a mut TestAppContext,
2146 ) -> (
2147 Entity<DockArea>,
2148 Vec<Entity<TestPanel>>,
2149 &'a mut VisualTestContext,
2150 ) {
2151 let (area, cx) = setup(cx);
2152 let log = log.clone();
2153 let names = names.to_vec();
2154 let panels = cx.update(|window, cx| {
2155 let panels: Vec<_> = names
2156 .iter()
2157 .map(|name| TestPanel::logging(name, &log, cx))
2158 .collect();
2159 let layout = panels
2160 .iter()
2161 .fold(DockLayout::tabs(), |layout, panel| {
2162 layout.panel(panel.clone())
2163 })
2164 .active_index(active_ix.unwrap_or(0));
2165 area.update(cx, |area, cx| area.set_center(layout, window, cx));
2166 panels
2167 });
2168 (area, panels, cx)
2169 }
2170
2171 fn group_of(
2173 area: &Entity<DockArea>,
2174 ix: usize,
2175 cx: &mut VisualTestContext,
2176 ) -> Entity<TabGroup> {
2177 let node = child_node(area, ix, cx);
2178 cx.read(|cx| area.read(cx).groups.get(&node).unwrap().entity.clone())
2179 }
2180
2181 fn move_panel_into(
2182 area: &Entity<DockArea>,
2183 panel: PanelId,
2184 node: NodeId,
2185 ix: Option<usize>,
2186 activate: bool,
2187 cx: &mut VisualTestContext,
2188 ) {
2189 cx.update(|window, cx| {
2190 area.update(cx, |area, cx| {
2191 area.move_panel(panel, InsertTarget::Tabs { node, ix, activate }, window, cx);
2192 });
2193 });
2194 cx.run_until_parked();
2195 }
2196
2197 fn is_center_empty(area: &Entity<DockArea>, cx: &mut VisualTestContext) -> bool {
2198 cx.read(|cx| area.read(cx).is_empty(DockPlacement::Center, cx))
2199 }
2200
2201 #[gpui::test]
2202 fn a_layout_installs_and_dumps_back_to_the_same_state(cx: &mut TestAppContext) {
2203 let (area, cx) = setup(cx);
2204 cx.update(|window, cx| {
2205 let alpha = TestPanel::new("Alpha", cx);
2206 let beta = TestPanel::new("Beta", cx);
2207 area.update(cx, |area, cx| {
2208 area.set_center(
2209 DockLayout::h_split()
2210 .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2211 .child(DockLayout::tabs().panel(beta), None),
2212 window,
2213 cx,
2214 );
2215 });
2216 });
2217
2218 let state = cx.read(|cx| area.read(cx).dump(cx));
2219 assert_eq!(state.center.panel_name, "StackPanel");
2220 assert_eq!(state.center.children.len(), 2);
2221 assert_eq!(state.center.children[0].children[0].panel_name, "Alpha");
2222 assert_eq!(state.center.children[1].children[0].panel_name, "Beta");
2223 }
2224
2225 #[gpui::test]
2226 fn moving_a_panel_reuses_its_entity(cx: &mut TestAppContext) {
2227 let log = log_of();
2228 let (area, alpha, cx) = two_groups(&log, cx);
2229 cx.run_until_parked();
2230 drain(&log);
2231
2232 let destination = child_node(&area, 1, cx);
2233 let destination_entity = cx.read(|cx| {
2234 area.read(cx)
2235 .groups
2236 .get(&destination)
2237 .unwrap()
2238 .entity
2239 .entity_id()
2240 });
2241
2242 move_alpha_into_the_other_group(&area, &alpha, cx);
2243
2244 assert_eq!(
2245 cx.read(|cx| area
2246 .read(cx)
2247 .groups
2248 .get(&destination)
2249 .unwrap()
2250 .entity
2251 .entity_id()),
2252 destination_entity,
2253 "the group the panel arrived in was reused, not rebuilt"
2254 );
2255
2256 let alpha_id = panel_id_of(&alpha);
2261 assert!(
2262 cx.read(|cx| area
2263 .read(cx)
2264 .layout(DockPlacement::Center)
2265 .unwrap()
2266 .find_panel_node(alpha_id))
2267 .is_some(),
2268 "the moved panel is still in the tree"
2269 );
2270
2271 let state = cx.read(|cx| area.read(cx).dump(cx));
2272 assert_eq!(
2276 state.center.children.len(),
2277 1,
2278 "the emptied group collapsed out of the split"
2279 );
2280 assert_eq!(
2281 state.center.children[0].children.len(),
2282 2,
2283 "both panels now share the surviving group"
2284 );
2285 }
2286
2287 #[gpui::test]
2288 fn a_moved_panel_is_not_told_it_was_removed(cx: &mut TestAppContext) {
2289 let log = log_of();
2290 let (area, alpha, cx) = two_groups(&log, cx);
2291 cx.run_until_parked();
2292 drain(&log);
2293
2294 move_alpha_into_the_other_group(&area, &alpha, cx);
2295
2296 assert!(
2297 !drain(&log).contains(&("Alpha", PanelSignal::Removed)),
2298 "moving a panel between groups must never deliver on_removed"
2299 );
2300 }
2301
2302 #[gpui::test]
2303 fn removing_a_panel_does_tell_it_it_was_removed(cx: &mut TestAppContext) {
2304 let log = log_of();
2308 let (area, alpha, cx) = two_groups(&log, cx);
2309 cx.run_until_parked();
2310 drain(&log);
2311
2312 cx.update(|window, cx| {
2313 area.update(cx, |area, cx| area.remove_panel(alpha.clone(), window, cx));
2314 });
2315 cx.run_until_parked();
2316
2317 assert!(
2318 drain(&log).contains(&("Alpha", PanelSignal::Removed)),
2319 "a genuine removal must deliver on_removed"
2320 );
2321 }
2322
2323 #[gpui::test]
2324 fn reconciling_an_unchanged_tree_creates_no_entities(cx: &mut TestAppContext) {
2325 let log = log_of();
2326 let (area, _alpha, cx) = two_groups(&log, cx);
2327 cx.run_until_parked();
2328 drain(&log);
2329
2330 let before = cx.read(|cx| area.read(cx).container_entity_ids());
2331 cx.update(|window, cx| area.update(cx, |area, cx| area.reconcile(window, cx)));
2332 let after = cx.read(|cx| area.read(cx).container_entity_ids());
2333
2334 assert!(!before.is_empty(), "there were containers to preserve");
2335 assert_eq!(
2336 before, after,
2337 "a steady-state pass creates and drops nothing"
2338 );
2339 cx.run_until_parked();
2340 assert_eq!(
2341 drain(&log),
2342 vec![],
2343 "and no panel was re-added or re-activated by it"
2344 );
2345 }
2346
2347 #[gpui::test]
2348 fn a_loaded_layout_round_trips_through_dump(cx: &mut TestAppContext) {
2349 let (area, cx) = setup(cx);
2350 cx.update(|_, cx| register_test_panels(cx));
2351
2352 let json = include_str!("fixtures/nested_splits.json");
2353 let state: DockAreaState = serde_json::from_str(json).unwrap();
2354
2355 cx.update(|window, cx| {
2356 area.update(cx, |area, cx| area.load(state.clone(), window, cx).unwrap())
2357 });
2358 let dumped = cx.read(|cx| area.read(cx).dump(cx));
2359
2360 cx.update(|window, cx| {
2361 area.update(cx, |area, cx| {
2362 area.load(dumped.clone(), window, cx).unwrap()
2363 })
2364 });
2365 let again = cx.read(|cx| area.read(cx).dump(cx));
2366
2367 assert_eq!(dumped, again, "load/dump must reach a fixpoint");
2368 assert_eq!(
2369 dumped.center.children.len(),
2370 3,
2371 "the fixture's nesting is flattened, as the state layer already pins"
2372 );
2373 assert_eq!(dumped.center.children[0].children[0].panel_name, "Alpha");
2374 }
2375
2376 #[gpui::test]
2377 fn a_dumped_live_layout_has_no_zero_sizes(cx: &mut TestAppContext) {
2378 let (area, cx) = setup(cx);
2379 cx.update(|window, cx| {
2380 let alpha = TestPanel::new("Alpha", cx);
2381 let beta = TestPanel::new("Beta", cx);
2382 let gamma = TestPanel::new("Gamma", cx);
2383 area.update(cx, |area, cx| {
2384 area.set_center(
2385 DockLayout::v_split()
2386 .child(DockLayout::tabs().panel(alpha), None)
2389 .child(DockLayout::tabs().panel(beta), Some(px(0.)))
2392 .child(DockLayout::tabs().panel(gamma), Some(px(240.))),
2393 window,
2394 cx,
2395 );
2396 });
2397 });
2398
2399 let state = cx.read(|cx| area.read(cx).dump(cx));
2400 let mut sizes = Vec::new();
2401 collect_sizes(&state.center, &mut sizes);
2402
2403 assert!(!sizes.is_empty(), "the layout has slots to check");
2404 assert!(
2405 sizes.iter().all(|size| *size > px(0.)),
2406 "an older build reads a persisted 0.0 back as a real zero-pixel panel: {sizes:?}"
2407 );
2408 }
2409
2410 #[gpui::test]
2417 fn a_dumped_split_writes_the_sizes_it_is_actually_drawn_at(cx: &mut TestAppContext) {
2418 let (area, cx) = setup(cx);
2419 cx.update(|window, cx| {
2420 let alpha = TestPanel::new("Alpha", cx);
2421 let beta = TestPanel::new("Beta", cx);
2422 area.update(cx, |area, cx| {
2423 area.set_center(
2424 DockLayout::h_split()
2425 .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2426 .child(DockLayout::tabs().panel(beta), Some(px(300.))),
2427 window,
2428 cx,
2429 );
2430 });
2431 });
2432 cx.run_until_parked();
2433
2434 let root = cx.read(|cx| {
2435 area.read(cx)
2436 .layout(DockPlacement::Center)
2437 .unwrap()
2438 .root()
2439 .id()
2440 });
2441 let measured = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2442 assert_ne!(
2443 measured,
2444 vec![px(300.), px(300.)],
2445 "the split has to have been rescaled by a layout pass, or this \
2446 test cannot tell the two preferences apart"
2447 );
2448
2449 let state = cx.read(|cx| area.read(cx).dump(cx));
2450 let PanelInfo::Stack { sizes, .. } = &state.center.info else {
2451 panic!("the center writes a stack");
2452 };
2453 assert_eq!(
2454 sizes, &measured,
2455 "the written sizes are the ones on screen, not the ones the tree \
2456 was built from"
2457 );
2458 }
2459
2460 #[gpui::test]
2461 fn set_split_sizes_restores_a_share_and_reports_it(cx: &mut TestAppContext) {
2462 let (area, cx) = setup(cx);
2463 cx.update(|window, cx| {
2464 let alpha = TestPanel::new("Alpha", cx);
2465 let beta = TestPanel::new("Beta", cx);
2466 area.update(cx, |area, cx| {
2467 area.set_center(
2468 DockLayout::h_split()
2469 .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2470 .child(DockLayout::tabs().panel(beta), Some(px(300.))),
2471 window,
2472 cx,
2473 );
2474 });
2475 });
2476 cx.run_until_parked();
2477
2478 let root = cx.read(|cx| {
2479 area.read(cx)
2480 .layout(DockPlacement::Center)
2481 .unwrap()
2482 .root()
2483 .id()
2484 });
2485 let events = Rc::new(Cell::new(0));
2486 let observed = events.clone();
2487 let _subscription = cx.update(|window, cx| {
2488 window.subscribe(&area, cx, move |_, event: &DockEvent, _, _| {
2489 if matches!(event, DockEvent::LayoutChanged) {
2490 observed.set(observed.get() + 1);
2491 }
2492 })
2493 });
2494
2495 cx.update(|window, cx| {
2496 area.update(cx, |area, cx| {
2497 area.set_split_sizes(root, vec![px(100.), px(300.)], window, cx);
2498 });
2499 });
2500 cx.run_until_parked();
2501 assert_eq!(events.get(), 1);
2502 let measured = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2503 let share = measured[0].as_f32() / (measured[0].as_f32() + measured[1].as_f32());
2504 assert!((share - 0.25).abs() < 0.01, "measured {measured:?}");
2505
2506 cx.update(|window, cx| {
2507 area.update(cx, |area, cx| {
2508 area.set_split_sizes(root, vec![px(10.)], window, cx);
2509 });
2510 });
2511 cx.run_until_parked();
2512 assert_eq!(events.get(), 1);
2513 let after = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2514 assert_eq!(after, measured);
2515 }
2516
2517 #[gpui::test]
2523 fn a_panel_dropped_beside_another_takes_half_the_split(cx: &mut TestAppContext) {
2524 let log = Log::default();
2525 let (area, panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2526 let group = child_node(&area, 0, cx);
2527 let beta = panel_id_of(&panels[1]);
2528
2529 cx.update(|window, cx| {
2530 area.update(cx, |area, cx| {
2531 area.move_panel(
2532 beta,
2533 InsertTarget::Split {
2534 node: group,
2535 placement: Placement::Right,
2536 size: None,
2537 },
2538 window,
2539 cx,
2540 );
2541 });
2542 });
2543 cx.run_until_parked();
2544
2545 let root = cx.read(|cx| {
2546 area.read(cx)
2547 .layout(DockPlacement::Center)
2548 .unwrap()
2549 .root()
2550 .id()
2551 });
2552 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2553
2554 assert_eq!(sizes.len(), 2, "the drop splits the center in two");
2555 let (left, right) = (sizes[0].as_f32(), sizes[1].as_f32());
2556 assert!(
2557 (left - right).abs() <= (left + right) * 0.02,
2558 "the two halves must be within 2% of each other, got {left} and {right}"
2559 );
2560 }
2561
2562 #[gpui::test]
2564 fn a_move_of_an_unowned_panel_is_ignored(cx: &mut TestAppContext) {
2565 let log = Log::default();
2566 let (area, _panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2567 let group = child_node(&area, 0, cx);
2568 let before = cx.read(|cx| area.read(cx).dump(cx));
2569
2570 move_panel_into(&area, PanelId::from_u64(9_999_999), group, None, true, cx);
2572
2573 let after = cx.read(|cx| area.read(cx).dump(cx));
2574 assert_eq!(
2575 before, after,
2576 "an unowned panel move must not touch the tree"
2577 );
2578 }
2579
2580 #[gpui::test]
2584 fn a_panel_dropped_across_the_axis_still_takes_half(cx: &mut TestAppContext) {
2585 let log = Log::default();
2586 let (area, panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2587 let group = child_node(&area, 0, cx);
2588 let beta = panel_id_of(&panels[1]);
2589
2590 cx.update(|window, cx| {
2591 area.update(cx, |area, cx| {
2592 area.move_panel(
2593 beta,
2594 InsertTarget::Split {
2595 node: group,
2596 placement: Placement::Bottom,
2597 size: None,
2598 },
2599 window,
2600 cx,
2601 );
2602 });
2603 });
2604 cx.run_until_parked();
2605
2606 let wrapper = child_node(&area, 0, cx);
2610 let sizes = cx.read(|cx| {
2611 area.read(cx).splits[&wrapper]
2612 .entity
2613 .read(cx)
2614 .sizes()
2615 .clone()
2616 });
2617 assert_eq!(sizes.len(), 2, "the drop splits the group in two");
2618 let (top, bottom) = (sizes[0].as_f32(), sizes[1].as_f32());
2619 assert!(
2620 (top - bottom).abs() <= (top + bottom) * 0.02,
2621 "the two halves must be within 2% of each other, got {top} and {bottom}"
2622 );
2623 }
2624
2625 #[gpui::test]
2628 fn a_panel_dropped_into_a_populated_split_takes_an_even_share(cx: &mut TestAppContext) {
2629 let (area, cx) = setup(cx);
2630 let panels = cx.update(|window, cx| {
2631 let alpha = TestPanel::new("Alpha", cx);
2632 let beta = TestPanel::new("Beta", cx);
2633 let gamma = TestPanel::new("Gamma", cx);
2634 area.update(cx, |area, cx| {
2635 area.set_center(
2636 DockLayout::h_split()
2637 .child(DockLayout::tabs().panel(alpha.clone()), Some(px(240.)))
2638 .child(
2639 DockLayout::tabs().panel(beta.clone()).panel(gamma.clone()),
2640 None,
2641 ),
2642 window,
2643 cx,
2644 );
2645 });
2646 vec![alpha, beta, gamma]
2647 });
2648 cx.run_until_parked();
2649
2650 let right = child_node(&area, 1, cx);
2651 let gamma = panel_id_of(&panels[2]);
2652 cx.update(|window, cx| {
2653 area.update(cx, |area, cx| {
2654 area.move_panel(
2655 gamma,
2656 InsertTarget::Split {
2657 node: right,
2658 placement: Placement::Right,
2659 size: None,
2660 },
2661 window,
2662 cx,
2663 );
2664 });
2665 });
2666 cx.run_until_parked();
2667
2668 let root = cx.read(|cx| {
2669 area.read(cx)
2670 .layout(DockPlacement::Center)
2671 .unwrap()
2672 .root()
2673 .id()
2674 });
2675 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2676 assert_eq!(sizes.len(), 3, "three slots side by side");
2677 let dropped = sizes[2].as_f32();
2678 let neighbour = sizes[1].as_f32();
2679 assert!(
2680 (dropped - neighbour).abs() <= (dropped + neighbour) * 0.02,
2681 "the dropped panel splits its neighbour evenly, got neighbour {neighbour} and dropped {dropped}"
2682 );
2683 }
2684
2685 #[gpui::test]
2689 fn a_panel_dropped_beside_a_dock_takes_half(cx: &mut TestAppContext) {
2690 for placement in [DockPlacement::Bottom, DockPlacement::Left] {
2691 let (area, cx) = setup(cx);
2692 let dropped = cx.update(|window, cx| {
2693 let resident = TestPanel::new("Resident", cx);
2694 let dropped = TestPanel::new("Dropped", cx);
2695 area.update(cx, |area, cx| {
2696 area.set_center(
2697 DockLayout::tabs().panel(TestPanel::new("Center", cx)),
2698 window,
2699 cx,
2700 );
2701 area.set_dock(
2702 placement,
2703 DockLayout::tabs().panel(resident).panel(dropped.clone()),
2704 window,
2705 cx,
2706 );
2707 area.set_dock_size(placement, px(400.), window, cx);
2708 });
2709 dropped
2710 });
2711 cx.run_until_parked();
2712
2713 let group = cx.read(|cx| {
2714 area.read(cx)
2715 .layout(placement)
2716 .unwrap()
2717 .find_panel_node(panel_id_of(&dropped))
2718 .expect("both panels start in the dock's only group")
2719 });
2720
2721 cx.update(|window, cx| {
2722 area.update(cx, |area, cx| {
2723 area.move_panel(
2724 panel_id_of(&dropped),
2725 InsertTarget::Split {
2726 node: group,
2727 placement: Placement::Bottom,
2728 size: None,
2729 },
2730 window,
2731 cx,
2732 );
2733 });
2734 });
2735 cx.run_until_parked();
2736
2737 let split = cx.read(|cx| {
2738 let tree = area.read(cx).layout(placement).unwrap();
2739 let root = tree.root();
2740 match root.kind() {
2741 PaneRef::Split { .. } => root.id(),
2742 _ => panic!("the drop must have produced a split"),
2743 }
2744 });
2745 let sizes = cx.read(|cx| area.read(cx).splits[&split].entity.read(cx).sizes().clone());
2746
2747 assert_eq!(sizes.len(), 2, "{placement:?}: the drop splits in two");
2748 let (first, second) = (sizes[0].as_f32(), sizes[1].as_f32());
2749 assert!(
2750 (first - second).abs() <= (first + second) * 0.02,
2751 "{placement:?}: expected halves, got {first} and {second}"
2752 );
2753 }
2754 }
2755
2756 #[gpui::test]
2763 fn an_explicit_slot_size_survives_the_first_layout_pass(cx: &mut TestAppContext) {
2764 let (area, cx) = setup(cx);
2765 cx.update(|window, cx| {
2766 let sidebar = TestPanel::new("Sidebar", cx);
2767 let content = TestPanel::new("Content", cx);
2768 area.update(cx, |area, cx| {
2769 area.set_center(
2770 DockLayout::h_split()
2771 .child(DockLayout::tabs().panel(sidebar), Some(px(200.)))
2772 .child(DockLayout::tabs().panel(content), None),
2773 window,
2774 cx,
2775 );
2776 });
2777 });
2778 cx.run_until_parked();
2779
2780 let root = cx.read(|cx| {
2781 area.read(cx)
2782 .layout(DockPlacement::Center)
2783 .unwrap()
2784 .root()
2785 .id()
2786 });
2787 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2788
2789 let fixed = sizes
2793 .first()
2794 .copied()
2795 .expect("the split has slots")
2796 .as_f32();
2797 assert!(
2798 (fixed - 200.).abs() <= 4.,
2799 "the fixed slot keeps its 200px instead of being rescaled by the \
2800 flexible sibling's placeholder, got {fixed}"
2801 );
2802 }
2803
2804 struct MeasuredPanel {
2807 name: &'static str,
2808 focus_handle: FocusHandle,
2809 }
2810
2811 impl MeasuredPanel {
2812 fn new(name: &'static str, cx: &mut App) -> Entity<Self> {
2813 cx.new(|cx| Self {
2814 name,
2815 focus_handle: cx.focus_handle(),
2816 })
2817 }
2818 }
2819
2820 impl Panel for MeasuredPanel {
2821 fn panel_name(&self) -> &'static str {
2822 self.name
2823 }
2824 }
2825
2826 impl EventEmitter<PanelEvent> for MeasuredPanel {}
2827
2828 impl Focusable for MeasuredPanel {
2829 fn focus_handle(&self, _: &App) -> FocusHandle {
2830 self.focus_handle.clone()
2831 }
2832 }
2833
2834 impl Render for MeasuredPanel {
2835 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2836 let name = self.name;
2837 div().size_full().debug_selector(move || name.into())
2838 }
2839 }
2840
2841 fn draw_frames(cx: &mut VisualTestContext, frames: usize) {
2842 for _ in 0..frames {
2843 cx.update(|window, cx| window.draw(cx).clear(cx));
2844 }
2845 }
2846
2847 #[gpui::test]
2854 fn switching_a_tab_leaves_an_untouched_split_where_it_was_drawn(cx: &mut TestAppContext) {
2855 let (area, cx) = setup(cx);
2856 cx.update(|window, cx| {
2857 area.update(cx, |area, cx| {
2858 area.set_center(
2859 DockLayout::tabs().panel(TestPanel::new("Center", cx)),
2860 window,
2861 cx,
2862 );
2863 area.set_dock(
2864 DockPlacement::Left,
2865 DockLayout::v_split()
2866 .child(
2867 DockLayout::tabs().panel(MeasuredPanel::new("upper-left", cx)),
2868 None,
2869 )
2870 .child(
2871 DockLayout::tabs().panel(MeasuredPanel::new("lower-left", cx)),
2872 Some(px(360.)),
2873 ),
2874 window,
2875 cx,
2876 );
2877 area.set_dock_size(DockPlacement::Left, px(350.), window, cx);
2878 area.set_dock(
2879 DockPlacement::Bottom,
2880 DockLayout::tabs()
2881 .panel(TestPanel::new("Tooltip", cx))
2882 .panel(TestPanel::new("Icon", cx)),
2883 window,
2884 cx,
2885 );
2886 area.set_dock_size(DockPlacement::Bottom, px(200.), window, cx);
2887 });
2888 });
2889 cx.run_until_parked();
2890 draw_frames(cx, 3);
2891 let before = (
2892 cx.debug_bounds("upper-left").unwrap(),
2893 cx.debug_bounds("lower-left").unwrap(),
2894 );
2895
2896 let bottom = cx.read(|cx| {
2897 area.read(cx)
2898 .layout(DockPlacement::Bottom)
2899 .unwrap()
2900 .root()
2901 .id()
2902 });
2903 let group = cx.read(|cx| area.read(cx).groups[&bottom].entity.clone());
2904 cx.update(|window, cx| {
2905 group.update(cx, |group, cx| group.select_tab(1, window, cx));
2906 });
2907 cx.run_until_parked();
2908 draw_frames(cx, 3);
2909 let after = (
2910 cx.debug_bounds("upper-left").unwrap(),
2911 cx.debug_bounds("lower-left").unwrap(),
2912 );
2913
2914 assert_eq!(
2915 before, after,
2916 "a tab change in the bottom dock must not move the left split"
2917 );
2918 }
2919
2920 #[gpui::test]
2925 fn switching_a_tab_keeps_a_restored_split_at_its_rescaled_share(cx: &mut TestAppContext) {
2926 let (area, cx) = setup(cx);
2927 cx.update(|window, cx| {
2928 area.update(cx, |area, cx| {
2929 area.set_center(
2930 DockLayout::h_split()
2931 .child(
2932 DockLayout::tabs()
2933 .panel(TestPanel::new("Alpha", cx))
2934 .panel(TestPanel::new("Beta", cx)),
2935 Some(px(620.)),
2936 )
2937 .child(
2938 DockLayout::tabs().panel(MeasuredPanel::new("second", cx)),
2939 Some(px(350.)),
2940 ),
2941 window,
2942 cx,
2943 );
2944 });
2945 });
2946 cx.run_until_parked();
2947 draw_frames(cx, 3);
2948 let before = cx.debug_bounds("second").unwrap();
2951 assert_ne!(
2952 before.right(),
2953 px(970.),
2954 "the window must not match the recorded total, or this test cannot \
2955 tell a rescaled split from the file's pixels"
2956 );
2957
2958 let group = group_of(&area, 0, cx);
2959 cx.update(|window, cx| {
2960 group.update(cx, |group, cx| group.select_tab(1, window, cx));
2961 });
2962 cx.run_until_parked();
2963 draw_frames(cx, 3);
2964 let after = cx.debug_bounds("second").unwrap();
2965
2966 assert_eq!(
2967 before, after,
2968 "a tab change must not hand the file's pixels back to the split"
2969 );
2970 }
2971
2972 #[gpui::test]
2983 fn the_shipped_fixture_survives_a_load_dump_load_round_trip(cx: &mut TestAppContext) {
2984 let (area, cx) = setup(cx);
2985 let fixture: DockAreaState =
2986 serde_json::from_str(include_str!("fixtures/layout.json")).unwrap();
2987
2988 cx.update(|window, cx| area.update(cx, |area, cx| area.load(fixture, window, cx).unwrap()));
2989 cx.run_until_parked();
2990 let first = cx.read(|cx| area.read(cx).dump(cx));
2991
2992 assert_eq!(first.center.children.len(), 2, "the center's two groups");
2993 assert_eq!(first.center.children[0].children.len(), 15);
2994 assert_eq!(first.center.children[1].children.len(), 1);
2995 for dock in [&first.left_dock, &first.bottom_dock, &first.right_dock] {
2996 let dock = dock.as_ref().expect("all three docks are attached");
2997 assert!(dock.open());
2998 assert!(
2999 !dock.panel().children.is_empty(),
3000 "a dock that loaded empty would round-trip just as stably"
3001 );
3002 }
3003 assert_eq!(first.left_dock.as_ref().unwrap().size(), px(350.));
3004 assert_eq!(first.bottom_dock.as_ref().unwrap().size(), px(200.));
3005 assert_eq!(first.right_dock.as_ref().unwrap().size(), px(320.));
3006
3007 cx.update(|window, cx| {
3008 area.update(cx, |area, cx| area.load(first.clone(), window, cx).unwrap())
3009 });
3010 cx.run_until_parked();
3011 let second = cx.read(|cx| area.read(cx).dump(cx));
3012
3013 assert_eq!(second, first, "dump == dump(load(dump))");
3014 }
3015
3016 #[gpui::test]
3017 fn an_unregistered_panel_survives_a_load_and_save_round_trip(cx: &mut TestAppContext) {
3018 let (area, cx) = setup(cx);
3019 cx.update(|_, cx| register_test_panels(cx));
3020
3021 let json = include_str!("fixtures/unregistered_panel.json");
3022 let state: DockAreaState = serde_json::from_str(json).unwrap();
3023
3024 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
3025 let dumped = cx.read(|cx| area.read(cx).dump(cx));
3026
3027 let leaf = &dumped.center.children[0].children[0];
3028 assert_eq!(leaf.panel_name, "PanelFromTheFuture");
3029 assert_eq!(
3030 leaf.info,
3031 PanelInfo::panel(serde_json::json!({"keep": "me"})),
3032 "a panel this build cannot construct keeps its payload"
3033 );
3034 }
3035
3036 #[gpui::test]
3037 fn a_dock_carries_its_own_tree_and_survives_a_round_trip(cx: &mut TestAppContext) {
3038 let (area, cx) = setup(cx);
3039 cx.update(|window, cx| {
3040 let alpha = TestPanel::new("Alpha", cx);
3041 let beta = TestPanel::new("Beta", cx);
3042 area.update(cx, |area, cx| {
3043 area.set_center(DockLayout::tabs().panel(alpha), window, cx);
3044 area.set_dock(
3045 DockPlacement::Left,
3046 DockLayout::tabs().panel(beta),
3047 window,
3048 cx,
3049 );
3050 });
3051 });
3052
3053 let center_ids = cx.read(|cx| {
3062 area.read(cx)
3063 .layout(DockPlacement::Center)
3064 .unwrap()
3065 .node_ids()
3066 });
3067 let dock_ids = cx.read(|cx| {
3068 area.read(cx)
3069 .layout(DockPlacement::Left)
3070 .unwrap()
3071 .node_ids()
3072 });
3073 assert!(!center_ids.is_empty() && !dock_ids.is_empty());
3074 assert!(
3075 center_ids.iter().all(|id| !dock_ids.contains(id)),
3076 "every tree in one area must draw from one id space: \
3077 center {center_ids:?} vs left {dock_ids:?}"
3078 );
3079
3080 let state = cx.read(|cx| area.read(cx).dump(cx));
3081 let left = state.left_dock.clone().expect("the left dock is written");
3082 assert_eq!(left.placement(), DockPlacement::Left);
3083 assert!(left.open());
3084 assert_eq!(left.panel().children[0].panel_name, "Beta");
3085 assert!(state.right_dock.is_none());
3086 }
3087
3088 #[gpui::test]
3089 fn a_panel_moved_between_regions_keeps_its_active_state(cx: &mut TestAppContext) {
3090 let log = log_of();
3091 let (area, alpha, cx) = two_groups(&log, cx);
3092 cx.run_until_parked();
3093 assert!(drain(&log).contains(&("Alpha", PanelSignal::Active(true))));
3096
3097 move_alpha_into_the_other_group(&area, &alpha, cx);
3098
3099 assert!(
3100 !drain(&log).contains(&("Alpha", PanelSignal::Active(true))),
3101 "a displayed panel dragged to another group must not be told `true` twice"
3102 );
3103 }
3104
3105 #[gpui::test]
3106 fn a_groups_close_intent_reaches_the_tree(cx: &mut TestAppContext) {
3107 let log = log_of();
3111 let (area, alpha, cx) = two_groups(&log, cx);
3112 cx.run_until_parked();
3113 drain(&log);
3114
3115 let node = child_node(&area, 0, cx);
3116 let alpha_id = panel_id_of(&alpha);
3117 cx.update(|_, cx| {
3118 let group = area.read(cx).groups.get(&node).unwrap().entity.clone();
3119 group.update(cx, |group, cx| group.close_panel(alpha_id, cx));
3120 });
3121 cx.run_until_parked();
3122
3123 assert!(
3124 cx.read(|cx| area
3125 .read(cx)
3126 .layout(DockPlacement::Center)
3127 .unwrap()
3128 .find_panel_node(alpha_id))
3129 .is_none(),
3130 "the close intent was applied to the tree"
3131 );
3132 assert!(drain(&log).contains(&("Alpha", PanelSignal::Removed)));
3133 }
3134
3135 #[gpui::test]
3136 fn replacing_the_center_tells_the_panels_that_left(cx: &mut TestAppContext) {
3137 let log = log_of();
3138 let (area, _alpha, cx) = two_groups(&log, cx);
3139 cx.run_until_parked();
3140 drain(&log);
3141
3142 cx.update(|window, cx| {
3143 let gamma = TestPanel::new("Gamma", cx);
3144 area.update(cx, |area, cx| {
3145 area.set_center(DockLayout::tabs().panel(gamma), window, cx)
3146 });
3147 });
3148 cx.run_until_parked();
3149
3150 let seen = drain(&log);
3151 assert!(seen.contains(&("Alpha", PanelSignal::Removed)));
3152 assert!(seen.contains(&("Beta", PanelSignal::Removed)));
3153 }
3154
3155 #[gpui::test]
3156 fn closing_a_zoomed_panel_clears_the_zoom(cx: &mut TestAppContext) {
3157 let log = log_of();
3158 let (area, alpha, cx) = two_groups(&log, cx);
3159 cx.run_until_parked();
3160
3161 let node = child_node(&area, 0, cx);
3162 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_in(node, window, cx)));
3163 assert!(cx.read(|cx| area.read(cx).is_zoomed()));
3164
3165 cx.update(|window, cx| {
3166 area.update(cx, |area, cx| area.remove_panel(alpha.clone(), window, cx))
3167 });
3168
3169 assert!(
3170 !cx.read(|cx| area.read(cx).is_zoomed()),
3171 "a zoomed panel that left the dock must not keep filling it"
3172 );
3173 }
3174
3175 struct SkinPlaceholder {
3179 state: PanelState,
3180 focus_handle: FocusHandle,
3181 }
3182
3183 impl Panel for SkinPlaceholder {
3184 fn panel_name(&self) -> &'static str {
3185 "SkinPlaceholder"
3186 }
3187
3188 fn dump(&self, _: &App) -> PanelState {
3189 self.state.clone()
3190 }
3191 }
3192
3193 impl EventEmitter<PanelEvent> for SkinPlaceholder {}
3194
3195 impl Focusable for SkinPlaceholder {
3196 fn focus_handle(&self, _: &App) -> FocusHandle {
3197 self.focus_handle.clone()
3198 }
3199 }
3200
3201 impl Render for SkinPlaceholder {
3202 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3203 Empty
3204 }
3205 }
3206
3207 struct PlaceholderSkin {
3208 asked: Rc<std::cell::RefCell<Vec<String>>>,
3209 }
3210
3211 impl DockAreaRenderer for PlaceholderSkin {
3212 fn build_placeholder(
3213 &self,
3214 state: &PanelState,
3215 _: &mut Window,
3216 cx: &mut App,
3217 ) -> Option<Arc<dyn PanelView>> {
3218 self.asked.borrow_mut().push(state.panel_name.clone());
3219 let state = state.clone();
3220 Some(Arc::new(cx.new(|cx| SkinPlaceholder {
3221 state,
3222 focus_handle: cx.focus_handle(),
3223 })))
3224 }
3225
3226 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
3227 Rc::new(BareTabGroup)
3228 }
3229 }
3230
3231 #[gpui::test]
3235 fn an_unbuildable_panel_becomes_the_skins_placeholder(cx: &mut TestAppContext) {
3236 cx.update(|cx| {
3237 let _ = crate::Theme::global_mut(cx);
3238 });
3239 let asked: Rc<std::cell::RefCell<Vec<String>>> = Rc::default();
3240 let skin = Rc::new(PlaceholderSkin {
3241 asked: asked.clone(),
3242 });
3243 let (area, cx) = cx.add_window_view(|window, cx| {
3244 DockArea::new("test-dock", None, window, cx).with_renderer(skin)
3245 });
3246
3247 cx.update(|window, cx| {
3249 let ghost = TestPanel::new("Ghost", cx);
3250 area.update(cx, |area, cx| {
3251 area.set_center(DockLayout::tabs().panel(ghost), window, cx)
3252 });
3253 });
3254 let state = cx.read(|cx| area.read(cx).dump(cx));
3255 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
3256 cx.run_until_parked();
3257
3258 assert_eq!(*asked.borrow(), vec!["Ghost".to_string()]);
3259 assert_eq!(
3260 cx.read(|cx| area
3261 .read(cx)
3262 .panels
3263 .values()
3264 .map(|panel| panel.panel_name(cx))
3265 .collect::<Vec<_>>()),
3266 vec!["SkinPlaceholder"],
3267 "base installed the skin's placeholder, not its own"
3268 );
3269 assert_eq!(
3270 cx.read(|cx| area.read(cx).dump(cx)).center.children[0].children[0].panel_name,
3271 "Ghost",
3272 "and the unknown panel still survives the next save"
3273 );
3274 }
3275
3276 #[gpui::test]
3277 fn removing_a_non_tail_child_shifts_the_split_sizes_with_it(cx: &mut TestAppContext) {
3278 let log = log_of();
3282 let (area, cx) = setup(cx);
3283 let alpha = cx.update(|window, cx| {
3284 let alpha = TestPanel::logging("Alpha", &log, cx);
3285 let beta = TestPanel::logging("Beta", &log, cx);
3286 let gamma = TestPanel::logging("Gamma", &log, cx);
3287 area.update(cx, |area, cx| {
3288 area.set_center(
3289 DockLayout::h_split()
3290 .child(DockLayout::tabs().panel(alpha.clone()), Some(px(100.)))
3291 .child(DockLayout::tabs().panel(beta), Some(px(200.)))
3292 .child(DockLayout::tabs().panel(gamma), Some(px(300.))),
3293 window,
3294 cx,
3295 );
3296 });
3297 alpha
3298 });
3299
3300 let root = cx.read(|cx| {
3301 area.read(cx)
3302 .layout(DockPlacement::Center)
3303 .unwrap()
3304 .root()
3305 .id()
3306 });
3307 let split = cx.read(|cx| area.read(cx).splits.get(&root).unwrap().entity.clone());
3308 let sizes = |cx: &mut VisualTestContext| cx.read(|cx| split.read(cx).sizes().clone());
3309
3310 let before = sizes(cx);
3314 assert_eq!(before.len(), 3);
3315 let kept_if_correct = before[1] / before[2];
3316 let kept_if_truncated = before[0] / before[1];
3317 assert!(
3318 (kept_if_correct - kept_if_truncated).abs() > 0.1,
3319 "the fixture must be able to tell the two outcomes apart"
3320 );
3321
3322 cx.update(|window, cx| area.update(cx, |area, cx| area.remove_panel(alpha, window, cx)));
3323
3324 let after = sizes(cx);
3325 assert_eq!(after.len(), 2);
3326 assert!(
3327 (after[0] / after[1] - kept_if_correct).abs() < 0.01,
3328 "the survivors kept their own proportions: slot 0 was removed, not \
3329 the tail — got {after:?} from {before:?}"
3330 );
3331 }
3332
3333 #[gpui::test]
3334 fn a_panel_moves_between_the_center_and_a_dock(cx: &mut TestAppContext) {
3335 let log = log_of();
3336 let (area, alpha, cx) = two_groups(&log, cx);
3337 cx.update(|window, cx| {
3338 let gamma = TestPanel::logging("Gamma", &log, cx);
3339 area.update(cx, |area, cx| {
3340 area.set_dock(
3341 DockPlacement::Left,
3342 DockLayout::tabs().panel(gamma),
3343 window,
3344 cx,
3345 );
3346 });
3347 });
3348 cx.run_until_parked();
3349 drain(&log);
3350
3351 let alpha_id = panel_id_of(&alpha);
3352 let dock_group = cx.read(|cx| {
3353 area.read(cx)
3354 .layout(DockPlacement::Left)
3355 .unwrap()
3356 .root()
3357 .id()
3358 });
3359
3360 cx.update(|window, cx| {
3361 area.update(cx, |area, cx| {
3362 area.move_panel(
3363 alpha_id,
3364 InsertTarget::Tabs {
3365 node: dock_group,
3366 ix: None,
3367 activate: true,
3368 },
3369 window,
3370 cx,
3371 );
3372 });
3373 });
3374 cx.run_until_parked();
3375
3376 assert!(
3377 cx.read(|cx| area
3378 .read(cx)
3379 .layout(DockPlacement::Center)
3380 .unwrap()
3381 .find_panel_node(alpha_id))
3382 .is_none(),
3383 "the panel left the center"
3384 );
3385 assert_eq!(
3386 cx.read(|cx| area
3387 .read(cx)
3388 .layout(DockPlacement::Left)
3389 .unwrap()
3390 .find_panel_node(alpha_id)),
3391 Some(dock_group),
3392 "and arrived in the dock's group"
3393 );
3394
3395 let seen = drain(&log);
3396 assert!(
3397 !seen.contains(&("Alpha", PanelSignal::Removed)),
3398 "crossing regions is still a move, not a removal"
3399 );
3400 assert!(
3401 !seen.contains(&("Alpha", PanelSignal::Active(true))),
3402 "and it was displayed in both, so it is not told `true` twice"
3403 );
3404 }
3405
3406 #[gpui::test]
3407 fn a_move_onto_an_unusable_target_leaves_no_stranded_panel(cx: &mut TestAppContext) {
3408 let log = log_of();
3413 let (area, _alpha, cx) = two_groups(&log, cx);
3414 let gamma = cx.update(|window, cx| {
3415 let gamma = TestPanel::logging("Gamma", &log, cx);
3416 area.update(cx, |area, cx| {
3417 area.set_dock(
3418 DockPlacement::Left,
3419 DockLayout::tabs().panel(gamma.clone()),
3420 window,
3421 cx,
3422 );
3423 });
3424 gamma
3425 });
3426 cx.run_until_parked();
3427 drain(&log);
3428
3429 let gamma_id = panel_id_of(&gamma);
3430 let center_root = cx.read(|cx| {
3433 area.read(cx)
3434 .layout(DockPlacement::Center)
3435 .unwrap()
3436 .root()
3437 .id()
3438 });
3439
3440 cx.update(|window, cx| {
3441 area.update(cx, |area, cx| {
3442 area.move_panel(
3443 gamma_id,
3444 InsertTarget::Tabs {
3445 node: center_root,
3446 ix: None,
3447 activate: true,
3448 },
3449 window,
3450 cx,
3451 );
3452 });
3453 });
3454 cx.run_until_parked();
3455
3456 assert!(
3457 cx.read(|cx| area.read(cx).panel(gamma_id).is_none()),
3458 "the view map agrees with the trees straight away, rather than \
3459 carrying a panel that belongs to no tree"
3460 );
3461 assert!(
3462 drain(&log).contains(&("Gamma", PanelSignal::Removed)),
3463 "and the panel was told so at the point of the call"
3464 );
3465 }
3466
3467 #[gpui::test]
3468 fn an_all_hidden_container_reports_itself_invisible(cx: &mut TestAppContext) {
3469 let (area, cx) = setup(cx);
3472 let beta = cx.update(|window, cx| {
3473 let alpha = TestPanel::new("Alpha", cx);
3474 let beta = TestPanel::new("Beta", cx);
3475 area.update(cx, |area, cx| {
3476 area.set_center(
3477 DockLayout::h_split()
3478 .child(DockLayout::tabs().panel(alpha), None)
3479 .child(DockLayout::tabs().panel(beta.clone()), None),
3480 window,
3481 cx,
3482 );
3483 });
3484 beta
3485 });
3486
3487 let visible = |ix: usize, cx: &mut VisualTestContext| {
3488 let node = child_node(&area, ix, cx);
3489 cx.read(|cx| {
3490 let area = area.read(cx);
3491 let tree = area.layout(DockPlacement::Center).unwrap();
3492 area.is_node_visible(tree.find_node(node).unwrap(), cx)
3493 })
3494 };
3495
3496 assert!(visible(0, cx) && visible(1, cx));
3497
3498 cx.update(|_, cx| beta.update(cx, |beta, cx| beta.set_visible(false, cx)));
3499
3500 assert!(visible(0, cx), "the visible group still holds its slot");
3501 assert!(
3502 !visible(1, cx),
3503 "a group whose every panel is hidden must give its slot up"
3504 );
3505 }
3506
3507 #[gpui::test]
3508 fn a_locked_area_seals_its_groups(cx: &mut TestAppContext) {
3509 let log = log_of();
3510 let (area, _alpha, cx) = two_groups(&log, cx);
3511 cx.run_until_parked();
3512
3513 let node = child_node(&area, 0, cx);
3514 let group = cx.read(|cx| area.read(cx).groups.get(&node).unwrap().entity.clone());
3515 assert!(
3516 cx.read(|cx| group.read(cx).is_closable(cx)),
3517 "an unlocked group's panel can be closed"
3518 );
3519
3520 cx.update(|window, cx| area.update(cx, |area, cx| area.set_locked(true, window, cx)));
3521
3522 assert!(
3523 !cx.read(|cx| group.read(cx).is_closable(cx)),
3524 "the lock reaches every group through the constraints push"
3525 );
3526 }
3527
3528 #[gpui::test]
3536 fn empty_center_round_trips_as_a_stack(cx: &mut TestAppContext) {
3537 let (area, cx) = setup(cx);
3538 let center = cx.read(|cx| area.read(cx).dump(cx).center);
3539
3540 assert_eq!(center.panel_name, "StackPanel");
3541 assert!(
3542 matches!(center.info, PanelInfo::Stack { .. }),
3543 "got {:?}",
3544 center.info
3545 );
3546 }
3547
3548 #[gpui::test]
3549 fn fresh_center_is_empty(cx: &mut TestAppContext) {
3550 let (area, cx) = setup(cx);
3551
3552 assert!(
3553 is_center_empty(&area, cx),
3554 "DockArea::new starts with an empty split centre"
3555 );
3556 }
3557
3558 #[gpui::test]
3559 fn center_holding_a_tab_group_is_not_empty(cx: &mut TestAppContext) {
3560 let log = log_of();
3561 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3562 cx.run_until_parked();
3563
3564 assert!(!is_center_empty(&area, cx));
3565 }
3566
3567 #[gpui::test]
3571 fn center_is_empty_again_once_every_panel_is_removed(cx: &mut TestAppContext) {
3572 let log = log_of();
3573 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3574 cx.run_until_parked();
3575
3576 for panel in panels {
3577 cx.update(|window, cx| {
3578 area.update(cx, |area, cx| area.remove_panel(panel.clone(), window, cx))
3579 });
3580 }
3581 cx.run_until_parked();
3582
3583 assert!(is_center_empty(&area, cx));
3584 }
3585
3586 #[gpui::test]
3587 fn center_is_not_empty_after_adding_to_a_tab_group(cx: &mut TestAppContext) {
3588 let (area, cx) = setup(cx);
3589 assert!(is_center_empty(&area, cx));
3590
3591 cx.update(|window, cx| {
3592 let alpha = TestPanel::new("Alpha", cx);
3593 area.update(cx, |area, cx| {
3594 area.add_panel(alpha, DockPlacement::Center, None, window, cx)
3595 });
3596 });
3597 cx.run_until_parked();
3598
3599 assert!(!is_center_empty(&area, cx));
3600 }
3601
3602 #[gpui::test]
3606 fn add_panel_view_registers_the_handle_it_was_given(cx: &mut TestAppContext) {
3607 let (area, cx) = setup(cx);
3608
3609 let view = cx.update(|window, cx| {
3610 let view: Arc<dyn PanelView> = Arc::new(TestPanel::new("Alpha", cx));
3611 area.update(cx, |area, cx| {
3612 area.add_panel_view(view.clone(), DockPlacement::Center, None, window, cx)
3613 });
3614 view
3615 });
3616 cx.run_until_parked();
3617
3618 let id = cx.read(|cx| view.panel_id(cx));
3619 assert!(
3620 cx.read(|cx| area
3621 .read(cx)
3622 .panel(id)
3623 .is_some_and(|stored| Arc::ptr_eq(stored, &view))),
3624 "the stored handle is the one that was handed over, under its own id"
3625 );
3626 assert!(!is_center_empty(&area, cx));
3627 }
3628
3629 #[gpui::test]
3632 fn center_holding_only_hidden_panels_is_empty(cx: &mut TestAppContext) {
3633 let log = log_of();
3634 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3635 cx.run_until_parked();
3636 assert!(!is_center_empty(&area, cx));
3637
3638 cx.update(|_, cx| {
3639 for panel in &panels {
3640 panel.update(cx, |panel, cx| panel.set_visible(false, cx));
3641 }
3642 });
3643 cx.run_until_parked();
3644
3645 assert_eq!(
3646 cx.read(|cx| area
3647 .read(cx)
3648 .layout(DockPlacement::Center)
3649 .unwrap()
3650 .panels()
3651 .count()),
3652 2,
3653 "hiding a panel does not remove it from the tab group"
3654 );
3655 assert!(is_center_empty(&area, cx));
3656 }
3657
3658 #[gpui::test]
3659 fn single_panel_group_receives_initial_active(cx: &mut TestAppContext) {
3660 let log = log_of();
3661 let (_area, _panels, cx) = one_group(&log, &["A"], None, cx);
3662 cx.run_until_parked();
3663
3664 assert_eq!(drain_active(&log), [("A", true)]);
3665 }
3666
3667 #[gpui::test]
3668 fn multi_tab_construction_notifies_only_displayed_panel(cx: &mut TestAppContext) {
3669 let log = log_of();
3670 let (_area, _panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
3671 cx.run_until_parked();
3672
3673 assert_eq!(drain_active(&log), [("A", true)]);
3675 }
3676
3677 #[gpui::test]
3678 fn active_index_restore_notifies_that_panel_only(cx: &mut TestAppContext) {
3679 let log = log_of();
3680 let (_area, _panels, cx) = one_group(&log, &["A", "B", "C"], Some(2), cx);
3681 cx.run_until_parked();
3682
3683 assert_eq!(drain_active(&log), [("C", true)]);
3684 }
3685
3686 #[gpui::test]
3687 fn switching_tabs_sends_false_then_true(cx: &mut TestAppContext) {
3688 let log = log_of();
3689 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3690 cx.run_until_parked();
3691 drain(&log);
3692
3693 let group = group_of(&area, 0, cx);
3694 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(1, window, cx)));
3695 cx.run_until_parked();
3696
3697 assert_eq!(drain_active(&log), [("A", false), ("B", true)]);
3698 }
3699
3700 #[gpui::test]
3701 fn select_panel_displays_that_tab_where_it_sits(cx: &mut TestAppContext) {
3702 let log = log_of();
3703 let (area, panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
3704 cx.run_until_parked();
3705 drain(&log);
3706
3707 let b = panel_id_of(&panels[1]);
3708 cx.update(|window, cx| area.update(cx, |area, cx| area.select_panel(b, window, cx)));
3709 cx.run_until_parked();
3710
3711 assert_eq!(drain_active(&log), [("A", false), ("B", true)]);
3712 let (order, active_ix) = cx.read(|cx| {
3713 let tree = area.read(cx).layout(DockPlacement::Center).unwrap();
3714 let node = tree.find_panel_node(b).unwrap();
3715 match tree.find_node(node).unwrap().kind() {
3716 PaneRef::Tabs { panels, active_ix } => (panels.to_vec(), active_ix),
3717 PaneRef::Split { .. } => panic!("a tab group"),
3718 }
3719 });
3720 assert_eq!(active_ix, 1, "the selected tab is displayed");
3721 assert_eq!(
3722 order,
3723 panels.iter().map(panel_id_of).collect::<Vec<_>>(),
3724 "selecting a tab does not move it"
3725 );
3726 }
3727
3728 #[gpui::test]
3729 fn select_panel_is_silent_for_the_displayed_or_an_unknown_panel(cx: &mut TestAppContext) {
3730 let log = log_of();
3731 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3732 cx.run_until_parked();
3733 drain(&log);
3734
3735 let a = panel_id_of(&panels[0]);
3736 let stranger = cx.update(|_, cx| panel_id_of(&TestPanel::logging("Z", &log, cx)));
3737 cx.update(|window, cx| {
3738 area.update(cx, |area, cx| {
3739 area.select_panel(a, window, cx);
3740 area.select_panel(stranger, window, cx);
3741 })
3742 });
3743 cx.run_until_parked();
3744
3745 assert_eq!(drain_active(&log), []);
3746 }
3747
3748 #[gpui::test]
3749 fn reselecting_active_tab_stays_silent(cx: &mut TestAppContext) {
3750 let log = log_of();
3751 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3752 cx.run_until_parked();
3753 drain(&log);
3754
3755 let group = group_of(&area, 0, cx);
3756 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(0, window, cx)));
3757 cx.run_until_parked();
3758
3759 assert_eq!(drain_active(&log), []);
3760 }
3761
3762 #[gpui::test]
3768 fn inserting_at_active_ix_swaps_notifications(cx: &mut TestAppContext) {
3769 let log = log_of();
3770 let (area, cx) = setup(cx);
3771 let c = cx.update(|window, cx| {
3772 let a = TestPanel::logging("A", &log, cx);
3773 let b = TestPanel::logging("B", &log, cx);
3774 let x = TestPanel::logging("X", &log, cx);
3775 let c = TestPanel::logging("C", &log, cx);
3776 area.update(cx, |area, cx| {
3777 area.set_center(
3778 DockLayout::h_split()
3779 .child(DockLayout::tabs().panel(a).panel(b), None)
3780 .child(DockLayout::tabs().panel(x).panel(c.clone()), None),
3781 window,
3782 cx,
3783 );
3784 });
3785 c
3786 });
3787 cx.run_until_parked();
3788 drain(&log);
3789
3790 let destination = child_node(&area, 0, cx);
3791 let c_id = panel_id_of(&c);
3792 move_panel_into(&area, c_id, destination, Some(0), true, cx);
3793
3794 assert_eq!(drain_active(&log), [("A", false), ("C", true)]);
3795 let group = group_of(&area, 0, cx);
3796 assert_eq!(cx.read(|cx| group.read(cx).active_ix()), 0);
3797 assert_eq!(
3798 cx.read(|cx| group.read(cx).panels()[0].panel_id(cx)),
3799 c_id,
3800 "the arriving panel took the slot it named"
3801 );
3802 }
3803
3804 #[gpui::test]
3805 fn removing_before_active_keeps_displayed_panel(cx: &mut TestAppContext) {
3806 let log = log_of();
3807 let (area, panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
3808 let group = group_of(&area, 0, cx);
3809 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(1, window, cx)));
3810 cx.run_until_parked();
3811 drain(&log);
3812
3813 cx.update(|window, cx| {
3814 area.update(cx, |area, cx| {
3815 area.remove_panel(panels[0].clone(), window, cx)
3816 })
3817 });
3818 cx.run_until_parked();
3819
3820 assert_eq!(drain_active(&log), []);
3821 assert_eq!(cx.read(|cx| group.read(cx).active_ix()), 0);
3822 assert_eq!(
3823 cx.read(|cx| group.read(cx).panels()[0].panel_id(cx)),
3824 panel_id_of(&panels[1]),
3825 "the same panel is still displayed, at its new index"
3826 );
3827 }
3828
3829 #[gpui::test]
3832 fn collapse_and_expand_notify_active_panel(cx: &mut TestAppContext) {
3833 let log = log_of();
3834 let (area, cx) = setup(cx);
3835 cx.update(|window, cx| {
3836 let a = TestPanel::logging("A", &log, cx);
3837 let b = TestPanel::logging("B", &log, cx);
3838 area.update(cx, |area, cx| {
3839 area.set_dock(
3840 DockPlacement::Left,
3841 DockLayout::tabs().panel(a).panel(b),
3842 window,
3843 cx,
3844 );
3845 });
3846 });
3847 cx.run_until_parked();
3848 drain(&log);
3849
3850 cx.update(|window, cx| {
3851 area.update(cx, |area, cx| {
3852 area.toggle_dock(DockPlacement::Left, window, cx)
3853 })
3854 });
3855 cx.run_until_parked();
3856 assert_eq!(drain_active(&log), [("A", false)]);
3857
3858 cx.update(|window, cx| {
3859 area.update(cx, |area, cx| {
3860 area.toggle_dock(DockPlacement::Left, window, cx)
3861 })
3862 });
3863 cx.run_until_parked();
3864 assert_eq!(drain_active(&log), [("A", true)]);
3865 }
3866
3867 #[gpui::test]
3868 fn background_add_is_silent_but_first_panel_is_not(cx: &mut TestAppContext) {
3869 let log = log_of();
3870 let (area, cx) = setup(cx);
3871 let d = cx.update(|window, cx| {
3872 let a = TestPanel::logging("A", &log, cx);
3873 let b = TestPanel::logging("B", &log, cx);
3874 let c = TestPanel::logging("C", &log, cx);
3875 let d = TestPanel::logging("D", &log, cx);
3876 area.update(cx, |area, cx| {
3877 area.set_center(
3878 DockLayout::h_split()
3879 .child(DockLayout::tabs().panel(a).panel(b), None)
3880 .child(DockLayout::tabs().panel(c).panel(d.clone()), None),
3881 window,
3882 cx,
3883 );
3884 });
3885 d
3886 });
3887 cx.run_until_parked();
3888 drain(&log);
3889
3890 let destination = child_node(&area, 0, cx);
3893 move_panel_into(&area, panel_id_of(&d), destination, None, false, cx);
3894 assert_eq!(drain_active(&log), []);
3895
3896 cx.update(|window, cx| {
3899 let e = TestPanel::logging("E", &log, cx);
3900 area.update(cx, |area, cx| {
3901 area.add_panel(e, DockPlacement::Left, None, window, cx)
3902 });
3903 });
3904 cx.run_until_parked();
3905 assert_eq!(drain_active(&log), [("E", true)]);
3906 }
3907
3908 #[gpui::test]
3909 fn drag_active_panel_to_other_group_stays_silent_for_it(cx: &mut TestAppContext) {
3910 let log = log_of();
3911 let (area, cx) = setup(cx);
3912 let a = cx.update(|window, cx| {
3913 let a = TestPanel::logging("A", &log, cx);
3914 let b = TestPanel::logging("B", &log, cx);
3915 let c = TestPanel::logging("C", &log, cx);
3916 area.update(cx, |area, cx| {
3917 area.set_center(
3918 DockLayout::h_split()
3919 .child(DockLayout::tabs().panel(a.clone()).panel(b), None)
3920 .child(DockLayout::tabs().panel(c), None),
3921 window,
3922 cx,
3923 );
3924 });
3925 a
3926 });
3927 cx.run_until_parked();
3928 drain(&log);
3929
3930 let destination = child_node(&area, 1, cx);
3933 move_panel_into(&area, panel_id_of(&a), destination, None, true, cx);
3934
3935 let seen = drain_active(&log);
3938 assert!(seen.contains(&("B", true)), "got {seen:?}");
3939 assert!(seen.contains(&("C", false)), "got {seen:?}");
3940 assert!(
3941 !seen.iter().any(|(name, _)| *name == "A"),
3942 "the moved panel was displayed before and after: {seen:?}"
3943 );
3944 }
3945
3946 #[gpui::test]
3947 fn drag_active_panel_to_background_slot_deactivates_it(cx: &mut TestAppContext) {
3948 let log = log_of();
3949 let (area, cx) = setup(cx);
3950 let a = cx.update(|window, cx| {
3951 let a = TestPanel::logging("A", &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.clone()), None)
3958 .child(DockLayout::tabs().panel(c).panel(d), None),
3959 window,
3960 cx,
3961 );
3962 });
3963 a
3964 });
3965 cx.run_until_parked();
3966 drain(&log);
3967
3968 let destination = child_node(&area, 1, cx);
3971 move_panel_into(&area, panel_id_of(&a), destination, None, false, cx);
3972
3973 assert_eq!(drain_active(&log), [("A", false)]);
3974 }
3975
3976 struct RecordingSkin {
3983 tab_bars: Rc<RefCell<Vec<NodeId>>>,
3984 }
3985
3986 struct RecordingTabGroup {
3987 drawn: Rc<RefCell<Vec<NodeId>>>,
3988 }
3989
3990 impl DockAreaRenderer for RecordingSkin {
3991 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
3992 Rc::new(RecordingTabGroup {
3993 drawn: self.tab_bars.clone(),
3994 })
3995 }
3996 }
3997
3998 impl TabGroupRenderer for RecordingTabGroup {
3999 fn render_tab_bar(
4000 &self,
4001 group: &TabGroupContext,
4002 _: &mut Window,
4003 _: &mut App,
4004 ) -> AnyElement {
4005 self.drawn.borrow_mut().push(group.node());
4006 Empty.into_any_element()
4007 }
4008 }
4009
4010 type DrawLog = Rc<RefCell<Vec<NodeId>>>;
4011
4012 fn setup_recording(
4014 cx: &mut TestAppContext,
4015 ) -> (Entity<DockArea>, DrawLog, &mut VisualTestContext) {
4016 cx.update(|cx| {
4017 let _ = crate::Theme::global_mut(cx);
4018 });
4019 let tab_bars: DrawLog = Rc::default();
4020 let skin = Rc::new(RecordingSkin {
4021 tab_bars: tab_bars.clone(),
4022 });
4023 let (area, cx) = cx.add_window_view(|window, cx| {
4024 DockArea::new("test-dock", None, window, cx).with_renderer(skin)
4025 });
4026 (area, tab_bars, cx)
4027 }
4028
4029 fn zoom_signals(log: &Log) -> Vec<(&'static str, PanelSignal)> {
4030 drain(log)
4031 .into_iter()
4032 .filter(|(_, signal)| matches!(signal, PanelSignal::Zoomed(_)))
4033 .collect()
4034 }
4035
4036 #[gpui::test]
4045 fn a_zoomed_group_is_drawn_whole_rather_than_as_its_bare_panel(cx: &mut TestAppContext) {
4046 let log = log_of();
4047 let (area, tab_bars, cx) = setup_recording(cx);
4048 cx.update(|window, cx| {
4049 let alpha = TestPanel::logging("Alpha", &log, cx);
4050 let beta = TestPanel::logging("Beta", &log, cx);
4051 area.update(cx, |area, cx| {
4052 area.set_center(
4053 DockLayout::h_split()
4054 .child(DockLayout::tabs().panel(alpha), None)
4055 .child(DockLayout::tabs().panel(beta), None),
4056 window,
4057 cx,
4058 );
4059 });
4060 });
4061 cx.run_until_parked();
4062
4063 let zoomed = child_node(&area, 0, cx);
4064 let other = child_node(&area, 1, cx);
4065 assert!(
4066 tab_bars.borrow().contains(&zoomed) && tab_bars.borrow().contains(&other),
4067 "both groups draw their own tab bar while nothing is zoomed"
4068 );
4069
4070 tab_bars.borrow_mut().clear();
4071 let group = group_of(&area, 0, cx);
4072 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
4073 cx.run_until_parked();
4074
4075 assert!(
4076 tab_bars.borrow().contains(&zoomed),
4077 "a zoomed group is rendered whole: its own tab bar is still drawn, \
4078 which is exactly what the bare panel does not carry"
4079 );
4080 assert!(
4081 !tab_bars.borrow().contains(&other),
4082 "and it is the only thing on screen"
4083 );
4084 }
4085
4086 #[gpui::test]
4092 fn clearing_the_zoom_from_outside_puts_the_groups_own_flag_back(cx: &mut TestAppContext) {
4093 let log = log_of();
4094 let (area, _alpha, cx) = two_groups(&log, cx);
4095 cx.run_until_parked();
4096 drain(&log);
4097
4098 let node = child_node(&area, 0, cx);
4099 let group = group_of(&area, 0, cx);
4100 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
4101 cx.run_until_parked();
4102 assert_eq!(cx.read(|cx| area.read(cx).zoomed_group()), Some(node));
4103 assert!(cx.read(|cx| group.read(cx).is_zoomed()));
4104 assert_eq!(
4105 zoom_signals(&log),
4106 vec![("Alpha", PanelSignal::Zoomed(true))]
4107 );
4108
4109 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_out(window, cx)));
4110 cx.run_until_parked();
4111
4112 assert!(!cx.read(|cx| area.read(cx).is_zoomed()));
4113 assert!(
4114 !cx.read(|cx| group.read(cx).is_zoomed()),
4115 "a group left flagged zoomed would stay locked and refuse every drop"
4116 );
4117 assert!(
4118 cx.read(|cx| group.read(cx).context(cx).is_droppable()),
4119 "and the lock the zoom imposed is lifted with it"
4120 );
4121 assert_eq!(
4122 zoom_signals(&log),
4123 vec![("Alpha", PanelSignal::Zoomed(false))],
4124 "the panel hears the zoom end too, not just the group"
4125 );
4126 }
4127
4128 #[gpui::test]
4131 fn a_group_that_refuses_to_zoom_leaves_the_area_unzoomed(cx: &mut TestAppContext) {
4132 let log = log_of();
4133 let (area, alpha, cx) = two_groups(&log, cx);
4134 cx.run_until_parked();
4135 cx.update(|_, cx| alpha.update(cx, |panel, cx| panel.set_zoomable(false, cx)));
4136
4137 let node = child_node(&area, 0, cx);
4138 let group = group_of(&area, 0, cx);
4139 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_in(node, window, cx)));
4140 cx.run_until_parked();
4141
4142 assert!(!cx.read(|cx| group.read(cx).is_zoomed()));
4143 assert!(
4144 !cx.read(|cx| area.read(cx).is_zoomed()),
4145 "the area must not fill itself with a group that never zoomed"
4146 );
4147 }
4148}