feat(editor): add update method with tab click and resize handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 09:54:57 +09:00
parent a642f8ef7e
commit 36fedb48bf

View File

@@ -1,11 +1,8 @@
const TAB_BAR_HEIGHT: f32 = 20.0;
const MIN_RATIO: f32 = 0.1;
const MAX_RATIO: f32 = 0.9;
#[allow(dead_code)]
const RESIZE_HANDLE_HALF: f32 = 3.0;
#[allow(dead_code)]
const GLYPH_W: f32 = 8.0;
#[allow(dead_code)]
const TAB_PADDING: f32 = 8.0;
#[derive(Clone, Copy, Debug)]
@@ -55,7 +52,6 @@ pub struct LeafLayout {
pub content_rect: Rect,
}
#[allow(dead_code)]
struct SplitLayout {
rect: Rect,
axis: Axis,
@@ -63,7 +59,6 @@ struct SplitLayout {
path: Vec<usize>,
}
#[allow(dead_code)]
struct ResizeState {
path: Vec<usize>,
axis: Axis,
@@ -71,15 +66,18 @@ struct ResizeState {
size: f32,
}
enum UpdateAction {
None,
StartResize(ResizeState),
SetActiveTab { leaf_index: usize, new_active: usize },
}
pub struct DockTree {
root: DockNode,
#[allow(dead_code)]
names: Vec<&'static str>,
cached_leaves: Vec<LeafLayout>,
cached_splits: Vec<SplitLayout>,
#[allow(dead_code)]
resizing: Option<ResizeState>,
#[allow(dead_code)]
prev_mouse_down: bool,
}
@@ -153,6 +151,111 @@ 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; }
let action = self.find_click_action(mouse_x, mouse_y);
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 over tab clicks)
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_len = self.names.get(panel_id as usize).map(|n| n.len()).unwrap_or(1);
let tab_w = name_len 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);
}
}
}
pub fn layout(&mut self, rect: Rect) -> Vec<(u32, Rect)> {
self.cached_leaves.clear();
self.cached_splits.clear();
@@ -268,4 +371,76 @@ mod tests {
);
dock.layout(Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 });
}
#[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);
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 });
dock.update(200.0, 150.0, true); // click on boundary at x=200
dock.update(120.0, 150.0, true); // drag to x=120
dock.update(120.0, 150.0, false); // release
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);
}
#[test]
fn test_resize_priority_over_tab_click() {
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 });
dock.update(200.0, 10.0, true); // click at boundary within tab bar
dock.update(180.0, 10.0, true); // drag
dock.update(180.0, 10.0, false); // release
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);
}
}