Skip to main content

ppt_rs/generator/
tables.rs

1//! Table creation support for PPTX generation
2
3/// Horizontal text alignment
4#[derive(Clone, Debug, Default, PartialEq)]
5pub enum CellAlign {
6    Left,
7    #[default]
8    Center,
9    Right,
10    Justify,
11}
12
13impl CellAlign {
14    /// Get the OOXML alignment value
15    pub fn as_str(&self) -> &'static str {
16        match self {
17            CellAlign::Left => "l",
18            CellAlign::Center => "ctr",
19            CellAlign::Right => "r",
20            CellAlign::Justify => "just",
21        }
22    }
23}
24
25/// Vertical text alignment
26#[derive(Clone, Debug, Default, PartialEq)]
27pub enum CellVAlign {
28    Top,
29    #[default]
30    Middle,
31    Bottom,
32}
33
34impl CellVAlign {
35    /// Get the OOXML anchor value
36    pub fn as_str(&self) -> &'static str {
37        match self {
38            CellVAlign::Top => "t",
39            CellVAlign::Middle => "ctr",
40            CellVAlign::Bottom => "b",
41        }
42    }
43}
44
45/// Table cell content
46#[derive(Clone, Debug)]
47pub struct TableCell {
48    pub text: String,
49    pub bold: bool,
50    pub italic: bool,
51    pub underline: bool,
52    pub text_color: Option<String>,      // RGB hex color for text
53    pub background_color: Option<String>, // RGB hex color for background
54    pub font_size: Option<u32>,          // Font size in points
55    pub font_family: Option<String>,     // Font family name
56    pub align: CellAlign,                // Horizontal alignment
57    pub valign: CellVAlign,              // Vertical alignment
58    pub wrap_text: bool,                 // Text wrapping
59    pub row_span: u32,
60    pub col_span: u32,
61    pub v_merge: bool,
62    pub h_merge: bool,
63}
64
65impl TableCell {
66    /// Create a new table cell
67    pub fn new(text: &str) -> Self {
68        TableCell {
69            text: text.to_string(),
70            bold: false,
71            italic: false,
72            underline: false,
73            text_color: None,
74            background_color: None,
75            font_size: None,
76            font_family: None,
77            align: CellAlign::Center,
78            valign: CellVAlign::Middle,
79            wrap_text: true,
80            row_span: 1,
81            col_span: 1,
82            v_merge: false,
83            h_merge: false,
84        }
85    }
86
87    /// Set cell text as bold
88    pub fn bold(mut self) -> Self {
89        self.bold = true;
90        self
91    }
92
93    /// Set cell text as italic
94    pub fn italic(mut self) -> Self {
95        self.italic = true;
96        self
97    }
98
99    /// Set cell text as underlined
100    pub fn underline(mut self) -> Self {
101        self.underline = true;
102        self
103    }
104
105
106    /// Set cell text color (RGB hex format, e.g., "FF0000" or "#FF0000")
107    pub fn text_color(mut self, color: &str) -> Self {
108        self.text_color = Some(color.trim_start_matches('#').to_uppercase());
109        self
110    }
111
112    /// Set cell background color (RGB hex format, e.g., "FF0000" or "#FF0000")
113    pub fn background_color(mut self, color: &str) -> Self {
114        self.background_color = Some(color.trim_start_matches('#').to_uppercase());
115        self
116    }
117
118    /// Set font size in points
119    pub fn font_size(mut self, size: u32) -> Self {
120        self.font_size = Some(size);
121        self
122    }
123
124    /// Set font family name
125    pub fn font_family(mut self, family: &str) -> Self {
126        self.font_family = Some(family.to_string());
127        self
128    }
129
130    /// Set horizontal text alignment
131    pub fn align(mut self, align: CellAlign) -> Self {
132        self.align = align;
133        self
134    }
135
136    /// Set horizontal text alignment to left
137    pub fn align_left(mut self) -> Self {
138        self.align = CellAlign::Left;
139        self
140    }
141
142    /// Set horizontal text alignment to right
143    pub fn align_right(mut self) -> Self {
144        self.align = CellAlign::Right;
145        self
146    }
147
148    /// Set horizontal text alignment to center
149    pub fn align_center(mut self) -> Self {
150        self.align = CellAlign::Center;
151        self
152    }
153
154    /// Set vertical text alignment
155    pub fn valign(mut self, valign: CellVAlign) -> Self {
156        self.valign = valign;
157        self
158    }
159
160    /// Set vertical text alignment to top
161    pub fn valign_top(mut self) -> Self {
162        self.valign = CellVAlign::Top;
163        self
164    }
165
166    /// Set vertical text alignment to bottom
167    pub fn valign_bottom(mut self) -> Self {
168        self.valign = CellVAlign::Bottom;
169        self
170    }
171
172    /// Enable or disable text wrapping
173    pub fn wrap(mut self, wrap: bool) -> Self {
174        self.wrap_text = wrap;
175        self
176    }
177
178    /// Set row span (number of rows to merge down)
179    pub fn with_row_span(mut self, span: u32) -> Self {
180        self.row_span = span;
181        self
182    }
183
184    /// Set column span (number of columns to merge right)
185    pub fn with_col_span(mut self, span: u32) -> Self {
186        self.col_span = span;
187        self
188    }
189
190    /// Set vertical merge flag (for cells covered by row span)
191    pub fn with_v_merge(mut self) -> Self {
192        self.v_merge = true;
193        self
194    }
195
196    /// Set horizontal merge flag (for cells covered by col span)
197    pub fn with_h_merge(mut self) -> Self {
198        self.h_merge = true;
199        self
200    }
201
202    /// Set horizontal span (gridSpan) - this cell covers multiple columns
203    pub fn grid_span(mut self, span: u32) -> Self {
204        self.col_span = span;
205        self
206    }
207
208    /// Set vertical span (rowSpan) - this cell covers multiple rows
209    pub fn row_span(mut self, span: u32) -> Self {
210        self.row_span = span;
211        self
212    }
213
214    /// Mark this cell as horizontally merged (covered by another cell's gridSpan)
215    pub fn h_merge(mut self) -> Self {
216        self.h_merge = true;
217        self
218    }
219
220    /// Mark this cell as vertically merged (covered by another cell's rowSpan)
221    pub fn v_merge(mut self) -> Self {
222        self.v_merge = true;
223        self
224    }
225}
226
227/// Table row
228#[derive(Clone, Debug)]
229pub struct TableRow {
230    pub cells: Vec<TableCell>,
231    pub height: Option<u32>, // in EMU
232}
233
234impl TableRow {
235    /// Create a new table row
236    pub fn new(cells: Vec<TableCell>) -> Self {
237        TableRow {
238            cells,
239            height: None,
240        }
241    }
242
243    /// Set row height
244    pub fn with_height(mut self, height: u32) -> Self {
245        self.height = Some(height);
246        self
247    }
248}
249
250/// Table definition
251#[derive(Clone, Debug)]
252pub struct Table {
253    pub rows: Vec<TableRow>,
254    pub column_widths: Vec<u32>, // in EMU
255    pub x: u32,                  // Position X in EMU
256    pub y: u32,                  // Position Y in EMU
257}
258
259impl Table {
260    /// Create a new table
261    pub fn new(rows: Vec<TableRow>, column_widths: Vec<u32>, x: u32, y: u32) -> Self {
262        Table {
263            rows,
264            column_widths,
265            x,
266            y,
267        }
268    }
269
270    /// Get number of columns
271    pub fn column_count(&self) -> usize {
272        self.column_widths.len()
273    }
274
275    /// Get number of rows
276    pub fn row_count(&self) -> usize {
277        self.rows.len()
278    }
279
280    /// Get total table width (sum of column widths)
281    pub fn width(&self) -> u32 {
282        self.column_widths.iter().sum()
283    }
284
285    /// Get total table height (sum of row heights)
286    pub fn height(&self) -> u32 {
287        self.rows
288            .iter()
289            .map(|r| r.height.unwrap_or(400000))
290            .sum()
291    }
292
293    /// Create a simple table from 2D data
294    pub fn from_data(data: Vec<Vec<&str>>, column_widths: Vec<u32>, x: u32, y: u32) -> Self {
295        let rows = data
296            .into_iter()
297            .map(|row| {
298                let cells = row
299                    .into_iter()
300                    .map(TableCell::new)
301                    .collect();
302                TableRow::new(cells)
303            })
304            .collect();
305
306        Table {
307            rows,
308            column_widths,
309            x,
310            y,
311        }
312    }
313}
314
315/// Table builder for fluent API
316pub struct TableBuilder {
317    rows: Vec<TableRow>,
318    column_widths: Vec<u32>,
319    x: u32,
320    y: u32,
321}
322
323impl TableBuilder {
324    /// Create a new table builder
325    pub fn new(column_widths: Vec<u32>) -> Self {
326        TableBuilder {
327            rows: Vec::new(),
328            column_widths,
329            x: 0,
330            y: 0,
331        }
332    }
333
334    /// Set table position
335    pub fn position(mut self, x: u32, y: u32) -> Self {
336        self.x = x;
337        self.y = y;
338        self
339    }
340
341    /// Add a row
342    pub fn add_row(mut self, row: TableRow) -> Self {
343        self.rows.push(row);
344        self
345    }
346
347    /// Add a simple row from strings
348    pub fn add_simple_row(mut self, cells: Vec<&str>) -> Self {
349        let row = TableRow::new(
350            cells
351                .into_iter()
352                .map(TableCell::new)
353                .collect(),
354        );
355        self.rows.push(row);
356        self
357    }
358
359    /// Build the table
360    pub fn build(self) -> Table {
361        Table {
362            rows: self.rows,
363            column_widths: self.column_widths,
364            x: self.x,
365            y: self.y,
366        }
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    #[test]
375    fn test_table_cell_builder() {
376        let cell = TableCell::new("Header")
377            .bold()
378            .italic()
379            .underline()
380            .text_color("FF0000")
381            .background_color("0000FF")
382            .font_size(24)
383            .font_family("Arial");
384        assert_eq!(cell.text, "Header");
385        assert!(cell.bold);
386        assert!(cell.italic);
387        assert!(cell.underline);
388        assert_eq!(cell.text_color, Some("FF0000".to_string()));
389        assert_eq!(cell.background_color, Some("0000FF".to_string()));
390        assert_eq!(cell.font_size, Some(24));
391        assert_eq!(cell.font_family, Some("Arial".to_string()));
392    }
393
394    #[test]
395    fn test_table_row() {
396        let cells = vec![TableCell::new("A"), TableCell::new("B")];
397        let row = TableRow::new(cells).with_height(500000);
398        assert_eq!(row.cells.len(), 2);
399        assert_eq!(row.height, Some(500000));
400    }
401
402    #[test]
403    fn test_table_from_data() {
404        let data = vec![
405            vec!["Name", "Age"],
406            vec!["Alice", "30"],
407            vec!["Bob", "25"],
408        ];
409        let table = Table::from_data(data, vec![1000000, 1000000], 0, 0);
410        assert_eq!(table.row_count(), 3);
411        assert_eq!(table.column_count(), 2);
412    }
413
414    #[test]
415    fn test_table_builder() {
416        let table = TableBuilder::new(vec![1000000, 1000000])
417            .position(100000, 200000)
418            .add_simple_row(vec!["Header 1", "Header 2"])
419            .add_simple_row(vec!["Cell 1", "Cell 2"])
420            .build();
421
422        assert_eq!(table.row_count(), 2);
423        assert_eq!(table.column_count(), 2);
424        assert_eq!(table.x, 100000);
425        assert_eq!(table.y, 200000);
426    }
427}