#[cfg(feature = "theme-table")]
use oxiui_core::Palette;
#[cfg(feature = "theme-table")]
use oxiui_theme::tokens::DesignTokens;
#[derive(Clone, Debug, PartialEq)]
pub struct TableTheme {
pub header_bg: [u8; 4],
pub header_fg: [u8; 4],
pub row_bg: [u8; 4],
pub row_stripe_bg: [u8; 4],
pub selection_bg: [u8; 4],
pub selection_fg: [u8; 4],
pub border_color: [u8; 4],
pub focus_ring_color: [u8; 4],
pub cell_fg: [u8; 4],
pub footer_bg: [u8; 4],
pub footer_fg: [u8; 4],
pub cell_padding_x: f32,
pub cell_padding_y: f32,
pub focus_radius: f32,
}
impl Default for TableTheme {
fn default() -> Self {
Self {
header_bg: [36, 40, 59, 255],
header_fg: [192, 202, 245, 255], row_bg: [26, 27, 38, 255],
row_stripe_bg: [31, 32, 53, 255],
selection_bg: [122, 162, 247, 77], selection_fg: [255, 255, 255, 255],
border_color: [86, 95, 137, 128], focus_ring_color: [122, 162, 247, 128],
cell_fg: [192, 202, 245, 255],
footer_bg: [36, 40, 59, 255],
footer_fg: [192, 202, 245, 200],
cell_padding_x: 8.0,
cell_padding_y: 4.0,
focus_radius: 2.0,
}
}
}
impl TableTheme {
#[cfg(feature = "theme-table")]
pub fn from_palette(palette: &Palette, tokens: Option<&DesignTokens>) -> Self {
use oxiui_theme::color::darken;
let stripe_color = darken(palette.background, 0.05);
let sel_a = (0.30_f32 * 255.0_f32).round() as u8;
let selection_bg = [
palette.primary.0,
palette.primary.1,
palette.primary.2,
sel_a,
];
let border_color = [palette.muted.0, palette.muted.1, palette.muted.2, 128];
let focus_ring_color = [palette.primary.0, palette.primary.1, palette.primary.2, 128];
let footer_fg = [palette.text.0, palette.text.1, palette.text.2, 200];
let (cell_padding_x, cell_padding_y, focus_radius) = if let Some(t) = tokens {
use oxiui_theme::tokens::{RadiusStep, SpacingStep};
(
t.spacing(SpacingStep::Sm), t.spacing(SpacingStep::Xs), t.radius(RadiusStep::Sm), )
} else {
(8.0, 4.0, 2.0)
};
Self {
header_bg: [palette.surface.0, palette.surface.1, palette.surface.2, 255],
header_fg: [palette.text.0, palette.text.1, palette.text.2, 255],
row_bg: [
palette.background.0,
palette.background.1,
palette.background.2,
255,
],
row_stripe_bg: [stripe_color.0, stripe_color.1, stripe_color.2, 255],
selection_bg,
selection_fg: [
palette.on_primary.0,
palette.on_primary.1,
palette.on_primary.2,
255,
],
border_color,
focus_ring_color,
cell_fg: [palette.text.0, palette.text.1, palette.text.2, 255],
footer_bg: [palette.surface.0, palette.surface.1, palette.surface.2, 255],
footer_fg,
cell_padding_x,
cell_padding_y,
focus_radius,
}
}
#[cfg(feature = "theme-table")]
pub fn from_tokens(tokens: &DesignTokens) -> Self {
use oxiui_core::Theme;
use oxiui_core::{Color, FontSpec, Palette};
use oxiui_theme::CooljapanTheme;
let theme = CooljapanTheme::new(
Palette {
background: Color(26, 27, 38, 255),
surface: Color(36, 40, 59, 255),
primary: Color(122, 162, 247, 255),
on_primary: Color(26, 27, 38, 255),
text: Color(192, 202, 245, 255),
muted: Color(86, 95, 137, 255),
},
FontSpec::new("Inter", 14.0, 400),
);
Self::from_palette(theme.palette(), Some(tokens))
}
pub fn is_dark(&self) -> bool {
let [r, g, b, _] = self.row_bg;
let luma =
0.2126 * (r as f32 / 255.0) + 0.7152 * (g as f32 / 255.0) + 0.0722 * (b as f32 / 255.0);
luma < 0.5
}
pub fn effective_row_bg(&self, row_index: usize, is_selected: bool, zebra: bool) -> [u8; 4] {
if is_selected {
let base = if zebra && row_index % 2 == 1 {
self.row_stripe_bg
} else {
self.row_bg
};
alpha_blend(self.selection_bg, base)
} else if zebra && row_index % 2 == 1 {
self.row_stripe_bg
} else {
self.row_bg
}
}
}
fn alpha_blend(src: [u8; 4], dst: [u8; 4]) -> [u8; 4] {
let a = src[3] as u32;
let ia = 255 - a;
let blend = |s: u8, d: u8| -> u8 {
let v = a * s as u32 + ia * d as u32;
((v + 127) / 255) as u8
};
[
blend(src[0], dst[0]),
blend(src[1], dst[1]),
blend(src[2], dst[2]),
255,
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_theme_is_dark() {
assert!(TableTheme::default().is_dark());
}
#[test]
fn default_header_bg_is_surface() {
let theme = TableTheme::default();
assert_eq!(theme.header_bg[0], 36);
assert_eq!(theme.header_bg[1], 40);
assert_eq!(theme.header_bg[2], 59);
assert_eq!(theme.header_bg[3], 255);
}
#[test]
fn effective_row_bg_normal() {
let theme = TableTheme::default();
let bg = theme.effective_row_bg(0, false, false);
assert_eq!(bg, theme.row_bg);
}
#[test]
fn effective_row_bg_zebra_odd() {
let theme = TableTheme::default();
let bg = theme.effective_row_bg(1, false, true);
assert_eq!(bg, theme.row_stripe_bg);
}
#[test]
fn effective_row_bg_zebra_even() {
let theme = TableTheme::default();
let bg = theme.effective_row_bg(0, false, true);
assert_eq!(bg, theme.row_bg);
}
#[test]
fn effective_row_bg_selected_is_blended() {
let theme = TableTheme::default();
let bg = theme.effective_row_bg(0, true, false);
assert_ne!(bg, theme.row_bg);
assert_eq!(bg[3], 255);
}
#[test]
fn alpha_blend_fully_transparent_is_dst() {
let src = [100, 150, 200, 0]; let dst = [10, 20, 30, 255];
let result = alpha_blend(src, dst);
assert!((result[0] as i32 - dst[0] as i32).abs() <= 1);
assert!((result[1] as i32 - dst[1] as i32).abs() <= 1);
assert!((result[2] as i32 - dst[2] as i32).abs() <= 1);
}
#[test]
fn alpha_blend_fully_opaque_is_src() {
let src = [100, 150, 200, 255]; let dst = [10, 20, 30, 255];
let result = alpha_blend(src, dst);
assert_eq!(result[0], 100);
assert_eq!(result[1], 150);
assert_eq!(result[2], 200);
assert_eq!(result[3], 255);
}
#[test]
fn selection_bg_has_partial_alpha() {
let theme = TableTheme::default();
let a = theme.selection_bg[3];
assert!(
a > 0 && a < 255,
"selection_bg alpha should be partial, got {a}"
);
}
#[test]
fn cell_padding_positive() {
let theme = TableTheme::default();
assert!(theme.cell_padding_x > 0.0);
assert!(theme.cell_padding_y > 0.0);
}
#[test]
fn focus_radius_non_negative() {
let theme = TableTheme::default();
assert!(theme.focus_radius >= 0.0);
}
#[cfg(feature = "theme-table")]
#[test]
fn from_tokens_returns_valid_theme() {
use oxiui_theme::tokens::DesignTokens;
let tokens = DesignTokens::default();
let theme = TableTheme::from_tokens(&tokens);
assert!((theme.cell_padding_x - 8.0).abs() < f32::EPSILON);
assert!((theme.cell_padding_y - 4.0).abs() < f32::EPSILON);
}
#[cfg(feature = "theme-table")]
#[test]
fn from_palette_header_bg_is_surface() {
use oxiui_core::{Color, Palette};
let palette = Palette {
background: Color(10, 10, 10, 255),
surface: Color(30, 30, 30, 255),
primary: Color(100, 150, 200, 255),
on_primary: Color(0, 0, 0, 255),
text: Color(220, 220, 220, 255),
muted: Color(80, 80, 80, 255),
};
let theme = TableTheme::from_palette(&palette, None);
assert_eq!(theme.header_bg[0], 30);
assert_eq!(theme.header_bg[1], 30);
assert_eq!(theme.header_bg[2], 30);
}
#[cfg(feature = "theme-table")]
#[test]
fn from_palette_selection_has_partial_alpha() {
use oxiui_core::{Color, Palette};
let palette = Palette {
background: Color(10, 10, 10, 255),
surface: Color(30, 30, 30, 255),
primary: Color(100, 150, 200, 255),
on_primary: Color(0, 0, 0, 255),
text: Color(220, 220, 220, 255),
muted: Color(80, 80, 80, 255),
};
let theme = TableTheme::from_palette(&palette, None);
let a = theme.selection_bg[3];
assert!(a > 0 && a < 255, "selection alpha must be partial, got {a}");
}
}