From 1571aa5f970bf361de6b827a07b1249bffb3eed1 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Thu, 26 Mar 2026 09:57:18 +0900 Subject: [PATCH] feat(editor): add draw_chrome for tab bars and split lines Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/voltex_editor/src/dock.rs | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/crates/voltex_editor/src/dock.rs b/crates/voltex_editor/src/dock.rs index 74b9829..9bcfd3e 100644 --- a/crates/voltex_editor/src/dock.rs +++ b/crates/voltex_editor/src/dock.rs @@ -1,3 +1,5 @@ +use crate::ui_context::UiContext; + const TAB_BAR_HEIGHT: f32 = 20.0; const MIN_RATIO: f32 = 0.1; const MAX_RATIO: f32 = 0.9; @@ -277,11 +279,66 @@ impl DockTree { }) .collect() } + + 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 each leaf + 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 (avoids borrow conflict with ui.font + ui.draw_list) + 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 bottom 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); + } + } + } + } } #[cfg(test)] mod tests { use super::*; + use crate::ui_context::UiContext; #[test] fn test_rect_contains() { @@ -443,4 +500,35 @@ mod tests { 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); } + + #[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 "AA"): color [40, 40, 40, 255] + let first_bg = &ui.draw_list.vertices[0]; + assert_eq!(first_bg.color, [40, 40, 40, 255]); + } }