use super::{DragData, Draggable, DropZone, DropZoneStyle};
use crate::{prelude::FluentBuilder as _, *};
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DockPosition {
Left,
Right,
Bottom,
Center,
}
impl DockPosition {
fn name(&self) -> &'static str {
match self {
DockPosition::Left => "left",
DockPosition::Right => "right",
DockPosition::Bottom => "bottom",
DockPosition::Center => "center",
}
}
fn from_name(name: &str) -> Option<Self> {
match name {
"left" => Some(DockPosition::Left),
"right" => Some(DockPosition::Right),
"bottom" => Some(DockPosition::Bottom),
"center" => Some(DockPosition::Center),
_ => None,
}
}
fn all() -> [DockPosition; 4] {
[
DockPosition::Left,
DockPosition::Center,
DockPosition::Right,
DockPosition::Bottom,
]
}
}
pub struct DockPanel {
pub id: SharedString,
pub title: SharedString,
pub position: DockPosition,
pub closable: bool,
pub content: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
}
impl DockPanel {
pub fn new(
id: impl Into<SharedString>,
title: impl Into<SharedString>,
position: DockPosition,
content: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
Self {
id: id.into(),
title: title.into(),
position,
closable: true,
content: Rc::new(content),
}
}
pub fn closable(mut self, closable: bool) -> Self {
self.closable = closable;
self
}
}
pub struct DockAreaState {
panels: Vec<DockPanel>,
active: HashMap<DockPosition, SharedString>,
hidden: HashSet<DockPosition>,
left_width: Pixels,
right_width: Pixels,
bottom_height: Pixels,
}
impl DockAreaState {
pub fn new() -> Self {
Self {
panels: Vec::new(),
active: HashMap::new(),
hidden: HashSet::new(),
left_width: px(240.0),
right_width: px(280.0),
bottom_height: px(200.0),
}
}
pub fn add_panel(&mut self, panel: DockPanel, cx: &mut Context<Self>) {
let id = panel.id.clone();
let position = panel.position;
if let Some(existing) = self.panels.iter_mut().find(|p| p.id == id) {
existing.title = panel.title;
existing.position = position;
existing.closable = panel.closable;
existing.content = panel.content;
} else {
self.panels.push(panel);
}
self.active.insert(position, id);
self.hidden.remove(&position);
cx.notify();
}
pub fn remove_panel(&mut self, id: &str, cx: &mut Context<Self>) -> bool {
let Some(ix) = self.panels.iter().position(|p| p.id == id) else {
return false;
};
let position = self.panels[ix].position;
self.panels.remove(ix);
if self
.active
.get(&position)
.is_some_and(|active| active == id)
{
self.active.remove(&position);
if let Some(next) = self.panels.iter().find(|p| p.position == position) {
self.active.insert(position, next.id.clone());
}
}
cx.notify();
true
}
pub fn move_panel(&mut self, id: &str, position: DockPosition, cx: &mut Context<Self>) {
if let Some(panel) = self.panels.iter_mut().find(|p| p.id == id) {
panel.position = position;
self.active.insert(position, panel.id.clone());
self.hidden.remove(&position);
cx.notify();
}
}
pub fn set_active(&mut self, position: DockPosition, id: &str, cx: &mut Context<Self>) {
if self
.panels
.iter()
.any(|p| p.id == id && p.position == position)
{
self.active.insert(position, id.into());
cx.notify();
}
}
pub fn set_visible(&mut self, position: DockPosition, visible: bool, cx: &mut Context<Self>) {
if position == DockPosition::Center {
return;
}
if visible {
self.hidden.remove(&position);
} else {
self.hidden.insert(position);
}
cx.notify();
}
pub fn is_visible(&self, position: DockPosition) -> bool {
position == DockPosition::Center
|| (!self.hidden.contains(&position)
&& self.panels.iter().any(|p| p.position == position))
}
pub fn set_left_width(&mut self, width: Pixels, cx: &mut Context<Self>) {
self.left_width = width;
cx.notify();
}
pub fn set_right_width(&mut self, width: Pixels, cx: &mut Context<Self>) {
self.right_width = width;
cx.notify();
}
pub fn set_bottom_height(&mut self, height: Pixels, cx: &mut Context<Self>) {
self.bottom_height = height;
cx.notify();
}
fn region_panels(&self, position: DockPosition) -> Vec<&DockPanel> {
self.panels
.iter()
.filter(|p| p.position == position)
.collect()
}
fn region_active(&self, position: DockPosition) -> Option<SharedString> {
if let Some(active) = self.active.get(&position) {
if self.panels.iter().any(|p| p.id == *active) {
return Some(active.clone());
}
}
self.panels
.iter()
.find(|p| p.position == position)
.map(|p| p.id.clone())
}
pub fn layout_json(&self) -> String {
let panels: Vec<serde_json::Value> = self
.panels
.iter()
.map(|p| {
serde_json::json!({
"id": p.id.to_string(),
"title": p.title.to_string(),
"position": p.position.name(),
"closable": p.closable,
})
})
.collect();
let active: HashMap<&str, String> = self
.active
.iter()
.map(|(pos, id)| (pos.name(), id.to_string()))
.collect();
serde_json::json!({
"version": 1,
"panels": panels,
"active": active,
"left_width": self.left_width.0,
"right_width": self.right_width.0,
"bottom_height": self.bottom_height.0,
})
.to_string()
}
pub fn restore_layout(&mut self, json: &str, cx: &mut Context<Self>) -> bool {
let Ok(value) = serde_json::from_str::<serde_json::Value>(json) else {
return false;
};
let Some(panels) = value.get("panels").and_then(|v| v.as_array()) else {
return false;
};
for item in panels {
let (Some(id), Some(position)) = (
item.get("id").and_then(|v| v.as_str()),
item.get("position")
.and_then(|v| v.as_str())
.and_then(DockPosition::from_name),
) else {
continue;
};
if let Some(panel) = self.panels.iter_mut().find(|p| p.id == id) {
panel.position = position;
}
}
self.active.clear();
if let Some(active) = value.get("active").and_then(|v| v.as_object()) {
for (pos_name, id) in active {
if let (Some(position), Some(id)) = (DockPosition::from_name(pos_name), id.as_str())
{
self.active.insert(position, id.into());
}
}
}
if let Some(w) = value.get("left_width").and_then(|v| v.as_f64()) {
self.left_width = px(w as f32);
}
if let Some(w) = value.get("right_width").and_then(|v| v.as_f64()) {
self.right_width = px(w as f32);
}
if let Some(h) = value.get("bottom_height").and_then(|v| v.as_f64()) {
self.bottom_height = px(h as f32);
}
cx.notify();
true
}
}
impl Default for DockAreaState {
fn default() -> Self {
Self::new()
}
}
#[derive(IntoElement)]
pub struct DockArea {
state: Entity<DockAreaState>,
style: StyleRefinement,
}
impl DockArea {
pub fn new(state: Entity<DockAreaState>) -> Self {
Self {
state,
style: StyleRefinement::default(),
}
}
}
impl Styled for DockArea {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for DockArea {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let muted_foreground = theme.tokens.muted_foreground.color;
let accent = theme.tokens.accent.color;
let panel_bg = theme.tokens.popover;
let border = theme.tokens.border.color;
let user_style = self.style;
let state = self.state.clone();
let snapshot = self.state.read(cx);
let mut region_ids: HashMap<DockPosition, Vec<(SharedString, SharedString, bool)>> =
HashMap::new();
for position in DockPosition::all() {
region_ids.insert(
position,
snapshot
.region_panels(position)
.iter()
.map(|p| (p.id.clone(), p.title.clone(), p.closable))
.collect(),
);
}
let active: HashMap<DockPosition, Option<SharedString>> = DockPosition::all()
.into_iter()
.map(|pos| (pos, snapshot.region_active(pos)))
.collect();
let (left_width, right_width, bottom_height) = (
snapshot.left_width,
snapshot.right_width,
snapshot.bottom_height,
);
let visible: HashMap<DockPosition, bool> = DockPosition::all()
.into_iter()
.map(|pos| (pos, snapshot.is_visible(pos)))
.collect();
let mut render_region = |position: DockPosition, size: Box<dyn FnOnce(Div) -> Div>| {
let panels = region_ids.get(&position).cloned().unwrap_or_default();
if panels.is_empty() || !visible.get(&position).copied().unwrap_or(false) {
return div().into_any_element();
}
let active_id = active.get(&position).cloned().flatten();
let mut header = div().flex().flex_row().items_center().gap(px(2.0));
for (id, title, closable) in &panels {
let is_active = active_id.as_ref() == Some(id);
let tab_state = state.clone();
let tab_id = id.clone();
let tab_title = title.clone();
let mut tab = div()
.id(format!("dock-tab-body-{id}"))
.flex()
.flex_row()
.items_center()
.gap(px(6.0))
.px(px(10.0))
.py(px(6.0))
.rounded_md()
.cursor_pointer()
.when(is_active, |this| this.bg(accent.opacity(0.12)))
.when(!is_active, |this| {
this.hover(|this| this.bg(accent.opacity(0.06)))
})
.child(
div()
.text_sm()
.text_color(if is_active { accent } else { muted_foreground })
.child(tab_title.clone()),
)
.on_click(move |_, _, cx| {
tab_state.update(cx, |state, cx| {
state.set_active(position, &tab_id, cx);
});
});
if *closable {
let close_state = state.clone();
let close_id = id.clone();
tab = tab.child(
div()
.id(format!("dock-close-{id}"))
.text_xs()
.text_color(muted_foreground)
.cursor_pointer()
.child("×")
.on_click(move |_, _, cx| {
close_state.update(cx, |state, cx| {
state.remove_panel(&close_id, cx);
});
}),
);
}
header = header.child(
Draggable::new(
format!("dock-tab-{id}"),
DragData::new(id.clone()).with_label(title.clone()),
)
.child(tab),
);
}
let content = self
.state
.read_with(cx, |state, _| {
active_id.as_ref().and_then(|active| {
state
.panels
.iter()
.find(|p| &p.id == active)
.map(|p| p.content.clone())
})
})
.map(|factory| factory(window, cx));
let mut body = div().flex_1().overflow_hidden();
if let Some(content) = content {
body = body.child(content);
}
let region = div()
.flex()
.flex_col()
.h_full()
.bg(panel_bg)
.child(header)
.child(div().h(px(1.0)).w_full().bg(border))
.child(body);
let region = size(region);
let drop_state = state.clone();
DropZone::<SharedString>::new(format!("dock-drop-{}", position.name()))
.drop_zone_style(DropZoneStyle::Filled)
.on_drop(move |data: &DragData<SharedString>, _, cx| {
drop_state.update(cx, |state, cx| {
state.move_panel(&data.data, position, cx);
});
})
.child(region)
.into_any_element()
};
let left = render_region(DockPosition::Left, Box::new(move |d| d.w(left_width)));
let center = render_region(DockPosition::Center, Box::new(move |d| d.flex_1()));
let right = render_region(DockPosition::Right, Box::new(move |d| d.w(right_width)));
let bottom = render_region(
DockPosition::Bottom,
Box::new(move |d| d.w_full().h(bottom_height)),
);
div()
.flex()
.flex_col()
.w_full()
.h_full()
.overflow_hidden()
.child(
div()
.flex()
.flex_row()
.flex_1()
.overflow_hidden()
.child(left)
.child(center)
.child(right),
)
.child(bottom)
.map(|mut this| {
this.style().refine(&user_style);
this
})
}
}