Skip to main content

gpui_base/dock/
dock_area.rs

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