use embedded_graphics::{
mono_font::{
MonoTextStyleBuilder,
ascii::{FONT_6X10, FONT_7X14},
},
pixelcolor::Rgb565,
prelude::*,
primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle},
text::{Alignment, Baseline, Text, TextStyleBuilder},
};
use super::{FsTheme, I18n, Localized, TouchEvent, TouchPhase};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TabSpec<'a, K> {
pub key: K,
pub icon: &'a str,
pub title: Localized<'a>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TabBar<'a, K, const N: usize> {
tabs: [TabSpec<'a, K>; N],
active_index: usize,
}
impl<'a, K: Copy, const N: usize> TabBar<'a, K, N> {
pub const fn new(tabs: [TabSpec<'a, K>; N], active_index: usize) -> Self {
Self { tabs, active_index }
}
pub fn select(&mut self, active_index: usize) {
self.active_index = active_index.min(N.saturating_sub(1));
}
pub fn active(&self) -> K {
self.tabs[self.active_index.min(N.saturating_sub(1))].key
}
pub fn content_frame(&self, bounds: Rectangle, theme: &FsTheme) -> Rectangle {
Rectangle::new(
bounds.top_left,
Size::new(
bounds.size.width,
bounds.size.height.saturating_sub(theme.tab_bar_height),
),
)
}
pub fn handle_touch(
&mut self,
touch: TouchEvent,
bounds: Rectangle,
theme: &FsTheme,
) -> Option<K> {
if !matches!(touch.phase, TouchPhase::End) {
return None;
}
let index = self.hit_test(touch, bounds, theme)?;
if index == self.active_index {
return None;
}
self.active_index = index;
Some(self.tabs[index].key)
}
pub fn draw_bar<D>(&self, display: &mut D, bounds: Rectangle, theme: &FsTheme, i18n: &I18n<'a>)
where
D: DrawTarget<Color = Rgb565>,
{
let bar = tab_bar_frame(bounds, theme);
bar.into_styled(PrimitiveStyle::with_fill(theme.surface))
.draw(display)
.ok();
Line::new(
bar.top_left,
bar.top_left + Point::new(bar.size.width as i32, 0),
)
.into_styled(PrimitiveStyle::with_stroke(theme.surface_alt, 2))
.draw(display)
.ok();
for index in 0..N {
self.draw_tab(display, index, bar, theme, i18n);
}
}
fn hit_test(&self, touch: TouchEvent, bounds: Rectangle, theme: &FsTheme) -> Option<usize> {
if !touch.within(tab_bar_frame(bounds, theme)) {
return None;
}
let width = bounds.size.width as i32 / N as i32;
let relative_x = touch.point.x - bounds.top_left.x;
let index = (relative_x / width.max(1)) as usize;
Some(index.min(N.saturating_sub(1)))
}
fn draw_tab<D>(
&self,
display: &mut D,
index: usize,
bar: Rectangle,
theme: &FsTheme,
i18n: &I18n<'a>,
) where
D: DrawTarget<Color = Rgb565>,
{
let tab = tab_frame::<N>(bar, index);
let spec = self.tabs[index];
let selected = index == self.active_index;
let accent = if selected {
theme.accent
} else {
theme.surface
};
let title_color = if selected {
theme.text_primary
} else {
theme.text_secondary
};
if selected {
Rectangle::new(tab.top_left, Size::new(tab.size.width, 4))
.into_styled(PrimitiveStyle::with_fill(accent))
.draw(display)
.ok();
}
let icon_style = MonoTextStyleBuilder::new()
.font(&FONT_7X14)
.text_color(title_color)
.build();
let title_style = MonoTextStyleBuilder::new()
.font(&FONT_6X10)
.text_color(title_color)
.build();
let text_style = TextStyleBuilder::new()
.alignment(Alignment::Center)
.baseline(Baseline::Middle)
.build();
Text::with_text_style(
spec.icon,
tab.center() + Point::new(0, -10),
icon_style,
text_style,
)
.draw(display)
.ok();
Text::with_text_style(
i18n.text(spec.title),
tab.center() + Point::new(0, 14),
title_style,
text_style,
)
.draw(display)
.ok();
let outline = PrimitiveStyleBuilder::new()
.stroke_color(theme.surface_alt)
.stroke_width(1)
.build();
Rectangle::new(tab.top_left, Size::new(tab.size.width, tab.size.height))
.into_styled(outline)
.draw(display)
.ok();
}
}
fn tab_bar_frame(bounds: Rectangle, theme: &FsTheme) -> Rectangle {
let top =
bounds.top_left + Point::new(0, bounds.size.height as i32 - theme.tab_bar_height as i32);
Rectangle::new(top, Size::new(bounds.size.width, theme.tab_bar_height))
}
fn tab_frame<const N: usize>(bar: Rectangle, index: usize) -> Rectangle {
let tab_width = (bar.size.width / N as u32).max(1);
let x = bar.top_left.x + (index as i32 * tab_width as i32);
let width = if index + 1 == N {
bar.size.width - (tab_width * index as u32)
} else {
tab_width
};
Rectangle::new(
Point::new(x, bar.top_left.y),
Size::new(width, bar.size.height),
)
}