24 KiB
Editor Docking System Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a binary split tree docking system to voltex_editor with resizable panels and tab switching.
Architecture: Binary tree of Split/Leaf nodes. Split nodes divide space by ratio, Leaf nodes hold tabs. DockTree owns the tree, caches layout results, handles resize drag and tab clicks internally. No unsafe code — borrow issues resolved by collecting mutation targets before applying.
Tech Stack: Rust, existing voltex_editor IMGUI (UiContext, DrawList, FontAtlas, LayoutState)
Spec: docs/superpowers/specs/2026-03-26-editor-docking-design.md
Task 1: Data model + layout algorithm
Files:
-
Create:
crates/voltex_editor/src/dock.rs -
Modify:
crates/voltex_editor/src/lib.rs -
Step 1: Write failing tests for Rect and layout
Add to crates/voltex_editor/src/dock.rs:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rect_contains() {
let r = Rect { x: 10.0, y: 20.0, w: 100.0, h: 50.0 };
assert!(r.contains(50.0, 40.0));
assert!(!r.contains(5.0, 40.0));
assert!(!r.contains(50.0, 80.0));
}
#[test]
fn test_layout_single_leaf() {
let mut dock = DockTree::new(
DockNode::leaf(vec![0]),
vec!["Panel0"],
);
let areas = dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
assert_eq!(areas.len(), 1);
assert_eq!(areas[0].0, 0);
let r = &areas[0].1;
assert!((r.y - 20.0).abs() < 1e-3);
assert!((r.h - 280.0).abs() < 1e-3);
}
#[test]
fn test_layout_horizontal_split() {
let mut dock = DockTree::new(
DockNode::split(Axis::Horizontal, 0.25, DockNode::leaf(vec![0]), DockNode::leaf(vec![1])),
vec!["Left", "Right"],
);
let areas = dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
assert_eq!(areas.len(), 2);
let left = areas.iter().find(|(id, _)| *id == 0).unwrap();
assert!((left.1.w - 100.0).abs() < 1e-3);
let right = areas.iter().find(|(id, _)| *id == 1).unwrap();
assert!((right.1.w - 300.0).abs() < 1e-3);
}
#[test]
fn test_layout_vertical_split() {
let mut dock = DockTree::new(
DockNode::split(Axis::Vertical, 0.5, DockNode::leaf(vec![0]), DockNode::leaf(vec![1])),
vec!["Top", "Bottom"],
);
let areas = dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
let top = areas.iter().find(|(id, _)| *id == 0).unwrap();
assert!((top.1.h - 130.0).abs() < 1e-3); // 150 - 20 tab bar
}
#[test]
fn test_layout_nested_split() {
let mut dock = DockTree::new(
DockNode::split(
Axis::Horizontal, 0.25,
DockNode::leaf(vec![0]),
DockNode::split(Axis::Vertical, 0.5, DockNode::leaf(vec![1]), DockNode::leaf(vec![2])),
),
vec!["A", "B", "C"],
);
let areas = dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
assert_eq!(areas.len(), 3);
}
#[test]
fn test_layout_active_tab_only() {
let mut dock = DockTree::new(
DockNode::Leaf { tabs: vec![0, 1, 2], active: 1 },
vec!["A", "B", "C"],
);
let areas = dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
assert_eq!(areas[0].0, 1);
}
#[test]
fn test_active_clamped_if_out_of_bounds() {
let mut dock = DockTree::new(
DockNode::Leaf { tabs: vec![0], active: 5 },
vec!["A"],
);
let areas = dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
assert_eq!(areas[0].0, 0);
}
#[test]
#[should_panic]
fn test_empty_tabs_panics() {
let mut dock = DockTree::new(
DockNode::Leaf { tabs: vec![], active: 0 },
vec![],
);
dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
}
}
- Step 2: Run tests to verify they fail
Run: cargo test -p voltex_editor --lib dock -- --nocapture
Expected: compilation errors (types don't exist)
- Step 3: Implement data model and layout
Write the implementation in crates/voltex_editor/src/dock.rs. Key design decisions:
layout_recursiveis a free function taking&DockNode,Rect, and&mut Vec<LeafLayout>/&mut Vec<SplitLayout>— avoids borrow issues with&mut selfvs&self.root.- No
unsafecode. No raw pointers. debug_assert!(!tabs.is_empty())inlayout_recursivefor the empty tabs invariant.LeafLayoutincludesleaf_index: usizefor mutation targeting.
const TAB_BAR_HEIGHT: f32 = 20.0;
const MIN_RATIO: f32 = 0.1;
const MAX_RATIO: f32 = 0.9;
const RESIZE_HANDLE_HALF: f32 = 3.0;
const GLYPH_W: f32 = 8.0;
const TAB_PADDING: f32 = 8.0;
#[derive(Clone, Copy, Debug)]
pub struct Rect {
pub x: f32, pub y: f32, pub w: f32, pub h: f32,
}
impl Rect {
pub fn contains(&self, px: f32, py: f32) -> bool {
px >= self.x && px < self.x + self.w && py >= self.y && py < self.y + self.h
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Axis { Horizontal, Vertical }
pub enum DockNode {
Leaf { tabs: Vec<u32>, active: usize },
Split { axis: Axis, ratio: f32, children: [Box<DockNode>; 2] },
}
impl DockNode {
pub fn leaf(tabs: Vec<u32>) -> Self {
DockNode::Leaf { tabs, active: 0 }
}
pub fn split(axis: Axis, ratio: f32, a: DockNode, b: DockNode) -> Self {
DockNode::Split { axis, ratio: ratio.clamp(MIN_RATIO, MAX_RATIO), children: [Box::new(a), Box::new(b)] }
}
}
pub struct LeafLayout {
pub leaf_index: usize,
pub tabs: Vec<u32>,
pub active: usize,
pub tab_bar_rect: Rect,
pub content_rect: Rect,
}
struct SplitLayout {
rect: Rect,
axis: Axis,
boundary: f32,
path: Vec<usize>,
}
struct ResizeState {
path: Vec<usize>,
axis: Axis,
origin: f32,
size: f32,
}
pub struct DockTree {
root: DockNode,
names: Vec<&'static str>,
cached_leaves: Vec<LeafLayout>,
cached_splits: Vec<SplitLayout>,
resizing: Option<ResizeState>,
prev_mouse_down: bool,
}
fn layout_recursive(
node: &DockNode,
rect: Rect,
path: &mut Vec<usize>,
leaf_counter: &mut usize,
leaves: &mut Vec<LeafLayout>,
splits: &mut Vec<SplitLayout>,
) {
match node {
DockNode::Leaf { tabs, active } => {
debug_assert!(!tabs.is_empty(), "DockNode::Leaf must have at least one tab");
let idx = *leaf_counter;
*leaf_counter += 1;
leaves.push(LeafLayout {
leaf_index: idx,
tabs: tabs.clone(),
active: (*active).min(tabs.len() - 1),
tab_bar_rect: Rect { x: rect.x, y: rect.y, w: rect.w, h: TAB_BAR_HEIGHT },
content_rect: Rect { x: rect.x, y: rect.y + TAB_BAR_HEIGHT, w: rect.w, h: (rect.h - TAB_BAR_HEIGHT).max(0.0) },
});
}
DockNode::Split { axis, ratio, children } => {
let (r1, r2, boundary) = match axis {
Axis::Horizontal => {
let w1 = rect.w * ratio;
let b = rect.x + w1;
(Rect { x: rect.x, y: rect.y, w: w1, h: rect.h },
Rect { x: b, y: rect.y, w: rect.w - w1, h: rect.h }, b)
}
Axis::Vertical => {
let h1 = rect.h * ratio;
let b = rect.y + h1;
(Rect { x: rect.x, y: rect.y, w: rect.w, h: h1 },
Rect { x: rect.x, y: b, w: rect.w, h: rect.h - h1 }, b)
}
};
splits.push(SplitLayout { rect, axis: *axis, boundary, path: path.clone() });
path.push(0);
layout_recursive(&children[0], r1, path, leaf_counter, leaves, splits);
path.pop();
path.push(1);
layout_recursive(&children[1], r2, path, leaf_counter, leaves, splits);
path.pop();
}
}
}
impl DockTree {
pub fn new(root: DockNode, names: Vec<&'static str>) -> Self {
DockTree { root, names, cached_leaves: Vec::new(), cached_splits: Vec::new(), resizing: None, prev_mouse_down: false }
}
pub fn layout(&mut self, rect: Rect) -> Vec<(u32, Rect)> {
self.cached_leaves.clear();
self.cached_splits.clear();
let mut path = Vec::new();
let mut counter = 0;
layout_recursive(&self.root, rect, &mut path, &mut counter, &mut self.cached_leaves, &mut self.cached_splits);
self.cached_leaves.iter().map(|l| {
let active = l.active.min(l.tabs.len().saturating_sub(1));
(l.tabs[active], l.content_rect)
}).collect()
}
}
- Step 4: Add module to lib.rs
pub mod dock;
pub use dock::{DockTree, DockNode, Axis, Rect, LeafLayout};
- Step 5: Run tests to verify they pass
Run: cargo test -p voltex_editor --lib dock -- --nocapture
Expected: all 8 tests PASS
- Step 6: Commit
git add crates/voltex_editor/src/dock.rs crates/voltex_editor/src/lib.rs
git commit -m "feat(editor): add dock tree data model and layout algorithm"
Task 2: Tab click + resize drag (update method)
Files:
-
Modify:
crates/voltex_editor/src/dock.rs -
Step 1: Write failing tests
#[test]
fn test_tab_click_switches_active() {
let mut dock = DockTree::new(
DockNode::Leaf { tabs: vec![0, 1, 2], active: 0 },
vec!["A", "B", "C"],
);
dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
// Tab "A": w = 1*8+8 = 16, tab "B" starts at x=16. Click at x=20 (inside "B").
dock.update(20.0, 10.0, true); // just_clicked detected here
dock.update(20.0, 10.0, false);
let areas = dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
assert_eq!(areas[0].0, 1);
}
#[test]
fn test_tab_click_no_change_on_single_tab() {
let mut dock = DockTree::new(DockNode::leaf(vec![0]), vec!["Only"]);
dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
dock.update(10.0, 10.0, true);
dock.update(10.0, 10.0, false);
let areas = dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
assert_eq!(areas[0].0, 0);
}
#[test]
fn test_resize_horizontal_drag() {
let mut dock = DockTree::new(
DockNode::split(Axis::Horizontal, 0.5, DockNode::leaf(vec![0]), DockNode::leaf(vec![1])),
vec!["L", "R"],
);
dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
// Boundary at x=200. Click on it, drag to x=120.
dock.update(200.0, 150.0, true);
dock.update(120.0, 150.0, true);
dock.update(120.0, 150.0, false);
let areas = dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
let left = areas.iter().find(|(id, _)| *id == 0).unwrap();
assert!((left.1.w - 120.0).abs() < 5.0, "left w={}", left.1.w);
}
#[test]
fn test_resize_clamps_ratio() {
let mut dock = DockTree::new(
DockNode::split(Axis::Horizontal, 0.5, DockNode::leaf(vec![0]), DockNode::leaf(vec![1])),
vec!["L", "R"],
);
dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
dock.update(200.0, 150.0, true);
dock.update(5.0, 150.0, true);
dock.update(5.0, 150.0, false);
let areas = dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
let left = areas.iter().find(|(id, _)| *id == 0).unwrap();
assert!((left.1.w - 40.0).abs() < 1e-3, "left w={}", left.1.w); // 400*0.1=40
}
#[test]
fn test_resize_priority_over_tab_click() {
// Resize handle should take priority when overlapping with tab bar
let mut dock = DockTree::new(
DockNode::split(Axis::Horizontal, 0.5,
DockNode::Leaf { tabs: vec![0, 1], active: 0 },
DockNode::leaf(vec![2]),
),
vec!["A", "B", "C"],
);
dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
// Click right at the boundary (x=200) within tab bar height (y=10)
dock.update(200.0, 10.0, true);
// If resize took priority, resizing should be Some
// Drag to verify ratio changes
dock.update(180.0, 10.0, true);
dock.update(180.0, 10.0, false);
let areas = dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
let left = areas.iter().find(|(id, _)| *id == 0 || *id == 1).unwrap();
assert!((left.1.w - 180.0).abs() < 5.0, "resize should have priority, w={}", left.1.w);
}
- Step 2: Run tests to verify they fail
Run: cargo test -p voltex_editor --lib dock::tests::test_tab_click -- --nocapture
Expected: FAIL (update doesn't exist)
- Step 3: Implement update method
Key design: collect mutation target into a local variable, then apply outside the loop to avoid borrow conflicts.
/// Possible mutation from update
enum UpdateAction {
None,
StartResize(ResizeState),
SetActiveTab { leaf_index: usize, new_active: usize },
}
impl DockTree {
pub fn update(&mut self, mouse_x: f32, mouse_y: f32, mouse_down: bool) {
let just_clicked = mouse_down && !self.prev_mouse_down;
self.prev_mouse_down = mouse_down;
// Active resize in progress
if let Some(ref state) = self.resizing {
if mouse_down {
let new_ratio = match state.axis {
Axis::Horizontal => (mouse_x - state.origin) / state.size,
Axis::Vertical => (mouse_y - state.origin) / state.size,
};
let clamped = new_ratio.clamp(MIN_RATIO, MAX_RATIO);
let path = state.path.clone();
Self::set_ratio_at_path(&mut self.root, &path, clamped);
} else {
self.resizing = None;
}
return;
}
if !just_clicked { return; }
// Determine action by reading cached layouts (immutable)
let action = self.find_click_action(mouse_x, mouse_y);
// Apply action (mutable)
match action {
UpdateAction::StartResize(state) => { self.resizing = Some(state); }
UpdateAction::SetActiveTab { leaf_index, new_active } => {
Self::set_active_nth(&mut self.root, leaf_index, new_active, &mut 0);
}
UpdateAction::None => {}
}
}
fn find_click_action(&self, mx: f32, my: f32) -> UpdateAction {
// Check resize handles first (priority)
for split in &self.cached_splits {
let hit = match split.axis {
Axis::Horizontal => mx >= split.boundary - RESIZE_HANDLE_HALF && mx <= split.boundary + RESIZE_HANDLE_HALF
&& my >= split.rect.y && my < split.rect.y + split.rect.h,
Axis::Vertical => my >= split.boundary - RESIZE_HANDLE_HALF && my <= split.boundary + RESIZE_HANDLE_HALF
&& mx >= split.rect.x && mx < split.rect.x + split.rect.w,
};
if hit {
let (origin, size) = match split.axis {
Axis::Horizontal => (split.rect.x, split.rect.w),
Axis::Vertical => (split.rect.y, split.rect.h),
};
return UpdateAction::StartResize(ResizeState { path: split.path.clone(), axis: split.axis, origin, size });
}
}
// Check tab bar clicks
for leaf in &self.cached_leaves {
if leaf.tabs.len() <= 1 { continue; }
if !leaf.tab_bar_rect.contains(mx, my) { continue; }
let mut tx = leaf.tab_bar_rect.x;
for (i, &panel_id) in leaf.tabs.iter().enumerate() {
let name = self.names.get(panel_id as usize).map(|n| n.len()).unwrap_or(1);
let tab_w = name as f32 * GLYPH_W + TAB_PADDING;
if mx >= tx && mx < tx + tab_w {
return UpdateAction::SetActiveTab { leaf_index: leaf.leaf_index, new_active: i };
}
tx += tab_w;
}
}
UpdateAction::None
}
fn set_ratio_at_path(node: &mut DockNode, path: &[usize], ratio: f32) {
if path.is_empty() {
if let DockNode::Split { ratio: r, .. } = node { *r = ratio; }
return;
}
if let DockNode::Split { children, .. } = node {
Self::set_ratio_at_path(&mut children[path[0]], &path[1..], ratio);
}
}
fn set_active_nth(node: &mut DockNode, target: usize, new_active: usize, count: &mut usize) {
match node {
DockNode::Leaf { active, .. } => {
if *count == target { *active = new_active; }
*count += 1;
}
DockNode::Split { children, .. } => {
Self::set_active_nth(&mut children[0], target, new_active, count);
Self::set_active_nth(&mut children[1], target, new_active, count);
}
}
}
}
- Step 4: Run tests to verify they pass
Run: cargo test -p voltex_editor --lib dock -- --nocapture
Expected: all tests PASS
- Step 5: Commit
git add crates/voltex_editor/src/dock.rs
git commit -m "feat(editor): add update method with tab click and resize handling"
Task 3: draw_chrome — tab bars and split lines
Files:
-
Modify:
crates/voltex_editor/src/dock.rs -
Step 1: Write failing tests
#[test]
fn test_draw_chrome_produces_draw_commands() {
let mut dock = DockTree::new(
DockNode::split(Axis::Horizontal, 0.5,
DockNode::Leaf { tabs: vec![0, 1], active: 0 },
DockNode::leaf(vec![2]),
),
vec!["A", "B", "C"],
);
let mut ui = UiContext::new(800.0, 600.0);
ui.begin_frame(0.0, 0.0, false);
dock.layout(Rect { x: 0.0, y: 0.0, w: 800.0, h: 600.0 });
dock.draw_chrome(&mut ui);
assert!(ui.draw_list.commands.len() >= 5);
}
#[test]
fn test_draw_chrome_active_tab_color() {
let mut dock = DockTree::new(
DockNode::Leaf { tabs: vec![0, 1], active: 1 },
vec!["AA", "BB"],
);
let mut ui = UiContext::new(800.0, 600.0);
ui.begin_frame(0.0, 0.0, false);
dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
dock.draw_chrome(&mut ui);
// First tab bg (inactive): color [40, 40, 40, 255]
let first_bg = &ui.draw_list.vertices[0];
assert_eq!(first_bg.color, [40, 40, 40, 255]);
}
- Step 2: Run tests to verify they fail
Run: cargo test -p voltex_editor --lib dock::tests::test_draw_chrome -- --nocapture
Expected: FAIL (draw_chrome doesn't exist)
- Step 3: Implement draw_chrome
Inline glyph rendering to avoid &mut ui.draw_list + &ui.font borrow conflict (same pattern as existing widgets.rs):
impl DockTree {
pub fn draw_chrome(&self, ui: &mut UiContext) {
let glyph_w = ui.font.glyph_width as f32;
let glyph_h = ui.font.glyph_height as f32;
const COLOR_TAB_ACTIVE: [u8; 4] = [60, 60, 60, 255];
const COLOR_TAB_INACTIVE: [u8; 4] = [40, 40, 40, 255];
const COLOR_TEXT: [u8; 4] = [0xEE, 0xEE, 0xEE, 0xFF];
const COLOR_SPLIT: [u8; 4] = [30, 30, 30, 255];
const COLOR_SPLIT_ACTIVE: [u8; 4] = [100, 100, 200, 255];
const COLOR_SEPARATOR: [u8; 4] = [50, 50, 50, 255];
// Draw tab bars
for leaf in &self.cached_leaves {
let bar = &leaf.tab_bar_rect;
let active = leaf.active.min(leaf.tabs.len().saturating_sub(1));
let mut tx = bar.x;
for (i, &panel_id) in leaf.tabs.iter().enumerate() {
let name = self.names.get(panel_id as usize).copied().unwrap_or("?");
let tab_w = name.len() as f32 * glyph_w + TAB_PADDING;
let bg = if i == active { COLOR_TAB_ACTIVE } else { COLOR_TAB_INACTIVE };
ui.draw_list.add_rect(tx, bar.y, tab_w, bar.h, bg);
// Inline text rendering
let text_x = tx + TAB_PADDING * 0.5;
let text_y = bar.y + (bar.h - glyph_h) * 0.5;
let mut cx = text_x;
for ch in name.chars() {
let (u0, v0, u1, v1) = ui.font.glyph_uv(ch);
ui.draw_list.add_rect_uv(cx, text_y, glyph_w, glyph_h, u0, v0, u1, v1, COLOR_TEXT);
cx += glyph_w;
}
tx += tab_w;
}
// Tab bar separator
ui.draw_list.add_rect(bar.x, bar.y + bar.h - 1.0, bar.w, 1.0, COLOR_SEPARATOR);
}
// Draw split lines
for split in &self.cached_splits {
let color = if self.resizing.as_ref().map(|r| &r.path) == Some(&split.path) {
COLOR_SPLIT_ACTIVE
} else {
COLOR_SPLIT
};
match split.axis {
Axis::Horizontal => ui.draw_list.add_rect(split.boundary - 0.5, split.rect.y, 1.0, split.rect.h, color),
Axis::Vertical => ui.draw_list.add_rect(split.rect.x, split.boundary - 0.5, split.rect.w, 1.0, color),
}
}
}
}
- Step 4: Run all tests
Run: cargo test -p voltex_editor --lib dock -- --nocapture
Expected: all tests PASS
- Step 5: Commit
git add crates/voltex_editor/src/dock.rs
git commit -m "feat(editor): add draw_chrome for tab bars and split lines"
Task 4: Integration test + update editor_demo
Files:
-
Modify:
crates/voltex_editor/src/dock.rs -
Modify:
examples/editor_demo/src/main.rs -
Step 1: Write integration test
#[test]
fn test_full_frame_cycle() {
let mut dock = DockTree::new(
DockNode::split(Axis::Horizontal, 0.3,
DockNode::Leaf { tabs: vec![0, 1], active: 0 },
DockNode::split(Axis::Vertical, 0.6, DockNode::leaf(vec![2]), DockNode::leaf(vec![3])),
),
vec!["Hierarchy", "Inspector", "Viewport", "Console"],
);
let mut ui = UiContext::new(1280.0, 720.0);
for _ in 0..3 {
ui.begin_frame(100.0, 100.0, false);
let areas = dock.layout(Rect { x: 0.0, y: 0.0, w: 1280.0, h: 720.0 });
dock.update(100.0, 100.0, false);
dock.draw_chrome(&mut ui);
assert_eq!(areas.len(), 4);
for (_, r) in &areas {
assert!(r.w > 0.0);
assert!(r.h > 0.0);
}
ui.end_frame();
}
}
- Step 2: Run test
Run: cargo test -p voltex_editor --lib dock::tests::test_full_frame -- --nocapture
Expected: PASS
- Step 3: Update editor_demo to use docking
Read examples/editor_demo/src/main.rs. Replace the single begin_panel/end_panel block with a docked layout:
-
Add
dock: DockTreeto the app state struct -
Initialize with 4-panel layout (Debug, Viewport, Properties, Console)
-
In render loop: call
dock.layout(),dock.update(),dock.draw_chrome() -
Draw panel contents by matching
panel_idand settingui.layout = LayoutState::new(rect.x + 4.0, rect.y + 4.0) -
Step 4: Build
Run: cargo build --example editor_demo
Expected: compiles
- Step 5: Run all tests
Run: cargo test -p voltex_editor -- --nocapture
Expected: all tests PASS
- Step 6: Commit
git add crates/voltex_editor/src/dock.rs examples/editor_demo/src/main.rs
git commit -m "feat(editor): integrate docking into editor_demo"
Task 5: Update docs
Files:
-
Modify:
docs/STATUS.md -
Modify:
docs/DEFERRED.md -
Step 1: Update STATUS.md
Add to Phase 8-4 section:
- voltex_editor: DockTree (binary split layout, resize, tabs)
Update test count.
- Step 2: Update DEFERRED.md
- ~~**도킹, 탭, 윈도우 드래그**~~ ✅ DockTree (이진 분할, 리사이즈, 탭 전환) 완료. 플로팅 윈도우/탭 드래그 이동 미구현.
- Step 3: Commit
git add docs/STATUS.md docs/DEFERRED.md
git commit -m "docs: update STATUS.md and DEFERRED.md with docking system"