appcore_filemaker/resolved_table.rs
1// =============================================================================
2// #######
3// ### ### F: resolved_table.rs
4// ## ## ## ## P: AppCore-Runtime
5// ## ##
6// C: 2026/08/30 05:00:00 by dnettoRaw
7// ## ## ## ## U: 2026/08/30 05:00:00 by dnettoRaw
8// ########### S: 1.0.2-rc
9// =============================================================================
10
11use serde::{Deserialize, Serialize};
12
13use crate::{
14 ComputedStyle, ErrorCode, FileMakerError, Insets, Rect, ResolvedTableColumn, TextLayout,
15};
16
17/// One exporter-ready table cell with final geometry and shaped text.
18#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
19pub struct ResolvedTableCell {
20 /// Stable source field.
21 pub field: String,
22 /// Display text derived before export.
23 pub text: String,
24 /// Cell rectangle in page coordinates.
25 pub bounds: Rect,
26 /// Resolved per-side padding; absent fields in older serialized scenes use zero.
27 #[serde(default)]
28 pub padding: Insets,
29 /// Final style after the data-rule layer.
30 pub style: ComputedStyle,
31 /// Shaped text constrained to the cell.
32 pub text_layout: TextLayout,
33}
34
35impl ResolvedTableCell {
36 /// Returns the text rectangle after resolving the retained cell insets.
37 pub fn content_bounds(&self) -> crate::Result<Rect> {
38 let width = self
39 .bounds
40 .size
41 .width
42 .checked_sub(self.padding.left.checked_add(self.padding.right)?)?;
43 let height = self
44 .bounds
45 .size
46 .height
47 .checked_sub(self.padding.top.checked_add(self.padding.bottom)?)?;
48 if width <= crate::Unit::ZERO || height <= crate::Unit::ZERO {
49 return Err(FileMakerError::new(
50 ErrorCode::LayoutInvalid,
51 "resolved cell padding leaves no text content area",
52 ));
53 }
54 Rect::new(
55 self.bounds.origin.x.checked_add(self.padding.left)?,
56 self.bounds.origin.y.checked_add(self.padding.top)?,
57 width,
58 height,
59 )
60 }
61}
62
63/// One exporter-ready table row.
64#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
65pub struct ResolvedTableRow {
66 /// Zero-based source row index across fragments.
67 pub source_index: u64,
68 /// Row rectangle in page coordinates.
69 pub bounds: Rect,
70 /// Group key when this row begins a group.
71 pub group_start: Option<String>,
72 /// Final row style after ordered conditional rules.
73 pub style: ComputedStyle,
74 /// Cells in visual column order.
75 pub cells: Vec<ResolvedTableCell>,
76}
77
78/// One physical-page fragment of a first-class table.
79#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
80pub struct ResolvedTableFragment {
81 /// Zero-based fragment index.
82 pub index: usize,
83 /// Final column widths shared by every fragment.
84 pub columns: Vec<ResolvedTableColumn>,
85 /// Header cells, empty when the header is not repeated.
86 pub header: Vec<ResolvedTableCell>,
87 /// Bounded rows assigned to this fragment.
88 pub rows: Vec<ResolvedTableRow>,
89 /// Final exact totals row, empty before the last fragment.
90 pub totals: Vec<ResolvedTableCell>,
91 /// Group active at the first row of this fragment.
92 pub starting_group: Option<String>,
93}