1use crate::Result;
4use crate::constants::*;
5use crate::error::TableError;
6use crate::table::{ColumnWidth, Table};
7use tracing::{debug, trace};
8
9#[derive(Debug, Clone)]
11pub struct TableLayout {
12 pub column_widths: Vec<f32>,
13 pub row_heights: Vec<f32>,
14 pub total_width: f32,
15 pub total_height: f32,
16}
17
18fn cell_is_bold(cell: &crate::table::Cell) -> bool {
19 cell.style.as_ref().map(|s| s.bold).unwrap_or(false)
20}
21
22fn metrics_for_cell<'a>(
23 table: &'a Table,
24 cell: &crate::table::Cell,
25) -> Option<&'a dyn crate::font::FontMetrics> {
26 if cell_is_bold(cell) {
27 table
28 .bold_font_metrics
29 .as_ref()
30 .map(|m| m.as_ref())
31 .or(table.font_metrics.as_ref().map(|m| m.as_ref()))
32 } else {
33 table.font_metrics.as_ref().map(|m| m.as_ref())
34 }
35}
36
37pub fn calculate_layout(table: &Table) -> Result<TableLayout> {
39 table.validate()?;
40
41 debug!(
42 "Calculating layout for table with {} rows",
43 table.rows.len()
44 );
45
46 let available_width = table.total_width.unwrap_or_else(|| {
48 estimate_total_width(table)
50 });
51
52 let column_widths = if let Some(ref width_specs) = table.column_widths {
54 resolve_column_widths(width_specs, available_width, table)?
55 } else {
56 calculate_column_widths(table)?
57 };
58
59 let row_heights = calculate_row_heights(table, &column_widths)?;
61
62 let total_width = column_widths.iter().sum();
64 let total_height = row_heights.iter().sum();
65
66 trace!("Layout calculated: {}x{}", total_width, total_height);
67
68 Ok(TableLayout {
69 column_widths,
70 row_heights,
71 total_width,
72 total_height,
73 })
74}
75
76fn estimate_total_width(_table: &Table) -> f32 {
78 LETTER_WIDTH - (DEFAULT_MARGIN * 2.0)
81}
82
83fn resolve_column_widths(
85 specs: &[ColumnWidth],
86 available_width: f32,
87 table: &Table,
88) -> Result<Vec<f32>> {
89 let mut resolved_widths = vec![0.0; specs.len()];
90 let mut total_fixed_width = 0.0;
91 let mut total_percentage = 0.0;
92 let mut auto_columns = Vec::new();
93
94 for (i, spec) in specs.iter().enumerate() {
96 match spec {
97 ColumnWidth::Pixels(width) => {
98 resolved_widths[i] = *width;
99 total_fixed_width += width;
100 }
101 ColumnWidth::Percentage(percent) => {
102 total_percentage += percent;
103 }
104 ColumnWidth::Auto => {
105 auto_columns.push(i);
106 }
107 }
108 }
109
110 let percentage_width = available_width * (total_percentage / 100.0);
112 let remaining_width = available_width - total_fixed_width - percentage_width;
113
114 for (i, spec) in specs.iter().enumerate() {
116 if let ColumnWidth::Percentage(percent) = spec {
117 resolved_widths[i] = available_width * (percent / 100.0);
118 }
119 }
120
121 if !auto_columns.is_empty() {
123 if remaining_width > 0.0 {
124 let mut auto_proportions = vec![0.0; auto_columns.len()];
126 let mut total_proportion = 0.0;
127
128 for (idx, &col) in auto_columns.iter().enumerate() {
129 let max_content_width = estimate_column_content_width(table, col);
131 auto_proportions[idx] = max_content_width;
132 total_proportion += max_content_width;
133 }
134
135 for (idx, &col) in auto_columns.iter().enumerate() {
137 if total_proportion > 0.0 {
138 resolved_widths[col] =
139 remaining_width * (auto_proportions[idx] / total_proportion);
140 } else {
141 resolved_widths[col] = remaining_width / auto_columns.len() as f32;
142 }
143 resolved_widths[col] = resolved_widths[col].max(MIN_COLUMN_WIDTH);
145 }
146 } else {
147 for &col in &auto_columns {
149 resolved_widths[col] = MIN_COLUMN_WIDTH;
150 }
151 }
152 }
153
154 trace!("Resolved column widths: {:?}", resolved_widths);
155 Ok(resolved_widths)
156}
157
158fn estimate_column_content_width(table: &Table, col_idx: usize) -> f32 {
160 let mut max_width = 0.0;
161
162 for row in &table.rows {
163 if col_idx < row.cells.len() {
164 let cell = &row.cells[col_idx];
165 let font_size = cell
166 .style
167 .as_ref()
168 .and_then(|s| s.font_size)
169 .unwrap_or(table.style.default_font_size);
170
171 let estimated_width = if let Some(metrics) = metrics_for_cell(table, cell) {
172 crate::drawing_utils::estimate_text_width_with_metrics(
173 &cell.content,
174 font_size,
175 metrics,
176 )
177 } else {
178 crate::drawing_utils::estimate_text_width(&cell.content, font_size)
179 };
180 max_width = f32::max(max_width, estimated_width);
181 }
182 }
183
184 let padding = table.style.padding.left + table.style.padding.right;
186 max_width + padding
187}
188
189fn calculate_column_widths(table: &Table) -> Result<Vec<f32>> {
191 let col_count = table.column_count();
192 if col_count == 0 {
193 return Err(TableError::LayoutError("No columns in table".to_string()));
194 }
195
196 let mut max_widths = vec![0.0; col_count];
197
198 for row in &table.rows {
199 for (i, cell) in row.cells.iter().enumerate() {
200 if i >= col_count {
201 break;
202 }
203
204 let font_size = cell
205 .style
206 .as_ref()
207 .and_then(|s| s.font_size)
208 .unwrap_or(table.style.default_font_size);
209
210 let estimated_width = if let Some(metrics) = metrics_for_cell(table, cell) {
211 crate::drawing_utils::estimate_text_width_with_metrics(
212 &cell.content,
213 font_size,
214 metrics,
215 )
216 } else {
217 crate::drawing_utils::estimate_text_width(&cell.content, font_size)
218 };
219
220 max_widths[i] = f32::max(max_widths[i], estimated_width);
221 }
222 }
223
224 let padding = table.style.padding.left + table.style.padding.right;
226 for width in &mut max_widths {
227 *width += padding;
228 *width = width.max(MIN_COLUMN_WIDTH);
230 }
231
232 trace!("Calculated column widths: {:?}", max_widths);
233 Ok(max_widths)
234}
235
236fn single_image_content_height(image: &crate::table::CellImage, available_width: f32) -> f32 {
238 if image.width_px == 0 || image.height_px == 0 {
239 return 0.0;
240 }
241 let aspect = image.aspect_ratio();
242 let mut height = available_width / aspect;
244 if let Some(max_h) = image.max_render_height_pts {
246 height = height.min(max_h);
247 }
248 height
249}
250
251fn images_content_height(images: &[crate::table::CellImage], available_width: f32) -> f32 {
253 if images.is_empty() {
254 return 0.0;
255 }
256 if images.len() == 1 {
257 return single_image_content_height(&images[0], available_width);
258 }
259 const IMAGE_GAP: f32 = 4.0;
261 let total_gap = IMAGE_GAP * (images.len() as f32 - 1.0);
262 let slot_w = (available_width - total_gap) / images.len() as f32;
263 images
264 .iter()
265 .map(|img| single_image_content_height(img, slot_w))
266 .fold(0.0f32, f32::max)
267}
268
269fn calculate_row_heights(table: &Table, column_widths: &[f32]) -> Result<Vec<f32>> {
271 let mut heights = Vec::with_capacity(table.rows.len());
272
273 for row in &table.rows {
274 if let Some(height) = row.height {
275 heights.push(height);
276 } else {
277 let mut max_height = 0.0;
279
280 for (i, cell) in row.cells.iter().enumerate() {
281 if i >= column_widths.len() {
282 break;
283 }
284
285 let padding = cell
286 .style
287 .as_ref()
288 .and_then(|s| s.padding.as_ref())
289 .unwrap_or(&table.style.padding);
290
291 let font_size = cell
292 .style
293 .as_ref()
294 .and_then(|s| s.font_size)
295 .unwrap_or(table.style.default_font_size);
296
297 let available_width = column_widths[i] - padding.left - padding.right;
299
300 let text_height = if cell.text_wrap {
302 if let Some(metrics) = metrics_for_cell(table, cell) {
303 crate::text::calculate_wrapped_text_height_with_metrics(
304 &cell.content,
305 available_width,
306 font_size,
307 DEFAULT_LINE_HEIGHT_MULTIPLIER,
308 metrics,
309 )
310 } else {
311 crate::text::calculate_wrapped_text_height(
312 &cell.content,
313 available_width,
314 font_size,
315 DEFAULT_LINE_HEIGHT_MULTIPLIER,
316 )
317 }
318 } else if !cell.content.is_empty() {
319 font_size_to_height(font_size)
320 } else {
321 0.0
322 };
323
324 let img_height = images_content_height(&cell.images, available_width);
326
327 max_height = f32::max(max_height, f32::max(text_height, img_height));
328 }
329
330 max_height += table.style.padding.top + table.style.padding.bottom;
332 max_height = max_height.max(font_size_to_height(table.style.default_font_size));
334
335 heights.push(max_height);
336 }
337 }
338
339 trace!("Calculated row heights: {:?}", heights);
340 Ok(heights)
341}
342
343fn font_size_to_height(font_size: f32) -> f32 {
345 font_size * DEFAULT_LINE_HEIGHT_MULTIPLIER
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use crate::table::{Cell, Row};
353
354 #[test]
355 fn test_layout_calculation() {
356 let table = Table::new()
357 .add_row(Row::new(vec![
358 Cell::new("Short"),
359 Cell::new("Medium text"),
360 Cell::new("This is a longer piece of text"),
361 ]))
362 .add_row(Row::new(vec![
363 Cell::new("A"),
364 Cell::new("B"),
365 Cell::new("C"),
366 ]));
367
368 let layout = calculate_layout(&table).unwrap();
369
370 assert_eq!(layout.column_widths.len(), 3);
371 assert_eq!(layout.row_heights.len(), 2);
372 assert!(layout.total_width > 0.0);
373 assert!(layout.total_height > 0.0);
374 }
375}