1use rosace_core::types::{Point, Rect, Size};
14use rosace_layout::Constraints;
15use rosace_render::Color;
16
17use super::{avail_w, BoxedWidget, Children, LayoutCtx, PaintCtx, Widget};
18
19#[derive(Clone, Copy, Debug, PartialEq)]
21enum ColumnSizing {
22 Auto,
24 Fixed(f32),
26 Flex(f32),
29}
30
31#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct TableColumn {
35 sizing: ColumnSizing,
36}
37
38impl TableColumn {
39 pub fn auto() -> Self { Self { sizing: ColumnSizing::Auto } }
42 pub fn fixed(px: f32) -> Self { Self { sizing: ColumnSizing::Fixed(px.max(0.0)) } }
44 pub fn flex(factor: f32) -> Self { Self { sizing: ColumnSizing::Flex(factor.max(0.0)) } }
49}
50
51pub struct Table {
55 columns: Vec<TableColumn>,
56 cells: Vec<BoxedWidget>,
58 row_lens: Vec<usize>,
59 h_spacing: f32,
60 v_spacing: f32,
61 cell_padding: f32,
63 row_background: Option<Color>,
65 divider_width: f32,
67 divider_color: Option<Color>,
68}
69
70impl Table {
71 pub fn new() -> Self {
73 Self {
74 columns: Vec::new(),
75 cells: Vec::new(),
76 row_lens: Vec::new(),
77 h_spacing: 8.0,
78 v_spacing: 8.0,
79 cell_padding: 0.0,
80 row_background: None,
81 divider_width: 0.0,
82 divider_color: None,
83 }
84 }
85 pub fn column(mut self, c: TableColumn) -> Self { self.columns.push(c); self }
87 pub fn columns(mut self, cs: Vec<TableColumn>) -> Self { self.columns.extend(cs); self }
89 pub fn row(mut self, cells: Vec<BoxedWidget>) -> Self {
91 self.row_lens.push(cells.len());
92 self.cells.extend(cells);
93 self
94 }
95 pub fn row_builder(mut self, count: usize, builder: impl Fn(usize) -> Vec<BoxedWidget>) -> Self {
100 for i in 0..count {
101 self = self.row(builder(i));
102 }
103 self
104 }
105 pub fn spacing(mut self, h: f32, v: f32) -> Self {
107 self.h_spacing = h.max(0.0);
108 self.v_spacing = v.max(0.0);
109 self
110 }
111 pub fn cell_padding(mut self, p: f32) -> Self { self.cell_padding = p.max(0.0); self }
113 pub fn row_background(mut self, c: Color) -> Self { self.row_background = Some(c); self }
115 pub fn divider(mut self, width: f32) -> Self { self.divider_width = width.max(0.0); self }
118 pub fn divider_color(mut self, c: Color) -> Self { self.divider_color = Some(c); self }
120
121 fn row_range(&self, r: usize) -> std::ops::Range<usize> {
123 let start: usize = self.row_lens[..r].iter().sum();
124 start..start + self.row_lens[r]
125 }
126
127 fn cell(&self, row: usize, col: usize) -> Option<&BoxedWidget> {
129 let range = self.row_range(row);
130 if col < self.row_lens[row] { self.cells.get(range.start + col) } else { None }
131 }
132
133 fn resolve_columns(&self, ctx: &LayoutCtx, total_w: f32) -> Vec<f32> {
139 let n = self.columns.len();
140 let gaps = self.h_spacing * n.saturating_sub(1) as f32;
141 let pad2 = self.cell_padding * 2.0;
142 let bounded = total_w.is_finite();
143 let measure_w = if bounded { total_w } else { f32::MAX };
144
145 let intrinsic = |i: usize| -> f32 {
147 let mut w = 0.0f32;
148 for row in 0..self.row_lens.len() {
149 if let Some(cell) = self.cell(row, i) {
150 let s = cell.layout(&ctx.with_constraints(
151 Constraints::loose(measure_w, f32::INFINITY),
152 ));
153 w = w.max(s.width);
154 }
155 }
156 w + pad2
157 };
158
159 let mut widths = vec![0.0f32; n];
160 let mut flex_sum = 0.0f32;
161 let mut used = 0.0f32;
162 for (i, col) in self.columns.iter().enumerate() {
163 match col.sizing {
164 ColumnSizing::Fixed(px) => { widths[i] = px; used += px; }
165 ColumnSizing::Auto => { widths[i] = intrinsic(i); used += widths[i]; }
166 ColumnSizing::Flex(_) if !bounded => {
167 widths[i] = intrinsic(i);
169 used += widths[i];
170 }
171 ColumnSizing::Flex(f) => flex_sum += f,
172 }
173 }
174 if bounded && flex_sum > 0.0 {
175 let leftover = (total_w - used - gaps).max(0.0);
176 for (i, col) in self.columns.iter().enumerate() {
177 if let ColumnSizing::Flex(f) = col.sizing {
178 widths[i] = leftover * (f / flex_sum);
179 }
180 }
181 }
182 widths
183 }
184
185 fn row_heights(&self, ctx: &LayoutCtx, widths: &[f32]) -> Vec<f32> {
188 let pad2 = self.cell_padding * 2.0;
189 (0..self.row_lens.len())
190 .map(|row| {
191 let mut h = 0.0f32;
192 for (col, w) in widths.iter().enumerate() {
193 if let Some(cell) = self.cell(row, col) {
194 let s = cell.layout(&ctx.with_constraints(
195 Constraints::loose((w - pad2).max(0.0), f32::INFINITY),
196 ));
197 h = h.max(s.height);
198 }
199 }
200 h + pad2
201 })
202 .collect()
203 }
204
205 fn content_size(&self, ctx: &LayoutCtx, total_w: f32) -> Size {
207 let widths = self.resolve_columns(ctx, total_w);
208 let heights = self.row_heights(ctx, &widths);
209 let gaps_w = self.h_spacing * self.columns.len().saturating_sub(1) as f32;
210 let gaps_h = self.v_spacing * heights.len().saturating_sub(1) as f32;
211 Size {
212 width: widths.iter().sum::<f32>() + gaps_w,
213 height: heights.iter().sum::<f32>() + gaps_h,
214 }
215 }
216
217 fn has_flex(&self) -> bool {
220 self.columns.iter().any(|c| matches!(c.sizing, ColumnSizing::Flex(_)))
221 }
222}
223
224impl Default for Table {
225 fn default() -> Self { Self::new() }
226}
227
228impl Widget for Table {
229 fn children(&self) -> Children<'_> { Children::Many(&self.cells) }
230
231 fn layout(&self, ctx: &LayoutCtx) -> Size {
232 let w = avail_w(ctx.constraints);
233 let content = self.content_size(ctx, w);
234 let width = if self.has_flex() && w.is_finite() { w } else { content.width };
235 ctx.constraints.constrain(Size { width, height: content.height })
236 }
237
238 fn paint(&self, ctx: &mut PaintCtx) {
239 let divider = self
241 .divider_color
242 .unwrap_or_else(|| ctx.tc(ctx.theme.colors.outline));
243
244 let r = ctx.rect;
245 let (widths, heights) = {
247 let lctx = ctx.layout_ctx(Constraints::loose(r.size.width, f32::INFINITY));
248 let widths = self.resolve_columns(&lctx, r.size.width);
249 let heights = self.row_heights(&lctx, &widths);
250 (widths, heights)
251 };
252
253 let pad = self.cell_padding;
254 let mut y = r.origin.y;
255 for (row, row_h) in heights.iter().enumerate() {
256 if row % 2 == 1 {
258 if let Some(bg) = self.row_background {
259 ctx.fill_rect(
260 Rect {
261 origin: Point { x: r.origin.x, y },
262 size: Size { width: r.size.width, height: *row_h },
263 },
264 bg,
265 );
266 }
267 }
268
269 let mut x = r.origin.x;
270 for (col, w) in widths.iter().enumerate() {
271 if let Some(cell) = self.cell(row, col) {
272 let content_w = (w - pad * 2.0).max(0.0);
273 let s = cell.layout(&ctx.layout_ctx(
274 Constraints::loose(content_w, f32::INFINITY),
275 ));
276 let rect = Rect {
278 origin: Point { x: x + pad, y: y + pad },
279 size: Size { width: s.width.min(content_w), height: s.height },
280 };
281 cell.paint(&mut ctx.child(rect));
282 }
283 x += w + self.h_spacing;
284 }
285
286 y += row_h;
287 if row + 1 < heights.len() {
289 if self.divider_width > 0.0 {
290 let dy = y + ((self.v_spacing - self.divider_width) / 2.0).max(0.0);
291 ctx.fill_rect(
292 Rect {
293 origin: Point { x: r.origin.x, y: dy },
294 size: Size { width: r.size.width, height: self.divider_width },
295 },
296 divider,
297 );
298 }
299 y += self.v_spacing;
300 }
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 struct FixedCell(f32, f32);
311 impl Widget for FixedCell {
312 fn layout(&self, _ctx: &LayoutCtx) -> Size {
313 Size { width: self.0, height: self.1 }
314 }
315 fn paint(&self, _ctx: &mut PaintCtx) {}
316 }
317
318 fn boxed(w: f32, h: f32) -> BoxedWidget { Box::new(FixedCell(w, h)) }
319
320 fn test_env() -> (rosace_render::FontCache, rosace_theme::ThemeData) {
321 (rosace_render::FontCache::embedded(), rosace_theme::built_in::dark_theme())
322 }
323
324 #[test]
325 fn fixed_auto_and_flex_columns_resolve_in_a_300px_width() {
326 let table = Table::new()
329 .column(TableColumn::fixed(100.0))
330 .column(TableColumn::auto())
331 .column(TableColumn::flex(1.0))
332 .spacing(10.0, 0.0)
333 .row(vec![boxed(40.0, 20.0), boxed(50.0, 30.0), boxed(10.0, 10.0)])
334 .row(vec![boxed(80.0, 15.0), boxed(30.0, 12.0), boxed(10.0, 10.0)]);
335 let (font, theme) = test_env();
336 let ctx = LayoutCtx::new(Constraints::loose(300.0, 1000.0), &font, &theme);
337 let widths = table.resolve_columns(&ctx, 300.0);
338 assert_eq!(widths, vec![100.0, 50.0, 130.0]);
339 assert_eq!(table.layout(&ctx).width, 300.0);
341 }
342
343 #[test]
344 fn two_flex_columns_share_leftover_by_factor() {
345 let table = Table::new()
347 .column(TableColumn::fixed(60.0))
348 .column(TableColumn::flex(1.0))
349 .column(TableColumn::flex(3.0))
350 .spacing(0.0, 0.0)
351 .row(vec![boxed(10.0, 10.0), boxed(10.0, 10.0), boxed(10.0, 10.0)]);
352 let (font, theme) = test_env();
353 let ctx = LayoutCtx::new(Constraints::loose(300.0, 1000.0), &font, &theme);
354 assert_eq!(table.resolve_columns(&ctx, 300.0), vec![60.0, 60.0, 180.0]);
355 }
356
357 #[test]
358 fn row_height_is_the_tallest_cell_of_each_row() {
359 let table = Table::new()
360 .column(TableColumn::fixed(100.0))
361 .column(TableColumn::fixed(100.0))
362 .spacing(0.0, 10.0)
363 .row(vec![boxed(40.0, 20.0), boxed(50.0, 44.0)])
364 .row(vec![boxed(40.0, 16.0), boxed(50.0, 8.0)]);
365 let (font, theme) = test_env();
366 let ctx = LayoutCtx::new(Constraints::loose(300.0, 1000.0), &font, &theme);
367 let heights = table.row_heights(&ctx, &[100.0, 100.0]);
368 assert_eq!(heights, vec![44.0, 16.0]);
369 assert_eq!(table.layout(&ctx).height, 70.0);
371 }
372
373 #[test]
374 fn cell_padding_grows_auto_columns_and_row_heights() {
375 let table = Table::new()
376 .column(TableColumn::auto())
377 .cell_padding(6.0)
378 .row(vec![boxed(50.0, 20.0)]);
379 let (font, theme) = test_env();
380 let ctx = LayoutCtx::new(Constraints::loose(300.0, 1000.0), &font, &theme);
381 assert_eq!(table.resolve_columns(&ctx, 300.0), vec![62.0]);
382 assert_eq!(table.layout(&ctx).height, 32.0);
383 assert_eq!(table.layout(&ctx).width, 62.0);
385 }
386}