use alloc::boxed::Box;
use rlvgl_core::{
event::Event,
font::{FontMetrics, WidgetFont},
renderer::Renderer,
widget::{Color, Rect, Widget},
};
use rlvgl_widgets::tabview::Tabview;
use crate::theme::{ColorScheme, ComponentSize, Theme, Variant};
pub use rlvgl_widgets::tabview::{TabBarPos, TabId};
pub struct Tabs {
inner: Tabview,
on_change: Option<Box<dyn FnMut(TabId)>>,
}
impl Tabs {
pub const TAB_NONE: TabId = Tabview::TAB_NONE;
pub fn new(bounds: Rect, bar_pos: TabBarPos) -> Self {
Self {
inner: Tabview::new(bounds, bar_pos),
on_change: None,
}
}
pub fn tab(mut self, name: &str) -> Self {
self.inner.add_tab(name);
self
}
pub fn add_tab(&mut self, name: &str) -> TabId {
self.inner.add_tab(name)
}
pub fn rename_tab(&mut self, id: TabId, name: &str) {
self.inner.rename_tab(id, name);
}
pub fn on_change<F: FnMut(TabId) + 'static>(mut self, handler: F) -> Self {
self.on_change = Some(Box::new(handler));
self
}
pub fn with_active_tab(mut self, id: TabId) -> Self {
self.set_active(id);
self
}
pub fn set_active(&mut self, id: TabId) {
let before = self.inner.active_tab();
self.inner.set_active(id);
self.emit_if_changed(before);
}
pub fn active_tab(&self) -> TabId {
self.inner.active_tab()
}
pub fn tab_count(&self) -> usize {
self.inner.tab_count()
}
pub fn tab_content_bounds(&self, id: TabId) -> Rect {
self.inner.tab_content_bounds(id)
}
pub fn bar_pos(&self) -> TabBarPos {
self.inner.bar_pos()
}
pub fn set_bar_pos(&mut self, pos: TabBarPos) {
self.inner.set_bar_pos(pos);
}
pub fn navigate_next_tab(&mut self) {
let before = self.inner.active_tab();
self.inner.navigate_next_tab();
self.emit_if_changed(before);
}
pub fn navigate_prev_tab(&mut self) {
let before = self.inner.active_tab();
self.inner.navigate_prev_tab();
self.emit_if_changed(before);
}
pub fn tab_color(mut self, color: Color) -> Self {
self.inner.tab_color = color;
self
}
pub fn active_tab_color(mut self, color: Color) -> Self {
self.inner.active_tab_color = color;
self
}
pub fn tab_text_color(mut self, color: Color) -> Self {
self.inner.tab_text_color = color;
self
}
pub fn active_tab_text_color(mut self, color: Color) -> Self {
self.inner.active_tab_text_color = color;
self
}
pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
self.inner.set_font(font);
}
pub fn themed(
mut self,
theme: &Theme,
scheme: ColorScheme,
variant: Variant,
size: ComponentSize,
) -> Self {
let resolved = theme.component_style(scheme, variant, size);
let colors = theme.scheme(scheme);
self.inner.style = resolved.style;
self.inner.active_tab_color = resolved.accent_color;
self.inner.active_tab_text_color = resolved.text_color;
self.inner.tab_color = colors.subtle;
self.inner.tab_text_color = colors.solid;
self
}
pub fn style(&self) -> &rlvgl_core::style::Style {
&self.inner.style
}
pub fn style_mut(&mut self) -> &mut rlvgl_core::style::Style {
&mut self.inner.style
}
fn emit_if_changed(&mut self, before: TabId) {
let after = self.inner.active_tab();
if before != after
&& let Some(cb) = self.on_change.as_mut()
{
cb(after);
}
}
}
impl Widget for Tabs {
fn bounds(&self) -> Rect {
self.inner.bounds()
}
fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
self.inner.widget_font_mut()
}
fn set_bounds(&mut self, bounds: Rect) {
self.inner.set_bounds(bounds);
}
fn draw(&self, renderer: &mut dyn Renderer) {
self.inner.draw(renderer);
}
fn handle_event(&mut self, event: &Event) -> bool {
self.inner.handle_event(event)
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::rc::Rc;
use core::cell::Cell;
#[test]
fn tabs_change_callback_fires_on_navigation() {
let active = Rc::new(Cell::new(Tabs::TAB_NONE));
let flag = active.clone();
let mut tabs = Tabs::new(rect(0, 0, 200, 100), TabBarPos::Top)
.tab("A")
.tab("B")
.on_change(move |id| flag.set(id));
tabs.navigate_next_tab();
assert_eq!(active.get(), TabId(1));
assert_eq!(tabs.active_tab(), TabId(1));
}
#[test]
fn tabs_themed_sets_active_and_inactive_colors() {
let theme = Theme::material_light();
let tabs = Tabs::new(rect(0, 0, 200, 100), TabBarPos::Top).themed(
&theme,
ColorScheme::Primary,
Variant::Solid,
ComponentSize::Md,
);
assert_eq!(tabs.style().bg_color, theme.tokens.colors.primary);
assert_eq!(
tabs.inner.tab_color,
theme.scheme(ColorScheme::Primary).subtle
);
}
#[test]
fn tabs_themed_active_distinct_from_inactive_in_non_solid_variant() {
let theme = Theme::material_light();
for variant in [Variant::Subtle, Variant::Outline, Variant::Ghost] {
let tabs = Tabs::new(rect(0, 0, 200, 100), TabBarPos::Top).themed(
&theme,
ColorScheme::Primary,
variant,
ComponentSize::Md,
);
assert_ne!(
tabs.inner.active_tab_color, tabs.inner.tab_color,
"active tab must stay distinct from inactive ({variant:?})"
);
assert_ne!(
tabs.inner.active_tab_color.3, 0,
"active tab must not be transparent ({variant:?})"
);
}
}
fn rect(x: i32, y: i32, width: i32, height: i32) -> Rect {
Rect {
x,
y,
width,
height,
}
}
}