Skip to main content

fop_layout/layout/table/
column.rs

1//! Column width calculation algorithms (fixed and auto layout)
2
3use fop_types::Length;
4
5use super::types::{BorderCollapse, ColumnInfo, ColumnWidth, GridCell, TableLayout};
6
7impl TableLayout {
8    /// Compute column widths using fixed layout algorithm
9    pub fn compute_fixed_widths(&self, columns: &[ColumnWidth]) -> Vec<Length> {
10        let n_cols = columns.len();
11        if n_cols == 0 {
12            return Vec::new();
13        }
14
15        // For collapsed borders, there's no spacing between cells
16        let total_spacing = match self.border_collapse {
17            BorderCollapse::Separate => self.border_spacing * (n_cols + 1) as i32,
18            BorderCollapse::Collapse => Length::ZERO,
19        };
20        let available_for_cols = self.available_width - total_spacing;
21
22        // Count proportional units
23        let total_proportional: f64 = columns
24            .iter()
25            .filter_map(|w| {
26                if let ColumnWidth::Proportional(p) = w {
27                    Some(*p)
28                } else {
29                    None
30                }
31            })
32            .sum();
33
34        // Calculate fixed total
35        let fixed_total: Length = columns
36            .iter()
37            .filter_map(|w| {
38                if let ColumnWidth::Fixed(len) = w {
39                    Some(*len)
40                } else {
41                    None
42                }
43            })
44            .fold(Length::ZERO, |acc, len| acc + len);
45
46        // Remaining width for proportional/auto
47        let remaining = available_for_cols - fixed_total;
48
49        // Compute each column width
50        columns
51            .iter()
52            .map(|spec| match spec {
53                ColumnWidth::Fixed(len) => *len,
54                ColumnWidth::Proportional(p) => {
55                    if total_proportional > 0.0 {
56                        Length::from_pt(remaining.to_pt() * p / total_proportional)
57                    } else {
58                        Length::ZERO
59                    }
60                }
61                ColumnWidth::Auto => {
62                    // In fixed layout, auto gets equal share of remaining
63                    let auto_count = columns
64                        .iter()
65                        .filter(|w| matches!(w, ColumnWidth::Auto))
66                        .count();
67                    if auto_count > 0 {
68                        remaining / auto_count as i32
69                    } else {
70                        Length::ZERO
71                    }
72                }
73            })
74            .collect()
75    }
76
77    /// Compute column widths using auto layout algorithm
78    ///
79    /// This implements the CSS2.1 automatic table layout algorithm:
80    /// 1. Calculate minimum and maximum width for each column based on content
81    /// 2. Assign fixed widths first
82    /// 3. Distribute remaining width to auto columns based on their min/max widths
83    /// 4. Handle proportional widths
84    ///
85    /// Algorithm details:
86    /// - **Fixed columns**: Use their specified width
87    /// - **Proportional columns**: Distribute remaining width by ratio
88    /// - **Auto columns**: Use content-based min/max widths
89    ///   - If space >= total max: Use max widths (no line breaking)
90    ///   - If min <= space < max: Interpolate between min and max
91    ///   - If space < min: Scale down from min (may overflow)
92    ///
93    /// This matches Apache FOP's AutoLayoutAlgorithm.java behavior.
94    pub fn compute_auto_widths(&self, column_info: &[ColumnInfo]) -> Vec<Length> {
95        let n_cols = column_info.len();
96        if n_cols == 0 {
97            return Vec::new();
98        }
99
100        // For collapsed borders, there's no spacing between cells
101        let total_spacing = match self.border_collapse {
102            BorderCollapse::Separate => self.border_spacing * (n_cols + 1) as i32,
103            BorderCollapse::Collapse => Length::ZERO,
104        };
105        let available_for_cols = self.available_width - total_spacing;
106
107        let mut widths = vec![Length::ZERO; n_cols];
108
109        // Step 1: Assign fixed widths
110        let mut fixed_total = Length::ZERO;
111        let mut auto_count = 0;
112        let mut proportional_count = 0;
113
114        for (i, info) in column_info.iter().enumerate() {
115            match info.width_spec {
116                ColumnWidth::Fixed(len) => {
117                    widths[i] = len;
118                    fixed_total += len;
119                }
120                ColumnWidth::Auto => {
121                    auto_count += 1;
122                }
123                ColumnWidth::Proportional(_) => {
124                    proportional_count += 1;
125                }
126            }
127        }
128
129        let remaining = available_for_cols - fixed_total;
130
131        // Step 2: Handle proportional columns
132        if proportional_count > 0 {
133            let total_proportional: f64 = column_info
134                .iter()
135                .filter_map(|info| {
136                    if let ColumnWidth::Proportional(p) = info.width_spec {
137                        Some(p)
138                    } else {
139                        None
140                    }
141                })
142                .sum();
143
144            if total_proportional > 0.0 {
145                for (i, info) in column_info.iter().enumerate() {
146                    if let ColumnWidth::Proportional(p) = info.width_spec {
147                        widths[i] = Length::from_pt(remaining.to_pt() * p / total_proportional);
148                    }
149                }
150                return widths;
151            }
152        }
153
154        // Step 3: Auto layout algorithm
155        if auto_count > 0 {
156            // Calculate total min and max widths for auto columns
157            let mut total_min = Length::ZERO;
158            let mut total_max = Length::ZERO;
159
160            for info in column_info.iter() {
161                if matches!(info.width_spec, ColumnWidth::Auto) {
162                    total_min += info.min_width;
163                    total_max += info.max_width;
164                }
165            }
166
167            // Distribute remaining width based on min/max constraints
168            if remaining >= total_max {
169                // Plenty of space - use max widths
170                for (i, info) in column_info.iter().enumerate() {
171                    if matches!(info.width_spec, ColumnWidth::Auto) {
172                        widths[i] = info.max_width;
173                    }
174                }
175            } else if remaining >= total_min {
176                // Between min and max - distribute proportionally
177                let range = total_max - total_min;
178                if range > Length::ZERO {
179                    for (i, info) in column_info.iter().enumerate() {
180                        if matches!(info.width_spec, ColumnWidth::Auto) {
181                            let col_range = info.max_width - info.min_width;
182                            let ratio = col_range.to_pt() / range.to_pt();
183                            let extra = Length::from_pt((remaining - total_min).to_pt() * ratio);
184                            widths[i] = info.min_width + extra;
185                        }
186                    }
187                } else {
188                    // All columns have same min/max - distribute equally
189                    let per_col = remaining / auto_count;
190                    for (i, info) in column_info.iter().enumerate() {
191                        if matches!(info.width_spec, ColumnWidth::Auto) {
192                            widths[i] = per_col;
193                        }
194                    }
195                }
196            } else {
197                // Not enough space - use min widths and scale down if needed
198                if total_min > Length::ZERO {
199                    let scale = remaining.to_pt() / total_min.to_pt();
200                    for (i, info) in column_info.iter().enumerate() {
201                        if matches!(info.width_spec, ColumnWidth::Auto) {
202                            widths[i] = Length::from_pt(info.min_width.to_pt() * scale);
203                        }
204                    }
205                } else {
206                    // Distribute equally
207                    let per_col = remaining / auto_count;
208                    for (i, info) in column_info.iter().enumerate() {
209                        if matches!(info.width_spec, ColumnWidth::Auto) {
210                            widths[i] = per_col;
211                        }
212                    }
213                }
214            }
215        }
216
217        widths
218    }
219
220    /// Measure content widths for cells in a column
221    /// Returns (min_width, max_width) for the column
222    pub fn measure_column_widths(
223        &self,
224        grid: &[Vec<Option<GridCell>>],
225        col_idx: usize,
226    ) -> (Length, Length) {
227        let mut min_width = Length::ZERO;
228        let mut max_width = Length::ZERO;
229
230        for row in grid {
231            if let Some(Some(cell)) = row.get(col_idx) {
232                // Only process actual cells (not span markers)
233                if cell.rowspan > 0 && cell.colspan > 0 {
234                    // For now, use simple heuristics
235                    // In a full implementation, this would measure actual text content
236                    let cell_min = Length::from_pt(30.0); // Minimum cell width
237                    let cell_max = Length::from_pt(200.0); // Maximum preferred width
238
239                    min_width = min_width.max(cell_min);
240                    max_width = max_width.max(cell_max);
241                }
242            }
243        }
244
245        // Ensure max >= min
246        if max_width < min_width {
247            max_width = min_width;
248        }
249
250        (min_width, max_width)
251    }
252
253    /// Update column info with measured widths from grid
254    pub fn update_column_info_from_grid(
255        &self,
256        column_info: &mut [ColumnInfo],
257        grid: &[Vec<Option<GridCell>>],
258    ) {
259        for (col_idx, info) in column_info.iter_mut().enumerate() {
260            if matches!(info.width_spec, ColumnWidth::Auto) {
261                let (min, max) = self.measure_column_widths(grid, col_idx);
262                info.min_width = min;
263                info.max_width = max;
264            }
265        }
266    }
267
268    /// Handle cells with colspan in width calculation
269    /// This distributes the cell's required width across spanned columns
270    pub fn distribute_colspan_widths(
271        &self,
272        column_info: &mut [ColumnInfo],
273        grid: &[Vec<Option<GridCell>>],
274    ) {
275        for row in grid {
276            for (col_idx, cell_opt) in row.iter().enumerate() {
277                if let Some(cell) = cell_opt {
278                    // Process cells with colspan > 1
279                    if cell.rowspan > 0 && cell.colspan > 1 && cell.col == col_idx {
280                        let end_col = (cell.col + cell.colspan).min(column_info.len());
281
282                        // Calculate total width of spanned columns
283                        let mut total_min = Length::ZERO;
284                        let mut auto_cols = 0;
285
286                        for i in cell.col..end_col {
287                            if let Some(info) = column_info.get(i) {
288                                if matches!(info.width_spec, ColumnWidth::Auto) {
289                                    total_min += info.min_width;
290                                    auto_cols += 1;
291                                }
292                            }
293                        }
294
295                        // For colspan cells, ensure minimum width
296                        // In a full implementation, this would measure the cell content
297                        let cell_min = Length::from_pt(50.0 * cell.colspan as f64);
298
299                        if cell_min > total_min && auto_cols > 0 {
300                            // Distribute extra width needed across auto columns
301                            let extra_per_col = (cell_min - total_min) / auto_cols;
302
303                            for i in cell.col..end_col {
304                                if let Some(info) = column_info.get_mut(i) {
305                                    if matches!(info.width_spec, ColumnWidth::Auto) {
306                                        info.min_width += extra_per_col;
307                                        info.max_width = info.max_width.max(info.min_width);
308                                    }
309                                }
310                            }
311                        }
312                    }
313                }
314            }
315        }
316    }
317}