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, TileMeta},
34 state_convert::{PanelBuilder, PanelSource as _},
35 tab_group::{BareTabGroup, TabGroup, TabGroupConstraints, TabGroupEvent, TabGroupRenderer},
36 tiles_state::{BareTiles, TilesEvent, TilesRenderer, TilesState},
37};
38
39pub enum DockEvent {
41 LayoutChanged,
45 DragDrop { item: AnyDrag, target: DropTarget },
47}
48
49#[derive(Clone, Copy, PartialEq, Eq, Debug)]
59enum Zoomed {
60 Group(NodeId),
62 Tile { node: NodeId, panel: PanelId },
66}
67
68#[derive(Clone, Copy)]
71enum Added {
72 Anywhere(Option<Pixels>),
75 AsTile(Bounds<Pixels>),
79}
80
81impl Added {
82 fn dock_size(self) -> Option<Pixels> {
83 match self {
84 Self::Anywhere(size) => size,
85 Self::AsTile(_) => None,
86 }
87 }
88}
89
90struct DockRegion {
92 tree: PaneTree,
93 dock: Dock,
94}
95
96struct Cached<T> {
100 entity: Entity<T>,
101 _subscription: Subscription,
102}
103
104struct CachedSplit {
113 entity: Entity<ResizableState>,
114 children: Vec<NodeId>,
115 sizes: Vec<Option<Pixels>>,
120 _subscription: Subscription,
121}
122
123pub struct DockArea {
128 id: SharedString,
129 version: Option<usize>,
130 bounds: Bounds<Pixels>,
131 this: WeakEntity<Self>,
132
133 center: PaneTree,
134 docks: HashMap<DockPlacement, DockRegion>,
135
136 groups: HashMap<NodeId, Cached<TabGroup>>,
137 splits: HashMap<NodeId, CachedSplit>,
138 tiles: HashMap<NodeId, Cached<TilesState>>,
139 panels: HashMap<PanelId, Arc<dyn PanelView>>,
140
141 locked: bool,
142 zoomed: Option<Zoomed>,
143 focus_handle: FocusHandle,
144 renderer: Rc<dyn DockAreaRenderer>,
145}
146
147impl DockArea {
148 pub fn new(
156 id: impl Into<SharedString>,
157 version: Option<usize>,
158 _window: &mut Window,
159 cx: &mut Context<Self>,
160 ) -> Self {
161 PanelRegistry::init(cx);
162
163 Self {
164 id: id.into(),
165 version,
166 bounds: Bounds::default(),
167 this: cx.weak_entity(),
168 center: PaneTree::new(RootKind::Split),
169 docks: HashMap::new(),
170 groups: HashMap::new(),
171 splits: HashMap::new(),
172 tiles: HashMap::new(),
173 panels: HashMap::new(),
174 locked: false,
175 zoomed: None,
176 focus_handle: cx.focus_handle(),
177 renderer: Rc::new(BareDockArea),
178 }
179 }
180
181 pub fn with_renderer(mut self, renderer: Rc<dyn DockAreaRenderer>) -> Self {
185 self.renderer = renderer;
186 self
187 }
188
189 pub fn id(&self) -> SharedString {
190 self.id.clone()
191 }
192
193 pub fn version(&self) -> Option<usize> {
194 self.version
195 }
196
197 pub fn set_version(&mut self, version: Option<usize>, cx: &mut Context<Self>) {
203 self.version = version;
204 cx.notify();
205 }
206
207 pub fn bounds(&self) -> Bounds<Pixels> {
210 self.bounds
211 }
212
213 pub fn layout(&self, placement: DockPlacement) -> Option<&PaneTree> {
220 match placement {
221 DockPlacement::Center => Some(&self.center),
222 _ => self.docks.get(&placement).map(|pane| &pane.tree),
223 }
224 }
225
226 pub fn panel(&self, panel: PanelId) -> Option<&Arc<dyn PanelView>> {
228 self.panels.get(&panel)
229 }
230
231 pub fn is_locked(&self) -> bool {
232 self.locked
233 }
234
235 pub fn is_empty(&self, placement: DockPlacement, cx: &App) -> bool {
242 self.layout(placement)
243 .is_none_or(|tree| !self.is_node_visible(tree.root(), cx))
244 }
245
246 pub fn set_locked(&mut self, locked: bool, window: &mut Window, cx: &mut Context<Self>) {
248 if self.locked == locked {
249 return;
250 }
251 self.locked = locked;
252 self.reconcile(window, cx);
255 }
256}
257
258impl DockArea {
260 pub fn set_center(&mut self, layout: DockLayout, window: &mut Window, cx: &mut Context<Self>) {
263 let (tree, panels) = PaneTree::from_layout(layout, RootKind::Split);
264 self.center = tree;
265 self.panels.extend(panels);
266 self.reconcile(window, cx);
267 cx.emit(DockEvent::LayoutChanged);
268 }
269
270 pub fn set_dock(
277 &mut self,
278 placement: DockPlacement,
279 layout: DockLayout,
280 window: &mut Window,
281 cx: &mut Context<Self>,
282 ) {
283 if placement == DockPlacement::Center {
284 return self.set_center(layout, window, cx);
285 }
286
287 let (tree, panels) = PaneTree::from_layout(layout, RootKind::Any);
288 let dock = self
289 .docks
290 .get(&placement)
291 .map(|pane| pane.dock)
292 .unwrap_or_else(|| Dock::new(PANEL_MIN_SIZE * 2.));
293 self.docks.insert(placement, DockRegion { tree, dock });
294 self.panels.extend(panels);
295 self.reconcile(window, cx);
296 cx.emit(DockEvent::LayoutChanged);
297 }
298
299 pub fn remove_dock(
302 &mut self,
303 placement: DockPlacement,
304 window: &mut Window,
305 cx: &mut Context<Self>,
306 ) {
307 if self.docks.remove(&placement).is_none() {
308 return;
309 }
310 self.reconcile(window, cx);
313 cx.emit(DockEvent::LayoutChanged);
314 }
315
316 pub fn has_dock(&self, placement: DockPlacement) -> bool {
317 self.docks.contains_key(&placement)
318 }
319
320 pub fn is_dock_open(&self, placement: DockPlacement) -> bool {
324 self.docks
325 .get(&placement)
326 .is_some_and(|pane| pane.dock.is_open())
327 }
328
329 pub fn toggle_dock(
332 &mut self,
333 placement: DockPlacement,
334 window: &mut Window,
335 cx: &mut Context<Self>,
336 ) {
337 let Some(pane) = self.docks.get_mut(&placement) else {
338 return;
339 };
340 if !pane.dock.is_collapsible() && pane.dock.is_open() {
341 return;
342 }
343 let open = pane.dock.is_open();
344 pane.dock.set_open(!open);
345 self.reconcile(window, cx);
349 cx.emit(DockEvent::LayoutChanged);
350 }
351
352 pub fn is_dock_collapsible(&self, placement: DockPlacement) -> bool {
355 self.docks
356 .get(&placement)
357 .is_some_and(|pane| pane.dock.is_collapsible())
358 }
359
360 pub fn set_dock_collapsible(
361 &mut self,
362 placement: DockPlacement,
363 collapsible: bool,
364 _window: &mut Window,
365 cx: &mut Context<Self>,
366 ) {
367 if let Some(pane) = self.docks.get_mut(&placement) {
368 pane.dock.set_collapsible(collapsible);
369 cx.notify();
370 }
371 }
372
373 pub fn dock_size(&self, placement: DockPlacement) -> Option<Pixels> {
375 self.docks.get(&placement).map(|pane| pane.dock.size())
376 }
377
378 pub fn set_dock_size(
379 &mut self,
380 placement: DockPlacement,
381 size: Pixels,
382 _window: &mut Window,
383 cx: &mut Context<Self>,
384 ) {
385 if let Some(pane) = self.docks.get_mut(&placement) {
386 let previous = pane.dock.size();
387 pane.dock.set_size(size);
388 if pane.dock.size() == previous {
389 return;
390 }
391 cx.notify();
392 cx.emit(DockEvent::LayoutChanged);
393 }
394 }
395}
396
397impl DockArea {
399 pub fn add_panel<P: Panel>(
403 &mut self,
404 panel: Entity<P>,
405 placement: DockPlacement,
406 size: Option<Pixels>,
407 window: &mut Window,
408 cx: &mut Context<Self>,
409 ) {
410 let id = PanelId::from(panel.entity_id());
411 self.add_panel_inner(
412 id,
413 Arc::new(panel),
414 placement,
415 Added::Anywhere(size),
416 window,
417 cx,
418 );
419 }
420
421 pub fn add_panel_view(
428 &mut self,
429 panel: Arc<dyn PanelView>,
430 placement: DockPlacement,
431 size: Option<Pixels>,
432 window: &mut Window,
433 cx: &mut Context<Self>,
434 ) {
435 let id = panel.panel_id(cx);
436 self.add_panel_inner(id, panel, placement, Added::Anywhere(size), window, cx);
437 }
438
439 pub fn add_tile<P: Panel>(
450 &mut self,
451 panel: Entity<P>,
452 placement: DockPlacement,
453 bounds: Bounds<Pixels>,
454 window: &mut Window,
455 cx: &mut Context<Self>,
456 ) {
457 let id = PanelId::from(panel.entity_id());
458 self.add_panel_inner(
459 id,
460 Arc::new(panel),
461 placement,
462 Added::AsTile(bounds),
463 window,
464 cx,
465 );
466 }
467
468 pub fn add_tile_view(
471 &mut self,
472 panel: Arc<dyn PanelView>,
473 placement: DockPlacement,
474 bounds: Bounds<Pixels>,
475 window: &mut Window,
476 cx: &mut Context<Self>,
477 ) {
478 let id = panel.panel_id(cx);
479 self.add_panel_inner(id, panel, placement, Added::AsTile(bounds), window, cx);
480 }
481
482 fn add_panel_inner(
483 &mut self,
484 id: PanelId,
485 panel: Arc<dyn PanelView>,
486 placement: DockPlacement,
487 added: Added,
488 window: &mut Window,
489 cx: &mut Context<Self>,
490 ) {
491 let previous = self.panels.insert(id, panel);
502
503 if matches!(added, Added::Anywhere(_))
508 && placement != DockPlacement::Center
509 && !self.docks.contains_key(&placement)
510 {
511 self.docks.insert(
512 placement,
513 DockRegion {
514 tree: PaneTree::new(RootKind::Any),
515 dock: Dock::new(added.dock_size().unwrap_or(PANEL_MIN_SIZE * 2.)),
516 },
517 );
518 }
519
520 let Some(tree) = self.tree_mut(placement) else {
521 self.restore_registration(id, previous);
522 return;
523 };
524 let target = match added {
525 Added::AsTile(bounds) => match first_tiles_canvas(tree.root()) {
529 Some(node) => InsertTarget::Tile { node, bounds },
530 None => {
531 self.restore_registration(id, previous);
532 return;
533 }
534 },
535 Added::Anywhere(size) => match first_tab_group(tree.root()) {
536 Some(node) => InsertTarget::Tabs {
537 node,
538 ix: None,
539 activate: true,
540 },
541 None => match first_tiles_canvas(tree.root()) {
547 Some(node) => InsertTarget::Tile {
548 node,
549 bounds: TileMeta::default().bounds,
550 },
551 None => InsertTarget::Split {
556 node: tree.root().id(),
557 placement: Placement::Right,
558 size,
559 },
560 },
561 },
562 };
563 let result = tree.insert_panel(id, target);
564 if !result.changed() {
565 self.restore_registration(id, previous);
569 return;
570 }
571 self.commit(result, window, cx);
572 }
573
574 fn restore_registration(&mut self, id: PanelId, previous: Option<Arc<dyn PanelView>>) {
577 match previous {
578 Some(view) => self.panels.insert(id, view),
579 None => self.panels.remove(&id),
580 };
581 }
582
583 pub fn remove_panel<P: Panel>(
585 &mut self,
586 panel: Entity<P>,
587 window: &mut Window,
588 cx: &mut Context<Self>,
589 ) {
590 self.remove_panel_id(PanelId::from(panel.entity_id()), window, cx);
591 }
592
593 pub fn move_panel(
596 &mut self,
597 panel: PanelId,
598 target: InsertTarget,
599 window: &mut Window,
600 cx: &mut Context<Self>,
601 ) {
602 let Some(destination) = self.placement_of_node(target_node(&target)) else {
603 return;
604 };
605 let source = self.placement_of_panel(panel);
606
607 if matches!(target, InsertTarget::Split { .. }) {
610 self.adopt_measured_sizes(destination, cx);
611 }
612
613 let was_active = self
617 .layout(source.unwrap_or(destination))
618 .and_then(|tree| tree.find_panel_node(panel))
619 .and_then(|node| self.groups.get(&node))
620 .and_then(|cached| cached.entity.read(cx).last_notified_active(panel));
621
622 let changed = match source {
623 Some(source) if source == destination => {
624 let Some(tree) = self.tree_mut(destination) else {
625 return;
626 };
627 tree.move_panel(panel, target).changed()
628 }
629 source => {
630 let detached = source
641 .and_then(|source| self.tree_mut(source))
642 .is_some_and(|tree| tree.remove_panel(panel).changed());
643 let Some(tree) = self.tree_mut(destination) else {
644 return;
645 };
646 let inserted = tree.insert_panel(panel, target).changed();
647 detached || inserted
648 }
649 };
650
651 self.commit_changed(changed, window, cx);
652
653 if let Some(active) = was_active {
654 if let Some(cached) = self
655 .layout(destination)
656 .and_then(|tree| tree.find_panel_node(panel))
657 .and_then(|node| self.groups.get(&node))
658 {
659 let group = cached.entity.clone();
660 group.update(cx, |group, _| group.seed_active(panel, active));
661 }
662 }
663 }
664
665 pub fn split_at(
667 &mut self,
668 node: NodeId,
669 panel: PanelId,
670 placement: Placement,
671 window: &mut Window,
672 cx: &mut Context<Self>,
673 ) {
674 let Some(region) = self.placement_of_node(node) else {
675 return;
676 };
677 self.adopt_measured_sizes(region, cx);
678 let Some(tree) = self.tree_mut(region) else {
679 return;
680 };
681 let result = tree.split(node, panel, placement, None);
682 self.commit(result, window, cx);
683 }
684
685 fn remove_panel_id(&mut self, panel: PanelId, window: &mut Window, cx: &mut Context<Self>) {
686 let Some(region) = self.placement_of_panel(panel) else {
687 return;
688 };
689 let Some(tree) = self.tree_mut(region) else {
690 return;
691 };
692 let result = tree.remove_panel(panel);
693 self.commit(result, window, cx);
694 }
695}
696
697impl DockArea {
708 pub fn set_zoomed_in(&mut self, node: NodeId, window: &mut Window, cx: &mut Context<Self>) {
714 self.set_zoom(Some(Zoomed::Group(node)), window, cx);
715 }
716
717 pub fn set_zoomed_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
723 self.set_zoom(None, window, cx);
724 }
725
726 pub fn is_zoomed(&self) -> bool {
727 self.zoomed.is_some()
728 }
729
730 pub fn zoomed_group(&self) -> Option<NodeId> {
732 match self.zoomed {
733 Some(Zoomed::Group(node)) => Some(node),
734 _ => None,
735 }
736 }
737
738 pub fn zoomed_tile(&self) -> Option<PanelId> {
740 match self.zoomed {
741 Some(Zoomed::Tile { panel, .. }) => Some(panel),
742 _ => None,
743 }
744 }
745
746 fn set_zoom(&mut self, zoomed: Option<Zoomed>, window: &mut Window, cx: &mut Context<Self>) {
754 if self.zoomed == zoomed {
755 return;
756 }
757
758 if let Some(previous) = self.zoomed {
759 self.drive_zoom(previous, false, window, cx);
760 }
761 let accepted = match zoomed {
762 Some(next) => self.drive_zoom(next, true, window, cx).then_some(next),
763 None => None,
764 };
765 self.zoomed = accepted;
766 cx.notify();
767 }
768
769 fn drive_zoom(
775 &mut self,
776 zoomed: Zoomed,
777 zoom_in: bool,
778 window: &mut Window,
779 cx: &mut Context<Self>,
780 ) -> bool {
781 match zoomed {
782 Zoomed::Group(node) => {
783 let Some(group) = self.groups.get(&node).map(|cached| cached.entity.clone()) else {
784 return false;
785 };
786 group.update(cx, |group, cx| {
787 group.set_zoomed(zoom_in, window, cx);
788 group.is_zoomed() == zoom_in
789 })
790 }
791 Zoomed::Tile { node, panel } => {
792 let Some(canvas) = self.tiles.get(&node).map(|cached| cached.entity.clone()) else {
793 return false;
794 };
795 canvas.update(cx, |canvas, cx| {
796 canvas.set_zoomed(zoom_in.then_some(panel), window, cx);
797 canvas.zoomed_tile() == zoom_in.then_some(panel)
798 })
799 }
800 }
801 }
802
803 fn zoomed_view(&self) -> Option<AnyView> {
808 match self.zoomed? {
809 Zoomed::Group(node) => Some(self.groups.get(&node)?.entity.clone().into()),
810 Zoomed::Tile { node, .. } => Some(self.tiles.get(&node)?.entity.clone().into()),
811 }
812 }
813}
814
815impl DockArea {
817 pub fn load(
822 &mut self,
823 state: DockAreaState,
824 window: &mut Window,
825 cx: &mut Context<Self>,
826 ) -> Result<()> {
827 self.version = state.version;
828 self.zoomed = None;
829 self.groups.clear();
833 self.splits.clear();
834 self.tiles.clear();
835 self.docks.clear();
836 let dock_area = self.this.clone();
841 let renderer = self.renderer.clone();
842 let mut built = Vec::new();
843 self.center = {
844 let mut builder = RegistryPanelBuilder {
845 dock_area: dock_area.clone(),
846 renderer: renderer.clone(),
847 built: &mut built,
848 window,
849 cx,
850 };
851 PaneTree::from_state(&state.center, RootKind::Split, &mut builder)
852 };
853
854 for dock_state in [state.left_dock, state.right_dock, state.bottom_dock]
855 .into_iter()
856 .flatten()
857 {
858 let tree = {
859 let mut builder = RegistryPanelBuilder {
860 dock_area: dock_area.clone(),
861 renderer: renderer.clone(),
862 built: &mut built,
863 window,
864 cx,
865 };
866 PaneTree::from_state(dock_state.panel(), RootKind::Any, &mut builder)
867 };
868 let mut dock = Dock::new(dock_state.size());
869 dock.set_open(dock_state.open());
870 self.docks
871 .insert(dock_state.placement(), DockRegion { tree, dock });
872 }
873
874 self.panels.extend(built);
875 self.reconcile(window, cx);
876 cx.emit(DockEvent::LayoutChanged);
877 Ok(())
878 }
879
880 pub fn dump(&self, cx: &App) -> DockAreaState {
901 let source = LivePanels::new(&self.panels, cx);
902
903 DockAreaState {
904 version: self.version,
905 center: self.resolved_tree(&self.center, cx).to_state(&source),
906 left_dock: self.dump_dock(DockPlacement::Left, &source, cx),
907 right_dock: self.dump_dock(DockPlacement::Right, &source, cx),
908 bottom_dock: self.dump_dock(DockPlacement::Bottom, &source, cx),
909 }
910 }
911
912 fn dump_dock(
913 &self,
914 placement: DockPlacement,
915 source: &LivePanels<'_>,
916 cx: &App,
917 ) -> Option<DockState> {
918 let pane = self.docks.get(&placement)?;
919 Some(DockState::new(
920 self.resolved_tree(&pane.tree, cx).to_state(source),
921 placement,
922 pane.dock.size(),
923 pane.dock.is_open(),
924 ))
925 }
926
927 fn resolved_tree(&self, tree: &PaneTree, cx: &App) -> PaneTree {
928 let mut tree = tree.clone();
929 self.resolve_sizes(tree.root_mut(), cx);
930 tree
931 }
932
933 fn resolve_sizes(&self, node: &mut PaneNode, cx: &App) {
934 let measured = self
935 .splits
936 .get(&node.id())
937 .map(|cached| cached.entity.read(cx).sizes().clone())
938 .unwrap_or_default();
939
940 let NodeKind::Split {
941 children, sizes, ..
942 } = node.kind_mut()
943 else {
944 return;
945 };
946
947 for (ix, size) in sizes.iter_mut().enumerate() {
948 let on_screen = measured.get(ix).copied().filter(|size| *size > px(0.));
953 let stored = (*size).filter(|size| *size > px(0.));
954 *size = Some(on_screen.or(stored).unwrap_or(PANEL_MIN_SIZE));
955 }
956
957 for child in children.iter_mut() {
958 self.resolve_sizes(child, cx);
959 }
960 }
961}
962
963impl DockArea {
965 fn commit(&mut self, result: EditResult, window: &mut Window, cx: &mut Context<Self>) {
974 self.commit_changed(result.changed(), window, cx);
975 }
976
977 fn commit_changed(&mut self, changed: bool, window: &mut Window, cx: &mut Context<Self>) {
979 if !changed {
980 return;
981 }
982
983 self.reconcile(window, cx);
984 cx.emit(DockEvent::LayoutChanged);
985 }
986
987 fn reconcile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
994 let mut plans = Vec::new();
997 plan_tree(&self.center, false, self.locked, &mut plans);
998 for pane in self.docks.values() {
999 plan_tree(&pane.tree, !pane.dock.is_open(), self.locked, &mut plans);
1000 }
1001
1002 let mut live_nodes = HashSet::with_capacity(plans.len());
1006 let mut live_panels: HashSet<PanelId> = HashSet::new();
1007
1008 for plan in plans {
1009 live_nodes.insert(plan.node());
1010 match plan {
1011 ContainerPlan::Split {
1012 node,
1013 axis,
1014 children,
1015 sizes,
1016 } => {
1017 let state = self.split_entity(node, cx);
1018 let (previous, adopted) = self
1019 .splits
1020 .get(&node)
1021 .map(|cached| (cached.children.clone(), cached.sizes.clone()))
1022 .unwrap_or_default();
1023 if previous != children || adopted != sizes {
1031 state.update(cx, |state, cx| {
1032 sync_split_panels(state, &previous, &children, &sizes, cx);
1033 state.sync_panels_count(axis, children.len(), cx);
1034 state.adopt_sizes(&scale_sizes_to(state.container_size(), &sizes), cx);
1050 });
1051 }
1052 if let Some(cached) = self.splits.get_mut(&node) {
1053 cached.children = children;
1054 cached.sizes = sizes;
1055 }
1056 }
1057 ContainerPlan::Group {
1058 node,
1059 panels,
1060 active_ix,
1061 constraints,
1062 } => {
1063 live_panels.extend(panels.iter().copied());
1064 let views = self.views_of(&panels);
1065 let group = self.group_entity(node, window, cx);
1066 group.update(cx, |group, cx| {
1067 group.set_constraints(constraints, window, cx);
1071 group.sync_from_tree(views, active_ix, window, cx);
1072 });
1073 }
1074 ContainerPlan::Tiles { node, tiles } => {
1075 live_panels.extend(tiles.iter().map(|(panel, _, _)| *panel));
1076 let mirrored = tiles
1077 .iter()
1078 .filter_map(|(panel, bounds, z_index)| {
1079 self.panels
1080 .get(panel)
1081 .map(|view| (view.clone(), *bounds, *z_index))
1082 })
1083 .collect();
1084 let canvas = self.tiles_entity(node, window, cx);
1085 canvas.update(cx, |canvas, cx| canvas.sync_from_tree(mirrored, cx));
1086 }
1087 }
1088 }
1089
1090 self.groups.retain(|node, _| live_nodes.contains(node));
1091 self.splits.retain(|node, _| live_nodes.contains(node));
1092 self.tiles.retain(|node, _| live_nodes.contains(node));
1093
1094 let departed: Vec<Arc<dyn PanelView>> = self
1095 .panels
1096 .iter()
1097 .filter(|(panel, _)| !live_panels.contains(panel))
1098 .map(|(_, view)| view.clone())
1099 .collect();
1100 self.panels.retain(|panel, _| live_panels.contains(panel));
1101
1102 let zoom_survives = match self.zoomed {
1112 Some(Zoomed::Group(node)) => self.groups.contains_key(&node),
1113 Some(Zoomed::Tile { node, panel }) => {
1114 self.tiles.contains_key(&node) && self.panels.contains_key(&panel)
1115 }
1116 None => true,
1117 };
1118 if !zoom_survives {
1119 self.set_zoom(None, window, cx);
1120 }
1121
1122 cx.notify();
1123 for view in departed {
1126 view.on_removed(window, cx);
1127 }
1128 }
1129
1130 fn views_of(&self, panels: &[PanelId]) -> Vec<Arc<dyn PanelView>> {
1132 debug_assert!(
1133 panels.iter().all(|panel| self.panels.contains_key(panel)),
1134 "every panel in a tree must have a live view; a missing one would \
1135 silently shift the group's active index"
1136 );
1137 panels
1138 .iter()
1139 .filter_map(|panel| self.panels.get(panel).cloned())
1140 .collect()
1141 }
1142
1143 fn group_entity(
1144 &mut self,
1145 node: NodeId,
1146 window: &mut Window,
1147 cx: &mut Context<Self>,
1148 ) -> Entity<TabGroup> {
1149 if let Some(cached) = self.groups.get(&node) {
1150 return cached.entity.clone();
1151 }
1152
1153 let renderer = self.renderer.tab_group_renderer();
1154 let entity = cx.new(|cx| TabGroup::new(node, window, cx).with_renderer(renderer));
1155 let subscription = cx.subscribe_in(&entity, window, Self::on_tab_group_event);
1156 self.groups.insert(
1157 node,
1158 Cached {
1159 entity: entity.clone(),
1160 _subscription: subscription,
1161 },
1162 );
1163 entity
1164 }
1165
1166 fn tiles_entity(
1167 &mut self,
1168 node: NodeId,
1169 window: &mut Window,
1170 cx: &mut Context<Self>,
1171 ) -> Entity<TilesState> {
1172 if let Some(cached) = self.tiles.get(&node) {
1173 return cached.entity.clone();
1174 }
1175
1176 let renderer = self.renderer.tiles_renderer();
1177 let entity = cx.new(|cx| TilesState::new(node, window, cx).with_renderer(renderer));
1178 let subscription = cx.subscribe_in(&entity, window, Self::on_tiles_event);
1179 self.tiles.insert(
1180 node,
1181 Cached {
1182 entity: entity.clone(),
1183 _subscription: subscription,
1184 },
1185 );
1186 entity
1187 }
1188
1189 fn split_entity(&mut self, node: NodeId, cx: &mut Context<Self>) -> Entity<ResizableState> {
1190 if let Some(cached) = self.splits.get(&node) {
1191 return cached.entity.clone();
1192 }
1193
1194 let entity = cx.new(|_| ResizableState::default());
1195 let subscription =
1205 cx.subscribe(&entity, move |this, state, _: &ResizablePanelEvent, cx| {
1206 let sizes: Vec<Option<Pixels>> = state
1207 .read(cx)
1208 .sizes()
1209 .iter()
1210 .map(|size| Some(*size))
1211 .collect();
1212 let Some(region) = this.placement_of_node(node) else {
1213 return;
1214 };
1215 let Some(tree) = this.tree_mut(region) else {
1216 return;
1217 };
1218 if tree.set_sizes(node, sizes.clone()).changed() {
1219 cx.emit(DockEvent::LayoutChanged);
1220 }
1221 if let Some(cached) = this.splits.get_mut(&node) {
1224 cached.sizes = sizes;
1225 }
1226 });
1227 self.splits.insert(
1228 node,
1229 CachedSplit {
1230 entity: entity.clone(),
1231 children: Vec::new(),
1232 sizes: Vec::new(),
1233 _subscription: subscription,
1234 },
1235 );
1236 entity
1237 }
1238}
1239
1240impl DockArea {
1242 fn on_tab_group_event(
1243 &mut self,
1244 group: &Entity<TabGroup>,
1245 event: &TabGroupEvent,
1246 window: &mut Window,
1247 cx: &mut Context<Self>,
1248 ) {
1249 match event {
1250 TabGroupEvent::Drop { panel, target, .. } => {
1251 self.move_panel(*panel, *target, window, cx)
1252 }
1253 TabGroupEvent::DragDrop { item, target } => cx.emit(DockEvent::DragDrop {
1254 item: item.clone(),
1255 target: target.clone(),
1256 }),
1257 TabGroupEvent::ClosePanel { panel } => self.remove_panel_id(*panel, window, cx),
1258 TabGroupEvent::ActiveChanged { ix } => {
1259 let node = group.read(cx).node();
1260 let Some(region) = self.placement_of_node(node) else {
1261 return;
1262 };
1263 let Some(tree) = self.tree_mut(region) else {
1264 return;
1265 };
1266 let result = tree.set_active(node, *ix);
1267 self.commit(result, window, cx);
1268 }
1269 TabGroupEvent::ZoomIn => {
1270 let node = group.read(cx).node();
1271 self.set_zoom(Some(Zoomed::Group(node)), window, cx);
1272 }
1273 TabGroupEvent::ZoomOut => {
1278 let node = group.read(cx).node();
1279 if self.zoomed == Some(Zoomed::Group(node)) {
1280 self.set_zoom(None, window, cx);
1281 }
1282 }
1283 }
1284 }
1285
1286 fn on_tiles_event(
1287 &mut self,
1288 canvas: &Entity<TilesState>,
1289 event: &TilesEvent,
1290 window: &mut Window,
1291 cx: &mut Context<Self>,
1292 ) {
1293 let node = canvas.read(cx).node();
1294 let Some(region) = self.placement_of_node(node) else {
1295 return;
1296 };
1297
1298 match event {
1299 TilesEvent::BoundsChanged { panel, bounds } => {
1300 let Some(tree) = self.tree_mut(region) else {
1301 return;
1302 };
1303 let result = tree.set_tile_bounds(*panel, *bounds);
1304 self.commit(result, window, cx);
1305 }
1306 TilesEvent::BringToFront { panel } => {
1307 let Some(tree) = self.tree_mut(region) else {
1308 return;
1309 };
1310 let result = tree.bring_to_front(*panel);
1311 self.commit(result, window, cx);
1312 }
1313 TilesEvent::ClosePanel { panel } => self.remove_panel_id(*panel, window, cx),
1314 TilesEvent::DragDrop { item } => cx.emit(DockEvent::DragDrop {
1315 item: item.clone(),
1316 target: DropTarget::Canvas,
1317 }),
1318 TilesEvent::ZoomIn { panel } => {
1319 self.set_zoom(
1320 Some(Zoomed::Tile {
1321 node,
1322 panel: *panel,
1323 }),
1324 window,
1325 cx,
1326 );
1327 }
1328 TilesEvent::ZoomOut => {
1331 if matches!(self.zoomed, Some(Zoomed::Tile { node: zoomed, .. }) if zoomed == node)
1332 {
1333 self.set_zoom(None, window, cx);
1334 }
1335 }
1336 }
1337 }
1338}
1339
1340impl DockArea {
1342 fn adopt_measured_sizes(&mut self, placement: DockPlacement, cx: &App) {
1349 let measured: HashMap<NodeId, Vec<Pixels>> = self
1350 .splits
1351 .iter()
1352 .filter(|(_, cached)| cached.entity.read(cx).container_size() > Pixels::ZERO)
1358 .map(|(node, cached)| (*node, cached.entity.read(cx).sizes().clone()))
1359 .collect();
1360
1361 if let Some(tree) = self.tree_mut(placement) {
1362 tree.adopt_measured_sizes(&measured);
1363 }
1364 }
1365
1366 fn tree_mut(&mut self, placement: DockPlacement) -> Option<&mut PaneTree> {
1367 match placement {
1368 DockPlacement::Center => Some(&mut self.center),
1369 _ => self.docks.get_mut(&placement).map(|pane| &mut pane.tree),
1370 }
1371 }
1372
1373 fn placement_of_node(&self, node: NodeId) -> Option<DockPlacement> {
1376 if self.center.find_node(node).is_some() {
1377 return Some(DockPlacement::Center);
1378 }
1379 self.docks
1380 .iter()
1381 .find(|(_, pane)| pane.tree.find_node(node).is_some())
1382 .map(|(placement, _)| *placement)
1383 }
1384
1385 fn placement_of_panel(&self, panel: PanelId) -> Option<DockPlacement> {
1386 if self.center.find_panel_node(panel).is_some() {
1387 return Some(DockPlacement::Center);
1388 }
1389 self.docks
1390 .iter()
1391 .find(|(_, pane)| pane.tree.find_panel_node(panel).is_some())
1392 .map(|(placement, _)| *placement)
1393 }
1394
1395 fn resize_dock(
1398 &mut self,
1399 placement: DockPlacement,
1400 pointer: Point<Pixels>,
1401 cx: &mut Context<Self>,
1402 ) {
1403 let opposite = match placement {
1404 DockPlacement::Left => self.dock_size(DockPlacement::Right),
1405 DockPlacement::Right => self.dock_size(DockPlacement::Left),
1406 _ => None,
1407 };
1408 let sizing = DockSizing::new(placement)
1409 .with_area_bounds(self.bounds)
1410 .with_opposite_dock_size(opposite.unwrap_or(px(0.)));
1411 let size = sizing.clamp(sizing.size_from_pointer(pointer));
1412
1413 if let Some(pane) = self.docks.get_mut(&placement) {
1414 pane.dock.set_size(size);
1415 cx.notify();
1416 }
1417 }
1418}
1419
1420impl DockArea {
1422 fn render_node(&self, node: &PaneNode, window: &mut Window, cx: &mut App) -> AnyElement {
1424 match node.kind() {
1425 PaneRef::Split {
1426 axis,
1427 children,
1428 sizes,
1429 } => {
1430 let group = match axis {
1431 Axis::Horizontal => h_resizable(("dock-split", node.id().as_u64())),
1432 Axis::Vertical => v_resizable(("dock-split", node.id().as_u64())),
1433 };
1434 let shown: Vec<bool> = children
1438 .iter()
1439 .map(|child| self.is_node_visible(child, cx))
1440 .collect();
1441 let grows = shown.iter().rposition(|shown| *shown);
1447 let panels: Vec<_> = children
1448 .iter()
1449 .zip(sizes.iter())
1450 .enumerate()
1451 .map(|(ix, (child, size))| {
1452 resizable_panel()
1453 .visible(shown[ix])
1454 .child(self.render_node(child, window, cx))
1455 .when_some(*size, |panel, size| {
1467 panel
1468 .size(size)
1469 .when(Some(ix) != grows, |panel| panel.flex_none())
1470 })
1471 })
1472 .collect();
1473
1474 let group = group
1475 .when_some(self.splits.get(&node.id()), |group, cached| {
1476 group.with_state(&cached.entity)
1477 })
1478 .with_handle_appearance({
1479 let renderer = self.renderer.clone();
1480 Rc::new(move |handle, window, cx| {
1481 renderer.render_split_handle(handle, window, cx)
1482 })
1483 })
1484 .children(panels);
1485
1486 self.renderer
1487 .split_frame(node.id(), axis, window, cx)
1488 .size_full()
1496 .flex_1()
1497 .min_h(px(0.))
1498 .overflow_hidden()
1499 .child(group)
1500 .into_any_element()
1501 }
1502 PaneRef::Tabs { .. } => match self.groups.get(&node.id()) {
1503 Some(cached) => cached.entity.clone().into_any_element(),
1504 None => Empty.into_any_element(),
1505 },
1506 PaneRef::Tiles { .. } => match self.tiles.get(&node.id()) {
1507 Some(cached) => cached.entity.clone().into_any_element(),
1508 None => Empty.into_any_element(),
1509 },
1510 }
1511 }
1512
1513 fn is_node_visible(&self, node: &PaneNode, cx: &App) -> bool {
1519 let panels = LivePanels::new(&self.panels, cx);
1520 match node.kind() {
1521 PaneRef::Split { children, .. } => {
1522 children.iter().any(|child| self.is_node_visible(child, cx))
1523 }
1524 PaneRef::Tabs { panels: ids, .. } => ids.iter().any(|panel| panels.is_visible(*panel)),
1525 PaneRef::Tiles { panels: tiles } => {
1526 tiles.iter().any(|tile| panels.is_visible(tile.panel()))
1527 }
1528 }
1529 }
1530
1531 fn render_dock(
1532 &self,
1533 placement: DockPlacement,
1534 window: &mut Window,
1535 cx: &mut App,
1536 ) -> Option<AnyElement> {
1537 let pane = self.docks.get(&placement)?;
1538 let dock = self.dock_context(placement, &pane.dock);
1539
1540 let size = dock_extent(&dock);
1545 if size <= px(0.) {
1546 return Some(div().into_any_element());
1547 }
1548
1549 let content = self.render_node(pane.tree.root(), window, cx);
1550 let chrome = self.renderer.render_dock(&dock, content, window, cx);
1559 Some(dock_frame(&dock, size).child(chrome).into_any_element())
1560 }
1561
1562 fn dock_context(&self, placement: DockPlacement, dock: &Dock) -> DockContext {
1563 let area = self.this.clone();
1564
1565 DockContext {
1566 placement,
1567 size: dock.size(),
1568 open: dock.is_open(),
1569 collapsible: dock.is_collapsible(),
1570 on_toggle: {
1571 let area = area.clone();
1572 Rc::new(move |window, cx| {
1573 _ = area.update(cx, |area, cx| area.toggle_dock(placement, window, cx));
1574 })
1575 },
1576 on_resize: Rc::new(move |pointer, _, cx| {
1577 _ = area.update(cx, |area, cx| area.resize_dock(placement, pointer, cx));
1578 }),
1579 }
1580 }
1581}
1582
1583impl EventEmitter<DockEvent> for DockArea {}
1584
1585impl Focusable for DockArea {
1586 fn focus_handle(&self, _: &App) -> FocusHandle {
1587 self.focus_handle.clone()
1588 }
1589}
1590
1591impl Render for DockArea {
1592 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1593 let area = cx.entity();
1594 let renderer = self.renderer.clone();
1595
1596 renderer
1597 .frame(window, cx)
1598 .test_support()
1599 .relative()
1606 .size_full()
1607 .overflow_hidden()
1608 .flex()
1609 .flex_row()
1610 .on_prepaint(move |bounds, _, cx| {
1611 area.update(cx, |area, _| area.bounds = bounds);
1612 })
1613 .track_focus(&self.focus_handle)
1614 .map(|frame| match self.zoomed_view() {
1615 Some(view) => frame.child(view),
1616 None => frame
1617 .when_some(
1618 self.render_dock(DockPlacement::Left, window, cx),
1619 ParentElement::child,
1620 )
1621 .child(
1622 renderer
1623 .center_frame(window, cx)
1624 .flex()
1629 .flex_1()
1630 .flex_col()
1631 .overflow_hidden()
1632 .child(self.render_node(self.center.root(), window, cx))
1633 .when_some(
1634 self.render_dock(DockPlacement::Bottom, window, cx),
1635 ParentElement::child,
1636 ),
1637 )
1638 .when_some(
1639 self.render_dock(DockPlacement::Right, window, cx),
1640 ParentElement::child,
1641 ),
1642 })
1643 }
1644}
1645
1646enum ContainerPlan {
1648 Split {
1649 node: NodeId,
1650 axis: Axis,
1651 children: Vec<NodeId>,
1652 sizes: Vec<Option<Pixels>>,
1653 },
1654 Group {
1655 node: NodeId,
1656 panels: Vec<PanelId>,
1657 active_ix: usize,
1658 constraints: TabGroupConstraints,
1659 },
1660 Tiles {
1661 node: NodeId,
1662 tiles: Vec<(PanelId, Bounds<Pixels>, usize)>,
1663 },
1664}
1665
1666impl ContainerPlan {
1667 fn node(&self) -> NodeId {
1668 match self {
1669 Self::Split { node, .. } | Self::Group { node, .. } | Self::Tiles { node, .. } => *node,
1670 }
1671 }
1672}
1673
1674fn scale_sizes_to(container: Pixels, sizes: &[Option<Pixels>]) -> Vec<Option<Pixels>> {
1681 let total: f32 = sizes.iter().flatten().map(|size| size.as_f32()).sum();
1682 if container <= px(0.) || total <= 0. || sizes.iter().any(Option::is_none) {
1683 return sizes.to_vec();
1684 }
1685
1686 let scale = container.as_f32() / total;
1687 sizes
1688 .iter()
1689 .map(|size| size.map(|size| px(size.as_f32() * scale)))
1690 .collect()
1691}
1692
1693fn sync_split_panels(
1703 state: &mut ResizableState,
1704 previous: &[NodeId],
1705 next: &[NodeId],
1706 sizes: &[Option<Pixels>],
1707 cx: &mut Context<ResizableState>,
1708) {
1709 let mut current = previous.to_vec();
1710
1711 for ix in (0..current.len()).rev() {
1713 if !next.contains(¤t[ix]) {
1714 if ix < state.sizes().len() {
1715 state.remove_panel(ix, cx);
1716 }
1717 current.remove(ix);
1718 }
1719 }
1720
1721 for (ix, node) in next.iter().enumerate() {
1722 if current.get(ix) == Some(node) {
1723 continue;
1724 }
1725 let at = ix.min(state.sizes().len());
1726 state.insert_panel(sizes.get(ix).copied().flatten(), Some(at), cx);
1731 current.insert(at.min(current.len()), *node);
1732 }
1733
1734 debug_assert_eq!(
1735 current, next,
1736 "the split's panel list must end up mirroring its children exactly; \
1737 a reordering edit would need its own case here"
1738 );
1739}
1740
1741fn plan_tree(tree: &PaneTree, collapsed: bool, locked: bool, out: &mut Vec<ContainerPlan>) {
1742 plan_node(tree.root(), true, collapsed, locked, out);
1744}
1745
1746fn plan_node(
1747 node: &PaneNode,
1748 alone: bool,
1749 collapsed: bool,
1750 locked: bool,
1751 out: &mut Vec<ContainerPlan>,
1752) {
1753 match node.kind() {
1754 PaneRef::Split {
1755 axis,
1756 children,
1757 sizes,
1758 } => {
1759 out.push(ContainerPlan::Split {
1760 node: node.id(),
1761 axis,
1762 children: children.iter().map(PaneNode::id).collect(),
1763 sizes: sizes.to_vec(),
1764 });
1765 let children_alone = children.len() <= 1;
1766 for child in children {
1767 plan_node(child, children_alone, collapsed, locked, out);
1768 }
1769 }
1770 PaneRef::Tabs { panels, active_ix } => out.push(ContainerPlan::Group {
1771 node: node.id(),
1772 panels: panels.to_vec(),
1773 active_ix,
1774 constraints: TabGroupConstraints::in_split(alone)
1775 .dock_locked(locked)
1776 .collapsed(collapsed),
1777 }),
1778 PaneRef::Tiles { panels } => out.push(ContainerPlan::Tiles {
1779 node: node.id(),
1780 tiles: panels
1781 .iter()
1782 .map(|tile| (tile.panel(), tile.bounds(), tile.z_index()))
1783 .collect(),
1784 }),
1785 }
1786}
1787
1788fn first_tab_group(node: &PaneNode) -> Option<NodeId> {
1789 match node.kind() {
1790 PaneRef::Tabs { .. } => Some(node.id()),
1791 PaneRef::Split { children, .. } => children.iter().find_map(first_tab_group),
1792 PaneRef::Tiles { .. } => None,
1793 }
1794}
1795
1796fn first_tiles_canvas(node: &PaneNode) -> Option<NodeId> {
1797 match node.kind() {
1798 PaneRef::Tiles { .. } => Some(node.id()),
1799 PaneRef::Split { children, .. } => children.iter().find_map(first_tiles_canvas),
1800 PaneRef::Tabs { .. } => None,
1801 }
1802}
1803
1804fn target_node(target: &InsertTarget) -> NodeId {
1805 match target {
1806 InsertTarget::Tabs { node, .. }
1807 | InsertTarget::Split { node, .. }
1808 | InsertTarget::Tile { node, .. } => *node,
1809 }
1810}
1811
1812struct RegistryPanelBuilder<'a, 'w, 'c> {
1814 dock_area: WeakEntity<DockArea>,
1815 renderer: Rc<dyn DockAreaRenderer>,
1816 built: &'a mut Vec<(PanelId, Arc<dyn PanelView>)>,
1817 window: &'w mut Window,
1818 cx: &'c mut App,
1819}
1820
1821impl PanelBuilder for RegistryPanelBuilder<'_, '_, '_> {
1822 fn build(&mut self, state: &PanelState, info: &PanelInfo) -> PanelId {
1823 let context = PanelBuildContext::new(self.dock_area.clone(), state, info);
1824 let view =
1825 match PanelRegistry::build_panel(&state.panel_name, context, self.window, self.cx) {
1826 Some(view) => view,
1827 None => self
1828 .renderer
1829 .build_placeholder(state, self.window, self.cx)
1830 .unwrap_or_else(|| {
1831 Arc::new(self.cx.new(|cx| PlaceholderPanel::new(state.clone(), cx)))
1832 as Arc<dyn PanelView>
1833 }),
1834 };
1835
1836 let id = view.panel_id(self.cx);
1837 self.built.push((id, view));
1838 id
1839 }
1840}
1841
1842struct PlaceholderPanel {
1849 state: PanelState,
1850 focus_handle: FocusHandle,
1851}
1852
1853impl PlaceholderPanel {
1854 fn new(state: PanelState, cx: &mut Context<Self>) -> Self {
1855 Self {
1856 state,
1857 focus_handle: cx.focus_handle(),
1858 }
1859 }
1860}
1861
1862impl Panel for PlaceholderPanel {
1863 fn panel_name(&self) -> &'static str {
1864 "InvalidPanel"
1865 }
1866
1867 fn dump(&self, _: &App) -> PanelState {
1868 self.state.clone()
1869 }
1870}
1871
1872impl EventEmitter<PanelEvent> for PlaceholderPanel {}
1873
1874impl Focusable for PlaceholderPanel {
1875 fn focus_handle(&self, _: &App) -> FocusHandle {
1876 self.focus_handle.clone()
1877 }
1878}
1879
1880impl Render for PlaceholderPanel {
1881 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1882 Empty
1883 }
1884}
1885
1886type DockToggleHandler = Rc<dyn Fn(&mut Window, &mut App)>;
1887type DockResizeHandler = Rc<dyn Fn(Point<Pixels>, &mut Window, &mut App)>;
1888
1889#[derive(Clone)]
1892pub struct DockContext {
1893 placement: DockPlacement,
1894 size: Pixels,
1895 open: bool,
1896 collapsible: bool,
1897 on_toggle: DockToggleHandler,
1898 on_resize: DockResizeHandler,
1899}
1900
1901impl DockContext {
1902 pub fn placement(&self) -> DockPlacement {
1903 self.placement
1904 }
1905
1906 pub fn size(&self) -> Pixels {
1909 self.size
1910 }
1911
1912 pub fn is_open(&self) -> bool {
1913 self.open
1914 }
1915
1916 pub fn is_collapsible(&self) -> bool {
1917 self.collapsible
1918 }
1919
1920 pub fn toggle(&self, window: &mut Window, cx: &mut App) {
1921 (self.on_toggle)(window, cx);
1922 }
1923
1924 pub fn resize_to(&self, pointer: Point<Pixels>, window: &mut Window, cx: &mut App) {
1927 (self.on_resize)(pointer, window, cx);
1928 }
1929}
1930
1931pub const CLOSED_BOTTOM_STRIP: Pixels = px(29.);
1935
1936pub fn dock_extent(dock: &DockContext) -> Pixels {
1938 match (dock.is_open(), dock.placement()) {
1939 (true, _) => dock.size(),
1940 (false, DockPlacement::Bottom) => CLOSED_BOTTOM_STRIP,
1941 (false, _) => px(0.),
1942 }
1943}
1944
1945pub fn dock_frame(dock: &DockContext, size: Pixels) -> Div {
1951 div()
1952 .flex()
1953 .flex_none()
1954 .relative()
1955 .overflow_hidden()
1956 .map(|this| match dock.placement() {
1957 DockPlacement::Left | DockPlacement::Right => this.flex_row().h_full().w(size),
1958 DockPlacement::Bottom => this.w_full().h(size),
1959 DockPlacement::Center => this,
1961 })
1962}
1963
1964#[allow(unused_variables)]
1975pub trait DockAreaRenderer: 'static {
1976 fn frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
1982 div().id("dock-area")
1983 }
1984
1985 fn split_frame(
1995 &self,
1996 node: NodeId,
1997 axis: Axis,
1998 window: &mut Window,
1999 cx: &mut App,
2000 ) -> Stateful<Div> {
2001 div().id(("dock-split-frame", node.as_u64()))
2002 }
2003
2004 fn center_frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
2008 div().id("dock-area-center")
2009 }
2010
2011 fn render_split_handle(
2018 &self,
2019 handle: &ResizeHandleContext,
2020 window: &mut Window,
2021 cx: &mut App,
2022 ) -> Option<AnyElement> {
2023 None
2024 }
2025
2026 fn render_dock(
2035 &self,
2036 dock: &DockContext,
2037 content: AnyElement,
2038 window: &mut Window,
2039 cx: &mut App,
2040 ) -> AnyElement {
2041 content
2042 }
2043
2044 fn build_placeholder(
2059 &self,
2060 state: &PanelState,
2061 window: &mut Window,
2062 cx: &mut App,
2063 ) -> Option<Arc<dyn PanelView>> {
2064 None
2065 }
2066
2067 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer>;
2068
2069 fn tiles_renderer(&self) -> Rc<dyn TilesRenderer>;
2070}
2071
2072struct BareDockArea;
2074
2075impl DockAreaRenderer for BareDockArea {
2076 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
2077 Rc::new(BareTabGroup)
2078 }
2079
2080 fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> {
2081 Rc::new(BareTiles)
2082 }
2083}
2084
2085#[cfg(test)]
2086impl DockArea {
2087 pub(crate) fn container_entity_ids(&self) -> Vec<(NodeId, gpui::EntityId)> {
2094 let mut ids: Vec<(NodeId, gpui::EntityId)> = self
2095 .groups
2096 .iter()
2097 .map(|(node, cached)| (*node, cached.entity.entity_id()))
2098 .chain(
2099 self.splits
2100 .iter()
2101 .map(|(node, cached)| (*node, cached.entity.entity_id())),
2102 )
2103 .chain(
2104 self.tiles
2105 .iter()
2106 .map(|(node, cached)| (*node, cached.entity.entity_id())),
2107 )
2108 .collect();
2109 ids.sort();
2110 ids
2111 }
2112}
2113
2114#[cfg(test)]
2115mod tests {
2116 use gpui::{TestAppContext, VisualTestContext};
2117
2118 use std::{
2119 cell::{Cell, RefCell},
2120 rc::Rc,
2121 };
2122
2123 use super::*;
2124 use crate::dock::test_support::{Log, PanelSignal, TestPanel, drain, drain_active, log_of};
2125 use crate::dock::{TabGroupContext, TileContext};
2126
2127 #[test]
2130 fn slot_sizes_are_re_expressed_as_shares_of_the_container() {
2131 let scaled = scale_sizes_to(px(800.), &[Some(px(300.)), Some(px(100.))]);
2132
2133 assert_eq!(scaled, vec![Some(px(600.)), Some(px(200.))]);
2134 }
2135
2136 #[test]
2139 fn an_unconstrained_slot_leaves_every_size_alone() {
2140 let sizes = [Some(px(300.)), None];
2141
2142 assert_eq!(scale_sizes_to(px(800.), &sizes), sizes.to_vec());
2143 }
2144
2145 #[test]
2148 fn an_unusable_container_or_total_leaves_every_size_alone() {
2149 let sizes = [Some(px(300.)), Some(px(100.))];
2150 assert_eq!(scale_sizes_to(px(0.), &sizes), sizes.to_vec());
2151
2152 let zeroed = [Some(px(0.)), Some(px(0.))];
2153 assert_eq!(scale_sizes_to(px(800.), &zeroed), zeroed.to_vec());
2154 }
2155
2156 fn setup(cx: &mut TestAppContext) -> (Entity<DockArea>, &mut VisualTestContext) {
2157 cx.update(|cx| {
2158 let _ = crate::Theme::global_mut(cx);
2159 });
2160 cx.add_window_view(|window, cx| DockArea::new("test-dock", None, window, cx))
2161 }
2162
2163 #[gpui::test]
2164 fn dock_size_change_emits_one_layout_event(cx: &mut TestAppContext) {
2165 let (area, cx) = setup(cx);
2166 cx.update(|window, cx| {
2167 area.update(cx, |area, cx| {
2168 area.set_dock(
2169 DockPlacement::Left,
2170 DockLayout::tabs().panel(TestPanel::new("Left", cx)),
2171 window,
2172 cx,
2173 );
2174 });
2175 });
2176
2177 let events = Rc::new(Cell::new(0));
2178 let observed = events.clone();
2179 let _subscription = cx.update(|window, cx| {
2180 window.subscribe(&area, cx, move |_, event: &DockEvent, _, _| {
2181 if matches!(event, DockEvent::LayoutChanged) {
2182 observed.set(observed.get() + 1);
2183 }
2184 })
2185 });
2186
2187 cx.update(|window, cx| {
2188 area.update(cx, |area, cx| {
2189 area.set_dock_size(DockPlacement::Left, px(320.), window, cx);
2190 area.set_dock_size(DockPlacement::Left, px(320.), window, cx);
2191 });
2192 });
2193 assert_eq!(
2194 events.get(),
2195 1,
2196 "only an effective size change is persisted"
2197 );
2198 }
2199
2200 fn two_groups<'a>(
2202 log: &Log,
2203 cx: &'a mut TestAppContext,
2204 ) -> (
2205 Entity<DockArea>,
2206 Entity<TestPanel>,
2207 &'a mut VisualTestContext,
2208 ) {
2209 let (area, cx) = setup(cx);
2210 let log = log.clone();
2211 let alpha = cx.update(|window, cx| {
2212 let alpha = TestPanel::logging("Alpha", &log, cx);
2213 let beta = TestPanel::logging("Beta", &log, cx);
2214 area.update(cx, |area, cx| {
2215 area.set_center(
2216 DockLayout::h_split()
2217 .child(DockLayout::tabs().panel(alpha.clone()), None)
2218 .child(DockLayout::tabs().panel(beta), None),
2219 window,
2220 cx,
2221 );
2222 });
2223 alpha
2224 });
2225 (area, alpha, cx)
2226 }
2227
2228 fn child_node(area: &Entity<DockArea>, ix: usize, cx: &mut VisualTestContext) -> NodeId {
2230 cx.read(|cx| {
2231 let PaneRef::Split { children, .. } = area
2232 .read(cx)
2233 .layout(DockPlacement::Center)
2234 .unwrap()
2235 .root()
2236 .kind()
2237 else {
2238 panic!("the center root is a split");
2239 };
2240 children[ix].id()
2241 })
2242 }
2243
2244 fn panel_id_of(panel: &Entity<TestPanel>) -> PanelId {
2245 PanelId::from(panel.entity_id())
2246 }
2247
2248 fn move_alpha_into_the_other_group(
2249 area: &Entity<DockArea>,
2250 alpha: &Entity<TestPanel>,
2251 cx: &mut VisualTestContext,
2252 ) {
2253 let target = child_node(area, 1, cx);
2254 let alpha_id = panel_id_of(alpha);
2255 cx.update(|window, cx| {
2256 area.update(cx, |area, cx| {
2257 area.move_panel(
2258 alpha_id,
2259 InsertTarget::Tabs {
2260 node: target,
2261 ix: None,
2262 activate: true,
2263 },
2264 window,
2265 cx,
2266 );
2267 });
2268 });
2269 cx.run_until_parked();
2270 }
2271
2272 fn collect_sizes(state: &PanelState, out: &mut Vec<Pixels>) {
2273 if let PanelInfo::Stack { sizes, .. } = &state.info {
2274 out.extend(sizes.iter().copied());
2275 }
2276 for child in &state.children {
2277 collect_sizes(child, out);
2278 }
2279 }
2280
2281 fn register_test_panels(cx: &mut App) {
2282 for name in ["Alpha", "Beta", "Gamma"] {
2283 crate::dock::registry::register_panel(cx, name, move |_, _, cx| {
2284 Arc::new(TestPanel::new(name, cx)) as Arc<dyn PanelView>
2285 });
2286 }
2287 }
2288
2289 fn one_group<'a>(
2295 log: &Log,
2296 names: &[&'static str],
2297 active_ix: Option<usize>,
2298 cx: &'a mut TestAppContext,
2299 ) -> (
2300 Entity<DockArea>,
2301 Vec<Entity<TestPanel>>,
2302 &'a mut VisualTestContext,
2303 ) {
2304 let (area, cx) = setup(cx);
2305 let log = log.clone();
2306 let names = names.to_vec();
2307 let panels = cx.update(|window, cx| {
2308 let panels: Vec<_> = names
2309 .iter()
2310 .map(|name| TestPanel::logging(name, &log, cx))
2311 .collect();
2312 let layout = panels
2313 .iter()
2314 .fold(DockLayout::tabs(), |layout, panel| {
2315 layout.panel(panel.clone())
2316 })
2317 .active_index(active_ix.unwrap_or(0));
2318 area.update(cx, |area, cx| area.set_center(layout, window, cx));
2319 panels
2320 });
2321 (area, panels, cx)
2322 }
2323
2324 fn group_of(
2326 area: &Entity<DockArea>,
2327 ix: usize,
2328 cx: &mut VisualTestContext,
2329 ) -> Entity<TabGroup> {
2330 let node = child_node(area, ix, cx);
2331 cx.read(|cx| area.read(cx).groups.get(&node).unwrap().entity.clone())
2332 }
2333
2334 fn move_panel_into(
2335 area: &Entity<DockArea>,
2336 panel: PanelId,
2337 node: NodeId,
2338 ix: Option<usize>,
2339 activate: bool,
2340 cx: &mut VisualTestContext,
2341 ) {
2342 cx.update(|window, cx| {
2343 area.update(cx, |area, cx| {
2344 area.move_panel(panel, InsertTarget::Tabs { node, ix, activate }, window, cx);
2345 });
2346 });
2347 cx.run_until_parked();
2348 }
2349
2350 fn is_center_empty(area: &Entity<DockArea>, cx: &mut VisualTestContext) -> bool {
2351 cx.read(|cx| area.read(cx).is_empty(DockPlacement::Center, cx))
2352 }
2353
2354 #[gpui::test]
2355 fn a_layout_installs_and_dumps_back_to_the_same_state(cx: &mut TestAppContext) {
2356 let (area, cx) = setup(cx);
2357 cx.update(|window, cx| {
2358 let alpha = TestPanel::new("Alpha", cx);
2359 let beta = TestPanel::new("Beta", cx);
2360 area.update(cx, |area, cx| {
2361 area.set_center(
2362 DockLayout::h_split()
2363 .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2364 .child(DockLayout::tabs().panel(beta), None),
2365 window,
2366 cx,
2367 );
2368 });
2369 });
2370
2371 let state = cx.read(|cx| area.read(cx).dump(cx));
2372 assert_eq!(state.center.panel_name, "StackPanel");
2373 assert_eq!(state.center.children.len(), 2);
2374 assert_eq!(state.center.children[0].children[0].panel_name, "Alpha");
2375 assert_eq!(state.center.children[1].children[0].panel_name, "Beta");
2376 }
2377
2378 #[gpui::test]
2379 fn moving_a_panel_reuses_its_entity(cx: &mut TestAppContext) {
2380 let log = log_of();
2381 let (area, alpha, cx) = two_groups(&log, cx);
2382 cx.run_until_parked();
2383 drain(&log);
2384
2385 let destination = child_node(&area, 1, cx);
2386 let destination_entity = cx.read(|cx| {
2387 area.read(cx)
2388 .groups
2389 .get(&destination)
2390 .unwrap()
2391 .entity
2392 .entity_id()
2393 });
2394
2395 move_alpha_into_the_other_group(&area, &alpha, cx);
2396
2397 assert_eq!(
2398 cx.read(|cx| area
2399 .read(cx)
2400 .groups
2401 .get(&destination)
2402 .unwrap()
2403 .entity
2404 .entity_id()),
2405 destination_entity,
2406 "the group the panel arrived in was reused, not rebuilt"
2407 );
2408
2409 let alpha_id = panel_id_of(&alpha);
2414 assert!(
2415 cx.read(|cx| area
2416 .read(cx)
2417 .layout(DockPlacement::Center)
2418 .unwrap()
2419 .find_panel_node(alpha_id))
2420 .is_some(),
2421 "the moved panel is still in the tree"
2422 );
2423
2424 let state = cx.read(|cx| area.read(cx).dump(cx));
2425 assert_eq!(
2429 state.center.children.len(),
2430 1,
2431 "the emptied group collapsed out of the split"
2432 );
2433 assert_eq!(
2434 state.center.children[0].children.len(),
2435 2,
2436 "both panels now share the surviving group"
2437 );
2438 }
2439
2440 #[gpui::test]
2441 fn a_moved_panel_is_not_told_it_was_removed(cx: &mut TestAppContext) {
2442 let log = log_of();
2443 let (area, alpha, cx) = two_groups(&log, cx);
2444 cx.run_until_parked();
2445 drain(&log);
2446
2447 move_alpha_into_the_other_group(&area, &alpha, cx);
2448
2449 assert!(
2450 !drain(&log).contains(&("Alpha", PanelSignal::Removed)),
2451 "moving a panel between groups must never deliver on_removed"
2452 );
2453 }
2454
2455 #[gpui::test]
2456 fn removing_a_panel_does_tell_it_it_was_removed(cx: &mut TestAppContext) {
2457 let log = log_of();
2461 let (area, alpha, cx) = two_groups(&log, cx);
2462 cx.run_until_parked();
2463 drain(&log);
2464
2465 cx.update(|window, cx| {
2466 area.update(cx, |area, cx| area.remove_panel(alpha.clone(), window, cx));
2467 });
2468 cx.run_until_parked();
2469
2470 assert!(
2471 drain(&log).contains(&("Alpha", PanelSignal::Removed)),
2472 "a genuine removal must deliver on_removed"
2473 );
2474 }
2475
2476 #[gpui::test]
2477 fn reconciling_an_unchanged_tree_creates_no_entities(cx: &mut TestAppContext) {
2478 let log = log_of();
2479 let (area, _alpha, cx) = two_groups(&log, cx);
2480 cx.run_until_parked();
2481 drain(&log);
2482
2483 let before = cx.read(|cx| area.read(cx).container_entity_ids());
2484 cx.update(|window, cx| area.update(cx, |area, cx| area.reconcile(window, cx)));
2485 let after = cx.read(|cx| area.read(cx).container_entity_ids());
2486
2487 assert!(!before.is_empty(), "there were containers to preserve");
2488 assert_eq!(
2489 before, after,
2490 "a steady-state pass creates and drops nothing"
2491 );
2492 cx.run_until_parked();
2493 assert_eq!(
2494 drain(&log),
2495 vec![],
2496 "and no panel was re-added or re-activated by it"
2497 );
2498 }
2499
2500 #[gpui::test]
2501 fn a_loaded_layout_round_trips_through_dump(cx: &mut TestAppContext) {
2502 let (area, cx) = setup(cx);
2503 cx.update(|_, cx| register_test_panels(cx));
2504
2505 let json = include_str!("fixtures/nested_splits.json");
2506 let state: DockAreaState = serde_json::from_str(json).unwrap();
2507
2508 cx.update(|window, cx| {
2509 area.update(cx, |area, cx| area.load(state.clone(), window, cx).unwrap())
2510 });
2511 let dumped = cx.read(|cx| area.read(cx).dump(cx));
2512
2513 cx.update(|window, cx| {
2514 area.update(cx, |area, cx| {
2515 area.load(dumped.clone(), window, cx).unwrap()
2516 })
2517 });
2518 let again = cx.read(|cx| area.read(cx).dump(cx));
2519
2520 assert_eq!(dumped, again, "load/dump must reach a fixpoint");
2521 assert_eq!(
2522 dumped.center.children.len(),
2523 3,
2524 "the fixture's nesting is flattened, as the state layer already pins"
2525 );
2526 assert_eq!(dumped.center.children[0].children[0].panel_name, "Alpha");
2527 }
2528
2529 #[gpui::test]
2530 fn a_dumped_live_layout_has_no_zero_sizes(cx: &mut TestAppContext) {
2531 let (area, cx) = setup(cx);
2532 cx.update(|window, cx| {
2533 let alpha = TestPanel::new("Alpha", cx);
2534 let beta = TestPanel::new("Beta", cx);
2535 let gamma = TestPanel::new("Gamma", cx);
2536 area.update(cx, |area, cx| {
2537 area.set_center(
2538 DockLayout::v_split()
2539 .child(DockLayout::tabs().panel(alpha), None)
2542 .child(DockLayout::tabs().panel(beta), Some(px(0.)))
2545 .child(DockLayout::tabs().panel(gamma), Some(px(240.))),
2546 window,
2547 cx,
2548 );
2549 });
2550 });
2551
2552 let state = cx.read(|cx| area.read(cx).dump(cx));
2553 let mut sizes = Vec::new();
2554 collect_sizes(&state.center, &mut sizes);
2555
2556 assert!(!sizes.is_empty(), "the layout has slots to check");
2557 assert!(
2558 sizes.iter().all(|size| *size > px(0.)),
2559 "an older build reads a persisted 0.0 back as a real zero-pixel panel: {sizes:?}"
2560 );
2561 }
2562
2563 #[gpui::test]
2570 fn a_dumped_split_writes_the_sizes_it_is_actually_drawn_at(cx: &mut TestAppContext) {
2571 let (area, cx) = setup(cx);
2572 cx.update(|window, cx| {
2573 let alpha = TestPanel::new("Alpha", cx);
2574 let beta = TestPanel::new("Beta", cx);
2575 area.update(cx, |area, cx| {
2576 area.set_center(
2577 DockLayout::h_split()
2578 .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2579 .child(DockLayout::tabs().panel(beta), Some(px(300.))),
2580 window,
2581 cx,
2582 );
2583 });
2584 });
2585 cx.run_until_parked();
2586
2587 let root = cx.read(|cx| {
2588 area.read(cx)
2589 .layout(DockPlacement::Center)
2590 .unwrap()
2591 .root()
2592 .id()
2593 });
2594 let measured = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2595 assert_ne!(
2596 measured,
2597 vec![px(300.), px(300.)],
2598 "the split has to have been rescaled by a layout pass, or this \
2599 test cannot tell the two preferences apart"
2600 );
2601
2602 let state = cx.read(|cx| area.read(cx).dump(cx));
2603 let PanelInfo::Stack { sizes, .. } = &state.center.info else {
2604 panic!("the center writes a stack");
2605 };
2606 assert_eq!(
2607 sizes, &measured,
2608 "the written sizes are the ones on screen, not the ones the tree \
2609 was built from"
2610 );
2611 }
2612
2613 #[gpui::test]
2619 fn a_panel_dropped_beside_another_takes_half_the_split(cx: &mut TestAppContext) {
2620 let log = Log::default();
2621 let (area, panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2622 let group = child_node(&area, 0, cx);
2623 let beta = panel_id_of(&panels[1]);
2624
2625 cx.update(|window, cx| {
2626 area.update(cx, |area, cx| {
2627 area.move_panel(
2628 beta,
2629 InsertTarget::Split {
2630 node: group,
2631 placement: Placement::Right,
2632 size: None,
2633 },
2634 window,
2635 cx,
2636 );
2637 });
2638 });
2639 cx.run_until_parked();
2640
2641 let root = cx.read(|cx| {
2642 area.read(cx)
2643 .layout(DockPlacement::Center)
2644 .unwrap()
2645 .root()
2646 .id()
2647 });
2648 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2649
2650 assert_eq!(sizes.len(), 2, "the drop splits the center in two");
2651 let (left, right) = (sizes[0].as_f32(), sizes[1].as_f32());
2652 assert!(
2653 (left - right).abs() <= (left + right) * 0.02,
2654 "the two halves must be within 2% of each other, got {left} and {right}"
2655 );
2656 }
2657
2658 #[gpui::test]
2662 fn a_panel_dropped_across_the_axis_still_takes_half(cx: &mut TestAppContext) {
2663 let log = Log::default();
2664 let (area, panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2665 let group = child_node(&area, 0, cx);
2666 let beta = panel_id_of(&panels[1]);
2667
2668 cx.update(|window, cx| {
2669 area.update(cx, |area, cx| {
2670 area.move_panel(
2671 beta,
2672 InsertTarget::Split {
2673 node: group,
2674 placement: Placement::Bottom,
2675 size: None,
2676 },
2677 window,
2678 cx,
2679 );
2680 });
2681 });
2682 cx.run_until_parked();
2683
2684 let wrapper = child_node(&area, 0, cx);
2688 let sizes = cx.read(|cx| {
2689 area.read(cx).splits[&wrapper]
2690 .entity
2691 .read(cx)
2692 .sizes()
2693 .clone()
2694 });
2695 assert_eq!(sizes.len(), 2, "the drop splits the group in two");
2696 let (top, bottom) = (sizes[0].as_f32(), sizes[1].as_f32());
2697 assert!(
2698 (top - bottom).abs() <= (top + bottom) * 0.02,
2699 "the two halves must be within 2% of each other, got {top} and {bottom}"
2700 );
2701 }
2702
2703 #[gpui::test]
2706 fn a_panel_dropped_into_a_populated_split_takes_an_even_share(cx: &mut TestAppContext) {
2707 let (area, cx) = setup(cx);
2708 let panels = cx.update(|window, cx| {
2709 let alpha = TestPanel::new("Alpha", cx);
2710 let beta = TestPanel::new("Beta", cx);
2711 let gamma = TestPanel::new("Gamma", cx);
2712 area.update(cx, |area, cx| {
2713 area.set_center(
2714 DockLayout::h_split()
2715 .child(DockLayout::tabs().panel(alpha.clone()), Some(px(240.)))
2716 .child(
2717 DockLayout::tabs().panel(beta.clone()).panel(gamma.clone()),
2718 None,
2719 ),
2720 window,
2721 cx,
2722 );
2723 });
2724 vec![alpha, beta, gamma]
2725 });
2726 cx.run_until_parked();
2727
2728 let right = child_node(&area, 1, cx);
2729 let gamma = panel_id_of(&panels[2]);
2730 cx.update(|window, cx| {
2731 area.update(cx, |area, cx| {
2732 area.move_panel(
2733 gamma,
2734 InsertTarget::Split {
2735 node: right,
2736 placement: Placement::Right,
2737 size: None,
2738 },
2739 window,
2740 cx,
2741 );
2742 });
2743 });
2744 cx.run_until_parked();
2745
2746 let root = cx.read(|cx| {
2747 area.read(cx)
2748 .layout(DockPlacement::Center)
2749 .unwrap()
2750 .root()
2751 .id()
2752 });
2753 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2754 assert_eq!(sizes.len(), 3, "three slots side by side");
2755 let dropped = sizes[2].as_f32();
2756 let neighbour = sizes[1].as_f32();
2757 assert!(
2758 (dropped - neighbour).abs() <= (dropped + neighbour) * 0.02,
2759 "the dropped panel splits its neighbour evenly, got neighbour {neighbour} and dropped {dropped}"
2760 );
2761 }
2762
2763 #[gpui::test]
2767 fn a_panel_dropped_beside_a_dock_takes_half(cx: &mut TestAppContext) {
2768 for placement in [DockPlacement::Bottom, DockPlacement::Left] {
2769 let (area, cx) = setup(cx);
2770 let dropped = cx.update(|window, cx| {
2771 let resident = TestPanel::new("Resident", cx);
2772 let dropped = TestPanel::new("Dropped", cx);
2773 area.update(cx, |area, cx| {
2774 area.set_center(
2775 DockLayout::tabs().panel(TestPanel::new("Center", cx)),
2776 window,
2777 cx,
2778 );
2779 area.set_dock(
2780 placement,
2781 DockLayout::tabs().panel(resident).panel(dropped.clone()),
2782 window,
2783 cx,
2784 );
2785 area.set_dock_size(placement, px(400.), window, cx);
2786 });
2787 dropped
2788 });
2789 cx.run_until_parked();
2790
2791 let group = cx.read(|cx| {
2792 area.read(cx)
2793 .layout(placement)
2794 .unwrap()
2795 .find_panel_node(panel_id_of(&dropped))
2796 .expect("both panels start in the dock's only group")
2797 });
2798
2799 cx.update(|window, cx| {
2800 area.update(cx, |area, cx| {
2801 area.move_panel(
2802 panel_id_of(&dropped),
2803 InsertTarget::Split {
2804 node: group,
2805 placement: Placement::Bottom,
2806 size: None,
2807 },
2808 window,
2809 cx,
2810 );
2811 });
2812 });
2813 cx.run_until_parked();
2814
2815 let split = cx.read(|cx| {
2816 let tree = area.read(cx).layout(placement).unwrap();
2817 let root = tree.root();
2818 match root.kind() {
2819 PaneRef::Split { .. } => root.id(),
2820 _ => panic!("the drop must have produced a split"),
2821 }
2822 });
2823 let sizes = cx.read(|cx| area.read(cx).splits[&split].entity.read(cx).sizes().clone());
2824
2825 assert_eq!(sizes.len(), 2, "{placement:?}: the drop splits in two");
2826 let (first, second) = (sizes[0].as_f32(), sizes[1].as_f32());
2827 assert!(
2828 (first - second).abs() <= (first + second) * 0.02,
2829 "{placement:?}: expected halves, got {first} and {second}"
2830 );
2831 }
2832 }
2833
2834 #[gpui::test]
2841 fn an_explicit_slot_size_survives_the_first_layout_pass(cx: &mut TestAppContext) {
2842 let (area, cx) = setup(cx);
2843 cx.update(|window, cx| {
2844 let sidebar = TestPanel::new("Sidebar", cx);
2845 let content = TestPanel::new("Content", cx);
2846 area.update(cx, |area, cx| {
2847 area.set_center(
2848 DockLayout::h_split()
2849 .child(DockLayout::tabs().panel(sidebar), Some(px(200.)))
2850 .child(DockLayout::tabs().panel(content), None),
2851 window,
2852 cx,
2853 );
2854 });
2855 });
2856 cx.run_until_parked();
2857
2858 let root = cx.read(|cx| {
2859 area.read(cx)
2860 .layout(DockPlacement::Center)
2861 .unwrap()
2862 .root()
2863 .id()
2864 });
2865 let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2866
2867 let fixed = sizes
2871 .first()
2872 .copied()
2873 .expect("the split has slots")
2874 .as_f32();
2875 assert!(
2876 (fixed - 200.).abs() <= 4.,
2877 "the fixed slot keeps its 200px instead of being rescaled by the \
2878 flexible sibling's placeholder, got {fixed}"
2879 );
2880 }
2881
2882 struct MeasuredPanel {
2885 name: &'static str,
2886 focus_handle: FocusHandle,
2887 }
2888
2889 impl MeasuredPanel {
2890 fn new(name: &'static str, cx: &mut App) -> Entity<Self> {
2891 cx.new(|cx| Self {
2892 name,
2893 focus_handle: cx.focus_handle(),
2894 })
2895 }
2896 }
2897
2898 impl Panel for MeasuredPanel {
2899 fn panel_name(&self) -> &'static str {
2900 self.name
2901 }
2902 }
2903
2904 impl EventEmitter<PanelEvent> for MeasuredPanel {}
2905
2906 impl Focusable for MeasuredPanel {
2907 fn focus_handle(&self, _: &App) -> FocusHandle {
2908 self.focus_handle.clone()
2909 }
2910 }
2911
2912 impl Render for MeasuredPanel {
2913 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2914 let name = self.name;
2915 div().size_full().debug_selector(move || name.into())
2916 }
2917 }
2918
2919 fn draw_frames(cx: &mut VisualTestContext, frames: usize) {
2920 for _ in 0..frames {
2921 cx.update(|window, cx| window.draw(cx).clear(cx));
2922 }
2923 }
2924
2925 #[gpui::test]
2932 fn switching_a_tab_leaves_an_untouched_split_where_it_was_drawn(cx: &mut TestAppContext) {
2933 let (area, cx) = setup(cx);
2934 cx.update(|window, cx| {
2935 area.update(cx, |area, cx| {
2936 area.set_center(
2937 DockLayout::tabs().panel(TestPanel::new("Center", cx)),
2938 window,
2939 cx,
2940 );
2941 area.set_dock(
2942 DockPlacement::Left,
2943 DockLayout::v_split()
2944 .child(
2945 DockLayout::tabs().panel(MeasuredPanel::new("upper-left", cx)),
2946 None,
2947 )
2948 .child(
2949 DockLayout::tabs().panel(MeasuredPanel::new("lower-left", cx)),
2950 Some(px(360.)),
2951 ),
2952 window,
2953 cx,
2954 );
2955 area.set_dock_size(DockPlacement::Left, px(350.), window, cx);
2956 area.set_dock(
2957 DockPlacement::Bottom,
2958 DockLayout::tabs()
2959 .panel(TestPanel::new("Tooltip", cx))
2960 .panel(TestPanel::new("Icon", cx)),
2961 window,
2962 cx,
2963 );
2964 area.set_dock_size(DockPlacement::Bottom, px(200.), window, cx);
2965 });
2966 });
2967 cx.run_until_parked();
2968 draw_frames(cx, 3);
2969 let before = (
2970 cx.debug_bounds("upper-left").unwrap(),
2971 cx.debug_bounds("lower-left").unwrap(),
2972 );
2973
2974 let bottom = cx.read(|cx| {
2975 area.read(cx)
2976 .layout(DockPlacement::Bottom)
2977 .unwrap()
2978 .root()
2979 .id()
2980 });
2981 let group = cx.read(|cx| area.read(cx).groups[&bottom].entity.clone());
2982 cx.update(|window, cx| {
2983 group.update(cx, |group, cx| group.select_tab(1, window, cx));
2984 });
2985 cx.run_until_parked();
2986 draw_frames(cx, 3);
2987 let after = (
2988 cx.debug_bounds("upper-left").unwrap(),
2989 cx.debug_bounds("lower-left").unwrap(),
2990 );
2991
2992 assert_eq!(
2993 before, after,
2994 "a tab change in the bottom dock must not move the left split"
2995 );
2996 }
2997
2998 #[gpui::test]
3003 fn switching_a_tab_keeps_a_restored_split_at_its_rescaled_share(cx: &mut TestAppContext) {
3004 let (area, cx) = setup(cx);
3005 cx.update(|window, cx| {
3006 area.update(cx, |area, cx| {
3007 area.set_center(
3008 DockLayout::h_split()
3009 .child(
3010 DockLayout::tabs()
3011 .panel(TestPanel::new("Alpha", cx))
3012 .panel(TestPanel::new("Beta", cx)),
3013 Some(px(620.)),
3014 )
3015 .child(
3016 DockLayout::tabs().panel(MeasuredPanel::new("second", cx)),
3017 Some(px(350.)),
3018 ),
3019 window,
3020 cx,
3021 );
3022 });
3023 });
3024 cx.run_until_parked();
3025 draw_frames(cx, 3);
3026 let before = cx.debug_bounds("second").unwrap();
3029 assert_ne!(
3030 before.right(),
3031 px(970.),
3032 "the window must not match the recorded total, or this test cannot \
3033 tell a rescaled split from the file's pixels"
3034 );
3035
3036 let group = group_of(&area, 0, cx);
3037 cx.update(|window, cx| {
3038 group.update(cx, |group, cx| group.select_tab(1, window, cx));
3039 });
3040 cx.run_until_parked();
3041 draw_frames(cx, 3);
3042 let after = cx.debug_bounds("second").unwrap();
3043
3044 assert_eq!(
3045 before, after,
3046 "a tab change must not hand the file's pixels back to the split"
3047 );
3048 }
3049
3050 #[gpui::test]
3061 fn the_shipped_fixture_survives_a_load_dump_load_round_trip(cx: &mut TestAppContext) {
3062 let (area, cx) = setup(cx);
3063 let fixture: DockAreaState =
3064 serde_json::from_str(include_str!("fixtures/layout.json")).unwrap();
3065
3066 cx.update(|window, cx| area.update(cx, |area, cx| area.load(fixture, window, cx).unwrap()));
3067 cx.run_until_parked();
3068 let first = cx.read(|cx| area.read(cx).dump(cx));
3069
3070 assert_eq!(first.center.children.len(), 2, "the center's two groups");
3071 assert_eq!(first.center.children[0].children.len(), 15);
3072 assert_eq!(first.center.children[1].children.len(), 1);
3073 for dock in [&first.left_dock, &first.bottom_dock, &first.right_dock] {
3074 let dock = dock.as_ref().expect("all three docks are attached");
3075 assert!(dock.open());
3076 assert!(
3077 !dock.panel().children.is_empty(),
3078 "a dock that loaded empty would round-trip just as stably"
3079 );
3080 }
3081 assert_eq!(first.left_dock.as_ref().unwrap().size(), px(350.));
3082 assert_eq!(first.bottom_dock.as_ref().unwrap().size(), px(200.));
3083 assert_eq!(first.right_dock.as_ref().unwrap().size(), px(320.));
3084
3085 cx.update(|window, cx| {
3086 area.update(cx, |area, cx| area.load(first.clone(), window, cx).unwrap())
3087 });
3088 cx.run_until_parked();
3089 let second = cx.read(|cx| area.read(cx).dump(cx));
3090
3091 assert_eq!(second, first, "dump == dump(load(dump))");
3092 }
3093
3094 #[gpui::test]
3095 fn an_unregistered_panel_survives_a_load_and_save_round_trip(cx: &mut TestAppContext) {
3096 let (area, cx) = setup(cx);
3097 cx.update(|_, cx| register_test_panels(cx));
3098
3099 let json = include_str!("fixtures/unregistered_panel.json");
3100 let state: DockAreaState = serde_json::from_str(json).unwrap();
3101
3102 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
3103 let dumped = cx.read(|cx| area.read(cx).dump(cx));
3104
3105 let leaf = &dumped.center.children[0].children[0];
3106 assert_eq!(leaf.panel_name, "PanelFromTheFuture");
3107 assert_eq!(
3108 leaf.info,
3109 PanelInfo::panel(serde_json::json!({"keep": "me"})),
3110 "a panel this build cannot construct keeps its payload"
3111 );
3112 }
3113
3114 #[gpui::test]
3115 fn a_dock_carries_its_own_tree_and_survives_a_round_trip(cx: &mut TestAppContext) {
3116 let (area, cx) = setup(cx);
3117 cx.update(|window, cx| {
3118 let alpha = TestPanel::new("Alpha", cx);
3119 let beta = TestPanel::new("Beta", cx);
3120 area.update(cx, |area, cx| {
3121 area.set_center(DockLayout::tabs().panel(alpha), window, cx);
3122 area.set_dock(
3123 DockPlacement::Left,
3124 DockLayout::tabs().panel(beta),
3125 window,
3126 cx,
3127 );
3128 });
3129 });
3130
3131 let center_ids = cx.read(|cx| {
3140 area.read(cx)
3141 .layout(DockPlacement::Center)
3142 .unwrap()
3143 .node_ids()
3144 });
3145 let dock_ids = cx.read(|cx| {
3146 area.read(cx)
3147 .layout(DockPlacement::Left)
3148 .unwrap()
3149 .node_ids()
3150 });
3151 assert!(!center_ids.is_empty() && !dock_ids.is_empty());
3152 assert!(
3153 center_ids.iter().all(|id| !dock_ids.contains(id)),
3154 "every tree in one area must draw from one id space: \
3155 center {center_ids:?} vs left {dock_ids:?}"
3156 );
3157
3158 let state = cx.read(|cx| area.read(cx).dump(cx));
3159 let left = state.left_dock.clone().expect("the left dock is written");
3160 assert_eq!(left.placement(), DockPlacement::Left);
3161 assert!(left.open());
3162 assert_eq!(left.panel().children[0].panel_name, "Beta");
3163 assert!(state.right_dock.is_none());
3164 }
3165
3166 #[gpui::test]
3167 fn a_panel_moved_between_regions_keeps_its_active_state(cx: &mut TestAppContext) {
3168 let log = log_of();
3169 let (area, alpha, cx) = two_groups(&log, cx);
3170 cx.run_until_parked();
3171 assert!(drain(&log).contains(&("Alpha", PanelSignal::Active(true))));
3174
3175 move_alpha_into_the_other_group(&area, &alpha, cx);
3176
3177 assert!(
3178 !drain(&log).contains(&("Alpha", PanelSignal::Active(true))),
3179 "a displayed panel dragged to another group must not be told `true` twice"
3180 );
3181 }
3182
3183 #[gpui::test]
3184 fn a_groups_close_intent_reaches_the_tree(cx: &mut TestAppContext) {
3185 let log = log_of();
3189 let (area, alpha, cx) = two_groups(&log, cx);
3190 cx.run_until_parked();
3191 drain(&log);
3192
3193 let node = child_node(&area, 0, cx);
3194 let alpha_id = panel_id_of(&alpha);
3195 cx.update(|_, cx| {
3196 let group = area.read(cx).groups.get(&node).unwrap().entity.clone();
3197 group.update(cx, |group, cx| group.close_panel(alpha_id, cx));
3198 });
3199 cx.run_until_parked();
3200
3201 assert!(
3202 cx.read(|cx| area
3203 .read(cx)
3204 .layout(DockPlacement::Center)
3205 .unwrap()
3206 .find_panel_node(alpha_id))
3207 .is_none(),
3208 "the close intent was applied to the tree"
3209 );
3210 assert!(drain(&log).contains(&("Alpha", PanelSignal::Removed)));
3211 }
3212
3213 #[gpui::test]
3214 fn replacing_the_center_tells_the_panels_that_left(cx: &mut TestAppContext) {
3215 let log = log_of();
3216 let (area, _alpha, cx) = two_groups(&log, cx);
3217 cx.run_until_parked();
3218 drain(&log);
3219
3220 cx.update(|window, cx| {
3221 let gamma = TestPanel::new("Gamma", cx);
3222 area.update(cx, |area, cx| {
3223 area.set_center(DockLayout::tabs().panel(gamma), window, cx)
3224 });
3225 });
3226 cx.run_until_parked();
3227
3228 let seen = drain(&log);
3229 assert!(seen.contains(&("Alpha", PanelSignal::Removed)));
3230 assert!(seen.contains(&("Beta", PanelSignal::Removed)));
3231 }
3232
3233 #[gpui::test]
3234 fn closing_a_zoomed_panel_clears_the_zoom(cx: &mut TestAppContext) {
3235 let log = log_of();
3236 let (area, alpha, cx) = two_groups(&log, cx);
3237 cx.run_until_parked();
3238
3239 let node = child_node(&area, 0, cx);
3240 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_in(node, window, cx)));
3241 assert!(cx.read(|cx| area.read(cx).is_zoomed()));
3242
3243 cx.update(|window, cx| {
3244 area.update(cx, |area, cx| area.remove_panel(alpha.clone(), window, cx))
3245 });
3246
3247 assert!(
3248 !cx.read(|cx| area.read(cx).is_zoomed()),
3249 "a zoomed panel that left the dock must not keep filling it"
3250 );
3251 }
3252
3253 #[gpui::test]
3258 fn adding_a_panel_to_a_tiles_region_lands_on_the_canvas(cx: &mut TestAppContext) {
3259 let (area, cx) = setup(cx);
3260 let bounds = Bounds {
3261 origin: gpui::point(px(40.), px(40.)),
3262 size: gpui::size(px(200.), px(150.)),
3263 };
3264 let beta = cx.update(|window, cx| {
3265 let alpha = TestPanel::new("Alpha", cx);
3266 let beta = TestPanel::new("Beta", cx);
3267 area.update(cx, |area, cx| {
3268 area.set_center(DockLayout::tiles().tile(alpha, bounds), window, cx);
3269 area.add_panel(beta.clone(), DockPlacement::Center, None, window, cx);
3270 });
3271 beta
3272 });
3273 cx.run_until_parked();
3274
3275 let canvas_node = child_node(&area, 0, cx);
3276 let panels = cx.read(|cx| {
3277 let PaneRef::Tiles { panels } = area
3278 .read(cx)
3279 .layout(DockPlacement::Center)
3280 .unwrap()
3281 .find_node(canvas_node)
3282 .expect("the canvas is still there, not split in two")
3283 .kind()
3284 else {
3285 panic!("the region is still a tiles canvas");
3286 };
3287 panels.to_vec()
3288 });
3289 assert_eq!(panels.len(), 2, "the panel joined the canvas as a tile");
3290
3291 let beta_id = panel_id_of(&beta);
3294 assert!(
3295 cx.read(|cx| area.read(cx).panel(beta_id).is_some()),
3296 "the added panel's view is registered"
3297 );
3298 let state = cx.read(|cx| area.read(cx).dump(cx));
3299 let names: Vec<&str> = state
3300 .center
3301 .children
3302 .iter()
3303 .map(|child| child.panel_name.as_str())
3304 .collect();
3305 assert_eq!(names, vec!["Alpha", "Beta"]);
3306 }
3307
3308 #[gpui::test]
3312 fn add_tile_places_the_panel_where_it_was_asked_to(cx: &mut TestAppContext) {
3313 let (area, cx) = setup(cx);
3314 let first = Bounds {
3315 origin: gpui::point(px(10.), px(10.)),
3316 size: gpui::size(px(100.), px(100.)),
3317 };
3318 let dropped = Bounds {
3319 origin: gpui::point(px(320.), px(180.)),
3320 size: gpui::size(px(240.), px(160.)),
3321 };
3322 let beta = cx.update(|window, cx| {
3323 let alpha = TestPanel::new("Alpha", cx);
3324 let beta = TestPanel::new("Beta", cx);
3325 area.update(cx, |area, cx| {
3326 area.set_center(DockLayout::tiles().tile(alpha, first), window, cx);
3327 area.add_tile(beta.clone(), DockPlacement::Center, dropped, window, cx);
3328 });
3329 beta
3330 });
3331 cx.run_until_parked();
3332
3333 let beta_id = panel_id_of(&beta);
3334 let canvas_node = child_node(&area, 0, cx);
3335 let tile = cx.read(|cx| {
3336 let PaneRef::Tiles { panels } = area
3337 .read(cx)
3338 .layout(DockPlacement::Center)
3339 .unwrap()
3340 .find_node(canvas_node)
3341 .unwrap()
3342 .kind()
3343 else {
3344 panic!("the region is still a tiles canvas");
3345 };
3346 *panels.iter().find(|tile| tile.panel() == beta_id).unwrap()
3347 });
3348 assert_eq!(tile.bounds(), dropped);
3349 assert!(
3350 cx.read(|cx| area.read(cx).panel(beta_id).is_some()),
3351 "the added panel's view is registered"
3352 );
3353 }
3354
3355 #[gpui::test]
3359 fn add_tile_does_nothing_to_a_region_with_no_canvas(cx: &mut TestAppContext) {
3360 let log = log_of();
3361 let (area, _, cx) = one_group(&log, &["Alpha"], None, cx);
3362 let bounds = Bounds {
3363 origin: gpui::point(px(10.), px(10.)),
3364 size: gpui::size(px(100.), px(100.)),
3365 };
3366 let beta = cx.update(|window, cx| {
3367 let beta = TestPanel::new("Beta", cx);
3368 area.update(cx, |area, cx| {
3369 area.add_tile(beta.clone(), DockPlacement::Center, bounds, window, cx);
3370 });
3371 beta
3372 });
3373 cx.run_until_parked();
3374
3375 let beta_id = panel_id_of(&beta);
3376 assert!(
3377 cx.read(|cx| area.read(cx).panel(beta_id).is_none()),
3378 "a panel nothing took must not linger in the view map"
3379 );
3380 let state = cx.read(|cx| area.read(cx).dump(cx));
3381 assert_eq!(state.center.children[0].children.len(), 1);
3382
3383 cx.update(|window, cx| {
3386 let gamma = TestPanel::new("Gamma", cx);
3387 area.update(cx, |area, cx| {
3388 area.add_tile(gamma, DockPlacement::Left, bounds, window, cx);
3389 });
3390 });
3391 cx.run_until_parked();
3392 assert!(
3393 cx.read(|cx| area.read(cx).layout(DockPlacement::Left).is_none()),
3394 "a tile with nowhere to go must not leave a dock behind"
3395 );
3396 }
3397
3398 #[gpui::test]
3405 fn a_declined_add_leaves_an_already_docked_panel_untouched(cx: &mut TestAppContext) {
3406 let log = log_of();
3407 let (area, panels, cx) = one_group(&log, &["Alpha"], None, cx);
3408 let alpha = panels[0].clone();
3409 let alpha_id = panel_id_of(&alpha);
3410 let bounds = Bounds {
3411 origin: gpui::point(px(10.), px(10.)),
3412 size: gpui::size(px(100.), px(100.)),
3413 };
3414
3415 let registered = cx.read(|cx| {
3416 Arc::as_ptr(
3417 area.read(cx)
3418 .panel(alpha_id)
3419 .expect("one_group registers it"),
3420 ) as *const ()
3421 });
3422
3423 cx.update(|window, cx| {
3425 area.update(cx, |area, cx| {
3426 area.add_tile(alpha.clone(), DockPlacement::Center, bounds, window, cx);
3427 });
3428 });
3429 cx.run_until_parked();
3430
3431 let handle = |cx: &mut VisualTestContext| {
3432 cx.read(|cx| {
3433 Arc::as_ptr(area.read(cx).panel(alpha_id).expect("still registered")) as *const ()
3434 })
3435 };
3436 assert_eq!(
3437 handle(cx),
3438 registered,
3439 "a panel that was already docked keeps the very handle it was \
3440 registered with; `add_tile` takes a bare entity, so overwriting \
3441 would cost a panel installed through `add_panel_view` its title"
3442 );
3443 assert!(
3444 cx.read(|cx| area
3445 .read(cx)
3446 .layout(DockPlacement::Center)
3447 .unwrap()
3448 .find_panel_node(alpha_id))
3449 .is_some(),
3450 "and keeps its place in the tree"
3451 );
3452 assert!(
3453 !drain(&log).contains(&("Alpha", PanelSignal::Removed)),
3454 "a declined add is not a removal"
3455 );
3456
3457 let state = cx.read(|cx| area.read(cx).dump(cx));
3460 assert_eq!(state.center.children[0].children[0].panel_name, "Alpha");
3461 }
3462
3463 #[gpui::test]
3464 fn dragging_a_tile_writes_its_new_bounds_back_into_the_tree(cx: &mut TestAppContext) {
3465 let (area, cx) = setup(cx);
3466 let bounds = Bounds {
3467 origin: gpui::point(px(40.), px(40.)),
3468 size: gpui::size(px(200.), px(150.)),
3469 };
3470 let alpha = cx.update(|window, cx| {
3471 let alpha = TestPanel::new("Alpha", cx);
3472 area.update(cx, |area, cx| {
3473 area.set_center(DockLayout::tiles().tile(alpha.clone(), bounds), window, cx);
3474 });
3475 alpha
3476 });
3477
3478 let node = cx.read(|cx| {
3479 area.read(cx)
3480 .layout(DockPlacement::Center)
3481 .unwrap()
3482 .root()
3483 .id()
3484 });
3485 let canvas_node = child_node(&area, 0, cx);
3488 assert_ne!(node, canvas_node);
3489 let canvas = cx.read(|cx| {
3490 area.read(cx)
3491 .tiles
3492 .get(&canvas_node)
3493 .unwrap()
3494 .entity
3495 .clone()
3496 });
3497
3498 cx.update(|window, cx| {
3501 let tile = canvas.read(cx).tiles(cx)[0].clone();
3502 tile.begin_move(gpui::point(px(100.), px(100.)), window, cx);
3503 tile.move_to(gpui::point(px(150.), px(100.)), window, cx);
3504 tile.end_move(window, cx);
3505 });
3506
3507 let node = cx.read(|cx| {
3508 area.read(cx)
3509 .layout(DockPlacement::Center)
3510 .unwrap()
3511 .find_node(canvas_node)
3512 .unwrap()
3513 .clone()
3514 });
3515 let PaneRef::Tiles { panels } = node.kind() else {
3516 panic!("expected a tiles node");
3517 };
3518 assert_eq!(panels[0].panel(), panel_id_of(&alpha));
3519 assert_eq!(
3520 panels[0].bounds().origin.x,
3521 px(90.),
3522 "the canvas reports the move and the tree records it"
3523 );
3524 }
3525
3526 struct SkinPlaceholder {
3530 state: PanelState,
3531 focus_handle: FocusHandle,
3532 }
3533
3534 impl Panel for SkinPlaceholder {
3535 fn panel_name(&self) -> &'static str {
3536 "SkinPlaceholder"
3537 }
3538
3539 fn dump(&self, _: &App) -> PanelState {
3540 self.state.clone()
3541 }
3542 }
3543
3544 impl EventEmitter<PanelEvent> for SkinPlaceholder {}
3545
3546 impl Focusable for SkinPlaceholder {
3547 fn focus_handle(&self, _: &App) -> FocusHandle {
3548 self.focus_handle.clone()
3549 }
3550 }
3551
3552 impl Render for SkinPlaceholder {
3553 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3554 Empty
3555 }
3556 }
3557
3558 struct PlaceholderSkin {
3559 asked: Rc<std::cell::RefCell<Vec<String>>>,
3560 }
3561
3562 impl DockAreaRenderer for PlaceholderSkin {
3563 fn build_placeholder(
3564 &self,
3565 state: &PanelState,
3566 _: &mut Window,
3567 cx: &mut App,
3568 ) -> Option<Arc<dyn PanelView>> {
3569 self.asked.borrow_mut().push(state.panel_name.clone());
3570 let state = state.clone();
3571 Some(Arc::new(cx.new(|cx| SkinPlaceholder {
3572 state,
3573 focus_handle: cx.focus_handle(),
3574 })))
3575 }
3576
3577 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
3578 Rc::new(BareTabGroup)
3579 }
3580
3581 fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> {
3582 Rc::new(BareTiles)
3583 }
3584 }
3585
3586 #[gpui::test]
3590 fn an_unbuildable_panel_becomes_the_skins_placeholder(cx: &mut TestAppContext) {
3591 cx.update(|cx| {
3592 let _ = crate::Theme::global_mut(cx);
3593 });
3594 let asked: Rc<std::cell::RefCell<Vec<String>>> = Rc::default();
3595 let skin = Rc::new(PlaceholderSkin {
3596 asked: asked.clone(),
3597 });
3598 let (area, cx) = cx.add_window_view(|window, cx| {
3599 DockArea::new("test-dock", None, window, cx).with_renderer(skin)
3600 });
3601
3602 cx.update(|window, cx| {
3604 let ghost = TestPanel::new("Ghost", cx);
3605 area.update(cx, |area, cx| {
3606 area.set_center(DockLayout::tabs().panel(ghost), window, cx)
3607 });
3608 });
3609 let state = cx.read(|cx| area.read(cx).dump(cx));
3610 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
3611 cx.run_until_parked();
3612
3613 assert_eq!(*asked.borrow(), vec!["Ghost".to_string()]);
3614 assert_eq!(
3615 cx.read(|cx| area
3616 .read(cx)
3617 .panels
3618 .values()
3619 .map(|panel| panel.panel_name(cx))
3620 .collect::<Vec<_>>()),
3621 vec!["SkinPlaceholder"],
3622 "base installed the skin's placeholder, not its own"
3623 );
3624 assert_eq!(
3625 cx.read(|cx| area.read(cx).dump(cx)).center.children[0].children[0].panel_name,
3626 "Ghost",
3627 "and the unknown panel still survives the next save"
3628 );
3629 }
3630
3631 #[gpui::test]
3632 fn a_persisted_tiles_canvas_restores_its_panels(cx: &mut TestAppContext) {
3633 let (area, cx) = setup(cx);
3638 cx.update(|_, cx| register_test_panels(cx));
3639
3640 let json = include_str!("fixtures/tiles_tab_panel_children.json");
3641 let state: DockAreaState = serde_json::from_str(json).unwrap();
3642 cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
3643
3644 let dumped = cx.read(|cx| area.read(cx).dump(cx));
3645 let tiles = &dumped.center;
3646 assert_eq!(tiles.panel_name, "Tiles");
3647 assert_eq!(
3648 tiles
3649 .children
3650 .iter()
3651 .map(|child| child.panel_name.as_str())
3652 .collect::<Vec<_>>(),
3653 vec!["Alpha", "Beta", "Gamma"],
3654 "the real panels are restored, not `InvalidPanel` placeholders"
3655 );
3656
3657 let canvas_node = child_node(&area, 0, cx);
3659 let canvas = cx.read(|cx| {
3660 area.read(cx)
3661 .tiles
3662 .get(&canvas_node)
3663 .unwrap()
3664 .entity
3665 .clone()
3666 });
3667 let names = cx.read(|cx| {
3668 canvas
3669 .read(cx)
3670 .tiles(cx)
3671 .iter()
3672 .map(|tile| tile.panel().panel_name(cx))
3673 .collect::<Vec<_>>()
3674 });
3675 assert_eq!(names, vec!["Alpha", "Beta", "Gamma"]);
3676 }
3677
3678 #[gpui::test]
3679 fn removing_a_non_tail_child_shifts_the_split_sizes_with_it(cx: &mut TestAppContext) {
3680 let log = log_of();
3684 let (area, cx) = setup(cx);
3685 let alpha = cx.update(|window, cx| {
3686 let alpha = TestPanel::logging("Alpha", &log, cx);
3687 let beta = TestPanel::logging("Beta", &log, cx);
3688 let gamma = TestPanel::logging("Gamma", &log, cx);
3689 area.update(cx, |area, cx| {
3690 area.set_center(
3691 DockLayout::h_split()
3692 .child(DockLayout::tabs().panel(alpha.clone()), Some(px(100.)))
3693 .child(DockLayout::tabs().panel(beta), Some(px(200.)))
3694 .child(DockLayout::tabs().panel(gamma), Some(px(300.))),
3695 window,
3696 cx,
3697 );
3698 });
3699 alpha
3700 });
3701
3702 let root = cx.read(|cx| {
3703 area.read(cx)
3704 .layout(DockPlacement::Center)
3705 .unwrap()
3706 .root()
3707 .id()
3708 });
3709 let split = cx.read(|cx| area.read(cx).splits.get(&root).unwrap().entity.clone());
3710 let sizes = |cx: &mut VisualTestContext| cx.read(|cx| split.read(cx).sizes().clone());
3711
3712 let before = sizes(cx);
3716 assert_eq!(before.len(), 3);
3717 let kept_if_correct = before[1] / before[2];
3718 let kept_if_truncated = before[0] / before[1];
3719 assert!(
3720 (kept_if_correct - kept_if_truncated).abs() > 0.1,
3721 "the fixture must be able to tell the two outcomes apart"
3722 );
3723
3724 cx.update(|window, cx| area.update(cx, |area, cx| area.remove_panel(alpha, window, cx)));
3725
3726 let after = sizes(cx);
3727 assert_eq!(after.len(), 2);
3728 assert!(
3729 (after[0] / after[1] - kept_if_correct).abs() < 0.01,
3730 "the survivors kept their own proportions: slot 0 was removed, not \
3731 the tail — got {after:?} from {before:?}"
3732 );
3733 }
3734
3735 #[gpui::test]
3736 fn a_panel_moves_between_the_center_and_a_dock(cx: &mut TestAppContext) {
3737 let log = log_of();
3738 let (area, alpha, cx) = two_groups(&log, cx);
3739 cx.update(|window, cx| {
3740 let gamma = TestPanel::logging("Gamma", &log, cx);
3741 area.update(cx, |area, cx| {
3742 area.set_dock(
3743 DockPlacement::Left,
3744 DockLayout::tabs().panel(gamma),
3745 window,
3746 cx,
3747 );
3748 });
3749 });
3750 cx.run_until_parked();
3751 drain(&log);
3752
3753 let alpha_id = panel_id_of(&alpha);
3754 let dock_group = cx.read(|cx| {
3755 area.read(cx)
3756 .layout(DockPlacement::Left)
3757 .unwrap()
3758 .root()
3759 .id()
3760 });
3761
3762 cx.update(|window, cx| {
3763 area.update(cx, |area, cx| {
3764 area.move_panel(
3765 alpha_id,
3766 InsertTarget::Tabs {
3767 node: dock_group,
3768 ix: None,
3769 activate: true,
3770 },
3771 window,
3772 cx,
3773 );
3774 });
3775 });
3776 cx.run_until_parked();
3777
3778 assert!(
3779 cx.read(|cx| area
3780 .read(cx)
3781 .layout(DockPlacement::Center)
3782 .unwrap()
3783 .find_panel_node(alpha_id))
3784 .is_none(),
3785 "the panel left the center"
3786 );
3787 assert_eq!(
3788 cx.read(|cx| area
3789 .read(cx)
3790 .layout(DockPlacement::Left)
3791 .unwrap()
3792 .find_panel_node(alpha_id)),
3793 Some(dock_group),
3794 "and arrived in the dock's group"
3795 );
3796
3797 let seen = drain(&log);
3798 assert!(
3799 !seen.contains(&("Alpha", PanelSignal::Removed)),
3800 "crossing regions is still a move, not a removal"
3801 );
3802 assert!(
3803 !seen.contains(&("Alpha", PanelSignal::Active(true))),
3804 "and it was displayed in both, so it is not told `true` twice"
3805 );
3806 }
3807
3808 #[gpui::test]
3809 fn a_move_onto_an_unusable_target_leaves_no_stranded_panel(cx: &mut TestAppContext) {
3810 let log = log_of();
3815 let (area, _alpha, cx) = two_groups(&log, cx);
3816 let gamma = cx.update(|window, cx| {
3817 let gamma = TestPanel::logging("Gamma", &log, cx);
3818 area.update(cx, |area, cx| {
3819 area.set_dock(
3820 DockPlacement::Left,
3821 DockLayout::tabs().panel(gamma.clone()),
3822 window,
3823 cx,
3824 );
3825 });
3826 gamma
3827 });
3828 cx.run_until_parked();
3829 drain(&log);
3830
3831 let gamma_id = panel_id_of(&gamma);
3832 let center_root = cx.read(|cx| {
3835 area.read(cx)
3836 .layout(DockPlacement::Center)
3837 .unwrap()
3838 .root()
3839 .id()
3840 });
3841
3842 cx.update(|window, cx| {
3843 area.update(cx, |area, cx| {
3844 area.move_panel(
3845 gamma_id,
3846 InsertTarget::Tabs {
3847 node: center_root,
3848 ix: None,
3849 activate: true,
3850 },
3851 window,
3852 cx,
3853 );
3854 });
3855 });
3856 cx.run_until_parked();
3857
3858 assert!(
3859 cx.read(|cx| area.read(cx).panel(gamma_id).is_none()),
3860 "the view map agrees with the trees straight away, rather than \
3861 carrying a panel that belongs to no tree"
3862 );
3863 assert!(
3864 drain(&log).contains(&("Gamma", PanelSignal::Removed)),
3865 "and the panel was told so at the point of the call"
3866 );
3867 }
3868
3869 #[gpui::test]
3870 fn an_all_hidden_container_reports_itself_invisible(cx: &mut TestAppContext) {
3871 let (area, cx) = setup(cx);
3874 let beta = cx.update(|window, cx| {
3875 let alpha = TestPanel::new("Alpha", cx);
3876 let beta = TestPanel::new("Beta", cx);
3877 area.update(cx, |area, cx| {
3878 area.set_center(
3879 DockLayout::h_split()
3880 .child(DockLayout::tabs().panel(alpha), None)
3881 .child(DockLayout::tabs().panel(beta.clone()), None),
3882 window,
3883 cx,
3884 );
3885 });
3886 beta
3887 });
3888
3889 let visible = |ix: usize, cx: &mut VisualTestContext| {
3890 let node = child_node(&area, ix, cx);
3891 cx.read(|cx| {
3892 let area = area.read(cx);
3893 let tree = area.layout(DockPlacement::Center).unwrap();
3894 area.is_node_visible(tree.find_node(node).unwrap(), cx)
3895 })
3896 };
3897
3898 assert!(visible(0, cx) && visible(1, cx));
3899
3900 cx.update(|_, cx| beta.update(cx, |beta, cx| beta.set_visible(false, cx)));
3901
3902 assert!(visible(0, cx), "the visible group still holds its slot");
3903 assert!(
3904 !visible(1, cx),
3905 "a group whose every panel is hidden must give its slot up"
3906 );
3907 }
3908
3909 #[gpui::test]
3910 fn a_locked_area_seals_its_groups(cx: &mut TestAppContext) {
3911 let log = log_of();
3912 let (area, _alpha, cx) = two_groups(&log, cx);
3913 cx.run_until_parked();
3914
3915 let node = child_node(&area, 0, cx);
3916 let group = cx.read(|cx| area.read(cx).groups.get(&node).unwrap().entity.clone());
3917 assert!(
3918 cx.read(|cx| group.read(cx).is_closable(cx)),
3919 "an unlocked group's panel can be closed"
3920 );
3921
3922 cx.update(|window, cx| area.update(cx, |area, cx| area.set_locked(true, window, cx)));
3923
3924 assert!(
3925 !cx.read(|cx| group.read(cx).is_closable(cx)),
3926 "the lock reaches every group through the constraints push"
3927 );
3928 }
3929
3930 #[gpui::test]
3938 fn empty_center_round_trips_as_a_stack(cx: &mut TestAppContext) {
3939 let (area, cx) = setup(cx);
3940 let center = cx.read(|cx| area.read(cx).dump(cx).center);
3941
3942 assert_eq!(center.panel_name, "StackPanel");
3943 assert!(
3944 matches!(center.info, PanelInfo::Stack { .. }),
3945 "got {:?}",
3946 center.info
3947 );
3948 }
3949
3950 #[gpui::test]
3951 fn fresh_center_is_empty(cx: &mut TestAppContext) {
3952 let (area, cx) = setup(cx);
3953
3954 assert!(
3955 is_center_empty(&area, cx),
3956 "DockArea::new starts with an empty split centre"
3957 );
3958 }
3959
3960 #[gpui::test]
3961 fn center_holding_a_tab_group_is_not_empty(cx: &mut TestAppContext) {
3962 let log = log_of();
3963 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3964 cx.run_until_parked();
3965
3966 assert!(!is_center_empty(&area, cx));
3967 }
3968
3969 #[gpui::test]
3973 fn center_is_empty_again_once_every_panel_is_removed(cx: &mut TestAppContext) {
3974 let log = log_of();
3975 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3976 cx.run_until_parked();
3977
3978 for panel in panels {
3979 cx.update(|window, cx| {
3980 area.update(cx, |area, cx| area.remove_panel(panel.clone(), window, cx))
3981 });
3982 }
3983 cx.run_until_parked();
3984
3985 assert!(is_center_empty(&area, cx));
3986 }
3987
3988 #[gpui::test]
3989 fn center_is_not_empty_after_adding_to_a_tab_group(cx: &mut TestAppContext) {
3990 let (area, cx) = setup(cx);
3991 assert!(is_center_empty(&area, cx));
3992
3993 cx.update(|window, cx| {
3994 let alpha = TestPanel::new("Alpha", cx);
3995 area.update(cx, |area, cx| {
3996 area.add_panel(alpha, DockPlacement::Center, None, window, cx)
3997 });
3998 });
3999 cx.run_until_parked();
4000
4001 assert!(!is_center_empty(&area, cx));
4002 }
4003
4004 #[gpui::test]
4008 fn add_panel_view_registers_the_handle_it_was_given(cx: &mut TestAppContext) {
4009 let (area, cx) = setup(cx);
4010
4011 let view = cx.update(|window, cx| {
4012 let view: Arc<dyn PanelView> = Arc::new(TestPanel::new("Alpha", cx));
4013 area.update(cx, |area, cx| {
4014 area.add_panel_view(view.clone(), DockPlacement::Center, None, window, cx)
4015 });
4016 view
4017 });
4018 cx.run_until_parked();
4019
4020 let id = cx.read(|cx| view.panel_id(cx));
4021 assert!(
4022 cx.read(|cx| area
4023 .read(cx)
4024 .panel(id)
4025 .is_some_and(|stored| Arc::ptr_eq(stored, &view))),
4026 "the stored handle is the one that was handed over, under its own id"
4027 );
4028 assert!(!is_center_empty(&area, cx));
4029 }
4030
4031 #[gpui::test]
4034 fn center_holding_only_hidden_panels_is_empty(cx: &mut TestAppContext) {
4035 let log = log_of();
4036 let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
4037 cx.run_until_parked();
4038 assert!(!is_center_empty(&area, cx));
4039
4040 cx.update(|_, cx| {
4041 for panel in &panels {
4042 panel.update(cx, |panel, cx| panel.set_visible(false, cx));
4043 }
4044 });
4045 cx.run_until_parked();
4046
4047 assert_eq!(
4048 cx.read(|cx| area
4049 .read(cx)
4050 .layout(DockPlacement::Center)
4051 .unwrap()
4052 .panels()
4053 .count()),
4054 2,
4055 "hiding a panel does not remove it from the tab group"
4056 );
4057 assert!(is_center_empty(&area, cx));
4058 }
4059
4060 #[gpui::test]
4065 fn center_holding_only_empty_tiles_is_empty(cx: &mut TestAppContext) {
4066 let (area, cx) = setup(cx);
4067 let bounds = Bounds {
4068 origin: gpui::point(px(10.), px(10.)),
4069 size: gpui::size(px(200.), px(200.)),
4070 };
4071 let alpha = cx.update(|window, cx| {
4072 let alpha = TestPanel::new("Alpha", cx);
4073 area.update(cx, |area, cx| {
4074 area.set_center(DockLayout::tiles().tile(alpha.clone(), bounds), window, cx)
4075 });
4076 alpha
4077 });
4078 cx.run_until_parked();
4079 assert!(!is_center_empty(&area, cx));
4080
4081 cx.update(|window, cx| area.update(cx, |area, cx| area.remove_panel(alpha, window, cx)));
4082 cx.run_until_parked();
4083
4084 assert!(is_center_empty(&area, cx));
4085 }
4086
4087 #[gpui::test]
4090 fn center_holding_only_hidden_tiles_is_empty(cx: &mut TestAppContext) {
4091 let (area, cx) = setup(cx);
4092 let bounds = Bounds {
4093 origin: gpui::point(px(10.), px(10.)),
4094 size: gpui::size(px(200.), px(200.)),
4095 };
4096 let alpha = cx.update(|window, cx| {
4097 let alpha = TestPanel::new("Alpha", cx);
4098 area.update(cx, |area, cx| {
4099 area.set_center(DockLayout::tiles().tile(alpha.clone(), bounds), window, cx)
4100 });
4101 alpha
4102 });
4103 cx.run_until_parked();
4104 assert!(!is_center_empty(&area, cx));
4105
4106 cx.update(|_, cx| alpha.update(cx, |alpha, cx| alpha.set_visible(false, cx)));
4107
4108 assert_eq!(
4109 cx.read(|cx| area
4110 .read(cx)
4111 .layout(DockPlacement::Center)
4112 .unwrap()
4113 .panels()
4114 .count()),
4115 1,
4116 "the tile is still on the canvas"
4117 );
4118 assert!(is_center_empty(&area, cx));
4119 }
4120
4121 #[gpui::test]
4122 fn single_panel_group_receives_initial_active(cx: &mut TestAppContext) {
4123 let log = log_of();
4124 let (_area, _panels, cx) = one_group(&log, &["A"], None, cx);
4125 cx.run_until_parked();
4126
4127 assert_eq!(drain_active(&log), [("A", true)]);
4128 }
4129
4130 #[gpui::test]
4131 fn multi_tab_construction_notifies_only_displayed_panel(cx: &mut TestAppContext) {
4132 let log = log_of();
4133 let (_area, _panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
4134 cx.run_until_parked();
4135
4136 assert_eq!(drain_active(&log), [("A", true)]);
4138 }
4139
4140 #[gpui::test]
4141 fn active_index_restore_notifies_that_panel_only(cx: &mut TestAppContext) {
4142 let log = log_of();
4143 let (_area, _panels, cx) = one_group(&log, &["A", "B", "C"], Some(2), cx);
4144 cx.run_until_parked();
4145
4146 assert_eq!(drain_active(&log), [("C", true)]);
4147 }
4148
4149 #[gpui::test]
4150 fn switching_tabs_sends_false_then_true(cx: &mut TestAppContext) {
4151 let log = log_of();
4152 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
4153 cx.run_until_parked();
4154 drain(&log);
4155
4156 let group = group_of(&area, 0, cx);
4157 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(1, window, cx)));
4158 cx.run_until_parked();
4159
4160 assert_eq!(drain_active(&log), [("A", false), ("B", true)]);
4161 }
4162
4163 #[gpui::test]
4164 fn reselecting_active_tab_stays_silent(cx: &mut TestAppContext) {
4165 let log = log_of();
4166 let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
4167 cx.run_until_parked();
4168 drain(&log);
4169
4170 let group = group_of(&area, 0, cx);
4171 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(0, window, cx)));
4172 cx.run_until_parked();
4173
4174 assert_eq!(drain_active(&log), []);
4175 }
4176
4177 #[gpui::test]
4183 fn inserting_at_active_ix_swaps_notifications(cx: &mut TestAppContext) {
4184 let log = log_of();
4185 let (area, cx) = setup(cx);
4186 let c = cx.update(|window, cx| {
4187 let a = TestPanel::logging("A", &log, cx);
4188 let b = TestPanel::logging("B", &log, cx);
4189 let x = TestPanel::logging("X", &log, cx);
4190 let c = TestPanel::logging("C", &log, cx);
4191 area.update(cx, |area, cx| {
4192 area.set_center(
4193 DockLayout::h_split()
4194 .child(DockLayout::tabs().panel(a).panel(b), None)
4195 .child(DockLayout::tabs().panel(x).panel(c.clone()), None),
4196 window,
4197 cx,
4198 );
4199 });
4200 c
4201 });
4202 cx.run_until_parked();
4203 drain(&log);
4204
4205 let destination = child_node(&area, 0, cx);
4206 let c_id = panel_id_of(&c);
4207 move_panel_into(&area, c_id, destination, Some(0), true, cx);
4208
4209 assert_eq!(drain_active(&log), [("A", false), ("C", true)]);
4210 let group = group_of(&area, 0, cx);
4211 assert_eq!(cx.read(|cx| group.read(cx).active_ix()), 0);
4212 assert_eq!(
4213 cx.read(|cx| group.read(cx).panels()[0].panel_id(cx)),
4214 c_id,
4215 "the arriving panel took the slot it named"
4216 );
4217 }
4218
4219 #[gpui::test]
4220 fn removing_before_active_keeps_displayed_panel(cx: &mut TestAppContext) {
4221 let log = log_of();
4222 let (area, panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
4223 let group = group_of(&area, 0, cx);
4224 cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(1, window, cx)));
4225 cx.run_until_parked();
4226 drain(&log);
4227
4228 cx.update(|window, cx| {
4229 area.update(cx, |area, cx| {
4230 area.remove_panel(panels[0].clone(), window, cx)
4231 })
4232 });
4233 cx.run_until_parked();
4234
4235 assert_eq!(drain_active(&log), []);
4236 assert_eq!(cx.read(|cx| group.read(cx).active_ix()), 0);
4237 assert_eq!(
4238 cx.read(|cx| group.read(cx).panels()[0].panel_id(cx)),
4239 panel_id_of(&panels[1]),
4240 "the same panel is still displayed, at its new index"
4241 );
4242 }
4243
4244 #[gpui::test]
4247 fn collapse_and_expand_notify_active_panel(cx: &mut TestAppContext) {
4248 let log = log_of();
4249 let (area, cx) = setup(cx);
4250 cx.update(|window, cx| {
4251 let a = TestPanel::logging("A", &log, cx);
4252 let b = TestPanel::logging("B", &log, cx);
4253 area.update(cx, |area, cx| {
4254 area.set_dock(
4255 DockPlacement::Left,
4256 DockLayout::tabs().panel(a).panel(b),
4257 window,
4258 cx,
4259 );
4260 });
4261 });
4262 cx.run_until_parked();
4263 drain(&log);
4264
4265 cx.update(|window, cx| {
4266 area.update(cx, |area, cx| {
4267 area.toggle_dock(DockPlacement::Left, window, cx)
4268 })
4269 });
4270 cx.run_until_parked();
4271 assert_eq!(drain_active(&log), [("A", false)]);
4272
4273 cx.update(|window, cx| {
4274 area.update(cx, |area, cx| {
4275 area.toggle_dock(DockPlacement::Left, window, cx)
4276 })
4277 });
4278 cx.run_until_parked();
4279 assert_eq!(drain_active(&log), [("A", true)]);
4280 }
4281
4282 #[gpui::test]
4283 fn background_add_is_silent_but_first_panel_is_not(cx: &mut TestAppContext) {
4284 let log = log_of();
4285 let (area, cx) = setup(cx);
4286 let d = cx.update(|window, cx| {
4287 let a = TestPanel::logging("A", &log, cx);
4288 let b = TestPanel::logging("B", &log, cx);
4289 let c = TestPanel::logging("C", &log, cx);
4290 let d = TestPanel::logging("D", &log, cx);
4291 area.update(cx, |area, cx| {
4292 area.set_center(
4293 DockLayout::h_split()
4294 .child(DockLayout::tabs().panel(a).panel(b), None)
4295 .child(DockLayout::tabs().panel(c).panel(d.clone()), None),
4296 window,
4297 cx,
4298 );
4299 });
4300 d
4301 });
4302 cx.run_until_parked();
4303 drain(&log);
4304
4305 let destination = child_node(&area, 0, cx);
4308 move_panel_into(&area, panel_id_of(&d), destination, None, false, cx);
4309 assert_eq!(drain_active(&log), []);
4310
4311 cx.update(|window, cx| {
4314 let e = TestPanel::logging("E", &log, cx);
4315 area.update(cx, |area, cx| {
4316 area.add_panel(e, DockPlacement::Left, None, window, cx)
4317 });
4318 });
4319 cx.run_until_parked();
4320 assert_eq!(drain_active(&log), [("E", true)]);
4321 }
4322
4323 #[gpui::test]
4324 fn drag_active_panel_to_other_group_stays_silent_for_it(cx: &mut TestAppContext) {
4325 let log = log_of();
4326 let (area, cx) = setup(cx);
4327 let a = cx.update(|window, cx| {
4328 let a = TestPanel::logging("A", &log, cx);
4329 let b = TestPanel::logging("B", &log, cx);
4330 let c = TestPanel::logging("C", &log, cx);
4331 area.update(cx, |area, cx| {
4332 area.set_center(
4333 DockLayout::h_split()
4334 .child(DockLayout::tabs().panel(a.clone()).panel(b), None)
4335 .child(DockLayout::tabs().panel(c), None),
4336 window,
4337 cx,
4338 );
4339 });
4340 a
4341 });
4342 cx.run_until_parked();
4343 drain(&log);
4344
4345 let destination = child_node(&area, 1, cx);
4348 move_panel_into(&area, panel_id_of(&a), destination, None, true, cx);
4349
4350 let seen = drain_active(&log);
4353 assert!(seen.contains(&("B", true)), "got {seen:?}");
4354 assert!(seen.contains(&("C", false)), "got {seen:?}");
4355 assert!(
4356 !seen.iter().any(|(name, _)| *name == "A"),
4357 "the moved panel was displayed before and after: {seen:?}"
4358 );
4359 }
4360
4361 #[gpui::test]
4362 fn drag_active_panel_to_background_slot_deactivates_it(cx: &mut TestAppContext) {
4363 let log = log_of();
4364 let (area, cx) = setup(cx);
4365 let a = cx.update(|window, cx| {
4366 let a = TestPanel::logging("A", &log, cx);
4367 let c = TestPanel::logging("C", &log, cx);
4368 let d = TestPanel::logging("D", &log, cx);
4369 area.update(cx, |area, cx| {
4370 area.set_center(
4371 DockLayout::h_split()
4372 .child(DockLayout::tabs().panel(a.clone()), None)
4373 .child(DockLayout::tabs().panel(c).panel(d), None),
4374 window,
4375 cx,
4376 );
4377 });
4378 a
4379 });
4380 cx.run_until_parked();
4381 drain(&log);
4382
4383 let destination = child_node(&area, 1, cx);
4386 move_panel_into(&area, panel_id_of(&a), destination, None, false, cx);
4387
4388 assert_eq!(drain_active(&log), [("A", false)]);
4389 }
4390
4391 #[gpui::test]
4392 fn closing_a_tile_removes_its_panel(cx: &mut TestAppContext) {
4393 let log = log_of();
4396 let (area, cx) = setup(cx);
4397 let bounds = Bounds {
4398 origin: gpui::point(px(10.), px(10.)),
4399 size: gpui::size(px(200.), px(200.)),
4400 };
4401 let alpha = cx.update(|window, cx| {
4402 let alpha = TestPanel::logging("Alpha", &log, cx);
4403 let beta = TestPanel::logging("Beta", &log, cx);
4404 area.update(cx, |area, cx| {
4405 area.set_center(
4406 DockLayout::tiles()
4407 .tile(alpha.clone(), bounds)
4408 .tile(beta, bounds),
4409 window,
4410 cx,
4411 );
4412 });
4413 alpha
4414 });
4415 cx.run_until_parked();
4416 drain(&log);
4417
4418 let canvas_node = child_node(&area, 0, cx);
4419 let canvas = cx.read(|cx| {
4420 area.read(cx)
4421 .tiles
4422 .get(&canvas_node)
4423 .unwrap()
4424 .entity
4425 .clone()
4426 });
4427 cx.update(|window, cx| {
4428 let tile = canvas.read(cx).tiles(cx)[0].clone();
4429 assert!(tile.is_closable());
4430 tile.close(window, cx);
4431 });
4432 cx.run_until_parked();
4433
4434 assert!(
4435 cx.read(|cx| area.read(cx).panel(panel_id_of(&alpha)).is_none()),
4436 "the closed tile's panel left the dock"
4437 );
4438 assert!(drain(&log).contains(&("Alpha", PanelSignal::Removed)));
4439 }
4440
4441 struct RecordingSkin {
4448 tab_bars: Rc<RefCell<Vec<NodeId>>>,
4449 drag_bars: Rc<RefCell<Vec<PanelId>>>,
4450 }
4451
4452 struct RecordingTabGroup {
4453 drawn: Rc<RefCell<Vec<NodeId>>>,
4454 }
4455
4456 struct RecordingTiles {
4457 drawn: Rc<RefCell<Vec<PanelId>>>,
4458 }
4459
4460 impl DockAreaRenderer for RecordingSkin {
4461 fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
4462 Rc::new(RecordingTabGroup {
4463 drawn: self.tab_bars.clone(),
4464 })
4465 }
4466
4467 fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> {
4468 Rc::new(RecordingTiles {
4469 drawn: self.drag_bars.clone(),
4470 })
4471 }
4472 }
4473
4474 impl TabGroupRenderer for RecordingTabGroup {
4475 fn render_tab_bar(
4476 &self,
4477 group: &TabGroupContext,
4478 _: &mut Window,
4479 _: &mut App,
4480 ) -> AnyElement {
4481 self.drawn.borrow_mut().push(group.node());
4482 Empty.into_any_element()
4483 }
4484 }
4485
4486 impl TilesRenderer for RecordingTiles {
4487 fn render_drag_bar(&self, tile: &TileContext, _: &mut Window, _: &mut App) -> AnyElement {
4488 self.drawn.borrow_mut().push(tile.panel_id());
4489 Empty.into_any_element()
4490 }
4491 }
4492
4493 type DrawLog = (Rc<RefCell<Vec<NodeId>>>, Rc<RefCell<Vec<PanelId>>>);
4494
4495 fn setup_recording(
4497 cx: &mut TestAppContext,
4498 ) -> (Entity<DockArea>, DrawLog, &mut VisualTestContext) {
4499 cx.update(|cx| {
4500 let _ = crate::Theme::global_mut(cx);
4501 });
4502 let tab_bars: Rc<RefCell<Vec<NodeId>>> = Rc::default();
4503 let drag_bars: Rc<RefCell<Vec<PanelId>>> = Rc::default();
4504 let skin = Rc::new(RecordingSkin {
4505 tab_bars: tab_bars.clone(),
4506 drag_bars: drag_bars.clone(),
4507 });
4508 let (area, cx) = cx.add_window_view(|window, cx| {
4509 DockArea::new("test-dock", None, window, cx).with_renderer(skin)
4510 });
4511 (area, (tab_bars, drag_bars), cx)
4512 }
4513
4514 fn zoom_signals(log: &Log) -> Vec<(&'static str, PanelSignal)> {
4515 drain(log)
4516 .into_iter()
4517 .filter(|(_, signal)| matches!(signal, PanelSignal::Zoomed(_)))
4518 .collect()
4519 }
4520
4521 #[gpui::test]
4530 fn a_zoomed_group_is_drawn_whole_rather_than_as_its_bare_panel(cx: &mut TestAppContext) {
4531 let log = log_of();
4532 let (area, (tab_bars, _), cx) = setup_recording(cx);
4533 cx.update(|window, cx| {
4534 let alpha = TestPanel::logging("Alpha", &log, cx);
4535 let beta = TestPanel::logging("Beta", &log, cx);
4536 area.update(cx, |area, cx| {
4537 area.set_center(
4538 DockLayout::h_split()
4539 .child(DockLayout::tabs().panel(alpha), None)
4540 .child(DockLayout::tabs().panel(beta), None),
4541 window,
4542 cx,
4543 );
4544 });
4545 });
4546 cx.run_until_parked();
4547
4548 let zoomed = child_node(&area, 0, cx);
4549 let other = child_node(&area, 1, cx);
4550 assert!(
4551 tab_bars.borrow().contains(&zoomed) && tab_bars.borrow().contains(&other),
4552 "both groups draw their own tab bar while nothing is zoomed"
4553 );
4554
4555 tab_bars.borrow_mut().clear();
4556 let group = group_of(&area, 0, cx);
4557 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
4558 cx.run_until_parked();
4559
4560 assert!(
4561 tab_bars.borrow().contains(&zoomed),
4562 "a zoomed group is rendered whole: its own tab bar is still drawn, \
4563 which is exactly what the bare panel does not carry"
4564 );
4565 assert!(
4566 !tab_bars.borrow().contains(&other),
4567 "and it is the only thing on screen"
4568 );
4569 }
4570
4571 #[gpui::test]
4577 fn a_zoomed_tile_is_drawn_by_its_canvas_with_its_chrome(cx: &mut TestAppContext) {
4578 let log = log_of();
4579 let (area, (_, drag_bars), cx) = setup_recording(cx);
4580 let bounds = Bounds {
4581 origin: gpui::point(px(40.), px(40.)),
4582 size: gpui::size(px(200.), px(150.)),
4583 };
4584 let (alpha, beta) = cx.update(|window, cx| {
4585 let alpha = TestPanel::logging("Alpha", &log, cx);
4586 let beta = TestPanel::logging("Beta", &log, cx);
4587 area.update(cx, |area, cx| {
4588 area.set_center(
4589 DockLayout::tiles()
4590 .tile(alpha.clone(), bounds)
4591 .tile(beta.clone(), bounds),
4592 window,
4593 cx,
4594 );
4595 });
4596 (alpha, beta)
4597 });
4598 cx.run_until_parked();
4599 drain(&log);
4600
4601 let canvas_node = child_node(&area, 0, cx);
4602 let canvas = cx.read(|cx| {
4603 area.read(cx)
4604 .tiles
4605 .get(&canvas_node)
4606 .unwrap()
4607 .entity
4608 .clone()
4609 });
4610 assert!(
4611 drag_bars.borrow().contains(&panel_id_of(&alpha))
4612 && drag_bars.borrow().contains(&panel_id_of(&beta)),
4613 "both tiles draw their own drag bar while nothing is zoomed"
4614 );
4615
4616 drag_bars.borrow_mut().clear();
4617 cx.update(|window, cx| {
4618 let tile = canvas.read(cx).tiles(cx)[0].clone();
4619 assert!(tile.is_zoomable());
4620 tile.toggle_zoom(window, cx);
4621 });
4622 cx.run_until_parked();
4623
4624 assert_eq!(
4625 cx.read(|cx| area.read(cx).zoomed_tile()),
4626 Some(panel_id_of(&alpha))
4627 );
4628 assert!(
4629 drag_bars.borrow().contains(&panel_id_of(&alpha)),
4630 "the zoomed tile keeps the chrome the bare panel does not carry"
4631 );
4632 assert!(
4633 !drag_bars.borrow().contains(&panel_id_of(&beta)),
4634 "and the tiles beside it are no longer drawn"
4635 );
4636 assert_eq!(
4637 zoom_signals(&log),
4638 vec![("Alpha", PanelSignal::Zoomed(true))],
4639 "the panel is told it was zoomed, as its group would have told it"
4640 );
4641
4642 cx.update(|window, cx| {
4646 let tile = canvas.read(cx).tiles(cx)[0].clone();
4647 tile.begin_move(gpui::point(px(100.), px(100.)), window, cx);
4648 });
4649 assert!(!cx.read(|cx| canvas.read(cx).tiles(cx)[0].is_moving()));
4650 }
4651
4652 #[gpui::test]
4658 fn clearing_the_zoom_from_outside_puts_the_groups_own_flag_back(cx: &mut TestAppContext) {
4659 let log = log_of();
4660 let (area, _alpha, cx) = two_groups(&log, cx);
4661 cx.run_until_parked();
4662 drain(&log);
4663
4664 let node = child_node(&area, 0, cx);
4665 let group = group_of(&area, 0, cx);
4666 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
4667 cx.run_until_parked();
4668 assert_eq!(cx.read(|cx| area.read(cx).zoomed_group()), Some(node));
4669 assert!(cx.read(|cx| group.read(cx).is_zoomed()));
4670 assert_eq!(
4671 zoom_signals(&log),
4672 vec![("Alpha", PanelSignal::Zoomed(true))]
4673 );
4674
4675 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_out(window, cx)));
4676 cx.run_until_parked();
4677
4678 assert!(!cx.read(|cx| area.read(cx).is_zoomed()));
4679 assert!(
4680 !cx.read(|cx| group.read(cx).is_zoomed()),
4681 "a group left flagged zoomed would stay locked and refuse every drop"
4682 );
4683 assert!(
4684 cx.read(|cx| group.read(cx).context(cx).is_droppable()),
4685 "and the lock the zoom imposed is lifted with it"
4686 );
4687 assert_eq!(
4688 zoom_signals(&log),
4689 vec![("Alpha", PanelSignal::Zoomed(false))],
4690 "the panel hears the zoom end too, not just the group"
4691 );
4692 }
4693
4694 #[gpui::test]
4697 fn a_group_that_refuses_to_zoom_leaves_the_area_unzoomed(cx: &mut TestAppContext) {
4698 let log = log_of();
4699 let (area, alpha, cx) = two_groups(&log, cx);
4700 cx.run_until_parked();
4701 cx.update(|_, cx| alpha.update(cx, |panel, cx| panel.set_zoomable(false, cx)));
4702
4703 let node = child_node(&area, 0, cx);
4704 let group = group_of(&area, 0, cx);
4705 cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_in(node, window, cx)));
4706 cx.run_until_parked();
4707
4708 assert!(!cx.read(|cx| group.read(cx).is_zoomed()));
4709 assert!(
4710 !cx.read(|cx| area.read(cx).is_zoomed()),
4711 "the area must not fill itself with a group that never zoomed"
4712 );
4713 }
4714}