appcore_filemaker/
table_columns.rs1use crate::{ColumnWidth, Dataset, ErrorCode, FileMakerError, Insets, Result, TableSpec, Unit};
14
15#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
17pub struct ResolvedTableColumn {
18 pub field: String,
20 pub header: String,
22 pub width: Unit,
24 pub align_x: crate::Alignment,
26 #[serde(default)]
28 pub padding: Insets,
29}
30
31pub fn resolve_table_columns(
33 spec: &TableSpec,
34 dataset: &dyn Dataset,
35 available_width: Unit,
36 logical_unit: Unit,
37 measure: &mut dyn FnMut(&str) -> Result<Unit>,
38) -> Result<Vec<ResolvedTableColumn>> {
39 spec.validate()?;
40 if available_width <= Unit::ZERO || logical_unit <= Unit::ZERO {
41 return Err(column_error("table column dimensions must be positive"));
42 }
43 let mut widths = Vec::with_capacity(spec.columns.len());
44 let mut flex_total = 0_u64;
45 for column in &spec.columns {
46 let width = match column.width {
47 ColumnWidth::Fixed(length) => length
48 .resolve(available_width, logical_unit)?
49 .ok_or_else(|| column_error("fixed table column cannot be auto"))?,
50 ColumnWidth::Auto => checked_measure(measure, &column.header)?
51 .checked_add(horizontal_padding(column, logical_unit)?)?,
52 ColumnWidth::Flex(weight) => {
53 flex_total = flex_total
54 .checked_add(u64::from(weight))
55 .ok_or_else(|| column_error("table flex weight overflow"))?;
56 Unit::ZERO
57 }
58 };
59 if !matches!(column.width, ColumnWidth::Flex(_)) && width <= Unit::ZERO {
60 return Err(column_error(
61 "fixed and automatic column widths must be positive",
62 ));
63 }
64 widths.push(width);
65 }
66 if spec
67 .columns
68 .iter()
69 .any(|column| matches!(column.width, ColumnWidth::Auto))
70 {
71 spec.visit_bounded_until(dataset, &mut |index, row| {
72 for (column_index, column) in spec.columns.iter().enumerate() {
73 if matches!(column.width, ColumnWidth::Auto) {
74 let value = row
75 .get(&column.field)
76 .map_or_else(String::new, crate::DataValue::display);
77 widths[column_index] = widths[column_index].max(
78 checked_measure(measure, &value)?
79 .checked_add(horizontal_padding(column, logical_unit)?)?,
80 );
81 }
82 }
83 Ok(index + 1 < u64::try_from(spec.auto_sample_rows).unwrap_or(u64::MAX))
84 })?;
85 }
86 let fixed_total = widths
87 .iter()
88 .try_fold(Unit::ZERO, |total, width| total.checked_add(*width))?;
89 let remaining = available_width.checked_sub(fixed_total)?;
90 if remaining < Unit::ZERO {
91 return Err(column_error(
92 "fixed and automatic columns exceed table width",
93 ));
94 }
95 if flex_total > 0 {
96 distribute_flex(spec, &mut widths, remaining, flex_total)?;
97 }
98 if widths.iter().any(|width| *width <= Unit::ZERO) {
99 return Err(column_error(
100 "resolved table columns must have positive width",
101 ));
102 }
103 spec.columns
104 .iter()
105 .zip(widths)
106 .map(|(column, width)| {
107 Ok(ResolvedTableColumn {
108 field: column.field.clone(),
109 header: column.header.clone(),
110 width,
111 align_x: column.align_x,
112 padding: column.padding.resolve(logical_unit)?,
113 })
114 })
115 .collect::<Result<Vec<_>>>()
116}
117
118fn horizontal_padding(column: &crate::TableColumn, logical_unit: Unit) -> Result<Unit> {
119 let padding = column.padding.resolve(logical_unit)?;
120 padding.left.checked_add(padding.right)
121}
122
123fn distribute_flex(
124 spec: &TableSpec,
125 widths: &mut [Unit],
126 remaining: Unit,
127 flex_total: u64,
128) -> Result<()> {
129 let last_flex = spec
130 .columns
131 .iter()
132 .rposition(|column| matches!(column.width, ColumnWidth::Flex(_)))
133 .ok_or_else(|| column_error("flex total has no flex column"))?;
134 let mut assigned = Unit::ZERO;
135 for (index, column) in spec.columns.iter().enumerate() {
136 let ColumnWidth::Flex(weight) = column.width else {
137 continue;
138 };
139 let width = if index == last_flex {
140 remaining.checked_sub(assigned)?
141 } else {
142 Unit::from_ratio(
143 i128::from(remaining.raw()) * i128::from(weight),
144 i128::from(flex_total) * i128::from(Unit::PER_POINT),
145 )?
146 };
147 widths[index] = width;
148 assigned = assigned.checked_add(width)?;
149 }
150 Ok(())
151}
152
153fn checked_measure(measure: &mut dyn FnMut(&str) -> Result<Unit>, value: &str) -> Result<Unit> {
154 let width = measure(value)?;
155 if width < Unit::ZERO {
156 return Err(column_error("table text measurement cannot be negative"));
157 }
158 Ok(width)
159}
160
161fn column_error(message: impl Into<String>) -> FileMakerError {
162 FileMakerError::new(ErrorCode::LayoutInvalid, message)
163}