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