use std::any::Any;
use astrelis_core::math::Vec2;
use astrelis_render::Color;
use astrelis_text::FontRenderer;
use crate::style::Style;
use crate::tree::{LayoutRect, NodeId};
use crate::widgets::Widget;
pub const DEFAULT_TAB_BAR_HEIGHT: f32 = 28.0;
pub const DEFAULT_TAB_PADDING: f32 = 12.0;
pub const DEFAULT_CLOSE_BUTTON_SIZE: f32 = 16.0;
pub fn default_tab_bar_color() -> Color {
Color::from_rgb_u8(40, 40, 50)
}
pub fn default_active_tab_color() -> Color {
Color::from_rgb_u8(60, 60, 80)
}
pub fn default_inactive_tab_color() -> Color {
Color::from_rgb_u8(50, 50, 60)
}
pub fn default_tab_text_color() -> Color {
Color::WHITE
}
pub fn default_tab_hover_color() -> Color {
Color::from_rgb_u8(70, 70, 90)
}
#[derive(Clone)]
pub struct DockTabs {
pub style: Style,
pub children: Vec<NodeId>,
pub tab_labels: Vec<String>,
pub active_tab: usize,
pub tab_bar_height: f32,
pub tab_bar_color: Color,
pub active_tab_color: Color,
pub inactive_tab_color: Color,
pub tab_text_color: Color,
pub tab_hover_color: Color,
pub hovered_tab: Option<usize>,
pub closable: bool,
pub tab_font_size: f32,
pub(crate) tab_widths: Vec<f32>,
pub dragging_tab_index: Option<usize>,
pub drag_drop_target: Option<usize>,
pub drag_cursor_pos: Option<Vec2>,
}
impl DockTabs {
pub fn new() -> Self {
Self {
style: Style::new().display(taffy::Display::Flex),
children: Vec::new(),
tab_labels: Vec::new(),
active_tab: 0,
tab_bar_height: DEFAULT_TAB_BAR_HEIGHT,
tab_bar_color: default_tab_bar_color(),
active_tab_color: default_active_tab_color(),
inactive_tab_color: default_inactive_tab_color(),
tab_text_color: default_tab_text_color(),
tab_hover_color: default_tab_hover_color(),
hovered_tab: None,
closable: false,
tab_font_size: 13.0,
tab_widths: Vec::new(),
dragging_tab_index: None,
drag_drop_target: None,
drag_cursor_pos: None,
}
}
pub fn add_tab(&mut self, label: impl Into<String>, content: NodeId) {
self.tab_labels.push(label.into());
self.children.push(content);
}
pub fn set_active_tab(&mut self, index: usize) -> Option<usize> {
if index < self.children.len() && index != self.active_tab {
let old_active = self.active_tab;
self.active_tab = index;
Some(old_active)
} else {
None
}
}
pub fn set_hovered_tab(&mut self, index: Option<usize>) {
self.hovered_tab = index;
}
pub fn close_tab(&mut self, index: usize) -> Option<NodeId> {
if index >= self.children.len() {
return None;
}
self.tab_labels.remove(index);
let removed = self.children.remove(index);
if self.active_tab >= self.children.len() && !self.children.is_empty() {
self.active_tab = self.children.len() - 1;
} else if self.active_tab > index {
self.active_tab -= 1;
}
Some(removed)
}
pub fn tab_count(&self) -> usize {
self.children.len()
}
pub fn tab_label(&self, index: usize) -> Option<&str> {
self.tab_labels.get(index).map(|s| s.as_str())
}
pub fn tab_bar_height(mut self, height: f32) -> Self {
self.tab_bar_height = height.max(16.0);
self
}
pub fn tab_colors(mut self, bar: Color, active: Color, inactive: Color) -> Self {
self.tab_bar_color = bar;
self.active_tab_color = active;
self.inactive_tab_color = inactive;
self
}
pub fn text_color(mut self, color: Color) -> Self {
self.tab_text_color = color;
self
}
pub fn hover_color(mut self, color: Color) -> Self {
self.tab_hover_color = color;
self
}
pub fn closable(mut self, closable: bool) -> Self {
self.closable = closable;
self
}
pub fn tab_font_size(mut self, size: f32) -> Self {
self.tab_font_size = size;
self
}
pub fn tab_bar_bounds(&self, layout: &LayoutRect) -> LayoutRect {
LayoutRect {
x: layout.x,
y: layout.y,
width: layout.width,
height: self.tab_bar_height,
}
}
pub fn content_bounds(&self, layout: &LayoutRect) -> LayoutRect {
LayoutRect {
x: layout.x,
y: layout.y + self.tab_bar_height,
width: layout.width,
height: (layout.height - self.tab_bar_height).max(0.0),
}
}
pub fn tab_bounds(&self, index: usize, layout: &LayoutRect) -> Option<LayoutRect> {
if index >= self.tab_labels.len() {
return None;
}
let mut x = layout.x;
for i in 0..index {
x += self.estimate_tab_width(i);
}
Some(LayoutRect {
x,
y: layout.y,
width: self.estimate_tab_width(index),
height: self.tab_bar_height,
})
}
fn estimate_tab_width(&self, index: usize) -> f32 {
let label = self.tab_labels.get(index).map(|s| s.as_str()).unwrap_or("");
let char_width = self.tab_font_size * 0.6;
let text_width = label.len() as f32 * char_width;
let close_width = if self.closable {
DEFAULT_CLOSE_BUTTON_SIZE + 4.0
} else {
0.0
};
text_width + DEFAULT_TAB_PADDING * 2.0 + close_width
}
pub fn close_button_bounds(&self, index: usize, layout: &LayoutRect) -> Option<LayoutRect> {
if !self.closable || index >= self.tab_labels.len() {
return None;
}
let tab_bounds = self.tab_bounds(index, layout)?;
let button_size = DEFAULT_CLOSE_BUTTON_SIZE;
let margin = (self.tab_bar_height - button_size) / 2.0;
Some(LayoutRect {
x: tab_bounds.x + tab_bounds.width - button_size - margin,
y: tab_bounds.y + margin,
width: button_size,
height: button_size,
})
}
pub fn hit_test_tab(&self, pos: Vec2, layout: &LayoutRect) -> Option<usize> {
let bar = self.tab_bar_bounds(layout);
if pos.y < bar.y || pos.y > bar.y + bar.height {
return None;
}
for i in 0..self.tab_labels.len() {
if let Some(tab_rect) = self.tab_bounds(i, layout) {
if pos.x >= tab_rect.x && pos.x <= tab_rect.x + tab_rect.width {
return Some(i);
}
}
}
None
}
pub fn hit_test_close_button(&self, pos: Vec2, layout: &LayoutRect) -> Option<usize> {
if !self.closable {
return None;
}
for i in 0..self.tab_labels.len() {
if let Some(close_rect) = self.close_button_bounds(i, layout) {
if pos.x >= close_rect.x
&& pos.x <= close_rect.x + close_rect.width
&& pos.y >= close_rect.y
&& pos.y <= close_rect.y + close_rect.height
{
return Some(i);
}
}
}
None
}
pub fn tab_background_color(&self, index: usize) -> Color {
if index == self.active_tab {
self.active_tab_color
} else if self.hovered_tab == Some(index) {
self.tab_hover_color
} else {
self.inactive_tab_color
}
}
pub fn active_content(&self) -> Option<NodeId> {
self.children.get(self.active_tab).copied()
}
pub fn start_tab_drag(&mut self, tab_index: usize) {
if tab_index < self.tab_labels.len() {
self.dragging_tab_index = Some(tab_index);
}
}
pub fn update_drop_target(&mut self, cursor_pos: Vec2, layout: &LayoutRect) {
if self.dragging_tab_index.is_none() {
self.drag_drop_target = None;
self.drag_cursor_pos = None;
return;
}
self.drag_cursor_pos = Some(cursor_pos);
let mut closest_index = 0;
let mut closest_dist = f32::MAX;
for i in 0..=self.tab_labels.len() {
let insertion_x = if i == 0 {
layout.x
} else if let Some(prev_bounds) = self.tab_bounds(i - 1, layout) {
prev_bounds.x + prev_bounds.width
} else {
layout.x
};
let dist = (cursor_pos.x - insertion_x).abs();
if dist < closest_dist {
closest_dist = dist;
closest_index = i;
}
}
self.drag_drop_target = Some(closest_index);
}
pub fn finish_tab_drag(&mut self) {
if let (Some(from_index), Some(to_index)) = (self.dragging_tab_index, self.drag_drop_target) {
let is_moving_left = to_index < from_index;
let is_moving_right = to_index > from_index + 1;
if is_moving_left || is_moving_right {
let label = self.tab_labels.remove(from_index);
let insert_index = if to_index > from_index { to_index - 1 } else { to_index };
self.tab_labels.insert(insert_index, label);
let child = self.children.remove(from_index);
self.children.insert(insert_index, child);
if self.active_tab == from_index {
self.active_tab = insert_index;
} else if self.active_tab > from_index && self.active_tab <= insert_index {
self.active_tab -= 1;
} else if self.active_tab < from_index && self.active_tab >= insert_index {
self.active_tab += 1;
}
}
}
self.dragging_tab_index = None;
self.drag_drop_target = None;
self.drag_cursor_pos = None;
}
pub fn cancel_tab_drag(&mut self) {
self.dragging_tab_index = None;
self.drag_drop_target = None;
self.drag_cursor_pos = None;
}
pub fn drop_indicator_bounds(&self, layout: &LayoutRect) -> Option<LayoutRect> {
let drop_index = self.drag_drop_target?;
let x = if drop_index == 0 {
layout.x
} else if let Some(prev_tab) = self.tab_bounds(drop_index - 1, layout) {
prev_tab.x + prev_tab.width
} else {
layout.x
};
Some(LayoutRect {
x,
y: layout.y,
width: 2.0, height: self.tab_bar_height,
})
}
}
impl Default for DockTabs {
fn default() -> Self {
Self::new()
}
}
impl Widget for DockTabs {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn style(&self) -> &Style {
&self.style
}
fn style_mut(&mut self) -> &mut Style {
&mut self.style
}
fn children(&self) -> &[NodeId] {
&self.children
}
fn children_mut(&mut self) -> Option<&mut Vec<NodeId>> {
Some(&mut self.children)
}
fn measure(&self, _available_space: Vec2, _font_renderer: Option<&FontRenderer>) -> Vec2 {
Vec2::ZERO
}
fn clone_box(&self) -> Box<dyn Widget> {
Box::new(self.clone())
}
}