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