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