1use crate::geometry::{Constraints, Rect, Size};
8use crate::layoutable::{
9 clip_to_fixed_height, finish_fit, measure_at_width, push_warning, resolve_auto_size, resolve_bound, shrink_and_bound_height,
10 wrap_children, LayoutCtx, LayoutResult, Layoutable,
11};
12use crate::render_node::{align_offset, RenderNode, StructRole};
13use crate::warnings::{LayoutWarning, LayoutWarningKind};
14use lightweight_pdf_core::{Color, ColumnWidth, Common, Element, Table, TableCell, TableColumn};
15
16const EPS: f32 = 0.01;
17
18fn resolve_column_widths(columns: &[TableColumn], available_width: f32) -> Vec<f32> {
23 let fixed_total: f32 = columns
24 .iter()
25 .filter_map(|c| match c.width {
26 ColumnWidth::Fixed(w) => Some(w),
27 ColumnWidth::Flex(_) => None,
28 })
29 .sum();
30 let flex_sum: f32 = columns
31 .iter()
32 .filter_map(|c| match c.width {
33 ColumnWidth::Flex(w) => Some(w),
34 ColumnWidth::Fixed(_) => None,
35 })
36 .sum();
37 let leftover = (available_width - fixed_total).max(0.0);
38
39 let mut widths = Vec::with_capacity(columns.len());
40 let mut last_flex_idx = None;
41 for (i, c) in columns.iter().enumerate() {
42 match c.width {
43 ColumnWidth::Fixed(w) => widths.push(w),
44 ColumnWidth::Flex(weight) => {
45 widths.push(if flex_sum > 0.0 { leftover * (weight / flex_sum) } else { 0.0 });
46 last_flex_idx = Some(i);
47 }
48 }
49 }
50 if let Some(i) = last_flex_idx {
51 let sum: f32 = widths.iter().sum();
52 widths[i] += available_width - sum;
53 }
54 widths
55}
56
57fn measure_row_height(ctx: &LayoutCtx, cells: &[TableCell], col_widths: &[f32], cell_padding: f32) -> f32 {
58 let mut max_h = 0.0f32;
59 let mut col_idx = 0;
60 for cell in cells {
61 if col_idx >= col_widths.len() {
62 break;
63 }
64 let span = cell.colspan.max(1);
65 let end_idx = (col_idx + span).min(col_widths.len());
66 let total_w: f32 = col_widths[col_idx..end_idx].iter().sum();
67 let padding = cell.padding.unwrap_or(cell_padding);
68 let inner_w = (total_w - 2.0 * padding).max(0.0);
69 let h = measure_at_width(ctx, &cell.element, inner_w).height + 2.0 * padding;
70 max_h = max_h.max(h);
71 col_idx = end_idx;
72 }
73 max_h
74}
75
76fn header_row_height(ctx: &LayoutCtx, table: &Table, col_widths: &[f32]) -> f32 {
79 table
80 .header
81 .as_ref()
82 .map(|h| measure_row_height(ctx, h, col_widths, table.cell_padding))
83 .unwrap_or(0.0)
84}
85
86pub fn table_min_unit(ctx: &LayoutCtx, table: &Table, width: f32) -> f32 {
91 let col_widths = resolve_column_widths(&table.columns, (width - 2.0 * table.common.padding).max(0.0));
92 let first_row_h = table
93 .rows
94 .first()
95 .map(|r| measure_row_height(ctx, r, &col_widths, table.cell_padding))
96 .unwrap_or(0.0);
97 header_row_height(ctx, table, &col_widths) + first_row_h
98}
99
100fn layout_row_cells(
101 ctx: &LayoutCtx,
102 table: &Table,
103 cells: &[TableCell],
104 col_widths: &[f32],
105 row_area: Rect,
106 warnings: &mut Vec<LayoutWarning>,
107 page: usize,
108) -> Vec<RenderNode> {
109 let cell_padding = table.cell_padding;
110 let mut nodes = Vec::with_capacity(cells.len());
111 let mut cursor_x = row_area.x;
112 let mut col_idx = 0;
113 for cell in cells {
114 if col_idx >= table.columns.len() {
115 push_warning(
116 warnings,
117 LayoutWarningKind::TableRowOverflow,
118 page,
119 format!(
120 "table row has {} cell(s), table has {} column(s) — extra cells dropped",
121 cells.len(),
122 table.columns.len()
123 ),
124 );
125 break;
126 }
127 let span = cell.colspan.max(1);
128 let end_idx = (col_idx + span).min(table.columns.len());
129 let total_w: f32 = col_widths[col_idx..end_idx].iter().sum();
130 let col_align = cell.align.unwrap_or(table.columns[col_idx].align);
131 let padding = cell.padding.unwrap_or(cell_padding);
132
133 let inner_w = (total_w - 2.0 * padding).max(0.0);
134 let content_h = (row_area.height - 2.0 * padding).max(0.0);
135 let cell_size = measure_at_width(ctx, &cell.element, inner_w);
136 let box_width = cell_size.width.min(inner_w).max(0.0);
137 let x_offset = align_offset(col_align, inner_w, box_width);
138 let cell_box = Rect {
139 x: cursor_x,
140 y: row_area.y,
141 width: total_w,
142 height: row_area.height,
143 };
144 let mut cell_children = Vec::new();
145 if cell.background.is_some() || cell.border.is_some() {
146 cell_children.push(RenderNode::Rect {
147 area: cell_box,
148 background: cell.background,
149 border: cell.border,
150 corner_radius: 0.0,
151 });
152 }
153 let cell_area = Rect {
154 x: cursor_x + padding + x_offset,
155 y: row_area.y + padding,
156 width: box_width,
157 height: content_h,
158 };
159 match cell.element.layout(ctx, cell_area, warnings, page) {
160 LayoutResult::Fit(node) => cell_children.push(node),
161 LayoutResult::Split { current, .. } => {
162 cell_children.push(current);
163 }
164 }
165 nodes.push(RenderNode::tagged(
169 StructRole::TableHeaderCell,
170 RenderNode::Group {
171 area: cell_box,
172 clip: false,
173 background: None,
174 border: None,
175 corner_radius: 0.0,
176 children: cell_children,
177 },
178 ));
179 cursor_x += total_w;
180 col_idx = end_idx;
181 }
182 nodes
183}
184
185struct RowRenderParams<'a> {
189 cells: &'a [TableCell],
190 y: f32,
191 row_height: f32,
192 background: Option<Color>,
193}
194
195fn render_row(
196 ctx: &LayoutCtx,
197 table: &Table,
198 col_widths: &[f32],
199 inner: &Rect,
200 warnings: &mut Vec<LayoutWarning>,
201 page: usize,
202 row: RowRenderParams,
203) -> RenderNode {
204 let row_area = Rect {
205 x: inner.x,
206 y: inner.y + row.y,
207 width: inner.width,
208 height: row.row_height,
209 };
210 let nodes = layout_row_cells(ctx, table, row.cells, col_widths, row_area, warnings, page);
211 RenderNode::tagged(
212 StructRole::TableRow,
213 RenderNode::Group {
214 area: row_area,
215 clip: true,
216 background: row.background,
217 border: None,
218 corner_radius: 0.0,
219 children: nodes,
220 },
221 )
222}
223
224struct CellPlacement<'a> {
239 row: usize,
240 col_start: usize,
241 col_end: usize,
242 cell: &'a TableCell,
243}
244
245fn plan_grid<'a>(
252 rows: &'a [Vec<TableCell>],
253 num_columns: usize,
254 warnings: &mut Vec<LayoutWarning>,
255 page: usize,
256) -> (Vec<CellPlacement<'a>>, Vec<bool>) {
257 let mut blocked_until = vec![0usize; num_columns];
260 let mut placements = Vec::new();
261 let mut is_continuation = Vec::with_capacity(rows.len());
262
263 for (row_idx, row) in rows.iter().enumerate() {
264 is_continuation.push((0..num_columns).any(|c| blocked_until[c] > row_idx));
265
266 let mut col_idx = 0;
267 for cell in row {
268 while col_idx < num_columns && blocked_until[col_idx] > row_idx {
269 col_idx += 1;
270 }
271 if col_idx >= num_columns {
272 push_warning(
273 warnings,
274 LayoutWarningKind::TableRowOverflow,
275 page,
276 format!("table row {row_idx} has more cells than the table has free column(s) after rowspan — extra cells dropped"),
277 );
278 break;
279 }
280 let col_end = (col_idx + cell.colspan.max(1)).min(num_columns);
281 let rowspan = cell.rowspan.max(1);
282 if rowspan > 1 {
283 for slot in blocked_until[col_idx..col_end].iter_mut() {
284 *slot = row_idx + rowspan;
285 }
286 }
287 placements.push(CellPlacement {
288 row: row_idx,
289 col_start: col_idx,
290 col_end,
291 cell,
292 });
293 col_idx = col_end;
294 }
295 }
296 (placements, is_continuation)
297}
298
299fn natural_row_heights(ctx: &LayoutCtx, placements: &[CellPlacement], num_rows: usize, col_widths: &[f32], cell_padding: f32) -> Vec<f32> {
303 let mut heights = vec![0.0f32; num_rows];
304 for p in placements {
305 if p.cell.rowspan.max(1) > 1 {
306 continue;
307 }
308 let padding = p.cell.padding.unwrap_or(cell_padding);
309 let inner_w = (col_widths[p.col_start..p.col_end].iter().sum::<f32>() - 2.0 * padding).max(0.0);
310 let h = measure_at_width(ctx, &p.cell.element, inner_w).height + 2.0 * padding;
311 heights[p.row] = heights[p.row].max(h);
312 }
313 heights
314}
315
316fn apply_rowspan_deficits(ctx: &LayoutCtx, placements: &[CellPlacement], heights: &mut [f32], col_widths: &[f32], cell_padding: f32) {
321 for p in placements {
322 let span = p.cell.rowspan.max(1);
323 if span <= 1 {
324 continue;
325 }
326 let padding = p.cell.padding.unwrap_or(cell_padding);
327 let inner_w = (col_widths[p.col_start..p.col_end].iter().sum::<f32>() - 2.0 * padding).max(0.0);
328 let needed = measure_at_width(ctx, &p.cell.element, inner_w).height + 2.0 * padding;
329 let end_row = (p.row + span).min(heights.len());
330 let available: f32 = heights[p.row..end_row].iter().sum();
331 if needed > available {
332 if let Some(last) = heights[p.row..end_row].last_mut() {
333 *last += needed - available;
334 }
335 }
336 }
337}
338
339fn atomic_blocks(is_continuation: &[bool]) -> Vec<std::ops::Range<usize>> {
343 let mut blocks = Vec::new();
344 let mut i = 0;
345 while i < is_continuation.len() {
346 let start = i;
347 i += 1;
348 while i < is_continuation.len() && is_continuation[i] {
349 i += 1;
350 }
351 blocks.push(start..i);
352 }
353 blocks
354}
355
356fn shrink_row_heights_to_fit(natural: &[f32], budget: f32) -> Vec<f32> {
361 let mut remaining_budget = budget.max(0.0);
362 natural
363 .iter()
364 .map(|&h| {
365 let take = h.min(remaining_budget);
366 remaining_budget -= take;
367 take
368 })
369 .collect()
370}
371
372struct TableRenderCtx<'a> {
376 ctx: &'a LayoutCtx<'a>,
377 table: &'a Table,
378 col_widths: &'a [f32],
379 placements: &'a [CellPlacement<'a>],
380 page: usize,
381}
382
383fn render_cells_starting_at(
389 trc: &TableRenderCtx,
390 row_area: &Rect,
391 warnings: &mut Vec<LayoutWarning>,
392 row_idx: usize,
393 local_row_idx: usize,
394 block_heights: &[f32],
395) -> Vec<RenderNode> {
396 let cell_padding = trc.table.cell_padding;
397 let mut nodes = Vec::new();
398 for p in trc.placements.iter().filter(|p| p.row == row_idx) {
399 let span = p.cell.rowspan.max(1);
400 let local_end = (local_row_idx + span).min(block_heights.len());
401 let cell_h: f32 = block_heights[local_row_idx..local_end].iter().sum();
402 let total_w: f32 = trc.col_widths[p.col_start..p.col_end].iter().sum();
403 let col_align = p.cell.align.unwrap_or(trc.table.columns[p.col_start].align);
404 let cursor_x = row_area.x + trc.col_widths[..p.col_start].iter().sum::<f32>();
405 let padding = p.cell.padding.unwrap_or(cell_padding);
406
407 let cell_box = Rect {
408 x: cursor_x,
409 y: row_area.y,
410 width: total_w,
411 height: cell_h,
412 };
413 let mut cell_children = Vec::new();
418 if p.cell.background.is_some() || p.cell.border.is_some() {
419 cell_children.push(RenderNode::Rect {
420 area: cell_box,
421 background: p.cell.background,
422 border: p.cell.border,
423 corner_radius: 0.0,
424 });
425 }
426
427 let inner_w = (total_w - 2.0 * padding).max(0.0);
428 let content_h = (cell_h - 2.0 * padding).max(0.0);
429 let cell_size = measure_at_width(trc.ctx, &p.cell.element, inner_w);
430 let box_width = cell_size.width.min(inner_w).max(0.0);
431 let x_offset = align_offset(col_align, inner_w, box_width);
432 let cell_area = Rect {
433 x: cursor_x + padding + x_offset,
434 y: row_area.y + padding,
435 width: box_width,
436 height: content_h,
437 };
438 match p.cell.element.layout(trc.ctx, cell_area, warnings, trc.page) {
439 LayoutResult::Fit(node) => cell_children.push(node),
440 LayoutResult::Split { current, .. } => cell_children.push(current),
441 }
442 nodes.push(RenderNode::tagged(
445 StructRole::TableCell,
446 RenderNode::Group {
447 area: cell_box,
448 clip: false,
449 background: None,
450 border: None,
451 corner_radius: 0.0,
452 children: cell_children,
453 },
454 ));
455 }
456 nodes
457}
458
459fn render_block(
470 trc: &TableRenderCtx,
471 inner: &Rect,
472 warnings: &mut Vec<LayoutWarning>,
473 block: std::ops::Range<usize>,
474 block_heights: &[f32], block_top_y: f32,
476 row_backgrounds: &[Option<Color>], ) -> RenderNode {
478 if block.len() == 1 {
479 let row_idx = block.start;
480 let row_area = Rect {
481 x: inner.x,
482 y: inner.y + block_top_y,
483 width: inner.width,
484 height: block_heights[0],
485 };
486 let nodes = render_cells_starting_at(trc, &row_area, warnings, row_idx, 0, block_heights);
487 return RenderNode::tagged(
488 StructRole::TableRow,
489 RenderNode::Group {
490 area: row_area,
491 clip: true,
492 background: row_backgrounds[row_idx],
493 border: None,
494 corner_radius: 0.0,
495 children: nodes,
496 },
497 );
498 }
499
500 let block_area = Rect {
501 x: inner.x,
502 y: inner.y + block_top_y,
503 width: inner.width,
504 height: block_heights.iter().sum(),
505 };
506 let mut children = Vec::new();
507 let mut cursor_y = block_top_y;
508 for (local_idx, row_idx) in block.clone().enumerate() {
509 let row_h = block_heights[local_idx];
510 let row_area = Rect {
511 x: inner.x,
512 y: inner.y + cursor_y,
513 width: inner.width,
514 height: row_h,
515 };
516 let mut row_children = Vec::new();
521 if let Some(bg) = row_backgrounds[row_idx] {
522 row_children.push(RenderNode::Rect {
523 area: row_area,
524 background: Some(bg),
525 border: None,
526 corner_radius: 0.0,
527 });
528 }
529 row_children.extend(render_cells_starting_at(
530 trc,
531 &row_area,
532 warnings,
533 row_idx,
534 local_idx,
535 block_heights,
536 ));
537 children.push(RenderNode::tagged(
538 StructRole::TableRow,
539 RenderNode::Group {
540 area: row_area,
541 clip: false,
542 background: None,
543 border: None,
544 corner_radius: 0.0,
545 children: row_children,
546 },
547 ));
548 cursor_y += row_h;
549 }
550 RenderNode::Group {
551 area: block_area,
552 clip: true,
553 background: None,
554 border: None,
555 corner_radius: 0.0,
556 children,
557 }
558}
559
560impl Layoutable for Table {
561 fn measure(&self, ctx: &LayoutCtx, constraints: Constraints) -> Size {
562 let (width, inner_width) = resolve_bound(self.common.width, constraints.max_width, self.common.padding);
563 let col_widths = resolve_column_widths(&self.columns, inner_width);
564 let mut total = header_row_height(ctx, self, &col_widths);
565 for row in &self.rows {
566 total += measure_row_height(ctx, row, &col_widths, self.cell_padding);
567 }
568 Size {
569 width,
570 height: resolve_auto_size(self.common.height, total, self.common.padding),
571 }
572 }
573
574 fn layout(&self, ctx: &LayoutCtx, area: Rect, warnings: &mut Vec<LayoutWarning>, page: usize) -> LayoutResult {
575 let (inner, bound_height) = shrink_and_bound_height(area, self.common.height, self.common.padding);
576 let col_widths = resolve_column_widths(&self.columns, inner.width);
577 let header_height = header_row_height(ctx, self, &col_widths);
578
579 let mut rendered = Vec::new();
580 let mut cursor_y = 0.0f32;
581
582 if let Some(header) = &self.header {
583 rendered.push(render_row(
584 ctx,
585 self,
586 &col_widths,
587 &inner,
588 warnings,
589 page,
590 RowRenderParams {
591 cells: header,
592 y: cursor_y,
593 row_height: header_height,
594 background: None,
595 },
596 ));
597 cursor_y += header_height;
598 }
599
600 let (placements, is_continuation) = plan_grid(&self.rows, self.columns.len(), warnings, page);
601 let mut row_heights = natural_row_heights(ctx, &placements, self.rows.len(), &col_widths, self.cell_padding);
602 apply_rowspan_deficits(ctx, &placements, &mut row_heights, &col_widths, self.cell_padding);
603 let row_backgrounds: Vec<Option<Color>> = (0..self.rows.len())
604 .map(|i| self.striped.filter(|_| (self.row_offset + i) % 2 == 1))
605 .collect();
606 let trc = TableRenderCtx {
607 ctx,
608 table: self,
609 col_widths: &col_widths,
610 placements: &placements,
611 page,
612 };
613
614 for block in atomic_blocks(&is_continuation) {
615 let block_height: f32 = row_heights[block.clone()].iter().sum();
616 let remaining = (bound_height - cursor_y).max(0.0);
617
618 if block_height <= remaining + EPS {
619 rendered.push(render_block(
620 &trc,
621 &inner,
622 warnings,
623 block.clone(),
624 &row_heights[block.clone()],
625 cursor_y,
626 &row_backgrounds,
627 ));
628 cursor_y += block_height;
629 continue;
630 }
631
632 if cursor_y <= header_height + EPS {
633 let forced_heights = shrink_row_heights_to_fit(&row_heights[block.clone()], remaining);
639 rendered.push(render_block(
640 &trc,
641 &inner,
642 warnings,
643 block.clone(),
644 &forced_heights,
645 cursor_y,
646 &row_backgrounds,
647 ));
648 let start = self.row_offset + block.start;
649 let end = self.row_offset + block.end - 1;
650 let hint = if start == end {
651 format!("Table row {start} larger than one page")
652 } else {
653 format!("Table rows {start}-{end} (rowspan) larger than one page")
654 };
655 push_warning(warnings, LayoutWarningKind::ForcedPageBreak, page, hint);
656 cursor_y = bound_height;
657 continue;
658 }
659
660 if let Some(fixed_height) = self.common.height {
665 let overflow_hint = (!self.rows[block.start..].is_empty()).then_some("Table content exceeds its fixed height");
666 return clip_to_fixed_height(area, fixed_height, &self.common, rendered, warnings, page, overflow_hint);
667 }
668
669 let remainder = Table {
670 columns: self.columns.clone(),
671 header: self.header.clone(),
672 rows: self.rows[block.start..].to_vec(),
673 striped: self.striped,
674 cell_padding: self.cell_padding,
675 row_offset: self.row_offset + block.start,
676 common: Common {
677 height: None,
678 ..self.common
679 },
680 };
681 let current = wrap_children(area, cursor_y, &self.common, rendered);
682 return LayoutResult::Split {
683 current,
684 remainder: Element::Table(remainder),
685 };
686 }
687
688 finish_fit(&self.common, area, cursor_y, rendered)
689 }
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695 use crate::warnings::LayoutWarningKind;
696 use lightweight_pdf_core::{Align, Element, Text as TextEl};
697
698 struct FixedMetrics;
699 impl crate::font_resolver::FontMetrics for FixedMetrics {
700 fn advance(&self, ch: char) -> f32 {
701 if ch == ' ' {
702 300.0
703 } else {
704 600.0
705 }
706 }
707 fn ascent(&self) -> f32 {
708 800.0
709 }
710 fn descent(&self) -> f32 {
711 -200.0
712 }
713 }
714 struct FixedResolver;
715 impl crate::font_resolver::FontResolver for FixedResolver {
716 fn metrics(&self, _key: lightweight_pdf_core::FontKey) -> &dyn crate::font_resolver::FontMetrics {
717 &FixedMetrics
718 }
719 }
720 fn ctx() -> LayoutCtx<'static> {
721 LayoutCtx::new(&FixedResolver)
722 }
723
724 fn row(cells: &[&str]) -> Vec<Element> {
725 cells.iter().map(|c| Element::Text(TextEl::new(*c))).collect()
726 }
727
728 #[test]
729 fn column_widths_sum_exactly_to_available_width() {
730 let columns = vec![
731 TableColumn::flex(1.0),
732 TableColumn::fixed(37.3),
733 TableColumn::flex(2.0),
734 TableColumn::fixed(19.9),
735 ];
736 let widths = resolve_column_widths(&columns, 400.0);
737 let sum: f32 = widths.iter().sum();
738 assert!((sum - 400.0).abs() < 1e-3, "widths must sum exactly to available width, got {sum}");
739 assert_eq!(widths[1], 37.3);
740 assert_eq!(widths[3], 19.9);
741 }
742
743 #[test]
744 fn header_repeats_and_all_rows_survive_a_page_split() {
745 let table = Table::new()
746 .columns([TableColumn::flex(1.0)])
747 .header(["Beschreibung"])
748 .rows((0..20).map(|i| row(&[Box::leak(format!("Zeile {i}").into_boxed_str())])));
749 let c = ctx();
750 let mut warnings = Vec::new();
751 let area = Rect {
752 x: 0.0,
753 y: 0.0,
754 width: 200.0,
755 height: 60.0, };
757 let mut pages = Vec::new();
758 let mut current = Element::Table(table);
759 loop {
760 match current.layout(&c, area, &mut warnings, pages.len() + 1) {
761 LayoutResult::Fit(node) => {
762 pages.push(node);
763 break;
764 }
765 LayoutResult::Split { current: node, remainder } => {
766 pages.push(node);
767 current = remainder;
768 }
769 }
770 if pages.len() > 100 {
771 panic!("pagination did not terminate");
772 }
773 }
774 assert!(pages.len() > 1, "expected the table to span multiple pages");
775
776 let mut seen_rows = Vec::new();
780 for page in &pages {
781 let RenderNode::Group { children, .. } = page.untagged() else {
782 panic!("expected a Group");
783 };
784 assert!(!children.is_empty(), "every page must render at least the header");
785 for row_node in children {
786 let RenderNode::Group { children: cells, .. } = row_node.untagged() else {
787 panic!("expected row Group");
788 };
789 let RenderNode::Group { children: cell_inner, .. } = cells[0].untagged() else {
790 panic!("expected cell Group");
791 };
792 let RenderNode::Group { children: text_wrap, .. } = cell_inner[0].untagged() else {
793 panic!("expected clipped text wrapper");
794 };
795 let RenderNode::TextLines { lines, .. } = &text_wrap[0] else {
796 panic!("expected TextLines");
797 };
798 seen_rows.push(lines.join(" "));
799 }
800 }
801 let header_count = seen_rows.iter().filter(|s| *s == "Beschreibung").count();
802 assert_eq!(header_count, pages.len(), "header must repeat on every page exactly once");
803 let data_rows: Vec<_> = seen_rows.iter().filter(|s| *s != "Beschreibung").collect();
804 assert_eq!(data_rows.len(), 20, "no row may be lost or duplicated across the split");
805 for (i, row) in data_rows.iter().enumerate() {
806 assert_eq!(*row, &format!("Zeile {i}"), "rows must stay in order");
807 }
808 }
809
810 #[test]
811 fn cell_hard_breaks_a_token_wider_than_the_column() {
812 let table = Table::new().columns([TableColumn::fixed(30.0)]).rows([row(&["ABCDEFGHIJ"])]); let c = ctx();
814 let mut warnings = Vec::new();
815 let area = Rect {
816 x: 0.0,
817 y: 0.0,
818 width: 30.0,
819 height: 200.0,
820 };
821 let LayoutResult::Fit(node) = Element::Table(table).layout(&c, area, &mut warnings, 1) else {
822 panic!("expected Fit");
823 };
824 let RenderNode::Group { children, .. } = node.untagged() else {
825 panic!("expected Group");
826 };
827 let RenderNode::Group { children: cells, .. } = children[0].untagged() else {
828 panic!("expected row group");
829 };
830 let RenderNode::Group { children: cell_inner, .. } = cells[0].untagged() else {
831 panic!("expected cell group");
832 };
833 let RenderNode::Group { children: text_wrap, .. } = cell_inner[0].untagged() else {
834 panic!("expected clipped text wrapper");
835 };
836 let RenderNode::TextLines { lines, .. } = &text_wrap[0] else {
837 panic!("expected TextLines");
838 };
839 assert!(lines.len() > 1, "a token wider than the column must hard-break onto multiple lines");
840 }
841
842 #[test]
843 fn row_height_grows_with_tallest_cell_without_moving_other_rows() {
844 let table = Table::new().columns([TableColumn::fixed(30.0), TableColumn::fixed(30.0)]).rows([
845 row(&["kurz", "kurz"]),
846 row(&["ein sehr sehr sehr sehr langer Zellinhalt der umbricht", "kurz"]),
847 row(&["kurz", "kurz"]),
848 ]);
849 let c = ctx();
850 let mut warnings = Vec::new();
851 let area = Rect {
852 x: 0.0,
853 y: 0.0,
854 width: 60.0,
855 height: 400.0,
856 };
857 let LayoutResult::Fit(node) = Element::Table(table).layout(&c, area, &mut warnings, 1) else {
858 panic!("expected Fit");
859 };
860 let RenderNode::Group { children: rows, .. } = node.untagged() else {
861 panic!("expected Group");
862 };
863 assert_eq!(rows.len(), 3);
864 let heights: Vec<f32> = rows
865 .iter()
866 .map(|r| match r.untagged() {
867 RenderNode::Group { area, .. } => area.height,
868 _ => panic!("expected Group"),
869 })
870 .collect();
871 assert!(heights[1] > heights[0], "the row with more content must be taller");
872 assert_eq!(heights[0], heights[2], "unrelated rows keep their own (equal) height");
873
874 let ys: Vec<f32> = rows
877 .iter()
878 .map(|r| match r.untagged() {
879 RenderNode::Group { area, .. } => area.y,
880 _ => unreachable!(),
881 })
882 .collect();
883 assert!(ys[1] >= ys[0] + heights[0] - EPS);
884 assert!(ys[2] >= ys[1] + heights[1] - EPS);
885 }
886
887 #[test]
888 fn striped_alternates_and_survives_a_split() {
889 let table = Table::new()
890 .columns([TableColumn::flex(1.0)])
891 .header(["H"])
892 .striped(Color::rgb(240, 240, 240))
893 .rows((0..6).map(|i| row(&[Box::leak(format!("R{i}").into_boxed_str())])));
894 let c = ctx();
895 let mut warnings = Vec::new();
896 let area = Rect {
899 x: 0.0,
900 y: 0.0,
901 width: 100.0,
902 height: 3.5 * 22.4,
903 };
904 let LayoutResult::Split { remainder, .. } = Element::Table(table).layout(&c, area, &mut warnings, 1) else {
905 panic!("expected a Split");
906 };
907 let Element::Table(remainder_table) = remainder else {
908 panic!("expected Table remainder");
909 };
910 assert_eq!(remainder_table.row_offset, 2);
914 }
915
916 #[test]
917 fn oversized_row_forces_its_own_page() {
918 let table = Table::new()
919 .columns([TableColumn::flex(1.0)])
920 .rows([row(&["normal"]), row(&["a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np"])]);
921 let c = ctx();
922 let mut warnings = Vec::new();
923 let area = Rect {
924 x: 0.0,
925 y: 0.0,
926 width: 100.0,
927 height: 100.0,
928 };
929 let mut pages = 0;
930 let mut current = Element::Table(table);
931 loop {
932 match current.layout(&c, area, &mut warnings, pages + 1) {
933 LayoutResult::Fit(_) => {
934 pages += 1;
935 break;
936 }
937 LayoutResult::Split { remainder, .. } => {
938 pages += 1;
939 current = remainder;
940 }
941 }
942 if pages > 50 {
943 panic!("pagination did not terminate");
944 }
945 }
946 assert!(pages >= 2, "the oversized row should push onto its own page");
947 assert!(warnings.iter().any(|w| w.kind == LayoutWarningKind::ForcedPageBreak));
948 }
949
950 #[test]
951 fn table_column_align_positions_short_content_in_the_column() {
952 let table = Table::new()
953 .columns([TableColumn::fixed(100.0).align(Align::End)])
954 .rows([row(&["42"])]);
955 let c = ctx();
956 let mut warnings = Vec::new();
957 let area = Rect {
958 x: 0.0,
959 y: 0.0,
960 width: 100.0,
961 height: 50.0,
962 };
963 let LayoutResult::Fit(node) = Element::Table(table).layout(&c, area, &mut warnings, 1) else {
964 panic!("expected Fit");
965 };
966 let RenderNode::Group { children: rows, .. } = node.untagged() else {
967 panic!("expected Group");
968 };
969 let RenderNode::Group { children: cells, .. } = rows[0].untagged() else {
970 panic!("expected row group");
971 };
972 let RenderNode::Group { children: cell_inner, .. } = cells[0].untagged() else {
973 panic!("expected cell group");
974 };
975 let RenderNode::Group { area: cell_area, .. } = cell_inner[0].untagged() else {
976 panic!("expected clipped cell wrapper");
977 };
978 assert!(cell_area.x > 50.0, "expected right-aligned cell, got x={}", cell_area.x);
981 }
982
983 fn cell(text: &str) -> TableCell {
984 TableCell::from(text)
985 }
986
987 #[test]
988 fn rowspan_cell_spans_multiple_rows_as_one_atomic_block() {
989 let table = Table::new()
990 .columns([TableColumn::fixed(30.0), TableColumn::fixed(30.0)])
991 .rows(vec![
992 vec![TableCell::new("Summe").rowspan(2), cell("Zeile 1")],
993 vec![cell("Zeile 2")],
997 ]);
998 let c = ctx();
999 let mut warnings = Vec::new();
1000 let area = Rect {
1001 x: 0.0,
1002 y: 0.0,
1003 width: 60.0,
1004 height: 400.0,
1005 };
1006 let LayoutResult::Fit(node) = Element::Table(table).layout(&c, area, &mut warnings, 1) else {
1007 panic!("expected Fit");
1008 };
1009 let RenderNode::Group { children: blocks, .. } = node.untagged() else {
1010 panic!("expected Group");
1011 };
1012 assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
1013 assert_eq!(
1016 blocks.len(),
1017 1,
1018 "expected the 2-row span to render as a single block, got {} top-level children",
1019 blocks.len()
1020 );
1021 let RenderNode::Group {
1022 area: block_area,
1023 children: block_rows,
1024 ..
1025 } = &blocks[0]
1026 else {
1027 panic!("expected block group");
1028 };
1029 assert_eq!(
1032 block_rows.len(),
1033 2,
1034 "expected 2 row children in the block, got {}",
1035 block_rows.len()
1036 );
1037 let total_cells: usize = block_rows
1038 .iter()
1039 .map(|r| match r.untagged() {
1040 RenderNode::Group { children, .. } => children.len(),
1041 _ => panic!("expected row group"),
1042 })
1043 .sum();
1044 assert_eq!(total_cells, 3, "expected 3 rendered cells, got {total_cells}");
1047 assert!(
1050 block_area.height > 30.0,
1051 "block should cover both rows, got height {}",
1052 block_area.height
1053 );
1054 }
1055
1056 #[test]
1057 fn rowspan_never_splits_across_a_page_boundary() {
1058 let mut rows: Vec<Vec<TableCell>> = (0..3).map(|i| vec![cell(&format!("R{i}")), cell("x")]).collect();
1059 rows.push(vec![TableCell::new("Spanned").rowspan(2), cell("y0")]);
1060 rows.push(vec![cell("y1")]);
1061 let table = Table::new()
1062 .columns([TableColumn::fixed(30.0), TableColumn::fixed(30.0)])
1063 .rows(rows);
1064 let c = ctx();
1065 let mut warnings = Vec::new();
1066 let area = Rect {
1071 x: 0.0,
1072 y: 0.0,
1073 width: 60.0,
1074 height: 4.0 * 22.4,
1075 };
1076 let LayoutResult::Split { current, remainder } = Element::Table(table).layout(&c, area, &mut warnings, 1) else {
1077 panic!("expected a Split");
1078 };
1079 let RenderNode::Group { children, .. } = current.untagged() else {
1080 panic!("expected Group");
1081 };
1082 assert_eq!(
1083 children.len(),
1084 3,
1085 "the rowspan block must not partially fit onto the current page, got {} rows",
1086 children.len()
1087 );
1088 let Element::Table(remainder_table) = remainder else {
1089 panic!("expected Table remainder");
1090 };
1091 assert_eq!(
1092 remainder_table.rows.len(),
1093 2,
1094 "the whole 2-row span must move to the continuation page together"
1095 );
1096 assert_eq!(remainder_table.row_offset, 3);
1097 }
1098
1099 #[test]
1100 fn continuation_row_with_too_many_cells_still_reports_overflow() {
1101 let table = Table::new()
1102 .columns([TableColumn::fixed(30.0), TableColumn::fixed(30.0)])
1103 .rows(vec![
1104 vec![TableCell::new("Spanned").rowspan(2), cell("a")],
1105 vec![cell("b"), cell("c")],
1108 ]);
1109 let c = ctx();
1110 let mut warnings = Vec::new();
1111 let area = Rect {
1112 x: 0.0,
1113 y: 0.0,
1114 width: 60.0,
1115 height: 400.0,
1116 };
1117 let _ = Element::Table(table).layout(&c, area, &mut warnings, 1);
1120 assert!(warnings.iter().any(|w| w.kind == LayoutWarningKind::TableRowOverflow));
1121 }
1122
1123 #[test]
1124 fn cell_background_overrides_the_row_stripe() {
1125 let stripe = Color::rgb(240, 240, 240);
1126 let cell_bg = Color::rgb(255, 0, 0);
1127 let table = Table::new()
1128 .columns([TableColumn::fixed(30.0), TableColumn::fixed(30.0)])
1129 .striped(stripe)
1130 .rows(vec![
1131 vec![cell("a"), cell("b")],
1132 vec![TableCell::new("c").background(cell_bg), cell("d")], ]);
1134 let c = ctx();
1135 let mut warnings = Vec::new();
1136 let area = Rect {
1137 x: 0.0,
1138 y: 0.0,
1139 width: 60.0,
1140 height: 400.0,
1141 };
1142 let LayoutResult::Fit(node) = Element::Table(table).layout(&c, area, &mut warnings, 1) else {
1143 panic!("expected Fit");
1144 };
1145 let RenderNode::Group { children: blocks, .. } = node.untagged() else {
1146 panic!("expected Group");
1147 };
1148 let RenderNode::Group {
1149 background: row_bg,
1150 children: cells,
1151 ..
1152 } = blocks[1].untagged()
1153 else {
1154 panic!("expected row group");
1155 };
1156 assert_eq!(*row_bg, Some(stripe));
1158 let has_cell_rect = cells.iter().any(|cell_node| {
1161 let RenderNode::Group {
1162 children: cell_children, ..
1163 } = cell_node.untagged()
1164 else {
1165 return false;
1166 };
1167 cell_children
1168 .iter()
1169 .any(|n| matches!(n, RenderNode::Rect { background: Some(bg), .. } if *bg == cell_bg))
1170 });
1171 assert!(
1172 has_cell_rect,
1173 "expected a cell-level background Rect overriding the stripe, got: {cells:?}"
1174 );
1175 }
1176
1177 #[test]
1178 fn cell_padding_overrides_the_table_default() {
1179 let table = Table::new()
1180 .columns([TableColumn::fixed(60.0)])
1181 .cell_padding(4.0)
1182 .rows(vec![vec![TableCell::new("x").padding(20.0)]]);
1183 let c = ctx();
1184 let mut warnings = Vec::new();
1185 let area = Rect {
1186 x: 0.0,
1187 y: 0.0,
1188 width: 60.0,
1189 height: 400.0,
1190 };
1191 let LayoutResult::Fit(node) = Element::Table(table).layout(&c, area, &mut warnings, 1) else {
1192 panic!("expected Fit");
1193 };
1194 let RenderNode::Group { children: blocks, .. } = node.untagged() else {
1195 panic!("expected Group");
1196 };
1197 let RenderNode::Group { area: row_area, .. } = blocks[0].untagged() else {
1200 panic!("expected row group");
1201 };
1202 assert!(
1203 row_area.height > 14.4 + 2.0 * 20.0 - EPS,
1204 "expected the cell's own padding to grow the row, got height {}",
1205 row_area.height
1206 );
1207 }
1208}