Files
gpui-native-kit/crates/layout/src/dock.rs
T
2026-11-02 16:24:20 +07:00

1056 lines
31 KiB
Rust

//! The dock: the center pane tree — tabbed panes the app fills from
//! registered panels, split two ways, collapsing as tabs close.
use std::collections::HashMap;
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, AnyView, App, AppContext as _, Axis, Context, ElementId, Entity,
InteractiveElement as _, IntoElement, ParentElement as _, Render, RenderOnce, Role,
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window, canvas,
div, px, relative, size,
};
use gpui_native_core::common::{h_flex, v_flex};
use gpui_native_core::context_menu::ContextMenu;
use gpui_native_core::menu::MenuItem;
use gpui_native_data::tabs::{Tab, Tabs, TabsVariant};
use gpui_native_icons::IconRole;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::resize_handle::ResizeHandle;
/// The narrowest either side of a split can get, as a fraction of the split.
const MIN_SPLIT_RATIO: f32 = 0.1;
/// The fraction a split opens at, and what a double-click on the seam restores.
const DEFAULT_SPLIT_RATIO: f32 = 0.5;
/// Builds the view a panel opens as, once per open.
type PanelBuilder = Box<dyn Fn(&mut Window, &mut App) -> AnyView>;
/// A registered panel: its identity, chrome, and how its view is built.
pub struct PanelDescriptor {
id: SharedString,
title: SharedString,
icon: Option<IconRole>,
build: PanelBuilder,
}
impl PanelDescriptor {
/// Creates a panel descriptor that opens through `build`.
pub fn new<I, T, F>(id: I, title: T, build: F) -> Self
where
I: Into<SharedString>,
T: Into<SharedString>,
F: Fn(&mut Window, &mut App) -> AnyView + 'static,
{
Self {
id: id.into(),
title: title.into(),
icon: None,
build: Box::new(build),
}
}
/// Shows an icon leading the tab label.
pub fn icon(mut self, role: IconRole) -> Self {
self.icon = Some(role);
self
}
}
/// Which side of its pane a split places the new pane on.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SplitDirection {
Left,
Right,
Top,
Bottom,
}
impl SplitDirection {
fn axis(self) -> Axis {
match self {
Self::Left | Self::Right => Axis::Horizontal,
Self::Top | Self::Bottom => Axis::Vertical,
}
}
/// Whether the new pane takes the split's leading slot.
fn leads(self) -> bool {
matches!(self, Self::Left | Self::Top)
}
}
/// The payload a dock tab drags: the panel to move.
#[derive(Clone)]
pub struct PanelDrag {
pub panel: SharedString,
}
/// The card that follows the cursor while a tab is dragged.
struct DragPreview {
title: SharedString,
}
impl Render for DragPreview {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
div()
.px_2()
.py_1()
.rounded(theme.radius.sm)
.border_1()
.border_color(theme.colors.border)
.bg(theme.colors.surface)
.text_size(theme.sizes.medium.text_size)
.text_color(theme.colors.foreground)
.opacity(0.85)
.child(self.title.clone())
}
}
/// One open panel view.
struct PanelTab {
panel: SharedString,
title: SharedString,
icon: Option<IconRole>,
view: AnyView,
}
/// A leaf: the tabs of one region and which is active.
struct Pane {
key: u64,
tabs: Vec<PanelTab>,
active: Option<SharedString>,
}
/// An internal node: two children along an axis.
struct Split {
key: u64,
axis: Axis,
ratio: f32,
a: Box<Node>,
b: Box<Node>,
}
enum Node {
Pane(Pane),
Split(Split),
}
/// The dock's pane tree, owned by the application.
pub struct DockState {
root: Node,
panels: HashMap<SharedString, PanelDescriptor>,
/// The pane new panels open into, kept on the last activated pane.
active_pane: u64,
next_key: u64,
}
impl DockState {
/// Creates a dock with one empty pane.
pub fn new(cx: &mut App) -> Entity<Self> {
cx.new(|_| Self {
root: Node::Pane(Pane {
key: 0,
tabs: Vec::new(),
active: None,
}),
panels: HashMap::new(),
active_pane: 0,
next_key: 1,
})
}
/// Registers a panel descriptor, replacing any earlier one with its id.
pub fn register_panel(&mut self, panel: PanelDescriptor, cx: &mut Context<Self>) {
if self.panels.insert(panel.id.clone(), panel).is_none() {
cx.notify();
}
}
/// Opens a panel into the active pane and activates its tab.
pub fn open_panel(&mut self, id: &str, window: &mut Window, cx: &mut Context<Self>) {
if self.is_open(id) {
self.activate_panel(id, cx);
return;
}
let Some(panel) = self.panels.get(id) else {
return;
};
let tab = PanelTab {
panel: SharedString::from(id),
title: panel.title.clone(),
icon: panel.icon,
view: (panel.build)(window, cx),
};
let pane_key = self.target_pane_key();
if let Some(pane) = self.pane_mut(pane_key) {
pane.tabs.push(tab);
pane.active = Some(SharedString::from(id));
}
self.active_pane = pane_key;
cx.notify();
}
/// Activates a panel's tab and makes its pane the active pane.
pub fn activate_panel(&mut self, id: &str, cx: &mut Context<Self>) {
let Some((pane_key, _)) = self.locate(id) else {
return;
};
let changed = {
let Some(pane) = self.pane_mut(pane_key) else {
return;
};
let changed = pane.active.as_deref() != Some(id);
if changed {
pane.active = Some(SharedString::from(id));
}
changed
};
self.active_pane = pane_key;
if changed {
cx.notify();
}
}
/// Closes a panel's tab.
pub fn close_panel(&mut self, id: &str, cx: &mut Context<Self>) {
let Some((pane_key, index)) = self.locate(id) else {
return;
};
let emptied = {
let Some(pane) = self.pane_mut(pane_key) else {
return;
};
let was_active = pane.active.as_deref() == Some(id);
pane.tabs.remove(index);
if was_active {
pane.active = pane
.tabs
.get(index)
.or_else(|| {
index
.checked_sub(1)
.and_then(|previous| pane.tabs.get(previous))
})
.map(|tab| tab.panel.clone());
}
pane.tabs.is_empty()
};
if emptied {
self.detach_pane(pane_key);
if self.active_pane == pane_key {
self.active_pane = self.first_pane_key().unwrap_or(0);
}
}
cx.notify();
}
/// Closes every other tab in the panel's pane, keeping this one active.
pub fn close_others(&mut self, id: &str, cx: &mut Context<Self>) {
let Some((pane_key, _)) = self.locate(id) else {
return;
};
let changed = {
let Some(pane) = self.pane_mut(pane_key) else {
return;
};
let before = pane.tabs.len();
pane.tabs.retain(|tab| tab.panel.as_ref() == id);
pane.active = Some(SharedString::from(id));
pane.tabs.len() != before
};
if changed {
cx.notify();
}
}
/// Closes every tab in the panel's pane; a non-root pane collapses.
pub fn close_pane(&mut self, id: &str, cx: &mut Context<Self>) {
let Some((pane_key, _)) = self.locate(id) else {
return;
};
let was_empty = self
.pane_mut(pane_key)
.is_none_or(|pane| pane.tabs.is_empty());
if was_empty {
return;
}
if let Some(pane) = self.pane_mut(pane_key) {
pane.tabs.clear();
pane.active = None;
}
self.detach_pane(pane_key);
if self.active_pane == pane_key {
self.active_pane = self.first_pane_key().unwrap_or(0);
}
cx.notify();
}
/// Moves a panel's tab into a new pane on `direction`'s side of its pane.
pub fn split_panel(&mut self, id: &str, direction: SplitDirection, cx: &mut Context<Self>) {
self.split_panel_at(id, id, direction, cx);
}
/// Moves a panel's tab into a new pane on `direction`.
pub fn split_panel_at(
&mut self,
id: &str,
anchor: &str,
direction: SplitDirection,
cx: &mut Context<Self>,
) {
let (Some((source_key, index)), Some((target_key, _))) =
(self.locate(id), self.locate(anchor))
else {
return;
};
let tab = {
let Some(pane) = self.pane_mut(source_key) else {
return;
};
let tab = pane.tabs.remove(index);
if pane.active.as_deref() == Some(id) {
pane.active = pane
.tabs
.get(index)
.or_else(|| {
index
.checked_sub(1)
.and_then(|previous| pane.tabs.get(previous))
})
.map(|remaining| remaining.panel.clone());
}
tab
};
let emptied = self
.pane_mut(source_key)
.is_none_or(|pane| pane.tabs.is_empty());
let new_key = self.next_key;
let split_key = self.next_key + 1;
self.next_key += 2;
let new_pane = Node::Pane(Pane {
key: new_key,
tabs: vec![tab],
active: Some(SharedString::from(id)),
});
Self::wrap_in(&mut self.root, target_key, new_pane, direction, split_key);
if emptied {
self.detach_pane(source_key);
}
self.active_pane = new_key;
cx.notify();
}
/// Moves a panel's tab into the pane holding `anchor`, appended last.
pub fn move_panel(&mut self, id: &str, anchor: &str, cx: &mut Context<Self>) {
let (Some((source_key, index)), Some((target_key, _))) =
(self.locate(id), self.locate(anchor))
else {
return;
};
if source_key == target_key {
return;
}
let tab = {
let Some(pane) = self.pane_mut(source_key) else {
return;
};
let tab = pane.tabs.remove(index);
if pane.active.as_deref() == Some(id) {
pane.active = pane
.tabs
.get(index)
.or_else(|| {
index
.checked_sub(1)
.and_then(|previous| pane.tabs.get(previous))
})
.map(|remaining| remaining.panel.clone());
}
tab
};
if let Some(pane) = self.pane_mut(target_key) {
pane.tabs.push(tab);
pane.active = Some(SharedString::from(id));
}
let emptied = self
.pane_mut(source_key)
.is_none_or(|pane| pane.tabs.is_empty());
if emptied {
self.detach_pane(source_key);
if self.active_pane == source_key {
self.active_pane = self.first_pane_key().unwrap_or(0);
}
}
self.active_pane = target_key;
cx.notify();
}
/// The active tab of the pane new panels open into.
pub fn active_panel(&self) -> Option<SharedString> {
let pane = pane_in(&self.root, self.target_pane_key())?;
pane.active.clone()
}
/// Whether a panel's tab is open anywhere in the tree.
pub fn is_open(&self, id: &str) -> bool {
self.locate(id).is_some()
}
fn locate(&self, id: &str) -> Option<(u64, usize)> {
Self::locate_in(&self.root, id)
}
fn locate_in(node: &Node, id: &str) -> Option<(u64, usize)> {
match node {
Node::Pane(pane) => pane
.tabs
.iter()
.position(|tab| tab.panel.as_ref() == id)
.map(|index| (pane.key, index)),
Node::Split(split) => {
Self::locate_in(&split.a, id).or_else(|| Self::locate_in(&split.b, id))
}
}
}
fn target_pane_key(&self) -> u64 {
if pane_in(&self.root, self.active_pane).is_some() {
self.active_pane
} else {
self.first_pane_key().unwrap_or(0)
}
}
fn first_pane_key(&self) -> Option<u64> {
Self::first_pane_in(&self.root)
}
fn first_pane_in(node: &Node) -> Option<u64> {
match node {
Node::Pane(pane) => Some(pane.key),
Node::Split(split) => {
Self::first_pane_in(&split.a).or_else(|| Self::first_pane_in(&split.b))
}
}
}
fn pane_mut(&mut self, key: u64) -> Option<&mut Pane> {
Self::pane_mut_in(&mut self.root, key)
}
fn pane_mut_in(node: &mut Node, key: u64) -> Option<&mut Pane> {
match node {
Node::Pane(pane) => (pane.key == key).then_some(pane),
Node::Split(split) => Self::pane_mut_in(&mut split.a, key)
.or_else(|| Self::pane_mut_in(&mut split.b, key)),
}
}
fn split_mut(node: &mut Node, key: u64) -> Option<&mut Split> {
let Node::Split(split) = node else {
return None;
};
if split.key == key {
return Some(split);
}
Self::split_mut(&mut split.a, key).or_else(|| Self::split_mut(&mut split.b, key))
}
/// Nudges a split's ratio by `fraction`, clamped at both ends.
fn resize_split(&mut self, key: u64, fraction: f32, cx: &mut Context<Self>) {
let Some(split) = Self::split_mut(&mut self.root, key) else {
return;
};
let ratio = (split.ratio + fraction).clamp(MIN_SPLIT_RATIO, 1.0 - MIN_SPLIT_RATIO);
if (split.ratio - ratio).abs() > f32::EPSILON {
split.ratio = ratio;
cx.notify();
}
}
/// Restores a split to its default ratio.
fn reset_split(&mut self, key: u64, cx: &mut Context<Self>) {
let Some(split) = Self::split_mut(&mut self.root, key) else {
return;
};
if (split.ratio - DEFAULT_SPLIT_RATIO).abs() > f32::EPSILON {
split.ratio = DEFAULT_SPLIT_RATIO;
cx.notify();
}
}
/// Replaces a pane with a split holding it and `new_pane`.
fn wrap_in(
node: &mut Node,
key: u64,
new_pane: Node,
direction: SplitDirection,
split_key: u64,
) {
match node {
Node::Pane(pane) if pane.key == key => {
let old_pane = take_node(node);
let split = if direction.leads() {
Split {
key: split_key,
axis: direction.axis(),
ratio: DEFAULT_SPLIT_RATIO,
a: Box::new(new_pane),
b: Box::new(old_pane),
}
} else {
Split {
key: split_key,
axis: direction.axis(),
ratio: DEFAULT_SPLIT_RATIO,
a: Box::new(old_pane),
b: Box::new(new_pane),
}
};
*node = Node::Split(split);
}
Node::Pane(_) => {}
Node::Split(split) => {
if pane_in(&split.a, key).is_some() {
Self::wrap_in(&mut split.a, key, new_pane, direction, split_key);
} else if pane_in(&split.b, key).is_some() {
Self::wrap_in(&mut split.b, key, new_pane, direction, split_key);
}
}
}
}
/// Removes a pane and replaces its parent split with the sibling subtree.
fn detach_pane(&mut self, key: u64) {
Self::detach_in(&mut self.root, key);
}
fn detach_in(node: &mut Node, key: u64) {
let Node::Split(split) = node else {
return;
};
let a_direct = direct_pane_key(&split.a) == Some(key);
let b_direct = direct_pane_key(&split.b) == Some(key);
if a_direct != b_direct {
let survivor = if a_direct {
take_node(&mut split.b)
} else {
take_node(&mut split.a)
};
*node = survivor;
} else {
Self::detach_in(&mut split.a, key);
Self::detach_in(&mut split.b, key);
}
}
/// The tree the dock renders from, one frame at a time.
fn snapshot(&self) -> TreeView {
fn snap(node: &Node) -> TreeView {
match node {
Node::Pane(pane) => TreeView::Pane(PaneView {
key: pane.key,
tabs: pane
.tabs
.iter()
.map(|tab| TabView {
panel: tab.panel.clone(),
title: tab.title.clone(),
icon: tab.icon,
view: tab.view.clone(),
})
.collect(),
active: pane.active.clone(),
}),
Node::Split(split) => TreeView::Split(SplitView {
key: split.key,
axis: split.axis,
ratio: split.ratio,
a: Box::new(snap(&split.a)),
b: Box::new(snap(&split.b)),
}),
}
}
snap(&self.root)
}
}
/// Finds a pane by key anywhere in the tree.
fn pane_in(node: &Node, key: u64) -> Option<&Pane> {
match node {
Node::Pane(pane) => (pane.key == key).then_some(pane),
Node::Split(split) => pane_in(&split.a, key).or_else(|| pane_in(&split.b, key)),
}
}
/// The pane key of a node that is a pane, for seam-level tree edits.
fn direct_pane_key(node: &Node) -> Option<u64> {
match node {
Node::Pane(pane) => Some(pane.key),
Node::Split(_) => None,
}
}
/// Takes a node out, leaving a placeholder that is never read again.
fn take_node(node: &mut Node) -> Node {
std::mem::replace(
node,
Node::Pane(Pane {
key: u64::MAX,
tabs: Vec::new(),
active: None,
}),
)
}
/// One open panel view, per frame.
#[derive(Clone)]
struct TabView {
panel: SharedString,
title: SharedString,
icon: Option<IconRole>,
view: AnyView,
}
#[derive(Clone)]
struct PaneView {
key: u64,
tabs: Vec<TabView>,
active: Option<SharedString>,
}
struct SplitView {
key: u64,
axis: Axis,
ratio: f32,
a: Box<TreeView>,
b: Box<TreeView>,
}
enum TreeView {
Pane(PaneView),
Split(SplitView),
}
/// A dock: renders the pane tree of a [`DockState`].
#[derive(IntoElement)]
pub struct Dock {
id: ElementId,
state: Entity<DockState>,
}
impl Dock {
/// Creates a dock bound to `state`.
pub fn new<I>(id: I, state: Entity<DockState>) -> Self
where
I: Into<ElementId>,
{
Self {
id: id.into(),
state,
}
}
}
impl RenderOnce for Dock {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let tree = self.state.read(cx).snapshot();
let root = render_node(&self.id, &self.state, &tree, window, cx);
div()
.id(self.id.clone())
.debug_selector({
let id = self.id.clone();
move || format!("dock-{id}")
})
.size_full()
.flex()
.flex_col()
.overflow_hidden()
.child(root)
}
}
fn render_node(
dock_id: &ElementId,
state: &Entity<DockState>,
node: &TreeView,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
match node {
TreeView::Pane(pane) => render_pane(dock_id, state, pane, cx),
TreeView::Split(split) => render_split(dock_id, state, split, window, cx),
}
}
fn render_split(
dock_id: &ElementId,
state: &Entity<DockState>,
split: &SplitView,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let span_state = window.with_element_namespace(dock_id.clone(), |window| {
window
.use_keyed_state(("split-span", split.key), cx, |_, _| size(px(0.), px(0.)))
.clone()
});
let seam = ResizeHandle::new(SharedString::from(format!("{dock_id}-seam-{}", split.key)))
.axis(split.axis)
.on_resize({
let span_state = span_state.clone();
let state = state.clone();
let axis = split.axis;
let key = split.key;
move |delta, _, cx| {
let span = match axis {
Axis::Horizontal => span_state.read(cx).width,
Axis::Vertical => span_state.read(cx).height,
};
if span > px(0.) {
let fraction = delta / span;
state.update(cx, |dock, cx| dock.resize_split(key, fraction, cx));
}
}
})
.on_reset({
let state = state.clone();
let key = split.key;
move |_, cx| state.update(cx, |dock, cx| dock.reset_split(key, cx))
});
div()
.id(SharedString::from(format!("{dock_id}-split-{}", split.key)))
.size_full()
.min_w_0()
.min_h_0()
.overflow_hidden()
.flex()
.when(split.axis == Axis::Horizontal, |split_el| {
split_el.flex_row()
})
.when(split.axis == Axis::Vertical, |split_el| split_el.flex_col())
.child(
canvas(|bounds, _, _| bounds.size, {
let span_state = span_state.clone();
move |_, span, _, cx| span_state.update(cx, |slot, _| *slot = span)
})
.absolute()
.inset_0(),
)
.child(
div()
.flex_grow(split.ratio)
.flex_basis(relative(0.))
.min_w_0()
.min_h_0()
.overflow_hidden()
.child(render_node(dock_id, state, &split.a, window, cx)),
)
.child(seam)
.child(
div()
.flex_grow(1.0 - split.ratio)
.flex_basis(relative(0.))
.min_w_0()
.min_h_0()
.overflow_hidden()
.child(render_node(dock_id, state, &split.b, window, cx)),
)
.into_any_element()
}
fn render_pane(
dock_id: &ElementId,
state: &Entity<DockState>,
pane: &PaneView,
cx: &App,
) -> AnyElement {
if pane.tabs.is_empty() {
return div().size_full().into_any_element();
}
let theme = cx.theme();
let metrics = theme.sizes.resolve(Size::default());
let pane_id = SharedString::from(format!("{dock_id}-pane-{}", pane.key));
let active_view = pane
.tabs
.iter()
.find(|tab| Some(&tab.panel) == pane.active.as_ref())
.map(|tab| tab.view.clone());
// The cache keeps an un-notified panel from re-rendering on dock frames.
let content = div()
.id(SharedString::from(format!(
"{dock_id}-content-{}",
pane.key
)))
.debug_selector({
let id = SharedString::from(format!("{dock_id}-content-{}", pane.key));
move || id.to_string()
})
.flex_1()
.min_h_0()
.overflow_hidden()
.role(Role::TabPanel)
.when_some(active_view, |content, view| {
content.child(view.cached(StyleRefinement::default().absolute().size_full()))
})
.when(cx.has_active_drag(), |content| {
content.child(render_drop_zones(state, pane))
});
let menu = ContextMenu::new(SharedString::from(format!(
"{dock_id}-pane-{}-menu",
pane.key
)))
.target({
let dock_id = dock_id.clone();
let state = state.clone();
let pane = pane.clone();
let strip_height = metrics.strip_height;
let padding_x = metrics.dense_padding_x;
move |_, _| {
let selected = pane
.active
.as_ref()
.and_then(|active| pane.tabs.iter().position(|tab| &tab.panel == active))
.unwrap_or(0);
let tabs = Tabs::new(SharedString::from(format!(
"{dock_id}-pane-{}-tabs",
pane.key
)))
.variant(TabsVariant::Pill)
.selected(selected)
.tabs({
let tabs = pane.tabs.clone();
move |_, _| {
tabs.iter()
.map(|tab| {
let mut entry = Tab::new(tab.title.clone());
if let Some(role) = tab.icon {
entry = entry.icon(role);
}
entry.closeable(true).wrap({
let panel = tab.panel.clone();
let title = tab.title.clone();
move |tab| {
tab.on_drag(
PanelDrag {
panel: panel.clone(),
},
{
let title = title.clone();
move |_, _, _, cx| {
cx.new(|_| DragPreview {
title: title.clone(),
})
}
},
)
.into_any_element()
}
})
})
.collect()
}
})
.on_change({
let state = state.clone();
let panels: Vec<SharedString> =
pane.tabs.iter().map(|tab| tab.panel.clone()).collect();
move |index, _, cx| {
if let Some(panel) = panels.get(index) {
state.update(cx, |dock, cx| dock.activate_panel(panel, cx));
}
}
})
.on_close({
let state = state.clone();
let panels: Vec<SharedString> =
pane.tabs.iter().map(|tab| tab.panel.clone()).collect();
move |index, _, cx| {
if let Some(panel) = panels.get(index) {
state.update(cx, |dock, cx| dock.close_panel(panel, cx));
}
}
});
// Chrome-less like the future titlebar: the pills carry the look.
h_flex()
.flex_none()
.h(strip_height)
.w_full()
.items_center()
.pr(padding_x)
.child(tabs)
.child(div().flex_1())
.into_any_element()
}
})
.items({
let state = state.clone();
let active = pane.active.clone();
let panels: Vec<SharedString> = pane.tabs.iter().map(|tab| tab.panel.clone()).collect();
move |_, _| {
let mut items = Vec::new();
if let Some(active) = &active {
items.push(MenuItem::new("Close").icon(IconRole::Close).handler({
let state = state.clone();
let active = active.clone();
move |_, cx| state.update(cx, |dock, cx| dock.close_panel(&active, cx))
}));
if panels.len() > 1 {
items.push(MenuItem::new("Close Others").handler({
let state = state.clone();
let active = active.clone();
move |_, cx| state.update(cx, |dock, cx| dock.close_others(&active, cx))
}));
}
}
if let Some(first) = panels.first() {
items.push(MenuItem::new("Close Pane").handler({
let state = state.clone();
let first = first.clone();
move |_, cx| state.update(cx, |dock, cx| dock.close_pane(&first, cx))
}));
}
items
}
});
v_flex()
.id(pane_id.clone())
.debug_selector({
let id = pane_id.clone();
move || id.to_string()
})
.size_full()
.overflow_hidden()
.child(menu)
.child(content)
.into_any_element()
}
/// The five drop targets a live tab drag sees over a pane's content:
///
/// - Four edge bands that split
/// - The center that moves the tab in.
fn render_drop_zones(state: &Entity<DockState>, pane: &PaneView) -> AnyElement {
let Some(anchor) = pane
.active
.clone()
.or_else(|| pane.tabs.first().map(|tab| tab.panel.clone()))
else {
return div().into_any_element();
};
div()
.absolute()
.inset_0()
.child(
drop_zone(state, &anchor, Some(SplitDirection::Left))
.absolute()
.top_0()
.bottom_0()
.left_0()
.w(relative(0.25)),
)
.child(
drop_zone(state, &anchor, Some(SplitDirection::Right))
.absolute()
.top_0()
.bottom_0()
.right_0()
.w(relative(0.25)),
)
.child(
drop_zone(state, &anchor, Some(SplitDirection::Top))
.absolute()
.top_0()
.left(relative(0.25))
.right(relative(0.25))
.h(relative(0.25)),
)
.child(
drop_zone(state, &anchor, Some(SplitDirection::Bottom))
.absolute()
.bottom_0()
.left(relative(0.25))
.right(relative(0.25))
.h(relative(0.25)),
)
.child(
drop_zone(state, &anchor, None)
.absolute()
.inset(relative(0.25)),
)
.into_any_element()
}
/// Renders a drop zone for panel dragging.
fn drop_zone(
state: &Entity<DockState>,
anchor: &str,
direction: Option<SplitDirection>,
) -> gpui::Div {
let state = state.clone();
let anchor = SharedString::from(anchor);
div()
.drag_over::<PanelDrag>(|zone, _, _, cx| zone.bg(cx.theme().colors.selection))
.on_drop::<PanelDrag>(move |drag, _, cx| {
state.update(cx, |dock, cx| match direction {
Some(direction) => {
dock.split_panel_at(&drag.panel, &anchor, direction, cx);
}
None => dock.move_panel(&drag.panel, &anchor, cx),
});
})
}