use rosace_core::types::{Point, Rect, Size};
use rosace_layout::Constraints;
use rosace_render::Color;
use super::{avail_w, BoxedWidget, Children, LayoutCtx, PaintCtx, Widget};
#[derive(Clone, Copy, Debug, PartialEq)]
enum ColumnSizing {
Auto,
Fixed(f32),
Flex(f32),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TableColumn {
sizing: ColumnSizing,
}
impl TableColumn {
pub fn auto() -> Self { Self { sizing: ColumnSizing::Auto } }
pub fn fixed(px: f32) -> Self { Self { sizing: ColumnSizing::Fixed(px.max(0.0)) } }
pub fn flex(factor: f32) -> Self { Self { sizing: ColumnSizing::Flex(factor.max(0.0)) } }
}
pub struct Table {
columns: Vec<TableColumn>,
cells: Vec<BoxedWidget>,
row_lens: Vec<usize>,
h_spacing: f32,
v_spacing: f32,
cell_padding: f32,
row_background: Option<Color>,
divider_width: f32,
divider_color: Option<Color>,
}
impl Table {
pub fn new() -> Self {
Self {
columns: Vec::new(),
cells: Vec::new(),
row_lens: Vec::new(),
h_spacing: 8.0,
v_spacing: 8.0,
cell_padding: 0.0,
row_background: None,
divider_width: 0.0,
divider_color: None,
}
}
pub fn column(mut self, c: TableColumn) -> Self { self.columns.push(c); self }
pub fn columns(mut self, cs: Vec<TableColumn>) -> Self { self.columns.extend(cs); self }
pub fn row(mut self, cells: Vec<BoxedWidget>) -> Self {
self.row_lens.push(cells.len());
self.cells.extend(cells);
self
}
pub fn row_builder(mut self, count: usize, builder: impl Fn(usize) -> Vec<BoxedWidget>) -> Self {
for i in 0..count {
self = self.row(builder(i));
}
self
}
pub fn spacing(mut self, h: f32, v: f32) -> Self {
self.h_spacing = h.max(0.0);
self.v_spacing = v.max(0.0);
self
}
pub fn cell_padding(mut self, p: f32) -> Self { self.cell_padding = p.max(0.0); self }
pub fn row_background(mut self, c: Color) -> Self { self.row_background = Some(c); self }
pub fn divider(mut self, width: f32) -> Self { self.divider_width = width.max(0.0); self }
pub fn divider_color(mut self, c: Color) -> Self { self.divider_color = Some(c); self }
fn row_range(&self, r: usize) -> std::ops::Range<usize> {
let start: usize = self.row_lens[..r].iter().sum();
start..start + self.row_lens[r]
}
fn cell(&self, row: usize, col: usize) -> Option<&BoxedWidget> {
let range = self.row_range(row);
if col < self.row_lens[row] { self.cells.get(range.start + col) } else { None }
}
fn resolve_columns(&self, ctx: &LayoutCtx, total_w: f32) -> Vec<f32> {
let n = self.columns.len();
let gaps = self.h_spacing * n.saturating_sub(1) as f32;
let pad2 = self.cell_padding * 2.0;
let bounded = total_w.is_finite();
let measure_w = if bounded { total_w } else { f32::MAX };
let intrinsic = |i: usize| -> f32 {
let mut w = 0.0f32;
for row in 0..self.row_lens.len() {
if let Some(cell) = self.cell(row, i) {
let s = cell.layout(&ctx.with_constraints(
Constraints::loose(measure_w, f32::INFINITY),
));
w = w.max(s.width);
}
}
w + pad2
};
let mut widths = vec![0.0f32; n];
let mut flex_sum = 0.0f32;
let mut used = 0.0f32;
for (i, col) in self.columns.iter().enumerate() {
match col.sizing {
ColumnSizing::Fixed(px) => { widths[i] = px; used += px; }
ColumnSizing::Auto => { widths[i] = intrinsic(i); used += widths[i]; }
ColumnSizing::Flex(_) if !bounded => {
widths[i] = intrinsic(i);
used += widths[i];
}
ColumnSizing::Flex(f) => flex_sum += f,
}
}
if bounded && flex_sum > 0.0 {
let leftover = (total_w - used - gaps).max(0.0);
for (i, col) in self.columns.iter().enumerate() {
if let ColumnSizing::Flex(f) = col.sizing {
widths[i] = leftover * (f / flex_sum);
}
}
}
widths
}
fn row_heights(&self, ctx: &LayoutCtx, widths: &[f32]) -> Vec<f32> {
let pad2 = self.cell_padding * 2.0;
(0..self.row_lens.len())
.map(|row| {
let mut h = 0.0f32;
for (col, w) in widths.iter().enumerate() {
if let Some(cell) = self.cell(row, col) {
let s = cell.layout(&ctx.with_constraints(
Constraints::loose((w - pad2).max(0.0), f32::INFINITY),
));
h = h.max(s.height);
}
}
h + pad2
})
.collect()
}
fn content_size(&self, ctx: &LayoutCtx, total_w: f32) -> Size {
let widths = self.resolve_columns(ctx, total_w);
let heights = self.row_heights(ctx, &widths);
let gaps_w = self.h_spacing * self.columns.len().saturating_sub(1) as f32;
let gaps_h = self.v_spacing * heights.len().saturating_sub(1) as f32;
Size {
width: widths.iter().sum::<f32>() + gaps_w,
height: heights.iter().sum::<f32>() + gaps_h,
}
}
fn has_flex(&self) -> bool {
self.columns.iter().any(|c| matches!(c.sizing, ColumnSizing::Flex(_)))
}
}
impl Default for Table {
fn default() -> Self { Self::new() }
}
impl Widget for Table {
fn children(&self) -> Children<'_> { Children::Many(&self.cells) }
fn layout(&self, ctx: &LayoutCtx) -> Size {
let w = avail_w(ctx.constraints);
let content = self.content_size(ctx, w);
let width = if self.has_flex() && w.is_finite() { w } else { content.width };
ctx.constraints.constrain(Size { width, height: content.height })
}
fn paint(&self, ctx: &mut PaintCtx) {
let divider = self
.divider_color
.unwrap_or_else(|| ctx.tc(ctx.theme.colors.outline));
let r = ctx.rect;
let (widths, heights) = {
let lctx = ctx.layout_ctx(Constraints::loose(r.size.width, f32::INFINITY));
let widths = self.resolve_columns(&lctx, r.size.width);
let heights = self.row_heights(&lctx, &widths);
(widths, heights)
};
let pad = self.cell_padding;
let mut y = r.origin.y;
for (row, row_h) in heights.iter().enumerate() {
if row % 2 == 1 {
if let Some(bg) = self.row_background {
ctx.fill_rect(
Rect {
origin: Point { x: r.origin.x, y },
size: Size { width: r.size.width, height: *row_h },
},
bg,
);
}
}
let mut x = r.origin.x;
for (col, w) in widths.iter().enumerate() {
if let Some(cell) = self.cell(row, col) {
let content_w = (w - pad * 2.0).max(0.0);
let s = cell.layout(&ctx.layout_ctx(
Constraints::loose(content_w, f32::INFINITY),
));
let rect = Rect {
origin: Point { x: x + pad, y: y + pad },
size: Size { width: s.width.min(content_w), height: s.height },
};
cell.paint(&mut ctx.child(rect));
}
x += w + self.h_spacing;
}
y += row_h;
if row + 1 < heights.len() {
if self.divider_width > 0.0 {
let dy = y + ((self.v_spacing - self.divider_width) / 2.0).max(0.0);
ctx.fill_rect(
Rect {
origin: Point { x: r.origin.x, y: dy },
size: Size { width: r.size.width, height: self.divider_width },
},
divider,
);
}
y += self.v_spacing;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct FixedCell(f32, f32);
impl Widget for FixedCell {
fn layout(&self, _ctx: &LayoutCtx) -> Size {
Size { width: self.0, height: self.1 }
}
fn paint(&self, _ctx: &mut PaintCtx) {}
}
fn boxed(w: f32, h: f32) -> BoxedWidget { Box::new(FixedCell(w, h)) }
fn test_env() -> (rosace_render::FontCache, rosace_theme::ThemeData) {
(rosace_render::FontCache::embedded(), rosace_theme::built_in::dark_theme())
}
#[test]
fn fixed_auto_and_flex_columns_resolve_in_a_300px_width() {
let table = Table::new()
.column(TableColumn::fixed(100.0))
.column(TableColumn::auto())
.column(TableColumn::flex(1.0))
.spacing(10.0, 0.0)
.row(vec![boxed(40.0, 20.0), boxed(50.0, 30.0), boxed(10.0, 10.0)])
.row(vec![boxed(80.0, 15.0), boxed(30.0, 12.0), boxed(10.0, 10.0)]);
let (font, theme) = test_env();
let ctx = LayoutCtx::new(Constraints::loose(300.0, 1000.0), &font, &theme);
let widths = table.resolve_columns(&ctx, 300.0);
assert_eq!(widths, vec![100.0, 50.0, 130.0]);
assert_eq!(table.layout(&ctx).width, 300.0);
}
#[test]
fn two_flex_columns_share_leftover_by_factor() {
let table = Table::new()
.column(TableColumn::fixed(60.0))
.column(TableColumn::flex(1.0))
.column(TableColumn::flex(3.0))
.spacing(0.0, 0.0)
.row(vec![boxed(10.0, 10.0), boxed(10.0, 10.0), boxed(10.0, 10.0)]);
let (font, theme) = test_env();
let ctx = LayoutCtx::new(Constraints::loose(300.0, 1000.0), &font, &theme);
assert_eq!(table.resolve_columns(&ctx, 300.0), vec![60.0, 60.0, 180.0]);
}
#[test]
fn row_height_is_the_tallest_cell_of_each_row() {
let table = Table::new()
.column(TableColumn::fixed(100.0))
.column(TableColumn::fixed(100.0))
.spacing(0.0, 10.0)
.row(vec![boxed(40.0, 20.0), boxed(50.0, 44.0)])
.row(vec![boxed(40.0, 16.0), boxed(50.0, 8.0)]);
let (font, theme) = test_env();
let ctx = LayoutCtx::new(Constraints::loose(300.0, 1000.0), &font, &theme);
let heights = table.row_heights(&ctx, &[100.0, 100.0]);
assert_eq!(heights, vec![44.0, 16.0]);
assert_eq!(table.layout(&ctx).height, 70.0);
}
#[test]
fn cell_padding_grows_auto_columns_and_row_heights() {
let table = Table::new()
.column(TableColumn::auto())
.cell_padding(6.0)
.row(vec![boxed(50.0, 20.0)]);
let (font, theme) = test_env();
let ctx = LayoutCtx::new(Constraints::loose(300.0, 1000.0), &font, &theme);
assert_eq!(table.resolve_columns(&ctx, 300.0), vec![62.0]);
assert_eq!(table.layout(&ctx).height, 32.0);
assert_eq!(table.layout(&ctx).width, 62.0);
}
}