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 split_at(
586 &mut self,
587 node: NodeId,
588 panel: PanelId,
589 placement: Placement,
590 window: &mut Window,
591 cx: &mut Context<Self>,
592 ) {
593 let Some(region) = self.placement_of_node(node) else {
594 return;
595 };
596 self.adopt_measured_sizes(region, cx);
597 let Some(tree) = self.tree_mut(region) else {
598 return;
599 };
600 let result = tree.split(node, panel, placement, None);
601 self.commit(result, window, cx);
602 }
603
604 fn remove_panel_id(&mut self, panel: PanelId, window: &mut Window, cx: &mut Context<Self>) {
605 let Some(region) = self.placement_of_panel(panel) else {
606 return;
607 };
608 let Some(tree) = self.tree_mut(region) else {
609 return;
610 };
611 let result = tree.remove_panel(panel);
612 self.commit(result, window, cx);
613 }
614}
615
616impl DockArea {
626 pub fn set_zoomed_in(&mut self, node: NodeId, window: &mut Window, cx: &mut Context<Self>) {
632 self.set_zoom(Some(Zoomed::Group(node)), window, cx);
633 }
634
635 pub fn set_zoomed_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
641 self.set_zoom(None, window, cx);
642 }
643
644 pub fn is_zoomed(&self) -> bool {
645 self.zoomed.is_some()
646 }
647
648 pub fn zoomed_group(&self) -> Option<NodeId> {
650 match self.zoomed {
651 Some(Zoomed::Group(node)) => Some(node),
652 _ => None,
653 }
654 }
655
656 fn set_zoom(&mut self, zoomed: Option<Zoomed>, window: &mut Window, cx: &mut Context<Self>) {
664 if self.zoomed == zoomed {
665 return;
666 }
667
668 if let Some(previous) = self.zoomed {
669 self.drive_zoom(previous, false, window, cx);
670 }
671 let accepted = match zoomed {
672 Some(next) => self.drive_zoom(next, true, window, cx).then_some(next),
673 None => None,
674 };
675 self.zoomed = accepted;
676 cx.notify();
677 }
678
679 fn drive_zoom(
685 &mut self,
686 zoomed: Zoomed,
687 zoom_in: bool,
688 window: &mut Window,
689 cx: &mut Context<Self>,
690 ) -> bool {
691 match zoomed {
692 Zoomed::Group(node) => {
693 let Some(group) = self.groups.get(&node).map(|cached| cached.entity.clone()) else {
694 return false;
695 };
696 group.update(cx, |group, cx| {
697 group.set_zoomed(zoom_in, window, cx);
698 group.is_zoomed() == zoom_in
699 })
700 }
701 }
702 }
703
704 fn zoomed_view(&self) -> Option<AnyView> {
709 match self.zoomed? {
710 Zoomed::Group(node) => Some(self.groups.get(&node)?.entity.clone().into()),
711 }
712 }
713}
714
715impl DockArea {
717 pub fn load(
722 &mut self,
723 state: DockAreaState,
724 window: &mut Window,
725 cx: &mut Context<Self>,
726 ) -> Result<()> {
727 self.version = state.version;
728 self.zoomed = None;
729 self.groups.clear();
733 self.splits.clear();
734 self.docks.clear();
735 let dock_area = self.this.clone();
740 let renderer = self.renderer.clone();
741 let mut built = Vec::new();
742 self.center = {
743 let mut builder = RegistryPanelBuilder {
744 dock_area: dock_area.clone(),
745 renderer: renderer.clone(),
746 built: &mut built,
747 window,
748 cx,
749 };
750 PaneTree::from_state(&state.center, RootKind::Split, &mut builder)
751 };
752
753 for dock_state in [state.left_dock, state.right_dock, state.bottom_dock]
754 .into_iter()
755 .flatten()
756 {
757 let tree = {
758 let mut builder = RegistryPanelBuilder {
759 dock_area: dock_area.clone(),
760 renderer: renderer.clone(),
761 built: &mut built,
762 window,
763 cx,
764 };
765 PaneTree::from_state(dock_state.panel(), RootKind::Any, &mut builder)
766 };
767 let mut dock = Dock::new(dock_state.size());
768 dock.set_open(dock_state.open());
769 self.docks
770 .insert(dock_state.placement(), DockRegion { tree, dock });
771 }
772
773 self.panels.extend(built);
774 self.reconcile(window, cx);
775 cx.emit(DockEvent::LayoutChanged);
776 Ok(())
777 }
778
779 pub fn dump(&self, cx: &App) -> DockAreaState {
800 let source = LivePanels::new(&self.panels, cx);
801
802 DockAreaState {
803 version: self.version,
804 center: self.resolved_tree(&self.center, cx).to_state(&source),
805 left_dock: self.dump_dock(DockPlacement::Left, &source, cx),
806 right_dock: self.dump_dock(DockPlacement::Right, &source, cx),
807 bottom_dock: self.dump_dock(DockPlacement::Bottom, &source, cx),
808 }
809 }
810
811 fn dump_dock(
812 &self,
813 placement: DockPlacement,
814 source: &LivePanels<'_>,
815 cx: &App,
816 ) -> Option<DockState> {
817 let pane = self.docks.get(&placement)?;
818 Some(DockState::new(
819 self.resolved_tree(&pane.tree, cx).to_state(source),
820 placement,
821 pane.dock.size(),
822 pane.dock.is_open(),
823 ))
824 }
825
826 fn resolved_tree(&self, tree: &PaneTree, cx: &App) -> PaneTree {
827 let mut tree = tree.clone();
828 self.resolve_sizes(tree.root_mut(), cx);
829 tree
830 }
831
832 fn resolve_sizes(&self, node: &mut PaneNode, cx: &App) {
833 let measured = self
834 .splits
835 .get(&node.id())
836 .map(|cached| cached.entity.read(cx).sizes().clone())
837 .unwrap_or_default();
838
839 let NodeKind::Split {
840 children, sizes, ..
841 } = node.kind_mut()
842 else {
843 return;
844 };
845
846 for (ix, size) in sizes.iter_mut().enumerate() {
847 let on_screen = measured.get(ix).copied().filter(|size| *size > px(0.));
852 let stored = (*size).filter(|size| *size > px(0.));
853 *size = Some(on_screen.or(stored).unwrap_or(PANEL_MIN_SIZE));
854 }
855
856 for child in children.iter_mut() {
857 self.resolve_sizes(child, cx);
858 }
859 }
860}
861
862impl DockArea {
864 fn commit(&mut self, result: EditResult, window: &mut Window, cx: &mut Context<Self>) {
873 self.commit_changed(result.changed(), window, cx);
874 }
875
876 fn commit_changed(&mut self, changed: bool, window: &mut Window, cx: &mut Context<Self>) {
878 if !changed {
879 return;
880 }
881
882 self.reconcile(window, cx);
883 cx.emit(DockEvent::LayoutChanged);
884 }
885
886 fn reconcile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
893 let mut plans = Vec::new();
896 plan_tree(&self.center, false, self.locked, &mut plans);
897 for pane in self.docks.values() {
898 plan_tree(&pane.tree, !pane.dock.is_open(), self.locked, &mut plans);
899 }
900
901 let mut live_nodes = HashSet::with_capacity(plans.len());
905 let mut live_panels: HashSet<PanelId> = HashSet::new();
906
907 for plan in plans {
908 live_nodes.insert(plan.node());
909 match plan {
910 ContainerPlan::Split {
911 node,
912 axis,
913 children,
914 sizes,
915 } => {
916 let state = self.split_entity(node, cx);
917 let (previous, adopted) = self
918 .splits
919 .get(&node)
920 .map(|cached| (cached.children.clone(), cached.sizes.clone()))
921 .unwrap_or_default();
922 if previous != children || adopted != sizes {
930 state.update(cx, |state, cx| {
931 sync_split_panels(state, &previous, &children, &sizes, cx);
932 state.sync_panels_count(axis, children.len(), cx);
933 state.adopt_sizes(&scale_sizes_to(state.container_size(), &sizes), cx);
949 });
950 }
951 if let Some(cached) = self.splits.get_mut(&node) {
952 cached.children = children;
953 cached.sizes = sizes;
954 }
955 }
956 ContainerPlan::Group {
957 node,
958 panels,
959 active_ix,
960 constraints,
961 } => {
962 live_panels.extend(panels.iter().copied());
963 let views = self.views_of(&panels);
964 let group = self.group_entity(node, window, cx);
965 group.update(cx, |group, cx| {
966 group.set_constraints(constraints, window, cx);
970 group.sync_from_tree(views, active_ix, window, cx);
971 });
972 }
973 }
974 }
975
976 self.groups.retain(|node, _| live_nodes.contains(node));
977 self.splits.retain(|node, _| live_nodes.contains(node));
978
979 let departed: Vec<Arc<dyn PanelView>> = self
980 .panels
981 .iter()
982 .filter(|(panel, _)| !live_panels.contains(panel))
983 .map(|(_, view)| view.clone())
984 .collect();
985 self.panels.retain(|panel, _| live_panels.contains(panel));
986
987 let zoom_survives = match self.zoomed {
997 Some(Zoomed::Group(node)) => self.groups.contains_key(&node),
998 None => true,
999 };
1000 if !zoom_survives {
1001 self.set_zoom(None, window, cx);
1002 }
1003
1004 cx.notify();
1005 for view in departed {
1008 view.on_removed(window, cx);
1009 }
1010 }
1011
1012 fn views_of(&self, panels: &[PanelId]) -> Vec<Arc<dyn PanelView>> {
1014 debug_assert!(
1015 panels.iter().all(|panel| self.panels.contains_key(panel)),
1016 "every panel in a tree must have a live view; a missing one would \
1017 silently shift the group's active index"
1018 );
1019 panels
1020 .iter()
1021 .filter_map(|panel| self.panels.get(panel).cloned())
1022 .collect()
1023 }
1024
1025 fn group_entity(
1026 &mut self,
1027 node: NodeId,
1028 window: &mut Window,
1029 cx: &mut Context<Self>,
1030 ) -> Entity<TabGroup> {
1031 if let Some(cached) = self.groups.get(&node) {
1032 return cached.entity.clone();
1033 }
1034
1035 let renderer = self.renderer.tab_group_renderer();
1036 let entity = cx.new(|cx| TabGroup::new(node, window, cx).with_renderer(renderer));
1037 let subscription = cx.subscribe_in(&entity, window, Self::on_tab_group_event);
1038 self.groups.insert(
1039 node,
1040 Cached {
1041 entity: entity.clone(),
1042 _subscription: subscription,
1043 },
1044 );
1045 entity
1046 }
1047
1048 fn split_entity(&mut self, node: NodeId, cx: &mut Context<Self>) -> Entity<ResizableState> {
1049 if let Some(cached) = self.splits.get(&node) {
1050 return cached.entity.clone();
1051 }
1052
1053 let entity = cx.new(|_| ResizableState::default());
1054 let subscription =
1064 cx.subscribe(&entity, move |this, state, _: &ResizablePanelEvent, cx| {
1065 let sizes: Vec<Option<Pixels>> = state
1066 .read(cx)
1067 .sizes()
1068 .iter()
1069 .map(|size| Some(*size))
1070 .collect();
1071 let Some(region) = this.placement_of_node(node) else {
1072 return;
1073 };
1074 let Some(tree) = this.tree_mut(region) else {
1075 return;
1076 };
1077 if tree.set_sizes(node, sizes.clone()).changed() {
1078 cx.emit(DockEvent::LayoutChanged);
1079 }
1080 if let Some(cached) = this.splits.get_mut(&node) {
1083 cached.sizes = sizes;
1084 }
1085 });
1086 self.splits.insert(
1087 node,
1088 CachedSplit {
1089 entity: entity.clone(),
1090 children: Vec::new(),
1091 sizes: Vec::new(),
1092 _subscription: subscription,
1093 },
1094 );
1095 entity
1096 }
1097}
1098
1099impl DockArea {
1101 fn on_tab_group_event(
1102 &mut self,
1103 group: &Entity<TabGroup>,
1104 event: &TabGroupEvent,
1105 window: &mut Window,
1106 cx: &mut Context<Self>,
1107 ) {
1108 match event {
1109 TabGroupEvent::Drop { panel, target, .. } => {
1110 self.move_panel(*panel, *target, window, cx)
1111 }
1112 TabGroupEvent::DragDrop { item, target } => cx.emit(DockEvent::DragDrop {
1113 item: item.clone(),
1114 target: *target,
1115 }),
1116 TabGroupEvent::ClosePanel { panel } => self.remove_panel_id(*panel, window, cx),
1117 TabGroupEvent::ActiveChanged { ix } => {
1118 let node = group.read(cx).node();
1119 let Some(region) = self.placement_of_node(node) else {
1120 return;
1121 };
1122 let Some(tree) = self.tree_mut(region) else {
1123 return;
1124 };
1125 let result = tree.set_active(node, *ix);
1126 self.commit(result, window, cx);
1127 }
1128 TabGroupEvent::ZoomIn => {
1129 let node = group.read(cx).node();
1130 self.set_zoom(Some(Zoomed::Group(node)), window, cx);
1131 }
1132 TabGroupEvent::ZoomOut => {
1137 let node = group.read(cx).node();
1138 if self.zoomed == Some(Zoomed::Group(node)) {
1139 self.set_zoom(None, window, cx);
1140 }
1141 }
1142 }
1143 }
1144}
1145
1146impl DockArea {
1148 fn adopt_measured_sizes(&mut self, placement: DockPlacement, cx: &App) {
1155 let measured: HashMap<NodeId, Vec<Pixels>> = self
1156 .splits
1157 .iter()
1158 .filter(|(_, cached)| cached.entity.read(cx).container_size() > Pixels::ZERO)
1164 .map(|(node, cached)| (*node, cached.entity.read(cx).sizes().clone()))
1165 .collect();
1166
1167 if let Some(tree) = self.tree_mut(placement) {
1168 tree.adopt_measured_sizes(&measured);
1169 }
1170 }
1171
1172 fn tree_mut(&mut self, placement: DockPlacement) -> Option<&mut PaneTree> {
1173 match placement {
1174 DockPlacement::Center => Some(&mut self.center),
1175 _ => self.docks.get_mut(&placement).map(|pane| &mut pane.tree),
1176 }
1177 }
1178
1179 fn placement_of_node(&self, node: NodeId) -> Option<DockPlacement> {
1182 if self.center.find_node(node).is_some() {
1183 return Some(DockPlacement::Center);
1184 }
1185 self.docks
1186 .iter()
1187 .find(|(_, pane)| pane.tree.find_node(node).is_some())
1188 .map(|(placement, _)| *placement)
1189 }
1190
1191 fn placement_of_panel(&self, panel: PanelId) -> Option<DockPlacement> {
1192 if self.center.find_panel_node(panel).is_some() {
1193 return Some(DockPlacement::Center);
1194 }
1195 self.docks
1196 .iter()
1197 .find(|(_, pane)| pane.tree.find_panel_node(panel).is_some())
1198 .map(|(placement, _)| *placement)
1199 }
1200
1201 fn resize_dock(
1209 &mut self,
1210 placement: DockPlacement,
1211 pointer: Point<Pixels>,
1212 window: &mut Window,
1213 cx: &mut Context<Self>,
1214 ) {
1215 let opposite = match placement {
1216 DockPlacement::Left => self.dock_size(DockPlacement::Right),
1217 DockPlacement::Right => self.dock_size(DockPlacement::Left),
1218 _ => None,
1219 };
1220 let sizing = DockSizing::new(placement)
1221 .with_area_bounds(self.bounds)
1222 .with_opposite_dock_size(opposite.unwrap_or(px(0.)));
1223 let size = sizing
1224 .size_from_pointer(pointer)
1225 .min(sizing.clamp(Pixels::MAX));
1226
1227 let Some(pane) = self.docks.get_mut(&placement) else {
1228 return;
1229 };
1230 let was_open = pane.dock.is_open();
1231 if placement == DockPlacement::Bottom && pane.dock.is_collapsible() && size < PANEL_MIN_SIZE
1232 {
1233 pane.dock.set_open(size > CLOSED_BOTTOM_STRIP);
1234 pane.dock.set_live_size(Some(size.max(CLOSED_BOTTOM_STRIP)));
1235 } else {
1236 pane.dock.set_open(true);
1237 pane.dock.set_live_size(None);
1238 pane.dock.set_size(size);
1239 }
1240 if pane.dock.is_open() != was_open {
1241 self.reconcile(window, cx);
1242 }
1243 cx.notify();
1244 }
1245
1246 fn end_dock_resize(
1249 &mut self,
1250 placement: DockPlacement,
1251 window: &mut Window,
1252 cx: &mut Context<Self>,
1253 ) {
1254 let Some(pane) = self.docks.get_mut(&placement) else {
1255 return;
1256 };
1257 let Some(size) = pane.dock.live_size() else {
1258 return;
1259 };
1260 pane.dock.set_live_size(None);
1261
1262 let open = size >= (CLOSED_BOTTOM_STRIP + PANEL_MIN_SIZE) / 2.;
1263 if open {
1264 pane.dock.set_size(PANEL_MIN_SIZE);
1265 }
1266 if pane.dock.is_open() != open {
1267 pane.dock.set_open(open);
1268 self.reconcile(window, cx);
1269 }
1270 cx.notify();
1271 }
1272}
1273
1274impl DockArea {
1276 fn render_node(&self, node: &PaneNode, window: &mut Window, cx: &mut App) -> AnyElement {
1278 match node.kind() {
1279 PaneRef::Split {
1280 axis,
1281 children,
1282 sizes,
1283 } => {
1284 let group = match axis {
1285 Axis::Horizontal => h_resizable(("dock-split", node.id().as_u64())),
1286 Axis::Vertical => v_resizable(("dock-split", node.id().as_u64())),
1287 };
1288 let shown: Vec<bool> = children
1292 .iter()
1293 .map(|child| self.is_node_visible(child, cx))
1294 .collect();
1295 let grows = shown.iter().rposition(|shown| *shown);
1301 let panels: Vec<_> = children
1302 .iter()
1303 .zip(sizes.iter())
1304 .enumerate()
1305 .map(|(ix, (child, size))| {
1306 resizable_panel()
1307 .visible(shown[ix])
1308 .child(self.render_node(child, window, cx))
1309 .when_some(*size, |panel, size| {
1321 panel
1322 .size(size)
1323 .when(Some(ix) != grows, |panel| panel.flex_none())
1324 })
1325 })
1326 .collect();
1327
1328 let group = group
1329 .when_some(self.splits.get(&node.id()), |group, cached| {
1330 group.with_state(&cached.entity)
1331 })
1332 .with_handle_appearance({
1333 let renderer = self.renderer.clone();
1334 Rc::new(move |handle, window, cx| {
1335 renderer.render_split_handle(handle, window, cx)
1336 })
1337 })
1338 .children(panels);
1339
1340 self.renderer
1341 .split_frame(node.id(), axis, window, cx)
1342 .size_full()
1350 .flex_1()
1351 .min_h(px(0.))
1352 .overflow_hidden()
1353 .child(group)
1354 .into_any_element()
1355 }
1356 PaneRef::Tabs { .. } => match self.groups.get(&node.id()) {
1357 Some(cached) => cached.entity.clone().into_any_element(),
1358 None => Empty.into_any_element(),
1359 },
1360 }
1361 }
1362
1363 fn is_node_visible(&self, node: &PaneNode, cx: &App) -> bool {
1369 let panels = LivePanels::new(&self.panels, cx);
1370 match node.kind() {
1371 PaneRef::Split { children, .. } => {
1372 children.iter().any(|child| self.is_node_visible(child, cx))
1373 }
1374 PaneRef::Tabs { panels: ids, .. } => ids.iter().any(|panel| panels.is_visible(*panel)),
1375 }
1376 }
1377
1378 fn render_dock(
1379 &self,
1380 placement: DockPlacement,
1381 window: &mut Window,
1382 cx: &mut App,
1383 ) -> Option<AnyElement> {
1384 let pane = self.docks.get(&placement)?;
1385 let dock = self.dock_context(placement, &pane.dock);
1386
1387 let size = dock_extent(&dock);
1392 if size <= px(0.) {
1393 return Some(div().into_any_element());
1394 }
1395
1396 let content = self.render_node(pane.tree.root(), window, cx);
1397 let chrome = self.renderer.render_dock(&dock, content, window, cx);
1406 Some(dock_frame(&dock, size).child(chrome).into_any_element())
1407 }
1408
1409 fn dock_context(&self, placement: DockPlacement, dock: &Dock) -> DockContext {
1410 let area = self.this.clone();
1411
1412 DockContext {
1413 placement,
1414 size: dock.live_size().unwrap_or(dock.size()),
1415 open: dock.is_open(),
1416 collapsible: dock.is_collapsible(),
1417 on_toggle: {
1418 let area = area.clone();
1419 Rc::new(move |window, cx| {
1420 _ = area.update(cx, |area, cx| area.toggle_dock(placement, window, cx));
1421 })
1422 },
1423 on_resize: {
1424 let area = area.clone();
1425 Rc::new(move |pointer, window, cx| {
1426 _ = area.update(cx, |area, cx| {
1427 area.resize_dock(placement, pointer, window, cx)
1428 });
1429 })
1430 },
1431 on_resize_end: Rc::new(move |window, cx| {
1432 _ = area.update(cx, |area, cx| area.end_dock_resize(placement, window, cx));
1433 }),
1434 }
1435 }
1436}
1437
1438impl EventEmitter<DockEvent> for DockArea {}
1439
1440impl Focusable for DockArea {
1441 fn focus_handle(&self, _: &App) -> FocusHandle {
1442 self.focus_handle.clone()
1443 }
1444}
1445
1446impl Render for DockArea {
1447 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1448 let area = cx.entity();
1449 let renderer = self.renderer.clone();
1450
1451 renderer
1452 .frame(window, cx)
1453 .test_support()
1454 .relative()
1461 .size_full()
1462 .overflow_hidden()
1463 .flex()
1464 .flex_row()
1465 .on_prepaint(move |bounds, _, cx| {
1466 area.update(cx, |area, _| area.bounds = bounds);
1467 })
1468 .track_focus(&self.focus_handle)
1469 .map(|frame| match self.zoomed_view() {
1470 Some(view) => frame.child(view),
1471 None => frame
1472 .when_some(
1473 self.render_dock(DockPlacement::Left, window, cx),
1474 ParentElement::child,
1475 )
1476 .child(
1477 renderer
1478 .center_frame(window, cx)
1479 .flex()
1484 .flex_1()
1485 .flex_col()
1486 .overflow_hidden()
1487 .child(self.render_node(self.center.root(), window, cx))
1488 .when_some(
1489 self.render_dock(DockPlacement::Bottom, window, cx),
1490 ParentElement::child,
1491 ),
1492 )
1493 .when_some(
1494 self.render_dock(DockPlacement::Right, window, cx),
1495 ParentElement::child,
1496 ),
1497 })
1498 }
1499}
1500
1501enum ContainerPlan {
1503 Split {
1504 node: NodeId,
1505 axis: Axis,
1506 children: Vec<NodeId>,
1507 sizes: Vec<Option<Pixels>>,
1508 },
1509 Group {
1510 node: NodeId,
1511 panels: Vec<PanelId>,
1512 active_ix: usize,
1513 constraints: TabGroupConstraints,
1514 },
1515}
1516
1517impl ContainerPlan {
1518 fn node(&self) -> NodeId {
1519 match self {
1520 Self::Split { node, .. } | Self::Group { node, .. } => *node,
1521 }
1522 }
1523}
1524
1525fn scale_sizes_to(container: Pixels, sizes: &[Option<Pixels>]) -> Vec<Option<Pixels>> {
1532 let total: f32 = sizes.iter().flatten().map(|size| size.as_f32()).sum();
1533 if container <= px(0.) || total <= 0. || sizes.iter().any(Option::is_none) {
1534 return sizes.to_vec();
1535 }
1536
1537 let scale = container.as_f32() / total;
1538 sizes
1539 .iter()
1540 .map(|size| size.map(|size| px(size.as_f32() * scale)))
1541 .collect()
1542}
1543
1544fn sync_split_panels(
1554 state: &mut ResizableState,
1555 previous: &[NodeId],
1556 next: &[NodeId],
1557 sizes: &[Option<Pixels>],
1558 cx: &mut Context<ResizableState>,
1559) {
1560 let mut current = previous.to_vec();
1561
1562 for ix in (0..current.len()).rev() {
1564 if !next.contains(¤t[ix]) {
1565 if ix < state.sizes().len() {
1566 state.remove_panel(ix, cx);
1567 }
1568 current.remove(ix);
1569 }
1570 }
1571
1572 for (ix, node) in next.iter().enumerate() {
1573 if current.get(ix) == Some(node) {
1574 continue;
1575 }
1576 let at = ix.min(state.sizes().len());
1577 state.insert_panel(sizes.get(ix).copied().flatten(), Some(at), cx);
1582 current.insert(at.min(current.len()), *node);
1583 }
1584
1585 debug_assert_eq!(
1586 current, next,
1587 "the split's panel list must end up mirroring its children exactly; \
1588 a reordering edit would need its own case here"
1589 );
1590}
1591
1592fn plan_tree(tree: &PaneTree, collapsed: bool, locked: bool, out: &mut Vec<ContainerPlan>) {
1593 plan_node(tree.root(), true, collapsed, locked, out);
1595}
1596
1597fn plan_node(
1598 node: &PaneNode,
1599 alone: bool,
1600 collapsed: bool,
1601 locked: bool,
1602 out: &mut Vec<ContainerPlan>,
1603) {
1604 match node.kind() {
1605 PaneRef::Split {
1606 axis,
1607 children,
1608 sizes,
1609 } => {
1610 out.push(ContainerPlan::Split {
1611 node: node.id(),
1612 axis,
1613 children: children.iter().map(PaneNode::id).collect(),
1614 sizes: sizes.to_vec(),
1615 });
1616 let children_alone = children.len() <= 1;
1617 for child in children {
1618 plan_node(child, children_alone, collapsed, locked, out);
1619 }
1620 }
1621 PaneRef::Tabs { panels, active_ix } => out.push(ContainerPlan::Group {
1622 node: node.id(),
1623 panels: panels.to_vec(),
1624 active_ix,
1625 constraints: TabGroupConstraints::in_split(alone)
1626 .dock_locked(locked)
1627 .collapsed(collapsed),
1628 }),
1629 }
1630}
1631
1632fn first_tab_group(node: &PaneNode) -> Option<NodeId> {
1633 match node.kind() {
1634 PaneRef::Tabs { .. } => Some(node.id()),
1635 PaneRef::Split { children, .. } => children.iter().find_map(first_tab_group),
1636 }
1637}
1638
1639fn target_node(target: &InsertTarget) -> NodeId {
1640 match target {
1641 InsertTarget::Tabs { node, .. } | InsertTarget::Split { node, .. } => *node,
1642 }
1643}
1644
1645struct RegistryPanelBuilder<'a, 'w, 'c> {
1647 dock_area: WeakEntity<DockArea>,
1648 renderer: Rc<dyn DockAreaRenderer>,
1649 built: &'a mut Vec<(PanelId, Arc<dyn PanelView>)>,
1650 window: &'w mut Window,
1651 cx: &'c mut App,
1652}
1653
1654impl PanelBuilder for RegistryPanelBuilder<'_, '_, '_> {
1655 fn build(&mut self, state: &PanelState, info: &PanelInfo) -> PanelId {
1656 let context = PanelBuildContext::new(self.dock_area.clone(), state, info);
1657 let view =
1658 match PanelRegistry::build_panel(&state.panel_name, context, self.window, self.cx) {
1659 Some(view) => view,
1660 None => self
1661 .renderer
1662 .build_placeholder(state, self.window, self.cx)
1663 .unwrap_or_else(|| {
1664 Arc::new(self.cx.new(|cx| PlaceholderPanel::new(state.clone(), cx)))
1665 as Arc<dyn PanelView>
1666 }),
1667 };
1668
1669 let id = view.panel_id(self.cx);
1670 self.built.push((id, view));
1671 id
1672 }
1673}
1674
1675struct PlaceholderPanel {
1682 state: PanelState,
1683 focus_handle: FocusHandle,
1684}
1685
1686impl PlaceholderPanel {
1687 fn new(state: PanelState, cx: &mut Context<Self>) -> Self {
1688 Self {
1689 state,
1690 focus_handle: cx.focus_handle(),
1691 }
1692 }
1693}
1694
1695impl Panel for PlaceholderPanel {
1696 fn panel_name(&self) -> &'static str {
1697 "InvalidPanel"
1698 }
1699
1700 fn dump(&self, _: &App) -> PanelState {
1701 self.state.clone()
1702 }
1703}
1704
1705impl EventEmitter<PanelEvent> for PlaceholderPanel {}
1706
1707impl Focusable for PlaceholderPanel {
1708 fn focus_handle(&self, _: &App) -> FocusHandle {
1709 self.focus_handle.clone()
1710 }
1711}
1712
1713impl Render for PlaceholderPanel {
1714 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1715 Empty
1716 }
1717}
1718
1719type DockToggleHandler = Rc<dyn Fn(&mut Window, &mut App)>;
1720type DockResizeHandler = Rc<dyn Fn(Point<Pixels>, &mut Window, &mut App)>;
1721
1722#[derive(Clone)]
1725pub struct DockContext {
1726 placement: DockPlacement,
1727 size: Pixels,
1728 open: bool,
1729 collapsible: bool,
1730 on_toggle: DockToggleHandler,
1731 on_resize: DockResizeHandler,
1732 on_resize_end: DockToggleHandler,
1733}
1734
1735impl DockContext {
1736 pub fn placement(&self) -> DockPlacement {
1737 self.placement
1738 }
1739
1740 pub fn size(&self) -> Pixels {
1743 self.size
1744 }
1745
1746 pub fn is_open(&self) -> bool {
1747 self.open
1748 }
1749
1750 pub fn is_collapsible(&self) -> bool {
1751 self.collapsible
1752 }
1753
1754 pub fn toggle(&self, window: &mut Window, cx: &mut App) {
1755 (self.on_toggle)(window, cx);
1756 }
1757
1758 pub fn resize_to(&self, pointer: Point<Pixels>, window: &mut Window, cx: &mut App) {
1761 (self.on_resize)(pointer, window, cx);
1762 }
1763
1764 pub fn end_resize(&self, window: &mut Window, cx: &mut App) {
1767 (self.on_resize_end)(window, cx);
1768 }
1769}
1770
1771pub const CLOSED_BOTTOM_STRIP: Pixels = px(29.);
1775
1776pub fn dock_extent(dock: &DockContext) -> Pixels {
1778 match (dock.is_open(), dock.placement()) {
1779 (true, _) => dock.size(),
1780 (false, DockPlacement::Bottom) => CLOSED_BOTTOM_STRIP,
1781 (false, _) => px(0.),
1782 }
1783}
1784
1785pub fn dock_frame(dock: &DockContext, size: Pixels) -> Div {
1791 div()
1792 .flex()
1793 .flex_none()
1794 .relative()
1795 .overflow_hidden()
1796 .map(|this| match dock.placement() {
1797 DockPlacement::Left | DockPlacement::Right => this.flex_row().h_full().w(size),
1798 DockPlacement::Bottom => this.w_full().h(size),
1799 DockPlacement::Center => this,
1801 })
1802}
1803
1804#[allow(unused_variables)]
1815pub trait DockAreaRenderer: 'static {
1816 fn frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
1822 div().id("dock-area")
1823 }
1824
1825 fn split_frame(
1835 &self,
1836 node: NodeId,
1837 axis: Axis,
1838 window: &mut Window,
1839 cx: &mut App,
1840 ) -> Stateful<Div> {
1841 div().id(("dock-split-frame", node.as_u64()))
1842 }
1843
1844 fn center_frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
1848 div().id("dock-area-center")
1849 }
1850
1851 fn render_split_handle(
1858 &self,
1859 handle: &ResizeHandleContext,
1860 window: &mut Window,
1861 cx: &mut App,
1862 ) -> Option<AnyElement> {
1863 None
1864 }
1865
1866 fn render_dock(
1875 &self,
1876 dock: &DockContext,
1877 content: AnyElement,
1878 window: &mut Window,
1879 cx: &mut App,
1880 ) -> AnyElement {
1881 content
1882 }
1883
1884 fn build_placeholder(
1899 &self,
1900 state: &PanelState,
1901 window: &mut Window,
1902 cx: &mut App,
1903 ) -> Option<Arc<dyn PanelView>> {
1904 None
1905 }
1906
1907 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer>;
1908}
1909
1910struct BareDockArea;
1912
1913impl DockAreaRenderer for BareDockArea {
1914 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
1915 Rc::new(BareTabGroup)
1916 }
1917}
1918
1919#[cfg(test)]
1920impl DockArea {
1921 pub(crate) fn container_entity_ids(&self) -> Vec<(NodeId, gpui::EntityId)> {
1928 let mut ids: Vec<(NodeId, gpui::EntityId)> = self
1929 .groups
1930 .iter()
1931 .map(|(node, cached)| (*node, cached.entity.entity_id()))
1932 .chain(
1933 self.splits
1934 .iter()
1935 .map(|(node, cached)| (*node, cached.entity.entity_id())),
1936 )
1937 .collect();
1938 ids.sort();
1939 ids
1940 }
1941}
1942
1943#[cfg(test)]
1944mod tests {
1945 use gpui::{TestAppContext, VisualTestContext};
1946
1947 use std::{
1948 cell::{Cell, RefCell},
1949 rc::Rc,
1950 };
1951
1952 use super::*;
1953 use crate::dock::TabGroupContext;
1954 use crate::dock::test_support::{Log, PanelSignal, TestPanel, drain, drain_active, log_of};
1955
1956 #[test]
1959 fn slot_sizes_are_re_expressed_as_shares_of_the_container() {
1960 let scaled = scale_sizes_to(px(800.), &[Some(px(300.)), Some(px(100.))]);
1961
1962 assert_eq!(scaled, vec![Some(px(600.)), Some(px(200.))]);
1963 }
1964
1965 #[test]
1968 fn an_unconstrained_slot_leaves_every_size_alone() {
1969 let sizes = [Some(px(300.)), None];
1970
1971 assert_eq!(scale_sizes_to(px(800.), &sizes), sizes.to_vec());
1972 }
1973
1974 #[test]
1977 fn an_unusable_container_or_total_leaves_every_size_alone() {
1978 let sizes = [Some(px(300.)), Some(px(100.))];
1979 assert_eq!(scale_sizes_to(px(0.), &sizes), sizes.to_vec());
1980
1981 let zeroed = [Some(px(0.)), Some(px(0.))];
1982 assert_eq!(scale_sizes_to(px(800.), &zeroed), zeroed.to_vec());
1983 }
1984
1985 fn setup(cx: &mut TestAppContext) -> (Entity<DockArea>, &mut VisualTestContext) {
1986 cx.update(|cx| {
1987 let _ = crate::Theme::global_mut(cx);
1988 });
1989 cx.add_window_view(|window, cx| DockArea::new("test-dock", None, window, cx))
1990 }
1991
1992 #[gpui::test]
1993 fn dock_size_change_emits_one_layout_event(cx: &mut TestAppContext) {
1994 let (area, cx) = setup(cx);
1995 cx.update(|window, cx| {
1996 area.update(cx, |area, cx| {
1997 area.set_dock(
1998 DockPlacement::Left,
1999 DockLayout::tabs().panel(TestPanel::new("Left", cx)),
2000 window,
2001 cx,
2002 );
2003 });
2004 });
2005
2006 let events = Rc::new(Cell::new(0));
2007 let observed = events.clone();
2008 let _subscription = cx.update(|window, cx| {
2009 window.subscribe(&area, cx, move |_, event: &DockEvent, _, _| {
2010 if matches!(event, DockEvent::LayoutChanged) {
2011 observed.set(observed.get() + 1);
2012 }
2013 })
2014 });
2015
2016 cx.update(|window, cx| {
2017 area.update(cx, |area, cx| {
2018 area.set_dock_size(DockPlacement::Left, px(320.), window, cx);
2019 area.set_dock_size(DockPlacement::Left, px(320.), window, cx);
2020 });
2021 });
2022 assert_eq!(
2023 events.get(),
2024 1,
2025 "only an effective size change is persisted"
2026 );
2027 }
2028
2029 fn two_groups<'a>(
2031 log: &Log,
2032 cx: &'a mut TestAppContext,
2033 ) -> (
2034 Entity<DockArea>,
2035 Entity<TestPanel>,
2036 &'a mut VisualTestContext,
2037 ) {
2038 let (area, cx) = setup(cx);
2039 let log = log.clone();
2040 let alpha = cx.update(|window, cx| {
2041 let alpha = TestPanel::logging("Alpha", &log, cx);
2042 let beta = TestPanel::logging("Beta", &log, cx);
2043 area.update(cx, |area, cx| {
2044 area.set_center(
2045 DockLayout::h_split()
2046 .child(DockLayout::tabs().panel(alpha.clone()), None)
2047 .child(DockLayout::tabs().panel(beta), None),
2048 window,
2049 cx,
2050 );
2051 });
2052 alpha
2053 });
2054 (area, alpha, cx)
2055 }
2056
2057 fn child_node(area: &Entity<DockArea>, ix: usize, cx: &mut VisualTestContext) -> NodeId {
2059 cx.read(|cx| {
2060 let PaneRef::Split { children, .. } = area
2061 .read(cx)
2062 .layout(DockPlacement::Center)
2063 .unwrap()
2064 .root()
2065 .kind()
2066 else {
2067 panic!("the center root is a split");
2068 };
2069 children[ix].id()
2070 })
2071 }
2072
2073 fn panel_id_of(panel: &Entity<TestPanel>) -> PanelId {
2074 PanelId::from(panel.entity_id())
2075 }
2076
2077 fn move_alpha_into_the_other_group(
2078 area: &Entity<DockArea>,
2079 alpha: &Entity<TestPanel>,
2080 cx: &mut VisualTestContext,
2081 ) {
2082 let target = child_node(area, 1, cx);
2083 let alpha_id = panel_id_of(alpha);
2084 cx.update(|window, cx| {
2085 area.update(cx, |area, cx| {
2086 area.move_panel(
2087 alpha_id,
2088 InsertTarget::Tabs {
2089 node: target,
2090 ix: None,
2091 activate: true,
2092 },
2093 window,
2094 cx,
2095 );
2096 });
2097 });
2098 cx.run_until_parked();
2099 }
2100
2101 fn collect_sizes(state: &PanelState, out: &mut Vec<Pixels>) {
2102 if let PanelInfo::Stack { sizes, .. } = &state.info {
2103 out.extend(sizes.iter().copied());
2104 }
2105 for child in &state.children {
2106 collect_sizes(child, out);
2107 }
2108 }
2109
2110 fn register_test_panels(cx: &mut App) {
2111 for name in ["Alpha", "Beta", "Gamma"] {
2112 crate::dock::registry::register_panel(cx, name, move |_, _, cx| {
2113 Arc::new(TestPanel::new(name, cx)) as Arc<dyn PanelView>
2114 });
2115 }
2116 }
2117
2118 fn one_group<'a>(
2124 log: &Log,
2125 names: &[&'static str],
2126 active_ix: Option<usize>,
2127 cx: &'a mut TestAppContext,
2128 ) -> (
2129 Entity<DockArea>,
2130 Vec<Entity<TestPanel>>,
2131 &'a mut VisualTestContext,
2132 ) {
2133 let (area, cx) = setup(cx);
2134 let log = log.clone();
2135 let names = names.to_vec();
2136 let panels = cx.update(|window, cx| {
2137 let panels: Vec<_> = names
2138 .iter()
2139 .map(|name| TestPanel::logging(name, &log, cx))
2140 .collect();
2141 let layout = panels
2142 .iter()
2143 .fold(DockLayout::tabs(), |layout, panel| {
2144 layout.panel(panel.clone())
2145 })
2146 .active_index(active_ix.unwrap_or(0));
2147 area.update(cx, |area, cx| area.set_center(layout, window, cx));
2148 panels
2149 });
2150 (area, panels, cx)
2151 }
2152
2153 fn group_of(
2155 area: &Entity<DockArea>,
2156 ix: usize,
2157 cx: &mut VisualTestContext,
2158 ) -> Entity<TabGroup> {
2159 let node = child_node(area, ix, cx);
2160 cx.read(|cx| area.read(cx).groups.get(&node).unwrap().entity.clone())
2161 }
2162
2163 fn move_panel_into(
2164 area: &Entity<DockArea>,
2165 panel: PanelId,
2166 node: NodeId,
2167 ix: Option<usize>,
2168 activate: bool,
2169 cx: &mut VisualTestContext,
2170 ) {
2171 cx.update(|window, cx| {
2172 area.update(cx, |area, cx| {
2173 area.move_panel(panel, InsertTarget::Tabs { node, ix, activate }, window, cx);
2174 });
2175 });
2176 cx.run_until_parked();
2177 }
2178
2179 fn is_center_empty(area: &Entity<DockArea>, cx: &mut VisualTestContext) -> bool {
2180 cx.read(|cx| area.read(cx).is_empty(DockPlacement::Center, cx))
2181 }
2182
2183 #[gpui::test]
2184 fn a_layout_installs_and_dumps_back_to_the_same_state(cx: &mut TestAppContext) {
2185 let (area, cx) = setup(cx);
2186 cx.update(|window, cx| {
2187 let alpha = TestPanel::new("Alpha", cx);
2188 let beta = TestPanel::new("Beta", cx);
2189 area.update(cx, |area, cx| {
2190 area.set_center(
2191 DockLayout::h_split()
2192 .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2193 .child(DockLayout::tabs().panel(beta), None),
2194 window,
2195 cx,
2196 );
2197 });
2198 });
2199
2200 let state = cx.read(|cx| area.read(cx).dump(cx));
2201 assert_eq!(state.center.panel_name, "StackPanel");
2202 assert_eq!(state.center.children.len(), 2);
2203 assert_eq!(state.center.children[0].children[0].panel_name, "Alpha");
2204 assert_eq!(state.center.children[1].children[0].panel_name, "Beta");
2205 }
2206
2207 #[gpui::test]
2208 fn moving_a_panel_reuses_its_entity(cx: &mut TestAppContext) {
2209 let log = log_of();
2210 let (area, alpha, cx) = two_groups(&log, cx);
2211 cx.run_until_parked();
2212 drain(&log);
2213
2214 let destination = child_node(&area, 1, cx);
2215 let destination_entity = cx.read(|cx| {
2216 area.read(cx)
2217 .groups
2218 .get(&destination)
2219 .unwrap()
2220 .entity
2221 .entity_id()
2222 });
2223
2224 move_alpha_into_the_other_group(&area, &alpha, cx);
2225
2226 assert_eq!(
2227 cx.read(|cx| area
2228 .read(cx)
2229 .groups
2230 .get(&destination)
2231 .unwrap()
2232 .entity
2233 .entity_id()),
2234 destination_entity,
2235 "the group the panel arrived in was reused, not rebuilt"
2236 );
2237
2238 let alpha_id = panel_id_of(&alpha);
2243 assert!(
2244 cx.read(|cx| area
2245 .read(cx)
2246 .layout(DockPlacement::Center)
2247 .unwrap()
2248 .find_panel_node(alpha_id))
2249 .is_some(),
2250 "the moved panel is still in the tree"
2251 );
2252
2253 let state = cx.read(|cx| area.read(cx).dump(cx));
2254 assert_eq!(
2258 state.center.children.len(),
2259 1,
2260 "the emptied group collapsed out of the split"
2261 );
2262 assert_eq!(
2263 state.center.children[0].children.len(),
2264 2,
2265 "both panels now share the surviving group"
2266 );
2267 }
2268
2269 #[gpui::test]
2270 fn a_moved_panel_is_not_told_it_was_removed(cx: &mut TestAppContext) {
2271 let log = log_of();
2272 let (area, alpha, cx) = two_groups(&log, cx);
2273 cx.run_until_parked();
2274 drain(&log);
2275
2276 move_alpha_into_the_other_group(&area, &alpha, cx);
2277
2278 assert!(
2279 !drain(&log).contains(&("Alpha", PanelSignal::Removed)),
2280 "moving a panel between groups must never deliver on_removed"
2281 );
2282 }
2283
2284 #[gpui::test]
2285 fn removing_a_panel_does_tell_it_it_was_removed(cx: &mut TestAppContext) {
2286 let log = log_of();
2290 let (area, alpha, cx) = two_groups(&log, cx);
2291 cx.run_until_parked();
2292 drain(&log);
2293
2294 cx.update(|window, cx| {
2295 area.update(cx, |area, cx| area.remove_panel(alpha.clone(), window, cx));
2296 });
2297 cx.run_until_parked();
2298
2299 assert!(
2300 drain(&log).contains(&("Alpha", PanelSignal::Removed)),
2301 "a genuine removal must deliver on_removed"
2302 );
2303 }
2304
2305 #[gpui::test]
2306 fn reconciling_an_unchanged_tree_creates_no_entities(cx: &mut TestAppContext) {
2307 let log = log_of();
2308 let (area, _alpha, cx) = two_groups(&log, cx);
2309 cx.run_until_parked();
2310 drain(&log);
2311
2312 let before = cx.read(|cx| area.read(cx).container_entity_ids());
2313 cx.update(|window, cx| area.update(cx, |area, cx| area.reconcile(window, cx)));
2314 let after = cx.read(|cx| area.read(cx).container_entity_ids());
2315
2316 assert!(!before.is_empty(), "there were containers to preserve");
2317 assert_eq!(
2318 before, after,
2319 "a steady-state pass creates and drops nothing"
2320 );
2321 cx.run_until_parked();
2322 assert_eq!(
2323 drain(&log),
2324 vec![],
2325 "and no panel was re-added or re-activated by it"
2326 );
2327 }
2328
2329 #[gpui::test]
2330 fn a_loaded_layout_round_trips_through_dump(cx: &mut TestAppContext) {
2331 let (area, cx) = setup(cx);
2332 cx.update(|_, cx| register_test_panels(cx));
2333
2334 let json = include_str!("fixtures/nested_splits.json");
2335 let state: DockAreaState = serde_json::from_str(json).unwrap();
2336
2337 cx.update(|window, cx| {
2338 area.update(cx, |area, cx| area.load(state.clone(), window, cx).unwrap())
2339 });
2340 let dumped = cx.read(|cx| area.read(cx).dump(cx));
2341
2342 cx.update(|window, cx| {
2343 area.update(cx, |area, cx| {
2344 area.load(dumped.clone(), window, cx).unwrap()
2345 })
2346 });
2347 let again = cx.read(|cx| area.read(cx).dump(cx));
2348
2349 assert_eq!(dumped, again, "load/dump must reach a fixpoint");
2350 assert_eq!(
2351 dumped.center.children.len(),
2352 3,
2353 "the fixture's nesting is flattened, as the state layer already pins"
2354 );
2355 assert_eq!(dumped.center.children[0].children[0].panel_name, "Alpha");
2356 }
2357
2358 #[gpui::test]
2359 fn a_dumped_live_layout_has_no_zero_sizes(cx: &mut TestAppContext) {
2360 let (area, cx) = setup(cx);
2361 cx.update(|window, cx| {
2362 let alpha = TestPanel::new("Alpha", cx);
2363 let beta = TestPanel::new("Beta", cx);
2364 let gamma = TestPanel::new("Gamma", cx);
2365 area.update(cx, |area, cx| {
2366 area.set_center(
2367 DockLayout::v_split()
2368 .child(DockLayout::tabs().panel(alpha), None)
2371 .child(DockLayout::tabs().panel(beta), Some(px(0.)))
2374 .child(DockLayout::tabs().panel(gamma), Some(px(240.))),
2375 window,
2376 cx,
2377 );
2378 });
2379 });
2380
2381 let state = cx.read(|cx| area.read(cx).dump(cx));
2382 let mut sizes = Vec::new();
2383 collect_sizes(&state.center, &mut sizes);
2384
2385 assert!(!sizes.is_empty(), "the layout has slots to check");
2386 assert!(
2387 sizes.iter().all(|size| *size > px(0.)),
2388 "an older build reads a persisted 0.0 back as a real zero-pixel panel: {sizes:?}"
2389 );
2390 }
2391
2392 #[gpui::test]
2399 fn a_dumped_split_writes_the_sizes_it_is_actually_drawn_at(cx: &mut TestAppContext) {
2400 let (area, cx) = setup(cx);
2401 cx.update(|window, cx| {
2402 let alpha = TestPanel::new("Alpha", cx);
2403 let beta = TestPanel::new("Beta", cx);
2404 area.update(cx, |area, cx| {
2405 area.set_center(
2406 DockLayout::h_split()
2407 .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2408 .child(DockLayout::tabs().panel(beta), Some(px(300.))),
2409 window,
2410 cx,
2411 );
2412 });
2413 });
2414 cx.run_until_parked();
2415
2416 let root = cx.read(|cx| {
2417 area.read(cx)
2418 .layout(DockPlacement::Center)
2419 .unwrap()
2420 .root()
2421 .id()
2422 });
2423 let measured = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2424 assert_ne!(
2425 measured,
2426 vec![px(300.), px(300.)],
2427 "the split has to have been rescaled by a layout pass, or this \
2428 test cannot tell the two preferences apart"
2429 );
2430
2431 let state = cx.read(|cx| area.read(cx).dump(cx));
2432 let PanelInfo::Stack { sizes, .. } = &state.center.info else {
2433 panic!("the center writes a stack");
2434 };
2435 assert_eq!(
2436 sizes, &measured,
2437 "the written sizes are the ones on screen, not the ones the tree \
2438 was built from"
2439 );
2440 }
2441
2442 #[gpui::test]
2448 fn a_panel_dropped_beside_another_takes_half_the_split(cx: &mut TestAppContext) {
2449 let log = Log::default();
2450 let (area, panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2451 let group = child_node(&area, 0, cx);
2452 let beta = panel_id_of(&panels[1]);
2453
2454 cx.update(|window, cx| {
2455 area.update(cx, |area, cx| {
2456 area.move_panel(
2457 beta,
2458 InsertTarget::Split {
2459 node: group,
2460 placement: Placement::Right,
2461 size: None,
2462 },
2463 window,
2464 cx,
2465 );
2466 });
2467 });
2468 cx.run_until_parked();
2469
2470 let root = cx.read(|cx| {
2471 area.read(cx)
2472 .layout(DockPlacement::Center)
2473 .unwrap()
2474 .root()
2475 .id()
2476 });
2477 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2478
2479 assert_eq!(sizes.len(), 2, "the drop splits the center in two");
2480 let (left, right) = (sizes[0].as_f32(), sizes[1].as_f32());
2481 assert!(
2482 (left - right).abs() <= (left + right) * 0.02,
2483 "the two halves must be within 2% of each other, got {left} and {right}"
2484 );
2485 }
2486
2487 #[gpui::test]
2489 fn a_move_of_an_unowned_panel_is_ignored(cx: &mut TestAppContext) {
2490 let log = Log::default();
2491 let (area, _panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2492 let group = child_node(&area, 0, cx);
2493 let before = cx.read(|cx| area.read(cx).dump(cx));
2494
2495 move_panel_into(&area, PanelId::from_u64(9_999_999), group, None, true, cx);
2497
2498 let after = cx.read(|cx| area.read(cx).dump(cx));
2499 assert_eq!(
2500 before, after,
2501 "an unowned panel move must not touch the tree"
2502 );
2503 }
2504
2505 #[gpui::test]
2509 fn a_panel_dropped_across_the_axis_still_takes_half(cx: &mut TestAppContext) {
2510 let log = Log::default();
2511 let (area, panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2512 let group = child_node(&area, 0, cx);
2513 let beta = panel_id_of(&panels[1]);
2514
2515 cx.update(|window, cx| {
2516 area.update(cx, |area, cx| {
2517 area.move_panel(
2518 beta,
2519 InsertTarget::Split {
2520 node: group,
2521 placement: Placement::Bottom,
2522 size: None,
2523 },
2524 window,
2525 cx,
2526 );
2527 });
2528 });
2529 cx.run_until_parked();
2530
2531 let wrapper = child_node(&area, 0, cx);
2535 let sizes = cx.read(|cx| {
2536 area.read(cx).splits[&wrapper]
2537 .entity
2538 .read(cx)
2539 .sizes()
2540 .clone()
2541 });
2542 assert_eq!(sizes.len(), 2, "the drop splits the group in two");
2543 let (top, bottom) = (sizes[0].as_f32(), sizes[1].as_f32());
2544 assert!(
2545 (top - bottom).abs() <= (top + bottom) * 0.02,
2546 "the two halves must be within 2% of each other, got {top} and {bottom}"
2547 );
2548 }
2549
2550 #[gpui::test]
2553 fn a_panel_dropped_into_a_populated_split_takes_an_even_share(cx: &mut TestAppContext) {
2554 let (area, cx) = setup(cx);
2555 let panels = cx.update(|window, cx| {
2556 let alpha = TestPanel::new("Alpha", cx);
2557 let beta = TestPanel::new("Beta", cx);
2558 let gamma = TestPanel::new("Gamma", cx);
2559 area.update(cx, |area, cx| {
2560 area.set_center(
2561 DockLayout::h_split()
2562 .child(DockLayout::tabs().panel(alpha.clone()), Some(px(240.)))
2563 .child(
2564 DockLayout::tabs().panel(beta.clone()).panel(gamma.clone()),
2565 None,
2566 ),
2567 window,
2568 cx,
2569 );
2570 });
2571 vec![alpha, beta, gamma]
2572 });
2573 cx.run_until_parked();
2574
2575 let right = child_node(&area, 1, cx);
2576 let gamma = panel_id_of(&panels[2]);
2577 cx.update(|window, cx| {
2578 area.update(cx, |area, cx| {
2579 area.move_panel(
2580 gamma,
2581 InsertTarget::Split {
2582 node: right,
2583 placement: Placement::Right,
2584 size: None,
2585 },
2586 window,
2587 cx,
2588 );
2589 });
2590 });
2591 cx.run_until_parked();
2592
2593 let root = cx.read(|cx| {
2594 area.read(cx)
2595 .layout(DockPlacement::Center)
2596 .unwrap()
2597 .root()
2598 .id()
2599 });
2600 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2601 assert_eq!(sizes.len(), 3, "three slots side by side");
2602 let dropped = sizes[2].as_f32();
2603 let neighbour = sizes[1].as_f32();
2604 assert!(
2605 (dropped - neighbour).abs() <= (dropped + neighbour) * 0.02,
2606 "the dropped panel splits its neighbour evenly, got neighbour {neighbour} and dropped {dropped}"
2607 );
2608 }
2609
2610 #[gpui::test]
2614 fn a_panel_dropped_beside_a_dock_takes_half(cx: &mut TestAppContext) {
2615 for placement in [DockPlacement::Bottom, DockPlacement::Left] {
2616 let (area, cx) = setup(cx);
2617 let dropped = cx.update(|window, cx| {
2618 let resident = TestPanel::new("Resident", cx);
2619 let dropped = TestPanel::new("Dropped", cx);
2620 area.update(cx, |area, cx| {
2621 area.set_center(
2622 DockLayout::tabs().panel(TestPanel::new("Center", cx)),
2623 window,
2624 cx,
2625 );
2626 area.set_dock(
2627 placement,
2628 DockLayout::tabs().panel(resident).panel(dropped.clone()),
2629 window,
2630 cx,
2631 );
2632 area.set_dock_size(placement, px(400.), window, cx);
2633 });
2634 dropped
2635 });
2636 cx.run_until_parked();
2637
2638 let group = cx.read(|cx| {
2639 area.read(cx)
2640 .layout(placement)
2641 .unwrap()
2642 .find_panel_node(panel_id_of(&dropped))
2643 .expect("both panels start in the dock's only group")
2644 });
2645
2646 cx.update(|window, cx| {
2647 area.update(cx, |area, cx| {
2648 area.move_panel(
2649 panel_id_of(&dropped),
2650 InsertTarget::Split {
2651 node: group,
2652 placement: Placement::Bottom,
2653 size: None,
2654 },
2655 window,
2656 cx,
2657 );
2658 });
2659 });
2660 cx.run_until_parked();
2661
2662 let split = cx.read(|cx| {
2663 let tree = area.read(cx).layout(placement).unwrap();
2664 let root = tree.root();
2665 match root.kind() {
2666 PaneRef::Split { .. } => root.id(),
2667 _ => panic!("the drop must have produced a split"),
2668 }
2669 });
2670 let sizes = cx.read(|cx| area.read(cx).splits[&split].entity.read(cx).sizes().clone());
2671
2672 assert_eq!(sizes.len(), 2, "{placement:?}: the drop splits in two");
2673 let (first, second) = (sizes[0].as_f32(), sizes[1].as_f32());
2674 assert!(
2675 (first - second).abs() <= (first + second) * 0.02,
2676 "{placement:?}: expected halves, got {first} and {second}"
2677 );
2678 }
2679 }
2680
2681 #[gpui::test]
2688 fn an_explicit_slot_size_survives_the_first_layout_pass(cx: &mut TestAppContext) {
2689 let (area, cx) = setup(cx);
2690 cx.update(|window, cx| {
2691 let sidebar = TestPanel::new("Sidebar", cx);
2692 let content = TestPanel::new("Content", cx);
2693 area.update(cx, |area, cx| {
2694 area.set_center(
2695 DockLayout::h_split()
2696 .child(DockLayout::tabs().panel(sidebar), Some(px(200.)))
2697 .child(DockLayout::tabs().panel(content), None),
2698 window,
2699 cx,
2700 );
2701 });
2702 });
2703 cx.run_until_parked();
2704
2705 let root = cx.read(|cx| {
2706 area.read(cx)
2707 .layout(DockPlacement::Center)
2708 .unwrap()
2709 .root()
2710 .id()
2711 });
2712 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2713
2714 let fixed = sizes
2718 .first()
2719 .copied()
2720 .expect("the split has slots")
2721 .as_f32();
2722 assert!(
2723 (fixed - 200.).abs() <= 4.,
2724 "the fixed slot keeps its 200px instead of being rescaled by the \
2725 flexible sibling's placeholder, got {fixed}"
2726 );
2727 }
2728
2729 struct MeasuredPanel {
2732 name: &'static str,
2733 focus_handle: FocusHandle,
2734 }
2735
2736 impl MeasuredPanel {
2737 fn new(name: &'static str, cx: &mut App) -> Entity<Self> {
2738 cx.new(|cx| Self {
2739 name,
2740 focus_handle: cx.focus_handle(),
2741 })
2742 }
2743 }
2744
2745 impl Panel for MeasuredPanel {
2746 fn panel_name(&self) -> &'static str {
2747 self.name
2748 }
2749 }
2750
2751 impl EventEmitter<PanelEvent> for MeasuredPanel {}
2752
2753 impl Focusable for MeasuredPanel {
2754 fn focus_handle(&self, _: &App) -> FocusHandle {
2755 self.focus_handle.clone()
2756 }
2757 }
2758
2759 impl Render for MeasuredPanel {
2760 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2761 let name = self.name;
2762 div().size_full().debug_selector(move || name.into())
2763 }
2764 }
2765
2766 fn draw_frames(cx: &mut VisualTestContext, frames: usize) {
2767 for _ in 0..frames {
2768 cx.update(|window, cx| window.draw(cx).clear(cx));
2769 }
2770 }
2771
2772 #[gpui::test]
2779 fn switching_a_tab_leaves_an_untouched_split_where_it_was_drawn(cx: &mut TestAppContext) {
2780 let (area, cx) = setup(cx);
2781 cx.update(|window, cx| {
2782 area.update(cx, |area, cx| {
2783 area.set_center(
2784 DockLayout::tabs().panel(TestPanel::new("Center", cx)),
2785 window,
2786 cx,
2787 );
2788 area.set_dock(
2789 DockPlacement::Left,
2790 DockLayout::v_split()
2791 .child(
2792 DockLayout::tabs().panel(MeasuredPanel::new("upper-left", cx)),
2793 None,
2794 )
2795 .child(
2796 DockLayout::tabs().panel(MeasuredPanel::new("lower-left", cx)),
2797 Some(px(360.)),
2798 ),
2799 window,
2800 cx,
2801 );
2802 area.set_dock_size(DockPlacement::Left, px(350.), window, cx);
2803 area.set_dock(
2804 DockPlacement::Bottom,
2805 DockLayout::tabs()
2806 .panel(TestPanel::new("Tooltip", cx))
2807 .panel(TestPanel::new("Icon", cx)),
2808 window,
2809 cx,
2810 );
2811 area.set_dock_size(DockPlacement::Bottom, px(200.), window, cx);
2812 });
2813 });
2814 cx.run_until_parked();
2815 draw_frames(cx, 3);
2816 let before = (
2817 cx.debug_bounds("upper-left").unwrap(),
2818 cx.debug_bounds("lower-left").unwrap(),
2819 );
2820
2821 let bottom = cx.read(|cx| {
2822 area.read(cx)
2823 .layout(DockPlacement::Bottom)
2824 .unwrap()
2825 .root()
2826 .id()
2827 });
2828 let group = cx.read(|cx| area.read(cx).groups[&bottom].entity.clone());
2829 cx.update(|window, cx| {
2830 group.update(cx, |group, cx| group.select_tab(1, window, cx));
2831 });
2832 cx.run_until_parked();
2833 draw_frames(cx, 3);
2834 let after = (
2835 cx.debug_bounds("upper-left").unwrap(),
2836 cx.debug_bounds("lower-left").unwrap(),
2837 );
2838
2839 assert_eq!(
2840 before, after,
2841 "a tab change in the bottom dock must not move the left split"
2842 );
2843 }
2844
2845 #[gpui::test]
2850 fn switching_a_tab_keeps_a_restored_split_at_its_rescaled_share(cx: &mut TestAppContext) {
2851 let (area, cx) = setup(cx);
2852 cx.update(|window, cx| {
2853 area.update(cx, |area, cx| {
2854 area.set_center(
2855 DockLayout::h_split()
2856 .child(
2857 DockLayout::tabs()
2858 .panel(TestPanel::new("Alpha", cx))
2859 .panel(TestPanel::new("Beta", cx)),
2860 Some(px(620.)),
2861 )
2862 .child(
2863 DockLayout::tabs().panel(MeasuredPanel::new("second", cx)),
2864 Some(px(350.)),
2865 ),
2866 window,
2867 cx,
2868 );
2869 });
2870 });
2871 cx.run_until_parked();
2872 draw_frames(cx, 3);
2873 let before = cx.debug_bounds("second").unwrap();
2876 assert_ne!(
2877 before.right(),
2878 px(970.),
2879 "the window must not match the recorded total, or this test cannot \
2880 tell a rescaled split from the file's pixels"
2881 );
2882
2883 let group = group_of(&area, 0, cx);
2884 cx.update(|window, cx| {
2885 group.update(cx, |group, cx| group.select_tab(1, window, cx));
2886 });
2887 cx.run_until_parked();
2888 draw_frames(cx, 3);
2889 let after = cx.debug_bounds("second").unwrap();
2890
2891 assert_eq!(
2892 before, after,
2893 "a tab change must not hand the file's pixels back to the split"
2894 );
2895 }
2896
2897 #[gpui::test]
2908 fn the_shipped_fixture_survives_a_load_dump_load_round_trip(cx: &mut TestAppContext) {
2909 let (area, cx) = setup(cx);
2910 let fixture: DockAreaState =
2911 serde_json::from_str(include_str!("fixtures/layout.json")).unwrap();
2912
2913 cx.update(|window, cx| area.update(cx, |area, cx| area.load(fixture, window, cx).unwrap()));
2914 cx.run_until_parked();
2915 let first = cx.read(|cx| area.read(cx).dump(cx));
2916
2917 assert_eq!(first.center.children.len(), 2, "the center's two groups");
2918 assert_eq!(first.center.children[0].children.len(), 15);
2919 assert_eq!(first.center.children[1].children.len(), 1);
2920 for dock in [&first.left_dock, &first.bottom_dock, &first.right_dock] {
2921 let dock = dock.as_ref().expect("all three docks are attached");
2922 assert!(dock.open());
2923 assert!(
2924 !dock.panel().children.is_empty(),
2925 "a dock that loaded empty would round-trip just as stably"
2926 );
2927 }
2928 assert_eq!(first.left_dock.as_ref().unwrap().size(), px(350.));
2929 assert_eq!(first.bottom_dock.as_ref().unwrap().size(), px(200.));
2930 assert_eq!(first.right_dock.as_ref().unwrap().size(), px(320.));
2931
2932 cx.update(|window, cx| {
2933 area.update(cx, |area, cx| area.load(first.clone(), window, cx).unwrap())
2934 });
2935 cx.run_until_parked();
2936 let second = cx.read(|cx| area.read(cx).dump(cx));
2937
2938 assert_eq!(second, first, "dump == dump(load(dump))");
2939 }
2940
2941 #[gpui::test]
2942 fn an_unregistered_panel_survives_a_load_and_save_round_trip(cx: &mut TestAppContext) {
2943 let (area, cx) = setup(cx);
2944 cx.update(|_, cx| register_test_panels(cx));
2945
2946 let json = include_str!("fixtures/unregistered_panel.json");
2947 let state: DockAreaState = serde_json::from_str(json).unwrap();
2948
2949 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
2950 let dumped = cx.read(|cx| area.read(cx).dump(cx));
2951
2952 let leaf = &dumped.center.children[0].children[0];
2953 assert_eq!(leaf.panel_name, "PanelFromTheFuture");
2954 assert_eq!(
2955 leaf.info,
2956 PanelInfo::panel(serde_json::json!({"keep": "me"})),
2957 "a panel this build cannot construct keeps its payload"
2958 );
2959 }
2960
2961 #[gpui::test]
2962 fn a_dock_carries_its_own_tree_and_survives_a_round_trip(cx: &mut TestAppContext) {
2963 let (area, cx) = setup(cx);
2964 cx.update(|window, cx| {
2965 let alpha = TestPanel::new("Alpha", cx);
2966 let beta = TestPanel::new("Beta", cx);
2967 area.update(cx, |area, cx| {
2968 area.set_center(DockLayout::tabs().panel(alpha), window, cx);
2969 area.set_dock(
2970 DockPlacement::Left,
2971 DockLayout::tabs().panel(beta),
2972 window,
2973 cx,
2974 );
2975 });
2976 });
2977
2978 let center_ids = cx.read(|cx| {
2987 area.read(cx)
2988 .layout(DockPlacement::Center)
2989 .unwrap()
2990 .node_ids()
2991 });
2992 let dock_ids = cx.read(|cx| {
2993 area.read(cx)
2994 .layout(DockPlacement::Left)
2995 .unwrap()
2996 .node_ids()
2997 });
2998 assert!(!center_ids.is_empty() && !dock_ids.is_empty());
2999 assert!(
3000 center_ids.iter().all(|id| !dock_ids.contains(id)),
3001 "every tree in one area must draw from one id space: \
3002 center {center_ids:?} vs left {dock_ids:?}"
3003 );
3004
3005 let state = cx.read(|cx| area.read(cx).dump(cx));
3006 let left = state.left_dock.clone().expect("the left dock is written");
3007 assert_eq!(left.placement(), DockPlacement::Left);
3008 assert!(left.open());
3009 assert_eq!(left.panel().children[0].panel_name, "Beta");
3010 assert!(state.right_dock.is_none());
3011 }
3012
3013 #[gpui::test]
3014 fn a_panel_moved_between_regions_keeps_its_active_state(cx: &mut TestAppContext) {
3015 let log = log_of();
3016 let (area, alpha, cx) = two_groups(&log, cx);
3017 cx.run_until_parked();
3018 assert!(drain(&log).contains(&("Alpha", PanelSignal::Active(true))));
3021
3022 move_alpha_into_the_other_group(&area, &alpha, cx);
3023
3024 assert!(
3025 !drain(&log).contains(&("Alpha", PanelSignal::Active(true))),
3026 "a displayed panel dragged to another group must not be told `true` twice"
3027 );
3028 }
3029
3030 #[gpui::test]
3031 fn a_groups_close_intent_reaches_the_tree(cx: &mut TestAppContext) {
3032 let log = log_of();
3036 let (area, alpha, cx) = two_groups(&log, cx);
3037 cx.run_until_parked();
3038 drain(&log);
3039
3040 let node = child_node(&area, 0, cx);
3041 let alpha_id = panel_id_of(&alpha);
3042 cx.update(|_, cx| {
3043 let group = area.read(cx).groups.get(&node).unwrap().entity.clone();
3044 group.update(cx, |group, cx| group.close_panel(alpha_id, cx));
3045 });
3046 cx.run_until_parked();
3047
3048 assert!(
3049 cx.read(|cx| area
3050 .read(cx)
3051 .layout(DockPlacement::Center)
3052 .unwrap()
3053 .find_panel_node(alpha_id))
3054 .is_none(),
3055 "the close intent was applied to the tree"
3056 );
3057 assert!(drain(&log).contains(&("Alpha", PanelSignal::Removed)));
3058 }
3059
3060 #[gpui::test]
3061 fn replacing_the_center_tells_the_panels_that_left(cx: &mut TestAppContext) {
3062 let log = log_of();
3063 let (area, _alpha, cx) = two_groups(&log, cx);
3064 cx.run_until_parked();
3065 drain(&log);
3066
3067 cx.update(|window, cx| {
3068 let gamma = TestPanel::new("Gamma", cx);
3069 area.update(cx, |area, cx| {
3070 area.set_center(DockLayout::tabs().panel(gamma), window, cx)
3071 });
3072 });
3073 cx.run_until_parked();
3074
3075 let seen = drain(&log);
3076 assert!(seen.contains(&("Alpha", PanelSignal::Removed)));
3077 assert!(seen.contains(&("Beta", PanelSignal::Removed)));
3078 }
3079
3080 #[gpui::test]
3081 fn closing_a_zoomed_panel_clears_the_zoom(cx: &mut TestAppContext) {
3082 let log = log_of();
3083 let (area, alpha, cx) = two_groups(&log, cx);
3084 cx.run_until_parked();
3085
3086 let node = child_node(&area, 0, cx);
3087 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_in(node, window, cx)));
3088 assert!(cx.read(|cx| area.read(cx).is_zoomed()));
3089
3090 cx.update(|window, cx| {
3091 area.update(cx, |area, cx| area.remove_panel(alpha.clone(), window, cx))
3092 });
3093
3094 assert!(
3095 !cx.read(|cx| area.read(cx).is_zoomed()),
3096 "a zoomed panel that left the dock must not keep filling it"
3097 );
3098 }
3099
3100 struct SkinPlaceholder {
3104 state: PanelState,
3105 focus_handle: FocusHandle,
3106 }
3107
3108 impl Panel for SkinPlaceholder {
3109 fn panel_name(&self) -> &'static str {
3110 "SkinPlaceholder"
3111 }
3112
3113 fn dump(&self, _: &App) -> PanelState {
3114 self.state.clone()
3115 }
3116 }
3117
3118 impl EventEmitter<PanelEvent> for SkinPlaceholder {}
3119
3120 impl Focusable for SkinPlaceholder {
3121 fn focus_handle(&self, _: &App) -> FocusHandle {
3122 self.focus_handle.clone()
3123 }
3124 }
3125
3126 impl Render for SkinPlaceholder {
3127 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3128 Empty
3129 }
3130 }
3131
3132 struct PlaceholderSkin {
3133 asked: Rc<std::cell::RefCell<Vec<String>>>,
3134 }
3135
3136 impl DockAreaRenderer for PlaceholderSkin {
3137 fn build_placeholder(
3138 &self,
3139 state: &PanelState,
3140 _: &mut Window,
3141 cx: &mut App,
3142 ) -> Option<Arc<dyn PanelView>> {
3143 self.asked.borrow_mut().push(state.panel_name.clone());
3144 let state = state.clone();
3145 Some(Arc::new(cx.new(|cx| SkinPlaceholder {
3146 state,
3147 focus_handle: cx.focus_handle(),
3148 })))
3149 }
3150
3151 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
3152 Rc::new(BareTabGroup)
3153 }
3154 }
3155
3156 #[gpui::test]
3160 fn an_unbuildable_panel_becomes_the_skins_placeholder(cx: &mut TestAppContext) {
3161 cx.update(|cx| {
3162 let _ = crate::Theme::global_mut(cx);
3163 });
3164 let asked: Rc<std::cell::RefCell<Vec<String>>> = Rc::default();
3165 let skin = Rc::new(PlaceholderSkin {
3166 asked: asked.clone(),
3167 });
3168 let (area, cx) = cx.add_window_view(|window, cx| {
3169 DockArea::new("test-dock", None, window, cx).with_renderer(skin)
3170 });
3171
3172 cx.update(|window, cx| {
3174 let ghost = TestPanel::new("Ghost", cx);
3175 area.update(cx, |area, cx| {
3176 area.set_center(DockLayout::tabs().panel(ghost), window, cx)
3177 });
3178 });
3179 let state = cx.read(|cx| area.read(cx).dump(cx));
3180 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
3181 cx.run_until_parked();
3182
3183 assert_eq!(*asked.borrow(), vec!["Ghost".to_string()]);
3184 assert_eq!(
3185 cx.read(|cx| area
3186 .read(cx)
3187 .panels
3188 .values()
3189 .map(|panel| panel.panel_name(cx))
3190 .collect::<Vec<_>>()),
3191 vec!["SkinPlaceholder"],
3192 "base installed the skin's placeholder, not its own"
3193 );
3194 assert_eq!(
3195 cx.read(|cx| area.read(cx).dump(cx)).center.children[0].children[0].panel_name,
3196 "Ghost",
3197 "and the unknown panel still survives the next save"
3198 );
3199 }
3200
3201 #[gpui::test]
3202 fn removing_a_non_tail_child_shifts_the_split_sizes_with_it(cx: &mut TestAppContext) {
3203 let log = log_of();
3207 let (area, cx) = setup(cx);
3208 let alpha = cx.update(|window, cx| {
3209 let alpha = TestPanel::logging("Alpha", &log, cx);
3210 let beta = TestPanel::logging("Beta", &log, cx);
3211 let gamma = TestPanel::logging("Gamma", &log, cx);
3212 area.update(cx, |area, cx| {
3213 area.set_center(
3214 DockLayout::h_split()
3215 .child(DockLayout::tabs().panel(alpha.clone()), Some(px(100.)))
3216 .child(DockLayout::tabs().panel(beta), Some(px(200.)))
3217 .child(DockLayout::tabs().panel(gamma), Some(px(300.))),
3218 window,
3219 cx,
3220 );
3221 });
3222 alpha
3223 });
3224
3225 let root = cx.read(|cx| {
3226 area.read(cx)
3227 .layout(DockPlacement::Center)
3228 .unwrap()
3229 .root()
3230 .id()
3231 });
3232 let split = cx.read(|cx| area.read(cx).splits.get(&root).unwrap().entity.clone());
3233 let sizes = |cx: &mut VisualTestContext| cx.read(|cx| split.read(cx).sizes().clone());
3234
3235 let before = sizes(cx);
3239 assert_eq!(before.len(), 3);
3240 let kept_if_correct = before[1] / before[2];
3241 let kept_if_truncated = before[0] / before[1];
3242 assert!(
3243 (kept_if_correct - kept_if_truncated).abs() > 0.1,
3244 "the fixture must be able to tell the two outcomes apart"
3245 );
3246
3247 cx.update(|window, cx| area.update(cx, |area, cx| area.remove_panel(alpha, window, cx)));
3248
3249 let after = sizes(cx);
3250 assert_eq!(after.len(), 2);
3251 assert!(
3252 (after[0] / after[1] - kept_if_correct).abs() < 0.01,
3253 "the survivors kept their own proportions: slot 0 was removed, not \
3254 the tail — got {after:?} from {before:?}"
3255 );
3256 }
3257
3258 #[gpui::test]
3259 fn a_panel_moves_between_the_center_and_a_dock(cx: &mut TestAppContext) {
3260 let log = log_of();
3261 let (area, alpha, cx) = two_groups(&log, cx);
3262 cx.update(|window, cx| {
3263 let gamma = TestPanel::logging("Gamma", &log, cx);
3264 area.update(cx, |area, cx| {
3265 area.set_dock(
3266 DockPlacement::Left,
3267 DockLayout::tabs().panel(gamma),
3268 window,
3269 cx,
3270 );
3271 });
3272 });
3273 cx.run_until_parked();
3274 drain(&log);
3275
3276 let alpha_id = panel_id_of(&alpha);
3277 let dock_group = cx.read(|cx| {
3278 area.read(cx)
3279 .layout(DockPlacement::Left)
3280 .unwrap()
3281 .root()
3282 .id()
3283 });
3284
3285 cx.update(|window, cx| {
3286 area.update(cx, |area, cx| {
3287 area.move_panel(
3288 alpha_id,
3289 InsertTarget::Tabs {
3290 node: dock_group,
3291 ix: None,
3292 activate: true,
3293 },
3294 window,
3295 cx,
3296 );
3297 });
3298 });
3299 cx.run_until_parked();
3300
3301 assert!(
3302 cx.read(|cx| area
3303 .read(cx)
3304 .layout(DockPlacement::Center)
3305 .unwrap()
3306 .find_panel_node(alpha_id))
3307 .is_none(),
3308 "the panel left the center"
3309 );
3310 assert_eq!(
3311 cx.read(|cx| area
3312 .read(cx)
3313 .layout(DockPlacement::Left)
3314 .unwrap()
3315 .find_panel_node(alpha_id)),
3316 Some(dock_group),
3317 "and arrived in the dock's group"
3318 );
3319
3320 let seen = drain(&log);
3321 assert!(
3322 !seen.contains(&("Alpha", PanelSignal::Removed)),
3323 "crossing regions is still a move, not a removal"
3324 );
3325 assert!(
3326 !seen.contains(&("Alpha", PanelSignal::Active(true))),
3327 "and it was displayed in both, so it is not told `true` twice"
3328 );
3329 }
3330
3331 #[gpui::test]
3332 fn a_move_onto_an_unusable_target_leaves_no_stranded_panel(cx: &mut TestAppContext) {
3333 let log = log_of();
3338 let (area, _alpha, cx) = two_groups(&log, cx);
3339 let gamma = cx.update(|window, cx| {
3340 let gamma = TestPanel::logging("Gamma", &log, cx);
3341 area.update(cx, |area, cx| {
3342 area.set_dock(
3343 DockPlacement::Left,
3344 DockLayout::tabs().panel(gamma.clone()),
3345 window,
3346 cx,
3347 );
3348 });
3349 gamma
3350 });
3351 cx.run_until_parked();
3352 drain(&log);
3353
3354 let gamma_id = panel_id_of(&gamma);
3355 let center_root = cx.read(|cx| {
3358 area.read(cx)
3359 .layout(DockPlacement::Center)
3360 .unwrap()
3361 .root()
3362 .id()
3363 });
3364
3365 cx.update(|window, cx| {
3366 area.update(cx, |area, cx| {
3367 area.move_panel(
3368 gamma_id,
3369 InsertTarget::Tabs {
3370 node: center_root,
3371 ix: None,
3372 activate: true,
3373 },
3374 window,
3375 cx,
3376 );
3377 });
3378 });
3379 cx.run_until_parked();
3380
3381 assert!(
3382 cx.read(|cx| area.read(cx).panel(gamma_id).is_none()),
3383 "the view map agrees with the trees straight away, rather than \
3384 carrying a panel that belongs to no tree"
3385 );
3386 assert!(
3387 drain(&log).contains(&("Gamma", PanelSignal::Removed)),
3388 "and the panel was told so at the point of the call"
3389 );
3390 }
3391
3392 #[gpui::test]
3393 fn an_all_hidden_container_reports_itself_invisible(cx: &mut TestAppContext) {
3394 let (area, cx) = setup(cx);
3397 let beta = cx.update(|window, cx| {
3398 let alpha = TestPanel::new("Alpha", cx);
3399 let beta = TestPanel::new("Beta", cx);
3400 area.update(cx, |area, cx| {
3401 area.set_center(
3402 DockLayout::h_split()
3403 .child(DockLayout::tabs().panel(alpha), None)
3404 .child(DockLayout::tabs().panel(beta.clone()), None),
3405 window,
3406 cx,
3407 );
3408 });
3409 beta
3410 });
3411
3412 let visible = |ix: usize, cx: &mut VisualTestContext| {
3413 let node = child_node(&area, ix, cx);
3414 cx.read(|cx| {
3415 let area = area.read(cx);
3416 let tree = area.layout(DockPlacement::Center).unwrap();
3417 area.is_node_visible(tree.find_node(node).unwrap(), cx)
3418 })
3419 };
3420
3421 assert!(visible(0, cx) && visible(1, cx));
3422
3423 cx.update(|_, cx| beta.update(cx, |beta, cx| beta.set_visible(false, cx)));
3424
3425 assert!(visible(0, cx), "the visible group still holds its slot");
3426 assert!(
3427 !visible(1, cx),
3428 "a group whose every panel is hidden must give its slot up"
3429 );
3430 }
3431
3432 #[gpui::test]
3433 fn a_locked_area_seals_its_groups(cx: &mut TestAppContext) {
3434 let log = log_of();
3435 let (area, _alpha, cx) = two_groups(&log, cx);
3436 cx.run_until_parked();
3437
3438 let node = child_node(&area, 0, cx);
3439 let group = cx.read(|cx| area.read(cx).groups.get(&node).unwrap().entity.clone());
3440 assert!(
3441 cx.read(|cx| group.read(cx).is_closable(cx)),
3442 "an unlocked group's panel can be closed"
3443 );
3444
3445 cx.update(|window, cx| area.update(cx, |area, cx| area.set_locked(true, window, cx)));
3446
3447 assert!(
3448 !cx.read(|cx| group.read(cx).is_closable(cx)),
3449 "the lock reaches every group through the constraints push"
3450 );
3451 }
3452
3453 #[gpui::test]
3461 fn empty_center_round_trips_as_a_stack(cx: &mut TestAppContext) {
3462 let (area, cx) = setup(cx);
3463 let center = cx.read(|cx| area.read(cx).dump(cx).center);
3464
3465 assert_eq!(center.panel_name, "StackPanel");
3466 assert!(
3467 matches!(center.info, PanelInfo::Stack { .. }),
3468 "got {:?}",
3469 center.info
3470 );
3471 }
3472
3473 #[gpui::test]
3474 fn fresh_center_is_empty(cx: &mut TestAppContext) {
3475 let (area, cx) = setup(cx);
3476
3477 assert!(
3478 is_center_empty(&area, cx),
3479 "DockArea::new starts with an empty split centre"
3480 );
3481 }
3482
3483 #[gpui::test]
3484 fn center_holding_a_tab_group_is_not_empty(cx: &mut TestAppContext) {
3485 let log = log_of();
3486 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3487 cx.run_until_parked();
3488
3489 assert!(!is_center_empty(&area, cx));
3490 }
3491
3492 #[gpui::test]
3496 fn center_is_empty_again_once_every_panel_is_removed(cx: &mut TestAppContext) {
3497 let log = log_of();
3498 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3499 cx.run_until_parked();
3500
3501 for panel in panels {
3502 cx.update(|window, cx| {
3503 area.update(cx, |area, cx| area.remove_panel(panel.clone(), window, cx))
3504 });
3505 }
3506 cx.run_until_parked();
3507
3508 assert!(is_center_empty(&area, cx));
3509 }
3510
3511 #[gpui::test]
3512 fn center_is_not_empty_after_adding_to_a_tab_group(cx: &mut TestAppContext) {
3513 let (area, cx) = setup(cx);
3514 assert!(is_center_empty(&area, cx));
3515
3516 cx.update(|window, cx| {
3517 let alpha = TestPanel::new("Alpha", cx);
3518 area.update(cx, |area, cx| {
3519 area.add_panel(alpha, DockPlacement::Center, None, window, cx)
3520 });
3521 });
3522 cx.run_until_parked();
3523
3524 assert!(!is_center_empty(&area, cx));
3525 }
3526
3527 #[gpui::test]
3531 fn add_panel_view_registers_the_handle_it_was_given(cx: &mut TestAppContext) {
3532 let (area, cx) = setup(cx);
3533
3534 let view = cx.update(|window, cx| {
3535 let view: Arc<dyn PanelView> = Arc::new(TestPanel::new("Alpha", cx));
3536 area.update(cx, |area, cx| {
3537 area.add_panel_view(view.clone(), DockPlacement::Center, None, window, cx)
3538 });
3539 view
3540 });
3541 cx.run_until_parked();
3542
3543 let id = cx.read(|cx| view.panel_id(cx));
3544 assert!(
3545 cx.read(|cx| area
3546 .read(cx)
3547 .panel(id)
3548 .is_some_and(|stored| Arc::ptr_eq(stored, &view))),
3549 "the stored handle is the one that was handed over, under its own id"
3550 );
3551 assert!(!is_center_empty(&area, cx));
3552 }
3553
3554 #[gpui::test]
3557 fn center_holding_only_hidden_panels_is_empty(cx: &mut TestAppContext) {
3558 let log = log_of();
3559 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3560 cx.run_until_parked();
3561 assert!(!is_center_empty(&area, cx));
3562
3563 cx.update(|_, cx| {
3564 for panel in &panels {
3565 panel.update(cx, |panel, cx| panel.set_visible(false, cx));
3566 }
3567 });
3568 cx.run_until_parked();
3569
3570 assert_eq!(
3571 cx.read(|cx| area
3572 .read(cx)
3573 .layout(DockPlacement::Center)
3574 .unwrap()
3575 .panels()
3576 .count()),
3577 2,
3578 "hiding a panel does not remove it from the tab group"
3579 );
3580 assert!(is_center_empty(&area, cx));
3581 }
3582
3583 #[gpui::test]
3584 fn single_panel_group_receives_initial_active(cx: &mut TestAppContext) {
3585 let log = log_of();
3586 let (_area, _panels, cx) = one_group(&log, &["A"], None, cx);
3587 cx.run_until_parked();
3588
3589 assert_eq!(drain_active(&log), [("A", true)]);
3590 }
3591
3592 #[gpui::test]
3593 fn multi_tab_construction_notifies_only_displayed_panel(cx: &mut TestAppContext) {
3594 let log = log_of();
3595 let (_area, _panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
3596 cx.run_until_parked();
3597
3598 assert_eq!(drain_active(&log), [("A", true)]);
3600 }
3601
3602 #[gpui::test]
3603 fn active_index_restore_notifies_that_panel_only(cx: &mut TestAppContext) {
3604 let log = log_of();
3605 let (_area, _panels, cx) = one_group(&log, &["A", "B", "C"], Some(2), cx);
3606 cx.run_until_parked();
3607
3608 assert_eq!(drain_active(&log), [("C", true)]);
3609 }
3610
3611 #[gpui::test]
3612 fn switching_tabs_sends_false_then_true(cx: &mut TestAppContext) {
3613 let log = log_of();
3614 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3615 cx.run_until_parked();
3616 drain(&log);
3617
3618 let group = group_of(&area, 0, cx);
3619 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(1, window, cx)));
3620 cx.run_until_parked();
3621
3622 assert_eq!(drain_active(&log), [("A", false), ("B", true)]);
3623 }
3624
3625 #[gpui::test]
3626 fn select_panel_displays_that_tab_where_it_sits(cx: &mut TestAppContext) {
3627 let log = log_of();
3628 let (area, panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
3629 cx.run_until_parked();
3630 drain(&log);
3631
3632 let b = panel_id_of(&panels[1]);
3633 cx.update(|window, cx| area.update(cx, |area, cx| area.select_panel(b, window, cx)));
3634 cx.run_until_parked();
3635
3636 assert_eq!(drain_active(&log), [("A", false), ("B", true)]);
3637 let (order, active_ix) = cx.read(|cx| {
3638 let tree = area.read(cx).layout(DockPlacement::Center).unwrap();
3639 let node = tree.find_panel_node(b).unwrap();
3640 match tree.find_node(node).unwrap().kind() {
3641 PaneRef::Tabs { panels, active_ix } => (panels.to_vec(), active_ix),
3642 PaneRef::Split { .. } => panic!("a tab group"),
3643 }
3644 });
3645 assert_eq!(active_ix, 1, "the selected tab is displayed");
3646 assert_eq!(
3647 order,
3648 panels.iter().map(panel_id_of).collect::<Vec<_>>(),
3649 "selecting a tab does not move it"
3650 );
3651 }
3652
3653 #[gpui::test]
3654 fn select_panel_is_silent_for_the_displayed_or_an_unknown_panel(cx: &mut TestAppContext) {
3655 let log = log_of();
3656 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3657 cx.run_until_parked();
3658 drain(&log);
3659
3660 let a = panel_id_of(&panels[0]);
3661 let stranger = cx.update(|_, cx| panel_id_of(&TestPanel::logging("Z", &log, cx)));
3662 cx.update(|window, cx| {
3663 area.update(cx, |area, cx| {
3664 area.select_panel(a, window, cx);
3665 area.select_panel(stranger, window, cx);
3666 })
3667 });
3668 cx.run_until_parked();
3669
3670 assert_eq!(drain_active(&log), []);
3671 }
3672
3673 #[gpui::test]
3674 fn reselecting_active_tab_stays_silent(cx: &mut TestAppContext) {
3675 let log = log_of();
3676 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3677 cx.run_until_parked();
3678 drain(&log);
3679
3680 let group = group_of(&area, 0, cx);
3681 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(0, window, cx)));
3682 cx.run_until_parked();
3683
3684 assert_eq!(drain_active(&log), []);
3685 }
3686
3687 #[gpui::test]
3693 fn inserting_at_active_ix_swaps_notifications(cx: &mut TestAppContext) {
3694 let log = log_of();
3695 let (area, cx) = setup(cx);
3696 let c = cx.update(|window, cx| {
3697 let a = TestPanel::logging("A", &log, cx);
3698 let b = TestPanel::logging("B", &log, cx);
3699 let x = TestPanel::logging("X", &log, cx);
3700 let c = TestPanel::logging("C", &log, cx);
3701 area.update(cx, |area, cx| {
3702 area.set_center(
3703 DockLayout::h_split()
3704 .child(DockLayout::tabs().panel(a).panel(b), None)
3705 .child(DockLayout::tabs().panel(x).panel(c.clone()), None),
3706 window,
3707 cx,
3708 );
3709 });
3710 c
3711 });
3712 cx.run_until_parked();
3713 drain(&log);
3714
3715 let destination = child_node(&area, 0, cx);
3716 let c_id = panel_id_of(&c);
3717 move_panel_into(&area, c_id, destination, Some(0), true, cx);
3718
3719 assert_eq!(drain_active(&log), [("A", false), ("C", true)]);
3720 let group = group_of(&area, 0, cx);
3721 assert_eq!(cx.read(|cx| group.read(cx).active_ix()), 0);
3722 assert_eq!(
3723 cx.read(|cx| group.read(cx).panels()[0].panel_id(cx)),
3724 c_id,
3725 "the arriving panel took the slot it named"
3726 );
3727 }
3728
3729 #[gpui::test]
3730 fn removing_before_active_keeps_displayed_panel(cx: &mut TestAppContext) {
3731 let log = log_of();
3732 let (area, panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
3733 let group = group_of(&area, 0, cx);
3734 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(1, window, cx)));
3735 cx.run_until_parked();
3736 drain(&log);
3737
3738 cx.update(|window, cx| {
3739 area.update(cx, |area, cx| {
3740 area.remove_panel(panels[0].clone(), window, cx)
3741 })
3742 });
3743 cx.run_until_parked();
3744
3745 assert_eq!(drain_active(&log), []);
3746 assert_eq!(cx.read(|cx| group.read(cx).active_ix()), 0);
3747 assert_eq!(
3748 cx.read(|cx| group.read(cx).panels()[0].panel_id(cx)),
3749 panel_id_of(&panels[1]),
3750 "the same panel is still displayed, at its new index"
3751 );
3752 }
3753
3754 #[gpui::test]
3757 fn collapse_and_expand_notify_active_panel(cx: &mut TestAppContext) {
3758 let log = log_of();
3759 let (area, cx) = setup(cx);
3760 cx.update(|window, cx| {
3761 let a = TestPanel::logging("A", &log, cx);
3762 let b = TestPanel::logging("B", &log, cx);
3763 area.update(cx, |area, cx| {
3764 area.set_dock(
3765 DockPlacement::Left,
3766 DockLayout::tabs().panel(a).panel(b),
3767 window,
3768 cx,
3769 );
3770 });
3771 });
3772 cx.run_until_parked();
3773 drain(&log);
3774
3775 cx.update(|window, cx| {
3776 area.update(cx, |area, cx| {
3777 area.toggle_dock(DockPlacement::Left, window, cx)
3778 })
3779 });
3780 cx.run_until_parked();
3781 assert_eq!(drain_active(&log), [("A", false)]);
3782
3783 cx.update(|window, cx| {
3784 area.update(cx, |area, cx| {
3785 area.toggle_dock(DockPlacement::Left, window, cx)
3786 })
3787 });
3788 cx.run_until_parked();
3789 assert_eq!(drain_active(&log), [("A", true)]);
3790 }
3791
3792 #[gpui::test]
3793 fn background_add_is_silent_but_first_panel_is_not(cx: &mut TestAppContext) {
3794 let log = log_of();
3795 let (area, cx) = setup(cx);
3796 let d = cx.update(|window, cx| {
3797 let a = TestPanel::logging("A", &log, cx);
3798 let b = TestPanel::logging("B", &log, cx);
3799 let c = TestPanel::logging("C", &log, cx);
3800 let d = TestPanel::logging("D", &log, cx);
3801 area.update(cx, |area, cx| {
3802 area.set_center(
3803 DockLayout::h_split()
3804 .child(DockLayout::tabs().panel(a).panel(b), None)
3805 .child(DockLayout::tabs().panel(c).panel(d.clone()), None),
3806 window,
3807 cx,
3808 );
3809 });
3810 d
3811 });
3812 cx.run_until_parked();
3813 drain(&log);
3814
3815 let destination = child_node(&area, 0, cx);
3818 move_panel_into(&area, panel_id_of(&d), destination, None, false, cx);
3819 assert_eq!(drain_active(&log), []);
3820
3821 cx.update(|window, cx| {
3824 let e = TestPanel::logging("E", &log, cx);
3825 area.update(cx, |area, cx| {
3826 area.add_panel(e, DockPlacement::Left, None, window, cx)
3827 });
3828 });
3829 cx.run_until_parked();
3830 assert_eq!(drain_active(&log), [("E", true)]);
3831 }
3832
3833 #[gpui::test]
3834 fn drag_active_panel_to_other_group_stays_silent_for_it(cx: &mut TestAppContext) {
3835 let log = log_of();
3836 let (area, cx) = setup(cx);
3837 let a = cx.update(|window, cx| {
3838 let a = TestPanel::logging("A", &log, cx);
3839 let b = TestPanel::logging("B", &log, cx);
3840 let c = TestPanel::logging("C", &log, cx);
3841 area.update(cx, |area, cx| {
3842 area.set_center(
3843 DockLayout::h_split()
3844 .child(DockLayout::tabs().panel(a.clone()).panel(b), None)
3845 .child(DockLayout::tabs().panel(c), None),
3846 window,
3847 cx,
3848 );
3849 });
3850 a
3851 });
3852 cx.run_until_parked();
3853 drain(&log);
3854
3855 let destination = child_node(&area, 1, cx);
3858 move_panel_into(&area, panel_id_of(&a), destination, None, true, cx);
3859
3860 let seen = drain_active(&log);
3863 assert!(seen.contains(&("B", true)), "got {seen:?}");
3864 assert!(seen.contains(&("C", false)), "got {seen:?}");
3865 assert!(
3866 !seen.iter().any(|(name, _)| *name == "A"),
3867 "the moved panel was displayed before and after: {seen:?}"
3868 );
3869 }
3870
3871 #[gpui::test]
3872 fn drag_active_panel_to_background_slot_deactivates_it(cx: &mut TestAppContext) {
3873 let log = log_of();
3874 let (area, cx) = setup(cx);
3875 let a = cx.update(|window, cx| {
3876 let a = TestPanel::logging("A", &log, cx);
3877 let c = TestPanel::logging("C", &log, cx);
3878 let d = TestPanel::logging("D", &log, cx);
3879 area.update(cx, |area, cx| {
3880 area.set_center(
3881 DockLayout::h_split()
3882 .child(DockLayout::tabs().panel(a.clone()), None)
3883 .child(DockLayout::tabs().panel(c).panel(d), None),
3884 window,
3885 cx,
3886 );
3887 });
3888 a
3889 });
3890 cx.run_until_parked();
3891 drain(&log);
3892
3893 let destination = child_node(&area, 1, cx);
3896 move_panel_into(&area, panel_id_of(&a), destination, None, false, cx);
3897
3898 assert_eq!(drain_active(&log), [("A", false)]);
3899 }
3900
3901 struct RecordingSkin {
3908 tab_bars: Rc<RefCell<Vec<NodeId>>>,
3909 }
3910
3911 struct RecordingTabGroup {
3912 drawn: Rc<RefCell<Vec<NodeId>>>,
3913 }
3914
3915 impl DockAreaRenderer for RecordingSkin {
3916 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
3917 Rc::new(RecordingTabGroup {
3918 drawn: self.tab_bars.clone(),
3919 })
3920 }
3921 }
3922
3923 impl TabGroupRenderer for RecordingTabGroup {
3924 fn render_tab_bar(
3925 &self,
3926 group: &TabGroupContext,
3927 _: &mut Window,
3928 _: &mut App,
3929 ) -> AnyElement {
3930 self.drawn.borrow_mut().push(group.node());
3931 Empty.into_any_element()
3932 }
3933 }
3934
3935 type DrawLog = Rc<RefCell<Vec<NodeId>>>;
3936
3937 fn setup_recording(
3939 cx: &mut TestAppContext,
3940 ) -> (Entity<DockArea>, DrawLog, &mut VisualTestContext) {
3941 cx.update(|cx| {
3942 let _ = crate::Theme::global_mut(cx);
3943 });
3944 let tab_bars: DrawLog = Rc::default();
3945 let skin = Rc::new(RecordingSkin {
3946 tab_bars: tab_bars.clone(),
3947 });
3948 let (area, cx) = cx.add_window_view(|window, cx| {
3949 DockArea::new("test-dock", None, window, cx).with_renderer(skin)
3950 });
3951 (area, tab_bars, cx)
3952 }
3953
3954 fn zoom_signals(log: &Log) -> Vec<(&'static str, PanelSignal)> {
3955 drain(log)
3956 .into_iter()
3957 .filter(|(_, signal)| matches!(signal, PanelSignal::Zoomed(_)))
3958 .collect()
3959 }
3960
3961 #[gpui::test]
3970 fn a_zoomed_group_is_drawn_whole_rather_than_as_its_bare_panel(cx: &mut TestAppContext) {
3971 let log = log_of();
3972 let (area, tab_bars, cx) = setup_recording(cx);
3973 cx.update(|window, cx| {
3974 let alpha = TestPanel::logging("Alpha", &log, cx);
3975 let beta = TestPanel::logging("Beta", &log, cx);
3976 area.update(cx, |area, cx| {
3977 area.set_center(
3978 DockLayout::h_split()
3979 .child(DockLayout::tabs().panel(alpha), None)
3980 .child(DockLayout::tabs().panel(beta), None),
3981 window,
3982 cx,
3983 );
3984 });
3985 });
3986 cx.run_until_parked();
3987
3988 let zoomed = child_node(&area, 0, cx);
3989 let other = child_node(&area, 1, cx);
3990 assert!(
3991 tab_bars.borrow().contains(&zoomed) && tab_bars.borrow().contains(&other),
3992 "both groups draw their own tab bar while nothing is zoomed"
3993 );
3994
3995 tab_bars.borrow_mut().clear();
3996 let group = group_of(&area, 0, cx);
3997 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
3998 cx.run_until_parked();
3999
4000 assert!(
4001 tab_bars.borrow().contains(&zoomed),
4002 "a zoomed group is rendered whole: its own tab bar is still drawn, \
4003 which is exactly what the bare panel does not carry"
4004 );
4005 assert!(
4006 !tab_bars.borrow().contains(&other),
4007 "and it is the only thing on screen"
4008 );
4009 }
4010
4011 #[gpui::test]
4017 fn clearing_the_zoom_from_outside_puts_the_groups_own_flag_back(cx: &mut TestAppContext) {
4018 let log = log_of();
4019 let (area, _alpha, cx) = two_groups(&log, cx);
4020 cx.run_until_parked();
4021 drain(&log);
4022
4023 let node = child_node(&area, 0, cx);
4024 let group = group_of(&area, 0, cx);
4025 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
4026 cx.run_until_parked();
4027 assert_eq!(cx.read(|cx| area.read(cx).zoomed_group()), Some(node));
4028 assert!(cx.read(|cx| group.read(cx).is_zoomed()));
4029 assert_eq!(
4030 zoom_signals(&log),
4031 vec![("Alpha", PanelSignal::Zoomed(true))]
4032 );
4033
4034 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_out(window, cx)));
4035 cx.run_until_parked();
4036
4037 assert!(!cx.read(|cx| area.read(cx).is_zoomed()));
4038 assert!(
4039 !cx.read(|cx| group.read(cx).is_zoomed()),
4040 "a group left flagged zoomed would stay locked and refuse every drop"
4041 );
4042 assert!(
4043 cx.read(|cx| group.read(cx).context(cx).is_droppable()),
4044 "and the lock the zoom imposed is lifted with it"
4045 );
4046 assert_eq!(
4047 zoom_signals(&log),
4048 vec![("Alpha", PanelSignal::Zoomed(false))],
4049 "the panel hears the zoom end too, not just the group"
4050 );
4051 }
4052
4053 #[gpui::test]
4056 fn a_group_that_refuses_to_zoom_leaves_the_area_unzoomed(cx: &mut TestAppContext) {
4057 let log = log_of();
4058 let (area, alpha, cx) = two_groups(&log, cx);
4059 cx.run_until_parked();
4060 cx.update(|_, cx| alpha.update(cx, |panel, cx| panel.set_zoomable(false, cx)));
4061
4062 let node = child_node(&area, 0, cx);
4063 let group = group_of(&area, 0, cx);
4064 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_in(node, window, cx)));
4065 cx.run_until_parked();
4066
4067 assert!(!cx.read(|cx| group.read(cx).is_zoomed()));
4068 assert!(
4069 !cx.read(|cx| area.read(cx).is_zoomed()),
4070 "the area must not fill itself with a group that never zoomed"
4071 );
4072 }
4073}