1pub mod grid;
39
40use hwpforge_foundation::{Color, HwpUnit};
41use schemars::JsonSchema;
42use serde::{Deserialize, Serialize};
43
44use crate::caption::Caption;
45use crate::object_id::ObjectId;
46use crate::paragraph::Paragraph;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
50#[serde(rename_all = "snake_case")]
51pub enum TablePageBreak {
52 #[default]
54 Cell,
55 Table,
57 None,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
63#[serde(rename_all = "snake_case")]
64pub enum TableVerticalAlign {
65 Top,
67 #[default]
69 Center,
70 Bottom,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
76pub struct TableMargin {
77 pub left: HwpUnit,
79 pub right: HwpUnit,
81 pub top: HwpUnit,
83 pub bottom: HwpUnit,
85}
86
87fn default_repeat_header() -> bool {
88 true
89}
90
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
113#[non_exhaustive]
114pub struct Table {
115 pub rows: Vec<TableRow>,
117 pub width: Option<HwpUnit>,
119 pub caption: Option<Caption>,
121 #[serde(default)]
123 pub page_break: TablePageBreak,
124 #[serde(default = "default_repeat_header")]
126 pub repeat_header: bool,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub cell_spacing: Option<HwpUnit>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub border_fill_id: Option<u32>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub inst_id: Option<ObjectId>,
139}
140
141impl Table {
142 #[must_use]
153 pub fn new(rows: Vec<TableRow>) -> Self {
154 Self {
155 rows,
156 width: None,
157 caption: None,
158 page_break: TablePageBreak::Cell,
159 repeat_header: true,
160 cell_spacing: None,
161 border_fill_id: None,
162 inst_id: None,
163 }
164 }
165
166 #[must_use]
168 pub fn with_width(mut self, width: HwpUnit) -> Self {
169 self.width = Some(width);
170 self
171 }
172
173 #[must_use]
175 pub fn with_caption(mut self, caption: Caption) -> Self {
176 self.caption = Some(caption);
177 self
178 }
179
180 #[must_use]
182 pub fn with_page_break(mut self, page_break: TablePageBreak) -> Self {
183 self.page_break = page_break;
184 self
185 }
186
187 #[must_use]
189 pub fn with_repeat_header(mut self, repeat_header: bool) -> Self {
190 self.repeat_header = repeat_header;
191 self
192 }
193
194 #[must_use]
196 pub fn with_cell_spacing(mut self, cell_spacing: HwpUnit) -> Self {
197 self.cell_spacing = Some(cell_spacing);
198 self
199 }
200
201 #[must_use]
203 pub fn with_border_fill_id(mut self, border_fill_id: u32) -> Self {
204 self.border_fill_id = Some(border_fill_id);
205 self
206 }
207
208 pub fn row_count(&self) -> usize {
210 self.rows.len()
211 }
212
213 pub fn col_count(&self) -> usize {
217 self.rows.first().map_or(0, |r| r.cells.len())
218 }
219
220 pub fn is_empty(&self) -> bool {
222 self.rows.is_empty()
223 }
224}
225
226impl std::fmt::Display for Table {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 write!(f, "Table({}x{})", self.row_count(), self.col_count())
229 }
230}
231
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
248#[non_exhaustive]
249pub struct TableRow {
250 pub cells: Vec<TableCell>,
252 pub height: Option<HwpUnit>,
254 #[serde(default)]
256 pub is_header: bool,
257}
258
259impl TableRow {
260 #[must_use]
277 pub fn new(cells: Vec<TableCell>) -> Self {
278 Self { cells, height: None, is_header: false }
279 }
280
281 #[must_use]
298 pub fn with_height(cells: Vec<TableCell>, height: HwpUnit) -> Self {
299 Self { cells, height: Some(height), is_header: false }
300 }
301
302 #[must_use]
304 pub fn with_header(mut self, is_header: bool) -> Self {
305 self.is_header = is_header;
306 self
307 }
308}
309
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
330#[non_exhaustive]
331pub struct TableCell {
332 pub paragraphs: Vec<Paragraph>,
334 pub col_span: u16,
336 pub row_span: u16,
338 pub width: HwpUnit,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub height: Option<HwpUnit>,
343 pub background: Option<Color>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub border_fill_id: Option<u32>,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub margin: Option<TableMargin>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub vertical_align: Option<TableVerticalAlign>,
354}
355
356impl TableCell {
357 #[must_use]
375 pub fn new(paragraphs: Vec<Paragraph>, width: HwpUnit) -> Self {
376 Self {
377 paragraphs,
378 col_span: 1,
379 row_span: 1,
380 width,
381 height: None,
382 background: None,
383 border_fill_id: None,
384 margin: None,
385 vertical_align: None,
386 }
387 }
388
389 #[must_use]
408 pub fn with_span(
409 paragraphs: Vec<Paragraph>,
410 width: HwpUnit,
411 col_span: u16,
412 row_span: u16,
413 ) -> Self {
414 Self {
415 paragraphs,
416 col_span,
417 row_span,
418 width,
419 height: None,
420 background: None,
421 border_fill_id: None,
422 margin: None,
423 vertical_align: None,
424 }
425 }
426
427 #[must_use]
429 pub fn with_height(mut self, height: HwpUnit) -> Self {
430 self.height = Some(height);
431 self
432 }
433
434 #[must_use]
436 pub fn with_background(mut self, background: Color) -> Self {
437 self.background = Some(background);
438 self
439 }
440
441 #[must_use]
443 pub fn with_border_fill_id(mut self, border_fill_id: u32) -> Self {
444 self.border_fill_id = Some(border_fill_id);
445 self
446 }
447
448 #[must_use]
450 pub fn with_margin(mut self, margin: TableMargin) -> Self {
451 self.margin = Some(margin);
452 self
453 }
454
455 #[must_use]
457 pub fn with_vertical_align(mut self, vertical_align: TableVerticalAlign) -> Self {
458 self.vertical_align = Some(vertical_align);
459 self
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use crate::run::Run;
467 use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
468
469 fn simple_paragraph() -> Paragraph {
470 Paragraph::with_runs(
471 vec![Run::text("cell", CharShapeIndex::new(0))],
472 ParaShapeIndex::new(0),
473 )
474 }
475
476 fn simple_cell() -> TableCell {
477 TableCell::new(vec![simple_paragraph()], HwpUnit::from_mm(50.0).unwrap())
478 }
479
480 fn simple_row() -> TableRow {
481 TableRow::new(vec![simple_cell(), simple_cell()])
482 }
483
484 fn simple_table() -> Table {
485 Table::new(vec![simple_row(), simple_row()])
486 }
487
488 #[test]
489 fn table_new() {
490 let t = simple_table();
491 assert_eq!(t.row_count(), 2);
492 assert_eq!(t.col_count(), 2);
493 assert!(!t.is_empty());
494 assert!(t.width.is_none());
495 assert!(t.caption.is_none());
496 assert_eq!(t.page_break, TablePageBreak::Cell);
497 assert!(t.repeat_header);
498 assert!(t.cell_spacing.is_none());
499 assert!(t.border_fill_id.is_none());
500 }
501
502 #[test]
503 fn empty_table() {
504 let t = Table::new(vec![]);
505 assert_eq!(t.row_count(), 0);
506 assert_eq!(t.col_count(), 0);
507 assert!(t.is_empty());
508 }
509
510 #[test]
511 fn table_with_caption() {
512 let t = simple_table().with_caption(crate::caption::Caption::default());
513 assert!(t.caption.is_some());
514 }
515
516 #[test]
517 fn table_with_width() {
518 let t = simple_table().with_width(HwpUnit::from_mm(150.0).unwrap());
519 assert!(t.width.is_some());
520 }
521
522 #[test]
523 fn table_with_page_break() {
524 let t = simple_table().with_page_break(TablePageBreak::Table);
525 assert_eq!(t.page_break, TablePageBreak::Table);
526 }
527
528 #[test]
529 fn table_with_repeat_header_disabled() {
530 let t = simple_table().with_repeat_header(false);
531 assert!(!t.repeat_header);
532 }
533
534 #[test]
535 fn cell_new_defaults() {
536 let cell = simple_cell();
537 assert_eq!(cell.col_span, 1);
538 assert_eq!(cell.row_span, 1);
539 assert!(cell.height.is_none());
540 assert!(cell.background.is_none());
541 assert!(cell.border_fill_id.is_none());
542 assert!(cell.margin.is_none());
543 assert!(cell.vertical_align.is_none());
544 assert_eq!(cell.paragraphs.len(), 1);
545 }
546
547 #[test]
548 fn cell_with_span() {
549 let cell =
550 TableCell::with_span(vec![simple_paragraph()], HwpUnit::from_mm(100.0).unwrap(), 3, 2);
551 assert_eq!(cell.col_span, 3);
552 assert_eq!(cell.row_span, 2);
553 }
554
555 #[test]
556 fn cell_with_background() {
557 let cell = simple_cell().with_background(Color::from_rgb(200, 200, 200));
558 assert!(cell.background.is_some());
559 }
560
561 #[test]
562 fn table_display() {
563 let t = simple_table();
564 assert_eq!(t.to_string(), "Table(2x2)");
565 }
566
567 #[test]
568 fn single_cell_table() {
569 let table = Table::new(vec![TableRow::with_height(
570 vec![simple_cell()],
571 HwpUnit::from_mm(10.0).unwrap(),
572 )]);
573 assert_eq!(table.row_count(), 1);
574 assert_eq!(table.col_count(), 1);
575 }
576
577 #[test]
578 fn row_with_fixed_height() {
579 let row = TableRow::with_height(vec![simple_cell()], HwpUnit::from_mm(25.0).unwrap());
580 assert!(row.height.is_some());
581 }
582
583 #[test]
584 fn row_new_auto_height() {
585 let row = TableRow::new(vec![simple_cell(), simple_cell()]);
586 assert_eq!(row.cells.len(), 2);
587 assert!(row.height.is_none());
588 }
589
590 #[test]
591 fn row_new_empty_cells() {
592 let row = TableRow::new(vec![]);
593 assert!(row.cells.is_empty());
594 assert!(row.height.is_none());
595 }
596
597 #[test]
598 fn row_with_height_constructor() {
599 let h = HwpUnit::from_mm(20.0).unwrap();
600 let row = TableRow::with_height(vec![simple_cell()], h);
601 assert_eq!(row.cells.len(), 1);
602 assert_eq!(row.height, Some(h));
603 }
604
605 #[test]
606 fn equality() {
607 let a = simple_table();
608 let b = simple_table();
609 assert_eq!(a, b);
610 }
611
612 #[test]
613 fn clone_independence() {
614 let t = simple_table();
615 let mut cloned = t.clone();
616 cloned.caption = Some(crate::caption::Caption::default());
617 assert!(t.caption.is_none());
618 }
619
620 #[test]
621 fn serde_roundtrip() {
622 let t = simple_table();
623 let json = serde_json::to_string(&t).unwrap();
624 let back: Table = serde_json::from_str(&json).unwrap();
625 assert_eq!(t, back);
626 }
627
628 #[test]
629 fn serde_with_all_optional_fields() {
630 let mut t = simple_table()
631 .with_width(HwpUnit::from_mm(150.0).unwrap())
632 .with_caption(crate::caption::Caption::default())
633 .with_page_break(TablePageBreak::None)
634 .with_repeat_header(false)
635 .with_cell_spacing(HwpUnit::from_mm(2.0).unwrap())
636 .with_border_fill_id(7);
637 t.rows[0].height = Some(HwpUnit::from_mm(20.0).unwrap());
638 t.rows[0].cells[0] = t.rows[0].cells[0]
639 .clone()
640 .with_background(Color::from_rgb(255, 0, 0))
641 .with_height(HwpUnit::from_mm(8.0).unwrap())
642 .with_border_fill_id(9)
643 .with_margin(TableMargin {
644 left: HwpUnit::from_mm(1.0).unwrap(),
645 right: HwpUnit::from_mm(2.0).unwrap(),
646 top: HwpUnit::from_mm(0.5).unwrap(),
647 bottom: HwpUnit::from_mm(0.25).unwrap(),
648 })
649 .with_vertical_align(TableVerticalAlign::Bottom);
650
651 let json = serde_json::to_string(&t).unwrap();
652 let back: Table = serde_json::from_str(&json).unwrap();
653 assert_eq!(t, back);
654 }
655
656 #[test]
657 fn serde_defaults_missing_new_fields() {
658 let json = r#"{"rows":[],"width":null,"caption":null}"#;
659 let back: Table = serde_json::from_str(json).unwrap();
660 assert_eq!(back.page_break, TablePageBreak::Cell);
661 assert!(back.repeat_header);
662 assert!(back.cell_spacing.is_none());
663 assert!(back.border_fill_id.is_none());
664 }
665
666 #[test]
667 fn table_margin_defaults_to_zero() {
668 let margin = TableMargin::default();
669 assert_eq!(margin.left, HwpUnit::ZERO);
670 assert_eq!(margin.right, HwpUnit::ZERO);
671 assert_eq!(margin.top, HwpUnit::ZERO);
672 assert_eq!(margin.bottom, HwpUnit::ZERO);
673 }
674
675 #[test]
676 fn cell_zero_span_allowed_at_construction() {
677 let cell = TableCell::with_span(
679 vec![simple_paragraph()],
680 HwpUnit::from_mm(50.0).unwrap(),
681 0, 0,
683 );
684 assert_eq!(cell.col_span, 0);
685 assert_eq!(cell.row_span, 0);
686 }
687
688 #[test]
689 fn row_new_sets_expected_defaults() {
690 let cells = vec![simple_cell()];
691 let row = TableRow::new(cells.clone());
692 assert_eq!(row.cells, cells);
693 assert!(row.height.is_none());
694 assert!(!row.is_header);
695 }
696}