1#[cfg(feature = "theme-table")]
33use oxiui_core::Palette;
34#[cfg(feature = "theme-table")]
35use oxiui_theme::tokens::DesignTokens;
36
37#[derive(Clone, Debug, PartialEq)]
46pub struct TableTheme {
47 pub header_bg: [u8; 4],
49 pub header_fg: [u8; 4],
51 pub row_bg: [u8; 4],
53 pub row_stripe_bg: [u8; 4],
55 pub selection_bg: [u8; 4],
57 pub selection_fg: [u8; 4],
59 pub border_color: [u8; 4],
61 pub focus_ring_color: [u8; 4],
63 pub cell_fg: [u8; 4],
65 pub footer_bg: [u8; 4],
67 pub footer_fg: [u8; 4],
69 pub cell_padding_x: f32,
71 pub cell_padding_y: f32,
73 pub focus_radius: f32,
75}
76
77impl Default for TableTheme {
78 fn default() -> Self {
84 Self {
85 header_bg: [36, 40, 59, 255],
87 header_fg: [192, 202, 245, 255], row_bg: [26, 27, 38, 255],
90 row_stripe_bg: [31, 32, 53, 255],
91 selection_bg: [122, 162, 247, 77], selection_fg: [255, 255, 255, 255],
94 border_color: [86, 95, 137, 128], focus_ring_color: [122, 162, 247, 128],
98 cell_fg: [192, 202, 245, 255],
100 footer_bg: [36, 40, 59, 255],
102 footer_fg: [192, 202, 245, 200],
103 cell_padding_x: 8.0,
105 cell_padding_y: 4.0,
106 focus_radius: 2.0,
107 }
108 }
109}
110
111impl TableTheme {
112 #[cfg(feature = "theme-table")]
117 pub fn from_palette(palette: &Palette, tokens: Option<&DesignTokens>) -> Self {
118 use oxiui_theme::color::darken;
119
120 let stripe_color = darken(palette.background, 0.05);
122
123 let sel_a = (0.30_f32 * 255.0_f32).round() as u8;
125 let selection_bg = [
126 palette.primary.0,
127 palette.primary.1,
128 palette.primary.2,
129 sel_a,
130 ];
131
132 let border_color = [palette.muted.0, palette.muted.1, palette.muted.2, 128];
134
135 let focus_ring_color = [palette.primary.0, palette.primary.1, palette.primary.2, 128];
137
138 let footer_fg = [palette.text.0, palette.text.1, palette.text.2, 200];
140
141 let (cell_padding_x, cell_padding_y, focus_radius) = if let Some(t) = tokens {
143 use oxiui_theme::tokens::{RadiusStep, SpacingStep};
144 (
145 t.spacing(SpacingStep::Sm), t.spacing(SpacingStep::Xs), t.radius(RadiusStep::Sm), )
149 } else {
150 (8.0, 4.0, 2.0)
151 };
152
153 Self {
154 header_bg: [palette.surface.0, palette.surface.1, palette.surface.2, 255],
155 header_fg: [palette.text.0, palette.text.1, palette.text.2, 255],
156 row_bg: [
157 palette.background.0,
158 palette.background.1,
159 palette.background.2,
160 255,
161 ],
162 row_stripe_bg: [stripe_color.0, stripe_color.1, stripe_color.2, 255],
163 selection_bg,
164 selection_fg: [
165 palette.on_primary.0,
166 palette.on_primary.1,
167 palette.on_primary.2,
168 255,
169 ],
170 border_color,
171 focus_ring_color,
172 cell_fg: [palette.text.0, palette.text.1, palette.text.2, 255],
173 footer_bg: [palette.surface.0, palette.surface.1, palette.surface.2, 255],
174 footer_fg,
175 cell_padding_x,
176 cell_padding_y,
177 focus_radius,
178 }
179 }
180
181 #[cfg(feature = "theme-table")]
186 pub fn from_tokens(tokens: &DesignTokens) -> Self {
187 use oxiui_core::Theme;
188 use oxiui_core::{Color, FontSpec, Palette};
189 use oxiui_theme::CooljapanTheme;
190
191 let theme = CooljapanTheme::new(
193 Palette {
194 background: Color(26, 27, 38, 255),
195 surface: Color(36, 40, 59, 255),
196 primary: Color(122, 162, 247, 255),
197 on_primary: Color(26, 27, 38, 255),
198 text: Color(192, 202, 245, 255),
199 muted: Color(86, 95, 137, 255),
200 },
201 FontSpec::new("Inter", 14.0, 400),
202 );
203 Self::from_palette(theme.palette(), Some(tokens))
204 }
205
206 pub fn is_dark(&self) -> bool {
210 let [r, g, b, _] = self.row_bg;
211 let luma =
213 0.2126 * (r as f32 / 255.0) + 0.7152 * (g as f32 / 255.0) + 0.0722 * (b as f32 / 255.0);
214 luma < 0.5
215 }
216
217 pub fn effective_row_bg(&self, row_index: usize, is_selected: bool, zebra: bool) -> [u8; 4] {
222 if is_selected {
223 let base = if zebra && row_index % 2 == 1 {
225 self.row_stripe_bg
226 } else {
227 self.row_bg
228 };
229 alpha_blend(self.selection_bg, base)
230 } else if zebra && row_index % 2 == 1 {
231 self.row_stripe_bg
232 } else {
233 self.row_bg
234 }
235 }
236}
237
238fn alpha_blend(src: [u8; 4], dst: [u8; 4]) -> [u8; 4] {
242 let a = src[3] as u32;
243 let ia = 255 - a;
244 let blend = |s: u8, d: u8| -> u8 {
245 let v = a * s as u32 + ia * d as u32;
246 ((v + 127) / 255) as u8
247 };
248 [
249 blend(src[0], dst[0]),
250 blend(src[1], dst[1]),
251 blend(src[2], dst[2]),
252 255,
253 ]
254}
255
256#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn default_theme_is_dark() {
264 assert!(TableTheme::default().is_dark());
265 }
266
267 #[test]
268 fn default_header_bg_is_surface() {
269 let theme = TableTheme::default();
270 assert_eq!(theme.header_bg[0], 36);
272 assert_eq!(theme.header_bg[1], 40);
273 assert_eq!(theme.header_bg[2], 59);
274 assert_eq!(theme.header_bg[3], 255);
275 }
276
277 #[test]
278 fn effective_row_bg_normal() {
279 let theme = TableTheme::default();
280 let bg = theme.effective_row_bg(0, false, false);
281 assert_eq!(bg, theme.row_bg);
282 }
283
284 #[test]
285 fn effective_row_bg_zebra_odd() {
286 let theme = TableTheme::default();
287 let bg = theme.effective_row_bg(1, false, true);
288 assert_eq!(bg, theme.row_stripe_bg);
289 }
290
291 #[test]
292 fn effective_row_bg_zebra_even() {
293 let theme = TableTheme::default();
294 let bg = theme.effective_row_bg(0, false, true);
295 assert_eq!(bg, theme.row_bg);
296 }
297
298 #[test]
299 fn effective_row_bg_selected_is_blended() {
300 let theme = TableTheme::default();
301 let bg = theme.effective_row_bg(0, true, false);
302 assert_ne!(bg, theme.row_bg);
305 assert_eq!(bg[3], 255);
307 }
308
309 #[test]
310 fn alpha_blend_fully_transparent_is_dst() {
311 let src = [100, 150, 200, 0]; let dst = [10, 20, 30, 255];
313 let result = alpha_blend(src, dst);
314 assert!((result[0] as i32 - dst[0] as i32).abs() <= 1);
316 assert!((result[1] as i32 - dst[1] as i32).abs() <= 1);
317 assert!((result[2] as i32 - dst[2] as i32).abs() <= 1);
318 }
319
320 #[test]
321 fn alpha_blend_fully_opaque_is_src() {
322 let src = [100, 150, 200, 255]; let dst = [10, 20, 30, 255];
324 let result = alpha_blend(src, dst);
325 assert_eq!(result[0], 100);
326 assert_eq!(result[1], 150);
327 assert_eq!(result[2], 200);
328 assert_eq!(result[3], 255);
329 }
330
331 #[test]
332 fn selection_bg_has_partial_alpha() {
333 let theme = TableTheme::default();
335 let a = theme.selection_bg[3];
336 assert!(
337 a > 0 && a < 255,
338 "selection_bg alpha should be partial, got {a}"
339 );
340 }
341
342 #[test]
343 fn cell_padding_positive() {
344 let theme = TableTheme::default();
345 assert!(theme.cell_padding_x > 0.0);
346 assert!(theme.cell_padding_y > 0.0);
347 }
348
349 #[test]
350 fn focus_radius_non_negative() {
351 let theme = TableTheme::default();
352 assert!(theme.focus_radius >= 0.0);
353 }
354
355 #[cfg(feature = "theme-table")]
356 #[test]
357 fn from_tokens_returns_valid_theme() {
358 use oxiui_theme::tokens::DesignTokens;
359 let tokens = DesignTokens::default();
360 let theme = TableTheme::from_tokens(&tokens);
361 assert!((theme.cell_padding_x - 8.0).abs() < f32::EPSILON);
363 assert!((theme.cell_padding_y - 4.0).abs() < f32::EPSILON);
364 }
365
366 #[cfg(feature = "theme-table")]
367 #[test]
368 fn from_palette_header_bg_is_surface() {
369 use oxiui_core::{Color, Palette};
370 let palette = Palette {
371 background: Color(10, 10, 10, 255),
372 surface: Color(30, 30, 30, 255),
373 primary: Color(100, 150, 200, 255),
374 on_primary: Color(0, 0, 0, 255),
375 text: Color(220, 220, 220, 255),
376 muted: Color(80, 80, 80, 255),
377 };
378 let theme = TableTheme::from_palette(&palette, None);
379 assert_eq!(theme.header_bg[0], 30);
380 assert_eq!(theme.header_bg[1], 30);
381 assert_eq!(theme.header_bg[2], 30);
382 }
383
384 #[cfg(feature = "theme-table")]
385 #[test]
386 fn from_palette_selection_has_partial_alpha() {
387 use oxiui_core::{Color, Palette};
388 let palette = Palette {
389 background: Color(10, 10, 10, 255),
390 surface: Color(30, 30, 30, 255),
391 primary: Color(100, 150, 200, 255),
392 on_primary: Color(0, 0, 0, 255),
393 text: Color(220, 220, 220, 255),
394 muted: Color(80, 80, 80, 255),
395 };
396 let theme = TableTheme::from_palette(&palette, None);
397 let a = theme.selection_bg[3];
398 assert!(a > 0 && a < 255, "selection alpha must be partial, got {a}");
399 }
400}