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