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