mod dock; mod invalid_panel; mod panel; mod resize_handle; mod stack_panel; mod state; mod tab_bar; mod tab_panel; mod window_controls; use std::sync::Arc; use anyhow::Result; pub use dock::*; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges, Entity, EntityId, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px, }; use gpui_component::{ElementExt, Placement}; pub use panel::*; pub use stack_panel::*; pub use state::*; pub use tab_panel::*; /// Initialize the dock, registering the [`PanelRegistry`] global. /// /// Call this from your app entry point, before building any [`DockArea`]. /// It is idempotent, so it is safe to call alongside `gpui_component::init`. pub fn init(cx: &mut App) { PanelRegistry::init(cx); } // Note: the action group name must not collide with gpui-component's own // `dock::` actions, which are linked into the same binary while the app still // depends on gpui-component (action names are registered globally per App). actions!(signed_dock, [ToggleZoom, ClosePanel]); /// Minimal i18n shim replacing gpui-component's `rust_i18n::t!()`. /// /// The upstream dock used `t!("Dock.*")` keys; we keep the same keys but /// resolve them to the English strings so the crate has no i18n dependency. pub(crate) fn t(key: &'static str) -> &'static str { match key { "Dock.Unnamed" => "Unnamed", "Dock.Close" => "Close", "Dock.Zoom In" => "Zoom In", "Dock.Zoom Out" => "Zoom Out", "Dock.Collapse" => "Collapse", "Dock.Expand" => "Expand", _ => key, } } /// The fixed height of the tab bar, which doubles as the window title bar. pub const TAB_BAR_HEIGHT: Pixels = px(44.); /// A host-owned drag item, dragged into the dock by the application. #[derive(Clone, Debug)] pub struct AnyDrag { pub value: Arc, } impl AnyDrag { pub fn new(value: T) -> Self { Self { value: Arc::new(value), } } } pub enum DockEvent { /// The layout of the dock has changed, subscribers this to save the layout. /// /// This event is emitted when every time the layout of the dock has changed, /// So it emits may be too frequently, you may want to debounce the event. LayoutChanged, /// A host-owned drag item ([`AnyDrag`]) was dropped inside the dock. DragDrop { item: AnyDrag, target: DropTarget }, } /// Where a host-owned drag landed, and how much the container can say about it. #[derive(Clone, Debug)] pub enum DropTarget { /// Dropped on a [`TabPanel`] in a split layout. A split layout has no free /// coordinates, so the container reports the panel and the edge it resolved /// from the cursor instead. /// /// `placement` is `None` for the centre zone, meaning merge into the tab /// group rather than split. Panel { tab_panel: Entity, placement: Option, }, } /// The main area of the dock. pub struct DockArea { id: SharedString, /// The version is used to special the default layout, this is like the `panel_version` in [`Panel`](Panel). version: Option, pub(crate) bounds: Bounds, /// The center view of the dock_area. center: DockItem, /// The left dock of the dock_area. left_dock: Option>, /// The entity_id of the [`TabPanel`](TabPanel) where each toggle button should be displayed, toggle_button_panels: Edges>, /// Whether to show the toggle button. toggle_button_visible: bool, /// The top zoom view of the dock_area, if any. zoom_view: Option, /// Lock panels layout, but allow to resize. locked: bool, _subscriptions: Vec, } /// DockItem is a tree structure that represents the layout of the dock. #[derive(Clone)] pub enum DockItem { /// Split layout Split { axis: Axis, /// Self size, only used for build split panels size: Option, items: Vec, /// Items sizes sizes: Vec>, view: Entity, }, /// Tab layout Tabs { /// Self size, only used for build split panels size: Option, items: Vec>, active_ix: usize, view: Entity, }, /// Panel layout Panel { /// Self size, only used for build split panels size: Option, view: Arc, }, } impl std::fmt::Debug for DockItem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DockItem::Split { axis, items, sizes, .. } => f .debug_struct("Split") .field("axis", axis) .field("items", &items.len()) .field("sizes", sizes) .finish(), DockItem::Tabs { items, active_ix, .. } => f .debug_struct("Tabs") .field("items", &items.len()) .field("active_ix", active_ix) .finish(), DockItem::Panel { .. } => f.debug_struct("Panel").finish(), } } } impl DockItem { /// Get the size of the DockItem. fn get_size(&self) -> Option { match self { Self::Split { size, .. } => *size, Self::Tabs { size, .. } => *size, Self::Panel { size, .. } => *size, } } /// Set size for the DockItem. pub fn size(mut self, new_size: impl Into) -> Self { let new_size: Option = Some(new_size.into()); match self { Self::Split { ref mut size, .. } => *size = new_size, Self::Tabs { ref mut size, .. } => *size = new_size, Self::Panel { ref mut size, .. } => *size = new_size, } self } /// Set active index for the DockItem, only valid for [`DockItem::Tabs`]. pub fn active_index(mut self, new_active_ix: usize, cx: &mut App) -> Self { debug_assert!( matches!(self, Self::Tabs { .. }), "active_ix can only be set for DockItem::Tabs" ); if let Self::Tabs { ref mut active_ix, ref mut view, .. } = self { *active_ix = new_active_ix; view.update(cx, |tab_panel, _| { tab_panel.active_ix = new_active_ix; }); } self } /// Create DockItem::Split with given split layout. pub fn split( axis: Axis, items: Vec, dock_area: &WeakEntity, window: &mut Window, cx: &mut App, ) -> Self { let sizes = items.iter().map(|item| item.get_size()).collect(); Self::split_with_sizes(axis, items, sizes, dock_area, window, cx) } /// Create DockItem with vertical split layout. pub fn v_split( items: Vec, dock_area: &WeakEntity, window: &mut Window, cx: &mut App, ) -> Self { Self::split(Axis::Vertical, items, dock_area, window, cx) } /// Create DockItem with horizontal split layout. pub fn h_split( items: Vec, dock_area: &WeakEntity, window: &mut Window, cx: &mut App, ) -> Self { Self::split(Axis::Horizontal, items, dock_area, window, cx) } /// Create DockItem with split layout, each item of panel have specified size. /// /// Please note that the `items` and `sizes` must have the same length. /// Set `None` in `sizes` to make the index of panel have auto size. pub fn split_with_sizes( axis: Axis, items: Vec, sizes: Vec>, dock_area: &WeakEntity, window: &mut Window, cx: &mut App, ) -> Self { let stack_panel = cx.new(|cx| { let mut stack_panel = StackPanel::new(axis, window, cx); for (i, item) in items.iter().enumerate() { let view = item.view(); let size = sizes.get(i).copied().flatten(); stack_panel.add_panel(view.clone(), size, dock_area.clone(), window, cx) } stack_panel }); window.defer(cx, { let stack_panel = stack_panel.clone(); let dock_area = dock_area.clone(); move |window, cx| { _ = dock_area.update(cx, |this, cx| { this.subscribe_panel(&stack_panel, window, cx); }); } }); Self::Split { axis, size: None, items, sizes, view: stack_panel, } } /// Create DockItem with panel layout pub fn panel(panel: Arc) -> Self { Self::Panel { size: None, view: panel, } } /// Create DockItem with tabs layout, items are displayed as tabs. /// /// The `active_ix` is the index of the active tab, if `None` the first tab is active. pub fn tabs( items: Vec>, dock_area: &WeakEntity, window: &mut Window, cx: &mut App, ) -> Self { let mut new_items: Vec> = vec![]; for item in items.into_iter() { new_items.push(item) } Self::new_tabs(new_items, None, dock_area, window, cx) } pub fn tab( item: Entity

, dock_area: &WeakEntity, window: &mut Window, cx: &mut App, ) -> Self { Self::new_tabs(vec![Arc::new(item.clone())], None, dock_area, window, cx) } fn new_tabs( items: Vec>, active_ix: Option, dock_area: &WeakEntity, window: &mut Window, cx: &mut App, ) -> Self { let active_ix = active_ix.unwrap_or(0); let tab_panel = cx.new(|cx| { let mut tab_panel = TabPanel::new(None, dock_area.clone(), window, cx); for item in items.iter() { tab_panel.add_panel(item.clone(), window, cx) } tab_panel.active_ix = active_ix; tab_panel }); Self::Tabs { size: None, items, active_ix, view: tab_panel, } } /// Returns the views of the dock item. pub fn view(&self) -> Arc { match self { Self::Split { view, .. } => Arc::new(view.clone()), Self::Tabs { view, .. } => Arc::new(view.clone()), Self::Panel { view, .. } => view.clone(), } } /// Whether this dock item currently holds no visible panel. /// /// Walks the live panel entities, not `items`: [`Self::add_panel`] only /// pushes into them, nothing ever removes, and splitting does not touch /// them at all. /// /// A container is empty when every child is, so a fresh one is empty. A /// leaf counts as empty while it is hidden, matching the render path, which /// skips panels whose [`Panel::visible`] is `false`. pub fn is_empty(&self, cx: &App) -> bool { fn is_empty(panel: &Arc, cx: &App) -> bool { let view = panel.view(); if let Ok(stack) = view.clone().downcast::() { return stack .read(cx) .panels .iter() .all(|panel| is_empty(panel, cx)); } if let Ok(tabs) = view.clone().downcast::() { return tabs.read(cx).panels.iter().all(|panel| is_empty(panel, cx)); } !panel.visible(cx) } is_empty(&self.view(), cx) } /// Find existing panel in the dock item. pub fn find_panel(&self, panel: Arc) -> Option> { match self { Self::Split { items, .. } => { items.iter().find_map(|item| item.find_panel(panel.clone())) } Self::Tabs { items, .. } => items.iter().find(|item| *item == &panel).cloned(), Self::Panel { view, .. } => Some(view.clone()), } } /// Add a panel to the dock item. pub fn add_panel( &mut self, panel: Arc, dock_area: &WeakEntity, window: &mut Window, cx: &mut App, ) { match self { Self::Tabs { view, items, .. } => { items.push(panel.clone()); view.update(cx, |tab_panel, cx| { tab_panel.add_panel(panel, window, cx); }); } Self::Split { view, items, .. } => { // Iter items to add panel to the first tabs for item in items.iter_mut() { if let DockItem::Tabs { view, .. } = item { view.update(cx, |tab_panel, cx| { tab_panel.add_panel(panel.clone(), window, cx); }); return; } } // Unable to find tabs, create new tabs let new_item = Self::tabs(vec![panel.clone()], dock_area, window, cx); items.push(new_item.clone()); view.update(cx, |stack_panel, cx| { stack_panel.add_panel(new_item.view(), None, dock_area.clone(), window, cx); }); } Self::Panel { .. } => {} } } /// Remove a panel from the dock item. pub fn remove_panel(&self, panel: Arc, window: &mut Window, cx: &mut App) { match self { DockItem::Tabs { view, .. } => { view.update(cx, |tab_panel, cx| { tab_panel.remove_panel(panel, window, cx); }); } DockItem::Split { items, view, .. } => { // For each child item, set collapsed state for item in items { item.remove_panel(panel.clone(), window, cx); } view.update(cx, |split, cx| { split.remove_panel(panel, window, cx); }); } DockItem::Panel { .. } => {} } } pub fn set_collapsed(&self, collapsed: bool, window: &mut Window, cx: &mut App) { match self { DockItem::Tabs { view, .. } => { view.update(cx, |tab_panel, cx| { tab_panel.set_collapsed(collapsed, window, cx); }); } DockItem::Split { items, .. } => { // For each child item, set collapsed state for item in items { item.set_collapsed(collapsed, window, cx); } } DockItem::Panel { view, .. } => view.set_active(!collapsed, window, cx), } } /// Recursively traverses to find the left-most and top-most TabPanel. pub(crate) fn left_top_tab_panel(&self, cx: &App) -> Option> { match self { DockItem::Tabs { view, .. } => Some(view.clone()), DockItem::Split { view, .. } => view.read(cx).left_top_tab_panel(true, cx), DockItem::Panel { .. } => None, } } } impl DockArea { pub fn new( id: impl Into, version: Option, window: &mut Window, cx: &mut Context, ) -> Self { let stack_panel = cx.new(|cx| StackPanel::new(Axis::Horizontal, window, cx)); let dock_item = DockItem::Split { axis: Axis::Horizontal, size: None, items: vec![], sizes: vec![], view: stack_panel.clone(), }; let mut this = Self { id: id.into(), version, bounds: Bounds::default(), center: dock_item, left_dock: None, zoom_view: None, toggle_button_panels: Edges::default(), toggle_button_visible: true, locked: false, _subscriptions: vec![], }; this.subscribe_panel(&stack_panel, window, cx); this } /// Return the bounds of the dock area. pub fn bounds(&self) -> Bounds { self.bounds } /// Set version of the dock area. pub fn set_version(&mut self, version: usize, _: &mut Window, cx: &mut Context) { self.version = Some(version); cx.notify(); } /// Return the center dock item. pub fn center(&self) -> &DockItem { &self.center } /// Whether the center area currently holds no visible panel. /// /// See [`DockItem::is_empty`]. Ask a dock the same question with /// [`Dock::panel`]. pub fn is_center_empty(&self, cx: &App) -> bool { self.center.is_empty(cx) } /// Return the left dock item. pub fn left_dock(&self) -> Option<&Entity> { self.left_dock.as_ref() } /// Remove the left dock. pub fn remove_left_dock(&mut self, _: &mut Window, _: &mut Context) { self.left_dock = None; } /// The the DockItem as the center of the dock area. /// /// This is used to render at the Center of the DockArea. pub fn set_center(&mut self, center: DockItem, window: &mut Window, cx: &mut Context) { self.subscribe_item(¢er, window, cx); self.center = center; self.update_toggle_button_tab_panels(window, cx); cx.notify(); } pub fn set_left_dock( &mut self, panel: DockItem, size: Option, open: bool, window: &mut Window, cx: &mut Context, ) { self.subscribe_item(&panel, window, cx); let weak_self = cx.entity().downgrade(); self.left_dock = Some(cx.new(|cx| { let mut dock = Dock::left(weak_self.clone(), window, cx); if let Some(size) = size { dock.set_size(size, window, cx); } dock.set_panel(panel, window, cx); dock.set_open(open, window, cx); dock })); self.update_toggle_button_tab_panels(window, cx); } /// Set locked state of the dock area, if locked, the dock area cannot be split or move, but allows to resize panels. pub fn set_locked(&mut self, locked: bool, _window: &mut Window, _cx: &mut App) { self.locked = locked; } /// Determine if the dock area is locked. #[inline] pub fn is_locked(&self) -> bool { self.locked } /// Determine if the dock area has a dock at the given placement. pub fn has_dock(&self, placement: DockPlacement) -> bool { match placement { DockPlacement::Left => self.left_dock.is_some(), DockPlacement::Center | DockPlacement::Right | DockPlacement::Bottom => false, } } /// Determine if the dock at the given placement is open. pub fn is_dock_open(&self, placement: DockPlacement, cx: &App) -> bool { match placement { DockPlacement::Left => self .left_dock .as_ref() .map(|dock| dock.read(cx).is_open()) .unwrap_or(false), DockPlacement::Center | DockPlacement::Right | DockPlacement::Bottom => false, } } /// Set the dock at the given placement to be open or closed. /// /// Only the left dock can be toggled. pub fn set_dock_collapsible( &mut self, collapsible_edges: Edges, window: &mut Window, cx: &mut Context, ) { if let Some(left_dock) = self.left_dock.as_ref() { left_dock.update(cx, |dock, cx| { dock.set_collapsible(collapsible_edges.left, window, cx); }); } } /// Determine if the dock at the given placement is collapsible. pub fn is_dock_collapsible(&self, placement: DockPlacement, cx: &App) -> bool { match placement { DockPlacement::Left => self .left_dock .as_ref() .map(|dock| dock.read(cx).collapsible) .unwrap_or(false), DockPlacement::Center | DockPlacement::Right | DockPlacement::Bottom => false, } } /// Toggle the dock at the given placement. pub fn toggle_dock( &self, placement: DockPlacement, window: &mut Window, cx: &mut Context, ) { if let DockPlacement::Left = placement && let Some(dock) = self.left_dock.as_ref() { dock.update(cx, |view, cx| { view.toggle_open(window, cx); }) } } /// Set the visibility of the toggle button. pub fn set_toggle_button_visible(&mut self, visible: bool, _: &mut Context) { self.toggle_button_visible = visible; } /// Add a panel item to the dock area at the given placement. /// /// - [`DockPlacement::Left`] adds the panel to the left dock (creating it /// if needed). /// - [`DockPlacement::Center`] adds the panel as a tab in the center. /// - [`DockPlacement::Right`] and [`DockPlacement::Bottom`] split the /// center so the panel lands on the given side of the existing center /// content. pub fn add_panel( &mut self, panel: Arc, placement: DockPlacement, window: &mut Window, cx: &mut Context, ) { let weak_self = cx.entity().downgrade(); match placement { DockPlacement::Left => { if let Some(dock) = self.left_dock.as_ref() { dock.update(cx, |dock, cx| dock.add_panel(panel, window, cx)) } else { self.set_left_dock( DockItem::tabs(vec![panel], &weak_self, window, cx), None, true, window, cx, ); } } DockPlacement::Right | DockPlacement::Bottom => { self.add_panel_to_center_side(panel, placement, window, cx); } DockPlacement::Center => { self.center .add_panel(panel, &cx.entity().downgrade(), window, cx); } } } /// Split the center so the new panel lands on the given side of the /// existing center content (right of it for [`DockPlacement::Right`], /// below it for [`DockPlacement::Bottom`]). /// /// The existing center keeps its place. Repeating adds with the same axis /// appends further tab groups to the split; a different axis wraps the /// current center in a new split. An empty center is added to directly. fn add_panel_to_center_side( &mut self, panel: Arc, placement: DockPlacement, window: &mut Window, cx: &mut Context, ) { debug_assert!(matches!( placement, DockPlacement::Right | DockPlacement::Bottom )); let weak_self = cx.entity().downgrade(); if self.center.is_empty(cx) { self.center.add_panel(panel, &weak_self, window, cx); } else if let DockItem::Split { axis, size: _, items, sizes, view, } = &mut self.center && *axis == placement.axis() { // The center is already split along this axis: append a new tab group. let new_item = DockItem::tabs(vec![panel], &weak_self, window, cx); items.push(new_item.clone()); sizes.push(None); view.update(cx, |stack, cx| { stack.add_panel(new_item.view(), None, weak_self.clone(), window, cx); }); } else { // Wrap the existing center in a new split. let existing = self.center.clone(); let new_item = DockItem::tabs(vec![panel], &weak_self, window, cx); let items = vec![existing, new_item]; let sizes = vec![None; items.len()]; self.center = DockItem::split_with_sizes(placement.axis(), items, sizes, &weak_self, window, cx); } self.update_toggle_button_tab_panels(window, cx); cx.notify(); } /// Remove panel from the DockArea at the given placement. pub fn remove_panel( &mut self, panel: Arc, placement: DockPlacement, window: &mut Window, cx: &mut Context, ) { match placement { DockPlacement::Left => { if let Some(dock) = self.left_dock.as_mut() { dock.update(cx, |dock, cx| { dock.remove_panel(panel, window, cx); }); } } DockPlacement::Center => { self.center.remove_panel(panel, window, cx); } DockPlacement::Right | DockPlacement::Bottom => { // The panel lives in the center; splits are part of it. self.center.remove_panel(panel, window, cx); } } cx.notify(); } /// Remove a panel from all docks. pub fn remove_panel_from_all_docks( &mut self, panel: Arc, window: &mut Window, cx: &mut Context, ) { self.remove_panel(panel.clone(), DockPlacement::Center, window, cx); self.remove_panel(panel.clone(), DockPlacement::Left, window, cx); } /// Load the state of the DockArea from the DockAreaState. /// /// See also [DockeArea::dump]. pub fn load( &mut self, state: DockAreaState, window: &mut Window, cx: &mut Context, ) -> Result<()> { self.version = state.version; let weak_self = cx.entity().downgrade(); if let Some(left_dock_state) = state.left_dock { self.left_dock = Some(left_dock_state.to_dock(weak_self.clone(), window, cx)); } self.center = state.center.to_item(weak_self, window, cx); self.update_toggle_button_tab_panels(window, cx); Ok(()) } /// Dump the dock panels layout to PanelState. /// /// See also [DockArea::load]. pub fn dump(&self, cx: &App) -> DockAreaState { let root = self.center.view(); let center = root.dump(cx); let left_dock = self .left_dock .as_ref() .map(|dock| DockState::new(dock.clone(), cx)); DockAreaState { version: self.version, center, left_dock, } } /// Subscribe event on the panels #[allow(clippy::only_used_in_recursion)] fn subscribe_item(&mut self, item: &DockItem, window: &mut Window, cx: &mut Context) { match item { DockItem::Split { items, view, .. } => { for item in items { self.subscribe_item(item, window, cx); } self._subscriptions.push(cx.subscribe_in( view, window, move |_, _, event, window, cx| { if let PanelEvent::LayoutChanged = event { cx.spawn_in(window, async move |view, window| { _ = view.update_in(window, |view, window, cx| { view.update_toggle_button_tab_panels(window, cx) }); }) .detach(); cx.emit(DockEvent::LayoutChanged); } }, )); } DockItem::Tabs { .. } => { // We subscribe to the tab panel event in StackPanel's insert_panel } DockItem::Panel { .. } => { // Not supported } } } /// Subscribe zoom event on the panel pub(crate) fn subscribe_panel( &mut self, view: &Entity

, window: &mut Window, cx: &mut Context, ) { let subscription = cx.subscribe_in( view, window, move |_, panel, event, window, cx| match event { PanelEvent::ZoomIn => { let panel = panel.clone(); cx.spawn_in(window, async move |view, window| { _ = view.update_in(window, |view, window, cx| { view.set_zoomed_in(panel, window, cx); cx.notify(); }); }) .detach(); } PanelEvent::ZoomOut => cx .spawn_in(window, async move |view, window| { _ = view.update_in(window, |view, window, cx| { view.set_zoomed_out(window, cx); }); }) .detach(), PanelEvent::LayoutChanged => { cx.spawn_in(window, async move |view, window| { _ = view.update_in(window, |view, window, cx| { view.update_toggle_button_tab_panels(window, cx) }); }) .detach(); cx.emit(DockEvent::LayoutChanged); } }, ); self._subscriptions.push(subscription); } /// Returns the ID of the dock area. pub fn id(&self) -> SharedString { self.id.clone() } pub fn set_zoomed_in( &mut self, panel: Entity

, _: &mut Window, cx: &mut Context, ) { self.zoom_view = Some(panel.into()); cx.notify(); } pub fn set_zoomed_out(&mut self, _: &mut Window, cx: &mut Context) { self.zoom_view = None; cx.notify(); } fn render_items(&self, _window: &mut Window, _cx: &mut Context) -> AnyElement { match &self.center { DockItem::Split { view, .. } => view.clone().into_any_element(), DockItem::Tabs { view, .. } => view.clone().into_any_element(), DockItem::Panel { view, .. } => view.clone().view().into_any_element(), } } pub fn update_toggle_button_tab_panels(&mut self, _: &mut Window, cx: &mut Context) { // The left dock's toggle button lives in the center's top-left tab panel. self.toggle_button_panels.left = self .center .left_top_tab_panel(cx) .map(|view| view.entity_id()); } } impl EventEmitter for DockArea {} impl Render for DockArea { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let view = cx.entity().clone(); div() .id("dock-area") .relative() .size_full() .overflow_hidden() .on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds)) .map(|this| { if let Some(zoom_view) = self.zoom_view.clone() { this.child(zoom_view) } else { // render dock this.child( div() .flex() .flex_row() .h_full() // Left dock .when_some(self.left_dock.clone(), |this, dock| { this.child(div().flex().flex_none().child(dock)) }) // Center .child( div() .flex_1() .overflow_hidden() .child(self.render_items(window, cx)), ), ) } }) } } #[cfg(test)] mod tests { use gpui::TestAppContext; use super::*; #[gpui::test] fn split_with_sizes_adds_each_child_once(cx: &mut TestAppContext) { cx.update(|cx| { cx.set_global(gpui_component::Theme::default()); cx.open_window(Default::default(), |window, cx| { let dock_area = cx.new(|cx| DockArea::new("test-dock", None, window, cx)); let weak_dock_area = dock_area.downgrade(); let children = vec![ DockItem::tabs(Vec::new(), &weak_dock_area, window, cx), DockItem::tabs(Vec::new(), &weak_dock_area, window, cx), ]; let split = DockItem::split_with_sizes( Axis::Horizontal, children, vec![None, None], &weak_dock_area, window, cx, ); let DockItem::Split { view, .. } = split else { unreachable!("split_with_sizes must return DockItem::Split"); }; assert_eq!(view.read(cx).panels_len(), 2); cx.new(|cx| gpui_component::Root::new(dock_area, window, cx)) }) .unwrap(); }); } }