use std::collections::HashMap;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use crate::area::DockAreaStyle;
pub const SYNTHETIC_AREA_ID_PREFIX: &str = "split.";
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub struct NodeId(pub u64);
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub struct TabId(pub u64);
impl TabId {
pub(crate) const PENDING: TabId = TabId(0);
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum SplitAxis {
Horizontal,
Vertical,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Edge {
Top,
Bottom,
Left,
Right,
}
impl Edge {
pub fn axis(self) -> SplitAxis {
match self {
Edge::Top | Edge::Bottom => SplitAxis::Vertical,
Edge::Left | Edge::Right => SplitAxis::Horizontal,
}
}
pub fn puts_new_in_a(self) -> bool {
matches!(self, Edge::Top | Edge::Left)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DockTabEntry {
pub window_id: String,
pub id: TabId,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DockLeaf {
pub area_id: String,
pub style: DockAreaStyle,
pub windows: Vec<DockTabEntry>,
pub active: Option<TabId>,
#[serde(default)]
pub persistent: bool,
}
impl DockLeaf {
pub fn new(area_id: impl Into<String>, style: DockAreaStyle) -> Self {
Self {
area_id: area_id.into(),
style,
windows: Vec::new(),
active: None,
persistent: false,
}
}
pub fn with_windows(mut self, windows: Vec<String>) -> Self {
self.windows = windows
.into_iter()
.map(|window_id| DockTabEntry {
window_id,
id: TabId::PENDING,
})
.collect();
self.active = self.windows.first().map(|t| t.id);
self
}
pub fn persistent(mut self) -> Self {
self.persistent = true;
self
}
pub fn is_persistent(&self) -> bool {
self.persistent
}
pub fn tabs(&self) -> impl Iterator<Item = (&str, TabId)> {
self.windows.iter().map(|t| (t.window_id.as_str(), t.id))
}
pub fn tab_index(&self, id: TabId) -> Option<usize> {
self.windows.iter().position(|t| t.id == id)
}
pub fn has_window(&self, window_id: &str) -> bool {
self.windows.iter().any(|t| t.window_id == window_id)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DockSplit {
pub axis: SplitAxis,
pub fraction: f32,
pub a: NodeId,
pub b: NodeId,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum DockNode {
Leaf(DockLeaf),
Split(DockSplit),
}
impl DockNode {
pub fn as_leaf(&self) -> Option<&DockLeaf> {
match self {
DockNode::Leaf(l) => Some(l),
_ => None,
}
}
pub fn as_leaf_mut(&mut self) -> Option<&mut DockLeaf> {
match self {
DockNode::Leaf(l) => Some(l),
_ => None,
}
}
pub fn as_split(&self) -> Option<&DockSplit> {
match self {
DockNode::Split(s) => Some(s),
_ => None,
}
}
pub fn as_split_mut(&mut self) -> Option<&mut DockSplit> {
match self {
DockNode::Split(s) => Some(s),
_ => None,
}
}
}
#[derive(Resource, Clone, Debug, Default, Serialize, Deserialize)]
pub struct DockTree {
pub nodes: HashMap<NodeId, DockNode>,
pub root: Option<NodeId>,
#[serde(default)]
next_id: u64,
#[serde(default)]
next_tab_id: u64,
}
impl DockTree {
pub fn new() -> Self {
Self::default()
}
fn fresh_id(&mut self) -> NodeId {
let id = NodeId(self.next_id);
self.next_id = self.next_id.wrapping_add(1);
id
}
fn fresh_tab_id(&mut self) -> TabId {
self.next_tab_id = self.next_tab_id.saturating_add(1).max(1);
TabId(self.next_tab_id)
}
pub fn insert(&mut self, mut node: DockNode) -> NodeId {
if let DockNode::Leaf(ref mut leaf) = node {
self.assign_pending_tab_ids(leaf);
}
let id = self.fresh_id();
self.nodes.insert(id, node);
id
}
fn assign_pending_tab_ids(&mut self, leaf: &mut DockLeaf) {
let active_was_pending = leaf.active == Some(TabId::PENDING);
let mut first_real: Option<TabId> = None;
for tab in leaf.windows.iter_mut() {
if tab.id == TabId::PENDING {
tab.id = self.fresh_tab_id();
}
if first_real.is_none() {
first_real = Some(tab.id);
}
}
if active_was_pending {
leaf.active = first_real;
}
}
pub fn add_tab(&mut self, leaf: NodeId, window_id: impl Into<String>) -> Option<TabId> {
let window_id = window_id.into();
if !matches!(self.nodes.get(&leaf), Some(DockNode::Leaf(_))) {
return None;
}
let id = self.fresh_tab_id();
let DockNode::Leaf(l) = self.nodes.get_mut(&leaf)? else {
return None;
};
l.windows.push(DockTabEntry { window_id, id });
l.active = Some(id);
Some(id)
}
pub fn get(&self, id: NodeId) -> Option<&DockNode> {
self.nodes.get(&id)
}
pub fn get_mut(&mut self, id: NodeId) -> Option<&mut DockNode> {
self.nodes.get_mut(&id)
}
pub fn set_root_leaf(&mut self, leaf: DockLeaf) -> NodeId {
let id = self.insert(DockNode::Leaf(leaf));
self.root = Some(id);
id
}
pub fn leaves_under(&self, root: NodeId) -> Vec<(NodeId, &DockLeaf)> {
let mut out = Vec::new();
self.leaves_under_inner(root, &mut out);
out
}
fn leaves_under_inner<'a>(&'a self, id: NodeId, out: &mut Vec<(NodeId, &'a DockLeaf)>) {
match self.nodes.get(&id) {
Some(DockNode::Leaf(l)) => out.push((id, l)),
Some(DockNode::Split(s)) => {
let (a, b) = (s.a, s.b);
self.leaves_under_inner(a, out);
self.leaves_under_inner(b, out);
}
None => {}
}
}
pub fn find_leaf_with_window(&self, window_id: &str) -> Option<NodeId> {
self.nodes.iter().find_map(|(id, node)| match node {
DockNode::Leaf(l) if l.has_window(window_id) => Some(*id),
_ => None,
})
}
pub fn find_leaf_for_tab(&self, tab: TabId) -> Option<NodeId> {
self.nodes.iter().find_map(|(id, node)| match node {
DockNode::Leaf(l) if l.windows.iter().any(|t| t.id == tab) => Some(*id),
_ => None,
})
}
pub fn tabs(&self) -> impl Iterator<Item = (NodeId, &DockTabEntry)> {
self.leaves()
.flat_map(|(leaf_id, leaf)| leaf.windows.iter().map(move |t| (leaf_id, t)))
}
pub fn find_by_area_id(&self, area_id: &str) -> Option<NodeId> {
self.nodes.iter().find_map(|(id, node)| match node {
DockNode::Leaf(l) if l.area_id == area_id => Some(*id),
_ => None,
})
}
pub fn parent_of(&self, child: NodeId) -> Option<NodeId> {
self.nodes.iter().find_map(|(id, node)| match node {
DockNode::Split(s) if s.a == child || s.b == child => Some(*id),
_ => None,
})
}
pub fn leaves(&self) -> impl Iterator<Item = (NodeId, &DockLeaf)> {
self.nodes.iter().filter_map(|(id, node)| match node {
DockNode::Leaf(l) => Some((*id, l)),
_ => None,
})
}
pub fn iter_dfs(&self) -> Vec<(NodeId, usize)> {
let mut out = Vec::new();
if let Some(root) = self.root {
self.dfs_into(root, 0, &mut out);
}
out
}
fn dfs_into(&self, id: NodeId, depth: usize, out: &mut Vec<(NodeId, usize)>) {
out.push((id, depth));
if let Some(DockNode::Split(s)) = self.nodes.get(&id) {
let (a, b) = (s.a, s.b);
self.dfs_into(a, depth + 1, out);
self.dfs_into(b, depth + 1, out);
}
}
pub fn split(&mut self, target: NodeId, edge: Edge, window: String) -> Option<(NodeId, TabId)> {
if !matches!(self.nodes.get(&target), Some(DockNode::Leaf(_))) {
return None;
}
let new_style = self
.nodes
.get(&target)
.and_then(|n| n.as_leaf())
.map(|l| l.style.clone())
.unwrap_or_default();
let new_leaf_id = self.fresh_id();
let tab_id = self.fresh_tab_id();
self.nodes.insert(
new_leaf_id,
DockNode::Leaf(DockLeaf {
area_id: fresh_area_id(&window, new_leaf_id),
style: new_style,
windows: vec![DockTabEntry {
window_id: window,
id: tab_id,
}],
active: Some(tab_id),
persistent: false,
}),
);
let parent = self.parent_of(target);
let (a, b) = if edge.puts_new_in_a() {
(new_leaf_id, target)
} else {
(target, new_leaf_id)
};
let split_id = self.insert(DockNode::Split(DockSplit {
axis: edge.axis(),
fraction: 0.5,
a,
b,
}));
match parent {
Some(parent_id) => {
if let Some(DockNode::Split(s)) = self.nodes.get_mut(&parent_id) {
if s.a == target {
s.a = split_id;
}
if s.b == target {
s.b = split_id;
}
}
}
None => {
if self.root == Some(target) {
self.root = Some(split_id);
}
}
}
Some((new_leaf_id, tab_id))
}
pub fn set_fraction(&mut self, split: NodeId, fraction: f32) {
if let Some(DockNode::Split(s)) = self.nodes.get_mut(&split) {
s.fraction = fraction.clamp(0.05, 0.95);
}
}
pub fn set_active(&mut self, leaf: NodeId, tab: TabId) {
if let Some(DockNode::Leaf(l)) = self.nodes.get_mut(&leaf)
&& l.windows.iter().any(|t| t.id == tab)
{
l.active = Some(tab);
}
}
pub fn move_tab(&mut self, tab: TabId, to: NodeId) {
self.insert_tab(tab, to, false, None);
}
pub fn insert_tab(&mut self, tab: TabId, to: NodeId, allow_same: bool, index: Option<usize>) {
let Some(from) = self.find_leaf_for_tab(tab) else {
return;
};
if !allow_same && from == to {
return;
}
if !matches!(self.nodes.get(&to), Some(DockNode::Leaf(_))) {
return;
}
let entry = {
let Some(DockNode::Leaf(l)) = self.nodes.get_mut(&from) else {
return;
};
let Some(pos) = l.windows.iter().position(|t| t.id == tab) else {
return;
};
let entry = l.windows.remove(pos);
if l.active == Some(tab) {
l.active = l.windows.first().map(|t| t.id);
}
entry
};
if let Some(DockNode::Leaf(l)) = self.nodes.get_mut(&to) {
let new_id = entry.id;
match index {
Some(idx) => l.windows.insert(idx.clamp(0, l.windows.len()), entry),
None => l.windows.push(entry),
}
l.active = Some(new_id);
}
self.simplify();
}
pub fn remove_tab(&mut self, tab: TabId) {
let Some(leaf) = self.find_leaf_for_tab(tab) else {
return;
};
if let Some(DockNode::Leaf(l)) = self.nodes.get_mut(&leaf) {
l.windows.retain(|t| t.id != tab);
if l.active == Some(tab) {
l.active = l.windows.first().map(|t| t.id);
}
}
self.simplify();
}
pub fn remove_window_kind(&mut self, window_id: &str) {
let to_remove: Vec<TabId> = self
.tabs()
.filter(|(_, t)| t.window_id == window_id)
.map(|(_, t)| t.id)
.collect();
for tab in to_remove {
self.remove_tab(tab);
}
}
pub fn simplify(&mut self) {
loop {
let root = self.root;
let empty_leaf_with_parent: Option<NodeId> = self
.nodes
.iter()
.find(|(id, node)| match node {
DockNode::Leaf(l) => {
l.windows.is_empty() && Some(**id) != root && !l.is_persistent()
}
_ => false,
})
.map(|(id, _)| *id);
let Some(empty_id) = empty_leaf_with_parent else {
return;
};
let Some(parent_id) = self.parent_of(empty_id) else {
self.nodes.remove(&empty_id);
continue;
};
let Some(DockNode::Split(s)) = self.nodes.get(&parent_id).cloned() else {
return;
};
let survivor = if s.a == empty_id { s.b } else { s.a };
let grandparent = self.parent_of(parent_id);
match grandparent {
Some(gp_id) => {
if let Some(DockNode::Split(gs)) = self.nodes.get_mut(&gp_id) {
if gs.a == parent_id {
gs.a = survivor;
}
if gs.b == parent_id {
gs.b = survivor;
}
}
}
None => {
if self.root == Some(parent_id) {
self.root = Some(survivor);
}
}
}
self.nodes.remove(&empty_id);
self.nodes.remove(&parent_id);
}
}
}
fn fresh_area_id(window_id: &str, leaf_id: NodeId) -> String {
format!("{SYNTHETIC_AREA_ID_PREFIX}{window_id}.{}", leaf_id.0)
}
#[cfg(test)]
mod tests {
use crate::area::{DefaultArea, ToAnchorId};
use super::*;
fn leaf(area_id: &str, windows: &[&str]) -> DockLeaf {
DockLeaf::new(area_id, DockAreaStyle::TabBar)
.with_windows(windows.iter().map(ToString::to_string).collect())
}
fn window_ids(t: &DockTree, leaf: NodeId) -> Vec<String> {
t.nodes[&leaf]
.as_leaf()
.unwrap()
.windows
.iter()
.map(|w| w.window_id.clone())
.collect()
}
fn active_window_id<'a>(t: &'a DockTree, leaf: NodeId) -> Option<&'a str> {
let l = t.nodes[&leaf].as_leaf()?;
let id = l.active?;
l.windows
.iter()
.find(|w| w.id == id)
.map(|w| w.window_id.as_str())
}
fn tab_id_for(t: &DockTree, leaf: NodeId, window_id: &str) -> TabId {
t.nodes[&leaf]
.as_leaf()
.unwrap()
.windows
.iter()
.find(|w| w.window_id == window_id)
.unwrap()
.id
}
#[test]
fn set_root_leaf_works() {
let mut t = DockTree::new();
let id = t.set_root_leaf(leaf("root", &["a"]));
assert_eq!(t.root, Some(id));
assert_eq!(t.leaves().count(), 1);
}
#[test]
fn pending_tab_ids_are_stamped_on_insert() {
let mut t = DockTree::new();
let root = t.set_root_leaf(leaf("root", &["a", "b"]));
let l = t.nodes[&root].as_leaf().unwrap();
assert!(l.windows.iter().all(|w| w.id != TabId::PENDING));
assert_eq!(l.active, Some(l.windows[0].id));
}
#[test]
fn split_inserts_new_leaf_and_wraps_target() {
let mut t = DockTree::new();
let root = t.set_root_leaf(leaf("root", &["a"]));
let (new_leaf, _) = t.split(root, Edge::Right, "b".into()).unwrap();
let root_split = t.nodes[&t.root.unwrap()].as_split().unwrap();
assert_eq!(root_split.axis, SplitAxis::Horizontal);
assert_eq!(root_split.a, root);
assert_eq!(root_split.b, new_leaf);
assert_eq!(root_split.fraction, 0.5);
assert_eq!(window_ids(&t, root), vec!["a"]);
assert_eq!(window_ids(&t, new_leaf), vec!["b"]);
}
#[test]
fn split_top_puts_new_in_a() {
let mut t = DockTree::new();
let root = t.set_root_leaf(leaf("root", &["a"]));
let (new_leaf, _) = t.split(root, Edge::Top, "b".into()).unwrap();
let s = t.nodes[&t.root.unwrap()].as_split().unwrap();
assert_eq!(s.a, new_leaf);
assert_eq!(s.b, root);
}
#[test]
fn split_bottom_puts_new_in_b() {
let mut t = DockTree::new();
let root = t.set_root_leaf(leaf("root", &["a"]));
let (new_leaf, _) = t.split(root, Edge::Bottom, "b".into()).unwrap();
let s = t.nodes[&t.root.unwrap()].as_split().unwrap();
assert_eq!(s.a, root);
assert_eq!(s.b, new_leaf);
}
#[test]
fn split_of_nested_leaf_preserves_other_sibling() {
let mut t = DockTree::new();
let root = t.set_root_leaf(leaf(&DefaultArea::Left.anchor_id(), &["a"]));
let (right, _) = t.split(root, Edge::Right, "b".into()).unwrap();
let _deeper = t.split(right, Edge::Bottom, "c".into()).unwrap();
assert_eq!(window_ids(&t, root), vec!["a"]);
let b_leaf = t.find_leaf_with_window("b").unwrap();
assert_eq!(window_ids(&t, b_leaf), vec!["b"]);
}
#[test]
fn move_tab_relocates_and_activates() {
let mut t = DockTree::new();
let root = t.set_root_leaf(leaf("root", &["a", "b"]));
let (right, _) = t.split(root, Edge::Right, "c".into()).unwrap();
let tab_a = tab_id_for(&t, root, "a");
t.move_tab(tab_a, right);
assert_eq!(window_ids(&t, root), vec!["b"]);
assert_eq!(window_ids(&t, right), vec!["c", "a"]);
assert_eq!(active_window_id(&t, right), Some("a"));
}
#[test]
fn move_last_tab_simplifies_tree() {
let mut t = DockTree::new();
let root = t.set_root_leaf(leaf("root", &["a"]));
let (right, _) = t.split(root, Edge::Right, "b".into()).unwrap();
let tab_a = tab_id_for(&t, root, "a");
t.move_tab(tab_a, right);
assert!(matches!(t.nodes[&t.root.unwrap()], DockNode::Leaf(_)));
assert_eq!(t.leaves().count(), 1);
let surviving = t.root.unwrap();
assert_eq!(window_ids(&t, surviving), vec!["b", "a"]);
}
#[test]
fn remove_last_tab_keeps_root_empty_leaf() {
let mut t = DockTree::new();
let root = t.set_root_leaf(leaf("root", &["a"]));
let tab_a = tab_id_for(&t, root, "a");
t.remove_tab(tab_a);
assert_eq!(t.root, Some(root));
assert!(t.nodes[&root].as_leaf().unwrap().windows.is_empty());
}
#[test]
fn set_fraction_clamps() {
let mut t = DockTree::new();
let root = t.set_root_leaf(leaf("root", &["a"]));
t.split(root, Edge::Right, "b".into());
let split_id = t.root.unwrap();
t.set_fraction(split_id, 0.0);
assert!(t.nodes[&split_id].as_split().unwrap().fraction >= 0.05);
t.set_fraction(split_id, 1.5);
assert!(t.nodes[&split_id].as_split().unwrap().fraction <= 0.95);
}
#[test]
fn set_active_requires_tab_in_leaf() {
let mut t = DockTree::new();
let root = t.set_root_leaf(leaf("root", &["a", "b"]));
let tab_b = tab_id_for(&t, root, "b");
t.set_active(root, tab_b);
assert_eq!(active_window_id(&t, root), Some("b"));
t.set_active(root, TabId(9999));
assert_eq!(active_window_id(&t, root), Some("b"));
}
#[test]
fn duplicate_window_kind_supported() {
let mut t = DockTree::new();
let root = t.set_root_leaf(DockLeaf::new("root", DockAreaStyle::TabBar));
let first = t.add_tab(root, "outliner").unwrap();
let second = t.add_tab(root, "outliner").unwrap();
assert_ne!(first, second);
assert_eq!(window_ids(&t, root), vec!["outliner", "outliner"]);
t.remove_tab(second);
let l = t.nodes[&root].as_leaf().unwrap();
assert_eq!(l.windows.len(), 1);
assert_eq!(l.windows[0].id, first);
assert_eq!(l.active, Some(first));
}
#[test]
fn serde_round_trip() {
let mut t = DockTree::new();
let root = t.set_root_leaf(leaf("root", &["a"]));
t.split(root, Edge::Right, "b".into());
let json = serde_json::to_string(&t).unwrap();
let restored: DockTree = serde_json::from_str(&json).unwrap();
assert_eq!(restored.leaves().count(), 2);
assert!(restored.find_leaf_with_window("a").is_some());
assert!(restored.find_leaf_with_window("b").is_some());
}
#[test]
fn persistent_leaf_kept_when_emptied_via_simplify() {
let mut t = DockTree::new();
let original = t.insert(DockNode::Leaf(
DockLeaf::new("right", DockAreaStyle::TabBar)
.with_windows(vec!["a".into()])
.persistent(),
));
t.root = Some(original);
let (other, _) = t.split(original, Edge::Right, "b".into()).unwrap();
let tab_a = tab_id_for(&t, original, "a");
t.move_tab(tab_a, other);
let persistent_leaf = t.nodes[&original].as_leaf().unwrap();
assert!(persistent_leaf.windows.is_empty());
assert!(persistent_leaf.is_persistent());
}
#[test]
fn nested_split_chain_simplifies_when_drained() {
let mut t = DockTree::new();
let root = t.set_root_leaf(leaf("root", &["a"]));
let (right, _) = t.split(root, Edge::Right, "b".into()).unwrap();
let _bottom = t.split(right, Edge::Bottom, "c".into()).unwrap();
let tab_b = tab_id_for(&t, right, "b");
t.move_tab(tab_b, root);
let bottom_leaf = t.find_leaf_with_window("c").unwrap();
let tab_c = tab_id_for(&t, bottom_leaf, "c");
t.move_tab(tab_c, root);
assert!(matches!(t.nodes[&t.root.unwrap()], DockNode::Leaf(_)));
assert_eq!(t.leaves().count(), 1);
}
}