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