use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use skia_safe::{Canvas, PaintStyle, Rect};
use crate::engine::renderer::{font_mgr, paint_from_hex, emoji_typeface, draw_text_with_fallback};
use crate::error::RustmotionError;
use crate::layout::{Constraints, LayoutNode};
use crate::schema::{LayerStyle, Size};
use crate::traits::{RenderContext, TimingConfig, Widget};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Table {
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
#[serde(default)]
pub header_color: Option<String>,
#[serde(default)]
pub row_colors: Option<Vec<String>>,
#[serde(default)]
pub border_color: Option<String>,
#[serde(default)]
pub header_text_color: Option<String>,
#[serde(default)]
pub size: Option<Size>,
#[serde(flatten)]
pub timing: TimingConfig,
#[serde(default)]
pub style: LayerStyle,
}
crate::impl_traits!(Table {
Animatable => style,
Timed => timing,
Styled => style,
});
impl Table {
fn font_size(&self) -> f32 {
self.style.font_size.unwrap_or(14.0)
}
fn row_height(&self) -> f32 {
self.font_size() * 2.5
}
fn make_font(&self, bold: bool) -> skia_safe::Font {
let fm = font_mgr();
let font_style = if bold {
skia_safe::FontStyle::bold()
} else {
skia_safe::FontStyle::normal()
};
let family = self
.style
.font_family
.as_deref()
.unwrap_or("Inter");
let typeface = fm
.match_family_style(family, font_style)
.or_else(|| fm.match_family_style("Helvetica", font_style))
.or_else(|| fm.match_family_style("Arial", font_style))
.expect(&RustmotionError::FontNotFound.to_string());
skia_safe::Font::from_typeface(typeface, self.font_size())
}
}
impl Widget for Table {
fn render(
&self,
canvas: &Canvas,
layout: &LayoutNode,
_ctx: &RenderContext,
_props: &crate::engine::animator::AnimatedProperties,
) -> Result<()> {
let w = layout.width;
let col_count = self.headers.len().max(1);
let col_w = w / col_count as f32;
let row_h = self.row_height();
let header_color = self.header_color.as_deref().unwrap_or("#374151");
let border_color = self.border_color.as_deref().unwrap_or("#4B5563");
let text_color = self.style.color.as_deref().unwrap_or("#FFFFFF");
let header_text_color = self.header_text_color.as_deref().unwrap_or("#FFFFFF");
let default_row_colors = vec!["#1F2937".to_string(), "#111827".to_string()];
let row_colors = self.row_colors.as_ref().unwrap_or(&default_row_colors);
let cell_padding = 12.0;
let has_radius = self.style.border_radius.unwrap_or(0.0) > 0.0;
if has_radius {
let radius = self.style.border_radius.unwrap_or(0.0);
let rect = Rect::from_xywh(0.0, 0.0, w, layout.height);
let rrect = skia_safe::RRect::new_rect_xy(rect, radius, radius);
canvas.save();
canvas.clip_rrect(rrect, skia_safe::ClipOp::Intersect, true);
}
let header_font = self.make_font(true);
let font_size = self.font_size();
let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size));
let (_, header_metrics) = header_font.metrics();
let header_ascent = -header_metrics.ascent;
let mut header_bg = paint_from_hex(header_color);
header_bg.set_style(PaintStyle::Fill);
canvas.draw_rect(Rect::from_xywh(0.0, 0.0, w, row_h), &header_bg);
let mut header_text_paint = paint_from_hex(header_text_color);
header_text_paint.set_anti_alias(true);
for (i, header) in self.headers.iter().enumerate() {
let x = i as f32 * col_w + cell_padding;
let y = (row_h - self.font_size()) / 2.0 + header_ascent;
draw_text_with_fallback(canvas, header, &header_font, &emoji_font, 0.0, x, y, &header_text_paint);
}
let body_font = self.make_font(false);
let (_, body_metrics) = body_font.metrics();
let body_ascent = -body_metrics.ascent;
let mut text_paint = paint_from_hex(text_color);
text_paint.set_anti_alias(true);
for (row_idx, row) in self.rows.iter().enumerate() {
let y_base = (row_idx + 1) as f32 * row_h;
let row_color_idx = row_idx % row_colors.len().max(1);
let row_bg_color = &row_colors[row_color_idx];
let mut row_bg = paint_from_hex(row_bg_color);
row_bg.set_style(PaintStyle::Fill);
canvas.draw_rect(Rect::from_xywh(0.0, y_base, w, row_h), &row_bg);
for (col_idx, cell) in row.iter().enumerate() {
if col_idx >= col_count {
break;
}
let x = col_idx as f32 * col_w + cell_padding;
let y = y_base + (row_h - self.font_size()) / 2.0 + body_ascent;
draw_text_with_fallback(canvas, cell, &body_font, &emoji_font, 0.0, x, y, &text_paint);
}
}
let mut border_paint = paint_from_hex(border_color);
border_paint.set_style(PaintStyle::Stroke);
border_paint.set_stroke_width(1.0);
let total_h = (1 + self.rows.len()) as f32 * row_h;
for i in 0..=(self.rows.len() + 1) {
let y = i as f32 * row_h;
canvas.draw_line((0.0, y), (w, y), &border_paint);
}
for i in 0..=col_count {
let x = i as f32 * col_w;
canvas.draw_line((x, 0.0), (x, total_h), &border_paint);
}
if has_radius {
canvas.restore();
}
Ok(())
}
fn measure(&self, _constraints: &Constraints) -> (f32, f32) {
if let Some(size) = &self.size {
return (size.width, size.height);
}
let col_count = self.headers.len().max(1);
let w = col_count as f32 * 120.0;
let h = (1 + self.rows.len()) as f32 * self.row_height();
(w, h)
}
}