use crate::render::{RenderContext, TextAlign, TextBaseline};
use super::super::colors::WidgetState;
use super::super::themes::tabs::TabTheme;
#[derive(Clone, Debug)]
pub struct Tab<'a> {
pub label: &'a str,
pub selected: bool,
pub state: WidgetState,
}
impl<'a> Tab<'a> {
pub fn new(label: &'a str) -> Self {
Self {
label,
selected: false,
state: WidgetState::Normal,
}
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn state(mut self, state: WidgetState) -> Self {
self.state = state;
self
}
}
pub fn render_tabs(
ctx: &mut dyn RenderContext,
x: f64,
y: f64,
tabs: &[Tab],
theme: &TabTheme,
) -> (f64, f64) {
if tabs.is_empty() {
return (0.0, 0.0);
}
let container_height = theme.container_height();
let container_padding = theme.container_padding();
let container_radius = theme.container_border_radius();
let tab_radius = theme.tab_border_radius();
let (_tab_padding_v, tab_padding_h) = theme.tab_padding();
ctx.set_font(theme.tab_font());
let mut total_tab_width = 0.0;
let mut tab_widths = Vec::with_capacity(tabs.len());
for tab in tabs {
let text_width = ctx.measure_text(tab.label);
let tab_width = text_width + (tab_padding_h * 2.0);
tab_widths.push(tab_width);
total_tab_width += tab_width;
}
let gap = 1.0;
let total_gaps = if tabs.len() > 1 {
(tabs.len() - 1) as f64 * gap
} else {
0.0
};
let container_width = total_tab_width + total_gaps + (container_padding * 2.0);
ctx.save();
ctx.set_fill_color(theme.container_bg());
ctx.fill_rounded_rect(x, y, container_width, container_height, container_radius);
ctx.restore();
let mut current_x = x + container_padding;
let tab_y = y + container_padding;
let tab_height = container_height - (container_padding * 2.0);
for (i, tab) in tabs.iter().enumerate() {
let tab_width = tab_widths[i];
let tab_bg = theme.tab_bg(tab.selected, tab.state);
if tab_bg != "transparent" {
ctx.save();
ctx.set_fill_color(tab_bg);
ctx.fill_rounded_rect(current_x, tab_y, tab_width, tab_height, tab_radius);
ctx.restore();
}
let text_color = theme.tab_text_color(tab.selected, tab.state);
ctx.save();
ctx.set_fill_color(text_color);
ctx.set_font(theme.tab_font());
ctx.set_text_align(TextAlign::Center);
ctx.set_text_baseline(TextBaseline::Middle);
let text_x = current_x + (tab_width / 2.0);
let text_y = tab_y + (tab_height / 2.0);
ctx.fill_text(tab.label, text_x, text_y);
ctx.restore();
current_x += tab_width + gap;
}
(container_width, container_height)
}
pub fn hit_test(
x: f64,
y: f64,
tabs: &[Tab],
mouse_x: f64,
mouse_y: f64,
theme: &TabTheme,
) -> Option<usize> {
if tabs.is_empty() {
return None;
}
let container_height = theme.container_height();
let container_padding = theme.container_padding();
let (_, tab_padding_h) = theme.tab_padding();
let tab_y = y + container_padding;
let tab_height = container_height - (container_padding * 2.0);
if mouse_y < tab_y || mouse_y > tab_y + tab_height {
return None;
}
let mut ctx_temp = DummyMeasureContext::new();
ctx_temp.set_font(theme.tab_font());
let mut current_x = x + container_padding;
let gap = 1.0;
for (i, tab) in tabs.iter().enumerate() {
let text_width = ctx_temp.measure_text(tab.label);
let tab_width = text_width + (tab_padding_h * 2.0);
if mouse_x >= current_x && mouse_x < current_x + tab_width {
return Some(i);
}
current_x += tab_width + gap;
}
None
}
struct DummyMeasureContext {
font_size: f64,
}
impl DummyMeasureContext {
fn new() -> Self {
Self { font_size: 13.0 }
}
fn set_font(&mut self, font: &str) {
if let Some(px_pos) = font.find("px") {
let start = font[..px_pos].rfind(|c: char| !c.is_numeric() && c != '.').map(|i| i + 1).unwrap_or(0);
if let Ok(size) = font[start..px_pos].parse::<f64>() {
self.font_size = size;
}
}
}
fn measure_text(&self, text: &str) -> f64 {
text.len() as f64 * self.font_size * 0.6
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::super::colors::AppearanceMode;
#[test]
fn test_tab_creation() {
let tab = Tab::new("Test");
assert_eq!(tab.label, "Test");
assert!(!tab.selected);
assert_eq!(tab.state, WidgetState::Normal);
}
#[test]
fn test_tab_builder() {
let tab = Tab::new("Test")
.selected(true)
.state(WidgetState::Hovered);
assert_eq!(tab.label, "Test");
assert!(tab.selected);
assert_eq!(tab.state, WidgetState::Hovered);
}
#[test]
fn test_empty_tabs() {
let theme = TabTheme::new(AppearanceMode::Dark);
let tabs: Vec<Tab> = vec![];
let result = hit_test(0.0, 0.0, &tabs, 10.0, 10.0, &theme);
assert_eq!(result, None);
}
#[test]
fn test_dummy_measure_context() {
let mut ctx = DummyMeasureContext::new();
assert_eq!(ctx.font_size, 13.0);
ctx.set_font("16px sans-serif");
assert_eq!(ctx.font_size, 16.0);
let width = ctx.measure_text("Hello");
assert!(width > 0.0);
}
}