use super::id::LeafId;
use super::rect::PanelRect;
use super::drop_zone::DropZone;
#[derive(Clone, Debug)]
pub struct TabInfo {
pub panel_id: LeafId,
pub title: String,
pub closable: bool,
pub rect: PanelRect,
}
impl TabInfo {
pub fn new(panel_id: LeafId, title: String, closable: bool) -> Self {
Self {
panel_id,
title,
closable,
rect: PanelRect::zero(),
}
}
}
pub struct TabBar {
tabs: Vec<TabInfo>,
active_idx: Option<usize>,
height: f32,
scroll_offset: f32,
}
impl TabBar {
const TAB_PADDING: f32 = 12.0;
const TAB_MIN_WIDTH: f32 = 80.0;
const TAB_MAX_WIDTH: f32 = 200.0;
const TAB_SPACING: f32 = 2.0;
pub fn new(height: f32) -> Self {
Self {
tabs: Vec::new(),
active_idx: None,
height,
scroll_offset: 0.0,
}
}
pub fn set_tabs(&mut self, tabs: Vec<(LeafId, String, bool)>) {
self.tabs = tabs
.into_iter()
.map(|(panel_id, title, closable)| TabInfo::new(panel_id, title, closable))
.collect();
}
pub fn set_active(&mut self, panel_id: Option<LeafId>) {
self.active_idx = panel_id.and_then(|id| {
self.tabs.iter().position(|tab| tab.panel_id == id)
});
}
pub fn layout(&mut self, area: PanelRect) {
let mut x = area.x - self.scroll_offset;
let y = area.y;
for tab in &mut self.tabs {
let text_width = tab.title.len() as f32 * 8.0;
let width = (text_width + Self::TAB_PADDING * 2.0)
.clamp(Self::TAB_MIN_WIDTH, Self::TAB_MAX_WIDTH);
tab.rect = PanelRect::new(x, y, width, self.height);
x += width + Self::TAB_SPACING;
}
}
pub fn hit_test(&self, x: f32, y: f32) -> Option<TabHit> {
for (idx, tab) in self.tabs.iter().enumerate() {
if tab.rect.contains(x, y) {
if tab.closable {
let close_size = 16.0;
let close_x = tab.rect.x + tab.rect.width - close_size - 8.0;
let close_y = tab.rect.y + (tab.rect.height - close_size) / 2.0;
let close_rect = PanelRect::new(close_x, close_y, close_size, close_size);
if close_rect.contains(x, y) {
return Some(TabHit::Close(idx));
}
}
return Some(TabHit::Tab(idx));
}
}
None
}
pub fn get_tab(&self, idx: usize) -> Option<&TabInfo> {
self.tabs.get(idx)
}
pub fn active_idx(&self) -> Option<usize> {
self.active_idx
}
pub fn tabs(&self) -> &[TabInfo] {
&self.tabs
}
pub fn height(&self) -> f32 {
self.height
}
pub fn scroll_offset(&self) -> f32 {
self.scroll_offset
}
pub fn set_scroll_offset(&mut self, offset: f32) {
self.scroll_offset = offset.max(0.0);
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TabHit {
Tab(usize),
Close(usize),
}
#[derive(Clone, Debug)]
pub struct TabDragState {
pub tab_idx: usize,
pub source_container: LeafId,
pub offset: (f32, f32),
pub preview_rect: PanelRect,
}
pub struct TabDragController {
dragging: Option<TabDragState>,
}
impl TabDragController {
pub fn new() -> Self {
Self { dragging: None }
}
pub fn start_drag(
&mut self,
tab_idx: usize,
container_id: LeafId,
mouse_pos: (f32, f32),
tab_rect: PanelRect,
) {
self.dragging = Some(TabDragState {
tab_idx,
source_container: container_id,
offset: (mouse_pos.0 - tab_rect.x, mouse_pos.1 - tab_rect.y),
preview_rect: tab_rect,
});
}
pub fn update_drag(&mut self, mouse_pos: (f32, f32)) {
if let Some(ref mut state) = self.dragging {
state.preview_rect.x = mouse_pos.0 - state.offset.0;
state.preview_rect.y = mouse_pos.1 - state.offset.1;
}
}
pub fn complete_drag(
&mut self,
target_container: LeafId,
zone: DropZone,
) -> Option<(LeafId, usize, LeafId, DropZone)> {
let state = self.dragging.take()?;
Some((
state.source_container,
state.tab_idx,
target_container,
zone,
))
}
pub fn cancel(&mut self) {
self.dragging = None;
}
pub fn is_dragging(&self) -> bool {
self.dragging.is_some()
}
pub fn drag_state(&self) -> Option<&TabDragState> {
self.dragging.as_ref()
}
}
impl Default for TabDragController {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct TabBarInfo {
pub container_id: LeafId,
pub rect: PanelRect,
pub tabs: Vec<TabItem>,
}
#[derive(Clone, Debug)]
pub struct TabItem {
pub panel_id: LeafId,
pub title: String,
pub rect: PanelRect,
pub is_active: bool,
pub close_rect: PanelRect,
}
#[derive(Clone, Debug)]
pub struct TabReorderState {
pub container_id: LeafId,
pub dragged_tab_id: LeafId,
pub original_index: usize,
pub current_x: f32,
pub insert_index: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tab_bar_layout() {
let mut tab_bar = TabBar::new(32.0);
tab_bar.set_tabs(vec![
(LeafId(1), "Chart".to_string(), true),
(LeafId(2), "Table".to_string(), true),
(LeafId(3), "Long Title Here".to_string(), false),
]);
let area = PanelRect::new(0.0, 0.0, 800.0, 32.0);
tab_bar.layout(area);
let tabs = tab_bar.tabs();
assert_eq!(tabs.len(), 3);
assert_eq!(tabs[0].rect.x, 0.0);
assert_eq!(tabs[0].rect.y, 0.0);
assert_eq!(tabs[0].rect.height, 32.0);
assert!(tabs[0].rect.width >= TabBar::TAB_MIN_WIDTH);
assert!(tabs[0].rect.width <= TabBar::TAB_MAX_WIDTH);
assert!(tabs[1].rect.x > tabs[0].rect.x);
}
#[test]
fn test_tab_bar_hit_test_tab() {
let mut tab_bar = TabBar::new(32.0);
tab_bar.set_tabs(vec![
(LeafId(1), "Chart".to_string(), true),
(LeafId(2), "Table".to_string(), true),
]);
let area = PanelRect::new(0.0, 0.0, 800.0, 32.0);
tab_bar.layout(area);
let first_tab_rect = tab_bar.tabs()[0].rect;
let hit = tab_bar.hit_test(first_tab_rect.x + 20.0, first_tab_rect.y + 16.0);
assert_eq!(hit, Some(TabHit::Tab(0)));
let hit = tab_bar.hit_test(1000.0, 16.0);
assert_eq!(hit, None);
}
#[test]
fn test_tab_bar_hit_test_close() {
let mut tab_bar = TabBar::new(32.0);
tab_bar.set_tabs(vec![
(LeafId(1), "Chart".to_string(), true),
]);
let area = PanelRect::new(0.0, 0.0, 800.0, 32.0);
tab_bar.layout(area);
let tab_rect = tab_bar.tabs()[0].rect;
let close_x = tab_rect.x + tab_rect.width - 16.0;
let close_y = tab_rect.y + tab_rect.height / 2.0;
let hit = tab_bar.hit_test(close_x, close_y);
assert_eq!(hit, Some(TabHit::Close(0)));
}
#[test]
fn test_tab_drag_lifecycle() {
let mut controller = TabDragController::new();
assert!(!controller.is_dragging());
let tab_rect = PanelRect::new(10.0, 0.0, 100.0, 32.0);
controller.start_drag(0, LeafId(1), (50.0, 16.0), tab_rect);
assert!(controller.is_dragging());
let state = controller.drag_state().unwrap();
assert_eq!(state.tab_idx, 0);
assert_eq!(state.source_container, LeafId(1));
controller.update_drag((100.0, 50.0));
let state = controller.drag_state().unwrap();
assert_eq!(state.preview_rect.x, 100.0 - state.offset.0);
assert_eq!(state.preview_rect.y, 50.0 - state.offset.1);
let result = controller.complete_drag(LeafId(2), DropZone::Center);
assert!(result.is_some());
let (source, idx, target, zone) = result.unwrap();
assert_eq!(source, LeafId(1));
assert_eq!(idx, 0);
assert_eq!(target, LeafId(2));
assert_eq!(zone, DropZone::Center);
assert!(!controller.is_dragging());
}
#[test]
fn test_tab_drag_cancel() {
let mut controller = TabDragController::new();
let tab_rect = PanelRect::new(10.0, 0.0, 100.0, 32.0);
controller.start_drag(0, LeafId(1), (50.0, 16.0), tab_rect);
assert!(controller.is_dragging());
controller.cancel();
assert!(!controller.is_dragging());
}
#[test]
fn test_tab_bar_set_active() {
let mut tab_bar = TabBar::new(32.0);
tab_bar.set_tabs(vec![
(LeafId(1), "Chart".to_string(), true),
(LeafId(2), "Table".to_string(), true),
(LeafId(3), "Timeline".to_string(), true),
]);
tab_bar.set_active(Some(LeafId(2)));
assert_eq!(tab_bar.active_idx(), Some(1));
tab_bar.set_active(Some(LeafId(99)));
assert_eq!(tab_bar.active_idx(), None);
tab_bar.set_active(None);
assert_eq!(tab_bar.active_idx(), None);
}
}