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 let Some(destination) = self.placement_of_node(target_node(&target)) else {
488 return;
489 };
490 let source = self.placement_of_panel(panel);
491
492 if matches!(target, InsertTarget::Split { .. }) {
495 self.adopt_measured_sizes(destination, cx);
496 }
497
498 let was_active = self
502 .layout(source.unwrap_or(destination))
503 .and_then(|tree| tree.find_panel_node(panel))
504 .and_then(|node| self.groups.get(&node))
505 .and_then(|cached| cached.entity.read(cx).last_notified_active(panel));
506
507 let changed = match source {
508 Some(source) if source == destination => {
509 let Some(tree) = self.tree_mut(destination) else {
510 return;
511 };
512 tree.move_panel(panel, target).changed()
513 }
514 source => {
515 let detached = source
526 .and_then(|source| self.tree_mut(source))
527 .is_some_and(|tree| tree.remove_panel(panel).changed());
528 let Some(tree) = self.tree_mut(destination) else {
529 return;
530 };
531 let inserted = tree.insert_panel(panel, target).changed();
532 detached || inserted
533 }
534 };
535
536 self.commit_changed(changed, window, cx);
537
538 if let Some(active) = was_active {
539 if let Some(cached) = self
540 .layout(destination)
541 .and_then(|tree| tree.find_panel_node(panel))
542 .and_then(|node| self.groups.get(&node))
543 {
544 let group = cached.entity.clone();
545 group.update(cx, |group, _| group.seed_active(panel, active));
546 }
547 }
548 }
549
550 pub fn select_panel(&mut self, panel: PanelId, window: &mut Window, cx: &mut Context<Self>) {
559 let Some(placement) = self.placement_of_panel(panel) else {
560 return;
561 };
562 let Some(tree) = self.tree_mut(placement) else {
563 return;
564 };
565 let Some(node) = tree.find_panel_node(panel) else {
566 return;
567 };
568 let ix = tree.find_node(node).and_then(|node| match node.kind() {
569 PaneRef::Tabs { panels, .. } => panels.iter().position(|held| *held == panel),
570 PaneRef::Split { .. } => None,
571 });
572 let Some(ix) = ix else {
573 return;
574 };
575 let result = tree.set_active(node, ix);
576 self.commit(result, window, cx);
577 }
578
579 pub fn split_at(
581 &mut self,
582 node: NodeId,
583 panel: PanelId,
584 placement: Placement,
585 window: &mut Window,
586 cx: &mut Context<Self>,
587 ) {
588 let Some(region) = self.placement_of_node(node) else {
589 return;
590 };
591 self.adopt_measured_sizes(region, cx);
592 let Some(tree) = self.tree_mut(region) else {
593 return;
594 };
595 let result = tree.split(node, panel, placement, None);
596 self.commit(result, window, cx);
597 }
598
599 fn remove_panel_id(&mut self, panel: PanelId, window: &mut Window, cx: &mut Context<Self>) {
600 let Some(region) = self.placement_of_panel(panel) else {
601 return;
602 };
603 let Some(tree) = self.tree_mut(region) else {
604 return;
605 };
606 let result = tree.remove_panel(panel);
607 self.commit(result, window, cx);
608 }
609}
610
611impl DockArea {
621 pub fn set_zoomed_in(&mut self, node: NodeId, window: &mut Window, cx: &mut Context<Self>) {
627 self.set_zoom(Some(Zoomed::Group(node)), window, cx);
628 }
629
630 pub fn set_zoomed_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
636 self.set_zoom(None, window, cx);
637 }
638
639 pub fn is_zoomed(&self) -> bool {
640 self.zoomed.is_some()
641 }
642
643 pub fn zoomed_group(&self) -> Option<NodeId> {
645 match self.zoomed {
646 Some(Zoomed::Group(node)) => Some(node),
647 _ => None,
648 }
649 }
650
651 fn set_zoom(&mut self, zoomed: Option<Zoomed>, window: &mut Window, cx: &mut Context<Self>) {
659 if self.zoomed == zoomed {
660 return;
661 }
662
663 if let Some(previous) = self.zoomed {
664 self.drive_zoom(previous, false, window, cx);
665 }
666 let accepted = match zoomed {
667 Some(next) => self.drive_zoom(next, true, window, cx).then_some(next),
668 None => None,
669 };
670 self.zoomed = accepted;
671 cx.notify();
672 }
673
674 fn drive_zoom(
680 &mut self,
681 zoomed: Zoomed,
682 zoom_in: bool,
683 window: &mut Window,
684 cx: &mut Context<Self>,
685 ) -> bool {
686 match zoomed {
687 Zoomed::Group(node) => {
688 let Some(group) = self.groups.get(&node).map(|cached| cached.entity.clone()) else {
689 return false;
690 };
691 group.update(cx, |group, cx| {
692 group.set_zoomed(zoom_in, window, cx);
693 group.is_zoomed() == zoom_in
694 })
695 }
696 }
697 }
698
699 fn zoomed_view(&self) -> Option<AnyView> {
704 match self.zoomed? {
705 Zoomed::Group(node) => Some(self.groups.get(&node)?.entity.clone().into()),
706 }
707 }
708}
709
710impl DockArea {
712 pub fn load(
717 &mut self,
718 state: DockAreaState,
719 window: &mut Window,
720 cx: &mut Context<Self>,
721 ) -> Result<()> {
722 self.version = state.version;
723 self.zoomed = None;
724 self.groups.clear();
728 self.splits.clear();
729 self.docks.clear();
730 let dock_area = self.this.clone();
735 let renderer = self.renderer.clone();
736 let mut built = Vec::new();
737 self.center = {
738 let mut builder = RegistryPanelBuilder {
739 dock_area: dock_area.clone(),
740 renderer: renderer.clone(),
741 built: &mut built,
742 window,
743 cx,
744 };
745 PaneTree::from_state(&state.center, RootKind::Split, &mut builder)
746 };
747
748 for dock_state in [state.left_dock, state.right_dock, state.bottom_dock]
749 .into_iter()
750 .flatten()
751 {
752 let tree = {
753 let mut builder = RegistryPanelBuilder {
754 dock_area: dock_area.clone(),
755 renderer: renderer.clone(),
756 built: &mut built,
757 window,
758 cx,
759 };
760 PaneTree::from_state(dock_state.panel(), RootKind::Any, &mut builder)
761 };
762 let mut dock = Dock::new(dock_state.size());
763 dock.set_open(dock_state.open());
764 self.docks
765 .insert(dock_state.placement(), DockRegion { tree, dock });
766 }
767
768 self.panels.extend(built);
769 self.reconcile(window, cx);
770 cx.emit(DockEvent::LayoutChanged);
771 Ok(())
772 }
773
774 pub fn dump(&self, cx: &App) -> DockAreaState {
795 let source = LivePanels::new(&self.panels, cx);
796
797 DockAreaState {
798 version: self.version,
799 center: self.resolved_tree(&self.center, cx).to_state(&source),
800 left_dock: self.dump_dock(DockPlacement::Left, &source, cx),
801 right_dock: self.dump_dock(DockPlacement::Right, &source, cx),
802 bottom_dock: self.dump_dock(DockPlacement::Bottom, &source, cx),
803 }
804 }
805
806 fn dump_dock(
807 &self,
808 placement: DockPlacement,
809 source: &LivePanels<'_>,
810 cx: &App,
811 ) -> Option<DockState> {
812 let pane = self.docks.get(&placement)?;
813 Some(DockState::new(
814 self.resolved_tree(&pane.tree, cx).to_state(source),
815 placement,
816 pane.dock.size(),
817 pane.dock.is_open(),
818 ))
819 }
820
821 fn resolved_tree(&self, tree: &PaneTree, cx: &App) -> PaneTree {
822 let mut tree = tree.clone();
823 self.resolve_sizes(tree.root_mut(), cx);
824 tree
825 }
826
827 fn resolve_sizes(&self, node: &mut PaneNode, cx: &App) {
828 let measured = self
829 .splits
830 .get(&node.id())
831 .map(|cached| cached.entity.read(cx).sizes().clone())
832 .unwrap_or_default();
833
834 let NodeKind::Split {
835 children, sizes, ..
836 } = node.kind_mut()
837 else {
838 return;
839 };
840
841 for (ix, size) in sizes.iter_mut().enumerate() {
842 let on_screen = measured.get(ix).copied().filter(|size| *size > px(0.));
847 let stored = (*size).filter(|size| *size > px(0.));
848 *size = Some(on_screen.or(stored).unwrap_or(PANEL_MIN_SIZE));
849 }
850
851 for child in children.iter_mut() {
852 self.resolve_sizes(child, cx);
853 }
854 }
855}
856
857impl DockArea {
859 fn commit(&mut self, result: EditResult, window: &mut Window, cx: &mut Context<Self>) {
868 self.commit_changed(result.changed(), window, cx);
869 }
870
871 fn commit_changed(&mut self, changed: bool, window: &mut Window, cx: &mut Context<Self>) {
873 if !changed {
874 return;
875 }
876
877 self.reconcile(window, cx);
878 cx.emit(DockEvent::LayoutChanged);
879 }
880
881 fn reconcile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
888 let mut plans = Vec::new();
891 plan_tree(&self.center, false, self.locked, &mut plans);
892 for pane in self.docks.values() {
893 plan_tree(&pane.tree, !pane.dock.is_open(), self.locked, &mut plans);
894 }
895
896 let mut live_nodes = HashSet::with_capacity(plans.len());
900 let mut live_panels: HashSet<PanelId> = HashSet::new();
901
902 for plan in plans {
903 live_nodes.insert(plan.node());
904 match plan {
905 ContainerPlan::Split {
906 node,
907 axis,
908 children,
909 sizes,
910 } => {
911 let state = self.split_entity(node, cx);
912 let (previous, adopted) = self
913 .splits
914 .get(&node)
915 .map(|cached| (cached.children.clone(), cached.sizes.clone()))
916 .unwrap_or_default();
917 if previous != children || adopted != sizes {
925 state.update(cx, |state, cx| {
926 sync_split_panels(state, &previous, &children, &sizes, cx);
927 state.sync_panels_count(axis, children.len(), cx);
928 state.adopt_sizes(&scale_sizes_to(state.container_size(), &sizes), cx);
944 });
945 }
946 if let Some(cached) = self.splits.get_mut(&node) {
947 cached.children = children;
948 cached.sizes = sizes;
949 }
950 }
951 ContainerPlan::Group {
952 node,
953 panels,
954 active_ix,
955 constraints,
956 } => {
957 live_panels.extend(panels.iter().copied());
958 let views = self.views_of(&panels);
959 let group = self.group_entity(node, window, cx);
960 group.update(cx, |group, cx| {
961 group.set_constraints(constraints, window, cx);
965 group.sync_from_tree(views, active_ix, window, cx);
966 });
967 }
968 }
969 }
970
971 self.groups.retain(|node, _| live_nodes.contains(node));
972 self.splits.retain(|node, _| live_nodes.contains(node));
973
974 let departed: Vec<Arc<dyn PanelView>> = self
975 .panels
976 .iter()
977 .filter(|(panel, _)| !live_panels.contains(panel))
978 .map(|(_, view)| view.clone())
979 .collect();
980 self.panels.retain(|panel, _| live_panels.contains(panel));
981
982 let zoom_survives = match self.zoomed {
992 Some(Zoomed::Group(node)) => self.groups.contains_key(&node),
993 None => true,
994 };
995 if !zoom_survives {
996 self.set_zoom(None, window, cx);
997 }
998
999 cx.notify();
1000 for view in departed {
1003 view.on_removed(window, cx);
1004 }
1005 }
1006
1007 fn views_of(&self, panels: &[PanelId]) -> Vec<Arc<dyn PanelView>> {
1009 debug_assert!(
1010 panels.iter().all(|panel| self.panels.contains_key(panel)),
1011 "every panel in a tree must have a live view; a missing one would \
1012 silently shift the group's active index"
1013 );
1014 panels
1015 .iter()
1016 .filter_map(|panel| self.panels.get(panel).cloned())
1017 .collect()
1018 }
1019
1020 fn group_entity(
1021 &mut self,
1022 node: NodeId,
1023 window: &mut Window,
1024 cx: &mut Context<Self>,
1025 ) -> Entity<TabGroup> {
1026 if let Some(cached) = self.groups.get(&node) {
1027 return cached.entity.clone();
1028 }
1029
1030 let renderer = self.renderer.tab_group_renderer();
1031 let entity = cx.new(|cx| TabGroup::new(node, window, cx).with_renderer(renderer));
1032 let subscription = cx.subscribe_in(&entity, window, Self::on_tab_group_event);
1033 self.groups.insert(
1034 node,
1035 Cached {
1036 entity: entity.clone(),
1037 _subscription: subscription,
1038 },
1039 );
1040 entity
1041 }
1042
1043 fn split_entity(&mut self, node: NodeId, cx: &mut Context<Self>) -> Entity<ResizableState> {
1044 if let Some(cached) = self.splits.get(&node) {
1045 return cached.entity.clone();
1046 }
1047
1048 let entity = cx.new(|_| ResizableState::default());
1049 let subscription =
1059 cx.subscribe(&entity, move |this, state, _: &ResizablePanelEvent, cx| {
1060 let sizes: Vec<Option<Pixels>> = state
1061 .read(cx)
1062 .sizes()
1063 .iter()
1064 .map(|size| Some(*size))
1065 .collect();
1066 let Some(region) = this.placement_of_node(node) else {
1067 return;
1068 };
1069 let Some(tree) = this.tree_mut(region) else {
1070 return;
1071 };
1072 if tree.set_sizes(node, sizes.clone()).changed() {
1073 cx.emit(DockEvent::LayoutChanged);
1074 }
1075 if let Some(cached) = this.splits.get_mut(&node) {
1078 cached.sizes = sizes;
1079 }
1080 });
1081 self.splits.insert(
1082 node,
1083 CachedSplit {
1084 entity: entity.clone(),
1085 children: Vec::new(),
1086 sizes: Vec::new(),
1087 _subscription: subscription,
1088 },
1089 );
1090 entity
1091 }
1092}
1093
1094impl DockArea {
1096 fn on_tab_group_event(
1097 &mut self,
1098 group: &Entity<TabGroup>,
1099 event: &TabGroupEvent,
1100 window: &mut Window,
1101 cx: &mut Context<Self>,
1102 ) {
1103 match event {
1104 TabGroupEvent::Drop { panel, target, .. } => {
1105 self.move_panel(*panel, *target, window, cx)
1106 }
1107 TabGroupEvent::DragDrop { item, target } => cx.emit(DockEvent::DragDrop {
1108 item: item.clone(),
1109 target: *target,
1110 }),
1111 TabGroupEvent::ClosePanel { panel } => self.remove_panel_id(*panel, window, cx),
1112 TabGroupEvent::ActiveChanged { ix } => {
1113 let node = group.read(cx).node();
1114 let Some(region) = self.placement_of_node(node) else {
1115 return;
1116 };
1117 let Some(tree) = self.tree_mut(region) else {
1118 return;
1119 };
1120 let result = tree.set_active(node, *ix);
1121 self.commit(result, window, cx);
1122 }
1123 TabGroupEvent::ZoomIn => {
1124 let node = group.read(cx).node();
1125 self.set_zoom(Some(Zoomed::Group(node)), window, cx);
1126 }
1127 TabGroupEvent::ZoomOut => {
1132 let node = group.read(cx).node();
1133 if self.zoomed == Some(Zoomed::Group(node)) {
1134 self.set_zoom(None, window, cx);
1135 }
1136 }
1137 }
1138 }
1139}
1140
1141impl DockArea {
1143 fn adopt_measured_sizes(&mut self, placement: DockPlacement, cx: &App) {
1150 let measured: HashMap<NodeId, Vec<Pixels>> = self
1151 .splits
1152 .iter()
1153 .filter(|(_, cached)| cached.entity.read(cx).container_size() > Pixels::ZERO)
1159 .map(|(node, cached)| (*node, cached.entity.read(cx).sizes().clone()))
1160 .collect();
1161
1162 if let Some(tree) = self.tree_mut(placement) {
1163 tree.adopt_measured_sizes(&measured);
1164 }
1165 }
1166
1167 fn tree_mut(&mut self, placement: DockPlacement) -> Option<&mut PaneTree> {
1168 match placement {
1169 DockPlacement::Center => Some(&mut self.center),
1170 _ => self.docks.get_mut(&placement).map(|pane| &mut pane.tree),
1171 }
1172 }
1173
1174 fn placement_of_node(&self, node: NodeId) -> Option<DockPlacement> {
1177 if self.center.find_node(node).is_some() {
1178 return Some(DockPlacement::Center);
1179 }
1180 self.docks
1181 .iter()
1182 .find(|(_, pane)| pane.tree.find_node(node).is_some())
1183 .map(|(placement, _)| *placement)
1184 }
1185
1186 fn placement_of_panel(&self, panel: PanelId) -> Option<DockPlacement> {
1187 if self.center.find_panel_node(panel).is_some() {
1188 return Some(DockPlacement::Center);
1189 }
1190 self.docks
1191 .iter()
1192 .find(|(_, pane)| pane.tree.find_panel_node(panel).is_some())
1193 .map(|(placement, _)| *placement)
1194 }
1195
1196 fn resize_dock(
1199 &mut self,
1200 placement: DockPlacement,
1201 pointer: Point<Pixels>,
1202 cx: &mut Context<Self>,
1203 ) {
1204 let opposite = match placement {
1205 DockPlacement::Left => self.dock_size(DockPlacement::Right),
1206 DockPlacement::Right => self.dock_size(DockPlacement::Left),
1207 _ => None,
1208 };
1209 let sizing = DockSizing::new(placement)
1210 .with_area_bounds(self.bounds)
1211 .with_opposite_dock_size(opposite.unwrap_or(px(0.)));
1212 let size = sizing.clamp(sizing.size_from_pointer(pointer));
1213
1214 if let Some(pane) = self.docks.get_mut(&placement) {
1215 pane.dock.set_size(size);
1216 cx.notify();
1217 }
1218 }
1219}
1220
1221impl DockArea {
1223 fn render_node(&self, node: &PaneNode, window: &mut Window, cx: &mut App) -> AnyElement {
1225 match node.kind() {
1226 PaneRef::Split {
1227 axis,
1228 children,
1229 sizes,
1230 } => {
1231 let group = match axis {
1232 Axis::Horizontal => h_resizable(("dock-split", node.id().as_u64())),
1233 Axis::Vertical => v_resizable(("dock-split", node.id().as_u64())),
1234 };
1235 let shown: Vec<bool> = children
1239 .iter()
1240 .map(|child| self.is_node_visible(child, cx))
1241 .collect();
1242 let grows = shown.iter().rposition(|shown| *shown);
1248 let panels: Vec<_> = children
1249 .iter()
1250 .zip(sizes.iter())
1251 .enumerate()
1252 .map(|(ix, (child, size))| {
1253 resizable_panel()
1254 .visible(shown[ix])
1255 .child(self.render_node(child, window, cx))
1256 .when_some(*size, |panel, size| {
1268 panel
1269 .size(size)
1270 .when(Some(ix) != grows, |panel| panel.flex_none())
1271 })
1272 })
1273 .collect();
1274
1275 let group = group
1276 .when_some(self.splits.get(&node.id()), |group, cached| {
1277 group.with_state(&cached.entity)
1278 })
1279 .with_handle_appearance({
1280 let renderer = self.renderer.clone();
1281 Rc::new(move |handle, window, cx| {
1282 renderer.render_split_handle(handle, window, cx)
1283 })
1284 })
1285 .children(panels);
1286
1287 self.renderer
1288 .split_frame(node.id(), axis, window, cx)
1289 .size_full()
1297 .flex_1()
1298 .min_h(px(0.))
1299 .overflow_hidden()
1300 .child(group)
1301 .into_any_element()
1302 }
1303 PaneRef::Tabs { .. } => match self.groups.get(&node.id()) {
1304 Some(cached) => cached.entity.clone().into_any_element(),
1305 None => Empty.into_any_element(),
1306 },
1307 }
1308 }
1309
1310 fn is_node_visible(&self, node: &PaneNode, cx: &App) -> bool {
1316 let panels = LivePanels::new(&self.panels, cx);
1317 match node.kind() {
1318 PaneRef::Split { children, .. } => {
1319 children.iter().any(|child| self.is_node_visible(child, cx))
1320 }
1321 PaneRef::Tabs { panels: ids, .. } => ids.iter().any(|panel| panels.is_visible(*panel)),
1322 }
1323 }
1324
1325 fn render_dock(
1326 &self,
1327 placement: DockPlacement,
1328 window: &mut Window,
1329 cx: &mut App,
1330 ) -> Option<AnyElement> {
1331 let pane = self.docks.get(&placement)?;
1332 let dock = self.dock_context(placement, &pane.dock);
1333
1334 let size = dock_extent(&dock);
1339 if size <= px(0.) {
1340 return Some(div().into_any_element());
1341 }
1342
1343 let content = self.render_node(pane.tree.root(), window, cx);
1344 let chrome = self.renderer.render_dock(&dock, content, window, cx);
1353 Some(dock_frame(&dock, size).child(chrome).into_any_element())
1354 }
1355
1356 fn dock_context(&self, placement: DockPlacement, dock: &Dock) -> DockContext {
1357 let area = self.this.clone();
1358
1359 DockContext {
1360 placement,
1361 size: dock.size(),
1362 open: dock.is_open(),
1363 collapsible: dock.is_collapsible(),
1364 on_toggle: {
1365 let area = area.clone();
1366 Rc::new(move |window, cx| {
1367 _ = area.update(cx, |area, cx| area.toggle_dock(placement, window, cx));
1368 })
1369 },
1370 on_resize: Rc::new(move |pointer, _, cx| {
1371 _ = area.update(cx, |area, cx| area.resize_dock(placement, pointer, cx));
1372 }),
1373 }
1374 }
1375}
1376
1377impl EventEmitter<DockEvent> for DockArea {}
1378
1379impl Focusable for DockArea {
1380 fn focus_handle(&self, _: &App) -> FocusHandle {
1381 self.focus_handle.clone()
1382 }
1383}
1384
1385impl Render for DockArea {
1386 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1387 let area = cx.entity();
1388 let renderer = self.renderer.clone();
1389
1390 renderer
1391 .frame(window, cx)
1392 .test_support()
1393 .relative()
1400 .size_full()
1401 .overflow_hidden()
1402 .flex()
1403 .flex_row()
1404 .on_prepaint(move |bounds, _, cx| {
1405 area.update(cx, |area, _| area.bounds = bounds);
1406 })
1407 .track_focus(&self.focus_handle)
1408 .map(|frame| match self.zoomed_view() {
1409 Some(view) => frame.child(view),
1410 None => frame
1411 .when_some(
1412 self.render_dock(DockPlacement::Left, window, cx),
1413 ParentElement::child,
1414 )
1415 .child(
1416 renderer
1417 .center_frame(window, cx)
1418 .flex()
1423 .flex_1()
1424 .flex_col()
1425 .overflow_hidden()
1426 .child(self.render_node(self.center.root(), window, cx))
1427 .when_some(
1428 self.render_dock(DockPlacement::Bottom, window, cx),
1429 ParentElement::child,
1430 ),
1431 )
1432 .when_some(
1433 self.render_dock(DockPlacement::Right, window, cx),
1434 ParentElement::child,
1435 ),
1436 })
1437 }
1438}
1439
1440enum ContainerPlan {
1442 Split {
1443 node: NodeId,
1444 axis: Axis,
1445 children: Vec<NodeId>,
1446 sizes: Vec<Option<Pixels>>,
1447 },
1448 Group {
1449 node: NodeId,
1450 panels: Vec<PanelId>,
1451 active_ix: usize,
1452 constraints: TabGroupConstraints,
1453 },
1454}
1455
1456impl ContainerPlan {
1457 fn node(&self) -> NodeId {
1458 match self {
1459 Self::Split { node, .. } | Self::Group { node, .. } => *node,
1460 }
1461 }
1462}
1463
1464fn scale_sizes_to(container: Pixels, sizes: &[Option<Pixels>]) -> Vec<Option<Pixels>> {
1471 let total: f32 = sizes.iter().flatten().map(|size| size.as_f32()).sum();
1472 if container <= px(0.) || total <= 0. || sizes.iter().any(Option::is_none) {
1473 return sizes.to_vec();
1474 }
1475
1476 let scale = container.as_f32() / total;
1477 sizes
1478 .iter()
1479 .map(|size| size.map(|size| px(size.as_f32() * scale)))
1480 .collect()
1481}
1482
1483fn sync_split_panels(
1493 state: &mut ResizableState,
1494 previous: &[NodeId],
1495 next: &[NodeId],
1496 sizes: &[Option<Pixels>],
1497 cx: &mut Context<ResizableState>,
1498) {
1499 let mut current = previous.to_vec();
1500
1501 for ix in (0..current.len()).rev() {
1503 if !next.contains(¤t[ix]) {
1504 if ix < state.sizes().len() {
1505 state.remove_panel(ix, cx);
1506 }
1507 current.remove(ix);
1508 }
1509 }
1510
1511 for (ix, node) in next.iter().enumerate() {
1512 if current.get(ix) == Some(node) {
1513 continue;
1514 }
1515 let at = ix.min(state.sizes().len());
1516 state.insert_panel(sizes.get(ix).copied().flatten(), Some(at), cx);
1521 current.insert(at.min(current.len()), *node);
1522 }
1523
1524 debug_assert_eq!(
1525 current, next,
1526 "the split's panel list must end up mirroring its children exactly; \
1527 a reordering edit would need its own case here"
1528 );
1529}
1530
1531fn plan_tree(tree: &PaneTree, collapsed: bool, locked: bool, out: &mut Vec<ContainerPlan>) {
1532 plan_node(tree.root(), true, collapsed, locked, out);
1534}
1535
1536fn plan_node(
1537 node: &PaneNode,
1538 alone: bool,
1539 collapsed: bool,
1540 locked: bool,
1541 out: &mut Vec<ContainerPlan>,
1542) {
1543 match node.kind() {
1544 PaneRef::Split {
1545 axis,
1546 children,
1547 sizes,
1548 } => {
1549 out.push(ContainerPlan::Split {
1550 node: node.id(),
1551 axis,
1552 children: children.iter().map(PaneNode::id).collect(),
1553 sizes: sizes.to_vec(),
1554 });
1555 let children_alone = children.len() <= 1;
1556 for child in children {
1557 plan_node(child, children_alone, collapsed, locked, out);
1558 }
1559 }
1560 PaneRef::Tabs { panels, active_ix } => out.push(ContainerPlan::Group {
1561 node: node.id(),
1562 panels: panels.to_vec(),
1563 active_ix,
1564 constraints: TabGroupConstraints::in_split(alone)
1565 .dock_locked(locked)
1566 .collapsed(collapsed),
1567 }),
1568 }
1569}
1570
1571fn first_tab_group(node: &PaneNode) -> Option<NodeId> {
1572 match node.kind() {
1573 PaneRef::Tabs { .. } => Some(node.id()),
1574 PaneRef::Split { children, .. } => children.iter().find_map(first_tab_group),
1575 }
1576}
1577
1578fn target_node(target: &InsertTarget) -> NodeId {
1579 match target {
1580 InsertTarget::Tabs { node, .. } | InsertTarget::Split { node, .. } => *node,
1581 }
1582}
1583
1584struct RegistryPanelBuilder<'a, 'w, 'c> {
1586 dock_area: WeakEntity<DockArea>,
1587 renderer: Rc<dyn DockAreaRenderer>,
1588 built: &'a mut Vec<(PanelId, Arc<dyn PanelView>)>,
1589 window: &'w mut Window,
1590 cx: &'c mut App,
1591}
1592
1593impl PanelBuilder for RegistryPanelBuilder<'_, '_, '_> {
1594 fn build(&mut self, state: &PanelState, info: &PanelInfo) -> PanelId {
1595 let context = PanelBuildContext::new(self.dock_area.clone(), state, info);
1596 let view =
1597 match PanelRegistry::build_panel(&state.panel_name, context, self.window, self.cx) {
1598 Some(view) => view,
1599 None => self
1600 .renderer
1601 .build_placeholder(state, self.window, self.cx)
1602 .unwrap_or_else(|| {
1603 Arc::new(self.cx.new(|cx| PlaceholderPanel::new(state.clone(), cx)))
1604 as Arc<dyn PanelView>
1605 }),
1606 };
1607
1608 let id = view.panel_id(self.cx);
1609 self.built.push((id, view));
1610 id
1611 }
1612}
1613
1614struct PlaceholderPanel {
1621 state: PanelState,
1622 focus_handle: FocusHandle,
1623}
1624
1625impl PlaceholderPanel {
1626 fn new(state: PanelState, cx: &mut Context<Self>) -> Self {
1627 Self {
1628 state,
1629 focus_handle: cx.focus_handle(),
1630 }
1631 }
1632}
1633
1634impl Panel for PlaceholderPanel {
1635 fn panel_name(&self) -> &'static str {
1636 "InvalidPanel"
1637 }
1638
1639 fn dump(&self, _: &App) -> PanelState {
1640 self.state.clone()
1641 }
1642}
1643
1644impl EventEmitter<PanelEvent> for PlaceholderPanel {}
1645
1646impl Focusable for PlaceholderPanel {
1647 fn focus_handle(&self, _: &App) -> FocusHandle {
1648 self.focus_handle.clone()
1649 }
1650}
1651
1652impl Render for PlaceholderPanel {
1653 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1654 Empty
1655 }
1656}
1657
1658type DockToggleHandler = Rc<dyn Fn(&mut Window, &mut App)>;
1659type DockResizeHandler = Rc<dyn Fn(Point<Pixels>, &mut Window, &mut App)>;
1660
1661#[derive(Clone)]
1664pub struct DockContext {
1665 placement: DockPlacement,
1666 size: Pixels,
1667 open: bool,
1668 collapsible: bool,
1669 on_toggle: DockToggleHandler,
1670 on_resize: DockResizeHandler,
1671}
1672
1673impl DockContext {
1674 pub fn placement(&self) -> DockPlacement {
1675 self.placement
1676 }
1677
1678 pub fn size(&self) -> Pixels {
1681 self.size
1682 }
1683
1684 pub fn is_open(&self) -> bool {
1685 self.open
1686 }
1687
1688 pub fn is_collapsible(&self) -> bool {
1689 self.collapsible
1690 }
1691
1692 pub fn toggle(&self, window: &mut Window, cx: &mut App) {
1693 (self.on_toggle)(window, cx);
1694 }
1695
1696 pub fn resize_to(&self, pointer: Point<Pixels>, window: &mut Window, cx: &mut App) {
1699 (self.on_resize)(pointer, window, cx);
1700 }
1701}
1702
1703pub const CLOSED_BOTTOM_STRIP: Pixels = px(29.);
1707
1708pub fn dock_extent(dock: &DockContext) -> Pixels {
1710 match (dock.is_open(), dock.placement()) {
1711 (true, _) => dock.size(),
1712 (false, DockPlacement::Bottom) => CLOSED_BOTTOM_STRIP,
1713 (false, _) => px(0.),
1714 }
1715}
1716
1717pub fn dock_frame(dock: &DockContext, size: Pixels) -> Div {
1723 div()
1724 .flex()
1725 .flex_none()
1726 .relative()
1727 .overflow_hidden()
1728 .map(|this| match dock.placement() {
1729 DockPlacement::Left | DockPlacement::Right => this.flex_row().h_full().w(size),
1730 DockPlacement::Bottom => this.w_full().h(size),
1731 DockPlacement::Center => this,
1733 })
1734}
1735
1736#[allow(unused_variables)]
1747pub trait DockAreaRenderer: 'static {
1748 fn frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
1754 div().id("dock-area")
1755 }
1756
1757 fn split_frame(
1767 &self,
1768 node: NodeId,
1769 axis: Axis,
1770 window: &mut Window,
1771 cx: &mut App,
1772 ) -> Stateful<Div> {
1773 div().id(("dock-split-frame", node.as_u64()))
1774 }
1775
1776 fn center_frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
1780 div().id("dock-area-center")
1781 }
1782
1783 fn render_split_handle(
1790 &self,
1791 handle: &ResizeHandleContext,
1792 window: &mut Window,
1793 cx: &mut App,
1794 ) -> Option<AnyElement> {
1795 None
1796 }
1797
1798 fn render_dock(
1807 &self,
1808 dock: &DockContext,
1809 content: AnyElement,
1810 window: &mut Window,
1811 cx: &mut App,
1812 ) -> AnyElement {
1813 content
1814 }
1815
1816 fn build_placeholder(
1831 &self,
1832 state: &PanelState,
1833 window: &mut Window,
1834 cx: &mut App,
1835 ) -> Option<Arc<dyn PanelView>> {
1836 None
1837 }
1838
1839 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer>;
1840}
1841
1842struct BareDockArea;
1844
1845impl DockAreaRenderer for BareDockArea {
1846 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
1847 Rc::new(BareTabGroup)
1848 }
1849}
1850
1851#[cfg(test)]
1852impl DockArea {
1853 pub(crate) fn container_entity_ids(&self) -> Vec<(NodeId, gpui::EntityId)> {
1860 let mut ids: Vec<(NodeId, gpui::EntityId)> = self
1861 .groups
1862 .iter()
1863 .map(|(node, cached)| (*node, cached.entity.entity_id()))
1864 .chain(
1865 self.splits
1866 .iter()
1867 .map(|(node, cached)| (*node, cached.entity.entity_id())),
1868 )
1869 .collect();
1870 ids.sort();
1871 ids
1872 }
1873}
1874
1875#[cfg(test)]
1876mod tests {
1877 use gpui::{TestAppContext, VisualTestContext};
1878
1879 use std::{
1880 cell::{Cell, RefCell},
1881 rc::Rc,
1882 };
1883
1884 use super::*;
1885 use crate::dock::TabGroupContext;
1886 use crate::dock::test_support::{Log, PanelSignal, TestPanel, drain, drain_active, log_of};
1887
1888 #[test]
1891 fn slot_sizes_are_re_expressed_as_shares_of_the_container() {
1892 let scaled = scale_sizes_to(px(800.), &[Some(px(300.)), Some(px(100.))]);
1893
1894 assert_eq!(scaled, vec![Some(px(600.)), Some(px(200.))]);
1895 }
1896
1897 #[test]
1900 fn an_unconstrained_slot_leaves_every_size_alone() {
1901 let sizes = [Some(px(300.)), None];
1902
1903 assert_eq!(scale_sizes_to(px(800.), &sizes), sizes.to_vec());
1904 }
1905
1906 #[test]
1909 fn an_unusable_container_or_total_leaves_every_size_alone() {
1910 let sizes = [Some(px(300.)), Some(px(100.))];
1911 assert_eq!(scale_sizes_to(px(0.), &sizes), sizes.to_vec());
1912
1913 let zeroed = [Some(px(0.)), Some(px(0.))];
1914 assert_eq!(scale_sizes_to(px(800.), &zeroed), zeroed.to_vec());
1915 }
1916
1917 fn setup(cx: &mut TestAppContext) -> (Entity<DockArea>, &mut VisualTestContext) {
1918 cx.update(|cx| {
1919 let _ = crate::Theme::global_mut(cx);
1920 });
1921 cx.add_window_view(|window, cx| DockArea::new("test-dock", None, window, cx))
1922 }
1923
1924 #[gpui::test]
1925 fn dock_size_change_emits_one_layout_event(cx: &mut TestAppContext) {
1926 let (area, cx) = setup(cx);
1927 cx.update(|window, cx| {
1928 area.update(cx, |area, cx| {
1929 area.set_dock(
1930 DockPlacement::Left,
1931 DockLayout::tabs().panel(TestPanel::new("Left", cx)),
1932 window,
1933 cx,
1934 );
1935 });
1936 });
1937
1938 let events = Rc::new(Cell::new(0));
1939 let observed = events.clone();
1940 let _subscription = cx.update(|window, cx| {
1941 window.subscribe(&area, cx, move |_, event: &DockEvent, _, _| {
1942 if matches!(event, DockEvent::LayoutChanged) {
1943 observed.set(observed.get() + 1);
1944 }
1945 })
1946 });
1947
1948 cx.update(|window, cx| {
1949 area.update(cx, |area, cx| {
1950 area.set_dock_size(DockPlacement::Left, px(320.), window, cx);
1951 area.set_dock_size(DockPlacement::Left, px(320.), window, cx);
1952 });
1953 });
1954 assert_eq!(
1955 events.get(),
1956 1,
1957 "only an effective size change is persisted"
1958 );
1959 }
1960
1961 fn two_groups<'a>(
1963 log: &Log,
1964 cx: &'a mut TestAppContext,
1965 ) -> (
1966 Entity<DockArea>,
1967 Entity<TestPanel>,
1968 &'a mut VisualTestContext,
1969 ) {
1970 let (area, cx) = setup(cx);
1971 let log = log.clone();
1972 let alpha = cx.update(|window, cx| {
1973 let alpha = TestPanel::logging("Alpha", &log, cx);
1974 let beta = TestPanel::logging("Beta", &log, cx);
1975 area.update(cx, |area, cx| {
1976 area.set_center(
1977 DockLayout::h_split()
1978 .child(DockLayout::tabs().panel(alpha.clone()), None)
1979 .child(DockLayout::tabs().panel(beta), None),
1980 window,
1981 cx,
1982 );
1983 });
1984 alpha
1985 });
1986 (area, alpha, cx)
1987 }
1988
1989 fn child_node(area: &Entity<DockArea>, ix: usize, cx: &mut VisualTestContext) -> NodeId {
1991 cx.read(|cx| {
1992 let PaneRef::Split { children, .. } = area
1993 .read(cx)
1994 .layout(DockPlacement::Center)
1995 .unwrap()
1996 .root()
1997 .kind()
1998 else {
1999 panic!("the center root is a split");
2000 };
2001 children[ix].id()
2002 })
2003 }
2004
2005 fn panel_id_of(panel: &Entity<TestPanel>) -> PanelId {
2006 PanelId::from(panel.entity_id())
2007 }
2008
2009 fn move_alpha_into_the_other_group(
2010 area: &Entity<DockArea>,
2011 alpha: &Entity<TestPanel>,
2012 cx: &mut VisualTestContext,
2013 ) {
2014 let target = child_node(area, 1, cx);
2015 let alpha_id = panel_id_of(alpha);
2016 cx.update(|window, cx| {
2017 area.update(cx, |area, cx| {
2018 area.move_panel(
2019 alpha_id,
2020 InsertTarget::Tabs {
2021 node: target,
2022 ix: None,
2023 activate: true,
2024 },
2025 window,
2026 cx,
2027 );
2028 });
2029 });
2030 cx.run_until_parked();
2031 }
2032
2033 fn collect_sizes(state: &PanelState, out: &mut Vec<Pixels>) {
2034 if let PanelInfo::Stack { sizes, .. } = &state.info {
2035 out.extend(sizes.iter().copied());
2036 }
2037 for child in &state.children {
2038 collect_sizes(child, out);
2039 }
2040 }
2041
2042 fn register_test_panels(cx: &mut App) {
2043 for name in ["Alpha", "Beta", "Gamma"] {
2044 crate::dock::registry::register_panel(cx, name, move |_, _, cx| {
2045 Arc::new(TestPanel::new(name, cx)) as Arc<dyn PanelView>
2046 });
2047 }
2048 }
2049
2050 fn one_group<'a>(
2056 log: &Log,
2057 names: &[&'static str],
2058 active_ix: Option<usize>,
2059 cx: &'a mut TestAppContext,
2060 ) -> (
2061 Entity<DockArea>,
2062 Vec<Entity<TestPanel>>,
2063 &'a mut VisualTestContext,
2064 ) {
2065 let (area, cx) = setup(cx);
2066 let log = log.clone();
2067 let names = names.to_vec();
2068 let panels = cx.update(|window, cx| {
2069 let panels: Vec<_> = names
2070 .iter()
2071 .map(|name| TestPanel::logging(name, &log, cx))
2072 .collect();
2073 let layout = panels
2074 .iter()
2075 .fold(DockLayout::tabs(), |layout, panel| {
2076 layout.panel(panel.clone())
2077 })
2078 .active_index(active_ix.unwrap_or(0));
2079 area.update(cx, |area, cx| area.set_center(layout, window, cx));
2080 panels
2081 });
2082 (area, panels, cx)
2083 }
2084
2085 fn group_of(
2087 area: &Entity<DockArea>,
2088 ix: usize,
2089 cx: &mut VisualTestContext,
2090 ) -> Entity<TabGroup> {
2091 let node = child_node(area, ix, cx);
2092 cx.read(|cx| area.read(cx).groups.get(&node).unwrap().entity.clone())
2093 }
2094
2095 fn move_panel_into(
2096 area: &Entity<DockArea>,
2097 panel: PanelId,
2098 node: NodeId,
2099 ix: Option<usize>,
2100 activate: bool,
2101 cx: &mut VisualTestContext,
2102 ) {
2103 cx.update(|window, cx| {
2104 area.update(cx, |area, cx| {
2105 area.move_panel(panel, InsertTarget::Tabs { node, ix, activate }, window, cx);
2106 });
2107 });
2108 cx.run_until_parked();
2109 }
2110
2111 fn is_center_empty(area: &Entity<DockArea>, cx: &mut VisualTestContext) -> bool {
2112 cx.read(|cx| area.read(cx).is_empty(DockPlacement::Center, cx))
2113 }
2114
2115 #[gpui::test]
2116 fn a_layout_installs_and_dumps_back_to_the_same_state(cx: &mut TestAppContext) {
2117 let (area, cx) = setup(cx);
2118 cx.update(|window, cx| {
2119 let alpha = TestPanel::new("Alpha", cx);
2120 let beta = TestPanel::new("Beta", cx);
2121 area.update(cx, |area, cx| {
2122 area.set_center(
2123 DockLayout::h_split()
2124 .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2125 .child(DockLayout::tabs().panel(beta), None),
2126 window,
2127 cx,
2128 );
2129 });
2130 });
2131
2132 let state = cx.read(|cx| area.read(cx).dump(cx));
2133 assert_eq!(state.center.panel_name, "StackPanel");
2134 assert_eq!(state.center.children.len(), 2);
2135 assert_eq!(state.center.children[0].children[0].panel_name, "Alpha");
2136 assert_eq!(state.center.children[1].children[0].panel_name, "Beta");
2137 }
2138
2139 #[gpui::test]
2140 fn moving_a_panel_reuses_its_entity(cx: &mut TestAppContext) {
2141 let log = log_of();
2142 let (area, alpha, cx) = two_groups(&log, cx);
2143 cx.run_until_parked();
2144 drain(&log);
2145
2146 let destination = child_node(&area, 1, cx);
2147 let destination_entity = cx.read(|cx| {
2148 area.read(cx)
2149 .groups
2150 .get(&destination)
2151 .unwrap()
2152 .entity
2153 .entity_id()
2154 });
2155
2156 move_alpha_into_the_other_group(&area, &alpha, cx);
2157
2158 assert_eq!(
2159 cx.read(|cx| area
2160 .read(cx)
2161 .groups
2162 .get(&destination)
2163 .unwrap()
2164 .entity
2165 .entity_id()),
2166 destination_entity,
2167 "the group the panel arrived in was reused, not rebuilt"
2168 );
2169
2170 let alpha_id = panel_id_of(&alpha);
2175 assert!(
2176 cx.read(|cx| area
2177 .read(cx)
2178 .layout(DockPlacement::Center)
2179 .unwrap()
2180 .find_panel_node(alpha_id))
2181 .is_some(),
2182 "the moved panel is still in the tree"
2183 );
2184
2185 let state = cx.read(|cx| area.read(cx).dump(cx));
2186 assert_eq!(
2190 state.center.children.len(),
2191 1,
2192 "the emptied group collapsed out of the split"
2193 );
2194 assert_eq!(
2195 state.center.children[0].children.len(),
2196 2,
2197 "both panels now share the surviving group"
2198 );
2199 }
2200
2201 #[gpui::test]
2202 fn a_moved_panel_is_not_told_it_was_removed(cx: &mut TestAppContext) {
2203 let log = log_of();
2204 let (area, alpha, cx) = two_groups(&log, cx);
2205 cx.run_until_parked();
2206 drain(&log);
2207
2208 move_alpha_into_the_other_group(&area, &alpha, cx);
2209
2210 assert!(
2211 !drain(&log).contains(&("Alpha", PanelSignal::Removed)),
2212 "moving a panel between groups must never deliver on_removed"
2213 );
2214 }
2215
2216 #[gpui::test]
2217 fn removing_a_panel_does_tell_it_it_was_removed(cx: &mut TestAppContext) {
2218 let log = log_of();
2222 let (area, alpha, cx) = two_groups(&log, cx);
2223 cx.run_until_parked();
2224 drain(&log);
2225
2226 cx.update(|window, cx| {
2227 area.update(cx, |area, cx| area.remove_panel(alpha.clone(), window, cx));
2228 });
2229 cx.run_until_parked();
2230
2231 assert!(
2232 drain(&log).contains(&("Alpha", PanelSignal::Removed)),
2233 "a genuine removal must deliver on_removed"
2234 );
2235 }
2236
2237 #[gpui::test]
2238 fn reconciling_an_unchanged_tree_creates_no_entities(cx: &mut TestAppContext) {
2239 let log = log_of();
2240 let (area, _alpha, cx) = two_groups(&log, cx);
2241 cx.run_until_parked();
2242 drain(&log);
2243
2244 let before = cx.read(|cx| area.read(cx).container_entity_ids());
2245 cx.update(|window, cx| area.update(cx, |area, cx| area.reconcile(window, cx)));
2246 let after = cx.read(|cx| area.read(cx).container_entity_ids());
2247
2248 assert!(!before.is_empty(), "there were containers to preserve");
2249 assert_eq!(
2250 before, after,
2251 "a steady-state pass creates and drops nothing"
2252 );
2253 cx.run_until_parked();
2254 assert_eq!(
2255 drain(&log),
2256 vec![],
2257 "and no panel was re-added or re-activated by it"
2258 );
2259 }
2260
2261 #[gpui::test]
2262 fn a_loaded_layout_round_trips_through_dump(cx: &mut TestAppContext) {
2263 let (area, cx) = setup(cx);
2264 cx.update(|_, cx| register_test_panels(cx));
2265
2266 let json = include_str!("fixtures/nested_splits.json");
2267 let state: DockAreaState = serde_json::from_str(json).unwrap();
2268
2269 cx.update(|window, cx| {
2270 area.update(cx, |area, cx| area.load(state.clone(), window, cx).unwrap())
2271 });
2272 let dumped = cx.read(|cx| area.read(cx).dump(cx));
2273
2274 cx.update(|window, cx| {
2275 area.update(cx, |area, cx| {
2276 area.load(dumped.clone(), window, cx).unwrap()
2277 })
2278 });
2279 let again = cx.read(|cx| area.read(cx).dump(cx));
2280
2281 assert_eq!(dumped, again, "load/dump must reach a fixpoint");
2282 assert_eq!(
2283 dumped.center.children.len(),
2284 3,
2285 "the fixture's nesting is flattened, as the state layer already pins"
2286 );
2287 assert_eq!(dumped.center.children[0].children[0].panel_name, "Alpha");
2288 }
2289
2290 #[gpui::test]
2291 fn a_dumped_live_layout_has_no_zero_sizes(cx: &mut TestAppContext) {
2292 let (area, cx) = setup(cx);
2293 cx.update(|window, cx| {
2294 let alpha = TestPanel::new("Alpha", cx);
2295 let beta = TestPanel::new("Beta", cx);
2296 let gamma = TestPanel::new("Gamma", cx);
2297 area.update(cx, |area, cx| {
2298 area.set_center(
2299 DockLayout::v_split()
2300 .child(DockLayout::tabs().panel(alpha), None)
2303 .child(DockLayout::tabs().panel(beta), Some(px(0.)))
2306 .child(DockLayout::tabs().panel(gamma), Some(px(240.))),
2307 window,
2308 cx,
2309 );
2310 });
2311 });
2312
2313 let state = cx.read(|cx| area.read(cx).dump(cx));
2314 let mut sizes = Vec::new();
2315 collect_sizes(&state.center, &mut sizes);
2316
2317 assert!(!sizes.is_empty(), "the layout has slots to check");
2318 assert!(
2319 sizes.iter().all(|size| *size > px(0.)),
2320 "an older build reads a persisted 0.0 back as a real zero-pixel panel: {sizes:?}"
2321 );
2322 }
2323
2324 #[gpui::test]
2331 fn a_dumped_split_writes_the_sizes_it_is_actually_drawn_at(cx: &mut TestAppContext) {
2332 let (area, cx) = setup(cx);
2333 cx.update(|window, cx| {
2334 let alpha = TestPanel::new("Alpha", cx);
2335 let beta = TestPanel::new("Beta", cx);
2336 area.update(cx, |area, cx| {
2337 area.set_center(
2338 DockLayout::h_split()
2339 .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2340 .child(DockLayout::tabs().panel(beta), Some(px(300.))),
2341 window,
2342 cx,
2343 );
2344 });
2345 });
2346 cx.run_until_parked();
2347
2348 let root = cx.read(|cx| {
2349 area.read(cx)
2350 .layout(DockPlacement::Center)
2351 .unwrap()
2352 .root()
2353 .id()
2354 });
2355 let measured = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2356 assert_ne!(
2357 measured,
2358 vec![px(300.), px(300.)],
2359 "the split has to have been rescaled by a layout pass, or this \
2360 test cannot tell the two preferences apart"
2361 );
2362
2363 let state = cx.read(|cx| area.read(cx).dump(cx));
2364 let PanelInfo::Stack { sizes, .. } = &state.center.info else {
2365 panic!("the center writes a stack");
2366 };
2367 assert_eq!(
2368 sizes, &measured,
2369 "the written sizes are the ones on screen, not the ones the tree \
2370 was built from"
2371 );
2372 }
2373
2374 #[gpui::test]
2380 fn a_panel_dropped_beside_another_takes_half_the_split(cx: &mut TestAppContext) {
2381 let log = Log::default();
2382 let (area, panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2383 let group = child_node(&area, 0, cx);
2384 let beta = panel_id_of(&panels[1]);
2385
2386 cx.update(|window, cx| {
2387 area.update(cx, |area, cx| {
2388 area.move_panel(
2389 beta,
2390 InsertTarget::Split {
2391 node: group,
2392 placement: Placement::Right,
2393 size: None,
2394 },
2395 window,
2396 cx,
2397 );
2398 });
2399 });
2400 cx.run_until_parked();
2401
2402 let root = cx.read(|cx| {
2403 area.read(cx)
2404 .layout(DockPlacement::Center)
2405 .unwrap()
2406 .root()
2407 .id()
2408 });
2409 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2410
2411 assert_eq!(sizes.len(), 2, "the drop splits the center in two");
2412 let (left, right) = (sizes[0].as_f32(), sizes[1].as_f32());
2413 assert!(
2414 (left - right).abs() <= (left + right) * 0.02,
2415 "the two halves must be within 2% of each other, got {left} and {right}"
2416 );
2417 }
2418
2419 #[gpui::test]
2423 fn a_panel_dropped_across_the_axis_still_takes_half(cx: &mut TestAppContext) {
2424 let log = Log::default();
2425 let (area, panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2426 let group = child_node(&area, 0, cx);
2427 let beta = panel_id_of(&panels[1]);
2428
2429 cx.update(|window, cx| {
2430 area.update(cx, |area, cx| {
2431 area.move_panel(
2432 beta,
2433 InsertTarget::Split {
2434 node: group,
2435 placement: Placement::Bottom,
2436 size: None,
2437 },
2438 window,
2439 cx,
2440 );
2441 });
2442 });
2443 cx.run_until_parked();
2444
2445 let wrapper = child_node(&area, 0, cx);
2449 let sizes = cx.read(|cx| {
2450 area.read(cx).splits[&wrapper]
2451 .entity
2452 .read(cx)
2453 .sizes()
2454 .clone()
2455 });
2456 assert_eq!(sizes.len(), 2, "the drop splits the group in two");
2457 let (top, bottom) = (sizes[0].as_f32(), sizes[1].as_f32());
2458 assert!(
2459 (top - bottom).abs() <= (top + bottom) * 0.02,
2460 "the two halves must be within 2% of each other, got {top} and {bottom}"
2461 );
2462 }
2463
2464 #[gpui::test]
2467 fn a_panel_dropped_into_a_populated_split_takes_an_even_share(cx: &mut TestAppContext) {
2468 let (area, cx) = setup(cx);
2469 let panels = cx.update(|window, cx| {
2470 let alpha = TestPanel::new("Alpha", cx);
2471 let beta = TestPanel::new("Beta", cx);
2472 let gamma = TestPanel::new("Gamma", cx);
2473 area.update(cx, |area, cx| {
2474 area.set_center(
2475 DockLayout::h_split()
2476 .child(DockLayout::tabs().panel(alpha.clone()), Some(px(240.)))
2477 .child(
2478 DockLayout::tabs().panel(beta.clone()).panel(gamma.clone()),
2479 None,
2480 ),
2481 window,
2482 cx,
2483 );
2484 });
2485 vec![alpha, beta, gamma]
2486 });
2487 cx.run_until_parked();
2488
2489 let right = child_node(&area, 1, cx);
2490 let gamma = panel_id_of(&panels[2]);
2491 cx.update(|window, cx| {
2492 area.update(cx, |area, cx| {
2493 area.move_panel(
2494 gamma,
2495 InsertTarget::Split {
2496 node: right,
2497 placement: Placement::Right,
2498 size: None,
2499 },
2500 window,
2501 cx,
2502 );
2503 });
2504 });
2505 cx.run_until_parked();
2506
2507 let root = cx.read(|cx| {
2508 area.read(cx)
2509 .layout(DockPlacement::Center)
2510 .unwrap()
2511 .root()
2512 .id()
2513 });
2514 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2515 assert_eq!(sizes.len(), 3, "three slots side by side");
2516 let dropped = sizes[2].as_f32();
2517 let neighbour = sizes[1].as_f32();
2518 assert!(
2519 (dropped - neighbour).abs() <= (dropped + neighbour) * 0.02,
2520 "the dropped panel splits its neighbour evenly, got neighbour {neighbour} and dropped {dropped}"
2521 );
2522 }
2523
2524 #[gpui::test]
2528 fn a_panel_dropped_beside_a_dock_takes_half(cx: &mut TestAppContext) {
2529 for placement in [DockPlacement::Bottom, DockPlacement::Left] {
2530 let (area, cx) = setup(cx);
2531 let dropped = cx.update(|window, cx| {
2532 let resident = TestPanel::new("Resident", cx);
2533 let dropped = TestPanel::new("Dropped", cx);
2534 area.update(cx, |area, cx| {
2535 area.set_center(
2536 DockLayout::tabs().panel(TestPanel::new("Center", cx)),
2537 window,
2538 cx,
2539 );
2540 area.set_dock(
2541 placement,
2542 DockLayout::tabs().panel(resident).panel(dropped.clone()),
2543 window,
2544 cx,
2545 );
2546 area.set_dock_size(placement, px(400.), window, cx);
2547 });
2548 dropped
2549 });
2550 cx.run_until_parked();
2551
2552 let group = cx.read(|cx| {
2553 area.read(cx)
2554 .layout(placement)
2555 .unwrap()
2556 .find_panel_node(panel_id_of(&dropped))
2557 .expect("both panels start in the dock's only group")
2558 });
2559
2560 cx.update(|window, cx| {
2561 area.update(cx, |area, cx| {
2562 area.move_panel(
2563 panel_id_of(&dropped),
2564 InsertTarget::Split {
2565 node: group,
2566 placement: Placement::Bottom,
2567 size: None,
2568 },
2569 window,
2570 cx,
2571 );
2572 });
2573 });
2574 cx.run_until_parked();
2575
2576 let split = cx.read(|cx| {
2577 let tree = area.read(cx).layout(placement).unwrap();
2578 let root = tree.root();
2579 match root.kind() {
2580 PaneRef::Split { .. } => root.id(),
2581 _ => panic!("the drop must have produced a split"),
2582 }
2583 });
2584 let sizes = cx.read(|cx| area.read(cx).splits[&split].entity.read(cx).sizes().clone());
2585
2586 assert_eq!(sizes.len(), 2, "{placement:?}: the drop splits in two");
2587 let (first, second) = (sizes[0].as_f32(), sizes[1].as_f32());
2588 assert!(
2589 (first - second).abs() <= (first + second) * 0.02,
2590 "{placement:?}: expected halves, got {first} and {second}"
2591 );
2592 }
2593 }
2594
2595 #[gpui::test]
2602 fn an_explicit_slot_size_survives_the_first_layout_pass(cx: &mut TestAppContext) {
2603 let (area, cx) = setup(cx);
2604 cx.update(|window, cx| {
2605 let sidebar = TestPanel::new("Sidebar", cx);
2606 let content = TestPanel::new("Content", cx);
2607 area.update(cx, |area, cx| {
2608 area.set_center(
2609 DockLayout::h_split()
2610 .child(DockLayout::tabs().panel(sidebar), Some(px(200.)))
2611 .child(DockLayout::tabs().panel(content), None),
2612 window,
2613 cx,
2614 );
2615 });
2616 });
2617 cx.run_until_parked();
2618
2619 let root = cx.read(|cx| {
2620 area.read(cx)
2621 .layout(DockPlacement::Center)
2622 .unwrap()
2623 .root()
2624 .id()
2625 });
2626 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2627
2628 let fixed = sizes
2632 .first()
2633 .copied()
2634 .expect("the split has slots")
2635 .as_f32();
2636 assert!(
2637 (fixed - 200.).abs() <= 4.,
2638 "the fixed slot keeps its 200px instead of being rescaled by the \
2639 flexible sibling's placeholder, got {fixed}"
2640 );
2641 }
2642
2643 struct MeasuredPanel {
2646 name: &'static str,
2647 focus_handle: FocusHandle,
2648 }
2649
2650 impl MeasuredPanel {
2651 fn new(name: &'static str, cx: &mut App) -> Entity<Self> {
2652 cx.new(|cx| Self {
2653 name,
2654 focus_handle: cx.focus_handle(),
2655 })
2656 }
2657 }
2658
2659 impl Panel for MeasuredPanel {
2660 fn panel_name(&self) -> &'static str {
2661 self.name
2662 }
2663 }
2664
2665 impl EventEmitter<PanelEvent> for MeasuredPanel {}
2666
2667 impl Focusable for MeasuredPanel {
2668 fn focus_handle(&self, _: &App) -> FocusHandle {
2669 self.focus_handle.clone()
2670 }
2671 }
2672
2673 impl Render for MeasuredPanel {
2674 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2675 let name = self.name;
2676 div().size_full().debug_selector(move || name.into())
2677 }
2678 }
2679
2680 fn draw_frames(cx: &mut VisualTestContext, frames: usize) {
2681 for _ in 0..frames {
2682 cx.update(|window, cx| window.draw(cx).clear(cx));
2683 }
2684 }
2685
2686 #[gpui::test]
2693 fn switching_a_tab_leaves_an_untouched_split_where_it_was_drawn(cx: &mut TestAppContext) {
2694 let (area, cx) = setup(cx);
2695 cx.update(|window, cx| {
2696 area.update(cx, |area, cx| {
2697 area.set_center(
2698 DockLayout::tabs().panel(TestPanel::new("Center", cx)),
2699 window,
2700 cx,
2701 );
2702 area.set_dock(
2703 DockPlacement::Left,
2704 DockLayout::v_split()
2705 .child(
2706 DockLayout::tabs().panel(MeasuredPanel::new("upper-left", cx)),
2707 None,
2708 )
2709 .child(
2710 DockLayout::tabs().panel(MeasuredPanel::new("lower-left", cx)),
2711 Some(px(360.)),
2712 ),
2713 window,
2714 cx,
2715 );
2716 area.set_dock_size(DockPlacement::Left, px(350.), window, cx);
2717 area.set_dock(
2718 DockPlacement::Bottom,
2719 DockLayout::tabs()
2720 .panel(TestPanel::new("Tooltip", cx))
2721 .panel(TestPanel::new("Icon", cx)),
2722 window,
2723 cx,
2724 );
2725 area.set_dock_size(DockPlacement::Bottom, px(200.), window, cx);
2726 });
2727 });
2728 cx.run_until_parked();
2729 draw_frames(cx, 3);
2730 let before = (
2731 cx.debug_bounds("upper-left").unwrap(),
2732 cx.debug_bounds("lower-left").unwrap(),
2733 );
2734
2735 let bottom = cx.read(|cx| {
2736 area.read(cx)
2737 .layout(DockPlacement::Bottom)
2738 .unwrap()
2739 .root()
2740 .id()
2741 });
2742 let group = cx.read(|cx| area.read(cx).groups[&bottom].entity.clone());
2743 cx.update(|window, cx| {
2744 group.update(cx, |group, cx| group.select_tab(1, window, cx));
2745 });
2746 cx.run_until_parked();
2747 draw_frames(cx, 3);
2748 let after = (
2749 cx.debug_bounds("upper-left").unwrap(),
2750 cx.debug_bounds("lower-left").unwrap(),
2751 );
2752
2753 assert_eq!(
2754 before, after,
2755 "a tab change in the bottom dock must not move the left split"
2756 );
2757 }
2758
2759 #[gpui::test]
2764 fn switching_a_tab_keeps_a_restored_split_at_its_rescaled_share(cx: &mut TestAppContext) {
2765 let (area, cx) = setup(cx);
2766 cx.update(|window, cx| {
2767 area.update(cx, |area, cx| {
2768 area.set_center(
2769 DockLayout::h_split()
2770 .child(
2771 DockLayout::tabs()
2772 .panel(TestPanel::new("Alpha", cx))
2773 .panel(TestPanel::new("Beta", cx)),
2774 Some(px(620.)),
2775 )
2776 .child(
2777 DockLayout::tabs().panel(MeasuredPanel::new("second", cx)),
2778 Some(px(350.)),
2779 ),
2780 window,
2781 cx,
2782 );
2783 });
2784 });
2785 cx.run_until_parked();
2786 draw_frames(cx, 3);
2787 let before = cx.debug_bounds("second").unwrap();
2790 assert_ne!(
2791 before.right(),
2792 px(970.),
2793 "the window must not match the recorded total, or this test cannot \
2794 tell a rescaled split from the file's pixels"
2795 );
2796
2797 let group = group_of(&area, 0, cx);
2798 cx.update(|window, cx| {
2799 group.update(cx, |group, cx| group.select_tab(1, window, cx));
2800 });
2801 cx.run_until_parked();
2802 draw_frames(cx, 3);
2803 let after = cx.debug_bounds("second").unwrap();
2804
2805 assert_eq!(
2806 before, after,
2807 "a tab change must not hand the file's pixels back to the split"
2808 );
2809 }
2810
2811 #[gpui::test]
2822 fn the_shipped_fixture_survives_a_load_dump_load_round_trip(cx: &mut TestAppContext) {
2823 let (area, cx) = setup(cx);
2824 let fixture: DockAreaState =
2825 serde_json::from_str(include_str!("fixtures/layout.json")).unwrap();
2826
2827 cx.update(|window, cx| area.update(cx, |area, cx| area.load(fixture, window, cx).unwrap()));
2828 cx.run_until_parked();
2829 let first = cx.read(|cx| area.read(cx).dump(cx));
2830
2831 assert_eq!(first.center.children.len(), 2, "the center's two groups");
2832 assert_eq!(first.center.children[0].children.len(), 15);
2833 assert_eq!(first.center.children[1].children.len(), 1);
2834 for dock in [&first.left_dock, &first.bottom_dock, &first.right_dock] {
2835 let dock = dock.as_ref().expect("all three docks are attached");
2836 assert!(dock.open());
2837 assert!(
2838 !dock.panel().children.is_empty(),
2839 "a dock that loaded empty would round-trip just as stably"
2840 );
2841 }
2842 assert_eq!(first.left_dock.as_ref().unwrap().size(), px(350.));
2843 assert_eq!(first.bottom_dock.as_ref().unwrap().size(), px(200.));
2844 assert_eq!(first.right_dock.as_ref().unwrap().size(), px(320.));
2845
2846 cx.update(|window, cx| {
2847 area.update(cx, |area, cx| area.load(first.clone(), window, cx).unwrap())
2848 });
2849 cx.run_until_parked();
2850 let second = cx.read(|cx| area.read(cx).dump(cx));
2851
2852 assert_eq!(second, first, "dump == dump(load(dump))");
2853 }
2854
2855 #[gpui::test]
2856 fn an_unregistered_panel_survives_a_load_and_save_round_trip(cx: &mut TestAppContext) {
2857 let (area, cx) = setup(cx);
2858 cx.update(|_, cx| register_test_panels(cx));
2859
2860 let json = include_str!("fixtures/unregistered_panel.json");
2861 let state: DockAreaState = serde_json::from_str(json).unwrap();
2862
2863 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
2864 let dumped = cx.read(|cx| area.read(cx).dump(cx));
2865
2866 let leaf = &dumped.center.children[0].children[0];
2867 assert_eq!(leaf.panel_name, "PanelFromTheFuture");
2868 assert_eq!(
2869 leaf.info,
2870 PanelInfo::panel(serde_json::json!({"keep": "me"})),
2871 "a panel this build cannot construct keeps its payload"
2872 );
2873 }
2874
2875 #[gpui::test]
2876 fn a_dock_carries_its_own_tree_and_survives_a_round_trip(cx: &mut TestAppContext) {
2877 let (area, cx) = setup(cx);
2878 cx.update(|window, cx| {
2879 let alpha = TestPanel::new("Alpha", cx);
2880 let beta = TestPanel::new("Beta", cx);
2881 area.update(cx, |area, cx| {
2882 area.set_center(DockLayout::tabs().panel(alpha), window, cx);
2883 area.set_dock(
2884 DockPlacement::Left,
2885 DockLayout::tabs().panel(beta),
2886 window,
2887 cx,
2888 );
2889 });
2890 });
2891
2892 let center_ids = cx.read(|cx| {
2901 area.read(cx)
2902 .layout(DockPlacement::Center)
2903 .unwrap()
2904 .node_ids()
2905 });
2906 let dock_ids = cx.read(|cx| {
2907 area.read(cx)
2908 .layout(DockPlacement::Left)
2909 .unwrap()
2910 .node_ids()
2911 });
2912 assert!(!center_ids.is_empty() && !dock_ids.is_empty());
2913 assert!(
2914 center_ids.iter().all(|id| !dock_ids.contains(id)),
2915 "every tree in one area must draw from one id space: \
2916 center {center_ids:?} vs left {dock_ids:?}"
2917 );
2918
2919 let state = cx.read(|cx| area.read(cx).dump(cx));
2920 let left = state.left_dock.clone().expect("the left dock is written");
2921 assert_eq!(left.placement(), DockPlacement::Left);
2922 assert!(left.open());
2923 assert_eq!(left.panel().children[0].panel_name, "Beta");
2924 assert!(state.right_dock.is_none());
2925 }
2926
2927 #[gpui::test]
2928 fn a_panel_moved_between_regions_keeps_its_active_state(cx: &mut TestAppContext) {
2929 let log = log_of();
2930 let (area, alpha, cx) = two_groups(&log, cx);
2931 cx.run_until_parked();
2932 assert!(drain(&log).contains(&("Alpha", PanelSignal::Active(true))));
2935
2936 move_alpha_into_the_other_group(&area, &alpha, cx);
2937
2938 assert!(
2939 !drain(&log).contains(&("Alpha", PanelSignal::Active(true))),
2940 "a displayed panel dragged to another group must not be told `true` twice"
2941 );
2942 }
2943
2944 #[gpui::test]
2945 fn a_groups_close_intent_reaches_the_tree(cx: &mut TestAppContext) {
2946 let log = log_of();
2950 let (area, alpha, cx) = two_groups(&log, cx);
2951 cx.run_until_parked();
2952 drain(&log);
2953
2954 let node = child_node(&area, 0, cx);
2955 let alpha_id = panel_id_of(&alpha);
2956 cx.update(|_, cx| {
2957 let group = area.read(cx).groups.get(&node).unwrap().entity.clone();
2958 group.update(cx, |group, cx| group.close_panel(alpha_id, cx));
2959 });
2960 cx.run_until_parked();
2961
2962 assert!(
2963 cx.read(|cx| area
2964 .read(cx)
2965 .layout(DockPlacement::Center)
2966 .unwrap()
2967 .find_panel_node(alpha_id))
2968 .is_none(),
2969 "the close intent was applied to the tree"
2970 );
2971 assert!(drain(&log).contains(&("Alpha", PanelSignal::Removed)));
2972 }
2973
2974 #[gpui::test]
2975 fn replacing_the_center_tells_the_panels_that_left(cx: &mut TestAppContext) {
2976 let log = log_of();
2977 let (area, _alpha, cx) = two_groups(&log, cx);
2978 cx.run_until_parked();
2979 drain(&log);
2980
2981 cx.update(|window, cx| {
2982 let gamma = TestPanel::new("Gamma", cx);
2983 area.update(cx, |area, cx| {
2984 area.set_center(DockLayout::tabs().panel(gamma), window, cx)
2985 });
2986 });
2987 cx.run_until_parked();
2988
2989 let seen = drain(&log);
2990 assert!(seen.contains(&("Alpha", PanelSignal::Removed)));
2991 assert!(seen.contains(&("Beta", PanelSignal::Removed)));
2992 }
2993
2994 #[gpui::test]
2995 fn closing_a_zoomed_panel_clears_the_zoom(cx: &mut TestAppContext) {
2996 let log = log_of();
2997 let (area, alpha, cx) = two_groups(&log, cx);
2998 cx.run_until_parked();
2999
3000 let node = child_node(&area, 0, cx);
3001 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_in(node, window, cx)));
3002 assert!(cx.read(|cx| area.read(cx).is_zoomed()));
3003
3004 cx.update(|window, cx| {
3005 area.update(cx, |area, cx| area.remove_panel(alpha.clone(), window, cx))
3006 });
3007
3008 assert!(
3009 !cx.read(|cx| area.read(cx).is_zoomed()),
3010 "a zoomed panel that left the dock must not keep filling it"
3011 );
3012 }
3013
3014 struct SkinPlaceholder {
3018 state: PanelState,
3019 focus_handle: FocusHandle,
3020 }
3021
3022 impl Panel for SkinPlaceholder {
3023 fn panel_name(&self) -> &'static str {
3024 "SkinPlaceholder"
3025 }
3026
3027 fn dump(&self, _: &App) -> PanelState {
3028 self.state.clone()
3029 }
3030 }
3031
3032 impl EventEmitter<PanelEvent> for SkinPlaceholder {}
3033
3034 impl Focusable for SkinPlaceholder {
3035 fn focus_handle(&self, _: &App) -> FocusHandle {
3036 self.focus_handle.clone()
3037 }
3038 }
3039
3040 impl Render for SkinPlaceholder {
3041 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3042 Empty
3043 }
3044 }
3045
3046 struct PlaceholderSkin {
3047 asked: Rc<std::cell::RefCell<Vec<String>>>,
3048 }
3049
3050 impl DockAreaRenderer for PlaceholderSkin {
3051 fn build_placeholder(
3052 &self,
3053 state: &PanelState,
3054 _: &mut Window,
3055 cx: &mut App,
3056 ) -> Option<Arc<dyn PanelView>> {
3057 self.asked.borrow_mut().push(state.panel_name.clone());
3058 let state = state.clone();
3059 Some(Arc::new(cx.new(|cx| SkinPlaceholder {
3060 state,
3061 focus_handle: cx.focus_handle(),
3062 })))
3063 }
3064
3065 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
3066 Rc::new(BareTabGroup)
3067 }
3068 }
3069
3070 #[gpui::test]
3074 fn an_unbuildable_panel_becomes_the_skins_placeholder(cx: &mut TestAppContext) {
3075 cx.update(|cx| {
3076 let _ = crate::Theme::global_mut(cx);
3077 });
3078 let asked: Rc<std::cell::RefCell<Vec<String>>> = Rc::default();
3079 let skin = Rc::new(PlaceholderSkin {
3080 asked: asked.clone(),
3081 });
3082 let (area, cx) = cx.add_window_view(|window, cx| {
3083 DockArea::new("test-dock", None, window, cx).with_renderer(skin)
3084 });
3085
3086 cx.update(|window, cx| {
3088 let ghost = TestPanel::new("Ghost", cx);
3089 area.update(cx, |area, cx| {
3090 area.set_center(DockLayout::tabs().panel(ghost), window, cx)
3091 });
3092 });
3093 let state = cx.read(|cx| area.read(cx).dump(cx));
3094 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
3095 cx.run_until_parked();
3096
3097 assert_eq!(*asked.borrow(), vec!["Ghost".to_string()]);
3098 assert_eq!(
3099 cx.read(|cx| area
3100 .read(cx)
3101 .panels
3102 .values()
3103 .map(|panel| panel.panel_name(cx))
3104 .collect::<Vec<_>>()),
3105 vec!["SkinPlaceholder"],
3106 "base installed the skin's placeholder, not its own"
3107 );
3108 assert_eq!(
3109 cx.read(|cx| area.read(cx).dump(cx)).center.children[0].children[0].panel_name,
3110 "Ghost",
3111 "and the unknown panel still survives the next save"
3112 );
3113 }
3114
3115 #[gpui::test]
3116 fn removing_a_non_tail_child_shifts_the_split_sizes_with_it(cx: &mut TestAppContext) {
3117 let log = log_of();
3121 let (area, cx) = setup(cx);
3122 let alpha = cx.update(|window, cx| {
3123 let alpha = TestPanel::logging("Alpha", &log, cx);
3124 let beta = TestPanel::logging("Beta", &log, cx);
3125 let gamma = TestPanel::logging("Gamma", &log, cx);
3126 area.update(cx, |area, cx| {
3127 area.set_center(
3128 DockLayout::h_split()
3129 .child(DockLayout::tabs().panel(alpha.clone()), Some(px(100.)))
3130 .child(DockLayout::tabs().panel(beta), Some(px(200.)))
3131 .child(DockLayout::tabs().panel(gamma), Some(px(300.))),
3132 window,
3133 cx,
3134 );
3135 });
3136 alpha
3137 });
3138
3139 let root = cx.read(|cx| {
3140 area.read(cx)
3141 .layout(DockPlacement::Center)
3142 .unwrap()
3143 .root()
3144 .id()
3145 });
3146 let split = cx.read(|cx| area.read(cx).splits.get(&root).unwrap().entity.clone());
3147 let sizes = |cx: &mut VisualTestContext| cx.read(|cx| split.read(cx).sizes().clone());
3148
3149 let before = sizes(cx);
3153 assert_eq!(before.len(), 3);
3154 let kept_if_correct = before[1] / before[2];
3155 let kept_if_truncated = before[0] / before[1];
3156 assert!(
3157 (kept_if_correct - kept_if_truncated).abs() > 0.1,
3158 "the fixture must be able to tell the two outcomes apart"
3159 );
3160
3161 cx.update(|window, cx| area.update(cx, |area, cx| area.remove_panel(alpha, window, cx)));
3162
3163 let after = sizes(cx);
3164 assert_eq!(after.len(), 2);
3165 assert!(
3166 (after[0] / after[1] - kept_if_correct).abs() < 0.01,
3167 "the survivors kept their own proportions: slot 0 was removed, not \
3168 the tail — got {after:?} from {before:?}"
3169 );
3170 }
3171
3172 #[gpui::test]
3173 fn a_panel_moves_between_the_center_and_a_dock(cx: &mut TestAppContext) {
3174 let log = log_of();
3175 let (area, alpha, cx) = two_groups(&log, cx);
3176 cx.update(|window, cx| {
3177 let gamma = TestPanel::logging("Gamma", &log, cx);
3178 area.update(cx, |area, cx| {
3179 area.set_dock(
3180 DockPlacement::Left,
3181 DockLayout::tabs().panel(gamma),
3182 window,
3183 cx,
3184 );
3185 });
3186 });
3187 cx.run_until_parked();
3188 drain(&log);
3189
3190 let alpha_id = panel_id_of(&alpha);
3191 let dock_group = cx.read(|cx| {
3192 area.read(cx)
3193 .layout(DockPlacement::Left)
3194 .unwrap()
3195 .root()
3196 .id()
3197 });
3198
3199 cx.update(|window, cx| {
3200 area.update(cx, |area, cx| {
3201 area.move_panel(
3202 alpha_id,
3203 InsertTarget::Tabs {
3204 node: dock_group,
3205 ix: None,
3206 activate: true,
3207 },
3208 window,
3209 cx,
3210 );
3211 });
3212 });
3213 cx.run_until_parked();
3214
3215 assert!(
3216 cx.read(|cx| area
3217 .read(cx)
3218 .layout(DockPlacement::Center)
3219 .unwrap()
3220 .find_panel_node(alpha_id))
3221 .is_none(),
3222 "the panel left the center"
3223 );
3224 assert_eq!(
3225 cx.read(|cx| area
3226 .read(cx)
3227 .layout(DockPlacement::Left)
3228 .unwrap()
3229 .find_panel_node(alpha_id)),
3230 Some(dock_group),
3231 "and arrived in the dock's group"
3232 );
3233
3234 let seen = drain(&log);
3235 assert!(
3236 !seen.contains(&("Alpha", PanelSignal::Removed)),
3237 "crossing regions is still a move, not a removal"
3238 );
3239 assert!(
3240 !seen.contains(&("Alpha", PanelSignal::Active(true))),
3241 "and it was displayed in both, so it is not told `true` twice"
3242 );
3243 }
3244
3245 #[gpui::test]
3246 fn a_move_onto_an_unusable_target_leaves_no_stranded_panel(cx: &mut TestAppContext) {
3247 let log = log_of();
3252 let (area, _alpha, cx) = two_groups(&log, cx);
3253 let gamma = cx.update(|window, cx| {
3254 let gamma = TestPanel::logging("Gamma", &log, cx);
3255 area.update(cx, |area, cx| {
3256 area.set_dock(
3257 DockPlacement::Left,
3258 DockLayout::tabs().panel(gamma.clone()),
3259 window,
3260 cx,
3261 );
3262 });
3263 gamma
3264 });
3265 cx.run_until_parked();
3266 drain(&log);
3267
3268 let gamma_id = panel_id_of(&gamma);
3269 let center_root = cx.read(|cx| {
3272 area.read(cx)
3273 .layout(DockPlacement::Center)
3274 .unwrap()
3275 .root()
3276 .id()
3277 });
3278
3279 cx.update(|window, cx| {
3280 area.update(cx, |area, cx| {
3281 area.move_panel(
3282 gamma_id,
3283 InsertTarget::Tabs {
3284 node: center_root,
3285 ix: None,
3286 activate: true,
3287 },
3288 window,
3289 cx,
3290 );
3291 });
3292 });
3293 cx.run_until_parked();
3294
3295 assert!(
3296 cx.read(|cx| area.read(cx).panel(gamma_id).is_none()),
3297 "the view map agrees with the trees straight away, rather than \
3298 carrying a panel that belongs to no tree"
3299 );
3300 assert!(
3301 drain(&log).contains(&("Gamma", PanelSignal::Removed)),
3302 "and the panel was told so at the point of the call"
3303 );
3304 }
3305
3306 #[gpui::test]
3307 fn an_all_hidden_container_reports_itself_invisible(cx: &mut TestAppContext) {
3308 let (area, cx) = setup(cx);
3311 let beta = cx.update(|window, cx| {
3312 let alpha = TestPanel::new("Alpha", cx);
3313 let beta = TestPanel::new("Beta", cx);
3314 area.update(cx, |area, cx| {
3315 area.set_center(
3316 DockLayout::h_split()
3317 .child(DockLayout::tabs().panel(alpha), None)
3318 .child(DockLayout::tabs().panel(beta.clone()), None),
3319 window,
3320 cx,
3321 );
3322 });
3323 beta
3324 });
3325
3326 let visible = |ix: usize, cx: &mut VisualTestContext| {
3327 let node = child_node(&area, ix, cx);
3328 cx.read(|cx| {
3329 let area = area.read(cx);
3330 let tree = area.layout(DockPlacement::Center).unwrap();
3331 area.is_node_visible(tree.find_node(node).unwrap(), cx)
3332 })
3333 };
3334
3335 assert!(visible(0, cx) && visible(1, cx));
3336
3337 cx.update(|_, cx| beta.update(cx, |beta, cx| beta.set_visible(false, cx)));
3338
3339 assert!(visible(0, cx), "the visible group still holds its slot");
3340 assert!(
3341 !visible(1, cx),
3342 "a group whose every panel is hidden must give its slot up"
3343 );
3344 }
3345
3346 #[gpui::test]
3347 fn a_locked_area_seals_its_groups(cx: &mut TestAppContext) {
3348 let log = log_of();
3349 let (area, _alpha, cx) = two_groups(&log, cx);
3350 cx.run_until_parked();
3351
3352 let node = child_node(&area, 0, cx);
3353 let group = cx.read(|cx| area.read(cx).groups.get(&node).unwrap().entity.clone());
3354 assert!(
3355 cx.read(|cx| group.read(cx).is_closable(cx)),
3356 "an unlocked group's panel can be closed"
3357 );
3358
3359 cx.update(|window, cx| area.update(cx, |area, cx| area.set_locked(true, window, cx)));
3360
3361 assert!(
3362 !cx.read(|cx| group.read(cx).is_closable(cx)),
3363 "the lock reaches every group through the constraints push"
3364 );
3365 }
3366
3367 #[gpui::test]
3375 fn empty_center_round_trips_as_a_stack(cx: &mut TestAppContext) {
3376 let (area, cx) = setup(cx);
3377 let center = cx.read(|cx| area.read(cx).dump(cx).center);
3378
3379 assert_eq!(center.panel_name, "StackPanel");
3380 assert!(
3381 matches!(center.info, PanelInfo::Stack { .. }),
3382 "got {:?}",
3383 center.info
3384 );
3385 }
3386
3387 #[gpui::test]
3388 fn fresh_center_is_empty(cx: &mut TestAppContext) {
3389 let (area, cx) = setup(cx);
3390
3391 assert!(
3392 is_center_empty(&area, cx),
3393 "DockArea::new starts with an empty split centre"
3394 );
3395 }
3396
3397 #[gpui::test]
3398 fn center_holding_a_tab_group_is_not_empty(cx: &mut TestAppContext) {
3399 let log = log_of();
3400 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3401 cx.run_until_parked();
3402
3403 assert!(!is_center_empty(&area, cx));
3404 }
3405
3406 #[gpui::test]
3410 fn center_is_empty_again_once_every_panel_is_removed(cx: &mut TestAppContext) {
3411 let log = log_of();
3412 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3413 cx.run_until_parked();
3414
3415 for panel in panels {
3416 cx.update(|window, cx| {
3417 area.update(cx, |area, cx| area.remove_panel(panel.clone(), window, cx))
3418 });
3419 }
3420 cx.run_until_parked();
3421
3422 assert!(is_center_empty(&area, cx));
3423 }
3424
3425 #[gpui::test]
3426 fn center_is_not_empty_after_adding_to_a_tab_group(cx: &mut TestAppContext) {
3427 let (area, cx) = setup(cx);
3428 assert!(is_center_empty(&area, cx));
3429
3430 cx.update(|window, cx| {
3431 let alpha = TestPanel::new("Alpha", cx);
3432 area.update(cx, |area, cx| {
3433 area.add_panel(alpha, DockPlacement::Center, None, window, cx)
3434 });
3435 });
3436 cx.run_until_parked();
3437
3438 assert!(!is_center_empty(&area, cx));
3439 }
3440
3441 #[gpui::test]
3445 fn add_panel_view_registers_the_handle_it_was_given(cx: &mut TestAppContext) {
3446 let (area, cx) = setup(cx);
3447
3448 let view = cx.update(|window, cx| {
3449 let view: Arc<dyn PanelView> = Arc::new(TestPanel::new("Alpha", cx));
3450 area.update(cx, |area, cx| {
3451 area.add_panel_view(view.clone(), DockPlacement::Center, None, window, cx)
3452 });
3453 view
3454 });
3455 cx.run_until_parked();
3456
3457 let id = cx.read(|cx| view.panel_id(cx));
3458 assert!(
3459 cx.read(|cx| area
3460 .read(cx)
3461 .panel(id)
3462 .is_some_and(|stored| Arc::ptr_eq(stored, &view))),
3463 "the stored handle is the one that was handed over, under its own id"
3464 );
3465 assert!(!is_center_empty(&area, cx));
3466 }
3467
3468 #[gpui::test]
3471 fn center_holding_only_hidden_panels_is_empty(cx: &mut TestAppContext) {
3472 let log = log_of();
3473 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3474 cx.run_until_parked();
3475 assert!(!is_center_empty(&area, cx));
3476
3477 cx.update(|_, cx| {
3478 for panel in &panels {
3479 panel.update(cx, |panel, cx| panel.set_visible(false, cx));
3480 }
3481 });
3482 cx.run_until_parked();
3483
3484 assert_eq!(
3485 cx.read(|cx| area
3486 .read(cx)
3487 .layout(DockPlacement::Center)
3488 .unwrap()
3489 .panels()
3490 .count()),
3491 2,
3492 "hiding a panel does not remove it from the tab group"
3493 );
3494 assert!(is_center_empty(&area, cx));
3495 }
3496
3497 #[gpui::test]
3498 fn single_panel_group_receives_initial_active(cx: &mut TestAppContext) {
3499 let log = log_of();
3500 let (_area, _panels, cx) = one_group(&log, &["A"], None, cx);
3501 cx.run_until_parked();
3502
3503 assert_eq!(drain_active(&log), [("A", true)]);
3504 }
3505
3506 #[gpui::test]
3507 fn multi_tab_construction_notifies_only_displayed_panel(cx: &mut TestAppContext) {
3508 let log = log_of();
3509 let (_area, _panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
3510 cx.run_until_parked();
3511
3512 assert_eq!(drain_active(&log), [("A", true)]);
3514 }
3515
3516 #[gpui::test]
3517 fn active_index_restore_notifies_that_panel_only(cx: &mut TestAppContext) {
3518 let log = log_of();
3519 let (_area, _panels, cx) = one_group(&log, &["A", "B", "C"], Some(2), cx);
3520 cx.run_until_parked();
3521
3522 assert_eq!(drain_active(&log), [("C", true)]);
3523 }
3524
3525 #[gpui::test]
3526 fn switching_tabs_sends_false_then_true(cx: &mut TestAppContext) {
3527 let log = log_of();
3528 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3529 cx.run_until_parked();
3530 drain(&log);
3531
3532 let group = group_of(&area, 0, cx);
3533 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(1, window, cx)));
3534 cx.run_until_parked();
3535
3536 assert_eq!(drain_active(&log), [("A", false), ("B", true)]);
3537 }
3538
3539 #[gpui::test]
3540 fn select_panel_displays_that_tab_where_it_sits(cx: &mut TestAppContext) {
3541 let log = log_of();
3542 let (area, panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
3543 cx.run_until_parked();
3544 drain(&log);
3545
3546 let b = panel_id_of(&panels[1]);
3547 cx.update(|window, cx| area.update(cx, |area, cx| area.select_panel(b, window, cx)));
3548 cx.run_until_parked();
3549
3550 assert_eq!(drain_active(&log), [("A", false), ("B", true)]);
3551 let (order, active_ix) = cx.read(|cx| {
3552 let tree = area.read(cx).layout(DockPlacement::Center).unwrap();
3553 let node = tree.find_panel_node(b).unwrap();
3554 match tree.find_node(node).unwrap().kind() {
3555 PaneRef::Tabs { panels, active_ix } => (panels.to_vec(), active_ix),
3556 PaneRef::Split { .. } => panic!("a tab group"),
3557 }
3558 });
3559 assert_eq!(active_ix, 1, "the selected tab is displayed");
3560 assert_eq!(
3561 order,
3562 panels.iter().map(panel_id_of).collect::<Vec<_>>(),
3563 "selecting a tab does not move it"
3564 );
3565 }
3566
3567 #[gpui::test]
3568 fn select_panel_is_silent_for_the_displayed_or_an_unknown_panel(cx: &mut TestAppContext) {
3569 let log = log_of();
3570 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3571 cx.run_until_parked();
3572 drain(&log);
3573
3574 let a = panel_id_of(&panels[0]);
3575 let stranger = cx.update(|_, cx| panel_id_of(&TestPanel::logging("Z", &log, cx)));
3576 cx.update(|window, cx| {
3577 area.update(cx, |area, cx| {
3578 area.select_panel(a, window, cx);
3579 area.select_panel(stranger, window, cx);
3580 })
3581 });
3582 cx.run_until_parked();
3583
3584 assert_eq!(drain_active(&log), []);
3585 }
3586
3587 #[gpui::test]
3588 fn reselecting_active_tab_stays_silent(cx: &mut TestAppContext) {
3589 let log = log_of();
3590 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3591 cx.run_until_parked();
3592 drain(&log);
3593
3594 let group = group_of(&area, 0, cx);
3595 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(0, window, cx)));
3596 cx.run_until_parked();
3597
3598 assert_eq!(drain_active(&log), []);
3599 }
3600
3601 #[gpui::test]
3607 fn inserting_at_active_ix_swaps_notifications(cx: &mut TestAppContext) {
3608 let log = log_of();
3609 let (area, cx) = setup(cx);
3610 let c = cx.update(|window, cx| {
3611 let a = TestPanel::logging("A", &log, cx);
3612 let b = TestPanel::logging("B", &log, cx);
3613 let x = TestPanel::logging("X", &log, cx);
3614 let c = TestPanel::logging("C", &log, cx);
3615 area.update(cx, |area, cx| {
3616 area.set_center(
3617 DockLayout::h_split()
3618 .child(DockLayout::tabs().panel(a).panel(b), None)
3619 .child(DockLayout::tabs().panel(x).panel(c.clone()), None),
3620 window,
3621 cx,
3622 );
3623 });
3624 c
3625 });
3626 cx.run_until_parked();
3627 drain(&log);
3628
3629 let destination = child_node(&area, 0, cx);
3630 let c_id = panel_id_of(&c);
3631 move_panel_into(&area, c_id, destination, Some(0), true, cx);
3632
3633 assert_eq!(drain_active(&log), [("A", false), ("C", true)]);
3634 let group = group_of(&area, 0, cx);
3635 assert_eq!(cx.read(|cx| group.read(cx).active_ix()), 0);
3636 assert_eq!(
3637 cx.read(|cx| group.read(cx).panels()[0].panel_id(cx)),
3638 c_id,
3639 "the arriving panel took the slot it named"
3640 );
3641 }
3642
3643 #[gpui::test]
3644 fn removing_before_active_keeps_displayed_panel(cx: &mut TestAppContext) {
3645 let log = log_of();
3646 let (area, panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
3647 let group = group_of(&area, 0, cx);
3648 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(1, window, cx)));
3649 cx.run_until_parked();
3650 drain(&log);
3651
3652 cx.update(|window, cx| {
3653 area.update(cx, |area, cx| {
3654 area.remove_panel(panels[0].clone(), window, cx)
3655 })
3656 });
3657 cx.run_until_parked();
3658
3659 assert_eq!(drain_active(&log), []);
3660 assert_eq!(cx.read(|cx| group.read(cx).active_ix()), 0);
3661 assert_eq!(
3662 cx.read(|cx| group.read(cx).panels()[0].panel_id(cx)),
3663 panel_id_of(&panels[1]),
3664 "the same panel is still displayed, at its new index"
3665 );
3666 }
3667
3668 #[gpui::test]
3671 fn collapse_and_expand_notify_active_panel(cx: &mut TestAppContext) {
3672 let log = log_of();
3673 let (area, cx) = setup(cx);
3674 cx.update(|window, cx| {
3675 let a = TestPanel::logging("A", &log, cx);
3676 let b = TestPanel::logging("B", &log, cx);
3677 area.update(cx, |area, cx| {
3678 area.set_dock(
3679 DockPlacement::Left,
3680 DockLayout::tabs().panel(a).panel(b),
3681 window,
3682 cx,
3683 );
3684 });
3685 });
3686 cx.run_until_parked();
3687 drain(&log);
3688
3689 cx.update(|window, cx| {
3690 area.update(cx, |area, cx| {
3691 area.toggle_dock(DockPlacement::Left, window, cx)
3692 })
3693 });
3694 cx.run_until_parked();
3695 assert_eq!(drain_active(&log), [("A", false)]);
3696
3697 cx.update(|window, cx| {
3698 area.update(cx, |area, cx| {
3699 area.toggle_dock(DockPlacement::Left, window, cx)
3700 })
3701 });
3702 cx.run_until_parked();
3703 assert_eq!(drain_active(&log), [("A", true)]);
3704 }
3705
3706 #[gpui::test]
3707 fn background_add_is_silent_but_first_panel_is_not(cx: &mut TestAppContext) {
3708 let log = log_of();
3709 let (area, cx) = setup(cx);
3710 let d = cx.update(|window, cx| {
3711 let a = TestPanel::logging("A", &log, cx);
3712 let b = TestPanel::logging("B", &log, cx);
3713 let c = TestPanel::logging("C", &log, cx);
3714 let d = TestPanel::logging("D", &log, cx);
3715 area.update(cx, |area, cx| {
3716 area.set_center(
3717 DockLayout::h_split()
3718 .child(DockLayout::tabs().panel(a).panel(b), None)
3719 .child(DockLayout::tabs().panel(c).panel(d.clone()), None),
3720 window,
3721 cx,
3722 );
3723 });
3724 d
3725 });
3726 cx.run_until_parked();
3727 drain(&log);
3728
3729 let destination = child_node(&area, 0, cx);
3732 move_panel_into(&area, panel_id_of(&d), destination, None, false, cx);
3733 assert_eq!(drain_active(&log), []);
3734
3735 cx.update(|window, cx| {
3738 let e = TestPanel::logging("E", &log, cx);
3739 area.update(cx, |area, cx| {
3740 area.add_panel(e, DockPlacement::Left, None, window, cx)
3741 });
3742 });
3743 cx.run_until_parked();
3744 assert_eq!(drain_active(&log), [("E", true)]);
3745 }
3746
3747 #[gpui::test]
3748 fn drag_active_panel_to_other_group_stays_silent_for_it(cx: &mut TestAppContext) {
3749 let log = log_of();
3750 let (area, cx) = setup(cx);
3751 let a = cx.update(|window, cx| {
3752 let a = TestPanel::logging("A", &log, cx);
3753 let b = TestPanel::logging("B", &log, cx);
3754 let c = TestPanel::logging("C", &log, cx);
3755 area.update(cx, |area, cx| {
3756 area.set_center(
3757 DockLayout::h_split()
3758 .child(DockLayout::tabs().panel(a.clone()).panel(b), None)
3759 .child(DockLayout::tabs().panel(c), None),
3760 window,
3761 cx,
3762 );
3763 });
3764 a
3765 });
3766 cx.run_until_parked();
3767 drain(&log);
3768
3769 let destination = child_node(&area, 1, cx);
3772 move_panel_into(&area, panel_id_of(&a), destination, None, true, cx);
3773
3774 let seen = drain_active(&log);
3777 assert!(seen.contains(&("B", true)), "got {seen:?}");
3778 assert!(seen.contains(&("C", false)), "got {seen:?}");
3779 assert!(
3780 !seen.iter().any(|(name, _)| *name == "A"),
3781 "the moved panel was displayed before and after: {seen:?}"
3782 );
3783 }
3784
3785 #[gpui::test]
3786 fn drag_active_panel_to_background_slot_deactivates_it(cx: &mut TestAppContext) {
3787 let log = log_of();
3788 let (area, cx) = setup(cx);
3789 let a = cx.update(|window, cx| {
3790 let a = TestPanel::logging("A", &log, cx);
3791 let c = TestPanel::logging("C", &log, cx);
3792 let d = TestPanel::logging("D", &log, cx);
3793 area.update(cx, |area, cx| {
3794 area.set_center(
3795 DockLayout::h_split()
3796 .child(DockLayout::tabs().panel(a.clone()), None)
3797 .child(DockLayout::tabs().panel(c).panel(d), None),
3798 window,
3799 cx,
3800 );
3801 });
3802 a
3803 });
3804 cx.run_until_parked();
3805 drain(&log);
3806
3807 let destination = child_node(&area, 1, cx);
3810 move_panel_into(&area, panel_id_of(&a), destination, None, false, cx);
3811
3812 assert_eq!(drain_active(&log), [("A", false)]);
3813 }
3814
3815 struct RecordingSkin {
3822 tab_bars: Rc<RefCell<Vec<NodeId>>>,
3823 }
3824
3825 struct RecordingTabGroup {
3826 drawn: Rc<RefCell<Vec<NodeId>>>,
3827 }
3828
3829 impl DockAreaRenderer for RecordingSkin {
3830 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
3831 Rc::new(RecordingTabGroup {
3832 drawn: self.tab_bars.clone(),
3833 })
3834 }
3835 }
3836
3837 impl TabGroupRenderer for RecordingTabGroup {
3838 fn render_tab_bar(
3839 &self,
3840 group: &TabGroupContext,
3841 _: &mut Window,
3842 _: &mut App,
3843 ) -> AnyElement {
3844 self.drawn.borrow_mut().push(group.node());
3845 Empty.into_any_element()
3846 }
3847 }
3848
3849 type DrawLog = Rc<RefCell<Vec<NodeId>>>;
3850
3851 fn setup_recording(
3853 cx: &mut TestAppContext,
3854 ) -> (Entity<DockArea>, DrawLog, &mut VisualTestContext) {
3855 cx.update(|cx| {
3856 let _ = crate::Theme::global_mut(cx);
3857 });
3858 let tab_bars: DrawLog = Rc::default();
3859 let skin = Rc::new(RecordingSkin {
3860 tab_bars: tab_bars.clone(),
3861 });
3862 let (area, cx) = cx.add_window_view(|window, cx| {
3863 DockArea::new("test-dock", None, window, cx).with_renderer(skin)
3864 });
3865 (area, tab_bars, cx)
3866 }
3867
3868 fn zoom_signals(log: &Log) -> Vec<(&'static str, PanelSignal)> {
3869 drain(log)
3870 .into_iter()
3871 .filter(|(_, signal)| matches!(signal, PanelSignal::Zoomed(_)))
3872 .collect()
3873 }
3874
3875 #[gpui::test]
3884 fn a_zoomed_group_is_drawn_whole_rather_than_as_its_bare_panel(cx: &mut TestAppContext) {
3885 let log = log_of();
3886 let (area, tab_bars, cx) = setup_recording(cx);
3887 cx.update(|window, cx| {
3888 let alpha = TestPanel::logging("Alpha", &log, cx);
3889 let beta = TestPanel::logging("Beta", &log, cx);
3890 area.update(cx, |area, cx| {
3891 area.set_center(
3892 DockLayout::h_split()
3893 .child(DockLayout::tabs().panel(alpha), None)
3894 .child(DockLayout::tabs().panel(beta), None),
3895 window,
3896 cx,
3897 );
3898 });
3899 });
3900 cx.run_until_parked();
3901
3902 let zoomed = child_node(&area, 0, cx);
3903 let other = child_node(&area, 1, cx);
3904 assert!(
3905 tab_bars.borrow().contains(&zoomed) && tab_bars.borrow().contains(&other),
3906 "both groups draw their own tab bar while nothing is zoomed"
3907 );
3908
3909 tab_bars.borrow_mut().clear();
3910 let group = group_of(&area, 0, cx);
3911 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
3912 cx.run_until_parked();
3913
3914 assert!(
3915 tab_bars.borrow().contains(&zoomed),
3916 "a zoomed group is rendered whole: its own tab bar is still drawn, \
3917 which is exactly what the bare panel does not carry"
3918 );
3919 assert!(
3920 !tab_bars.borrow().contains(&other),
3921 "and it is the only thing on screen"
3922 );
3923 }
3924
3925 #[gpui::test]
3931 fn clearing_the_zoom_from_outside_puts_the_groups_own_flag_back(cx: &mut TestAppContext) {
3932 let log = log_of();
3933 let (area, _alpha, cx) = two_groups(&log, cx);
3934 cx.run_until_parked();
3935 drain(&log);
3936
3937 let node = child_node(&area, 0, cx);
3938 let group = group_of(&area, 0, cx);
3939 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
3940 cx.run_until_parked();
3941 assert_eq!(cx.read(|cx| area.read(cx).zoomed_group()), Some(node));
3942 assert!(cx.read(|cx| group.read(cx).is_zoomed()));
3943 assert_eq!(
3944 zoom_signals(&log),
3945 vec![("Alpha", PanelSignal::Zoomed(true))]
3946 );
3947
3948 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_out(window, cx)));
3949 cx.run_until_parked();
3950
3951 assert!(!cx.read(|cx| area.read(cx).is_zoomed()));
3952 assert!(
3953 !cx.read(|cx| group.read(cx).is_zoomed()),
3954 "a group left flagged zoomed would stay locked and refuse every drop"
3955 );
3956 assert!(
3957 cx.read(|cx| group.read(cx).context(cx).is_droppable()),
3958 "and the lock the zoom imposed is lifted with it"
3959 );
3960 assert_eq!(
3961 zoom_signals(&log),
3962 vec![("Alpha", PanelSignal::Zoomed(false))],
3963 "the panel hears the zoom end too, not just the group"
3964 );
3965 }
3966
3967 #[gpui::test]
3970 fn a_group_that_refuses_to_zoom_leaves_the_area_unzoomed(cx: &mut TestAppContext) {
3971 let log = log_of();
3972 let (area, alpha, cx) = two_groups(&log, cx);
3973 cx.run_until_parked();
3974 cx.update(|_, cx| alpha.update(cx, |panel, cx| panel.set_zoomable(false, cx)));
3975
3976 let node = child_node(&area, 0, cx);
3977 let group = group_of(&area, 0, cx);
3978 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_in(node, window, cx)));
3979 cx.run_until_parked();
3980
3981 assert!(!cx.read(|cx| group.read(cx).is_zoomed()));
3982 assert!(
3983 !cx.read(|cx| area.read(cx).is_zoomed()),
3984 "the area must not fill itself with a group that never zoomed"
3985 );
3986 }
3987}