Skip to main content

gpui_base/dock/
dock_area.rs

1//! The dock area: the trees, the entity cache that mirrors them, and the
2//! reconciliation that keeps the two in step.
3
4use crate::TestSupportExt as _;
5use std::{
6    collections::{HashMap, HashSet},
7    rc::Rc,
8    sync::Arc,
9};
10
11use anyhow::Result;
12use gpui::{
13    AnyElement, AnyView, App, AppContext as _, Axis, Bounds, Context, Div, Empty, Entity,
14    EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, ParentElement,
15    Pixels, Point, Render, SharedString, Stateful, Styled as _, Subscription, WeakEntity, Window,
16    div, prelude::FluentBuilder as _, px,
17};
18
19use crate::{
20    ElementExt as _, Placement, ResizablePanelEvent, ResizableState, ResizeHandleContext,
21    h_resizable, resizable::PANEL_MIN_SIZE, resizable_panel, v_resizable,
22};
23
24use super::{
25    dock_placement::{Dock, DockSizing},
26    drag::{AnyDrag, DropTarget},
27    layout::{
28        DockLayout, EditResult, InsertTarget, NodeId, NodeKind, PaneNode, PaneRef, PaneTree,
29        PanelId, RootKind,
30    },
31    panel::{LivePanels, Panel, PanelEvent, PanelView},
32    registry::{PanelBuildContext, PanelRegistry},
33    state::{DockAreaState, DockPlacement, DockState, PanelInfo, PanelState, TileMeta},
34    state_convert::{PanelBuilder, PanelSource as _},
35    tab_group::{BareTabGroup, TabGroup, TabGroupConstraints, TabGroupEvent, TabGroupRenderer},
36    tiles_state::{BareTiles, TilesEvent, TilesRenderer, TilesState},
37};
38
39/// What the dock area reports outward.
40pub enum DockEvent {
41    /// The layout changed. Subscribe to persist it; this fires on every edit,
42    /// including each step of a tile drag, so a subscriber that writes to disk
43    /// should debounce.
44    LayoutChanged,
45    /// A host-owned drag item was dropped inside the dock.
46    DragDrop { item: AnyDrag, target: DropTarget },
47}
48
49/// What fills the whole area, when something does.
50///
51/// A zoom names a *container*, never the panel inside it. The old dock zoomed
52/// the `TabPanel`: `TabPanel` is the only thing that ever emitted
53/// `PanelEvent::ZoomIn` — `subscribe_panel` was handed `StackPanel`s too, but
54/// those never zoom — so `set_zoomed_in` only ever received a whole tab panel,
55/// and a zoomed panel kept its tab bar, its toolbar and its menu. That is
56/// where the control that zooms back out lives. Naming the panel instead would
57/// strip all of it and leave the user with no way back.
58#[derive(Clone, Copy, PartialEq, Eq, Debug)]
59enum Zoomed {
60    /// A tab group, rendered whole through its own [`TabGroupRenderer`].
61    Group(NodeId),
62    /// One tile of a canvas, rendered by that canvas through its own
63    /// [`TilesRenderer`]. The canvas is what draws a tile's chrome, so the
64    /// canvas is what is rendered.
65    Tile { node: NodeId, panel: PanelId },
66}
67
68/// What a caller asked for when adding a panel, which is also which entry
69/// point it came through.
70#[derive(Clone, Copy)]
71enum Added {
72    /// Wherever the region's own shape puts it. The size seeds a dock that
73    /// does not exist yet, and the slot of a group that has to be made.
74    Anywhere(Option<Pixels>),
75    /// A tile at these bounds, or nowhere: the bounds name a place only a
76    /// canvas has, so a region without one is left alone rather than growing a
77    /// tab group the caller never asked for.
78    AsTile(Bounds<Pixels>),
79}
80
81impl Added {
82    fn dock_size(self) -> Option<Pixels> {
83        match self {
84            Self::Anywhere(size) => size,
85            Self::AsTile(_) => None,
86        }
87    }
88}
89
90/// One dock: its own layout tree plus the open/size/collapsible state.
91struct DockRegion {
92    tree: PaneTree,
93    dock: Dock,
94}
95
96/// A cached container entity together with the subscription that carries its
97/// intents back here. Kept as one value so dropping the cache entry drops the
98/// subscription with it.
99struct Cached<T> {
100    entity: Entity<T>,
101    _subscription: Subscription,
102}
103
104/// A split's cached `ResizableState`, plus the child order that state's panel
105/// list currently mirrors.
106///
107/// The order is what makes a reconcile index-precise. `ResizableState` keeps
108/// the authoritative size on `panels[ix]` and only ever consults the tree's
109/// size as an initial value, so appending and truncating at the tail —
110/// which is all `sync_panels_count` can do — leaves the survivors of a
111/// non-tail removal wearing their predecessors' widths.
112struct CachedSplit {
113    entity: Entity<ResizableState>,
114    children: Vec<NodeId>,
115    /// The tree sizes the state last adopted. A reconcile that finds them
116    /// unchanged leaves the state alone: re-adopting a `None` un-pins the
117    /// measurement the layout pass resolved it to, and the split re-flexes
118    /// although the edit never touched it.
119    sizes: Vec<Option<Pixels>>,
120    _subscription: Subscription,
121}
122
123/// The main area of the dock.
124///
125/// It owns one [`PaneTree`] per region and the entity cache that mirrors
126/// them. Nothing else turns a tree edit into live entities.
127pub struct DockArea {
128    id: SharedString,
129    version: Option<usize>,
130    bounds: Bounds<Pixels>,
131    this: WeakEntity<Self>,
132
133    center: PaneTree,
134    docks: HashMap<DockPlacement, DockRegion>,
135
136    groups: HashMap<NodeId, Cached<TabGroup>>,
137    splits: HashMap<NodeId, CachedSplit>,
138    tiles: HashMap<NodeId, Cached<TilesState>>,
139    panels: HashMap<PanelId, Arc<dyn PanelView>>,
140
141    locked: bool,
142    zoomed: Option<Zoomed>,
143    focus_handle: FocusHandle,
144    renderer: Rc<dyn DockAreaRenderer>,
145}
146
147impl DockArea {
148    /// An empty area that draws nothing but its panels.
149    ///
150    /// `id` names the area for the host's own persistence; `version` is
151    /// written into [`Self::dump`] and read back by [`Self::load`], for a host
152    /// that wants to reject or migrate a layout an older build wrote.
153    ///
154    /// Install appearance with [`Self::with_renderer`].
155    pub fn new(
156        id: impl Into<SharedString>,
157        version: Option<usize>,
158        _window: &mut Window,
159        cx: &mut Context<Self>,
160    ) -> Self {
161        PanelRegistry::init(cx);
162
163        Self {
164            id: id.into(),
165            version,
166            bounds: Bounds::default(),
167            this: cx.weak_entity(),
168            center: PaneTree::new(RootKind::Split),
169            docks: HashMap::new(),
170            groups: HashMap::new(),
171            splits: HashMap::new(),
172            tiles: HashMap::new(),
173            panels: HashMap::new(),
174            locked: false,
175            zoomed: None,
176            focus_handle: cx.focus_handle(),
177            renderer: Rc::new(BareDockArea),
178        }
179    }
180
181    /// Install the appearance for this area and everything under it: the
182    /// renderer also supplies the [`TabGroupRenderer`] and [`TilesRenderer`]
183    /// every container it builds will use.
184    pub fn with_renderer(mut self, renderer: Rc<dyn DockAreaRenderer>) -> Self {
185        self.renderer = renderer;
186        self
187    }
188
189    pub fn id(&self) -> SharedString {
190        self.id.clone()
191    }
192
193    pub fn version(&self) -> Option<usize> {
194        self.version
195    }
196
197    /// Change the schema version a later [`dump`](Self::dump) writes.
198    ///
199    /// Set at construction for an area whose layout never changes shape. A
200    /// host that installs one of several preset layouts into the same area
201    /// picks the version with the preset, which is after the area exists.
202    pub fn set_version(&mut self, version: Option<usize>, cx: &mut Context<Self>) {
203        self.version = version;
204        cx.notify();
205    }
206
207    /// The area's own bounds, recorded each frame. Dock resizing measures
208    /// against it.
209    pub fn bounds(&self) -> Bounds<Pixels> {
210        self.bounds
211    }
212
213    /// The tree for one region, or `None` for a dock that does not exist.
214    ///
215    /// The `Option` is in the signature rather than hidden behind a panic
216    /// because a dock is genuinely optional — it is `Option<DockState>` in the
217    /// persisted schema — and there is no borrowable empty tree to hand back
218    /// for one that is absent.
219    pub fn layout(&self, placement: DockPlacement) -> Option<&PaneTree> {
220        match placement {
221            DockPlacement::Center => Some(&self.center),
222            _ => self.docks.get(&placement).map(|pane| &pane.tree),
223        }
224    }
225
226    /// The live view for a panel, if the dock still holds it.
227    pub fn panel(&self, panel: PanelId) -> Option<&Arc<dyn PanelView>> {
228        self.panels.get(&panel)
229    }
230
231    pub fn is_locked(&self) -> bool {
232        self.locked
233    }
234
235    /// Whether a region currently holds no visible panel.
236    ///
237    /// This is the question the old `DockItem::is_empty` answered, and it is
238    /// the same one [`Self::is_node_visible`] answers per container: a region
239    /// is empty when nothing in it would be drawn. A region that does not
240    /// exist — a dock that was never installed — is empty too.
241    pub fn is_empty(&self, placement: DockPlacement, cx: &App) -> bool {
242        self.layout(placement)
243            .is_none_or(|tree| !self.is_node_visible(tree.root(), cx))
244    }
245
246    /// Lock the layout against rearranging. Resizing stays available.
247    pub fn set_locked(&mut self, locked: bool, window: &mut Window, cx: &mut Context<Self>) {
248        if self.locked == locked {
249            return;
250        }
251        self.locked = locked;
252        // The lock is one of the facts every group is told, so it only takes
253        // effect once the groups have been re-told.
254        self.reconcile(window, cx);
255    }
256}
257
258/// Installing layouts: the center region and the three docks.
259impl DockArea {
260    /// Replace the center region with a described layout. Whatever was there
261    /// leaves the dock, so its panels are told [`Panel::on_removed`].
262    pub fn set_center(&mut self, layout: DockLayout, window: &mut Window, cx: &mut Context<Self>) {
263        let (tree, panels) = PaneTree::from_layout(layout, RootKind::Split);
264        self.center = tree;
265        self.panels.extend(panels);
266        self.reconcile(window, cx);
267        cx.emit(DockEvent::LayoutChanged);
268    }
269
270    /// Replace one dock with a described layout, creating the dock if the area
271    /// does not have one there yet. A new dock keeps the size and open state of
272    /// the one it replaces, so re-filling a dock does not resize it.
273    ///
274    /// [`DockPlacement::Center`] defers to [`Self::set_center`]: the center is
275    /// not a dock and has no size or open state of its own.
276    pub fn set_dock(
277        &mut self,
278        placement: DockPlacement,
279        layout: DockLayout,
280        window: &mut Window,
281        cx: &mut Context<Self>,
282    ) {
283        if placement == DockPlacement::Center {
284            return self.set_center(layout, window, cx);
285        }
286
287        let (tree, panels) = PaneTree::from_layout(layout, RootKind::Any);
288        let dock = self
289            .docks
290            .get(&placement)
291            .map(|pane| pane.dock)
292            .unwrap_or_else(|| Dock::new(PANEL_MIN_SIZE * 2.));
293        self.docks.insert(placement, DockRegion { tree, dock });
294        self.panels.extend(panels);
295        self.reconcile(window, cx);
296        cx.emit(DockEvent::LayoutChanged);
297    }
298
299    /// Take a dock away entirely, panels and all. Distinct from
300    /// [`Self::toggle_dock`], which only takes it off screen.
301    pub fn remove_dock(
302        &mut self,
303        placement: DockPlacement,
304        window: &mut Window,
305        cx: &mut Context<Self>,
306    ) {
307        if self.docks.remove(&placement).is_none() {
308            return;
309        }
310        // The dock's panels leave the dock entirely, so they are told, unlike
311        // the panels of a dock that merely closed.
312        self.reconcile(window, cx);
313        cx.emit(DockEvent::LayoutChanged);
314    }
315
316    pub fn has_dock(&self, placement: DockPlacement) -> bool {
317        self.docks.contains_key(&placement)
318    }
319
320    /// Whether a dock is on screen. A dock the area does not have is never
321    /// open, so this answers the question a caller usually means without a
322    /// preceding [`Self::has_dock`].
323    pub fn is_dock_open(&self, placement: DockPlacement) -> bool {
324        self.docks
325            .get(&placement)
326            .is_some_and(|pane| pane.dock.is_open())
327    }
328
329    /// Open a closed dock or close an open one. A dock that is not
330    /// collapsible refuses to close; there is nothing to refuse when opening.
331    pub fn toggle_dock(
332        &mut self,
333        placement: DockPlacement,
334        window: &mut Window,
335        cx: &mut Context<Self>,
336    ) {
337        let Some(pane) = self.docks.get_mut(&placement) else {
338            return;
339        };
340        if !pane.dock.is_collapsible() && pane.dock.is_open() {
341            return;
342        }
343        let open = pane.dock.is_open();
344        pane.dock.set_open(!open);
345        // A closed dock takes its displayed panel off screen, which the
346        // active-state contract counts as no panel being displayed — that is
347        // what `TabGroupConstraints::collapsed` carries.
348        self.reconcile(window, cx);
349        cx.emit(DockEvent::LayoutChanged);
350    }
351
352    /// Whether a dock may be collapsed at all. A skin drawing a collapse
353    /// affordance in a tab bar reads this to decide whether to offer one.
354    pub fn is_dock_collapsible(&self, placement: DockPlacement) -> bool {
355        self.docks
356            .get(&placement)
357            .is_some_and(|pane| pane.dock.is_collapsible())
358    }
359
360    pub fn set_dock_collapsible(
361        &mut self,
362        placement: DockPlacement,
363        collapsible: bool,
364        _window: &mut Window,
365        cx: &mut Context<Self>,
366    ) {
367        if let Some(pane) = self.docks.get_mut(&placement) {
368            pane.dock.set_collapsible(collapsible);
369            cx.notify();
370        }
371    }
372
373    /// The dock's size along its axis.
374    pub fn dock_size(&self, placement: DockPlacement) -> Option<Pixels> {
375        self.docks.get(&placement).map(|pane| pane.dock.size())
376    }
377
378    pub fn set_dock_size(
379        &mut self,
380        placement: DockPlacement,
381        size: Pixels,
382        _window: &mut Window,
383        cx: &mut Context<Self>,
384    ) {
385        if let Some(pane) = self.docks.get_mut(&placement) {
386            let previous = pane.dock.size();
387            pane.dock.set_size(size);
388            if pane.dock.size() == previous {
389                return;
390            }
391            cx.notify();
392            cx.emit(DockEvent::LayoutChanged);
393        }
394    }
395}
396
397/// Editing the layout.
398impl DockArea {
399    /// Add a panel to a region, merging it into the first tab group there,
400    /// placing it on the region's tiles canvas if that is what the region is,
401    /// or starting a group when the region is empty.
402    pub fn add_panel<P: Panel>(
403        &mut self,
404        panel: Entity<P>,
405        placement: DockPlacement,
406        size: Option<Pixels>,
407        window: &mut Window,
408        cx: &mut Context<Self>,
409    ) {
410        let id = PanelId::from(panel.entity_id());
411        self.add_panel_inner(
412            id,
413            Arc::new(panel),
414            placement,
415            Added::Anywhere(size),
416            window,
417            cx,
418        );
419    }
420
421    /// Add an already-wrapped panel handle to a region.
422    ///
423    /// The companion to [`Self::add_panel`], for a layer that hands base its
424    /// own concrete handle — see [`PanelView::as_any`] — rather than a bare
425    /// entity. The id comes from [`PanelView::panel_id`], which is the only
426    /// place it can come from once the entity is behind the handle.
427    pub fn add_panel_view(
428        &mut self,
429        panel: Arc<dyn PanelView>,
430        placement: DockPlacement,
431        size: Option<Pixels>,
432        window: &mut Window,
433        cx: &mut Context<Self>,
434    ) {
435        let id = panel.panel_id(cx);
436        self.add_panel_inner(id, panel, placement, Added::Anywhere(size), window, cx);
437    }
438
439    /// Add a panel to a region's tiles canvas at `bounds`.
440    ///
441    /// [`Self::add_panel`] places a tile too, but only where the canvas
442    /// itself chooses; this is for a host that knows where the tile belongs —
443    /// most of all one acting on [`DockEvent::DragDrop`] with a
444    /// [`DropTarget::Canvas`], which reports *that* something was dropped on
445    /// a canvas and leaves placing it to the host.
446    ///
447    /// A region with no tiles canvas has nowhere to put a tile, so nothing
448    /// happens and the panel is not registered.
449    pub fn add_tile<P: Panel>(
450        &mut self,
451        panel: Entity<P>,
452        placement: DockPlacement,
453        bounds: Bounds<Pixels>,
454        window: &mut Window,
455        cx: &mut Context<Self>,
456    ) {
457        let id = PanelId::from(panel.entity_id());
458        self.add_panel_inner(
459            id,
460            Arc::new(panel),
461            placement,
462            Added::AsTile(bounds),
463            window,
464            cx,
465        );
466    }
467
468    /// [`Self::add_tile`] for an already-wrapped handle, for the same reason
469    /// [`Self::add_panel_view`] is the companion to [`Self::add_panel`].
470    pub fn add_tile_view(
471        &mut self,
472        panel: Arc<dyn PanelView>,
473        placement: DockPlacement,
474        bounds: Bounds<Pixels>,
475        window: &mut Window,
476        cx: &mut Context<Self>,
477    ) {
478        let id = panel.panel_id(cx);
479        self.add_panel_inner(id, panel, placement, Added::AsTile(bounds), window, cx);
480    }
481
482    fn add_panel_inner(
483        &mut self,
484        id: PanelId,
485        panel: Arc<dyn PanelView>,
486        placement: DockPlacement,
487        added: Added,
488        window: &mut Window,
489        cx: &mut Context<Self>,
490    ) {
491        // The registration is written before the target is resolved, because
492        // both want `&mut self`, so an add that finds nowhere to put the panel
493        // has to undo it. *Undo*, not remove: adding a panel the dock already
494        // holds is a legitimate call — a host re-placing one it owns — and
495        // dropping its view would strand it in a tree with no entity, which is
496        // what `reconcile`'s `views_of` asserts against. Restoring the previous
497        // handle rather than keeping this one matters too: the two differ when
498        // a panel registered through `add_panel_view` is then named by
499        // `add_tile`, and keeping the bare entity would cost the panel its
500        // title for a call that otherwise did nothing.
501        let previous = self.panels.insert(id, panel);
502
503        // A dock is created to hold the panel, but only for a caller that will
504        // take whatever shape the region offers. A tile has to land on a
505        // canvas, and a freshly made dock has none, so making one here would
506        // leave an empty dock behind after the insert below declines.
507        if matches!(added, Added::Anywhere(_))
508            && placement != DockPlacement::Center
509            && !self.docks.contains_key(&placement)
510        {
511            self.docks.insert(
512                placement,
513                DockRegion {
514                    tree: PaneTree::new(RootKind::Any),
515                    dock: Dock::new(added.dock_size().unwrap_or(PANEL_MIN_SIZE * 2.)),
516                },
517            );
518        }
519
520        let Some(tree) = self.tree_mut(placement) else {
521            self.restore_registration(id, previous);
522            return;
523        };
524        let target = match added {
525            // An explicit tile: only a canvas can hold it. Falling back to a
526            // tab group would put the panel somewhere the caller did not ask
527            // for and silently discard the bounds.
528            Added::AsTile(bounds) => match first_tiles_canvas(tree.root()) {
529                Some(node) => InsertTarget::Tile { node, bounds },
530                None => {
531                    self.restore_registration(id, previous);
532                    return;
533                }
534            },
535            Added::Anywhere(size) => match first_tab_group(tree.root()) {
536                Some(node) => InsertTarget::Tabs {
537                    node,
538                    ix: None,
539                    activate: true,
540                },
541                // A region that is a tiles canvas takes a tile, at the
542                // placement `TileMeta` defaults to — which is what the old
543                // `DockItem::add_panel`'s `Tiles` arm did with no bounds in
544                // hand. Splitting a canvas in two instead would wrap the whole
545                // thing in a stack the user never asked for.
546                None => match first_tiles_canvas(tree.root()) {
547                    Some(node) => InsertTarget::Tile {
548                        node,
549                        bounds: TileMeta::default().bounds,
550                    },
551                    // An empty region has no container to merge into, so the
552                    // panel makes one beside the root. `normalize` then removes
553                    // the emptied root and, for a dock, collapses the wrapper
554                    // away again.
555                    None => InsertTarget::Split {
556                        node: tree.root().id(),
557                        placement: Placement::Right,
558                        size,
559                    },
560                },
561            },
562        };
563        let result = tree.insert_panel(id, target);
564        if !result.changed() {
565            // Nothing took the panel, so a newly registered one must not
566            // linger in the view map and be told `on_removed` by the next
567            // reconcile.
568            self.restore_registration(id, previous);
569            return;
570        }
571        self.commit(result, window, cx);
572    }
573
574    /// Put the view map back the way an add found it, for one that placed
575    /// nothing. `previous` is what [`HashMap::insert`] handed back.
576    fn restore_registration(&mut self, id: PanelId, previous: Option<Arc<dyn PanelView>>) {
577        match previous {
578            Some(view) => self.panels.insert(id, view),
579            None => self.panels.remove(&id),
580        };
581    }
582
583    /// Remove a panel from wherever it lives, telling it that it was removed.
584    pub fn remove_panel<P: Panel>(
585        &mut self,
586        panel: Entity<P>,
587        window: &mut Window,
588        cx: &mut Context<Self>,
589    ) {
590        self.remove_panel_id(PanelId::from(panel.entity_id()), window, cx);
591    }
592
593    /// Move a panel to a new home. The panel never leaves the dock, so it is
594    /// never told it was removed.
595    pub fn move_panel(
596        &mut self,
597        panel: PanelId,
598        target: InsertTarget,
599        window: &mut Window,
600        cx: &mut Context<Self>,
601    ) {
602        let Some(destination) = self.placement_of_node(target_node(&target)) else {
603            return;
604        };
605        let source = self.placement_of_panel(panel);
606
607        // A split target divides an existing slot, so the tree needs real
608        // pixels to divide.
609        if matches!(target, InsertTarget::Split { .. }) {
610            self.adopt_measured_sizes(destination, cx);
611        }
612
613        // Read what the source group last told the panel *before* the edit,
614        // so the destination can be seeded with it. Without this a panel
615        // dragged between groups while displayed is told `true` twice.
616        let was_active = self
617            .layout(source.unwrap_or(destination))
618            .and_then(|tree| tree.find_panel_node(panel))
619            .and_then(|node| self.groups.get(&node))
620            .and_then(|cached| cached.entity.read(cx).last_notified_active(panel));
621
622        let changed = match source {
623            Some(source) if source == destination => {
624                let Some(tree) = self.tree_mut(destination) else {
625                    return;
626                };
627                tree.move_panel(panel, target).changed()
628            }
629            source => {
630                // Across trees a move is a detach plus an insert. The detach's
631                // `removed_panels` is deliberately dropped on the floor: the
632                // panel is still in the dock, so it must not hear `on_removed`.
633                //
634                // Both halves are committed on, not just the insert. A target
635                // whose node kind does not match the insert is a silent no-op
636                // in `apply_insert`, and committing on the insert alone would
637                // early-return with the panel already gone from the source —
638                // stranded in `self.panels`, belonging to no tree, for the
639                // next reconcile to prune and destroy.
640                let detached = source
641                    .and_then(|source| self.tree_mut(source))
642                    .is_some_and(|tree| tree.remove_panel(panel).changed());
643                let Some(tree) = self.tree_mut(destination) else {
644                    return;
645                };
646                let inserted = tree.insert_panel(panel, target).changed();
647                detached || inserted
648            }
649        };
650
651        self.commit_changed(changed, window, cx);
652
653        if let Some(active) = was_active {
654            if let Some(cached) = self
655                .layout(destination)
656                .and_then(|tree| tree.find_panel_node(panel))
657                .and_then(|node| self.groups.get(&node))
658            {
659                let group = cached.entity.clone();
660                group.update(cx, |group, _| group.seed_active(panel, active));
661            }
662        }
663    }
664
665    /// Put `panel` in a new tab group beside `node`.
666    pub fn split_at(
667        &mut self,
668        node: NodeId,
669        panel: PanelId,
670        placement: Placement,
671        window: &mut Window,
672        cx: &mut Context<Self>,
673    ) {
674        let Some(region) = self.placement_of_node(node) else {
675            return;
676        };
677        self.adopt_measured_sizes(region, cx);
678        let Some(tree) = self.tree_mut(region) else {
679            return;
680        };
681        let result = tree.split(node, panel, placement, None);
682        self.commit(result, window, cx);
683    }
684
685    fn remove_panel_id(&mut self, panel: PanelId, window: &mut Window, cx: &mut Context<Self>) {
686        let Some(region) = self.placement_of_panel(panel) else {
687            return;
688        };
689        let Some(tree) = self.tree_mut(region) else {
690            return;
691        };
692        let result = tree.remove_panel(panel);
693        self.commit(result, window, cx);
694    }
695}
696
697/// Zooming.
698///
699/// There is no `set_zoomed_in(panel)` here. A zoom is a container's own act:
700/// only the container knows whether its displayed panel is zoomable, and only
701/// the container can tell that panel it was zoomed. So the way in is
702/// [`TabGroupContext::toggle_zoom`](super::TabGroupContext::toggle_zoom) or
703/// [`TileContext::toggle_zoom`](super::TileContext::toggle_zoom) — a skin has
704/// one of those wherever it draws a zoom control — or
705/// [`Self::set_zoomed_in`] by node, which delegates to the same place. The
706/// area then installs the container that reported it.
707impl DockArea {
708    /// Zoom the tab group at `node` in, as if its own zoom control had been
709    /// used.
710    ///
711    /// Nothing happens for a node that is not a live tab group, or when the
712    /// group refuses — the group is the one that knows.
713    pub fn set_zoomed_in(&mut self, node: NodeId, window: &mut Window, cx: &mut Context<Self>) {
714        self.set_zoom(Some(Zoomed::Group(node)), window, cx);
715    }
716
717    /// Clear the zoom, putting the zoomed container's own flag back with it.
718    ///
719    /// A container toggles its zoom itself and only reports it, so an area
720    /// that dropped the view without telling the container would leave it
721    /// believing it still fills the dock — and a zoomed group refuses drops.
722    pub fn set_zoomed_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
723        self.set_zoom(None, window, cx);
724    }
725
726    pub fn is_zoomed(&self) -> bool {
727        self.zoomed.is_some()
728    }
729
730    /// The tab group filling the area, if a group is what is zoomed.
731    pub fn zoomed_group(&self) -> Option<NodeId> {
732        match self.zoomed {
733            Some(Zoomed::Group(node)) => Some(node),
734            _ => None,
735        }
736    }
737
738    /// The tile filling the area, if a tile is what is zoomed.
739    pub fn zoomed_tile(&self) -> Option<PanelId> {
740        match self.zoomed {
741            Some(Zoomed::Tile { panel, .. }) => Some(panel),
742            _ => None,
743        }
744    }
745
746    /// The one place `self.zoomed` is written.
747    ///
748    /// Every write drives the container's own flag through the same call, and
749    /// the new target is recorded only if that container accepted it. So the
750    /// area cannot show a container the container does not think is zoomed,
751    /// and cannot leave a container flagged zoomed while showing something
752    /// else — the split state a group would carry as a permanent lock.
753    fn set_zoom(&mut self, zoomed: Option<Zoomed>, window: &mut Window, cx: &mut Context<Self>) {
754        if self.zoomed == zoomed {
755            return;
756        }
757
758        if let Some(previous) = self.zoomed {
759            self.drive_zoom(previous, false, window, cx);
760        }
761        let accepted = match zoomed {
762            Some(next) => self.drive_zoom(next, true, window, cx).then_some(next),
763            None => None,
764        };
765        self.zoomed = accepted;
766        cx.notify();
767    }
768
769    /// Ask one container to zoom in or out, and report whether it now agrees.
770    ///
771    /// A container that has already been left out of the cache — its node is
772    /// gone from the tree — has nothing to say and nothing to put back, so it
773    /// answers `false` and a zoom naming it is never installed.
774    fn drive_zoom(
775        &mut self,
776        zoomed: Zoomed,
777        zoom_in: bool,
778        window: &mut Window,
779        cx: &mut Context<Self>,
780    ) -> bool {
781        match zoomed {
782            Zoomed::Group(node) => {
783                let Some(group) = self.groups.get(&node).map(|cached| cached.entity.clone()) else {
784                    return false;
785                };
786                group.update(cx, |group, cx| {
787                    group.set_zoomed(zoom_in, window, cx);
788                    group.is_zoomed() == zoom_in
789                })
790            }
791            Zoomed::Tile { node, panel } => {
792                let Some(canvas) = self.tiles.get(&node).map(|cached| cached.entity.clone()) else {
793                    return false;
794                };
795                canvas.update(cx, |canvas, cx| {
796                    canvas.set_zoomed(zoom_in.then_some(panel), window, cx);
797                    canvas.zoomed_tile() == zoom_in.then_some(panel)
798                })
799            }
800        }
801    }
802
803    /// The container that fills the area when something is zoomed.
804    ///
805    /// The container, not the panel inside it: this is what keeps a zoomed
806    /// group's tab bar and a zoomed tile's chrome on screen.
807    fn zoomed_view(&self) -> Option<AnyView> {
808        match self.zoomed? {
809            Zoomed::Group(node) => Some(self.groups.get(&node)?.entity.clone().into()),
810            Zoomed::Tile { node, .. } => Some(self.tiles.get(&node)?.entity.clone().into()),
811        }
812    }
813}
814
815/// Persistence.
816impl DockArea {
817    /// Read a persisted layout, rebuilding every panel through
818    /// [`PanelRegistry`]. A panel this build does not know about becomes a
819    /// placeholder that carries the original [`PanelState`] forward, so the
820    /// next save does not erase it.
821    pub fn load(
822        &mut self,
823        state: DockAreaState,
824        window: &mut Window,
825        cx: &mut Context<Self>,
826    ) -> Result<()> {
827        self.version = state.version;
828        self.zoomed = None;
829        // Nothing in the old layout survives a load, so the caches are
830        // emptied rather than reconciled: every node id in the new trees is
831        // freshly minted and would miss the old cache anyway.
832        self.groups.clear();
833        self.splits.clear();
834        self.tiles.clear();
835        self.docks.clear();
836        // `self.panels` is deliberately *not* cleared: leaving the outgoing
837        // panels in it lets `reconcile` prune them, which is what tells them
838        // they were removed.
839
840        let dock_area = self.this.clone();
841        let renderer = self.renderer.clone();
842        let mut built = Vec::new();
843        self.center = {
844            let mut builder = RegistryPanelBuilder {
845                dock_area: dock_area.clone(),
846                renderer: renderer.clone(),
847                built: &mut built,
848                window,
849                cx,
850            };
851            PaneTree::from_state(&state.center, RootKind::Split, &mut builder)
852        };
853
854        for dock_state in [state.left_dock, state.right_dock, state.bottom_dock]
855            .into_iter()
856            .flatten()
857        {
858            let tree = {
859                let mut builder = RegistryPanelBuilder {
860                    dock_area: dock_area.clone(),
861                    renderer: renderer.clone(),
862                    built: &mut built,
863                    window,
864                    cx,
865                };
866                PaneTree::from_state(dock_state.panel(), RootKind::Any, &mut builder)
867            };
868            let mut dock = Dock::new(dock_state.size());
869            dock.set_open(dock_state.open());
870            self.docks
871                .insert(dock_state.placement(), DockRegion { tree, dock });
872        }
873
874        self.panels.extend(built);
875        self.reconcile(window, cx);
876        cx.emit(DockEvent::LayoutChanged);
877        Ok(())
878    }
879
880    /// Write the layout out.
881    ///
882    /// Slot sizes are resolved to concrete pixels first. The tree represents
883    /// an unconstrained slot as `None` and the writer emits `0.0` for it,
884    /// which *this* reader maps back to `None` — but an older build has no
885    /// notion of the sentinel and would construct a real zero-pixel panel from
886    /// it. Preference order is the split's measured size, then the tree's own
887    /// size, then [`PANEL_MIN_SIZE`] for a slot nothing has ever measured.
888    ///
889    /// The measurement wins because the tree does not track every change to
890    /// it. `ResizableState` only emits `Resized` from a finished drag, so that
891    /// is all the subscription in [`Self::split_entity`] writes back;
892    /// `adjust_to_container_size` rescales every slot silently on each window
893    /// resize, insert and remove. Preferring the tree would persist load-time
894    /// or last-drag pixels after a window resize, and worse, mix them: a slot
895    /// left `None` by a later insert would be filled from the current
896    /// measurement while its untouched siblings kept file-era numbers, so the
897    /// written ratio would match neither the file nor the screen. Reading the
898    /// measurement for every slot of a split writes one internally consistent
899    /// set, which is what the old `StackPanel::dump` did.
900    pub fn dump(&self, cx: &App) -> DockAreaState {
901        let source = LivePanels::new(&self.panels, cx);
902
903        DockAreaState {
904            version: self.version,
905            center: self.resolved_tree(&self.center, cx).to_state(&source),
906            left_dock: self.dump_dock(DockPlacement::Left, &source, cx),
907            right_dock: self.dump_dock(DockPlacement::Right, &source, cx),
908            bottom_dock: self.dump_dock(DockPlacement::Bottom, &source, cx),
909        }
910    }
911
912    fn dump_dock(
913        &self,
914        placement: DockPlacement,
915        source: &LivePanels<'_>,
916        cx: &App,
917    ) -> Option<DockState> {
918        let pane = self.docks.get(&placement)?;
919        Some(DockState::new(
920            self.resolved_tree(&pane.tree, cx).to_state(source),
921            placement,
922            pane.dock.size(),
923            pane.dock.is_open(),
924        ))
925    }
926
927    fn resolved_tree(&self, tree: &PaneTree, cx: &App) -> PaneTree {
928        let mut tree = tree.clone();
929        self.resolve_sizes(tree.root_mut(), cx);
930        tree
931    }
932
933    fn resolve_sizes(&self, node: &mut PaneNode, cx: &App) {
934        let measured = self
935            .splits
936            .get(&node.id())
937            .map(|cached| cached.entity.read(cx).sizes().clone())
938            .unwrap_or_default();
939
940        let NodeKind::Split {
941            children, sizes, ..
942        } = node.kind_mut()
943        else {
944            return;
945        };
946
947        for (ix, size) in sizes.iter_mut().enumerate() {
948            // A slot already holding zero is exactly as unsafe as an absent
949            // one: it is the same byte in the file and the same zero-pixel
950            // panel in an older build. So both the measurement and the stored
951            // value have to clear zero before they can be written.
952            let on_screen = measured.get(ix).copied().filter(|size| *size > px(0.));
953            let stored = (*size).filter(|size| *size > px(0.));
954            *size = Some(on_screen.or(stored).unwrap_or(PANEL_MIN_SIZE));
955        }
956
957        for child in children.iter_mut() {
958            self.resolve_sizes(child, cx);
959        }
960    }
961}
962
963/// Reconciliation.
964impl DockArea {
965    /// Apply one edit: bring the entity cache back in line and say so.
966    ///
967    /// `on_removed` is not fired from `EditResult::removed_panels` here.
968    /// [`Self::reconcile`] fires it instead, from the panels it prunes: that
969    /// is the same set for a plain removal, and it also covers the panels a
970    /// wholesale `set_center`, `set_dock`, `remove_dock` or `load` displaces,
971    /// which no `EditResult` describes at all. It is also the safer default —
972    /// a caller cannot forget a list it does not pass.
973    fn commit(&mut self, result: EditResult, window: &mut Window, cx: &mut Context<Self>) {
974        self.commit_changed(result.changed(), window, cx);
975    }
976
977    /// [`Self::commit`] for an edit that took more than one `EditResult`.
978    fn commit_changed(&mut self, changed: bool, window: &mut Window, cx: &mut Context<Self>) {
979        if !changed {
980            return;
981        }
982
983        self.reconcile(window, cx);
984        cx.emit(DockEvent::LayoutChanged);
985    }
986
987    /// Bring the entity cache in line with the trees.
988    ///
989    /// Because `NodeId` survives every edit and every normalization rule, a
990    /// steady-state pass creates and drops nothing; only genuinely new or dead
991    /// containers churn. That is what keeps a drag from resetting the state of
992    /// panels it did not touch.
993    fn reconcile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
994        // Planned first, applied second: the plan borrows the trees, and
995        // applying it needs `&mut self` to fill the caches.
996        let mut plans = Vec::new();
997        plan_tree(&self.center, false, self.locked, &mut plans);
998        for pane in self.docks.values() {
999            plan_tree(&pane.tree, !pane.dock.is_open(), self.locked, &mut plans);
1000        }
1001
1002        // Sets rather than vectors: these are membership tests, run once per
1003        // cached entity and once per live panel, and a drag reaches this path
1004        // on every mouse move.
1005        let mut live_nodes = HashSet::with_capacity(plans.len());
1006        let mut live_panels: HashSet<PanelId> = HashSet::new();
1007
1008        for plan in plans {
1009            live_nodes.insert(plan.node());
1010            match plan {
1011                ContainerPlan::Split {
1012                    node,
1013                    axis,
1014                    children,
1015                    sizes,
1016                } => {
1017                    let state = self.split_entity(node, cx);
1018                    let (previous, adopted) = self
1019                        .splits
1020                        .get(&node)
1021                        .map(|cached| (cached.children.clone(), cached.sizes.clone()))
1022                        .unwrap_or_default();
1023                    // Only a split the edit changed is handed anything. The
1024                    // state of every other split may legitimately disagree
1025                    // with its tree — a window resize rescales it silently —
1026                    // and pushing the tree's sizes back would move slots the
1027                    // edit never named. For a slot the tree leaves `None`, it
1028                    // would also un-pin the measurement the first layout pass
1029                    // resolved it to, and the whole split re-flexes.
1030                    if previous != children || adopted != sizes {
1031                        state.update(cx, |state, cx| {
1032                            sync_split_panels(state, &previous, &children, &sizes, cx);
1033                            state.sync_panels_count(axis, children.len(), cx);
1034                            // The tree is authoritative on how space divides,
1035                            // so its sizes land last — `insert_panel`
1036                            // renormalizes everything it touches, which would
1037                            // otherwise undo the share an edit just decided.
1038                            //
1039                            // What the tree decides is the *share*, not the
1040                            // pixel count. The file holds absolute pixels
1041                            // measured in whatever window last saved it, so
1042                            // restoring into a different one leaves their
1043                            // total off the container — and
1044                            // `adjust_to_container_size` rescales the state to
1045                            // the container on the very next pass. Adopting
1046                            // the raw numbers would re-assert the stale total,
1047                            // so hand over the share instead and both sides
1048                            // already agree.
1049                            state.adopt_sizes(&scale_sizes_to(state.container_size(), &sizes), cx);
1050                        });
1051                    }
1052                    if let Some(cached) = self.splits.get_mut(&node) {
1053                        cached.children = children;
1054                        cached.sizes = sizes;
1055                    }
1056                }
1057                ContainerPlan::Group {
1058                    node,
1059                    panels,
1060                    active_ix,
1061                    constraints,
1062                } => {
1063                    live_panels.extend(panels.iter().copied());
1064                    let views = self.views_of(&panels);
1065                    let group = self.group_entity(node, window, cx);
1066                    group.update(cx, |group, cx| {
1067                        // Every group must be told this. A group nobody has
1068                        // constrained stays `sealed()` and silently declines
1069                        // drags, drops and closes.
1070                        group.set_constraints(constraints, window, cx);
1071                        group.sync_from_tree(views, active_ix, window, cx);
1072                    });
1073                }
1074                ContainerPlan::Tiles { node, tiles } => {
1075                    live_panels.extend(tiles.iter().map(|(panel, _, _)| *panel));
1076                    let mirrored = tiles
1077                        .iter()
1078                        .filter_map(|(panel, bounds, z_index)| {
1079                            self.panels
1080                                .get(panel)
1081                                .map(|view| (view.clone(), *bounds, *z_index))
1082                        })
1083                        .collect();
1084                    let canvas = self.tiles_entity(node, window, cx);
1085                    canvas.update(cx, |canvas, cx| canvas.sync_from_tree(mirrored, cx));
1086                }
1087            }
1088        }
1089
1090        self.groups.retain(|node, _| live_nodes.contains(node));
1091        self.splits.retain(|node, _| live_nodes.contains(node));
1092        self.tiles.retain(|node, _| live_nodes.contains(node));
1093
1094        let departed: Vec<Arc<dyn PanelView>> = self
1095            .panels
1096            .iter()
1097            .filter(|(panel, _)| !live_panels.contains(panel))
1098            .map(|(_, view)| view.clone())
1099            .collect();
1100        self.panels.retain(|panel, _| live_panels.contains(panel));
1101
1102        // A zoomed container fills the whole area, so one that has just left
1103        // the dock would otherwise keep filling it with nothing behind it.
1104        //
1105        // It is the container going away that ends the zoom, not a panel:
1106        // a group survives its displayed panel closing, and the next tab
1107        // takes over, still zoomed. The old `TabPanel::remove_panel` instead
1108        // emitted `ZoomOut` on every removal, which cleared the dock's zoom
1109        // even for a panel in some other tab panel entirely — and left the
1110        // zoomed `TabPanel` still flagged zoomed while the dock was not.
1111        let zoom_survives = match self.zoomed {
1112            Some(Zoomed::Group(node)) => self.groups.contains_key(&node),
1113            Some(Zoomed::Tile { node, panel }) => {
1114                self.tiles.contains_key(&node) && self.panels.contains_key(&panel)
1115            }
1116            None => true,
1117        };
1118        if !zoom_survives {
1119            self.set_zoom(None, window, cx);
1120        }
1121
1122        cx.notify();
1123        // Last, so a panel reacting to this sees a dock that already agrees
1124        // with its trees.
1125        for view in departed {
1126            view.on_removed(window, cx);
1127        }
1128    }
1129
1130    /// The views for a group's panel ids, in tab order.
1131    fn views_of(&self, panels: &[PanelId]) -> Vec<Arc<dyn PanelView>> {
1132        debug_assert!(
1133            panels.iter().all(|panel| self.panels.contains_key(panel)),
1134            "every panel in a tree must have a live view; a missing one would \
1135             silently shift the group's active index"
1136        );
1137        panels
1138            .iter()
1139            .filter_map(|panel| self.panels.get(panel).cloned())
1140            .collect()
1141    }
1142
1143    fn group_entity(
1144        &mut self,
1145        node: NodeId,
1146        window: &mut Window,
1147        cx: &mut Context<Self>,
1148    ) -> Entity<TabGroup> {
1149        if let Some(cached) = self.groups.get(&node) {
1150            return cached.entity.clone();
1151        }
1152
1153        let renderer = self.renderer.tab_group_renderer();
1154        let entity = cx.new(|cx| TabGroup::new(node, window, cx).with_renderer(renderer));
1155        let subscription = cx.subscribe_in(&entity, window, Self::on_tab_group_event);
1156        self.groups.insert(
1157            node,
1158            Cached {
1159                entity: entity.clone(),
1160                _subscription: subscription,
1161            },
1162        );
1163        entity
1164    }
1165
1166    fn tiles_entity(
1167        &mut self,
1168        node: NodeId,
1169        window: &mut Window,
1170        cx: &mut Context<Self>,
1171    ) -> Entity<TilesState> {
1172        if let Some(cached) = self.tiles.get(&node) {
1173            return cached.entity.clone();
1174        }
1175
1176        let renderer = self.renderer.tiles_renderer();
1177        let entity = cx.new(|cx| TilesState::new(node, window, cx).with_renderer(renderer));
1178        let subscription = cx.subscribe_in(&entity, window, Self::on_tiles_event);
1179        self.tiles.insert(
1180            node,
1181            Cached {
1182                entity: entity.clone(),
1183                _subscription: subscription,
1184            },
1185        );
1186        entity
1187    }
1188
1189    fn split_entity(&mut self, node: NodeId, cx: &mut Context<Self>) -> Entity<ResizableState> {
1190        if let Some(cached) = self.splits.get(&node) {
1191            return cached.entity.clone();
1192        }
1193
1194        let entity = cx.new(|_| ResizableState::default());
1195        // A drag on a resize handle changes only the measured sizes. Writing
1196        // them straight back keeps the tree describing the layout the user
1197        // arranged, which is what a later insert or removal scales from and
1198        // what a region with no live split entity is dumped from.
1199        //
1200        // It does not make the tree authoritative on slot sizes generally, and
1201        // `dump` does not treat it as such: only a finished drag arrives here,
1202        // while `adjust_to_container_size` rewrites the measurements silently
1203        // on every window resize. See [`Self::dump`].
1204        let subscription =
1205            cx.subscribe(&entity, move |this, state, _: &ResizablePanelEvent, cx| {
1206                let sizes: Vec<Option<Pixels>> = state
1207                    .read(cx)
1208                    .sizes()
1209                    .iter()
1210                    .map(|size| Some(*size))
1211                    .collect();
1212                let Some(region) = this.placement_of_node(node) else {
1213                    return;
1214                };
1215                let Some(tree) = this.tree_mut(region) else {
1216                    return;
1217                };
1218                if tree.set_sizes(node, sizes.clone()).changed() {
1219                    cx.emit(DockEvent::LayoutChanged);
1220                }
1221                // The state is where these came from, so the next reconcile
1222                // has nothing to hand it.
1223                if let Some(cached) = this.splits.get_mut(&node) {
1224                    cached.sizes = sizes;
1225                }
1226            });
1227        self.splits.insert(
1228            node,
1229            CachedSplit {
1230                entity: entity.clone(),
1231                children: Vec::new(),
1232                sizes: Vec::new(),
1233                _subscription: subscription,
1234            },
1235        );
1236        entity
1237    }
1238}
1239
1240/// Intents arriving from the containers.
1241impl DockArea {
1242    fn on_tab_group_event(
1243        &mut self,
1244        group: &Entity<TabGroup>,
1245        event: &TabGroupEvent,
1246        window: &mut Window,
1247        cx: &mut Context<Self>,
1248    ) {
1249        match event {
1250            TabGroupEvent::Drop { panel, target, .. } => {
1251                self.move_panel(*panel, *target, window, cx)
1252            }
1253            TabGroupEvent::DragDrop { item, target } => cx.emit(DockEvent::DragDrop {
1254                item: item.clone(),
1255                target: target.clone(),
1256            }),
1257            TabGroupEvent::ClosePanel { panel } => self.remove_panel_id(*panel, window, cx),
1258            TabGroupEvent::ActiveChanged { ix } => {
1259                let node = group.read(cx).node();
1260                let Some(region) = self.placement_of_node(node) else {
1261                    return;
1262                };
1263                let Some(tree) = self.tree_mut(region) else {
1264                    return;
1265                };
1266                let result = tree.set_active(node, *ix);
1267                self.commit(result, window, cx);
1268            }
1269            TabGroupEvent::ZoomIn => {
1270                let node = group.read(cx).node();
1271                self.set_zoom(Some(Zoomed::Group(node)), window, cx);
1272            }
1273            // Only the group that is actually on screen can give the dock
1274            // back. A group told to zoom out to make room for another one
1275            // reports it too, and that report must not undo the zoom that
1276            // replaced it.
1277            TabGroupEvent::ZoomOut => {
1278                let node = group.read(cx).node();
1279                if self.zoomed == Some(Zoomed::Group(node)) {
1280                    self.set_zoom(None, window, cx);
1281                }
1282            }
1283        }
1284    }
1285
1286    fn on_tiles_event(
1287        &mut self,
1288        canvas: &Entity<TilesState>,
1289        event: &TilesEvent,
1290        window: &mut Window,
1291        cx: &mut Context<Self>,
1292    ) {
1293        let node = canvas.read(cx).node();
1294        let Some(region) = self.placement_of_node(node) else {
1295            return;
1296        };
1297
1298        match event {
1299            TilesEvent::BoundsChanged { panel, bounds } => {
1300                let Some(tree) = self.tree_mut(region) else {
1301                    return;
1302                };
1303                let result = tree.set_tile_bounds(*panel, *bounds);
1304                self.commit(result, window, cx);
1305            }
1306            TilesEvent::BringToFront { panel } => {
1307                let Some(tree) = self.tree_mut(region) else {
1308                    return;
1309                };
1310                let result = tree.bring_to_front(*panel);
1311                self.commit(result, window, cx);
1312            }
1313            TilesEvent::ClosePanel { panel } => self.remove_panel_id(*panel, window, cx),
1314            TilesEvent::DragDrop { item } => cx.emit(DockEvent::DragDrop {
1315                item: item.clone(),
1316                target: DropTarget::Canvas,
1317            }),
1318            TilesEvent::ZoomIn { panel } => {
1319                self.set_zoom(
1320                    Some(Zoomed::Tile {
1321                        node,
1322                        panel: *panel,
1323                    }),
1324                    window,
1325                    cx,
1326                );
1327            }
1328            // As with a tab group: only the canvas actually on screen can
1329            // give the dock back.
1330            TilesEvent::ZoomOut => {
1331                if matches!(self.zoomed, Some(Zoomed::Tile { node: zoomed, .. }) if zoomed == node)
1332                {
1333                    self.set_zoom(None, window, cx);
1334                }
1335            }
1336        }
1337    }
1338}
1339
1340/// Region lookup.
1341impl DockArea {
1342    /// Feed every split's measured size back into the tree before an edit
1343    /// that has to divide space.
1344    ///
1345    /// Done here rather than continuously: the tree is the record of what the
1346    /// user arranged, and rewriting it on every layout pass would let a
1347    /// transient window size become the stored layout.
1348    fn adopt_measured_sizes(&mut self, placement: DockPlacement, cx: &App) {
1349        let measured: HashMap<NodeId, Vec<Pixels>> = self
1350            .splits
1351            .iter()
1352            // Only splits that have actually been laid out. A split created
1353            // earlier in this same edit has a zero container and its `sizes`
1354            // are whatever `insert_panel` left behind — adopting those would
1355            // freeze a placeholder ratio into the tree, and every later edit
1356            // would divide space according to it.
1357            .filter(|(_, cached)| cached.entity.read(cx).container_size() > Pixels::ZERO)
1358            .map(|(node, cached)| (*node, cached.entity.read(cx).sizes().clone()))
1359            .collect();
1360
1361        if let Some(tree) = self.tree_mut(placement) {
1362            tree.adopt_measured_sizes(&measured);
1363        }
1364    }
1365
1366    fn tree_mut(&mut self, placement: DockPlacement) -> Option<&mut PaneTree> {
1367        match placement {
1368            DockPlacement::Center => Some(&mut self.center),
1369            _ => self.docks.get_mut(&placement).map(|pane| &mut pane.tree),
1370        }
1371    }
1372
1373    /// Which region a container belongs to. Unambiguous because `NodeId`s are
1374    /// allocated globally rather than per tree.
1375    fn placement_of_node(&self, node: NodeId) -> Option<DockPlacement> {
1376        if self.center.find_node(node).is_some() {
1377            return Some(DockPlacement::Center);
1378        }
1379        self.docks
1380            .iter()
1381            .find(|(_, pane)| pane.tree.find_node(node).is_some())
1382            .map(|(placement, _)| *placement)
1383    }
1384
1385    fn placement_of_panel(&self, panel: PanelId) -> Option<DockPlacement> {
1386        if self.center.find_panel_node(panel).is_some() {
1387            return Some(DockPlacement::Center);
1388        }
1389        self.docks
1390            .iter()
1391            .find(|(_, pane)| pane.tree.find_panel_node(panel).is_some())
1392            .map(|(placement, _)| *placement)
1393    }
1394
1395    /// Resize one dock from a pointer position, clamped so neither this dock
1396    /// nor the one opposite is squeezed below its minimum.
1397    fn resize_dock(
1398        &mut self,
1399        placement: DockPlacement,
1400        pointer: Point<Pixels>,
1401        cx: &mut Context<Self>,
1402    ) {
1403        let opposite = match placement {
1404            DockPlacement::Left => self.dock_size(DockPlacement::Right),
1405            DockPlacement::Right => self.dock_size(DockPlacement::Left),
1406            _ => None,
1407        };
1408        let sizing = DockSizing::new(placement)
1409            .with_area_bounds(self.bounds)
1410            .with_opposite_dock_size(opposite.unwrap_or(px(0.)));
1411        let size = sizing.clamp(sizing.size_from_pointer(pointer));
1412
1413        if let Some(pane) = self.docks.get_mut(&placement) {
1414            pane.dock.set_size(size);
1415            cx.notify();
1416        }
1417    }
1418}
1419
1420/// Rendering.
1421impl DockArea {
1422    /// Lower one container to an element.
1423    fn render_node(&self, node: &PaneNode, window: &mut Window, cx: &mut App) -> AnyElement {
1424        match node.kind() {
1425            PaneRef::Split {
1426                axis,
1427                children,
1428                sizes,
1429            } => {
1430                let group = match axis {
1431                    Axis::Horizontal => h_resizable(("dock-split", node.id().as_u64())),
1432                    Axis::Vertical => v_resizable(("dock-split", node.id().as_u64())),
1433                };
1434                // A container whose every panel is hidden must not keep
1435                // occupying its slot. No renderer hook could supply this:
1436                // only the area can see the panels behind a node.
1437                let shown: Vec<bool> = children
1438                    .iter()
1439                    .map(|child| self.is_node_visible(child, cx))
1440                    .collect();
1441                // The slot that absorbs the leftover has to be one that is
1442                // actually drawn. A hidden slot renders nothing and grows
1443                // nothing, so making it the flexible one leaves every drawn
1444                // slot rigid and the split ends short of its container — the
1445                // empty strip this picks the *last shown* slot to avoid.
1446                let grows = shown.iter().rposition(|shown| *shown);
1447                let panels: Vec<_> = children
1448                    .iter()
1449                    .zip(sizes.iter())
1450                    .enumerate()
1451                    .map(|(ix, (child, size))| {
1452                        resizable_panel()
1453                            .visible(shown[ix])
1454                            .child(self.render_node(child, window, cx))
1455                            // `flex_none` is what makes the size stick.
1456                            // `ResizablePanel` sets `flex_grow: 1` on itself,
1457                            // so a slot given a size would otherwise treat it
1458                            // as a flex-basis and still absorb an equal share
1459                            // of the leftover — a 200px sidebar rendering
1460                            // 1075px wide in a 1950px split.
1461                            // The growth slot absorbs container growth. A
1462                            // drag records every measured size as pixels; if
1463                            // all of them became `flex_none`, a later viewport
1464                            // resize would leave an empty strip after the
1465                            // split instead of keeping the Dock filled.
1466                            .when_some(*size, |panel, size| {
1467                                panel
1468                                    .size(size)
1469                                    .when(Some(ix) != grows, |panel| panel.flex_none())
1470                            })
1471                    })
1472                    .collect();
1473
1474                let group = group
1475                    .when_some(self.splits.get(&node.id()), |group, cached| {
1476                        group.with_state(&cached.entity)
1477                    })
1478                    .with_handle_appearance({
1479                        let renderer = self.renderer.clone();
1480                        Rc::new(move |handle, window, cx| {
1481                            renderer.render_split_handle(handle, window, cx)
1482                        })
1483                    })
1484                    .children(panels);
1485
1486                self.renderer
1487                    .split_frame(node.id(), axis, window, cx)
1488                    // A split frame with no size collapses: base puts it
1489                    // between a `resizable_panel` and the resizable group, and
1490                    // between `center_frame` and the centre's root split, and
1491                    // neither parent sizes it. `size_full` and `flex_1` are
1492                    // belt and braces -- either alone passes every case I could
1493                    // construct, so this does not depend on which one wins in a
1494                    // given parent.
1495                    .size_full()
1496                    .flex_1()
1497                    .min_h(px(0.))
1498                    .overflow_hidden()
1499                    .child(group)
1500                    .into_any_element()
1501            }
1502            PaneRef::Tabs { .. } => match self.groups.get(&node.id()) {
1503                Some(cached) => cached.entity.clone().into_any_element(),
1504                None => Empty.into_any_element(),
1505            },
1506            PaneRef::Tiles { .. } => match self.tiles.get(&node.id()) {
1507                Some(cached) => cached.entity.clone().into_any_element(),
1508                None => Empty.into_any_element(),
1509            },
1510        }
1511    }
1512
1513    /// Whether anything in this container is on screen.
1514    ///
1515    /// Mirrors the old `StackPanel::render`, which asked each slot's
1516    /// `TabPanel::visible` — "does this group hold any visible panel?" — and
1517    /// hid the slot when it did not.
1518    fn is_node_visible(&self, node: &PaneNode, cx: &App) -> bool {
1519        let panels = LivePanels::new(&self.panels, cx);
1520        match node.kind() {
1521            PaneRef::Split { children, .. } => {
1522                children.iter().any(|child| self.is_node_visible(child, cx))
1523            }
1524            PaneRef::Tabs { panels: ids, .. } => ids.iter().any(|panel| panels.is_visible(*panel)),
1525            PaneRef::Tiles { panels: tiles } => {
1526                tiles.iter().any(|tile| panels.is_visible(tile.panel()))
1527            }
1528        }
1529    }
1530
1531    fn render_dock(
1532        &self,
1533        placement: DockPlacement,
1534        window: &mut Window,
1535        cx: &mut App,
1536    ) -> Option<AnyElement> {
1537        let pane = self.docks.get(&placement)?;
1538        let dock = self.dock_context(placement, &pane.dock);
1539
1540        // A closed left or right dock takes no space at all; a closed bottom
1541        // dock keeps a strip so its tab bar stays clickable. Nothing is drawn
1542        // for a dock with no extent, and the renderer is not asked for chrome
1543        // nobody can see.
1544        let size = dock_extent(&dock);
1545        if size <= px(0.) {
1546            return Some(div().into_any_element());
1547        }
1548
1549        let content = self.render_node(pane.tree.root(), window, cx);
1550        // The box is applied here rather than left to the renderer, and that is
1551        // the whole point of it being here. A dock's extent along its own axis
1552        // is not presentation -- it is what makes the dock a column beside the
1553        // centre instead of a block in the flow below it -- and a renderer that
1554        // did not know to state it produced a dock with no width, every pane
1555        // inside it shrunk to its content. `render_dock` on the renderer is a
1556        // chrome hook, so a renderer that draws nothing at all still gets a
1557        // dock that is the right shape.
1558        let chrome = self.renderer.render_dock(&dock, content, window, cx);
1559        Some(dock_frame(&dock, size).child(chrome).into_any_element())
1560    }
1561
1562    fn dock_context(&self, placement: DockPlacement, dock: &Dock) -> DockContext {
1563        let area = self.this.clone();
1564
1565        DockContext {
1566            placement,
1567            size: dock.size(),
1568            open: dock.is_open(),
1569            collapsible: dock.is_collapsible(),
1570            on_toggle: {
1571                let area = area.clone();
1572                Rc::new(move |window, cx| {
1573                    _ = area.update(cx, |area, cx| area.toggle_dock(placement, window, cx));
1574                })
1575            },
1576            on_resize: Rc::new(move |pointer, _, cx| {
1577                _ = area.update(cx, |area, cx| area.resize_dock(placement, pointer, cx));
1578            }),
1579        }
1580    }
1581}
1582
1583impl EventEmitter<DockEvent> for DockArea {}
1584
1585impl Focusable for DockArea {
1586    fn focus_handle(&self, _: &App) -> FocusHandle {
1587        self.focus_handle.clone()
1588    }
1589}
1590
1591impl Render for DockArea {
1592    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1593        let area = cx.entity();
1594        let renderer = self.renderer.clone();
1595
1596        renderer
1597            .frame(window, cx)
1598            .test_support()
1599            // Structure, applied after the hook and not inside it. A dock area
1600            // lays its left dock, centre and right dock out in a row; a frame
1601            // that is not one stacks them down the window instead, which is
1602            // what every renderer that is not `DockSkin` used to get, because
1603            // the row lived in `DockSkin`'s override of this hook and the trait
1604            // default is a bare `div`.
1605            .relative()
1606            .size_full()
1607            .overflow_hidden()
1608            .flex()
1609            .flex_row()
1610            .on_prepaint(move |bounds, _, cx| {
1611                area.update(cx, |area, _| area.bounds = bounds);
1612            })
1613            .track_focus(&self.focus_handle)
1614            .map(|frame| match self.zoomed_view() {
1615                Some(view) => frame.child(view),
1616                None => frame
1617                    .when_some(
1618                        self.render_dock(DockPlacement::Left, window, cx),
1619                        ParentElement::child,
1620                    )
1621                    .child(
1622                        renderer
1623                            .center_frame(window, cx)
1624                            // Same reason as the frame above: the centre is
1625                            // whatever the side docks leave, in a column with
1626                            // the bottom dock. Without this it is neither, and
1627                            // shrinks to its content.
1628                            .flex()
1629                            .flex_1()
1630                            .flex_col()
1631                            .overflow_hidden()
1632                            .child(self.render_node(self.center.root(), window, cx))
1633                            .when_some(
1634                                self.render_dock(DockPlacement::Bottom, window, cx),
1635                                ParentElement::child,
1636                            ),
1637                    )
1638                    .when_some(
1639                        self.render_dock(DockPlacement::Right, window, cx),
1640                        ParentElement::child,
1641                    ),
1642            })
1643    }
1644}
1645
1646/// One container's worth of reconciliation work, snapshotted out of the tree.
1647enum ContainerPlan {
1648    Split {
1649        node: NodeId,
1650        axis: Axis,
1651        children: Vec<NodeId>,
1652        sizes: Vec<Option<Pixels>>,
1653    },
1654    Group {
1655        node: NodeId,
1656        panels: Vec<PanelId>,
1657        active_ix: usize,
1658        constraints: TabGroupConstraints,
1659    },
1660    Tiles {
1661        node: NodeId,
1662        tiles: Vec<(PanelId, Bounds<Pixels>, usize)>,
1663    },
1664}
1665
1666impl ContainerPlan {
1667    fn node(&self) -> NodeId {
1668        match self {
1669            Self::Split { node, .. } | Self::Group { node, .. } | Self::Tiles { node, .. } => *node,
1670        }
1671    }
1672}
1673
1674/// Re-express slot sizes as shares of `container`, keeping their proportions.
1675///
1676/// Returns them unchanged unless every slot is constrained and both the
1677/// container and the recorded total are usable: an unconstrained slot is laid
1678/// out by flex and takes the leftover, so scaling only the constrained ones
1679/// would move a divider nothing asked to move.
1680fn scale_sizes_to(container: Pixels, sizes: &[Option<Pixels>]) -> Vec<Option<Pixels>> {
1681    let total: f32 = sizes.iter().flatten().map(|size| size.as_f32()).sum();
1682    if container <= px(0.) || total <= 0. || sizes.iter().any(Option::is_none) {
1683        return sizes.to_vec();
1684    }
1685
1686    let scale = container.as_f32() / total;
1687    sizes
1688        .iter()
1689        .map(|size| size.map(|size| px(size.as_f32() * scale)))
1690        .collect()
1691}
1692
1693/// Bring one split's `ResizableState` panel list from `previous` to `next`,
1694/// inserting and removing at the exact index rather than at the tail.
1695///
1696/// `ResizableState` treats the size handed to `insert_panel` as an initial
1697/// value only; from then on `panels[ix].size` is authoritative. So an
1698/// append-and-truncate sync leaves every survivor of a non-tail removal
1699/// wearing the width of whoever used to sit at its index — which is the most
1700/// ordinary edit in the dock, a drag that empties a group, and it is carried
1701/// into the save file by `resolve_sizes`.
1702fn sync_split_panels(
1703    state: &mut ResizableState,
1704    previous: &[NodeId],
1705    next: &[NodeId],
1706    sizes: &[Option<Pixels>],
1707    cx: &mut Context<ResizableState>,
1708) {
1709    let mut current = previous.to_vec();
1710
1711    // Removals from the tail, so the indices ahead of each removal stay valid.
1712    for ix in (0..current.len()).rev() {
1713        if !next.contains(&current[ix]) {
1714            if ix < state.sizes().len() {
1715                state.remove_panel(ix, cx);
1716            }
1717            current.remove(ix);
1718        }
1719    }
1720
1721    for (ix, node) in next.iter().enumerate() {
1722        if current.get(ix) == Some(node) {
1723            continue;
1724        }
1725        let at = ix.min(state.sizes().len());
1726        // Passed through as-is. The tree already decided this slot's share
1727        // when the panel was inserted — a drop halves the neighbour it landed
1728        // beside — so there is nothing to compute here, and nothing that
1729        // depends on a container this state may not have measured yet.
1730        state.insert_panel(sizes.get(ix).copied().flatten(), Some(at), cx);
1731        current.insert(at.min(current.len()), *node);
1732    }
1733
1734    debug_assert_eq!(
1735        current, next,
1736        "the split's panel list must end up mirroring its children exactly; \
1737         a reordering edit would need its own case here"
1738    );
1739}
1740
1741fn plan_tree(tree: &PaneTree, collapsed: bool, locked: bool, out: &mut Vec<ContainerPlan>) {
1742    // The root has nothing beside it by definition.
1743    plan_node(tree.root(), true, collapsed, locked, out);
1744}
1745
1746fn plan_node(
1747    node: &PaneNode,
1748    alone: bool,
1749    collapsed: bool,
1750    locked: bool,
1751    out: &mut Vec<ContainerPlan>,
1752) {
1753    match node.kind() {
1754        PaneRef::Split {
1755            axis,
1756            children,
1757            sizes,
1758        } => {
1759            out.push(ContainerPlan::Split {
1760                node: node.id(),
1761                axis,
1762                children: children.iter().map(PaneNode::id).collect(),
1763                sizes: sizes.to_vec(),
1764            });
1765            let children_alone = children.len() <= 1;
1766            for child in children {
1767                plan_node(child, children_alone, collapsed, locked, out);
1768            }
1769        }
1770        PaneRef::Tabs { panels, active_ix } => out.push(ContainerPlan::Group {
1771            node: node.id(),
1772            panels: panels.to_vec(),
1773            active_ix,
1774            constraints: TabGroupConstraints::in_split(alone)
1775                .dock_locked(locked)
1776                .collapsed(collapsed),
1777        }),
1778        PaneRef::Tiles { panels } => out.push(ContainerPlan::Tiles {
1779            node: node.id(),
1780            tiles: panels
1781                .iter()
1782                .map(|tile| (tile.panel(), tile.bounds(), tile.z_index()))
1783                .collect(),
1784        }),
1785    }
1786}
1787
1788fn first_tab_group(node: &PaneNode) -> Option<NodeId> {
1789    match node.kind() {
1790        PaneRef::Tabs { .. } => Some(node.id()),
1791        PaneRef::Split { children, .. } => children.iter().find_map(first_tab_group),
1792        PaneRef::Tiles { .. } => None,
1793    }
1794}
1795
1796fn first_tiles_canvas(node: &PaneNode) -> Option<NodeId> {
1797    match node.kind() {
1798        PaneRef::Tiles { .. } => Some(node.id()),
1799        PaneRef::Split { children, .. } => children.iter().find_map(first_tiles_canvas),
1800        PaneRef::Tabs { .. } => None,
1801    }
1802}
1803
1804fn target_node(target: &InsertTarget) -> NodeId {
1805    match target {
1806        InsertTarget::Tabs { node, .. }
1807        | InsertTarget::Split { node, .. }
1808        | InsertTarget::Tile { node, .. } => *node,
1809    }
1810}
1811
1812/// Rebuilds panels out of persisted state through [`PanelRegistry`].
1813struct RegistryPanelBuilder<'a, 'w, 'c> {
1814    dock_area: WeakEntity<DockArea>,
1815    renderer: Rc<dyn DockAreaRenderer>,
1816    built: &'a mut Vec<(PanelId, Arc<dyn PanelView>)>,
1817    window: &'w mut Window,
1818    cx: &'c mut App,
1819}
1820
1821impl PanelBuilder for RegistryPanelBuilder<'_, '_, '_> {
1822    fn build(&mut self, state: &PanelState, info: &PanelInfo) -> PanelId {
1823        let context = PanelBuildContext::new(self.dock_area.clone(), state, info);
1824        let view =
1825            match PanelRegistry::build_panel(&state.panel_name, context, self.window, self.cx) {
1826                Some(view) => view,
1827                None => self
1828                    .renderer
1829                    .build_placeholder(state, self.window, self.cx)
1830                    .unwrap_or_else(|| {
1831                        Arc::new(self.cx.new(|cx| PlaceholderPanel::new(state.clone(), cx)))
1832                            as Arc<dyn PanelView>
1833                    }),
1834            };
1835
1836        let id = view.panel_id(self.cx);
1837        self.built.push((id, view));
1838        id
1839    }
1840}
1841
1842/// Stands in for a panel this build cannot construct.
1843///
1844/// It draws nothing — an "unknown panel" message is presentation and belongs
1845/// above this seam — but it keeps the original [`PanelState`] and hands it
1846/// back from [`Panel::dump`], so a layout written by a newer build survives a
1847/// load and save here instead of losing the panel.
1848struct PlaceholderPanel {
1849    state: PanelState,
1850    focus_handle: FocusHandle,
1851}
1852
1853impl PlaceholderPanel {
1854    fn new(state: PanelState, cx: &mut Context<Self>) -> Self {
1855        Self {
1856            state,
1857            focus_handle: cx.focus_handle(),
1858        }
1859    }
1860}
1861
1862impl Panel for PlaceholderPanel {
1863    fn panel_name(&self) -> &'static str {
1864        "InvalidPanel"
1865    }
1866
1867    fn dump(&self, _: &App) -> PanelState {
1868        self.state.clone()
1869    }
1870}
1871
1872impl EventEmitter<PanelEvent> for PlaceholderPanel {}
1873
1874impl Focusable for PlaceholderPanel {
1875    fn focus_handle(&self, _: &App) -> FocusHandle {
1876        self.focus_handle.clone()
1877    }
1878}
1879
1880impl Render for PlaceholderPanel {
1881    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1882        Empty
1883    }
1884}
1885
1886type DockToggleHandler = Rc<dyn Fn(&mut Window, &mut App)>;
1887type DockResizeHandler = Rc<dyn Fn(Point<Pixels>, &mut Window, &mut App)>;
1888
1889/// What a skin needs to draw one dock, and the callbacks it invokes rather
1890/// than reimplementing the open/close and clamping behavior.
1891#[derive(Clone)]
1892pub struct DockContext {
1893    placement: DockPlacement,
1894    size: Pixels,
1895    open: bool,
1896    collapsible: bool,
1897    on_toggle: DockToggleHandler,
1898    on_resize: DockResizeHandler,
1899}
1900
1901impl DockContext {
1902    pub fn placement(&self) -> DockPlacement {
1903        self.placement
1904    }
1905
1906    /// The dock's extent along its own axis: width for left/right, height for
1907    /// bottom.
1908    pub fn size(&self) -> Pixels {
1909        self.size
1910    }
1911
1912    pub fn is_open(&self) -> bool {
1913        self.open
1914    }
1915
1916    pub fn is_collapsible(&self) -> bool {
1917        self.collapsible
1918    }
1919
1920    pub fn toggle(&self, window: &mut Window, cx: &mut App) {
1921        (self.on_toggle)(window, cx);
1922    }
1923
1924    /// Resize from a pointer position in window coordinates. Base clamps it
1925    /// against the area bounds and the opposite dock.
1926    pub fn resize_to(&self, pointer: Point<Pixels>, window: &mut Window, cx: &mut App) {
1927        (self.on_resize)(pointer, window, cx);
1928    }
1929}
1930
1931/// A closed bottom dock keeps this much, so its tab bar stays clickable. A
1932/// closed side dock keeps nothing: there is no tab bar left to click at zero
1933/// width, and reopening it is the application's to offer.
1934pub const CLOSED_BOTTOM_STRIP: Pixels = px(29.);
1935
1936/// How much room a dock asks for along its own axis.
1937pub fn dock_extent(dock: &DockContext) -> Pixels {
1938    match (dock.is_open(), dock.placement()) {
1939        (true, _) => dock.size(),
1940        (false, DockPlacement::Bottom) => CLOSED_BOTTOM_STRIP,
1941        (false, _) => px(0.),
1942    }
1943}
1944
1945/// The box a dock occupies: its extent along its own axis, full across, and
1946/// held at that size rather than stretched by the row it sits in.
1947///
1948/// Structural, not decorative, which is why it is built here and not in a
1949/// renderer. See [`DockArea::render_dock`].
1950pub fn dock_frame(dock: &DockContext, size: Pixels) -> Div {
1951    div()
1952        .flex()
1953        .flex_none()
1954        .relative()
1955        .overflow_hidden()
1956        .map(|this| match dock.placement() {
1957            DockPlacement::Left | DockPlacement::Right => this.flex_row().h_full().w(size),
1958            DockPlacement::Bottom => this.w_full().h(size),
1959            // Base never builds a dock for the centre.
1960            DockPlacement::Center => this,
1961        })
1962}
1963
1964/// Appearance for the dock area. Base draws none of it.
1965///
1966/// The frame hooks return the element itself rather than wrapping one, for the
1967/// same reason [`TabGroupRenderer`]'s do: base tracks focus and records the
1968/// area bounds on the very element the skin styles.
1969///
1970/// There is no separate `render_resize_handle` hook. A handle needs to be
1971/// positioned against the dock it resizes, and positioning is the skin's; the
1972/// skin draws it inside [`Self::render_dock`] and drives it through
1973/// [`DockContext::resize_to`].
1974#[allow(unused_variables)]
1975pub trait DockAreaRenderer: 'static {
1976    /// The area's outer frame, which base records its bounds on.
1977    /// Appearance only. The area is laid out as a row around whatever this
1978    /// returns, because that is what makes a dock a column beside the centre
1979    /// rather than a block above it, and a renderer cannot be expected to know
1980    /// it had a row to declare.
1981    fn frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
1982        div().id("dock-area")
1983    }
1984
1985    /// One split container's frame, around base's resizable group.
1986    ///
1987    /// This one really is a wrapper, unlike the other frame hooks, and
1988    /// deliberately: base attaches nothing to it, so there is no hit area to
1989    /// separate from the painted area. It exists because the old `StackPanel`
1990    /// carried real appearance here — a background and an overflow clip — and
1991    /// without it a skin could style a dock and a tab group but nothing in
1992    /// between. `Stateful<Div>` rather than a plain one so the skin keeps a
1993    /// role, a tooltip, and scroll tracking.
1994    fn split_frame(
1995        &self,
1996        node: NodeId,
1997        axis: Axis,
1998        window: &mut Window,
1999        cx: &mut App,
2000    ) -> Stateful<Div> {
2001        div().id(("dock-split-frame", node.as_u64()))
2002    }
2003
2004    /// The column holding the center region and the bottom dock.
2005    /// Appearance only; see [`DockAreaRenderer::frame`]. The centre fills what
2006    /// the side docks leave and stacks with the bottom dock either way.
2007    fn center_frame(&self, window: &mut Window, cx: &mut App) -> Stateful<Div> {
2008        div().id("dock-area-center")
2009    }
2010
2011    /// The painted part of the divider between two slots of a split.
2012    ///
2013    /// `None` keeps base's own one-pixel line, so a skin that has no opinion
2014    /// about dividers implements nothing. The hit area, the cursor and the
2015    /// drag itself stay with base either way — this hook supplies appearance
2016    /// only, and is told the axis and whether the divider is being dragged.
2017    fn render_split_handle(
2018        &self,
2019        handle: &ResizeHandleContext,
2020        window: &mut Window,
2021        cx: &mut App,
2022    ) -> Option<AnyElement> {
2023        None
2024    }
2025
2026    /// One dock's chrome around its content: the title strip, the collapse
2027    /// affordance, and the resize handle.
2028    ///
2029    /// Chrome only. The dock's own box -- its extent along its own axis, and
2030    /// the `flex_none` that holds it there -- is applied by
2031    /// [`DockArea::render_dock`] around whatever this returns, so a renderer
2032    /// cannot misplace a dock by not knowing to size it, and the default here
2033    /// can be what it is: the content, undecorated.
2034    fn render_dock(
2035        &self,
2036        dock: &DockContext,
2037        content: AnyElement,
2038        window: &mut Window,
2039        cx: &mut App,
2040    ) -> AnyElement {
2041        content
2042    }
2043
2044    /// The stand-in for a panel this build cannot construct — one whose
2045    /// `panel_name` no [`PanelRegistry`] builder answers to. `None` takes
2046    /// base's own placeholder, which draws nothing.
2047    ///
2048    /// The hook exists because a placeholder cannot be wrapped after the
2049    /// fact: presentation reaches base only through the handle a panel is
2050    /// registered behind, so whoever creates the panel decides what it can
2051    /// draw. An "unknown panel" message is presentation, so the skin creates
2052    /// that panel or does without one.
2053    ///
2054    /// A placeholder is what gets written back out on the next save, so one
2055    /// supplied here should answer [`Panel::dump`] with `state` unchanged —
2056    /// otherwise saving after a load erases the panel it stood in for. Base's
2057    /// own placeholder does; nothing here can enforce it of a skin's.
2058    fn build_placeholder(
2059        &self,
2060        state: &PanelState,
2061        window: &mut Window,
2062        cx: &mut App,
2063    ) -> Option<Arc<dyn PanelView>> {
2064        None
2065    }
2066
2067    fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer>;
2068
2069    fn tiles_renderer(&self) -> Rc<dyn TilesRenderer>;
2070}
2071
2072/// The renderer an area starts with: the layout and nothing else.
2073struct BareDockArea;
2074
2075impl DockAreaRenderer for BareDockArea {
2076    fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
2077        Rc::new(BareTabGroup)
2078    }
2079
2080    fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> {
2081        Rc::new(BareTiles)
2082    }
2083}
2084
2085#[cfg(test)]
2086impl DockArea {
2087    /// Every cached container, as `(node, entity)`.
2088    ///
2089    /// Entity ids, not just node ids: node ids alone would compare equal even
2090    /// if every container entity had been torn down and rebuilt under the same
2091    /// key, which is exactly the failure the reconciliation contract exists to
2092    /// prevent.
2093    pub(crate) fn container_entity_ids(&self) -> Vec<(NodeId, gpui::EntityId)> {
2094        let mut ids: Vec<(NodeId, gpui::EntityId)> = self
2095            .groups
2096            .iter()
2097            .map(|(node, cached)| (*node, cached.entity.entity_id()))
2098            .chain(
2099                self.splits
2100                    .iter()
2101                    .map(|(node, cached)| (*node, cached.entity.entity_id())),
2102            )
2103            .chain(
2104                self.tiles
2105                    .iter()
2106                    .map(|(node, cached)| (*node, cached.entity.entity_id())),
2107            )
2108            .collect();
2109        ids.sort();
2110        ids
2111    }
2112}
2113
2114#[cfg(test)]
2115mod tests {
2116    use gpui::{TestAppContext, VisualTestContext};
2117
2118    use std::{
2119        cell::{Cell, RefCell},
2120        rc::Rc,
2121    };
2122
2123    use super::*;
2124    use crate::dock::test_support::{Log, PanelSignal, TestPanel, drain, drain_active, log_of};
2125    use crate::dock::{TabGroupContext, TileContext};
2126
2127    /// The file holds pixels measured in whatever window last saved it, so its
2128    /// total is off the container the layout is restored into.
2129    #[test]
2130    fn slot_sizes_are_re_expressed_as_shares_of_the_container() {
2131        let scaled = scale_sizes_to(px(800.), &[Some(px(300.)), Some(px(100.))]);
2132
2133        assert_eq!(scaled, vec![Some(px(600.)), Some(px(200.))]);
2134    }
2135
2136    /// An unconstrained slot is laid out by flex and takes the leftover, so
2137    /// scaling only its siblings would move a divider nothing asked to move.
2138    #[test]
2139    fn an_unconstrained_slot_leaves_every_size_alone() {
2140        let sizes = [Some(px(300.)), None];
2141
2142        assert_eq!(scale_sizes_to(px(800.), &sizes), sizes.to_vec());
2143    }
2144
2145    /// Nothing to scale against before the first layout pass, or when the
2146    /// recorded sizes carry no length at all.
2147    #[test]
2148    fn an_unusable_container_or_total_leaves_every_size_alone() {
2149        let sizes = [Some(px(300.)), Some(px(100.))];
2150        assert_eq!(scale_sizes_to(px(0.), &sizes), sizes.to_vec());
2151
2152        let zeroed = [Some(px(0.)), Some(px(0.))];
2153        assert_eq!(scale_sizes_to(px(800.), &zeroed), zeroed.to_vec());
2154    }
2155
2156    fn setup(cx: &mut TestAppContext) -> (Entity<DockArea>, &mut VisualTestContext) {
2157        cx.update(|cx| {
2158            let _ = crate::Theme::global_mut(cx);
2159        });
2160        cx.add_window_view(|window, cx| DockArea::new("test-dock", None, window, cx))
2161    }
2162
2163    #[gpui::test]
2164    fn dock_size_change_emits_one_layout_event(cx: &mut TestAppContext) {
2165        let (area, cx) = setup(cx);
2166        cx.update(|window, cx| {
2167            area.update(cx, |area, cx| {
2168                area.set_dock(
2169                    DockPlacement::Left,
2170                    DockLayout::tabs().panel(TestPanel::new("Left", cx)),
2171                    window,
2172                    cx,
2173                );
2174            });
2175        });
2176
2177        let events = Rc::new(Cell::new(0));
2178        let observed = events.clone();
2179        let _subscription = cx.update(|window, cx| {
2180            window.subscribe(&area, cx, move |_, event: &DockEvent, _, _| {
2181                if matches!(event, DockEvent::LayoutChanged) {
2182                    observed.set(observed.get() + 1);
2183                }
2184            })
2185        });
2186
2187        cx.update(|window, cx| {
2188            area.update(cx, |area, cx| {
2189                area.set_dock_size(DockPlacement::Left, px(320.), window, cx);
2190                area.set_dock_size(DockPlacement::Left, px(320.), window, cx);
2191            });
2192        });
2193        assert_eq!(
2194            events.get(),
2195            1,
2196            "only an effective size change is persisted"
2197        );
2198    }
2199
2200    /// Two tab groups side by side, holding one logging panel each.
2201    fn two_groups<'a>(
2202        log: &Log,
2203        cx: &'a mut TestAppContext,
2204    ) -> (
2205        Entity<DockArea>,
2206        Entity<TestPanel>,
2207        &'a mut VisualTestContext,
2208    ) {
2209        let (area, cx) = setup(cx);
2210        let log = log.clone();
2211        let alpha = cx.update(|window, cx| {
2212            let alpha = TestPanel::logging("Alpha", &log, cx);
2213            let beta = TestPanel::logging("Beta", &log, cx);
2214            area.update(cx, |area, cx| {
2215                area.set_center(
2216                    DockLayout::h_split()
2217                        .child(DockLayout::tabs().panel(alpha.clone()), None)
2218                        .child(DockLayout::tabs().panel(beta), None),
2219                    window,
2220                    cx,
2221                );
2222            });
2223            alpha
2224        });
2225        (area, alpha, cx)
2226    }
2227
2228    /// The id of the center split's `ix`-th child container.
2229    fn child_node(area: &Entity<DockArea>, ix: usize, cx: &mut VisualTestContext) -> NodeId {
2230        cx.read(|cx| {
2231            let PaneRef::Split { children, .. } = area
2232                .read(cx)
2233                .layout(DockPlacement::Center)
2234                .unwrap()
2235                .root()
2236                .kind()
2237            else {
2238                panic!("the center root is a split");
2239            };
2240            children[ix].id()
2241        })
2242    }
2243
2244    fn panel_id_of(panel: &Entity<TestPanel>) -> PanelId {
2245        PanelId::from(panel.entity_id())
2246    }
2247
2248    fn move_alpha_into_the_other_group(
2249        area: &Entity<DockArea>,
2250        alpha: &Entity<TestPanel>,
2251        cx: &mut VisualTestContext,
2252    ) {
2253        let target = child_node(area, 1, cx);
2254        let alpha_id = panel_id_of(alpha);
2255        cx.update(|window, cx| {
2256            area.update(cx, |area, cx| {
2257                area.move_panel(
2258                    alpha_id,
2259                    InsertTarget::Tabs {
2260                        node: target,
2261                        ix: None,
2262                        activate: true,
2263                    },
2264                    window,
2265                    cx,
2266                );
2267            });
2268        });
2269        cx.run_until_parked();
2270    }
2271
2272    fn collect_sizes(state: &PanelState, out: &mut Vec<Pixels>) {
2273        if let PanelInfo::Stack { sizes, .. } = &state.info {
2274            out.extend(sizes.iter().copied());
2275        }
2276        for child in &state.children {
2277            collect_sizes(child, out);
2278        }
2279    }
2280
2281    fn register_test_panels(cx: &mut App) {
2282        for name in ["Alpha", "Beta", "Gamma"] {
2283            crate::dock::registry::register_panel(cx, name, move |_, _, cx| {
2284                Arc::new(TestPanel::new(name, cx)) as Arc<dyn PanelView>
2285            });
2286        }
2287    }
2288
2289    /// One tab group holding `names`, installed as the whole center.
2290    ///
2291    /// The `DockItem::tabs` the old `TabPanel` tests built is now a described
2292    /// layout the area reconciles, so the group entity is reached through the
2293    /// tree rather than handed back by the constructor.
2294    fn one_group<'a>(
2295        log: &Log,
2296        names: &[&'static str],
2297        active_ix: Option<usize>,
2298        cx: &'a mut TestAppContext,
2299    ) -> (
2300        Entity<DockArea>,
2301        Vec<Entity<TestPanel>>,
2302        &'a mut VisualTestContext,
2303    ) {
2304        let (area, cx) = setup(cx);
2305        let log = log.clone();
2306        let names = names.to_vec();
2307        let panels = cx.update(|window, cx| {
2308            let panels: Vec<_> = names
2309                .iter()
2310                .map(|name| TestPanel::logging(name, &log, cx))
2311                .collect();
2312            let layout = panels
2313                .iter()
2314                .fold(DockLayout::tabs(), |layout, panel| {
2315                    layout.panel(panel.clone())
2316                })
2317                .active_index(active_ix.unwrap_or(0));
2318            area.update(cx, |area, cx| area.set_center(layout, window, cx));
2319            panels
2320        });
2321        (area, panels, cx)
2322    }
2323
2324    /// The live group behind the center split's `ix`-th child.
2325    fn group_of(
2326        area: &Entity<DockArea>,
2327        ix: usize,
2328        cx: &mut VisualTestContext,
2329    ) -> Entity<TabGroup> {
2330        let node = child_node(area, ix, cx);
2331        cx.read(|cx| area.read(cx).groups.get(&node).unwrap().entity.clone())
2332    }
2333
2334    fn move_panel_into(
2335        area: &Entity<DockArea>,
2336        panel: PanelId,
2337        node: NodeId,
2338        ix: Option<usize>,
2339        activate: bool,
2340        cx: &mut VisualTestContext,
2341    ) {
2342        cx.update(|window, cx| {
2343            area.update(cx, |area, cx| {
2344                area.move_panel(panel, InsertTarget::Tabs { node, ix, activate }, window, cx);
2345            });
2346        });
2347        cx.run_until_parked();
2348    }
2349
2350    fn is_center_empty(area: &Entity<DockArea>, cx: &mut VisualTestContext) -> bool {
2351        cx.read(|cx| area.read(cx).is_empty(DockPlacement::Center, cx))
2352    }
2353
2354    #[gpui::test]
2355    fn a_layout_installs_and_dumps_back_to_the_same_state(cx: &mut TestAppContext) {
2356        let (area, cx) = setup(cx);
2357        cx.update(|window, cx| {
2358            let alpha = TestPanel::new("Alpha", cx);
2359            let beta = TestPanel::new("Beta", cx);
2360            area.update(cx, |area, cx| {
2361                area.set_center(
2362                    DockLayout::h_split()
2363                        .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2364                        .child(DockLayout::tabs().panel(beta), None),
2365                    window,
2366                    cx,
2367                );
2368            });
2369        });
2370
2371        let state = cx.read(|cx| area.read(cx).dump(cx));
2372        assert_eq!(state.center.panel_name, "StackPanel");
2373        assert_eq!(state.center.children.len(), 2);
2374        assert_eq!(state.center.children[0].children[0].panel_name, "Alpha");
2375        assert_eq!(state.center.children[1].children[0].panel_name, "Beta");
2376    }
2377
2378    #[gpui::test]
2379    fn moving_a_panel_reuses_its_entity(cx: &mut TestAppContext) {
2380        let log = log_of();
2381        let (area, alpha, cx) = two_groups(&log, cx);
2382        cx.run_until_parked();
2383        drain(&log);
2384
2385        let destination = child_node(&area, 1, cx);
2386        let destination_entity = cx.read(|cx| {
2387            area.read(cx)
2388                .groups
2389                .get(&destination)
2390                .unwrap()
2391                .entity
2392                .entity_id()
2393        });
2394
2395        move_alpha_into_the_other_group(&area, &alpha, cx);
2396
2397        assert_eq!(
2398            cx.read(|cx| area
2399                .read(cx)
2400                .groups
2401                .get(&destination)
2402                .unwrap()
2403                .entity
2404                .entity_id()),
2405            destination_entity,
2406            "the group the panel arrived in was reused, not rebuilt"
2407        );
2408
2409        // A liveness flag on the panel would not say this. The invariant is
2410        // that the panel is still in the tree and was never told it was
2411        // removed — which is exactly what `EditResult::removed_panels` encodes
2412        // by excluding moves.
2413        let alpha_id = panel_id_of(&alpha);
2414        assert!(
2415            cx.read(|cx| area
2416                .read(cx)
2417                .layout(DockPlacement::Center)
2418                .unwrap()
2419                .find_panel_node(alpha_id))
2420                .is_some(),
2421            "the moved panel is still in the tree"
2422        );
2423
2424        let state = cx.read(|cx| area.read(cx).dump(cx));
2425        // One child, not two: emptying the first group removes it, and a
2426        // `RootKind::Split` root is never collapsed, so what is left is a
2427        // one-child split root.
2428        assert_eq!(
2429            state.center.children.len(),
2430            1,
2431            "the emptied group collapsed out of the split"
2432        );
2433        assert_eq!(
2434            state.center.children[0].children.len(),
2435            2,
2436            "both panels now share the surviving group"
2437        );
2438    }
2439
2440    #[gpui::test]
2441    fn a_moved_panel_is_not_told_it_was_removed(cx: &mut TestAppContext) {
2442        let log = log_of();
2443        let (area, alpha, cx) = two_groups(&log, cx);
2444        cx.run_until_parked();
2445        drain(&log);
2446
2447        move_alpha_into_the_other_group(&area, &alpha, cx);
2448
2449        assert!(
2450            !drain(&log).contains(&("Alpha", PanelSignal::Removed)),
2451            "moving a panel between groups must never deliver on_removed"
2452        );
2453    }
2454
2455    #[gpui::test]
2456    fn removing_a_panel_does_tell_it_it_was_removed(cx: &mut TestAppContext) {
2457        // Without this, `a_moved_panel_is_not_told_it_was_removed` would pass
2458        // just as well against a `DockArea` that never calls `on_removed` at
2459        // all.
2460        let log = log_of();
2461        let (area, alpha, cx) = two_groups(&log, cx);
2462        cx.run_until_parked();
2463        drain(&log);
2464
2465        cx.update(|window, cx| {
2466            area.update(cx, |area, cx| area.remove_panel(alpha.clone(), window, cx));
2467        });
2468        cx.run_until_parked();
2469
2470        assert!(
2471            drain(&log).contains(&("Alpha", PanelSignal::Removed)),
2472            "a genuine removal must deliver on_removed"
2473        );
2474    }
2475
2476    #[gpui::test]
2477    fn reconciling_an_unchanged_tree_creates_no_entities(cx: &mut TestAppContext) {
2478        let log = log_of();
2479        let (area, _alpha, cx) = two_groups(&log, cx);
2480        cx.run_until_parked();
2481        drain(&log);
2482
2483        let before = cx.read(|cx| area.read(cx).container_entity_ids());
2484        cx.update(|window, cx| area.update(cx, |area, cx| area.reconcile(window, cx)));
2485        let after = cx.read(|cx| area.read(cx).container_entity_ids());
2486
2487        assert!(!before.is_empty(), "there were containers to preserve");
2488        assert_eq!(
2489            before, after,
2490            "a steady-state pass creates and drops nothing"
2491        );
2492        cx.run_until_parked();
2493        assert_eq!(
2494            drain(&log),
2495            vec![],
2496            "and no panel was re-added or re-activated by it"
2497        );
2498    }
2499
2500    #[gpui::test]
2501    fn a_loaded_layout_round_trips_through_dump(cx: &mut TestAppContext) {
2502        let (area, cx) = setup(cx);
2503        cx.update(|_, cx| register_test_panels(cx));
2504
2505        let json = include_str!("fixtures/nested_splits.json");
2506        let state: DockAreaState = serde_json::from_str(json).unwrap();
2507
2508        cx.update(|window, cx| {
2509            area.update(cx, |area, cx| area.load(state.clone(), window, cx).unwrap())
2510        });
2511        let dumped = cx.read(|cx| area.read(cx).dump(cx));
2512
2513        cx.update(|window, cx| {
2514            area.update(cx, |area, cx| {
2515                area.load(dumped.clone(), window, cx).unwrap()
2516            })
2517        });
2518        let again = cx.read(|cx| area.read(cx).dump(cx));
2519
2520        assert_eq!(dumped, again, "load/dump must reach a fixpoint");
2521        assert_eq!(
2522            dumped.center.children.len(),
2523            3,
2524            "the fixture's nesting is flattened, as the state layer already pins"
2525        );
2526        assert_eq!(dumped.center.children[0].children[0].panel_name, "Alpha");
2527    }
2528
2529    #[gpui::test]
2530    fn a_dumped_live_layout_has_no_zero_sizes(cx: &mut TestAppContext) {
2531        let (area, cx) = setup(cx);
2532        cx.update(|window, cx| {
2533            let alpha = TestPanel::new("Alpha", cx);
2534            let beta = TestPanel::new("Beta", cx);
2535            let gamma = TestPanel::new("Gamma", cx);
2536            area.update(cx, |area, cx| {
2537                area.set_center(
2538                    DockLayout::v_split()
2539                        // Unconstrained, which the tree stores as `None` and
2540                        // the writer would otherwise emit as 0.0.
2541                        .child(DockLayout::tabs().panel(alpha), None)
2542                        // Already zero. Resolving only the `None` slots would
2543                        // leave this one writing the same unsafe byte.
2544                        .child(DockLayout::tabs().panel(beta), Some(px(0.)))
2545                        .child(DockLayout::tabs().panel(gamma), Some(px(240.))),
2546                    window,
2547                    cx,
2548                );
2549            });
2550        });
2551
2552        let state = cx.read(|cx| area.read(cx).dump(cx));
2553        let mut sizes = Vec::new();
2554        collect_sizes(&state.center, &mut sizes);
2555
2556        assert!(!sizes.is_empty(), "the layout has slots to check");
2557        assert!(
2558            sizes.iter().all(|size| *size > px(0.)),
2559            "an older build reads a persisted 0.0 back as a real zero-pixel panel: {sizes:?}"
2560        );
2561    }
2562
2563    /// The tree only hears about slot sizes a drag finished on: the `Resized`
2564    /// subscription fires from `done_resizing`, while every window resize
2565    /// rescales `ResizableState::sizes()` silently. So `dump` reads the
2566    /// measurement for a split it has on screen, and this pins that it does —
2567    /// preferring the tree here would persist the described `300.0` after a
2568    /// layout pass has already rescaled that slot to fit the window.
2569    #[gpui::test]
2570    fn a_dumped_split_writes_the_sizes_it_is_actually_drawn_at(cx: &mut TestAppContext) {
2571        let (area, cx) = setup(cx);
2572        cx.update(|window, cx| {
2573            let alpha = TestPanel::new("Alpha", cx);
2574            let beta = TestPanel::new("Beta", cx);
2575            area.update(cx, |area, cx| {
2576                area.set_center(
2577                    DockLayout::h_split()
2578                        .child(DockLayout::tabs().panel(alpha), Some(px(300.)))
2579                        .child(DockLayout::tabs().panel(beta), Some(px(300.))),
2580                    window,
2581                    cx,
2582                );
2583            });
2584        });
2585        cx.run_until_parked();
2586
2587        let root = cx.read(|cx| {
2588            area.read(cx)
2589                .layout(DockPlacement::Center)
2590                .unwrap()
2591                .root()
2592                .id()
2593        });
2594        let measured = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2595        assert_ne!(
2596            measured,
2597            vec![px(300.), px(300.)],
2598            "the split has to have been rescaled by a layout pass, or this \
2599             test cannot tell the two preferences apart"
2600        );
2601
2602        let state = cx.read(|cx| area.read(cx).dump(cx));
2603        let PanelInfo::Stack { sizes, .. } = &state.center.info else {
2604            panic!("the center writes a stack");
2605        };
2606        assert_eq!(
2607            sizes, &measured,
2608            "the written sizes are the ones on screen, not the ones the tree \
2609             was built from"
2610        );
2611    }
2612
2613    /// A drop that splits carries no size — `TabGroup` builds
2614    /// `InsertTarget::Split { size: None }` — so the split has to decide one,
2615    /// and sharing the container equally is the decision. Passing the `None`
2616    /// straight to `ResizableState` instead makes the new slot the flexible
2617    /// one among fixed siblings, and it stops looking like a half.
2618    #[gpui::test]
2619    fn a_panel_dropped_beside_another_takes_half_the_split(cx: &mut TestAppContext) {
2620        let log = Log::default();
2621        let (area, panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2622        let group = child_node(&area, 0, cx);
2623        let beta = panel_id_of(&panels[1]);
2624
2625        cx.update(|window, cx| {
2626            area.update(cx, |area, cx| {
2627                area.move_panel(
2628                    beta,
2629                    InsertTarget::Split {
2630                        node: group,
2631                        placement: Placement::Right,
2632                        size: None,
2633                    },
2634                    window,
2635                    cx,
2636                );
2637            });
2638        });
2639        cx.run_until_parked();
2640
2641        let root = cx.read(|cx| {
2642            area.read(cx)
2643                .layout(DockPlacement::Center)
2644                .unwrap()
2645                .root()
2646                .id()
2647        });
2648        let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2649
2650        assert_eq!(sizes.len(), 2, "the drop splits the center in two");
2651        let (left, right) = (sizes[0].as_f32(), sizes[1].as_f32());
2652        assert!(
2653            (left - right).abs() <= (left + right) * 0.02,
2654            "the two halves must be within 2% of each other, got {left} and {right}"
2655        );
2656    }
2657
2658    /// The other drop geometry: a placement whose axis differs from the
2659    /// parent's wraps the target in a fresh split, so the sizes are decided
2660    /// by a `ResizableState` that has never been measured.
2661    #[gpui::test]
2662    fn a_panel_dropped_across_the_axis_still_takes_half(cx: &mut TestAppContext) {
2663        let log = Log::default();
2664        let (area, panels, cx) = one_group(&log, &["Alpha", "Beta"], None, cx);
2665        let group = child_node(&area, 0, cx);
2666        let beta = panel_id_of(&panels[1]);
2667
2668        cx.update(|window, cx| {
2669            area.update(cx, |area, cx| {
2670                area.move_panel(
2671                    beta,
2672                    InsertTarget::Split {
2673                        node: group,
2674                        placement: Placement::Bottom,
2675                        size: None,
2676                    },
2677                    window,
2678                    cx,
2679                );
2680            });
2681        });
2682        cx.run_until_parked();
2683
2684        // Dropping across the axis wraps the target in a new split, which
2685        // becomes the center root's only child — the root itself still holds
2686        // one slot.
2687        let wrapper = child_node(&area, 0, cx);
2688        let sizes = cx.read(|cx| {
2689            area.read(cx).splits[&wrapper]
2690                .entity
2691                .read(cx)
2692                .sizes()
2693                .clone()
2694        });
2695        assert_eq!(sizes.len(), 2, "the drop splits the group in two");
2696        let (top, bottom) = (sizes[0].as_f32(), sizes[1].as_f32());
2697        assert!(
2698            (top - bottom).abs() <= (top + bottom) * 0.02,
2699            "the two halves must be within 2% of each other, got {top} and {bottom}"
2700        );
2701    }
2702
2703    /// A drop into a split that already holds more than one slot, which is the
2704    /// shape a real workspace is in by the time anyone drags anything.
2705    #[gpui::test]
2706    fn a_panel_dropped_into_a_populated_split_takes_an_even_share(cx: &mut TestAppContext) {
2707        let (area, cx) = setup(cx);
2708        let panels = cx.update(|window, cx| {
2709            let alpha = TestPanel::new("Alpha", cx);
2710            let beta = TestPanel::new("Beta", cx);
2711            let gamma = TestPanel::new("Gamma", cx);
2712            area.update(cx, |area, cx| {
2713                area.set_center(
2714                    DockLayout::h_split()
2715                        .child(DockLayout::tabs().panel(alpha.clone()), Some(px(240.)))
2716                        .child(
2717                            DockLayout::tabs().panel(beta.clone()).panel(gamma.clone()),
2718                            None,
2719                        ),
2720                    window,
2721                    cx,
2722                );
2723            });
2724            vec![alpha, beta, gamma]
2725        });
2726        cx.run_until_parked();
2727
2728        let right = child_node(&area, 1, cx);
2729        let gamma = panel_id_of(&panels[2]);
2730        cx.update(|window, cx| {
2731            area.update(cx, |area, cx| {
2732                area.move_panel(
2733                    gamma,
2734                    InsertTarget::Split {
2735                        node: right,
2736                        placement: Placement::Right,
2737                        size: None,
2738                    },
2739                    window,
2740                    cx,
2741                );
2742            });
2743        });
2744        cx.run_until_parked();
2745
2746        let root = cx.read(|cx| {
2747            area.read(cx)
2748                .layout(DockPlacement::Center)
2749                .unwrap()
2750                .root()
2751                .id()
2752        });
2753        let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2754        assert_eq!(sizes.len(), 3, "three slots side by side");
2755        let dropped = sizes[2].as_f32();
2756        let neighbour = sizes[1].as_f32();
2757        assert!(
2758            (dropped - neighbour).abs() <= (dropped + neighbour) * 0.02,
2759            "the dropped panel splits its neighbour evenly, got neighbour {neighbour} and dropped {dropped}"
2760        );
2761    }
2762
2763    /// A dock's root is usually a bare tab group, so a drop beside it takes
2764    /// the "wrap the target in a new split" path rather than the "insert into
2765    /// the existing split" one — and that split has never been measured.
2766    #[gpui::test]
2767    fn a_panel_dropped_beside_a_dock_takes_half(cx: &mut TestAppContext) {
2768        for placement in [DockPlacement::Bottom, DockPlacement::Left] {
2769            let (area, cx) = setup(cx);
2770            let dropped = cx.update(|window, cx| {
2771                let resident = TestPanel::new("Resident", cx);
2772                let dropped = TestPanel::new("Dropped", cx);
2773                area.update(cx, |area, cx| {
2774                    area.set_center(
2775                        DockLayout::tabs().panel(TestPanel::new("Center", cx)),
2776                        window,
2777                        cx,
2778                    );
2779                    area.set_dock(
2780                        placement,
2781                        DockLayout::tabs().panel(resident).panel(dropped.clone()),
2782                        window,
2783                        cx,
2784                    );
2785                    area.set_dock_size(placement, px(400.), window, cx);
2786                });
2787                dropped
2788            });
2789            cx.run_until_parked();
2790
2791            let group = cx.read(|cx| {
2792                area.read(cx)
2793                    .layout(placement)
2794                    .unwrap()
2795                    .find_panel_node(panel_id_of(&dropped))
2796                    .expect("both panels start in the dock's only group")
2797            });
2798
2799            cx.update(|window, cx| {
2800                area.update(cx, |area, cx| {
2801                    area.move_panel(
2802                        panel_id_of(&dropped),
2803                        InsertTarget::Split {
2804                            node: group,
2805                            placement: Placement::Bottom,
2806                            size: None,
2807                        },
2808                        window,
2809                        cx,
2810                    );
2811                });
2812            });
2813            cx.run_until_parked();
2814
2815            let split = cx.read(|cx| {
2816                let tree = area.read(cx).layout(placement).unwrap();
2817                let root = tree.root();
2818                match root.kind() {
2819                    PaneRef::Split { .. } => root.id(),
2820                    _ => panic!("the drop must have produced a split"),
2821                }
2822            });
2823            let sizes = cx.read(|cx| area.read(cx).splits[&split].entity.read(cx).sizes().clone());
2824
2825            assert_eq!(sizes.len(), 2, "{placement:?}: the drop splits in two");
2826            let (first, second) = (sizes[0].as_f32(), sizes[1].as_f32());
2827            assert!(
2828                (first - second).abs() <= (first + second) * 0.02,
2829                "{placement:?}: expected halves, got {first} and {second}"
2830            );
2831        }
2832    }
2833
2834    /// A slot given an explicit size keeps it on the frame after the first.
2835    ///
2836    /// The layout is laid out once with a flexible sibling, and the flexible
2837    /// slot's placeholder measurement used to drag the fixed one with it when
2838    /// the container was first measured — the layout visibly jumped once,
2839    /// then settled.
2840    #[gpui::test]
2841    fn an_explicit_slot_size_survives_the_first_layout_pass(cx: &mut TestAppContext) {
2842        let (area, cx) = setup(cx);
2843        cx.update(|window, cx| {
2844            let sidebar = TestPanel::new("Sidebar", cx);
2845            let content = TestPanel::new("Content", cx);
2846            area.update(cx, |area, cx| {
2847                area.set_center(
2848                    DockLayout::h_split()
2849                        .child(DockLayout::tabs().panel(sidebar), Some(px(200.)))
2850                        .child(DockLayout::tabs().panel(content), None),
2851                    window,
2852                    cx,
2853                );
2854            });
2855        });
2856        cx.run_until_parked();
2857
2858        let root = cx.read(|cx| {
2859            area.read(cx)
2860                .layout(DockPlacement::Center)
2861                .unwrap()
2862                .root()
2863                .id()
2864        });
2865        let sizes = cx.read(|cx| area.read(cx).splits[&root].entity.read(cx).sizes().clone());
2866
2867        // Within a couple of pixels of what was asked for — the measured
2868        // value carries the frame's own rounding. The bug this pins made it
2869        // 587px.
2870        let fixed = sizes
2871            .first()
2872            .copied()
2873            .expect("the split has slots")
2874            .as_f32();
2875        assert!(
2876            (fixed - 200.).abs() <= 4.,
2877            "the fixed slot keeps its 200px instead of being rescaled by the \
2878             flexible sibling's placeholder, got {fixed}"
2879        );
2880    }
2881
2882    /// A panel that draws a measurable box, so a test can read where a slot
2883    /// actually landed rather than what `ResizableState` believes about it.
2884    struct MeasuredPanel {
2885        name: &'static str,
2886        focus_handle: FocusHandle,
2887    }
2888
2889    impl MeasuredPanel {
2890        fn new(name: &'static str, cx: &mut App) -> Entity<Self> {
2891            cx.new(|cx| Self {
2892                name,
2893                focus_handle: cx.focus_handle(),
2894            })
2895        }
2896    }
2897
2898    impl Panel for MeasuredPanel {
2899        fn panel_name(&self) -> &'static str {
2900            self.name
2901        }
2902    }
2903
2904    impl EventEmitter<PanelEvent> for MeasuredPanel {}
2905
2906    impl Focusable for MeasuredPanel {
2907        fn focus_handle(&self, _: &App) -> FocusHandle {
2908            self.focus_handle.clone()
2909        }
2910    }
2911
2912    impl Render for MeasuredPanel {
2913        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2914            let name = self.name;
2915            div().size_full().debug_selector(move || name.into())
2916        }
2917    }
2918
2919    fn draw_frames(cx: &mut VisualTestContext, frames: usize) {
2920        for _ in 0..frames {
2921            cx.update(|window, cx| window.draw(cx).clear(cx));
2922        }
2923    }
2924
2925    /// Switching a tab edits one group, yet `commit` reconciles every
2926    /// container. Reconciling a split the edit did not touch re-adopted the
2927    /// tree's `None` for its flexible slot, un-pinning the size the first
2928    /// layout pass had measured for it, so the split re-flexed from scratch
2929    /// and its sized neighbour shrank. This is the dock example's left column
2930    /// getting shorter on every tab click in the bottom dock.
2931    #[gpui::test]
2932    fn switching_a_tab_leaves_an_untouched_split_where_it_was_drawn(cx: &mut TestAppContext) {
2933        let (area, cx) = setup(cx);
2934        cx.update(|window, cx| {
2935            area.update(cx, |area, cx| {
2936                area.set_center(
2937                    DockLayout::tabs().panel(TestPanel::new("Center", cx)),
2938                    window,
2939                    cx,
2940                );
2941                area.set_dock(
2942                    DockPlacement::Left,
2943                    DockLayout::v_split()
2944                        .child(
2945                            DockLayout::tabs().panel(MeasuredPanel::new("upper-left", cx)),
2946                            None,
2947                        )
2948                        .child(
2949                            DockLayout::tabs().panel(MeasuredPanel::new("lower-left", cx)),
2950                            Some(px(360.)),
2951                        ),
2952                    window,
2953                    cx,
2954                );
2955                area.set_dock_size(DockPlacement::Left, px(350.), window, cx);
2956                area.set_dock(
2957                    DockPlacement::Bottom,
2958                    DockLayout::tabs()
2959                        .panel(TestPanel::new("Tooltip", cx))
2960                        .panel(TestPanel::new("Icon", cx)),
2961                    window,
2962                    cx,
2963                );
2964                area.set_dock_size(DockPlacement::Bottom, px(200.), window, cx);
2965            });
2966        });
2967        cx.run_until_parked();
2968        draw_frames(cx, 3);
2969        let before = (
2970            cx.debug_bounds("upper-left").unwrap(),
2971            cx.debug_bounds("lower-left").unwrap(),
2972        );
2973
2974        let bottom = cx.read(|cx| {
2975            area.read(cx)
2976                .layout(DockPlacement::Bottom)
2977                .unwrap()
2978                .root()
2979                .id()
2980        });
2981        let group = cx.read(|cx| area.read(cx).groups[&bottom].entity.clone());
2982        cx.update(|window, cx| {
2983            group.update(cx, |group, cx| group.select_tab(1, window, cx));
2984        });
2985        cx.run_until_parked();
2986        draw_frames(cx, 3);
2987        let after = (
2988            cx.debug_bounds("upper-left").unwrap(),
2989            cx.debug_bounds("lower-left").unwrap(),
2990        );
2991
2992        assert_eq!(
2993            before, after,
2994            "a tab change in the bottom dock must not move the left split"
2995        );
2996    }
2997
2998    /// The other way a reconcile could move an untouched split: its file holds
2999    /// pixels measured in some other window, the first layout pass rescaled
3000    /// the state to the container it actually has, and handing the file's
3001    /// pixels back on a tab change slides the divider to a third position.
3002    #[gpui::test]
3003    fn switching_a_tab_keeps_a_restored_split_at_its_rescaled_share(cx: &mut TestAppContext) {
3004        let (area, cx) = setup(cx);
3005        cx.update(|window, cx| {
3006            area.update(cx, |area, cx| {
3007                area.set_center(
3008                    DockLayout::h_split()
3009                        .child(
3010                            DockLayout::tabs()
3011                                .panel(TestPanel::new("Alpha", cx))
3012                                .panel(TestPanel::new("Beta", cx)),
3013                            Some(px(620.)),
3014                        )
3015                        .child(
3016                            DockLayout::tabs().panel(MeasuredPanel::new("second", cx)),
3017                            Some(px(350.)),
3018                        ),
3019                    window,
3020                    cx,
3021                );
3022            });
3023        });
3024        cx.run_until_parked();
3025        draw_frames(cx, 3);
3026        // The second slot's box tells where the divider is: its left edge is
3027        // the first slot's width, and the two add up to the container.
3028        let before = cx.debug_bounds("second").unwrap();
3029        assert_ne!(
3030            before.right(),
3031            px(970.),
3032            "the window must not match the recorded total, or this test cannot \
3033             tell a rescaled split from the file's pixels"
3034        );
3035
3036        let group = group_of(&area, 0, cx);
3037        cx.update(|window, cx| {
3038            group.update(cx, |group, cx| group.select_tab(1, window, cx));
3039        });
3040        cx.run_until_parked();
3041        draw_frames(cx, 3);
3042        let after = cx.debug_bounds("second").unwrap();
3043
3044        assert_eq!(
3045            before, after,
3046            "a tab change must not hand the file's pixels back to the split"
3047        );
3048    }
3049
3050    /// The headline claim of the whole extraction, in one place: a layout
3051    /// written by the shipped dock loads into the tree world, draws, and saves
3052    /// back to a state the next load reproduces exactly.
3053    ///
3054    /// The fixture is a real user's file — a two-group center plus all three
3055    /// docks — and its panels are not registered here, so every leaf takes the
3056    /// placeholder path that carries the original `PanelState` forward. That
3057    /// is the load-bearing half: a build that dropped an unknown panel would
3058    /// still reach a fixpoint, so the region assertions below check the
3059    /// content is *there* before the fixpoint says it is stable.
3060    #[gpui::test]
3061    fn the_shipped_fixture_survives_a_load_dump_load_round_trip(cx: &mut TestAppContext) {
3062        let (area, cx) = setup(cx);
3063        let fixture: DockAreaState =
3064            serde_json::from_str(include_str!("fixtures/layout.json")).unwrap();
3065
3066        cx.update(|window, cx| area.update(cx, |area, cx| area.load(fixture, window, cx).unwrap()));
3067        cx.run_until_parked();
3068        let first = cx.read(|cx| area.read(cx).dump(cx));
3069
3070        assert_eq!(first.center.children.len(), 2, "the center's two groups");
3071        assert_eq!(first.center.children[0].children.len(), 15);
3072        assert_eq!(first.center.children[1].children.len(), 1);
3073        for dock in [&first.left_dock, &first.bottom_dock, &first.right_dock] {
3074            let dock = dock.as_ref().expect("all three docks are attached");
3075            assert!(dock.open());
3076            assert!(
3077                !dock.panel().children.is_empty(),
3078                "a dock that loaded empty would round-trip just as stably"
3079            );
3080        }
3081        assert_eq!(first.left_dock.as_ref().unwrap().size(), px(350.));
3082        assert_eq!(first.bottom_dock.as_ref().unwrap().size(), px(200.));
3083        assert_eq!(first.right_dock.as_ref().unwrap().size(), px(320.));
3084
3085        cx.update(|window, cx| {
3086            area.update(cx, |area, cx| area.load(first.clone(), window, cx).unwrap())
3087        });
3088        cx.run_until_parked();
3089        let second = cx.read(|cx| area.read(cx).dump(cx));
3090
3091        assert_eq!(second, first, "dump == dump(load(dump))");
3092    }
3093
3094    #[gpui::test]
3095    fn an_unregistered_panel_survives_a_load_and_save_round_trip(cx: &mut TestAppContext) {
3096        let (area, cx) = setup(cx);
3097        cx.update(|_, cx| register_test_panels(cx));
3098
3099        let json = include_str!("fixtures/unregistered_panel.json");
3100        let state: DockAreaState = serde_json::from_str(json).unwrap();
3101
3102        cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
3103        let dumped = cx.read(|cx| area.read(cx).dump(cx));
3104
3105        let leaf = &dumped.center.children[0].children[0];
3106        assert_eq!(leaf.panel_name, "PanelFromTheFuture");
3107        assert_eq!(
3108            leaf.info,
3109            PanelInfo::panel(serde_json::json!({"keep": "me"})),
3110            "a panel this build cannot construct keeps its payload"
3111        );
3112    }
3113
3114    #[gpui::test]
3115    fn a_dock_carries_its_own_tree_and_survives_a_round_trip(cx: &mut TestAppContext) {
3116        let (area, cx) = setup(cx);
3117        cx.update(|window, cx| {
3118            let alpha = TestPanel::new("Alpha", cx);
3119            let beta = TestPanel::new("Beta", cx);
3120            area.update(cx, |area, cx| {
3121                area.set_center(DockLayout::tabs().panel(alpha), window, cx);
3122                area.set_dock(
3123                    DockPlacement::Left,
3124                    DockLayout::tabs().panel(beta),
3125                    window,
3126                    cx,
3127                );
3128            });
3129        });
3130
3131        // Node ids are globally allocated, so the center and the dock never
3132        // claim the same entity-cache slot.
3133        //
3134        // Comparing the two *roots* would not pin this: under a per-tree
3135        // counter the center's root is minted after its tab group and the
3136        // dock's is not, so the roots differ anyway. The collision is between
3137        // the two trees' tab-group nodes, and the property the cache actually
3138        // depends on is that no id is shared at all.
3139        let center_ids = cx.read(|cx| {
3140            area.read(cx)
3141                .layout(DockPlacement::Center)
3142                .unwrap()
3143                .node_ids()
3144        });
3145        let dock_ids = cx.read(|cx| {
3146            area.read(cx)
3147                .layout(DockPlacement::Left)
3148                .unwrap()
3149                .node_ids()
3150        });
3151        assert!(!center_ids.is_empty() && !dock_ids.is_empty());
3152        assert!(
3153            center_ids.iter().all(|id| !dock_ids.contains(id)),
3154            "every tree in one area must draw from one id space: \
3155             center {center_ids:?} vs left {dock_ids:?}"
3156        );
3157
3158        let state = cx.read(|cx| area.read(cx).dump(cx));
3159        let left = state.left_dock.clone().expect("the left dock is written");
3160        assert_eq!(left.placement(), DockPlacement::Left);
3161        assert!(left.open());
3162        assert_eq!(left.panel().children[0].panel_name, "Beta");
3163        assert!(state.right_dock.is_none());
3164    }
3165
3166    #[gpui::test]
3167    fn a_panel_moved_between_regions_keeps_its_active_state(cx: &mut TestAppContext) {
3168        let log = log_of();
3169        let (area, alpha, cx) = two_groups(&log, cx);
3170        cx.run_until_parked();
3171        // Alpha is the displayed tab of its own group, so it has been told
3172        // `true` exactly once.
3173        assert!(drain(&log).contains(&("Alpha", PanelSignal::Active(true))));
3174
3175        move_alpha_into_the_other_group(&area, &alpha, cx);
3176
3177        assert!(
3178            !drain(&log).contains(&("Alpha", PanelSignal::Active(true))),
3179            "a displayed panel dragged to another group must not be told `true` twice"
3180        );
3181    }
3182
3183    #[gpui::test]
3184    fn a_groups_close_intent_reaches_the_tree(cx: &mut TestAppContext) {
3185        // Nothing else here proves `DockArea` subscribes to `TabGroupEvent`
3186        // at all: a group reports intents and does nothing itself, so an
3187        // unsubscribed area is a dock region that silently does nothing.
3188        let log = log_of();
3189        let (area, alpha, cx) = two_groups(&log, cx);
3190        cx.run_until_parked();
3191        drain(&log);
3192
3193        let node = child_node(&area, 0, cx);
3194        let alpha_id = panel_id_of(&alpha);
3195        cx.update(|_, cx| {
3196            let group = area.read(cx).groups.get(&node).unwrap().entity.clone();
3197            group.update(cx, |group, cx| group.close_panel(alpha_id, cx));
3198        });
3199        cx.run_until_parked();
3200
3201        assert!(
3202            cx.read(|cx| area
3203                .read(cx)
3204                .layout(DockPlacement::Center)
3205                .unwrap()
3206                .find_panel_node(alpha_id))
3207                .is_none(),
3208            "the close intent was applied to the tree"
3209        );
3210        assert!(drain(&log).contains(&("Alpha", PanelSignal::Removed)));
3211    }
3212
3213    #[gpui::test]
3214    fn replacing_the_center_tells_the_panels_that_left(cx: &mut TestAppContext) {
3215        let log = log_of();
3216        let (area, _alpha, cx) = two_groups(&log, cx);
3217        cx.run_until_parked();
3218        drain(&log);
3219
3220        cx.update(|window, cx| {
3221            let gamma = TestPanel::new("Gamma", cx);
3222            area.update(cx, |area, cx| {
3223                area.set_center(DockLayout::tabs().panel(gamma), window, cx)
3224            });
3225        });
3226        cx.run_until_parked();
3227
3228        let seen = drain(&log);
3229        assert!(seen.contains(&("Alpha", PanelSignal::Removed)));
3230        assert!(seen.contains(&("Beta", PanelSignal::Removed)));
3231    }
3232
3233    #[gpui::test]
3234    fn closing_a_zoomed_panel_clears_the_zoom(cx: &mut TestAppContext) {
3235        let log = log_of();
3236        let (area, alpha, cx) = two_groups(&log, cx);
3237        cx.run_until_parked();
3238
3239        let node = child_node(&area, 0, cx);
3240        cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_in(node, window, cx)));
3241        assert!(cx.read(|cx| area.read(cx).is_zoomed()));
3242
3243        cx.update(|window, cx| {
3244            area.update(cx, |area, cx| area.remove_panel(alpha.clone(), window, cx))
3245        });
3246
3247        assert!(
3248            !cx.read(|cx| area.read(cx).is_zoomed()),
3249            "a zoomed panel that left the dock must not keep filling it"
3250        );
3251    }
3252
3253    /// A canvas region is the one shape `add_panel` cannot merge into a tab
3254    /// group, and the old `DockItem::add_panel` gave it its own arm. Without
3255    /// one, the `None` fallback splits the region and wraps the whole canvas
3256    /// in a stack the user never asked for.
3257    #[gpui::test]
3258    fn adding_a_panel_to_a_tiles_region_lands_on_the_canvas(cx: &mut TestAppContext) {
3259        let (area, cx) = setup(cx);
3260        let bounds = Bounds {
3261            origin: gpui::point(px(40.), px(40.)),
3262            size: gpui::size(px(200.), px(150.)),
3263        };
3264        let beta = cx.update(|window, cx| {
3265            let alpha = TestPanel::new("Alpha", cx);
3266            let beta = TestPanel::new("Beta", cx);
3267            area.update(cx, |area, cx| {
3268                area.set_center(DockLayout::tiles().tile(alpha, bounds), window, cx);
3269                area.add_panel(beta.clone(), DockPlacement::Center, None, window, cx);
3270            });
3271            beta
3272        });
3273        cx.run_until_parked();
3274
3275        let canvas_node = child_node(&area, 0, cx);
3276        let panels = cx.read(|cx| {
3277            let PaneRef::Tiles { panels } = area
3278                .read(cx)
3279                .layout(DockPlacement::Center)
3280                .unwrap()
3281                .find_node(canvas_node)
3282                .expect("the canvas is still there, not split in two")
3283                .kind()
3284            else {
3285                panic!("the region is still a tiles canvas");
3286            };
3287            panels.to_vec()
3288        });
3289        assert_eq!(panels.len(), 2, "the panel joined the canvas as a tile");
3290
3291        // Registration, not just placement: a tile the area holds no view for
3292        // is dropped by the next `reconcile` and persists as an empty name.
3293        let beta_id = panel_id_of(&beta);
3294        assert!(
3295            cx.read(|cx| area.read(cx).panel(beta_id).is_some()),
3296            "the added panel's view is registered"
3297        );
3298        let state = cx.read(|cx| area.read(cx).dump(cx));
3299        let names: Vec<&str> = state
3300            .center
3301            .children
3302            .iter()
3303            .map(|child| child.panel_name.as_str())
3304            .collect();
3305        assert_eq!(names, vec!["Alpha", "Beta"]);
3306    }
3307
3308    /// The bounds are the whole point of this entry: a host acting on
3309    /// `DockEvent::DragDrop { target: DropTarget::Canvas }` knows where the
3310    /// drop landed and has no other way to say so.
3311    #[gpui::test]
3312    fn add_tile_places_the_panel_where_it_was_asked_to(cx: &mut TestAppContext) {
3313        let (area, cx) = setup(cx);
3314        let first = Bounds {
3315            origin: gpui::point(px(10.), px(10.)),
3316            size: gpui::size(px(100.), px(100.)),
3317        };
3318        let dropped = Bounds {
3319            origin: gpui::point(px(320.), px(180.)),
3320            size: gpui::size(px(240.), px(160.)),
3321        };
3322        let beta = cx.update(|window, cx| {
3323            let alpha = TestPanel::new("Alpha", cx);
3324            let beta = TestPanel::new("Beta", cx);
3325            area.update(cx, |area, cx| {
3326                area.set_center(DockLayout::tiles().tile(alpha, first), window, cx);
3327                area.add_tile(beta.clone(), DockPlacement::Center, dropped, window, cx);
3328            });
3329            beta
3330        });
3331        cx.run_until_parked();
3332
3333        let beta_id = panel_id_of(&beta);
3334        let canvas_node = child_node(&area, 0, cx);
3335        let tile = cx.read(|cx| {
3336            let PaneRef::Tiles { panels } = area
3337                .read(cx)
3338                .layout(DockPlacement::Center)
3339                .unwrap()
3340                .find_node(canvas_node)
3341                .unwrap()
3342                .kind()
3343            else {
3344                panic!("the region is still a tiles canvas");
3345            };
3346            *panels.iter().find(|tile| tile.panel() == beta_id).unwrap()
3347        });
3348        assert_eq!(tile.bounds(), dropped);
3349        assert!(
3350            cx.read(|cx| area.read(cx).panel(beta_id).is_some()),
3351            "the added panel's view is registered"
3352        );
3353    }
3354
3355    /// An explicit tile names a place only a canvas has. Falling through to
3356    /// the tab-group arm would put the panel somewhere the caller never asked
3357    /// for and drop the bounds on the floor.
3358    #[gpui::test]
3359    fn add_tile_does_nothing_to_a_region_with_no_canvas(cx: &mut TestAppContext) {
3360        let log = log_of();
3361        let (area, _, cx) = one_group(&log, &["Alpha"], None, cx);
3362        let bounds = Bounds {
3363            origin: gpui::point(px(10.), px(10.)),
3364            size: gpui::size(px(100.), px(100.)),
3365        };
3366        let beta = cx.update(|window, cx| {
3367            let beta = TestPanel::new("Beta", cx);
3368            area.update(cx, |area, cx| {
3369                area.add_tile(beta.clone(), DockPlacement::Center, bounds, window, cx);
3370            });
3371            beta
3372        });
3373        cx.run_until_parked();
3374
3375        let beta_id = panel_id_of(&beta);
3376        assert!(
3377            cx.read(|cx| area.read(cx).panel(beta_id).is_none()),
3378            "a panel nothing took must not linger in the view map"
3379        );
3380        let state = cx.read(|cx| area.read(cx).dump(cx));
3381        assert_eq!(state.center.children[0].children.len(), 1);
3382
3383        // Nor does asking a dock that does not exist conjure an empty one to
3384        // decline the tile from.
3385        cx.update(|window, cx| {
3386            let gamma = TestPanel::new("Gamma", cx);
3387            area.update(cx, |area, cx| {
3388                area.add_tile(gamma, DockPlacement::Left, bounds, window, cx);
3389            });
3390        });
3391        cx.run_until_parked();
3392        assert!(
3393            cx.read(|cx| area.read(cx).layout(DockPlacement::Left).is_none()),
3394            "a tile with nowhere to go must not leave a dock behind"
3395        );
3396    }
3397
3398    /// The call `add_tile` was written for is a host re-placing a panel it
3399    /// already holds, so a failed one must leave that panel exactly as it
3400    /// found it. Registering first and removing on failure would drop the view
3401    /// of a panel still sitting in a tree, which `reconcile`'s `views_of`
3402    /// asserts against in dev and answers with a shifted active index in
3403    /// release.
3404    #[gpui::test]
3405    fn a_declined_add_leaves_an_already_docked_panel_untouched(cx: &mut TestAppContext) {
3406        let log = log_of();
3407        let (area, panels, cx) = one_group(&log, &["Alpha"], None, cx);
3408        let alpha = panels[0].clone();
3409        let alpha_id = panel_id_of(&alpha);
3410        let bounds = Bounds {
3411            origin: gpui::point(px(10.), px(10.)),
3412            size: gpui::size(px(100.), px(100.)),
3413        };
3414
3415        let registered = cx.read(|cx| {
3416            Arc::as_ptr(
3417                area.read(cx)
3418                    .panel(alpha_id)
3419                    .expect("one_group registers it"),
3420            ) as *const ()
3421        });
3422
3423        // The center is a tab group, so there is no canvas to take the tile.
3424        cx.update(|window, cx| {
3425            area.update(cx, |area, cx| {
3426                area.add_tile(alpha.clone(), DockPlacement::Center, bounds, window, cx);
3427            });
3428        });
3429        cx.run_until_parked();
3430
3431        let handle = |cx: &mut VisualTestContext| {
3432            cx.read(|cx| {
3433                Arc::as_ptr(area.read(cx).panel(alpha_id).expect("still registered")) as *const ()
3434            })
3435        };
3436        assert_eq!(
3437            handle(cx),
3438            registered,
3439            "a panel that was already docked keeps the very handle it was \
3440             registered with; `add_tile` takes a bare entity, so overwriting \
3441             would cost a panel installed through `add_panel_view` its title"
3442        );
3443        assert!(
3444            cx.read(|cx| area
3445                .read(cx)
3446                .layout(DockPlacement::Center)
3447                .unwrap()
3448                .find_panel_node(alpha_id))
3449                .is_some(),
3450            "and keeps its place in the tree"
3451        );
3452        assert!(
3453            !drain(&log).contains(&("Alpha", PanelSignal::Removed)),
3454            "a declined add is not a removal"
3455        );
3456
3457        // The whole dock still reconciles, which is the failure `views_of`
3458        // would otherwise assert on.
3459        let state = cx.read(|cx| area.read(cx).dump(cx));
3460        assert_eq!(state.center.children[0].children[0].panel_name, "Alpha");
3461    }
3462
3463    #[gpui::test]
3464    fn dragging_a_tile_writes_its_new_bounds_back_into_the_tree(cx: &mut TestAppContext) {
3465        let (area, cx) = setup(cx);
3466        let bounds = Bounds {
3467            origin: gpui::point(px(40.), px(40.)),
3468            size: gpui::size(px(200.), px(150.)),
3469        };
3470        let alpha = cx.update(|window, cx| {
3471            let alpha = TestPanel::new("Alpha", cx);
3472            area.update(cx, |area, cx| {
3473                area.set_center(DockLayout::tiles().tile(alpha.clone(), bounds), window, cx);
3474            });
3475            alpha
3476        });
3477
3478        let node = cx.read(|cx| {
3479            area.read(cx)
3480                .layout(DockPlacement::Center)
3481                .unwrap()
3482                .root()
3483                .id()
3484        });
3485        // A `RootKind::Split` center wraps the canvas, so the canvas is the
3486        // wrapper's only child.
3487        let canvas_node = child_node(&area, 0, cx);
3488        assert_ne!(node, canvas_node);
3489        let canvas = cx.read(|cx| {
3490            area.read(cx)
3491                .tiles
3492                .get(&canvas_node)
3493                .unwrap()
3494                .entity
3495                .clone()
3496        });
3497
3498        // A drag of exactly one grid step, far from every other edge, so no
3499        // snapping rewrites it.
3500        cx.update(|window, cx| {
3501            let tile = canvas.read(cx).tiles(cx)[0].clone();
3502            tile.begin_move(gpui::point(px(100.), px(100.)), window, cx);
3503            tile.move_to(gpui::point(px(150.), px(100.)), window, cx);
3504            tile.end_move(window, cx);
3505        });
3506
3507        let node = cx.read(|cx| {
3508            area.read(cx)
3509                .layout(DockPlacement::Center)
3510                .unwrap()
3511                .find_node(canvas_node)
3512                .unwrap()
3513                .clone()
3514        });
3515        let PaneRef::Tiles { panels } = node.kind() else {
3516            panic!("expected a tiles node");
3517        };
3518        assert_eq!(panels[0].panel(), panel_id_of(&alpha));
3519        assert_eq!(
3520            panels[0].bounds().origin.x,
3521            px(90.),
3522            "the canvas reports the move and the tree records it"
3523        );
3524    }
3525
3526    /// The skin's stand-in for a panel this build cannot construct. It keeps
3527    /// the original state, which is the obligation
3528    /// [`DockAreaRenderer::build_placeholder`] documents.
3529    struct SkinPlaceholder {
3530        state: PanelState,
3531        focus_handle: FocusHandle,
3532    }
3533
3534    impl Panel for SkinPlaceholder {
3535        fn panel_name(&self) -> &'static str {
3536            "SkinPlaceholder"
3537        }
3538
3539        fn dump(&self, _: &App) -> PanelState {
3540            self.state.clone()
3541        }
3542    }
3543
3544    impl EventEmitter<PanelEvent> for SkinPlaceholder {}
3545
3546    impl Focusable for SkinPlaceholder {
3547        fn focus_handle(&self, _: &App) -> FocusHandle {
3548            self.focus_handle.clone()
3549        }
3550    }
3551
3552    impl Render for SkinPlaceholder {
3553        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3554            Empty
3555        }
3556    }
3557
3558    struct PlaceholderSkin {
3559        asked: Rc<std::cell::RefCell<Vec<String>>>,
3560    }
3561
3562    impl DockAreaRenderer for PlaceholderSkin {
3563        fn build_placeholder(
3564            &self,
3565            state: &PanelState,
3566            _: &mut Window,
3567            cx: &mut App,
3568        ) -> Option<Arc<dyn PanelView>> {
3569            self.asked.borrow_mut().push(state.panel_name.clone());
3570            let state = state.clone();
3571            Some(Arc::new(cx.new(|cx| SkinPlaceholder {
3572                state,
3573                focus_handle: cx.focus_handle(),
3574            })))
3575        }
3576
3577        fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
3578            Rc::new(BareTabGroup)
3579        }
3580
3581        fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> {
3582            Rc::new(BareTiles)
3583        }
3584    }
3585
3586    /// A panel no builder answers for becomes the skin's placeholder rather
3587    /// than base's draw-nothing one, so the "unknown panel" message the old
3588    /// `InvalidPanel` drew has somewhere to live.
3589    #[gpui::test]
3590    fn an_unbuildable_panel_becomes_the_skins_placeholder(cx: &mut TestAppContext) {
3591        cx.update(|cx| {
3592            let _ = crate::Theme::global_mut(cx);
3593        });
3594        let asked: Rc<std::cell::RefCell<Vec<String>>> = Rc::default();
3595        let skin = Rc::new(PlaceholderSkin {
3596            asked: asked.clone(),
3597        });
3598        let (area, cx) = cx.add_window_view(|window, cx| {
3599            DockArea::new("test-dock", None, window, cx).with_renderer(skin)
3600        });
3601
3602        // Nothing is registered, so the round trip cannot rebuild this panel.
3603        cx.update(|window, cx| {
3604            let ghost = TestPanel::new("Ghost", cx);
3605            area.update(cx, |area, cx| {
3606                area.set_center(DockLayout::tabs().panel(ghost), window, cx)
3607            });
3608        });
3609        let state = cx.read(|cx| area.read(cx).dump(cx));
3610        cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
3611        cx.run_until_parked();
3612
3613        assert_eq!(*asked.borrow(), vec!["Ghost".to_string()]);
3614        assert_eq!(
3615            cx.read(|cx| area
3616                .read(cx)
3617                .panels
3618                .values()
3619                .map(|panel| panel.panel_name(cx))
3620                .collect::<Vec<_>>()),
3621            vec!["SkinPlaceholder"],
3622            "base installed the skin's placeholder, not its own"
3623        );
3624        assert_eq!(
3625            cx.read(|cx| area.read(cx).dump(cx)).center.children[0].children[0].panel_name,
3626            "Ghost",
3627            "and the unknown panel still survives the next save"
3628        );
3629    }
3630
3631    #[gpui::test]
3632    fn a_persisted_tiles_canvas_restores_its_panels(cx: &mut TestAppContext) {
3633        // Every tiles canvas the old dock ever wrote has `TabPanel`-shaped
3634        // children. Read literally, each one misses the registry, becomes a
3635        // placeholder, and the user's panels inside it are never built at
3636        // all — a saved canvas comes back as blank tiles.
3637        let (area, cx) = setup(cx);
3638        cx.update(|_, cx| register_test_panels(cx));
3639
3640        let json = include_str!("fixtures/tiles_tab_panel_children.json");
3641        let state: DockAreaState = serde_json::from_str(json).unwrap();
3642        cx.update(|window, cx| area.update(cx, |area, cx| area.load(state, window, cx).unwrap()));
3643
3644        let dumped = cx.read(|cx| area.read(cx).dump(cx));
3645        let tiles = &dumped.center;
3646        assert_eq!(tiles.panel_name, "Tiles");
3647        assert_eq!(
3648            tiles
3649                .children
3650                .iter()
3651                .map(|child| child.panel_name.as_str())
3652                .collect::<Vec<_>>(),
3653            vec!["Alpha", "Beta", "Gamma"],
3654            "the real panels are restored, not `InvalidPanel` placeholders"
3655        );
3656
3657        // And they are live entities the canvas can draw, not just bytes.
3658        let canvas_node = child_node(&area, 0, cx);
3659        let canvas = cx.read(|cx| {
3660            area.read(cx)
3661                .tiles
3662                .get(&canvas_node)
3663                .unwrap()
3664                .entity
3665                .clone()
3666        });
3667        let names = cx.read(|cx| {
3668            canvas
3669                .read(cx)
3670                .tiles(cx)
3671                .iter()
3672                .map(|tile| tile.panel().panel_name(cx))
3673                .collect::<Vec<_>>()
3674        });
3675        assert_eq!(names, vec!["Alpha", "Beta", "Gamma"]);
3676    }
3677
3678    #[gpui::test]
3679    fn removing_a_non_tail_child_shifts_the_split_sizes_with_it(cx: &mut TestAppContext) {
3680        // `ResizableState` keeps the authoritative size on `panels[ix]`, so a
3681        // tail-truncating sync leaves the survivors of a non-tail removal
3682        // wearing their predecessors' widths.
3683        let log = log_of();
3684        let (area, cx) = setup(cx);
3685        let alpha = cx.update(|window, cx| {
3686            let alpha = TestPanel::logging("Alpha", &log, cx);
3687            let beta = TestPanel::logging("Beta", &log, cx);
3688            let gamma = TestPanel::logging("Gamma", &log, cx);
3689            area.update(cx, |area, cx| {
3690                area.set_center(
3691                    DockLayout::h_split()
3692                        .child(DockLayout::tabs().panel(alpha.clone()), Some(px(100.)))
3693                        .child(DockLayout::tabs().panel(beta), Some(px(200.)))
3694                        .child(DockLayout::tabs().panel(gamma), Some(px(300.))),
3695                    window,
3696                    cx,
3697                );
3698            });
3699            alpha
3700        });
3701
3702        let root = cx.read(|cx| {
3703            area.read(cx)
3704                .layout(DockPlacement::Center)
3705                .unwrap()
3706                .root()
3707                .id()
3708        });
3709        let split = cx.read(|cx| area.read(cx).splits.get(&root).unwrap().entity.clone());
3710        let sizes = |cx: &mut VisualTestContext| cx.read(|cx| split.read(cx).sizes().clone());
3711
3712        // Absolute widths are `ResizableState`'s business — it redistributes
3713        // against the measured container — so what is asserted is which slot
3714        // disappeared, through the ratio the survivors keep.
3715        let before = sizes(cx);
3716        assert_eq!(before.len(), 3);
3717        let kept_if_correct = before[1] / before[2];
3718        let kept_if_truncated = before[0] / before[1];
3719        assert!(
3720            (kept_if_correct - kept_if_truncated).abs() > 0.1,
3721            "the fixture must be able to tell the two outcomes apart"
3722        );
3723
3724        cx.update(|window, cx| area.update(cx, |area, cx| area.remove_panel(alpha, window, cx)));
3725
3726        let after = sizes(cx);
3727        assert_eq!(after.len(), 2);
3728        assert!(
3729            (after[0] / after[1] - kept_if_correct).abs() < 0.01,
3730            "the survivors kept their own proportions: slot 0 was removed, not \
3731             the tail — got {after:?} from {before:?}"
3732        );
3733    }
3734
3735    #[gpui::test]
3736    fn a_panel_moves_between_the_center_and_a_dock(cx: &mut TestAppContext) {
3737        let log = log_of();
3738        let (area, alpha, cx) = two_groups(&log, cx);
3739        cx.update(|window, cx| {
3740            let gamma = TestPanel::logging("Gamma", &log, cx);
3741            area.update(cx, |area, cx| {
3742                area.set_dock(
3743                    DockPlacement::Left,
3744                    DockLayout::tabs().panel(gamma),
3745                    window,
3746                    cx,
3747                );
3748            });
3749        });
3750        cx.run_until_parked();
3751        drain(&log);
3752
3753        let alpha_id = panel_id_of(&alpha);
3754        let dock_group = cx.read(|cx| {
3755            area.read(cx)
3756                .layout(DockPlacement::Left)
3757                .unwrap()
3758                .root()
3759                .id()
3760        });
3761
3762        cx.update(|window, cx| {
3763            area.update(cx, |area, cx| {
3764                area.move_panel(
3765                    alpha_id,
3766                    InsertTarget::Tabs {
3767                        node: dock_group,
3768                        ix: None,
3769                        activate: true,
3770                    },
3771                    window,
3772                    cx,
3773                );
3774            });
3775        });
3776        cx.run_until_parked();
3777
3778        assert!(
3779            cx.read(|cx| area
3780                .read(cx)
3781                .layout(DockPlacement::Center)
3782                .unwrap()
3783                .find_panel_node(alpha_id))
3784                .is_none(),
3785            "the panel left the center"
3786        );
3787        assert_eq!(
3788            cx.read(|cx| area
3789                .read(cx)
3790                .layout(DockPlacement::Left)
3791                .unwrap()
3792                .find_panel_node(alpha_id)),
3793            Some(dock_group),
3794            "and arrived in the dock's group"
3795        );
3796
3797        let seen = drain(&log);
3798        assert!(
3799            !seen.contains(&("Alpha", PanelSignal::Removed)),
3800            "crossing regions is still a move, not a removal"
3801        );
3802        assert!(
3803            !seen.contains(&("Alpha", PanelSignal::Active(true))),
3804            "and it was displayed in both, so it is not told `true` twice"
3805        );
3806    }
3807
3808    #[gpui::test]
3809    fn a_move_onto_an_unusable_target_leaves_no_stranded_panel(cx: &mut TestAppContext) {
3810        // `apply_insert` is a silent no-op when the target node's kind does
3811        // not match. Committing on the insert alone would early-return with
3812        // the panel already gone from the source tree but still in the view
3813        // map, for some later unrelated edit to prune and destroy.
3814        let log = log_of();
3815        let (area, _alpha, cx) = two_groups(&log, cx);
3816        let gamma = cx.update(|window, cx| {
3817            let gamma = TestPanel::logging("Gamma", &log, cx);
3818            area.update(cx, |area, cx| {
3819                area.set_dock(
3820                    DockPlacement::Left,
3821                    DockLayout::tabs().panel(gamma.clone()),
3822                    window,
3823                    cx,
3824                );
3825            });
3826            gamma
3827        });
3828        cx.run_until_parked();
3829        drain(&log);
3830
3831        let gamma_id = panel_id_of(&gamma);
3832        // The center root is a split, so a `Tabs` insert naming it does
3833        // nothing at all.
3834        let center_root = cx.read(|cx| {
3835            area.read(cx)
3836                .layout(DockPlacement::Center)
3837                .unwrap()
3838                .root()
3839                .id()
3840        });
3841
3842        cx.update(|window, cx| {
3843            area.update(cx, |area, cx| {
3844                area.move_panel(
3845                    gamma_id,
3846                    InsertTarget::Tabs {
3847                        node: center_root,
3848                        ix: None,
3849                        activate: true,
3850                    },
3851                    window,
3852                    cx,
3853                );
3854            });
3855        });
3856        cx.run_until_parked();
3857
3858        assert!(
3859            cx.read(|cx| area.read(cx).panel(gamma_id).is_none()),
3860            "the view map agrees with the trees straight away, rather than \
3861             carrying a panel that belongs to no tree"
3862        );
3863        assert!(
3864            drain(&log).contains(&("Gamma", PanelSignal::Removed)),
3865            "and the panel was told so at the point of the call"
3866        );
3867    }
3868
3869    #[gpui::test]
3870    fn an_all_hidden_container_reports_itself_invisible(cx: &mut TestAppContext) {
3871        // What the old `StackPanel::render` asked of each slot before hiding
3872        // it. `render_node` feeds this straight to `resizable_panel().visible`.
3873        let (area, cx) = setup(cx);
3874        let beta = cx.update(|window, cx| {
3875            let alpha = TestPanel::new("Alpha", cx);
3876            let beta = TestPanel::new("Beta", cx);
3877            area.update(cx, |area, cx| {
3878                area.set_center(
3879                    DockLayout::h_split()
3880                        .child(DockLayout::tabs().panel(alpha), None)
3881                        .child(DockLayout::tabs().panel(beta.clone()), None),
3882                    window,
3883                    cx,
3884                );
3885            });
3886            beta
3887        });
3888
3889        let visible = |ix: usize, cx: &mut VisualTestContext| {
3890            let node = child_node(&area, ix, cx);
3891            cx.read(|cx| {
3892                let area = area.read(cx);
3893                let tree = area.layout(DockPlacement::Center).unwrap();
3894                area.is_node_visible(tree.find_node(node).unwrap(), cx)
3895            })
3896        };
3897
3898        assert!(visible(0, cx) && visible(1, cx));
3899
3900        cx.update(|_, cx| beta.update(cx, |beta, cx| beta.set_visible(false, cx)));
3901
3902        assert!(visible(0, cx), "the visible group still holds its slot");
3903        assert!(
3904            !visible(1, cx),
3905            "a group whose every panel is hidden must give its slot up"
3906        );
3907    }
3908
3909    #[gpui::test]
3910    fn a_locked_area_seals_its_groups(cx: &mut TestAppContext) {
3911        let log = log_of();
3912        let (area, _alpha, cx) = two_groups(&log, cx);
3913        cx.run_until_parked();
3914
3915        let node = child_node(&area, 0, cx);
3916        let group = cx.read(|cx| area.read(cx).groups.get(&node).unwrap().entity.clone());
3917        assert!(
3918            cx.read(|cx| group.read(cx).is_closable(cx)),
3919            "an unlocked group's panel can be closed"
3920        );
3921
3922        cx.update(|window, cx| area.update(cx, |area, cx| area.set_locked(true, window, cx)));
3923
3924        assert!(
3925            !cx.read(|cx| group.read(cx).is_closable(cx)),
3926            "the lock reaches every group through the constraints push"
3927        );
3928    }
3929
3930    // The tests below were ported from `crates/component/src/dock/tab_panel.rs` when
3931    // the dock skin was rebuilt on this crate. They are the surviving record
3932    // of the `is_empty` semantics and the documented `set_active` contract.
3933
3934    /// An empty `StackPanel` used to dump as `PanelInfo::Panel`, the
3935    /// `PanelState` default, so restoring looked it up in `PanelRegistry` and
3936    /// failed.
3937    #[gpui::test]
3938    fn empty_center_round_trips_as_a_stack(cx: &mut TestAppContext) {
3939        let (area, cx) = setup(cx);
3940        let center = cx.read(|cx| area.read(cx).dump(cx).center);
3941
3942        assert_eq!(center.panel_name, "StackPanel");
3943        assert!(
3944            matches!(center.info, PanelInfo::Stack { .. }),
3945            "got {:?}",
3946            center.info
3947        );
3948    }
3949
3950    #[gpui::test]
3951    fn fresh_center_is_empty(cx: &mut TestAppContext) {
3952        let (area, cx) = setup(cx);
3953
3954        assert!(
3955            is_center_empty(&area, cx),
3956            "DockArea::new starts with an empty split centre"
3957        );
3958    }
3959
3960    #[gpui::test]
3961    fn center_holding_a_tab_group_is_not_empty(cx: &mut TestAppContext) {
3962        let log = log_of();
3963        let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
3964        cx.run_until_parked();
3965
3966        assert!(!is_center_empty(&area, cx));
3967    }
3968
3969    /// The tree still lists the group's node here until the last panel goes,
3970    /// so anything reading node counts rather than panels would report
3971    /// non-empty.
3972    #[gpui::test]
3973    fn center_is_empty_again_once_every_panel_is_removed(cx: &mut TestAppContext) {
3974        let log = log_of();
3975        let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
3976        cx.run_until_parked();
3977
3978        for panel in panels {
3979            cx.update(|window, cx| {
3980                area.update(cx, |area, cx| area.remove_panel(panel.clone(), window, cx))
3981            });
3982        }
3983        cx.run_until_parked();
3984
3985        assert!(is_center_empty(&area, cx));
3986    }
3987
3988    #[gpui::test]
3989    fn center_is_not_empty_after_adding_to_a_tab_group(cx: &mut TestAppContext) {
3990        let (area, cx) = setup(cx);
3991        assert!(is_center_empty(&area, cx));
3992
3993        cx.update(|window, cx| {
3994            let alpha = TestPanel::new("Alpha", cx);
3995            area.update(cx, |area, cx| {
3996                area.add_panel(alpha, DockPlacement::Center, None, window, cx)
3997            });
3998        });
3999        cx.run_until_parked();
4000
4001        assert!(!is_center_empty(&area, cx));
4002    }
4003
4004    /// The pre-wrapped companion of `add_panel`, for a layer that hands base
4005    /// its own handle. The panel it registers is the very handle it was
4006    /// given, keyed by the id that handle reports.
4007    #[gpui::test]
4008    fn add_panel_view_registers_the_handle_it_was_given(cx: &mut TestAppContext) {
4009        let (area, cx) = setup(cx);
4010
4011        let view = cx.update(|window, cx| {
4012            let view: Arc<dyn PanelView> = Arc::new(TestPanel::new("Alpha", cx));
4013            area.update(cx, |area, cx| {
4014                area.add_panel_view(view.clone(), DockPlacement::Center, None, window, cx)
4015            });
4016            view
4017        });
4018        cx.run_until_parked();
4019
4020        let id = cx.read(|cx| view.panel_id(cx));
4021        assert!(
4022            cx.read(|cx| area
4023                .read(cx)
4024                .panel(id)
4025                .is_some_and(|stored| Arc::ptr_eq(stored, &view))),
4026            "the stored handle is the one that was handed over, under its own id"
4027        );
4028        assert!(!is_center_empty(&area, cx));
4029    }
4030
4031    /// Rendering skips invisible panels, so a centre holding only hidden ones
4032    /// draws nothing and counts as empty.
4033    #[gpui::test]
4034    fn center_holding_only_hidden_panels_is_empty(cx: &mut TestAppContext) {
4035        let log = log_of();
4036        let (area, panels, cx) = one_group(&log, &["A", "B"], None, cx);
4037        cx.run_until_parked();
4038        assert!(!is_center_empty(&area, cx));
4039
4040        cx.update(|_, cx| {
4041            for panel in &panels {
4042                panel.update(cx, |panel, cx| panel.set_visible(false, cx));
4043            }
4044        });
4045        cx.run_until_parked();
4046
4047        assert_eq!(
4048            cx.read(|cx| area
4049                .read(cx)
4050                .layout(DockPlacement::Center)
4051                .unwrap()
4052                .panels()
4053                .count()),
4054            2,
4055            "hiding a panel does not remove it from the tab group"
4056        );
4057        assert!(is_center_empty(&area, cx));
4058    }
4059
4060    /// The old `TabPanel` inside `Tiles` had no parent `StackPanel` to remove
4061    /// itself from, so emptying it left the tile behind and the walk had to
4062    /// recurse. `normalize` now removes the emptied canvas outright, which is
4063    /// the stronger outcome and is what this pins.
4064    #[gpui::test]
4065    fn center_holding_only_empty_tiles_is_empty(cx: &mut TestAppContext) {
4066        let (area, cx) = setup(cx);
4067        let bounds = Bounds {
4068            origin: gpui::point(px(10.), px(10.)),
4069            size: gpui::size(px(200.), px(200.)),
4070        };
4071        let alpha = cx.update(|window, cx| {
4072            let alpha = TestPanel::new("Alpha", cx);
4073            area.update(cx, |area, cx| {
4074                area.set_center(DockLayout::tiles().tile(alpha.clone(), bounds), window, cx)
4075            });
4076            alpha
4077        });
4078        cx.run_until_parked();
4079        assert!(!is_center_empty(&area, cx));
4080
4081        cx.update(|window, cx| area.update(cx, |area, cx| area.remove_panel(alpha, window, cx)));
4082        cx.run_until_parked();
4083
4084        assert!(is_center_empty(&area, cx));
4085    }
4086
4087    /// The recursion the previous test no longer reaches: a canvas that still
4088    /// holds its tile, but whose every panel is hidden.
4089    #[gpui::test]
4090    fn center_holding_only_hidden_tiles_is_empty(cx: &mut TestAppContext) {
4091        let (area, cx) = setup(cx);
4092        let bounds = Bounds {
4093            origin: gpui::point(px(10.), px(10.)),
4094            size: gpui::size(px(200.), px(200.)),
4095        };
4096        let alpha = cx.update(|window, cx| {
4097            let alpha = TestPanel::new("Alpha", cx);
4098            area.update(cx, |area, cx| {
4099                area.set_center(DockLayout::tiles().tile(alpha.clone(), bounds), window, cx)
4100            });
4101            alpha
4102        });
4103        cx.run_until_parked();
4104        assert!(!is_center_empty(&area, cx));
4105
4106        cx.update(|_, cx| alpha.update(cx, |alpha, cx| alpha.set_visible(false, cx)));
4107
4108        assert_eq!(
4109            cx.read(|cx| area
4110                .read(cx)
4111                .layout(DockPlacement::Center)
4112                .unwrap()
4113                .panels()
4114                .count()),
4115            1,
4116            "the tile is still on the canvas"
4117        );
4118        assert!(is_center_empty(&area, cx));
4119    }
4120
4121    #[gpui::test]
4122    fn single_panel_group_receives_initial_active(cx: &mut TestAppContext) {
4123        let log = log_of();
4124        let (_area, _panels, cx) = one_group(&log, &["A"], None, cx);
4125        cx.run_until_parked();
4126
4127        assert_eq!(drain_active(&log), [("A", true)]);
4128    }
4129
4130    #[gpui::test]
4131    fn multi_tab_construction_notifies_only_displayed_panel(cx: &mut TestAppContext) {
4132        let log = log_of();
4133        let (_area, _panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
4134        cx.run_until_parked();
4135
4136        // No false-then-true flip on A, no duplicate true, B/C silent.
4137        assert_eq!(drain_active(&log), [("A", true)]);
4138    }
4139
4140    #[gpui::test]
4141    fn active_index_restore_notifies_that_panel_only(cx: &mut TestAppContext) {
4142        let log = log_of();
4143        let (_area, _panels, cx) = one_group(&log, &["A", "B", "C"], Some(2), cx);
4144        cx.run_until_parked();
4145
4146        assert_eq!(drain_active(&log), [("C", true)]);
4147    }
4148
4149    #[gpui::test]
4150    fn switching_tabs_sends_false_then_true(cx: &mut TestAppContext) {
4151        let log = log_of();
4152        let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
4153        cx.run_until_parked();
4154        drain(&log);
4155
4156        let group = group_of(&area, 0, cx);
4157        cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(1, window, cx)));
4158        cx.run_until_parked();
4159
4160        assert_eq!(drain_active(&log), [("A", false), ("B", true)]);
4161    }
4162
4163    #[gpui::test]
4164    fn reselecting_active_tab_stays_silent(cx: &mut TestAppContext) {
4165        let log = log_of();
4166        let (area, _panels, cx) = one_group(&log, &["A", "B"], None, cx);
4167        cx.run_until_parked();
4168        drain(&log);
4169
4170        let group = group_of(&area, 0, cx);
4171        cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(0, window, cx)));
4172        cx.run_until_parked();
4173
4174        assert_eq!(drain_active(&log), []);
4175    }
4176
4177    /// The old `TabPanel::insert_panel_at` took a brand-new panel; the tree
4178    /// API inserts a panel that is already in the dock, so `C` arrives from a
4179    /// second group. It was a background tab there and so has been told
4180    /// nothing, which is what makes the arrival a genuine activation rather
4181    /// than a seeded handoff.
4182    #[gpui::test]
4183    fn inserting_at_active_ix_swaps_notifications(cx: &mut TestAppContext) {
4184        let log = log_of();
4185        let (area, cx) = setup(cx);
4186        let c = cx.update(|window, cx| {
4187            let a = TestPanel::logging("A", &log, cx);
4188            let b = TestPanel::logging("B", &log, cx);
4189            let x = TestPanel::logging("X", &log, cx);
4190            let c = TestPanel::logging("C", &log, cx);
4191            area.update(cx, |area, cx| {
4192                area.set_center(
4193                    DockLayout::h_split()
4194                        .child(DockLayout::tabs().panel(a).panel(b), None)
4195                        .child(DockLayout::tabs().panel(x).panel(c.clone()), None),
4196                    window,
4197                    cx,
4198                );
4199            });
4200            c
4201        });
4202        cx.run_until_parked();
4203        drain(&log);
4204
4205        let destination = child_node(&area, 0, cx);
4206        let c_id = panel_id_of(&c);
4207        move_panel_into(&area, c_id, destination, Some(0), true, cx);
4208
4209        assert_eq!(drain_active(&log), [("A", false), ("C", true)]);
4210        let group = group_of(&area, 0, cx);
4211        assert_eq!(cx.read(|cx| group.read(cx).active_ix()), 0);
4212        assert_eq!(
4213            cx.read(|cx| group.read(cx).panels()[0].panel_id(cx)),
4214            c_id,
4215            "the arriving panel took the slot it named"
4216        );
4217    }
4218
4219    #[gpui::test]
4220    fn removing_before_active_keeps_displayed_panel(cx: &mut TestAppContext) {
4221        let log = log_of();
4222        let (area, panels, cx) = one_group(&log, &["A", "B", "C"], None, cx);
4223        let group = group_of(&area, 0, cx);
4224        cx.update(|window, cx| group.update(cx, |group, cx| group.select_tab(1, window, cx)));
4225        cx.run_until_parked();
4226        drain(&log);
4227
4228        cx.update(|window, cx| {
4229            area.update(cx, |area, cx| {
4230                area.remove_panel(panels[0].clone(), window, cx)
4231            })
4232        });
4233        cx.run_until_parked();
4234
4235        assert_eq!(drain_active(&log), []);
4236        assert_eq!(cx.read(|cx| group.read(cx).active_ix()), 0);
4237        assert_eq!(
4238            cx.read(|cx| group.read(cx).panels()[0].panel_id(cx)),
4239            panel_id_of(&panels[1]),
4240            "the same panel is still displayed, at its new index"
4241        );
4242    }
4243
4244    /// Collapsing is now a dock closing, which is what
4245    /// `TabGroupConstraints::collapsed` carries.
4246    #[gpui::test]
4247    fn collapse_and_expand_notify_active_panel(cx: &mut TestAppContext) {
4248        let log = log_of();
4249        let (area, cx) = setup(cx);
4250        cx.update(|window, cx| {
4251            let a = TestPanel::logging("A", &log, cx);
4252            let b = TestPanel::logging("B", &log, cx);
4253            area.update(cx, |area, cx| {
4254                area.set_dock(
4255                    DockPlacement::Left,
4256                    DockLayout::tabs().panel(a).panel(b),
4257                    window,
4258                    cx,
4259                );
4260            });
4261        });
4262        cx.run_until_parked();
4263        drain(&log);
4264
4265        cx.update(|window, cx| {
4266            area.update(cx, |area, cx| {
4267                area.toggle_dock(DockPlacement::Left, window, cx)
4268            })
4269        });
4270        cx.run_until_parked();
4271        assert_eq!(drain_active(&log), [("A", false)]);
4272
4273        cx.update(|window, cx| {
4274            area.update(cx, |area, cx| {
4275                area.toggle_dock(DockPlacement::Left, window, cx)
4276            })
4277        });
4278        cx.run_until_parked();
4279        assert_eq!(drain_active(&log), [("A", true)]);
4280    }
4281
4282    #[gpui::test]
4283    fn background_add_is_silent_but_first_panel_is_not(cx: &mut TestAppContext) {
4284        let log = log_of();
4285        let (area, cx) = setup(cx);
4286        let d = cx.update(|window, cx| {
4287            let a = TestPanel::logging("A", &log, cx);
4288            let b = TestPanel::logging("B", &log, cx);
4289            let c = TestPanel::logging("C", &log, cx);
4290            let d = TestPanel::logging("D", &log, cx);
4291            area.update(cx, |area, cx| {
4292                area.set_center(
4293                    DockLayout::h_split()
4294                        .child(DockLayout::tabs().panel(a).panel(b), None)
4295                        .child(DockLayout::tabs().panel(c).panel(d.clone()), None),
4296                    window,
4297                    cx,
4298                );
4299            });
4300            d
4301        });
4302        cx.run_until_parked();
4303        drain(&log);
4304
4305        // D is a background tab in its own group and arrives as a background
4306        // tab in the other one, so nothing changes for anybody.
4307        let destination = child_node(&area, 0, cx);
4308        move_panel_into(&area, panel_id_of(&d), destination, None, false, cx);
4309        assert_eq!(drain_active(&log), []);
4310
4311        // The first panel of a region that had none is displayed regardless,
4312        // so it must be told.
4313        cx.update(|window, cx| {
4314            let e = TestPanel::logging("E", &log, cx);
4315            area.update(cx, |area, cx| {
4316                area.add_panel(e, DockPlacement::Left, None, window, cx)
4317            });
4318        });
4319        cx.run_until_parked();
4320        assert_eq!(drain_active(&log), [("E", true)]);
4321    }
4322
4323    #[gpui::test]
4324    fn drag_active_panel_to_other_group_stays_silent_for_it(cx: &mut TestAppContext) {
4325        let log = log_of();
4326        let (area, cx) = setup(cx);
4327        let a = cx.update(|window, cx| {
4328            let a = TestPanel::logging("A", &log, cx);
4329            let b = TestPanel::logging("B", &log, cx);
4330            let c = TestPanel::logging("C", &log, cx);
4331            area.update(cx, |area, cx| {
4332                area.set_center(
4333                    DockLayout::h_split()
4334                        .child(DockLayout::tabs().panel(a.clone()).panel(b), None)
4335                        .child(DockLayout::tabs().panel(c), None),
4336                    window,
4337                    cx,
4338                );
4339            });
4340            a
4341        });
4342        cx.run_until_parked();
4343        drain(&log);
4344
4345        // A was already told `true`; becoming the target's displayed tab must
4346        // not repeat it.
4347        let destination = child_node(&area, 1, cx);
4348        move_panel_into(&area, panel_id_of(&a), destination, None, true, cx);
4349
4350        // Two groups reconcile independently, so their deliveries interleave
4351        // in no guaranteed order; what is pinned is which ones happen.
4352        let seen = drain_active(&log);
4353        assert!(seen.contains(&("B", true)), "got {seen:?}");
4354        assert!(seen.contains(&("C", false)), "got {seen:?}");
4355        assert!(
4356            !seen.iter().any(|(name, _)| *name == "A"),
4357            "the moved panel was displayed before and after: {seen:?}"
4358        );
4359    }
4360
4361    #[gpui::test]
4362    fn drag_active_panel_to_background_slot_deactivates_it(cx: &mut TestAppContext) {
4363        let log = log_of();
4364        let (area, cx) = setup(cx);
4365        let a = cx.update(|window, cx| {
4366            let a = TestPanel::logging("A", &log, cx);
4367            let c = TestPanel::logging("C", &log, cx);
4368            let d = TestPanel::logging("D", &log, cx);
4369            area.update(cx, |area, cx| {
4370                area.set_center(
4371                    DockLayout::h_split()
4372                        .child(DockLayout::tabs().panel(a.clone()), None)
4373                        .child(DockLayout::tabs().panel(c).panel(d), None),
4374                    window,
4375                    cx,
4376                );
4377            });
4378            a
4379        });
4380        cx.run_until_parked();
4381        drain(&log);
4382
4383        // A was told `true` and becomes a background tab, so it gets one
4384        // `false`.
4385        let destination = child_node(&area, 1, cx);
4386        move_panel_into(&area, panel_id_of(&a), destination, None, false, cx);
4387
4388        assert_eq!(drain_active(&log), [("A", false)]);
4389    }
4390
4391    #[gpui::test]
4392    fn closing_a_tile_removes_its_panel(cx: &mut TestAppContext) {
4393        // `TileContext::is_closable` would otherwise be a control a skin can
4394        // draw and never wire up.
4395        let log = log_of();
4396        let (area, cx) = setup(cx);
4397        let bounds = Bounds {
4398            origin: gpui::point(px(10.), px(10.)),
4399            size: gpui::size(px(200.), px(200.)),
4400        };
4401        let alpha = cx.update(|window, cx| {
4402            let alpha = TestPanel::logging("Alpha", &log, cx);
4403            let beta = TestPanel::logging("Beta", &log, cx);
4404            area.update(cx, |area, cx| {
4405                area.set_center(
4406                    DockLayout::tiles()
4407                        .tile(alpha.clone(), bounds)
4408                        .tile(beta, bounds),
4409                    window,
4410                    cx,
4411                );
4412            });
4413            alpha
4414        });
4415        cx.run_until_parked();
4416        drain(&log);
4417
4418        let canvas_node = child_node(&area, 0, cx);
4419        let canvas = cx.read(|cx| {
4420            area.read(cx)
4421                .tiles
4422                .get(&canvas_node)
4423                .unwrap()
4424                .entity
4425                .clone()
4426        });
4427        cx.update(|window, cx| {
4428            let tile = canvas.read(cx).tiles(cx)[0].clone();
4429            assert!(tile.is_closable());
4430            tile.close(window, cx);
4431        });
4432        cx.run_until_parked();
4433
4434        assert!(
4435            cx.read(|cx| area.read(cx).panel(panel_id_of(&alpha)).is_none()),
4436            "the closed tile's panel left the dock"
4437        );
4438        assert!(drain(&log).contains(&("Alpha", PanelSignal::Removed)));
4439    }
4440
4441    /// A skin that records what it was asked to draw.
4442    ///
4443    /// The chrome is the point: a tab bar is drawn by the *group*, and a
4444    /// tile's drag bar by the *canvas*. Neither runs if the area renders the
4445    /// bare panel instead, so what lands in these logs says which of the two
4446    /// is on screen — a question no reading of `is_zoomed()` can answer.
4447    struct RecordingSkin {
4448        tab_bars: Rc<RefCell<Vec<NodeId>>>,
4449        drag_bars: Rc<RefCell<Vec<PanelId>>>,
4450    }
4451
4452    struct RecordingTabGroup {
4453        drawn: Rc<RefCell<Vec<NodeId>>>,
4454    }
4455
4456    struct RecordingTiles {
4457        drawn: Rc<RefCell<Vec<PanelId>>>,
4458    }
4459
4460    impl DockAreaRenderer for RecordingSkin {
4461        fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
4462            Rc::new(RecordingTabGroup {
4463                drawn: self.tab_bars.clone(),
4464            })
4465        }
4466
4467        fn tiles_renderer(&self) -> Rc<dyn TilesRenderer> {
4468            Rc::new(RecordingTiles {
4469                drawn: self.drag_bars.clone(),
4470            })
4471        }
4472    }
4473
4474    impl TabGroupRenderer for RecordingTabGroup {
4475        fn render_tab_bar(
4476            &self,
4477            group: &TabGroupContext,
4478            _: &mut Window,
4479            _: &mut App,
4480        ) -> AnyElement {
4481            self.drawn.borrow_mut().push(group.node());
4482            Empty.into_any_element()
4483        }
4484    }
4485
4486    impl TilesRenderer for RecordingTiles {
4487        fn render_drag_bar(&self, tile: &TileContext, _: &mut Window, _: &mut App) -> AnyElement {
4488            self.drawn.borrow_mut().push(tile.panel_id());
4489            Empty.into_any_element()
4490        }
4491    }
4492
4493    type DrawLog = (Rc<RefCell<Vec<NodeId>>>, Rc<RefCell<Vec<PanelId>>>);
4494
4495    /// [`setup`], with a skin that records the tab bars and drag bars drawn.
4496    fn setup_recording(
4497        cx: &mut TestAppContext,
4498    ) -> (Entity<DockArea>, DrawLog, &mut VisualTestContext) {
4499        cx.update(|cx| {
4500            let _ = crate::Theme::global_mut(cx);
4501        });
4502        let tab_bars: Rc<RefCell<Vec<NodeId>>> = Rc::default();
4503        let drag_bars: Rc<RefCell<Vec<PanelId>>> = Rc::default();
4504        let skin = Rc::new(RecordingSkin {
4505            tab_bars: tab_bars.clone(),
4506            drag_bars: drag_bars.clone(),
4507        });
4508        let (area, cx) = cx.add_window_view(|window, cx| {
4509            DockArea::new("test-dock", None, window, cx).with_renderer(skin)
4510        });
4511        (area, (tab_bars, drag_bars), cx)
4512    }
4513
4514    fn zoom_signals(log: &Log) -> Vec<(&'static str, PanelSignal)> {
4515        drain(log)
4516            .into_iter()
4517            .filter(|(_, signal)| matches!(signal, PanelSignal::Zoomed(_)))
4518            .collect()
4519    }
4520
4521    /// The regression this exists for: zooming shows the *group*, tab bar and
4522    /// all, not the panel inside it.
4523    ///
4524    /// The old dock zoomed the whole `TabPanel` — every `subscribe_panel` call
4525    /// site handed it one — so the tab bar, the toolbar and the panel menu
4526    /// stayed on screen, and that is where the control that zooms back out
4527    /// lives. A zoom rendering the bare panel would still fill the area and
4528    /// still answer `is_zoomed()`; only the tab bar tells the two apart.
4529    #[gpui::test]
4530    fn a_zoomed_group_is_drawn_whole_rather_than_as_its_bare_panel(cx: &mut TestAppContext) {
4531        let log = log_of();
4532        let (area, (tab_bars, _), cx) = setup_recording(cx);
4533        cx.update(|window, cx| {
4534            let alpha = TestPanel::logging("Alpha", &log, cx);
4535            let beta = TestPanel::logging("Beta", &log, cx);
4536            area.update(cx, |area, cx| {
4537                area.set_center(
4538                    DockLayout::h_split()
4539                        .child(DockLayout::tabs().panel(alpha), None)
4540                        .child(DockLayout::tabs().panel(beta), None),
4541                    window,
4542                    cx,
4543                );
4544            });
4545        });
4546        cx.run_until_parked();
4547
4548        let zoomed = child_node(&area, 0, cx);
4549        let other = child_node(&area, 1, cx);
4550        assert!(
4551            tab_bars.borrow().contains(&zoomed) && tab_bars.borrow().contains(&other),
4552            "both groups draw their own tab bar while nothing is zoomed"
4553        );
4554
4555        tab_bars.borrow_mut().clear();
4556        let group = group_of(&area, 0, cx);
4557        cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
4558        cx.run_until_parked();
4559
4560        assert!(
4561            tab_bars.borrow().contains(&zoomed),
4562            "a zoomed group is rendered whole: its own tab bar is still drawn, \
4563             which is exactly what the bare panel does not carry"
4564        );
4565        assert!(
4566            !tab_bars.borrow().contains(&other),
4567            "and it is the only thing on screen"
4568        );
4569    }
4570
4571    /// Zooming a tile shows its canvas drawing that one tile with its chrome.
4572    ///
4573    /// A tile was a `TabPanel` in the old dock, so it zoomed with its own bar
4574    /// too. The canvas is what draws a tile's chrome, so the canvas is what
4575    /// the area renders.
4576    #[gpui::test]
4577    fn a_zoomed_tile_is_drawn_by_its_canvas_with_its_chrome(cx: &mut TestAppContext) {
4578        let log = log_of();
4579        let (area, (_, drag_bars), cx) = setup_recording(cx);
4580        let bounds = Bounds {
4581            origin: gpui::point(px(40.), px(40.)),
4582            size: gpui::size(px(200.), px(150.)),
4583        };
4584        let (alpha, beta) = cx.update(|window, cx| {
4585            let alpha = TestPanel::logging("Alpha", &log, cx);
4586            let beta = TestPanel::logging("Beta", &log, cx);
4587            area.update(cx, |area, cx| {
4588                area.set_center(
4589                    DockLayout::tiles()
4590                        .tile(alpha.clone(), bounds)
4591                        .tile(beta.clone(), bounds),
4592                    window,
4593                    cx,
4594                );
4595            });
4596            (alpha, beta)
4597        });
4598        cx.run_until_parked();
4599        drain(&log);
4600
4601        let canvas_node = child_node(&area, 0, cx);
4602        let canvas = cx.read(|cx| {
4603            area.read(cx)
4604                .tiles
4605                .get(&canvas_node)
4606                .unwrap()
4607                .entity
4608                .clone()
4609        });
4610        assert!(
4611            drag_bars.borrow().contains(&panel_id_of(&alpha))
4612                && drag_bars.borrow().contains(&panel_id_of(&beta)),
4613            "both tiles draw their own drag bar while nothing is zoomed"
4614        );
4615
4616        drag_bars.borrow_mut().clear();
4617        cx.update(|window, cx| {
4618            let tile = canvas.read(cx).tiles(cx)[0].clone();
4619            assert!(tile.is_zoomable());
4620            tile.toggle_zoom(window, cx);
4621        });
4622        cx.run_until_parked();
4623
4624        assert_eq!(
4625            cx.read(|cx| area.read(cx).zoomed_tile()),
4626            Some(panel_id_of(&alpha))
4627        );
4628        assert!(
4629            drag_bars.borrow().contains(&panel_id_of(&alpha)),
4630            "the zoomed tile keeps the chrome the bare panel does not carry"
4631        );
4632        assert!(
4633            !drag_bars.borrow().contains(&panel_id_of(&beta)),
4634            "and the tiles beside it are no longer drawn"
4635        );
4636        assert_eq!(
4637            zoom_signals(&log),
4638            vec![("Alpha", PanelSignal::Zoomed(true))],
4639            "the panel is told it was zoomed, as its group would have told it"
4640        );
4641
4642        // A zoomed tile is no longer at its stored bounds, so there is
4643        // nothing for a move to mean — the tiles counterpart of a zoomed
4644        // group reporting itself locked.
4645        cx.update(|window, cx| {
4646            let tile = canvas.read(cx).tiles(cx)[0].clone();
4647            tile.begin_move(gpui::point(px(100.), px(100.)), window, cx);
4648        });
4649        assert!(!cx.read(|cx| canvas.read(cx).tiles(cx)[0].is_moving()));
4650    }
4651
4652    /// The area's zoom and the container's own flag are written together, so
4653    /// neither can be left believing something the other does not.
4654    ///
4655    /// A group left flagged zoomed reports itself locked for good, and a
4656    /// locked group refuses every drop.
4657    #[gpui::test]
4658    fn clearing_the_zoom_from_outside_puts_the_groups_own_flag_back(cx: &mut TestAppContext) {
4659        let log = log_of();
4660        let (area, _alpha, cx) = two_groups(&log, cx);
4661        cx.run_until_parked();
4662        drain(&log);
4663
4664        let node = child_node(&area, 0, cx);
4665        let group = group_of(&area, 0, cx);
4666        cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
4667        cx.run_until_parked();
4668        assert_eq!(cx.read(|cx| area.read(cx).zoomed_group()), Some(node));
4669        assert!(cx.read(|cx| group.read(cx).is_zoomed()));
4670        assert_eq!(
4671            zoom_signals(&log),
4672            vec![("Alpha", PanelSignal::Zoomed(true))]
4673        );
4674
4675        cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_out(window, cx)));
4676        cx.run_until_parked();
4677
4678        assert!(!cx.read(|cx| area.read(cx).is_zoomed()));
4679        assert!(
4680            !cx.read(|cx| group.read(cx).is_zoomed()),
4681            "a group left flagged zoomed would stay locked and refuse every drop"
4682        );
4683        assert!(
4684            cx.read(|cx| group.read(cx).context(cx).is_droppable()),
4685            "and the lock the zoom imposed is lifted with it"
4686        );
4687        assert_eq!(
4688            zoom_signals(&log),
4689            vec![("Alpha", PanelSignal::Zoomed(false))],
4690            "the panel hears the zoom end too, not just the group"
4691        );
4692    }
4693
4694    /// A group that refuses to zoom must not leave the area showing it as
4695    /// zoomed: the area records a zoom only once the container agrees.
4696    #[gpui::test]
4697    fn a_group_that_refuses_to_zoom_leaves_the_area_unzoomed(cx: &mut TestAppContext) {
4698        let log = log_of();
4699        let (area, alpha, cx) = two_groups(&log, cx);
4700        cx.run_until_parked();
4701        cx.update(|_, cx| alpha.update(cx, |panel, cx| panel.set_zoomable(false, cx)));
4702
4703        let node = child_node(&area, 0, cx);
4704        let group = group_of(&area, 0, cx);
4705        cx.update(|window, cx| area.update(cx, |area, cx| area.set_zoomed_in(node, window, cx)));
4706        cx.run_until_parked();
4707
4708        assert!(!cx.read(|cx| group.read(cx).is_zoomed()));
4709        assert!(
4710            !cx.read(|cx| area.read(cx).is_zoomed()),
4711            "the area must not fill itself with a group that never zoomed"
4712        );
4713    }
4714}