Skip to main content

deckcraft_model/
table.rs

1//! Tables (graphic frames with `a:tbl`).
2
3use serde::{Deserialize, Serialize};
4
5use crate::style::{ColorRef, Fill, Line};
6use crate::text::{Anchor, TextBody};
7
8#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
9#[serde(default, rename_all = "camelCase")]
10pub struct Cell {
11    pub text: TextBody,
12    pub fill: Option<Fill>,
13    /// Left, right, top, bottom borders; `None` = from the table style.
14    pub borders: [Option<Line>; 4],
15    pub diag_down: Option<Line>,
16    pub diag_up: Option<Line>,
17    /// Margins (l, r, t, b) in points; `None` = defaults (7.2, 7.2, 3.6, 3.6).
18    pub margins: Option<[f64; 4]>,
19    pub anchor: Option<Anchor>,
20    pub grid_span: u32,
21    pub row_span: u32,
22    /// Covered by a merge to the left / above.
23    pub h_merge: bool,
24    pub v_merge: bool,
25    pub vertical: Option<crate::text::TextDir>,
26}
27
28impl Cell {
29    pub fn new(text: &str) -> Self {
30        Cell {
31            text: TextBody::from_text(text),
32            grid_span: 1,
33            row_span: 1,
34            ..Default::default()
35        }
36    }
37    pub fn is_covered(&self) -> bool {
38        self.h_merge || self.v_merge
39    }
40}
41
42#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
43#[serde(default, rename_all = "camelCase")]
44pub struct Row {
45    pub height: f64,
46    pub cells: Vec<Cell>,
47}
48
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
50#[serde(default, rename_all = "camelCase")]
51pub struct Table {
52    /// Column widths in points.
53    pub cols: Vec<f64>,
54    pub rows: Vec<Row>,
55    /// Table style id (our built-in style names or a PPTX style GUID).
56    pub style: String,
57    pub first_row: bool,
58    pub first_col: bool,
59    pub last_row: bool,
60    pub last_col: bool,
61    pub band_row: bool,
62    pub band_col: bool,
63    /// `a:tblPr` fill: a fill behind the whole table (over the style's own background).
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub background: Option<Fill>,
66}
67
68impl Default for Table {
69    fn default() -> Self {
70        Table {
71            cols: vec![],
72            rows: vec![],
73            style: "medium2-accent1".into(),
74            first_row: true,
75            first_col: false,
76            last_row: false,
77            last_col: false,
78            band_row: true,
79            band_col: false,
80            background: None,
81        }
82    }
83}
84
85impl Table {
86    pub fn new(rows: usize, cols: usize, width: f64, row_height: f64) -> Self {
87        let rows = rows.clamp(1, 1000);
88        let cols = cols.clamp(1, 1000);
89        let cw = if cols > 0 { width / cols as f64 } else { width };
90        Table {
91            cols: vec![cw; cols],
92            rows: (0..rows)
93                .map(|_| Row {
94                    height: row_height,
95                    cells: (0..cols).map(|_| Cell::new("")).collect(),
96                })
97                .collect(),
98            ..Default::default()
99        }
100    }
101    pub fn n_rows(&self) -> usize {
102        self.rows.len()
103    }
104    pub fn n_cols(&self) -> usize {
105        self.cols.len()
106    }
107    pub fn cell(&self, r: usize, c: usize) -> Option<&Cell> {
108        self.rows.get(r).and_then(|row| row.cells.get(c))
109    }
110    pub fn cell_mut(&mut self, r: usize, c: usize) -> Option<&mut Cell> {
111        self.rows.get_mut(r).and_then(|row| row.cells.get_mut(c))
112    }
113    pub fn width(&self) -> f64 {
114        self.cols.iter().sum()
115    }
116    pub fn height(&self) -> f64 {
117        self.rows.iter().map(|r| r.height).sum()
118    }
119    pub fn insert_row(&mut self, at: usize) {
120        let at = at.min(self.rows.len());
121        let h = self
122            .rows
123            .get(at.saturating_sub(1))
124            .map(|r| r.height)
125            .unwrap_or(30.0);
126        self.rows.insert(
127            at,
128            Row {
129                height: h,
130                cells: (0..self.cols.len()).map(|_| Cell::new("")).collect(),
131            },
132        );
133    }
134    pub fn insert_col(&mut self, at: usize) {
135        let at = at.min(self.cols.len());
136        let w = self.cols.get(at.saturating_sub(1)).copied().unwrap_or(80.0);
137        self.cols.insert(at, w);
138        for r in &mut self.rows {
139            let i = at.min(r.cells.len());
140            r.cells.insert(i, Cell::new(""));
141        }
142    }
143    pub fn delete_row(&mut self, at: usize) -> bool {
144        if self.rows.len() <= 1 || at >= self.rows.len() {
145            return false;
146        }
147        self.rows.remove(at);
148        true
149    }
150    pub fn delete_col(&mut self, at: usize) -> bool {
151        if self.cols.len() <= 1 || at >= self.cols.len() {
152            return false;
153        }
154        self.cols.remove(at);
155        for r in &mut self.rows {
156            if at < r.cells.len() {
157                r.cells.remove(at);
158            }
159        }
160        true
161    }
162    /// Merge the rectangle (r0,c0)–(r1,c1) inclusive into its top-left cell.
163    pub fn merge(&mut self, r0: usize, c0: usize, r1: usize, c1: usize) -> bool {
164        let (r0, r1) = (r0.min(r1), r0.max(r1));
165        let (c0, c1) = (c0.min(c1), c0.max(c1));
166        if r1 >= self.rows.len() || c1 >= self.cols.len() || (r0 == r1 && c0 == c1) {
167            return false;
168        }
169        let mut text = vec![];
170        for r in r0..=r1 {
171            for c in c0..=c1 {
172                if let Some(cell) = self.cell_mut(r, c) {
173                    if !cell.text.is_empty() {
174                        text.append(&mut cell.text.paragraphs);
175                    }
176                    cell.h_merge = c > c0;
177                    cell.v_merge = r > r0;
178                    cell.grid_span = 1;
179                    cell.row_span = 1;
180                }
181            }
182        }
183        if let Some(cell) = self.cell_mut(r0, c0) {
184            cell.grid_span = (c1 - c0 + 1) as u32;
185            cell.row_span = (r1 - r0 + 1) as u32;
186            cell.h_merge = false;
187            cell.v_merge = false;
188            if !text.is_empty() {
189                cell.text.paragraphs = text;
190            }
191        }
192        true
193    }
194    /// Undo a merge at (r, c).
195    pub fn split(&mut self, r: usize, c: usize) -> bool {
196        let Some(cell) = self.cell(r, c) else {
197            return false;
198        };
199        let (gs, rs) = (
200            cell.grid_span.max(1) as usize,
201            cell.row_span.max(1) as usize,
202        );
203        if gs == 1 && rs == 1 {
204            return false;
205        }
206        for rr in r..r + rs {
207            for cc in c..c + gs {
208                if let Some(x) = self.cell_mut(rr, cc) {
209                    x.h_merge = false;
210                    x.v_merge = false;
211                    x.grid_span = 1;
212                    x.row_span = 1;
213                }
214            }
215        }
216        true
217    }
218    /// Make every row have exactly `cols.len()` cells (repair after loading).
219    pub fn normalize(&mut self) {
220        let n = self.cols.len();
221        for r in &mut self.rows {
222            r.cells.resize_with(n, || Cell::new(""));
223            for c in &mut r.cells {
224                c.grid_span = c.grid_span.max(1);
225                c.row_span = c.row_span.max(1);
226            }
227        }
228    }
229}
230
231/// A table style defined in the file (`ppt/tableStyles.xml`), as PowerPoint writes the built-in
232/// styles a deck uses: how each part of a table (whole table, banded rows and columns, first and
233/// last rows and columns, corner cells) is filled, bordered and lettered.
234#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
235#[serde(default, rename_all = "camelCase")]
236pub struct TableStyleDef {
237    /// The style's GUID (`a:tableStyleId` on tables).
238    pub id: String,
239    pub name: String,
240    pub parts: Vec<(TablePart, TablePartStyle)>,
241    /// `a:tblBg`: a fill behind the whole table (only `fill` and `fill_ref` are used).
242    pub background: Option<TablePartStyle>,
243    /// The definition as read, written back unchanged.
244    #[serde(skip_serializing_if = "String::is_empty")]
245    pub raw: String,
246}
247
248impl TableStyleDef {
249    pub fn part(&self, p: TablePart) -> Option<&TablePartStyle> {
250        self.parts.iter().find(|(k, _)| *k == p).map(|(_, s)| s)
251    }
252}
253
254/// The parts of a table style, in the order they apply (later ones win).
255#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub enum TablePart {
258    WholeTbl,
259    Band1H,
260    Band2H,
261    Band1V,
262    Band2V,
263    LastCol,
264    FirstCol,
265    LastRow,
266    FirstRow,
267    SeCell,
268    SwCell,
269    NeCell,
270    NwCell,
271}
272
273impl TablePart {
274    pub const ALL: [TablePart; 13] = [
275        TablePart::WholeTbl,
276        TablePart::Band1H,
277        TablePart::Band2H,
278        TablePart::Band1V,
279        TablePart::Band2V,
280        TablePart::LastCol,
281        TablePart::FirstCol,
282        TablePart::LastRow,
283        TablePart::FirstRow,
284        TablePart::SeCell,
285        TablePart::SwCell,
286        TablePart::NeCell,
287        TablePart::NwCell,
288    ];
289
290    pub fn xml(self) -> &'static str {
291        match self {
292            TablePart::WholeTbl => "wholeTbl",
293            TablePart::Band1H => "band1H",
294            TablePart::Band2H => "band2H",
295            TablePart::Band1V => "band1V",
296            TablePart::Band2V => "band2V",
297            TablePart::LastCol => "lastCol",
298            TablePart::FirstCol => "firstCol",
299            TablePart::LastRow => "lastRow",
300            TablePart::FirstRow => "firstRow",
301            TablePart::SeCell => "seCell",
302            TablePart::SwCell => "swCell",
303            TablePart::NeCell => "neCell",
304            TablePart::NwCell => "nwCell",
305        }
306    }
307
308    pub fn from_xml(s: &str) -> Option<TablePart> {
309        TablePart::ALL.into_iter().find(|p| p.xml() == s)
310    }
311}
312
313/// One part of a table style. A theme reference (`a:fillRef`, `a:lnRef`) is kept as the theme's
314/// style index with its colour.
315#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
316#[serde(default, rename_all = "camelCase")]
317pub struct TablePartStyle {
318    pub fill: Option<Fill>,
319    pub fill_ref: Option<(u32, ColorRef)>,
320    /// Left, right, top, bottom, inside horizontal, inside vertical.
321    pub borders: [Option<TableBorder>; 6],
322    pub text_color: Option<ColorRef>,
323    pub bold: Option<bool>,
324    pub italic: Option<bool>,
325}
326
327/// A border of a table style part: a line, or a theme line style reference.
328#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
329#[serde(rename_all = "camelCase", tag = "kind")]
330pub enum TableBorder {
331    Line { line: Line },
332    Ref { idx: u32, color: ColorRef },
333}
334
335/// Built-in table styles: (id, label). Styles are generated from the theme accents.
336pub fn table_styles() -> Vec<(String, String)> {
337    let mut v = vec![
338        ("none".to_string(), "No Style, No Grid".to_string()),
339        ("grid".to_string(), "No Style, Table Grid".to_string()),
340    ];
341    for (kind, label) in [
342        ("light1", "Light Style 1"),
343        ("light2", "Light Style 2"),
344        ("medium2", "Medium Style 2"),
345        ("medium4", "Medium Style 4"),
346        ("dark1", "Dark Style 1"),
347    ] {
348        v.push((format!("{kind}-tx1"), label.to_string()));
349        for a in 1..=6 {
350            v.push((format!("{kind}-accent{a}"), format!("{label} - Accent {a}")));
351        }
352    }
353    v
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn insert_delete_merge_split() {
362        let mut t = Table::new(3, 3, 300.0, 30.0);
363        assert_eq!((t.n_rows(), t.n_cols()), (3, 3));
364        t.insert_row(1);
365        t.insert_col(3);
366        assert_eq!((t.n_rows(), t.n_cols()), (4, 4));
367        assert!(t.delete_row(0) && t.delete_col(0));
368        assert_eq!((t.n_rows(), t.n_cols()), (3, 3));
369        assert!(t.merge(0, 0, 1, 1));
370        assert_eq!(
371            t.cell(0, 0).map(|c| (c.grid_span, c.row_span)),
372            Some((2, 2))
373        );
374        assert!(t.cell(1, 1).is_some_and(|c| c.is_covered()));
375        assert!(t.split(0, 0));
376        assert!(!t.cell(1, 1).is_some_and(|c| c.is_covered()));
377        assert!(!t.merge(5, 5, 6, 6));
378        assert!(!t.delete_row(99));
379        let mut one = Table::new(1, 1, 10.0, 10.0);
380        assert!(!one.delete_row(0) && !one.delete_col(0));
381        assert!(table_styles().len() > 30);
382    }
383}