1028 lines
34 KiB
Rust
1028 lines
34 KiB
Rust
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<dyn std::any::Any + Send + Sync>,
|
|
}
|
|
|
|
impl AnyDrag {
|
|
pub fn new<T: 'static + Send + Sync>(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<TabPanel>,
|
|
placement: Option<Placement>,
|
|
},
|
|
}
|
|
|
|
/// 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<usize>,
|
|
pub(crate) bounds: Bounds<Pixels>,
|
|
|
|
/// The center view of the dock_area.
|
|
center: DockItem,
|
|
/// The left dock of the dock_area.
|
|
left_dock: Option<Entity<Dock>>,
|
|
|
|
/// The entity_id of the [`TabPanel`](TabPanel) where each toggle button should be displayed,
|
|
toggle_button_panels: Edges<Option<EntityId>>,
|
|
|
|
/// Whether to show the toggle button.
|
|
toggle_button_visible: bool,
|
|
/// The top zoom view of the dock_area, if any.
|
|
zoom_view: Option<AnyView>,
|
|
|
|
/// Lock panels layout, but allow to resize.
|
|
locked: bool,
|
|
|
|
_subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
/// 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<Pixels>,
|
|
items: Vec<DockItem>,
|
|
/// Items sizes
|
|
sizes: Vec<Option<Pixels>>,
|
|
view: Entity<StackPanel>,
|
|
},
|
|
/// Tab layout
|
|
Tabs {
|
|
/// Self size, only used for build split panels
|
|
size: Option<Pixels>,
|
|
items: Vec<Arc<dyn PanelView>>,
|
|
active_ix: usize,
|
|
view: Entity<TabPanel>,
|
|
},
|
|
/// Panel layout
|
|
Panel {
|
|
/// Self size, only used for build split panels
|
|
size: Option<Pixels>,
|
|
view: Arc<dyn PanelView>,
|
|
},
|
|
}
|
|
|
|
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<Pixels> {
|
|
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<Pixels>) -> Self {
|
|
let new_size: Option<Pixels> = 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<DockItem>,
|
|
dock_area: &WeakEntity<DockArea>,
|
|
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<DockItem>,
|
|
dock_area: &WeakEntity<DockArea>,
|
|
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<DockItem>,
|
|
dock_area: &WeakEntity<DockArea>,
|
|
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<DockItem>,
|
|
sizes: Vec<Option<Pixels>>,
|
|
dock_area: &WeakEntity<DockArea>,
|
|
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<dyn PanelView>) -> 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<Arc<dyn PanelView>>,
|
|
dock_area: &WeakEntity<DockArea>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Self {
|
|
let mut new_items: Vec<Arc<dyn PanelView>> = vec![];
|
|
for item in items.into_iter() {
|
|
new_items.push(item)
|
|
}
|
|
Self::new_tabs(new_items, None, dock_area, window, cx)
|
|
}
|
|
|
|
pub fn tab<P: Panel>(
|
|
item: Entity<P>,
|
|
dock_area: &WeakEntity<DockArea>,
|
|
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<Arc<dyn PanelView>>,
|
|
active_ix: Option<usize>,
|
|
dock_area: &WeakEntity<DockArea>,
|
|
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<dyn PanelView> {
|
|
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<dyn PanelView>, cx: &App) -> bool {
|
|
let view = panel.view();
|
|
|
|
if let Ok(stack) = view.clone().downcast::<StackPanel>() {
|
|
return stack
|
|
.read(cx)
|
|
.panels
|
|
.iter()
|
|
.all(|panel| is_empty(panel, cx));
|
|
}
|
|
if let Ok(tabs) = view.clone().downcast::<TabPanel>() {
|
|
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<dyn PanelView>) -> Option<Arc<dyn PanelView>> {
|
|
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<dyn PanelView>,
|
|
dock_area: &WeakEntity<DockArea>,
|
|
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<dyn PanelView>, 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<Entity<TabPanel>> {
|
|
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<SharedString>,
|
|
version: Option<usize>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> 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<Pixels> {
|
|
self.bounds
|
|
}
|
|
|
|
/// Set version of the dock area.
|
|
pub fn set_version(&mut self, version: usize, _: &mut Window, cx: &mut Context<Self>) {
|
|
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<Dock>> {
|
|
self.left_dock.as_ref()
|
|
}
|
|
|
|
/// Remove the left dock.
|
|
pub fn remove_left_dock(&mut self, _: &mut Window, _: &mut Context<Self>) {
|
|
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>) {
|
|
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<Pixels>,
|
|
open: bool,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
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<bool>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
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<Self>,
|
|
) {
|
|
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>) {
|
|
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<dyn PanelView>,
|
|
placement: DockPlacement,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
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<dyn PanelView>,
|
|
placement: DockPlacement,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
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<dyn PanelView>,
|
|
placement: DockPlacement,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
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<dyn PanelView>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
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<Self>,
|
|
) -> 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<Self>) {
|
|
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<P: Panel>(
|
|
&mut self,
|
|
view: &Entity<P>,
|
|
window: &mut Window,
|
|
cx: &mut Context<DockArea>,
|
|
) {
|
|
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<P: Panel>(
|
|
&mut self,
|
|
panel: Entity<P>,
|
|
_: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.zoom_view = Some(panel.into());
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn set_zoomed_out(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
|
self.zoom_view = None;
|
|
cx.notify();
|
|
}
|
|
|
|
fn render_items(&self, _window: &mut Window, _cx: &mut Context<Self>) -> 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<Self>) {
|
|
// 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<DockEvent> for DockArea {}
|
|
impl Render for DockArea {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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();
|
|
});
|
|
}
|
|
}
|