Skip to main content

ppt_rs/parts/
table.rs

1//! Table part
2//!
3//! Represents table data embedded in slides with advanced formatting.
4//!
5//! # Features
6//! - Cell merging (row span, column span)
7//! - Text formatting (bold, italic, underline, strikethrough)
8//! - Cell alignment (horizontal and vertical)
9//! - Borders (all sides, individual sides)
10//! - Background colors and gradients
11//! - Font customization (size, color, family)
12//! - Table styles
13
14use super::base::{ContentType, Part, PartType};
15use crate::core::{escape_xml, ToXml};
16use crate::exc::PptxError;
17
18/// Horizontal alignment
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum HorizontalAlign {
21    #[default]
22    Left,
23    Center,
24    Right,
25    Justify,
26}
27
28impl HorizontalAlign {
29    pub fn as_str(&self) -> &'static str {
30        match self {
31            HorizontalAlign::Left => "l",
32            HorizontalAlign::Center => "ctr",
33            HorizontalAlign::Right => "r",
34            HorizontalAlign::Justify => "just",
35        }
36    }
37}
38
39/// Vertical alignment
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub enum VerticalAlign {
42    Top,
43    #[default]
44    Middle,
45    Bottom,
46}
47
48impl VerticalAlign {
49    pub fn as_str(&self) -> &'static str {
50        match self {
51            VerticalAlign::Top => "t",
52            VerticalAlign::Middle => "ctr",
53            VerticalAlign::Bottom => "b",
54        }
55    }
56}
57
58/// Border style
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum BorderStyle {
61    #[default]
62    Solid,
63    Dashed,
64    Dotted,
65    Double,
66    None,
67}
68
69impl BorderStyle {
70    pub fn as_str(&self) -> &'static str {
71        match self {
72            BorderStyle::Solid => "solid",
73            BorderStyle::Dashed => "dash",
74            BorderStyle::Dotted => "dot",
75            BorderStyle::Double => "dbl",
76            BorderStyle::None => "none",
77        }
78    }
79}
80
81/// Cell border
82#[derive(Debug, Clone, Default)]
83pub struct CellBorder {
84    pub width: i32, // in EMU (12700 = 1pt)
85    pub color: String,
86    pub style: BorderStyle,
87}
88
89impl CellBorder {
90    pub fn new(width_pt: f32, color: impl Into<String>) -> Self {
91        CellBorder {
92            width: (width_pt * 12700.0) as i32,
93            color: color.into(),
94            style: BorderStyle::Solid,
95        }
96    }
97
98    pub fn style(mut self, style: BorderStyle) -> Self {
99        self.style = style;
100        self
101    }
102
103    pub fn to_xml(&self, tag: &str) -> String {
104        if self.style == BorderStyle::None {
105            return format!("<a:{}/>\n", tag);
106        }
107        format!(
108            r#"<a:{} w="{}" cap="flat" cmpd="sng" algn="ctr">
109              <a:solidFill><a:srgbClr val="{}"/></a:solidFill>
110              <a:prstDash val="{}"/>
111            </a:{}>"#,
112            tag,
113            self.width,
114            self.color.trim_start_matches('#'),
115            self.style.as_str(),
116            tag
117        )
118    }
119}
120
121/// Cell borders (all four sides)
122#[derive(Debug, Clone, Default)]
123pub struct CellBorders {
124    pub left: Option<CellBorder>,
125    pub right: Option<CellBorder>,
126    pub top: Option<CellBorder>,
127    pub bottom: Option<CellBorder>,
128}
129
130impl CellBorders {
131    pub fn all(border: CellBorder) -> Self {
132        CellBorders {
133            left: Some(border.clone()),
134            right: Some(border.clone()),
135            top: Some(border.clone()),
136            bottom: Some(border),
137        }
138    }
139
140    pub fn none() -> Self {
141        let no_border = CellBorder {
142            width: 0,
143            color: String::new(),
144            style: BorderStyle::None,
145        };
146        CellBorders {
147            left: Some(no_border.clone()),
148            right: Some(no_border.clone()),
149            top: Some(no_border.clone()),
150            bottom: Some(no_border),
151        }
152    }
153
154    pub fn to_xml(&self) -> String {
155        let mut xml = String::new();
156        if let Some(ref b) = self.left {
157            xml.push_str(&b.to_xml("lnL"));
158        }
159        if let Some(ref b) = self.right {
160            xml.push_str(&b.to_xml("lnR"));
161        }
162        if let Some(ref b) = self.top {
163            xml.push_str(&b.to_xml("lnT"));
164        }
165        if let Some(ref b) = self.bottom {
166            xml.push_str(&b.to_xml("lnB"));
167        }
168        xml
169    }
170}
171
172impl ToXml for CellBorders {
173    fn to_xml(&self) -> String {
174        CellBorders::to_xml(self)
175    }
176}
177
178/// Cell margins
179#[derive(Debug, Clone)]
180pub struct CellMargins {
181    pub left: i32, // in EMU
182    pub right: i32,
183    pub top: i32,
184    pub bottom: i32,
185}
186
187impl Default for CellMargins {
188    fn default() -> Self {
189        CellMargins {
190            left: 91440, // 0.1 inch
191            right: 91440,
192            top: 45720, // 0.05 inch
193            bottom: 45720,
194        }
195    }
196}
197
198impl CellMargins {
199    pub fn uniform(margin: i32) -> Self {
200        CellMargins {
201            left: margin,
202            right: margin,
203            top: margin,
204            bottom: margin,
205        }
206    }
207}
208
209/// Table cell with advanced formatting
210#[derive(Debug, Clone)]
211pub struct TableCellPart {
212    pub text: String,
213    pub row_span: u32,
214    pub col_span: u32,
215    pub bold: bool,
216    pub italic: bool,
217    pub underline: bool,
218    pub strikethrough: bool,
219    pub background_color: Option<String>,
220    pub text_color: Option<String>,
221    pub font_size: Option<u32>,
222    pub font_family: Option<String>,
223    pub h_align: HorizontalAlign,
224    pub v_align: VerticalAlign,
225    pub borders: Option<CellBorders>,
226    pub margins: Option<CellMargins>,
227    pub is_merged: bool, // For cells that are part of a merge (not the anchor)
228}
229
230impl TableCellPart {
231    /// Create a new table cell
232    pub fn new(text: impl Into<String>) -> Self {
233        TableCellPart {
234            text: text.into(),
235            row_span: 1,
236            col_span: 1,
237            bold: false,
238            italic: false,
239            underline: false,
240            strikethrough: false,
241            background_color: None,
242            text_color: None,
243            font_size: None,
244            font_family: None,
245            h_align: HorizontalAlign::default(),
246            v_align: VerticalAlign::default(),
247            borders: None,
248            margins: None,
249            is_merged: false,
250        }
251    }
252
253    /// Create a merged placeholder cell (for cells covered by a span)
254    pub fn merged() -> Self {
255        let mut cell = Self::new("");
256        cell.is_merged = true;
257        cell
258    }
259
260    /// Set bold
261    pub fn bold(mut self) -> Self {
262        self.bold = true;
263        self
264    }
265
266    /// Set italic
267    pub fn italic(mut self) -> Self {
268        self.italic = true;
269        self
270    }
271
272    /// Set underline
273    pub fn underline(mut self) -> Self {
274        self.underline = true;
275        self
276    }
277
278    /// Set strikethrough
279    pub fn strikethrough(mut self) -> Self {
280        self.strikethrough = true;
281        self
282    }
283
284    /// Set background color
285    pub fn background(mut self, color: impl Into<String>) -> Self {
286        self.background_color = Some(color.into());
287        self
288    }
289
290    /// Set text color
291    pub fn color(mut self, color: impl Into<String>) -> Self {
292        self.text_color = Some(color.into());
293        self
294    }
295
296    /// Set font size (in points)
297    pub fn font_size(mut self, size: u32) -> Self {
298        self.font_size = Some(size);
299        self
300    }
301
302    /// Set font family
303    pub fn font(mut self, family: impl Into<String>) -> Self {
304        self.font_family = Some(family.into());
305        self
306    }
307
308    /// Set horizontal alignment
309    pub fn align(mut self, align: HorizontalAlign) -> Self {
310        self.h_align = align;
311        self
312    }
313
314    /// Set vertical alignment
315    pub fn valign(mut self, align: VerticalAlign) -> Self {
316        self.v_align = align;
317        self
318    }
319
320    /// Center text (horizontal and vertical)
321    pub fn center(mut self) -> Self {
322        self.h_align = HorizontalAlign::Center;
323        self.v_align = VerticalAlign::Middle;
324        self
325    }
326
327    /// Set row span
328    pub fn row_span(mut self, span: u32) -> Self {
329        self.row_span = span;
330        self
331    }
332
333    /// Set column span
334    pub fn col_span(mut self, span: u32) -> Self {
335        self.col_span = span;
336        self
337    }
338
339    /// Set all borders
340    pub fn borders(mut self, borders: CellBorders) -> Self {
341        self.borders = Some(borders);
342        self
343    }
344
345    /// Set uniform border on all sides
346    pub fn border(mut self, width_pt: f32, color: impl Into<String>) -> Self {
347        self.borders = Some(CellBorders::all(CellBorder::new(width_pt, color)));
348        self
349    }
350
351    /// Set cell margins
352    pub fn margins(mut self, margins: CellMargins) -> Self {
353        self.margins = Some(margins);
354        self
355    }
356
357    /// Generate XML for this cell
358    pub fn to_xml(&self) -> String {
359        // Handle merged cells (placeholders)
360        if self.is_merged {
361            return r#"<a:tc hMerge="1"><a:txBody><a:bodyPr/><a:lstStyle/><a:p/></a:txBody><a:tcPr/></a:tc>"#.to_string();
362        }
363
364        let mut attrs = String::new();
365        if self.row_span > 1 {
366            attrs.push_str(&format!(r#" rowSpan="{}""#, self.row_span));
367        }
368        if self.col_span > 1 {
369            attrs.push_str(&format!(r#" gridSpan="{}""#, self.col_span));
370        }
371
372        // Background fill
373        let bg_xml = self
374            .background_color
375            .as_ref()
376            .map(|c| {
377                format!(
378                    r#"<a:solidFill><a:srgbClr val="{}"/></a:solidFill>"#,
379                    c.trim_start_matches('#')
380                )
381            })
382            .unwrap_or_default();
383
384        // Text run properties
385        let mut rpr_attrs = String::new();
386        if self.bold {
387            rpr_attrs.push_str(r#" b="1""#);
388        }
389        if self.italic {
390            rpr_attrs.push_str(r#" i="1""#);
391        }
392        if self.underline {
393            rpr_attrs.push_str(r#" u="sng""#);
394        }
395        if self.strikethrough {
396            rpr_attrs.push_str(r#" strike="sngStrike""#);
397        }
398        if let Some(size) = self.font_size {
399            rpr_attrs.push_str(&format!(r#" sz="{}""#, size * 100));
400        }
401
402        // Text color
403        let color_xml = self
404            .text_color
405            .as_ref()
406            .map(|c| {
407                format!(
408                    r#"<a:solidFill><a:srgbClr val="{}"/></a:solidFill>"#,
409                    c.trim_start_matches('#')
410                )
411            })
412            .unwrap_or_default();
413
414        // Font family
415        let font_xml = self
416            .font_family
417            .as_ref()
418            .map(|f| format!(r#"<a:latin typeface="{}"/>"#, f))
419            .unwrap_or_default();
420
421        // Paragraph alignment
422        let p_align = format!(r#" algn="{}""#, self.h_align.as_str());
423
424        // Cell properties
425        let mut tcpr_attrs = format!(r#" anchor="{}""#, self.v_align.as_str());
426        if let Some(ref m) = self.margins {
427            tcpr_attrs.push_str(&format!(
428                r#" marL="{}" marR="{}" marT="{}" marB="{}""#,
429                m.left, m.right, m.top, m.bottom
430            ));
431        }
432
433        // Borders
434        let borders_xml = self
435            .borders
436            .as_ref()
437            .map(|b| b.to_xml())
438            .unwrap_or_default();
439
440        format!(
441            r#"<a:tc{}>
442          <a:txBody>
443            <a:bodyPr/>
444            <a:lstStyle/>
445            <a:p{}>
446              <a:r>
447                <a:rPr lang="en-US"{}>{}{}</a:rPr>
448                <a:t>{}</a:t>
449              </a:r>
450            </a:p>
451          </a:txBody>
452          <a:tcPr{}>{}{}</a:tcPr>
453        </a:tc>"#,
454            attrs,
455            p_align,
456            rpr_attrs,
457            color_xml,
458            font_xml,
459            escape_xml(&self.text),
460            tcpr_attrs,
461            borders_xml,
462            bg_xml
463        )
464    }
465}
466
467impl ToXml for TableCellPart {
468    fn to_xml(&self) -> String {
469        TableCellPart::to_xml(self)
470    }
471}
472
473/// Table row
474#[derive(Debug, Clone)]
475pub struct TableRowPart {
476    pub cells: Vec<TableCellPart>,
477    pub height: Option<i64>, // in EMU
478}
479
480impl TableRowPart {
481    /// Create a new table row
482    pub fn new(cells: Vec<TableCellPart>) -> Self {
483        TableRowPart {
484            cells,
485            height: None,
486        }
487    }
488
489    /// Set row height in EMU
490    pub fn height(mut self, height: i64) -> Self {
491        self.height = Some(height);
492        self
493    }
494
495    /// Generate XML for this row
496    pub fn to_xml(&self) -> String {
497        let height_attr = self
498            .height
499            .map(|h| format!(r#" h="{}""#, h))
500            .unwrap_or_default();
501
502        let cells_xml: String = self
503            .cells
504            .iter()
505            .map(|c| c.to_xml())
506            .collect::<Vec<_>>()
507            .join("\n        ");
508
509        format!(
510            r#"<a:tr{}>
511        {}
512      </a:tr>"#,
513            height_attr, cells_xml
514        )
515    }
516}
517
518impl ToXml for TableRowPart {
519    fn to_xml(&self) -> String {
520        TableRowPart::to_xml(self)
521    }
522}
523
524/// Table part for embedding in slides
525#[derive(Debug, Clone)]
526pub struct TablePart {
527    pub rows: Vec<TableRowPart>,
528    pub col_widths: Vec<i64>, // in EMU
529    pub x: i64,
530    pub y: i64,
531    pub width: i64,
532    pub height: i64,
533}
534
535impl TablePart {
536    /// Create a new table part
537    pub fn new() -> Self {
538        TablePart {
539            rows: vec![],
540            col_widths: vec![],
541            x: 914400,       // 1 inch
542            y: 1828800,      // 2 inches
543            width: 7315200,  // 8 inches
544            height: 1828800, // 2 inches
545        }
546    }
547
548    /// Add a row
549    pub fn add_row(mut self, row: TableRowPart) -> Self {
550        // Auto-calculate column widths if not set
551        if self.col_widths.is_empty() && !row.cells.is_empty() {
552            let col_count = row.cells.len();
553            let col_width = self.width / col_count as i64;
554            self.col_widths = vec![col_width; col_count];
555        }
556        self.rows.push(row);
557        self
558    }
559
560    /// Set position
561    pub fn position(mut self, x: i64, y: i64) -> Self {
562        self.x = x;
563        self.y = y;
564        self
565    }
566
567    /// Set size
568    pub fn size(mut self, width: i64, height: i64) -> Self {
569        self.width = width;
570        self.height = height;
571        self
572    }
573
574    /// Set column widths
575    pub fn col_widths(mut self, widths: Vec<i64>) -> Self {
576        self.col_widths = widths;
577        self
578    }
579
580    /// Generate table XML for embedding in a slide
581    pub fn to_slide_xml(&self, shape_id: usize) -> String {
582        let grid_cols: String = self
583            .col_widths
584            .iter()
585            .map(|w| format!(r#"<a:gridCol w="{}"/>"#, w))
586            .collect::<Vec<_>>()
587            .join("\n        ");
588
589        let rows_xml: String = self
590            .rows
591            .iter()
592            .map(|r| r.to_xml())
593            .collect::<Vec<_>>()
594            .join("\n      ");
595
596        format!(
597            r#"<p:graphicFrame>
598  <p:nvGraphicFramePr>
599    <p:cNvPr id="{}" name="Table {}"/>
600    <p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr>
601    <p:nvPr/>
602  </p:nvGraphicFramePr>
603  <p:xfrm>
604    <a:off x="{}" y="{}"/>
605    <a:ext cx="{}" cy="{}"/>
606  </p:xfrm>
607  <a:graphic>
608    <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
609      <a:tbl>
610        <a:tblPr firstRow="1" bandRow="1">
611          <a:tableStyleId>{{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}}</a:tableStyleId>
612        </a:tblPr>
613        <a:tblGrid>
614        {}
615        </a:tblGrid>
616      {}
617      </a:tbl>
618    </a:graphicData>
619  </a:graphic>
620</p:graphicFrame>"#,
621            shape_id, shape_id, self.x, self.y, self.width, self.height, grid_cols, rows_xml
622        )
623    }
624}
625
626impl Default for TablePart {
627    fn default() -> Self {
628        Self::new()
629    }
630}
631
632impl Part for TablePart {
633    fn path(&self) -> &str {
634        "" // Tables are embedded in slides, not separate parts
635    }
636
637    fn part_type(&self) -> PartType {
638        PartType::Slide // Tables are part of slides
639    }
640
641    fn content_type(&self) -> ContentType {
642        ContentType::Xml
643    }
644
645    fn to_xml(&self) -> Result<String, PptxError> {
646        Ok(self.to_slide_xml(2))
647    }
648
649    fn from_xml(_xml: &str) -> Result<Self, PptxError> {
650        Ok(TablePart::new())
651    }
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657
658    #[test]
659    fn test_table_cell_new() {
660        let cell = TableCellPart::new("Test");
661        assert_eq!(cell.text, "Test");
662        assert!(!cell.bold);
663    }
664
665    #[test]
666    fn test_table_cell_formatting() {
667        let cell = TableCellPart::new("Bold")
668            .bold()
669            .color("FF0000")
670            .font_size(14);
671        assert!(cell.bold);
672        assert_eq!(cell.text_color, Some("FF0000".to_string()));
673        assert_eq!(cell.font_size, Some(14));
674    }
675
676    #[test]
677    fn test_table_cell_span() {
678        let cell = TableCellPart::new("Merged").row_span(2).col_span(3);
679        assert_eq!(cell.row_span, 2);
680        assert_eq!(cell.col_span, 3);
681    }
682
683    #[test]
684    fn test_table_row_new() {
685        let row = TableRowPart::new(vec![TableCellPart::new("A"), TableCellPart::new("B")]);
686        assert_eq!(row.cells.len(), 2);
687    }
688
689    #[test]
690    fn test_table_part_new() {
691        let table = TablePart::new()
692            .add_row(TableRowPart::new(vec![
693                TableCellPart::new("Header 1"),
694                TableCellPart::new("Header 2"),
695            ]))
696            .add_row(TableRowPart::new(vec![
697                TableCellPart::new("Data 1"),
698                TableCellPart::new("Data 2"),
699            ]));
700        assert_eq!(table.rows.len(), 2);
701        assert_eq!(table.col_widths.len(), 2);
702    }
703
704    #[test]
705    fn test_table_to_xml() {
706        let table = TablePart::new().add_row(TableRowPart::new(vec![TableCellPart::new("Test")]));
707        let xml = table.to_slide_xml(5);
708        assert!(xml.contains("p:graphicFrame"));
709        assert!(xml.contains("a:tbl"));
710        assert!(xml.contains("Test"));
711    }
712}