1use serde::{Deserialize, Serialize};
2
3use super::{fixed_text::VerticalAlign, Element, RenderContext, RenderResult};
4use crate::{
5 compliance::ua::StructTag,
6 layout::TextAlign,
7 richtext::marks::AppliedStyle,
8 styles::RgbColor,
9};
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
15#[serde(rename_all = "snake_case")]
16pub enum RowHeight {
17 #[default]
19 Auto,
20 AtLeast(f64),
22 Exact(f64),
24}
25
26#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct CellBorders {
31 pub top: Option<CellBorder>,
32 pub bottom: Option<CellBorder>,
33 pub left: Option<CellBorder>,
34 pub right: Option<CellBorder>,
35}
36
37impl CellBorders {
38 pub fn is_empty(&self) -> bool {
39 self.top.is_none() && self.bottom.is_none()
40 && self.left.is_none() && self.right.is_none()
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct CellBorder {
47 pub width_mm: f64,
48 pub color: RgbColor,
49 pub style: BorderLineStyle,
50}
51
52impl Default for CellBorder {
53 fn default() -> Self {
54 Self {
55 width_mm: 0.3,
56 color: RgbColor { r: 0.8, g: 0.8, b: 0.8 },
57 style: BorderLineStyle::Solid,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, Default)]
64#[serde(rename_all = "snake_case")]
65pub enum BorderLineStyle {
66 #[default]
67 Solid,
68 Dashed,
69 Dotted,
70 None,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct CellPadding {
78 pub top_mm: f64,
79 pub bottom_mm: f64,
80 pub left_mm: f64,
81 pub right_mm: f64,
82}
83
84impl Default for CellPadding {
85 fn default() -> Self {
86 Self { top_mm: 1.0, bottom_mm: 1.0, left_mm: 2.0, right_mm: 2.0 }
87 }
88}
89
90impl CellPadding {
91 pub fn uniform(mm: f64) -> Self {
92 Self { top_mm: mm, bottom_mm: mm, left_mm: mm, right_mm: mm }
93 }
94
95 pub fn horizontal_vertical(h_mm: f64, v_mm: f64) -> Self {
96 Self { top_mm: v_mm, bottom_mm: v_mm, left_mm: h_mm, right_mm: h_mm }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct TableStyle {
105 pub outer_border: Option<CellBorder>,
107 pub inner_border: Option<CellBorder>,
109 pub header_background: Option<RgbColor>,
111 pub stripe_color: Option<RgbColor>,
113}
114
115impl TableStyle {
116 pub fn grid() -> Self {
118 Self {
119 outer_border: Some(CellBorder::default()),
120 inner_border: Some(CellBorder::default()),
121 header_background: Some(RgbColor { r: 0.85, g: 0.88, b: 0.95 }),
122 stripe_color: None,
123 }
124 }
125
126 pub fn bordered() -> Self {
128 Self {
129 outer_border: Some(CellBorder { width_mm: 0.5, ..CellBorder::default() }),
130 inner_border: None,
131 header_background: Some(RgbColor { r: 0.85, g: 0.88, b: 0.95 }),
132 stripe_color: Some(RgbColor { r: 0.96, g: 0.96, b: 0.96 }),
133 }
134 }
135
136 pub fn striped() -> Self {
138 Self {
139 outer_border: None,
140 inner_border: None,
141 header_background: Some(RgbColor { r: 0.85, g: 0.88, b: 0.95 }),
142 stripe_color: Some(RgbColor { r: 0.96, g: 0.96, b: 0.96 }),
143 }
144 }
145
146 pub fn plain() -> Self {
148 Self {
149 outer_border: None,
150 inner_border: None,
151 header_background: None,
152 stripe_color: None,
153 }
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct TableCell {
162 pub text: String,
163 #[serde(default = "default_span")]
165 pub col_span: u16,
166 #[serde(default = "default_span")]
168 pub row_span: u16,
169 #[serde(default)]
171 pub alignment: TextAlign,
172 #[serde(default)]
174 pub borders: CellBorders,
175 #[serde(default)]
177 pub background: Option<RgbColor>,
178 #[serde(default)]
180 pub vertical_align: VerticalAlign,
181 #[serde(default)]
183 pub padding: CellPadding,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub style_ref: Option<String>,
187 #[serde(skip)]
189 pub nested_table: Option<Box<Table>>,
190}
191
192fn default_span() -> u16 { 1 }
193
194impl TableCell {
195 pub fn new(text: impl Into<String>) -> Self {
196 Self {
197 text: text.into(),
198 col_span: 1,
199 row_span: 1,
200 alignment: TextAlign::Left,
201 borders: CellBorders::default(),
202 background: None,
203 vertical_align: VerticalAlign::Top,
204 padding: CellPadding::default(),
205 style_ref: None,
206 nested_table: None,
207 }
208 }
209
210 pub fn nested_table(mut self, table: Table) -> Self {
212 self.nested_table = Some(Box::new(table));
213 self
214 }
215
216 pub fn style(mut self, name: impl Into<String>) -> Self {
218 self.style_ref = Some(name.into());
219 self
220 }
221
222 pub fn padding(mut self, padding: CellPadding) -> Self {
223 self.padding = padding;
224 self
225 }
226
227 pub fn col_span(mut self, n: u16) -> Self {
228 self.col_span = n.max(1);
229 self
230 }
231
232 pub fn row_span(mut self, n: u16) -> Self {
233 self.row_span = n.max(1);
234 self
235 }
236
237 pub fn align(mut self, alignment: TextAlign) -> Self {
238 self.alignment = alignment;
239 self
240 }
241
242 pub fn background(mut self, color: RgbColor) -> Self {
243 self.background = Some(color);
244 self
245 }
246}
247
248impl From<String> for TableCell {
249 fn from(s: String) -> Self { Self::new(s) }
250}
251
252impl From<&str> for TableCell {
253 fn from(s: &str) -> Self { Self::new(s) }
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct TableRow {
261 pub cells: Vec<TableCell>,
262 pub height: RowHeight,
263 pub is_header: bool,
264}
265
266impl TableRow {
267 pub fn new(cells: Vec<TableCell>) -> Self {
268 Self { cells, height: RowHeight::Auto, is_header: false }
269 }
270
271 pub fn plain(cells: Vec<String>) -> Self {
273 Self::new(cells.into_iter().map(TableCell::from).collect())
274 }
275
276 pub fn height_exact(mut self, mm: f64) -> Self {
278 self.height = RowHeight::Exact(mm);
279 self
280 }
281
282 pub fn height_at_least(mut self, mm: f64) -> Self {
284 self.height = RowHeight::AtLeast(mm);
285 self
286 }
287}
288
289pub struct TableBuilder {
293 header_rows: Vec<TableRow>,
294 body_rows: Vec<TableRow>,
295 col_widths: Option<Vec<f64>>,
296 show_header_background: bool,
297 stripe_rows: bool,
298 table_style: Option<TableStyle>,
299}
300
301impl TableBuilder {
302 pub fn header_row(mut self, cells: Vec<TableCell>) -> Self {
303 let mut row = TableRow::new(cells);
304 row.is_header = true;
305 self.header_rows.push(row);
306 self
307 }
308
309 pub fn row(mut self, cells: Vec<TableCell>) -> Self {
310 self.body_rows.push(TableRow::new(cells));
311 self
312 }
313
314 pub fn col_widths(mut self, pcts: Vec<f64>) -> Self {
315 self.col_widths = Some(pcts);
316 self
317 }
318
319 pub fn stripe(mut self) -> Self {
320 self.stripe_rows = true;
321 self
322 }
323
324 pub fn table_style(mut self, style: TableStyle) -> Self {
326 self.table_style = Some(style);
327 self
328 }
329
330 pub fn build(self) -> Table {
331 Table {
332 headers: Vec::new(),
333 header_rows: self.header_rows,
334 rows: self.body_rows,
335 col_widths: self.col_widths,
336 show_header_background: self.show_header_background,
337 stripe_rows: self.stripe_rows,
338 table_style: self.table_style,
339 }
340 }
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct Table {
348 pub headers: Vec<String>,
350 #[serde(default)]
352 pub header_rows: Vec<TableRow>,
353 pub rows: Vec<TableRow>,
354 pub col_widths: Option<Vec<f64>>,
356 pub show_header_background: bool,
358 pub stripe_rows: bool,
360 #[serde(default)]
363 pub table_style: Option<TableStyle>,
364}
365
366impl Table {
367 pub fn new(headers: Vec<String>, rows: Vec<TableRow>) -> Self {
368 Self {
369 headers,
370 header_rows: Vec::new(),
371 rows,
372 col_widths: None,
373 show_header_background: true,
374 stripe_rows: true,
375 table_style: None,
376 }
377 }
378
379 pub fn builder() -> TableBuilder {
380 TableBuilder {
381 header_rows: Vec::new(),
382 body_rows: Vec::new(),
383 col_widths: None,
384 show_header_background: true,
385 stripe_rows: false,
386 table_style: None,
387 }
388 }
389
390 pub fn col_widths(mut self, widths: Vec<f64>) -> Self {
391 self.col_widths = Some(widths);
392 self
393 }
394
395 pub fn with_table_style(mut self, style: TableStyle) -> Self {
397 self.table_style = Some(style);
398 self
399 }
400
401 pub fn stripe(self) -> Self {
403 self
404 }
405
406 fn min_row_height_mm() -> f64 { 6.5 }
407
408 fn effective_row_height(row: &TableRow, measured: f64) -> f64 {
409 match row.height {
410 RowHeight::Auto => measured.max(Self::min_row_height_mm()),
411 RowHeight::AtLeast(h) => measured.max(h),
412 RowHeight::Exact(h) => h,
413 }
414 }
415
416 fn col_widths_mm(&self, usable_width: f64, col_count: usize) -> Vec<f64> {
418 if col_count == 0 { return Vec::new(); }
419 match &self.col_widths {
420 Some(pcts) => pcts.iter().map(|p| p / 100.0 * usable_width).collect(),
421 None => vec![usable_width / col_count as f64; col_count],
422 }
423 }
424
425 fn effective_col_count(&self) -> usize {
427 let mut max = self.headers.len();
428 for r in &self.header_rows {
429 let span_sum: usize = r.cells.iter().map(|c| c.col_span as usize).sum();
430 max = max.max(span_sum);
431 }
432 for r in &self.rows {
433 let span_sum: usize = r.cells.iter().map(|c| c.col_span as usize).sum();
434 max = max.max(span_sum);
435 }
436 max
437 }
438
439 fn measure_row_height(
441 &self,
442 row: &TableRow,
443 col_widths: &[f64],
444 ctx: &RenderContext,
445 ) -> f64 {
446 let fs = ctx.style.font_size_body;
447 let mut measured = Self::min_row_height_mm();
448 let col_count = col_widths.len();
449 let mut col_idx = 0;
450
451 for cell in &row.cells {
452 let span = (cell.col_span as usize).min(col_count.saturating_sub(col_idx));
453 if span == 0 { break; }
454 let w: f64 = col_widths[col_idx..col_idx + span].iter().sum();
455 let h_pad = cell.padding.left_mm + cell.padding.right_mm;
456 let v_pad = cell.padding.top_mm + cell.padding.bottom_mm;
457 let inner_w = (w - h_pad).max(1.0);
458 let r = ctx.layout_engine.layout_plain(
459 &ctx.fonts, &cell.text, inner_w, cell.alignment, fs,
460 AppliedStyle::default(),
461 );
462 measured = measured.max(r.total_height_mm + v_pad);
463 col_idx += span;
464 }
465
466 Self::effective_row_height(row, measured)
467 }
468
469 #[allow(clippy::too_many_arguments)]
471 fn render_row(
472 &self,
473 row: &TableRow,
474 col_widths: &[f64],
475 x_base: f64,
476 row_h: f64,
477 body_row_idx: usize,
478 is_header: bool,
479 ctx: &mut RenderContext,
480 ) {
481 let y_top = ctx.flow.cursor_y_mm;
482 let y_bottom = y_top - row_h;
483 let tc = ctx.style.text_color.clone();
484 let col_count = col_widths.len();
485 let total_w: f64 = col_widths.iter().sum();
486
487 let bg: Option<RgbColor> = if let Some(ref ts) = self.table_style {
489 if is_header {
490 ts.header_background.clone()
491 } else if body_row_idx % 2 == 1 {
492 ts.stripe_color.clone()
493 } else {
494 None
495 }
496 } else if is_header && self.show_header_background {
497 let pc = &ctx.style.primary_color;
498 Some(RgbColor {
499 r: pc.r * 0.85 + 0.15,
500 g: pc.g * 0.85 + 0.15,
501 b: pc.b * 0.85 + 0.15,
502 })
503 } else if !is_header && self.stripe_rows && body_row_idx % 2 == 1 {
504 Some(RgbColor { r: 0.96, g: 0.96, b: 0.96 })
505 } else {
506 None
507 };
508
509 if let Some(bg_col) = bg {
510 if ctx.ua_config.enabled { ctx.backend.begin_artifact_content(); }
511 let _ = ctx.backend.draw_rect(x_base, y_bottom, total_w, row_h, &bg_col);
512 if ctx.ua_config.enabled { ctx.backend.end_tagged_content(); }
513 }
514
515 let fs = ctx.style.font_size_body;
516 let mut col_x = x_base;
517 let mut col_idx = 0;
518
519 for cell in &row.cells {
520 let span = (cell.col_span as usize).min(col_count.saturating_sub(col_idx));
521 if span == 0 { break; }
522 let cell_w: f64 = col_widths[col_idx..col_idx + span].iter().sum();
523
524 if let Some(ref cell_bg) = cell.background {
526 if ctx.ua_config.enabled { ctx.backend.begin_artifact_content(); }
527 let _ = ctx.backend.draw_rect(col_x, y_bottom, cell_w, row_h, cell_bg);
528 if ctx.ua_config.enabled { ctx.backend.end_tagged_content(); }
529 }
530
531 let h_pad = cell.padding.left_mm + cell.padding.right_mm;
532 let inner_w = (cell_w - h_pad).max(1.0);
533
534 if let Some(ref nested) = cell.nested_table {
535 let saved_x = ctx.layout.content_x_mm;
536 let saved_w = ctx.layout.content_width_mm;
537 let saved_cursor = ctx.flow.cursor_y_mm;
538
539 ctx.layout.content_x_mm = col_x + cell.padding.left_mm;
540 ctx.layout.content_width_mm = inner_w;
541 ctx.flow.cursor_y_mm = y_top - cell.padding.top_mm;
542 ctx.resume_index = 0;
543
544 let _ = nested.render(ctx);
545
546 ctx.layout.content_x_mm = saved_x;
547 ctx.layout.content_width_mm = saved_w;
548 ctx.flow.cursor_y_mm = saved_cursor;
549 } else {
550 let result = ctx.layout_engine.layout_plain(
551 &ctx.fonts, &cell.text, inner_w, cell.alignment, fs,
552 AppliedStyle::default(),
553 );
554
555 let content_h = result.total_height_mm;
556 let text_y_start = match cell.vertical_align {
557 VerticalAlign::Top => y_top - cell.padding.top_mm,
558 VerticalAlign::Middle => {
559 let inner_h = row_h - cell.padding.top_mm - cell.padding.bottom_mm;
560 y_top - cell.padding.top_mm - ((inner_h - content_h) / 2.0).max(0.0)
561 }
562 VerticalAlign::Bottom => y_bottom + cell.padding.bottom_mm + content_h,
563 };
564
565 let mut line_y = text_y_start;
566 for line in &result.lines {
567 if line_y - line.height_mm < y_bottom + cell.padding.bottom_mm {
568 break;
569 }
570 for seg in &line.segments {
571 if seg.text.is_empty() { continue; }
572 let Some(font_ref) = ctx.get_font_ref(seg.style.bold, seg.style.italic) else { continue };
573 let x = col_x + cell.padding.left_mm + seg.x_offset_mm;
574 let _ = ctx.draw_text(&seg.text, x, line_y, fs, font_ref, &tc);
575 }
576 line_y -= line.height_mm;
577 }
578 }
579
580 if !cell.borders.is_empty() {
582 draw_cell_borders(ctx, &cell.borders, col_x, y_bottom, cell_w, row_h);
583 }
584
585 col_x += cell_w;
586 col_idx += span;
587 }
588
589 let row_border: Option<(f32, RgbColor)> = match &self.table_style {
592 Some(ts) => ts.inner_border.as_ref().map(|b| {
593 ((b.width_mm * 72.0 / 25.4) as f32, b.color.clone())
594 }),
595 None => Some((0.3_f32, RgbColor { r: 0.75, g: 0.75, b: 0.75 })),
596 };
597 if let Some((pt, color)) = row_border {
598 if ctx.ua_config.enabled { ctx.backend.begin_artifact_content(); }
599 let _ = ctx.backend.draw_line(x_base, y_bottom, x_base + total_w, y_bottom, pt, &color);
600 if ctx.ua_config.enabled { ctx.backend.end_tagged_content(); }
601 }
602 }
603}
604
605fn draw_cell_borders(
608 ctx: &mut RenderContext,
609 borders: &CellBorders,
610 x: f64, y: f64, w: f64, h: f64,
611) {
612 let ua = ctx.ua_config.enabled;
613 if ua { ctx.backend.begin_artifact_content(); }
614 let draw_h = |ctx: &mut RenderContext, border: &CellBorder, bx: f64, by: f64, len: f64| {
615 let pt = (border.width_mm * 72.0 / 25.4) as f32;
616 let _ = ctx.backend.draw_line(bx, by, bx + len, by, pt, &border.color);
617 };
618 let draw_v = |ctx: &mut RenderContext, border: &CellBorder, bx: f64, by: f64, len: f64| {
619 let pt = (border.width_mm * 72.0 / 25.4) as f32;
620 let _ = ctx.backend.draw_line(bx, by, bx, by + len, pt, &border.color);
621 };
622 if let Some(ref b) = borders.top { draw_h(ctx, b, x, y + h, w); }
623 if let Some(ref b) = borders.bottom { draw_h(ctx, b, x, y, w); }
624 if let Some(ref b) = borders.left { draw_v(ctx, b, x, y, h); }
625 if let Some(ref b) = borders.right { draw_v(ctx, b, x + w, y, h); }
626 if ua { ctx.backend.end_tagged_content(); }
627}
628
629fn ua_tag_row(ctx: &mut RenderContext, is_header: bool) {
632 let mcid = ctx.next_mcid();
633 let cell_tag = if is_header { StructTag::TH } else { StructTag::TD };
634 ctx.ua_begin_group(StructTag::TR, None);
635 ctx.ua_begin_group(cell_tag, None);
636 ctx.ua_content_ref(mcid);
637 ctx.ua_end_group();
638 ctx.ua_end_group();
639 ctx.backend.begin_tagged_content(b"TR", mcid);
640}
641
642impl Element for Table {
645 fn estimated_height_mm(&self) -> f64 {
646 let header_h = if self.headers.is_empty() && self.header_rows.is_empty() {
647 0.0
648 } else {
649 Self::min_row_height_mm()
650 };
651 let rows_h: f64 = self.rows.iter().map(|r| match r.height {
652 RowHeight::Exact(h) => h,
653 RowHeight::AtLeast(h) => h.max(Self::min_row_height_mm()),
654 RowHeight::Auto => Self::min_row_height_mm(),
655 }).sum();
656 header_h + rows_h
657 }
658
659 fn render(&self, ctx: &mut RenderContext) -> crate::Result<RenderResult> {
660 let col_count = self.effective_col_count();
661 if col_count == 0 {
662 return Ok(RenderResult::done());
663 }
664
665 let usable_w = ctx.layout.content_width_mm;
666 let x_base = ctx.layout.content_x_mm;
667 let col_widths = self.col_widths_mm(usable_w, col_count);
668 let start = ctx.resume_index;
669 let ua = ctx.ua_config.enabled;
670
671 let header_rows: Vec<&TableRow> = if !self.header_rows.is_empty() {
672 self.header_rows.iter().collect()
673 } else {
674 Vec::new()
675 };
676 let simple_headers = self.header_rows.is_empty() && !self.headers.is_empty();
677 let hdr_count = if simple_headers { 1 } else { header_rows.len() };
678
679 let has_headers = simple_headers || !header_rows.is_empty();
680
681 if ua && start == 0 {
682 ctx.ua_begin_group(StructTag::Table, None);
683 if has_headers { ctx.ua_begin_group(StructTag::THead, None); }
684 }
685
686 if start > 0 {
688 if simple_headers {
689 let hdr_row = TableRow {
690 cells: self.headers.iter().map(TableCell::new).collect(),
691 height: RowHeight::Auto,
692 is_header: true,
693 };
694 let row_h = self.measure_row_height(&hdr_row, &col_widths, ctx);
695 if ua { ua_tag_row(ctx, true); }
696 self.render_row(&hdr_row, &col_widths, x_base, row_h, 0, true, ctx);
697 if ua { ctx.backend.end_tagged_content(); }
698 ctx.flow.advance(row_h);
699 } else {
700 for hdr in &header_rows {
701 let row_h = self.measure_row_height(hdr, &col_widths, ctx);
702 if ua { ua_tag_row(ctx, true); }
703 self.render_row(hdr, &col_widths, x_base, row_h, 0, true, ctx);
704 if ua { ctx.backend.end_tagged_content(); }
705 ctx.flow.advance(row_h);
706 }
707 }
708 }
709
710 if start == 0 {
712 if simple_headers {
713 let hdr_row = TableRow {
714 cells: self.headers.iter().map(TableCell::new).collect(),
715 height: RowHeight::Auto,
716 is_header: true,
717 };
718 let row_h = self.measure_row_height(&hdr_row, &col_widths, ctx);
719 if ua { ua_tag_row(ctx, true); }
720 self.render_row(&hdr_row, &col_widths, x_base, row_h, 0, true, ctx);
721 if ua { ctx.backend.end_tagged_content(); }
722 ctx.flow.advance(row_h);
723 } else {
724 for hdr in &header_rows {
725 let row_h = self.measure_row_height(hdr, &col_widths, ctx);
726 if ua { ua_tag_row(ctx, true); }
727 self.render_row(hdr, &col_widths, x_base, row_h, 0, true, ctx);
728 if ua { ctx.backend.end_tagged_content(); }
729 ctx.flow.advance(row_h);
730 }
731 }
732 }
733
734 if ua && start == 0 {
735 if has_headers { ctx.ua_end_group(); } ctx.ua_begin_group(StructTag::TBody, None);
737 }
738
739 let body_start = start.saturating_sub(hdr_count);
741
742 for (i, row) in self.rows.iter().enumerate().skip(body_start) {
743 let row_h = self.measure_row_height(row, &col_widths, ctx);
744
745 if ctx.flow.would_overflow(row_h) && i > body_start {
746 ctx.resume_index = hdr_count + i;
747 if ua {
748 ctx.ua_end_group(); ctx.ua_end_group(); }
751 return Ok(RenderResult::more());
752 }
753
754 if ua { ua_tag_row(ctx, false); }
755 self.render_row(row, &col_widths, x_base, row_h, i, false, ctx);
756 if ua { ctx.backend.end_tagged_content(); }
757 ctx.flow.advance(row_h);
758 }
759
760 if ua {
761 ctx.ua_end_group(); ctx.ua_end_group(); }
764 Ok(RenderResult::done())
765 }
766}