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