Skip to main content

pptx_rs/shape/
table.rs

1//! `Table`:高阶表格。
2//!
3//! 表格在 OOXML 中嵌套较深(`graphicFrame` > `graphic` > `graphicData` > `tbl`),
4//! 本高阶 API 把它们"摊平"为 [`TableShape`],并以 `(row, col)` 二维索引访问。
5//!
6//! # 与 python-pptx 的对应
7//!
8//! - `pptx.table.Table` ←→ [`TableShape`];
9//! - `_Row` / `_Column` / `_Cell` ←→ `Row` / `Col` / `Cell`(oxml 模型)。
10//!
11//! # 单元格语义
12//!
13//! - 每个 cell 持有 [`TextBody`],可含多段多 Run;
14//! - 颜色由 `Cell::fill` 决定(`Color::None` 时不写 `<a:solidFill/>`);
15//! - 边距由 `Cell::margin` 决定(顺序:top, left, bottom, right)。
16//!
17//! # 限制
18//!
19//! - 列宽 / 行高必须显式设置(默认全 0)。
20//! - 表格样式通过 GUID 引用 PowerPoint 内置样式(见 [`crate::oxml::table::TableStyle`])。
21//!
22//! # 示例
23//!
24//! ```no_run
25//! use pptx_rs::shape::TableShape;
26//! use pptx_rs::EmuExt;
27//! use pptx_rs::Inches;
28//!
29//! let mut t = TableShape::new(2, 3, Inches(2.0).emu(), Inches(0.5).emu());
30//! t.set_cell_text(0, 0, "A1").unwrap();
31//! t.set_cell_text(0, 1, "B1").unwrap();
32//! ```
33
34use crate::oxml::shape::{Graphic as OxmlGraphic, GraphicFrame as OxmlFrame};
35use crate::oxml::table::{Cell as OxmlCell, Col as OxmlCol, Row as OxmlRow, Table as OxmlTable};
36use crate::oxml::txbody::TextBody;
37use crate::shape::base::Shape;
38use crate::units::Emu;
39
40/// 表格(行优先,行内 cell)。
41#[derive(Clone, Debug, Default)]
42pub struct TableShape {
43    /// 内部 oxml 句柄(`GraphicFrame`)。
44    pub(crate) frame: OxmlFrame,
45}
46
47impl TableShape {
48    /// 构造一个指定行列的表格(cell 文本留空)。
49    #[allow(clippy::field_reassign_with_default)]
50    pub fn new(rows: usize, cols: usize, col_width: Emu, row_height: Emu) -> Self {
51        let mut t = OxmlTable::default();
52        t.cols = (0..cols).map(|_| OxmlCol { width: col_width }).collect();
53        t.rows = (0..rows)
54            .map(|_| OxmlRow {
55                height: row_height,
56                cells: (0..cols).map(|_| OxmlCell::default()).collect(),
57                header: false,
58            })
59            .collect();
60        let frame = OxmlFrame {
61            graphic: OxmlGraphic::Table(t),
62            ..Default::default()
63        };
64        TableShape { frame }
65    }
66
67    /// 从 oxml Frame 构造。
68    pub fn from_frame(frame: OxmlFrame) -> Self {
69        TableShape { frame }
70    }
71
72    /// 取 oxml Table 引用。
73    pub fn table(&self) -> &OxmlTable {
74        match &self.frame.graphic {
75            OxmlGraphic::Table(t) => t,
76            // 不变量被破坏时(frame.graphic 不是 Table)panic——
77            // 与库整体"零 panic"约定一致,此处属内部不变量违反,
78            // 不走静默忽略路径(与 ChartShape 等"返回 Option"的设计不同,
79            // 因为 TableShape 的高阶 API 假设 frame.graphic 一定是 Table)。
80            _ => unreachable!("TableShape.table(): frame.graphic 不是 Table 变体"),
81        }
82    }
83    /// 取 oxml Table 可变引用。
84    pub fn table_mut(&mut self) -> &mut OxmlTable {
85        match &mut self.frame.graphic {
86            OxmlGraphic::Table(t) => t,
87            _ => unreachable!("TableShape.table_mut(): frame.graphic 不是 Table 变体"),
88        }
89    }
90
91    /// 行列数。
92    pub fn dims(&self) -> (usize, usize) {
93        let t = self.table();
94        (t.rows.len(), t.cols.len())
95    }
96
97    /// **是否**启用首行特殊格式(`firstRow="1"`)。
98    ///
99    /// 对标 python-pptx `Table.first_row`。
100    pub fn first_row(&self) -> bool {
101        self.table().tbl_look.first_row
102    }
103    /// 设置首行特殊格式。
104    pub fn set_first_row(&mut self, v: bool) {
105        self.table_mut().tbl_look.first_row = v;
106    }
107
108    /// **是否**启用末行特殊格式(`lastRow="1"`)。
109    ///
110    /// 对标 python-pptx `Table.last_row`。
111    pub fn last_row(&self) -> bool {
112        self.table().tbl_look.last_row
113    }
114    /// 设置末行特殊格式。
115    pub fn set_last_row(&mut self, v: bool) {
116        self.table_mut().tbl_look.last_row = v;
117    }
118
119    /// **是否**启用首列特殊格式(`firstColumn="1"`)。
120    ///
121    /// 对标 python-pptx `Table.first_column`。
122    pub fn first_column(&self) -> bool {
123        self.table().tbl_look.first_column
124    }
125    /// 设置首列特殊格式。
126    pub fn set_first_column(&mut self, v: bool) {
127        self.table_mut().tbl_look.first_column = v;
128    }
129
130    /// **是否**启用末列特殊格式(`lastColumn="1"`)。
131    ///
132    /// 对标 python-pptx `Table.last_column`。
133    pub fn last_column(&self) -> bool {
134        self.table().tbl_look.last_column
135    }
136    /// 设置末列特殊格式。
137    pub fn set_last_column(&mut self, v: bool) {
138        self.table_mut().tbl_look.last_column = v;
139    }
140
141    /// **是否**启用水平条纹(`noHBand="0"` 表示启用)。
142    ///
143    /// 对标 python-pptx `Table.horz_banding`。
144    /// 注意:OOXML 中 `noHBand="1"` 表示**禁用**,本方法返回取反值(true=启用)。
145    pub fn horz_banding(&self) -> bool {
146        !self.table().tbl_look.no_h_band
147    }
148    /// 设置水平条纹。
149    pub fn set_horz_banding(&mut self, v: bool) {
150        self.table_mut().tbl_look.no_h_band = !v;
151    }
152
153    /// **是否**启用垂直条纹(`noVBand="0"` 表示启用)。
154    ///
155    /// 对标 python-pptx `Table.vert_banding`。
156    /// 注意:OOXML 中 `noVBand="1"` 表示**禁用**,本方法返回取反值(true=启用)。
157    pub fn vert_banding(&self) -> bool {
158        !self.table().tbl_look.no_v_band
159    }
160    /// 设置垂直条纹。
161    pub fn set_vert_banding(&mut self, v: bool) {
162        self.table_mut().tbl_look.no_v_band = !v;
163    }
164
165    // --------------------- 表格样式 API(TODO-030) ---------------------
166
167    /// 设置表格样式(按内置样式名称)。
168    ///
169    /// 对标 python-pptx `Table.apply_style(style_id)`。
170    ///
171    /// # 参数
172    /// - `name`:内置样式名称(如 "Medium Style 2 - Accent 1")。
173    ///
174    /// # 返回值
175    /// - 成功设置返回 `true`;
176    /// - 名称不在内置注册表中返回 `false`。
177    ///
178    /// # 示例
179    ///
180    /// ```
181    /// use pptx_rs::shape::TableShape;
182    /// use pptx_rs::Emu;
183    ///
184    /// let mut t = TableShape::new(2, 2, Emu(1000), Emu(500));
185    /// assert!(t.set_style("Medium Style 2 - Accent 1"));
186    /// assert!(!t.set_style("Unknown Style"));
187    /// ```
188    pub fn set_style(&mut self, name: &str) -> bool {
189        self.table_mut().set_style(name)
190    }
191
192    /// 设置表格样式(按原始 GUID)。
193    ///
194    /// 用于设置不在内置注册表中的样式(如自定义 tableStyles.xml 中定义的样式)。
195    ///
196    /// # 参数
197    /// - `guid`:样式 GUID 字符串(如 `{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}`)。
198    pub fn set_style_id(&mut self, guid: impl Into<String>) {
199        self.table_mut().set_style_id(guid);
200    }
201
202    /// 取表格样式引用。
203    pub fn table_style(&self) -> Option<&crate::oxml::table::TableStyle> {
204        self.table().table_style.as_ref()
205    }
206
207    /// 清除表格样式引用。
208    pub fn clear_style(&mut self) {
209        self.table_mut().clear_style();
210    }
211
212    /// 取 cell 文本(把多段多 Run 拼起来,段间 `\n`)。
213    pub fn cell_text(&self, row: usize, col: usize) -> Option<String> {
214        let t = self.table();
215        let r = t.rows.get(row)?;
216        let c = r.cells.get(col)?;
217        let mut s = String::new();
218        for p in &c.text.paragraphs {
219            if !s.is_empty() {
220                s.push('\n');
221            }
222            for run in &p.runs {
223                s.push_str(&run.text);
224            }
225        }
226        Some(s)
227    }
228
229    /// 设 cell 文本(自动新建段落 + Run)。
230    ///
231    /// # 错误
232    /// - [`crate::Error::IndexOutOfRange`]:`row` 或 `col` 越界。
233    pub fn set_cell_text(&mut self, row: usize, col: usize, text: &str) -> crate::Result<()> {
234        let t = self.table_mut();
235        let r = t
236            .rows
237            .get_mut(row)
238            .ok_or(crate::Error::IndexOutOfRange(row))?;
239        let c = r
240            .cells
241            .get_mut(col)
242            .ok_or(crate::Error::IndexOutOfRange(col))?;
243        let mut tb = TextBody::new();
244        let mut p = crate::oxml::txbody::Paragraph::new();
245        p.runs.push(crate::oxml::txbody::Run::new(text));
246        tb.paragraphs.push(p);
247        c.text = tb;
248        Ok(())
249    }
250
251    /// 取 cell 可变引用。
252    ///
253    /// 对应 python-pptx 中 `table.cell(row, col)`。**包含**创建空 cell 的能力(越界会
254    /// 自动 push),适合"按需扩展"场景。
255    pub fn cell_mut(&mut self, row: usize, col: usize) -> &mut OxmlCell {
256        let t = self.table_mut();
257        // 自动补齐
258        while t.rows.len() <= row {
259            let ncols = t.cols.len().max(1);
260            t.rows.push(OxmlRow {
261                height: Emu(0),
262                cells: (0..ncols).map(|_| OxmlCell::default()).collect(),
263                header: false,
264            });
265        }
266        let r = &mut t.rows[row];
267        while r.cells.len() <= col {
268            r.cells.push(OxmlCell::default());
269        }
270        &mut r.cells[col]
271    }
272
273    /// 取 cell 不可变引用。
274    pub fn cell(&self, row: usize, col: usize) -> Option<&OxmlCell> {
275        let t = self.table();
276        t.rows.get(row)?.cells.get(col)
277    }
278
279    /// 设列宽(覆盖 col 处的宽度)。
280    pub fn set_column_width(&mut self, col: usize, w: Emu) -> crate::Result<()> {
281        let t = self.table_mut();
282        let c = t
283            .cols
284            .get_mut(col)
285            .ok_or(crate::Error::IndexOutOfRange(col))?;
286        c.width = w;
287        Ok(())
288    }
289
290    /// 取列宽。
291    pub fn column_width(&self, col: usize) -> Option<Emu> {
292        self.table().cols.get(col).map(|c| c.width)
293    }
294
295    /// 设行高。
296    pub fn set_row_height(&mut self, row: usize, h: Emu) -> crate::Result<()> {
297        let t = self.table_mut();
298        let r = t
299            .rows
300            .get_mut(row)
301            .ok_or(crate::Error::IndexOutOfRange(row))?;
302        r.height = h;
303        Ok(())
304    }
305
306    /// 取行高。
307    pub fn row_height(&self, row: usize) -> Option<Emu> {
308        self.table().rows.get(row).map(|r| r.height)
309    }
310
311    /// 标记某行为表头(首行加粗等特殊样式)。
312    pub fn set_header_row(&mut self, row: usize, is_header: bool) -> crate::Result<()> {
313        let t = self.table_mut();
314        let r = t
315            .rows
316            .get_mut(row)
317            .ok_or(crate::Error::IndexOutOfRange(row))?;
318        r.header = is_header;
319        Ok(())
320    }
321
322    /// 行数。
323    pub fn row_count(&self) -> usize {
324        self.table().rows.len()
325    }
326    /// 列数。
327    pub fn column_count(&self) -> usize {
328        self.table().cols.len()
329    }
330
331    // --------------------- python-pptx 风格扩展 ---------------------
332
333    /// 取整行(不可变)。
334    pub fn row(&self, idx: usize) -> Option<&OxmlRow> {
335        self.table().rows.get(idx)
336    }
337    /// 取整行(可变)。
338    pub fn row_mut(&mut self, idx: usize) -> Option<&mut OxmlRow> {
339        self.table_mut().rows.get_mut(idx)
340    }
341    /// 整列(不可变)。
342    pub fn column(&self, idx: usize) -> Option<&OxmlCol> {
343        self.table().cols.get(idx)
344    }
345    /// 整列(可变)。
346    pub fn column_mut(&mut self, idx: usize) -> Option<&mut OxmlCol> {
347        self.table_mut().cols.get_mut(idx)
348    }
349
350    /// 设 cell 填充色。
351    ///
352    /// 对应 python-pptx `cell.fill.solid(); cell.fill.fore_color.rgb = ...`。
353    pub fn set_cell_fill(
354        &mut self,
355        row: usize,
356        col: usize,
357        c: crate::oxml::color::Color,
358    ) -> crate::Result<()> {
359        let t = self.table_mut();
360        let r = t
361            .rows
362            .get_mut(row)
363            .ok_or(crate::Error::IndexOutOfRange(row))?;
364        let cidx = r
365            .cells
366            .get_mut(col)
367            .ok_or(crate::Error::IndexOutOfRange(col))?;
368        cidx.fill = c;
369        Ok(())
370    }
371
372    /// 取 cell 填充色(克隆)。
373    pub fn cell_fill(&self, row: usize, col: usize) -> Option<crate::oxml::color::Color> {
374        self.cell(row, col).map(|c| c.fill.clone())
375    }
376
377    /// 设 cell 四向内边距(EMU,顺序 top/left/bottom/right)。
378    pub fn set_cell_margins(
379        &mut self,
380        row: usize,
381        col: usize,
382        top: Emu,
383        left: Emu,
384        bottom: Emu,
385        right: Emu,
386    ) -> crate::Result<()> {
387        let t = self.table_mut();
388        let r = t
389            .rows
390            .get_mut(row)
391            .ok_or(crate::Error::IndexOutOfRange(row))?;
392        let c = r
393            .cells
394            .get_mut(col)
395            .ok_or(crate::Error::IndexOutOfRange(col))?;
396        c.margin = (Some(top), Some(left), Some(bottom), Some(right));
397        Ok(())
398    }
399
400    /// 取 cell 文本帧可变引用(`cell.text_frame` 风格)。
401    ///
402    /// 通过 `cell_mut` 返回 [`OxmlCell`],其 `.text` 字段就是 `TextBody`;
403    /// 调用方需要进一步 `.text` 访问或用 `TextFrame::from(&mut c.text)` 包装。
404    pub fn cell_text_frame_mut(&mut self, row: usize, col: usize) -> &mut TextBody {
405        &mut self.cell_mut(row, col).text
406    }
407
408    /// 追加新行(返回行索引与可变引用)。
409    ///
410    /// 列数沿用现有 `cols.len()`。
411    pub fn add_row(&mut self, height: Emu) -> &mut OxmlRow {
412        let t = self.table_mut();
413        let ncols = t.cols.len().max(1);
414        t.rows.push(OxmlRow {
415            height,
416            cells: (0..ncols).map(|_| OxmlCell::default()).collect(),
417            header: false,
418        });
419        // push 后直接按索引取最后一个元素,避免 expect
420        let idx = t.rows.len() - 1;
421        &mut t.rows[idx]
422    }
423
424    /// 追加新列。
425    pub fn add_column(&mut self, width: Emu) -> &mut OxmlCol {
426        let t = self.table_mut();
427        t.cols.push(OxmlCol { width });
428        // push 后直接按索引取最后一个元素,避免 expect
429        let idx = t.cols.len() - 1;
430        &mut t.cols[idx]
431    }
432
433    /// 删除指定行。
434    ///
435    /// 对标 python-pptx 中通过 XML 操作删除 `<a:tr>` 的能力。
436    ///
437    /// # 参数
438    /// - `idx`:行索引(0-based)。
439    ///
440    /// # 错误
441    /// - [`crate::Error::IndexOutOfRange`]:`idx` 越界。
442    pub fn remove_row(&mut self, idx: usize) -> crate::Result<()> {
443        let t = self.table_mut();
444        if idx >= t.rows.len() {
445            return Err(crate::Error::IndexOutOfRange(idx));
446        }
447        t.rows.remove(idx);
448        Ok(())
449    }
450
451    /// 删除指定列。
452    ///
453    /// 同时从每一行中移除对应位置的 cell。
454    ///
455    /// # 参数
456    /// - `idx`:列索引(0-based)。
457    ///
458    /// # 错误
459    /// - [`crate::Error::IndexOutOfRange`]:`idx` 越界。
460    pub fn remove_column(&mut self, idx: usize) -> crate::Result<()> {
461        let t = self.table_mut();
462        if idx >= t.cols.len() {
463            return Err(crate::Error::IndexOutOfRange(idx));
464        }
465        t.cols.remove(idx);
466        // 同步删除每行中对应位置的 cell
467        for r in &mut t.rows {
468            if idx < r.cells.len() {
469                r.cells.remove(idx);
470            }
471        }
472        Ok(())
473    }
474
475    /// 合并指定矩形区域的单元格。
476    ///
477    /// 对标 python-pptx `cell.merge(other_cell)`。
478    ///
479    /// # 参数
480    /// - `row1, col1`:合并区域左上角;
481    /// - `row2, col2`:合并区域右下角。
482    ///
483    /// # 行为
484    /// - 左上角 cell 设为合并源(`grid_span` / `row_span`);
485    /// - 区域内其它 cell 设为 `h_merge` / `v_merge` 虚拟单元格;
486    /// - 若区域只有 1×1 则为 no-op。
487    ///
488    /// # 错误
489    /// - [`crate::Error::IndexOutOfRange`]:索引越界;
490    /// - [`crate::Error::Other`]:`row2 < row1` 或 `col2 < col1`。
491    pub fn merge_cells(
492        &mut self,
493        row1: usize,
494        col1: usize,
495        row2: usize,
496        col2: usize,
497    ) -> crate::Result<()> {
498        if row2 < row1 || col2 < col1 {
499            return Err(crate::Error::Other(
500                "merge_cells: row2/col2 不能小于 row1/col1".into(),
501            ));
502        }
503        let nrows = self.row_count();
504        let ncols = self.column_count();
505        if row2 >= nrows || col2 >= ncols {
506            return Err(crate::Error::IndexOutOfRange(row2.max(col2)));
507        }
508        let grid_span = (col2 - col1 + 1) as u32;
509        let row_span = (row2 - row1 + 1) as u32;
510        // 如果只有 1x1,无需合并
511        if grid_span == 1 && row_span == 1 {
512            return Ok(());
513        }
514        let t = self.table_mut();
515        // 设置合并源(左上角)
516        {
517            let cell = &mut t.rows[row1].cells[col1];
518            cell.grid_span = grid_span;
519            cell.row_span = row_span;
520            cell.h_merge = false;
521            cell.v_merge = false;
522        }
523        // 设置被合并方(虚拟单元格)
524        for r in row1..=row2 {
525            for c in col1..=col2 {
526                if r == row1 && c == col1 {
527                    continue; // 跳过合并源
528                }
529                let cell = &mut t.rows[r].cells[c];
530                cell.grid_span = 1;
531                cell.row_span = 1;
532                // hMerge 用于同一行内被合并的 cell
533                cell.h_merge = r == row1;
534                // vMerge 用于跨行被合并的 cell
535                cell.v_merge = r != row1;
536            }
537        }
538        Ok(())
539    }
540
541    /// 拆分单元格(TODO-029 高阶 API)。
542    ///
543    /// 把一个由 [`Self::merge_cells`] 合并出的"合并源"单元格还原为
544    /// 多个独立单元格。与 `merge_cells` 是逆操作。
545    ///
546    /// # 参数
547    /// - `row, col`:合并源单元格的位置(即合并时的左上角,必须是
548    ///   `grid_span > 1` 或 `row_span > 1` 的"真实"单元格,不能是 h_merge/v_merge
549    ///   的虚拟单元格)。
550    ///
551    /// # 行为
552    /// - 把 `(row, col)` 的 `grid_span` / `row_span` 重置为 1;
553    /// - 把合并区域内的所有虚拟单元格(`h_merge` / `v_merge` 为 true)解除虚拟状态;
554    /// - 单元格的文本/填充/边框等属性不会被重置——只会修改合并相关字段。
555    ///
556    /// # 错误
557    /// - [`crate::Error::IndexOutOfRange`]:`row` / `col` 越界;
558    /// - [`crate::Error::Other`]:目标单元格不是合并源(`grid_span == 1 && row_span == 1`),
559    ///   或目标单元格是虚拟单元格(`h_merge` / `v_merge` 为 true)。
560    pub fn split_cell(&mut self, row: usize, col: usize) -> crate::Result<()> {
561        let nrows = self.row_count();
562        let ncols = self.column_count();
563        if row >= nrows || col >= ncols {
564            return Err(crate::Error::IndexOutOfRange(row.max(col)));
565        }
566        let t = self.table_mut();
567        let cell = &t.rows[row].cells[col];
568        // 不能拆分虚拟单元格
569        if cell.h_merge || cell.v_merge {
570            return Err(crate::Error::Other(
571                "split_cell: 目标单元格是虚拟单元格,请对合并源调用 split_cell".into(),
572            ));
573        }
574        let grid_span = cell.grid_span;
575        let row_span = cell.row_span;
576        // 不是合并源
577        if grid_span <= 1 && row_span <= 1 {
578            return Err(crate::Error::Other(
579                "split_cell: 目标单元格不是合并源(grid_span 和 row_span 均为 1)".into(),
580            ));
581        }
582        // 合并区域右下角
583        let row2 = row + row_span as usize - 1;
584        let col2 = col + grid_span as usize - 1;
585        // 重置合并源
586        {
587            let cell = &mut t.rows[row].cells[col];
588            cell.grid_span = 1;
589            cell.row_span = 1;
590            cell.h_merge = false;
591            cell.v_merge = false;
592        }
593        // 解除虚拟单元格状态
594        for r in row..=row2 {
595            for c in col..=col2 {
596                if r == row && c == col {
597                    continue; // 跳过原合并源
598                }
599                let cell = &mut t.rows[r].cells[c];
600                cell.grid_span = 1;
601                cell.row_span = 1;
602                cell.h_merge = false;
603                cell.v_merge = false;
604            }
605        }
606        Ok(())
607    }
608
609    /// 设置单元格边框。
610    ///
611    /// 对标 python-pptx `cell.border_top` / `border_bottom` / `border_left` / `border_right`。
612    ///
613    /// # 参数
614    /// - `row, col`:单元格位置;
615    /// - `side`:边框方向([`BorderSide`]);
616    /// - `width`:边框宽度(EMU);
617    /// - `color`:边框颜色(`Color::None` 表示使用主题继承);
618    /// - `no_fill`:是否写出 `<a:noFill/>`(无填充边框)。
619    ///
620    /// # 错误
621    /// - [`crate::Error::IndexOutOfRange`]:`row` 或 `col` 越界。
622    pub fn set_cell_border(
623        &mut self,
624        row: usize,
625        col: usize,
626        side: BorderSide,
627        width: Emu,
628        color: crate::oxml::color::Color,
629        no_fill: bool,
630    ) -> crate::Result<()> {
631        let t = self.table_mut();
632        let r = t
633            .rows
634            .get_mut(row)
635            .ok_or(crate::Error::IndexOutOfRange(row))?;
636        let c = r
637            .cells
638            .get_mut(col)
639            .ok_or(crate::Error::IndexOutOfRange(col))?;
640        let border = crate::oxml::table::CellBorder {
641            color,
642            width,
643            no_fill,
644        };
645        match side {
646            BorderSide::Left => c.border_left = Some(border),
647            BorderSide::Right => c.border_right = Some(border),
648            BorderSide::Top => c.border_top = Some(border),
649            BorderSide::Bottom => c.border_bottom = Some(border),
650        }
651        Ok(())
652    }
653
654    /// 取单元格边框(克隆)。
655    pub fn cell_border(
656        &self,
657        row: usize,
658        col: usize,
659        side: BorderSide,
660    ) -> Option<crate::oxml::table::CellBorder> {
661        let c = self.cell(row, col)?;
662        match side {
663            BorderSide::Left => c.border_left.clone(),
664            BorderSide::Right => c.border_right.clone(),
665            BorderSide::Top => c.border_top.clone(),
666            BorderSide::Bottom => c.border_bottom.clone(),
667        }
668    }
669
670    // --------------------- 占位符(TODO-007 表格占位符类型化填充) ---------------------
671
672    /// 将本表格形状标记为占位符(TODO-007 表格占位符类型化填充)。
673    ///
674    /// 写出 XML 时会在 `<p:nvGraphicFramePr>/<p:nvPr>` 内插入
675    /// `<p:ph type="tbl" idx="..."/>`,使 PowerPoint 把该 graphicFrame
676    /// 识别为表格占位符的填充实例。
677    ///
678    /// # 参数
679    /// - `ph_idx`:占位符索引(对应 `<p:ph idx="..."/>`)。
680    /// - `ph_type`:占位符类型字符串(如 `"tbl"` / `"obj"`),`None` 时省略 `type` 属性。
681    pub fn set_placeholder(&mut self, ph_idx: u32, ph_type: Option<&str>) {
682        self.frame.is_placeholder = true;
683        self.frame.ph_idx = Some(ph_idx);
684        self.frame.ph_type = ph_type.map(|s| s.to_string());
685    }
686
687    /// 清除占位符标记,使本表格形状变为普通 graphicFrame。
688    pub fn clear_placeholder(&mut self) {
689        self.frame.is_placeholder = false;
690        self.frame.ph_idx = None;
691        self.frame.ph_type = None;
692    }
693
694    /// 是否被标记为占位符。
695    pub fn is_placeholder(&self) -> bool {
696        self.frame.is_placeholder
697    }
698
699    /// 占位符索引(若已标记)。
700    pub fn ph_idx(&self) -> Option<u32> {
701        self.frame.ph_idx
702    }
703
704    /// 占位符类型字符串(若已标记)。
705    pub fn ph_type(&self) -> Option<&str> {
706        self.frame.ph_type.as_deref()
707    }
708}
709
710/// 边框方向枚举。
711#[derive(Clone, Copy, Debug, PartialEq, Eq)]
712pub enum BorderSide {
713    /// 左边框(`<a:lnL>`)。
714    Left,
715    /// 右边框(`<a:lnR>`)。
716    Right,
717    /// 上边框(`<a:lnT>`)。
718    Top,
719    /// 下边框(`<a:lnB>`)。
720    Bottom,
721}
722
723impl Shape for TableShape {
724    fn id(&self) -> u32 {
725        self.frame.id
726    }
727    fn set_id(&mut self, id: u32) {
728        self.frame.id = id;
729    }
730    fn name(&self) -> &str {
731        &self.frame.name
732    }
733    fn set_name(&mut self, name: String) {
734        self.frame.name = name;
735    }
736    fn shape_type(&self) -> &'static str {
737        "table"
738    }
739
740    fn left(&self) -> Emu {
741        self.frame.properties.xfrm.off_x.unwrap_or_default()
742    }
743    fn set_left(&mut self, emu: Emu) {
744        self.frame.properties.xfrm.off_x = Some(emu);
745    }
746    fn top(&self) -> Emu {
747        self.frame.properties.xfrm.off_y.unwrap_or_default()
748    }
749    fn set_top(&mut self, emu: Emu) {
750        self.frame.properties.xfrm.off_y = Some(emu);
751    }
752    fn width(&self) -> Emu {
753        self.frame.properties.xfrm.ext_cx.unwrap_or_default()
754    }
755    fn set_width(&mut self, emu: Emu) {
756        self.frame.properties.xfrm.ext_cx = Some(emu);
757    }
758    fn height(&self) -> Emu {
759        self.frame.properties.xfrm.ext_cy.unwrap_or_default()
760    }
761    fn set_height(&mut self, emu: Emu) {
762        self.frame.properties.xfrm.ext_cy = Some(emu);
763    }
764
765    /// 表格不支持旋转(OOXML 规范)。调用 [`Shape::set_rotation`] 会被忽略。
766    fn rotation(&self) -> f64 {
767        0.0
768    }
769    fn set_rotation(&mut self, _deg: f64) {}
770}
771
772#[cfg(test)]
773mod tests {
774    use super::*;
775    use crate::oxml::color::Color;
776    use crate::units::{Emu, RGBColor};
777
778    /// `merge_cells` 正确设置 gridSpan/rowSpan/hMerge/vMerge。
779    #[test]
780    fn merge_cells_sets_attributes() {
781        let mut t = TableShape::new(3, 3, Emu(1000), Emu(500));
782        // 合并 (0,0) 到 (1,2) —— 2 行 3 列
783        t.merge_cells(0, 0, 1, 2).unwrap();
784        // 合并源 (0,0)
785        let origin = t.cell(0, 0).unwrap();
786        assert_eq!(origin.grid_span, 3);
787        assert_eq!(origin.row_span, 2);
788        assert!(!origin.h_merge);
789        assert!(!origin.v_merge);
790        // 同行被合并方 (0,1) / (0,2) —— hMerge
791        let h1 = t.cell(0, 1).unwrap();
792        assert!(h1.h_merge);
793        assert!(!h1.v_merge);
794        // 跨行被合并方 (1,0) / (1,1) / (1,2) —— vMerge
795        let v1 = t.cell(1, 0).unwrap();
796        assert!(!v1.h_merge);
797        assert!(v1.v_merge);
798    }
799
800    /// `merge_cells` 1×1 为 no-op。
801    #[test]
802    fn merge_cells_1x1_is_noop() {
803        let mut t = TableShape::new(2, 2, Emu(1000), Emu(500));
804        t.merge_cells(0, 0, 0, 0).unwrap();
805        let c = t.cell(0, 0).unwrap();
806        assert_eq!(c.grid_span, 0); // 默认值 0
807        assert_eq!(c.row_span, 0);
808    }
809
810    /// `merge_cells` 越界返回错误。
811    #[test]
812    fn merge_cells_out_of_bounds() {
813        let mut t = TableShape::new(2, 2, Emu(1000), Emu(500));
814        assert!(t.merge_cells(0, 0, 5, 5).is_err());
815        assert!(t.merge_cells(1, 0, 0, 0).is_err()); // row2 < row1
816    }
817
818    /// `set_cell_border` 正确设置边框。
819    #[test]
820    fn set_cell_border_works() {
821        let mut t = TableShape::new(2, 2, Emu(1000), Emu(500));
822        t.set_cell_border(
823            0,
824            0,
825            BorderSide::Top,
826            Emu(9525),
827            Color::RGB(RGBColor::RED),
828            false,
829        )
830        .unwrap();
831        let b = t.cell_border(0, 0, BorderSide::Top).unwrap();
832        assert_eq!(b.width.value(), 9525);
833        assert!(!b.no_fill);
834    }
835
836    /// `tblLook` 高阶 API 正确设置和读取布尔属性。
837    ///
838    /// 这是 TODO-028 的测试:验证 first_row / last_row / first_column / last_column
839    /// / horz_banding / vert_banding 的 getter/setter 行为。
840    #[test]
841    fn tbl_look_boolean_attributes() {
842        let mut t = TableShape::new(2, 2, Emu(1000), Emu(500));
843        // 默认值(与 PowerPoint 一致)
844        assert!(t.first_row(), "默认 firstRow=true");
845        assert!(!t.last_row(), "默认 lastRow=false");
846        assert!(t.first_column(), "默认 firstColumn=true");
847        assert!(!t.last_column(), "默认 lastColumn=false");
848        assert!(t.horz_banding(), "默认 horz_banding=true");
849        assert!(!t.vert_banding(), "默认 vert_banding=false");
850
851        // 修改值
852        t.set_first_row(false);
853        t.set_last_row(true);
854        t.set_first_column(false);
855        t.set_last_column(true);
856        t.set_horz_banding(false);
857        t.set_vert_banding(true);
858
859        // 验证
860        assert!(!t.first_row());
861        assert!(t.last_row());
862        assert!(!t.first_column());
863        assert!(t.last_column());
864        assert!(!t.horz_banding());
865        assert!(t.vert_banding());
866
867        // 验证底层 oxml 字段也正确更新
868        let lk = &t.table().tbl_look;
869        assert!(!lk.first_row);
870        assert!(lk.last_row);
871        assert!(!lk.first_column);
872        assert!(lk.last_column);
873        assert!(lk.no_h_band, "horz_banding=false → noHBand=true");
874        assert!(!lk.no_v_band, "vert_banding=true → noVBand=false");
875    }
876
877    /// `remove_row` / `remove_column` 正确删除。
878    #[test]
879    fn remove_row_and_column() {
880        let mut t = TableShape::new(3, 3, Emu(1000), Emu(500));
881        t.remove_row(1).unwrap();
882        assert_eq!(t.row_count(), 2);
883        t.remove_column(1).unwrap();
884        assert_eq!(t.column_count(), 2);
885        // 每行 cell 数也应同步减少
886        assert_eq!(t.table().rows[0].cells.len(), 2);
887    }
888
889    /// `remove_row` 越界返回错误。
890    #[test]
891    fn remove_row_out_of_bounds() {
892        let mut t = TableShape::new(2, 2, Emu(1000), Emu(500));
893        assert!(t.remove_row(5).is_err());
894        assert!(t.remove_column(5).is_err());
895    }
896
897    // --------------------- 表格样式测试(TODO-030) ---------------------
898
899    /// `set_style` 正确设置内置样式。
900    #[test]
901    fn set_style_builtin() {
902        let mut t = TableShape::new(2, 2, Emu(1000), Emu(500));
903        assert!(t.set_style("Medium Style 2 - Accent 1"));
904        let style = t.table_style().expect("style 应已设置");
905        assert_eq!(style.style_id(), "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}");
906        assert_eq!(style.style_name(), Some("Medium Style 2 - Accent 1"));
907    }
908
909    /// `set_style` 对未知名称返回 `false`。
910    #[test]
911    fn set_style_unknown_returns_false() {
912        let mut t = TableShape::new(2, 2, Emu(1000), Emu(500));
913        assert!(!t.set_style("Nonexistent Style"));
914        assert!(t.table_style().is_none());
915    }
916
917    /// `set_style_id` 用原始 GUID 设置样式。
918    #[test]
919    fn set_style_id_raw_guid() {
920        let mut t = TableShape::new(2, 2, Emu(1000), Emu(500));
921        t.set_style_id("{5940675A-B579-460E-94D1-54222C63F5DA}");
922        let style = t.table_style().expect("style 应已设置");
923        assert_eq!(style.style_id(), "{5940675A-B579-460E-94D1-54222C63F5DA}");
924    }
925
926    /// `clear_style` 清除样式引用。
927    #[test]
928    fn clear_style_works() {
929        let mut t = TableShape::new(2, 2, Emu(1000), Emu(500));
930        t.set_style("Medium Style 2 - Accent 1");
931        assert!(t.table_style().is_some());
932        t.clear_style();
933        assert!(t.table_style().is_none());
934    }
935
936    /// 表格样式序列化正确写出 `<a:tableStyleId>`。
937    #[test]
938    fn table_style_serializes() {
939        let mut t = TableShape::new(1, 1, Emu(1000), Emu(500));
940        t.set_style("No Style, Table Grid");
941        let mut w = crate::oxml::writer::XmlWriter::new();
942        t.table().write_xml(&mut w);
943        let xml = &w.buf;
944        assert!(xml.contains("<a:tableStyleId>"));
945        assert!(xml.contains("{5940675A-B579-460E-94D1-54222C63F5DA}"));
946        assert!(xml.contains("</a:tableStyleId>"));
947    }
948}