Skip to main content

lightweight_pdf_core/
table.rs

1//! `Table` element (Phase 3, `plan/phases/phase-3-tables.md`): the element
2//! that matters most for invoices. Cells are plain `Element`s so they reuse
3//! the exact same `Layoutable`/text-wrap machinery as everything else —
4//! no separate cell-content model.
5
6use crate::element::Element;
7use crate::style::{Align, Border, Color, Common};
8
9/// A column's width: `fixed(w)` reserves an exact width, `flex(weight)`
10/// shares the leftover space proportionally (taffy `flex-grow` analogy,
11/// ADR-004 / `03-builder-api-design.md`) — the same distribution step as
12/// `Row`, not a generic flex implementation.
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "snake_case"))]
14#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
15#[derive(Clone, Copy, Debug)]
16pub enum ColumnWidth {
17    Fixed(f32),
18    Flex(f32),
19}
20
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(deny_unknown_fields))]
22#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
23#[derive(Clone, Copy, Debug)]
24pub struct TableColumn {
25    pub width: ColumnWidth,
26    #[cfg_attr(feature = "serde", serde(default))]
27    pub align: Align,
28}
29
30impl TableColumn {
31    pub fn fixed(width: f32) -> Self {
32        TableColumn {
33            width: ColumnWidth::Fixed(width),
34            align: Align::Start,
35        }
36    }
37
38    pub fn flex(weight: f32) -> Self {
39        TableColumn {
40            width: ColumnWidth::Flex(weight),
41            align: Align::Start,
42        }
43    }
44
45    pub fn align(mut self, align: Align) -> Self {
46        self.align = align;
47        self
48    }
49}
50
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(deny_unknown_fields))]
52#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
53#[derive(Clone, Debug)]
54pub struct TableCell {
55    pub element: Element,
56    #[cfg_attr(feature = "serde", serde(default = "TableCell::default_span"))]
57    pub colspan: usize,
58    /// How many rows (including this one) this cell's box extends down
59    /// through. A continuation row (one a `rowspan > 1` cell from an
60    /// earlier row still covers) simply omits a `TableCell` for that
61    /// column — same convention as HTML `<tr>`/`<td>`, not a separate
62    /// "placeholder" cell type.
63    #[cfg_attr(feature = "serde", serde(default = "TableCell::default_span"))]
64    pub rowspan: usize,
65    #[cfg_attr(feature = "serde", serde(default))]
66    pub align: Option<Align>,
67    /// Overrides the row's zebra stripe for this cell only (precedence:
68    /// cell beats row beats column — the same order `.align()` already
69    /// follows against `TableColumn::align`).
70    #[cfg_attr(feature = "serde", serde(default))]
71    pub background: Option<Color>,
72    #[cfg_attr(feature = "serde", serde(default))]
73    pub border: Option<Border>,
74    /// Overrides `Table::cell_padding` for this cell only.
75    #[cfg_attr(feature = "serde", serde(default))]
76    pub padding: Option<f32>,
77}
78
79impl TableCell {
80    pub fn new(element: impl Into<Element>) -> Self {
81        TableCell {
82            element: element.into(),
83            colspan: 1,
84            rowspan: 1,
85            align: None,
86            background: None,
87            border: None,
88            padding: None,
89        }
90    }
91
92    #[cfg(feature = "serde")]
93    fn default_span() -> usize {
94        1
95    }
96
97    pub fn colspan(mut self, colspan: usize) -> Self {
98        self.colspan = colspan.max(1);
99        self
100    }
101
102    pub fn rowspan(mut self, rowspan: usize) -> Self {
103        self.rowspan = rowspan.max(1);
104        self
105    }
106
107    pub fn align(mut self, align: Align) -> Self {
108        self.align = Some(align);
109        self
110    }
111
112    pub fn background(mut self, color: Color) -> Self {
113        self.background = Some(color);
114        self
115    }
116
117    pub fn border(mut self, border: Border) -> Self {
118        self.border = Some(border);
119        self
120    }
121
122    pub fn padding(mut self, padding: f32) -> Self {
123        self.padding = Some(padding);
124        self
125    }
126}
127
128impl<T: Into<Element>> From<T> for TableCell {
129    fn from(value: T) -> Self {
130        TableCell::new(value)
131    }
132}
133
134/// Implemented by a domain type (an invoice line item, a report row, ...)
135/// that knows how to render itself as one table row — lets callers write
136/// `Table::new()...from_rows(&items)` instead of hand-building
137/// `vec![vec![Element::from(..), ...]]` per row, where the column order is
138/// invisible at the call site and only checked at runtime. The plain
139/// `.rows(vec![vec![..]])` form stays available for ad hoc tables.
140pub trait TableRow {
141    fn cells(&self) -> Vec<TableCell>;
142}
143
144#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(deny_unknown_fields))]
145#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
146#[derive(Clone, Debug, Default)]
147pub struct Table {
148    #[cfg_attr(feature = "serde", serde(default))]
149    pub columns: Vec<TableColumn>,
150    #[cfg_attr(feature = "serde", serde(default))]
151    pub header: Option<Vec<TableCell>>,
152    #[cfg_attr(feature = "serde", serde(default))]
153    pub rows: Vec<Vec<TableCell>>,
154    /// Alternating row background ("Zebra-Streifen"), see
155    /// `02-elementcatalog-and-features.md`. Applies to data rows only (a
156    /// striped header would be indistinguishable from a striped data row).
157    #[cfg_attr(feature = "serde", serde(default))]
158    pub striped: Option<Color>,
159    /// Inner spacing on every side of each cell's content, same default
160    /// (4pt) header and data rows.
161    #[cfg_attr(feature = "serde", serde(default = "Table::default_cell_padding"))]
162    pub cell_padding: f32,
163    /// Absolute index of `rows[0]` within the *original*, unsplit table —
164    /// 0 unless this `Table` is itself the remainder produced by a
165    /// previous page's `LayoutResult::Split`. Not part of the public
166    /// builder surface; exists purely so `.striped()` keeps alternating
167    /// correctly across a page break instead of resetting per page.
168    #[cfg_attr(feature = "serde", serde(skip))]
169    pub row_offset: usize,
170    #[cfg_attr(feature = "serde", serde(default))]
171    pub common: Common,
172}
173
174impl Table {
175    #[cfg(feature = "serde")]
176    fn default_cell_padding() -> f32 {
177        Table::new().cell_padding
178    }
179
180    pub fn new() -> Self {
181        Table {
182            cell_padding: 4.0,
183            ..Default::default()
184        }
185    }
186
187    pub fn columns(mut self, columns: impl IntoIterator<Item = TableColumn>) -> Self {
188        self.columns = columns.into_iter().collect();
189        self
190    }
191
192    pub fn header(mut self, cells: impl IntoIterator<Item = impl Into<TableCell>>) -> Self {
193        self.header = Some(cells.into_iter().map(Into::into).collect());
194        self
195    }
196
197    pub fn rows(mut self, rows: impl IntoIterator<Item = impl IntoIterator<Item = impl Into<TableCell>>>) -> Self {
198        self.rows = rows.into_iter().map(|row| row.into_iter().map(Into::into).collect()).collect();
199        self
200    }
201
202    /// `.rows(..)` for anything implementing `TableRow` — one row per item,
203    /// in order.
204    pub fn from_rows<T: TableRow>(mut self, items: &[T]) -> Self {
205        self.rows = items.iter().map(TableRow::cells).collect();
206        self
207    }
208
209    pub fn striped(mut self, color: Color) -> Self {
210        self.striped = Some(color);
211        self
212    }
213
214    pub fn cell_padding(mut self, padding: f32) -> Self {
215        self.cell_padding = padding;
216        self
217    }
218
219    pub fn width(mut self, width: f32) -> Self {
220        self.common.width = Some(width);
221        self
222    }
223
224    pub fn height(mut self, height: f32) -> Self {
225        self.common.height = Some(height);
226        self
227    }
228
229    pub fn flex(mut self, factor: f32) -> Self {
230        self.common.flex = Some(factor);
231        self
232    }
233
234    pub fn keep_with_next(mut self) -> Self {
235        self.common.keep_with_next = true;
236        self
237    }
238}