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:
@@ -1,11 +1,8 @@
|
|||||||
const TAB_BAR_HEIGHT: f32 = 20.0;
|
const TAB_BAR_HEIGHT: f32 = 20.0;
|
||||||
const MIN_RATIO: f32 = 0.1;
|
const MIN_RATIO: f32 = 0.1;
|
||||||
const MAX_RATIO: f32 = 0.9;
|
const MAX_RATIO: f32 = 0.9;
|
||||||
#[allow(dead_code)]
|
|
||||||
const RESIZE_HANDLE_HALF: f32 = 3.0;
|
const RESIZE_HANDLE_HALF: f32 = 3.0;
|
||||||
#[allow(dead_code)]
|
|
||||||
const GLYPH_W: f32 = 8.0;
|
const GLYPH_W: f32 = 8.0;
|
||||||
#[allow(dead_code)]
|
|
||||||
const TAB_PADDING: f32 = 8.0;
|
const TAB_PADDING: f32 = 8.0;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
@@ -55,7 +52,6 @@ pub struct LeafLayout {
|
|||||||
pub content_rect: Rect,
|
pub content_rect: Rect,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
struct SplitLayout {
|
struct SplitLayout {
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
@@ -63,7 +59,6 @@ struct SplitLayout {
|
|||||||
path: Vec<usize>,
|
path: Vec<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
struct ResizeState {
|
struct ResizeState {
|
||||||
path: Vec<usize>,
|
path: Vec<usize>,
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
@@ -71,15 +66,18 @@ struct ResizeState {
|
|||||||
size: f32,
|
size: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum UpdateAction {
|
||||||
|
None,
|
||||||
|
StartResize(ResizeState),
|
||||||
|
SetActiveTab { leaf_index: usize, new_active: usize },
|
||||||
|
}
|
||||||
|
|
||||||
pub struct DockTree {
|
pub struct DockTree {
|
||||||
root: DockNode,
|
root: DockNode,
|
||||||
#[allow(dead_code)]
|
|
||||||
names: Vec<&'static str>,
|
names: Vec<&'static str>,
|
||||||
cached_leaves: Vec<LeafLayout>,
|
cached_leaves: Vec<LeafLayout>,
|
||||||
cached_splits: Vec<SplitLayout>,
|
cached_splits: Vec<SplitLayout>,
|
||||||
#[allow(dead_code)]
|
|
||||||
resizing: Option<ResizeState>,
|
resizing: Option<ResizeState>,
|
||||||
#[allow(dead_code)]
|
|
||||||
prev_mouse_down: bool,
|
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)> {
|
pub fn layout(&mut self, rect: Rect) -> Vec<(u32, Rect)> {
|
||||||
self.cached_leaves.clear();
|
self.cached_leaves.clear();
|
||||||
self.cached_splits.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 });
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user