use crate::Result;
use crate::constants::*;
use crate::error::TableError;
use crate::table::{ColumnWidth, Table};
use tracing::{debug, trace};
#[derive(Debug, Clone)]
pub struct TableLayout {
pub column_widths: Vec<f32>,
pub row_heights: Vec<f32>,
pub total_width: f32,
pub total_height: f32,
}
fn cell_is_bold(cell: &crate::table::Cell) -> bool {
cell.style.as_ref().map(|s| s.bold).unwrap_or(false)
}
fn metrics_for_cell<'a>(
table: &'a Table,
cell: &crate::table::Cell,
) -> Option<&'a dyn crate::font::FontMetrics> {
if cell_is_bold(cell) {
table
.bold_font_metrics
.as_ref()
.map(|m| m.as_ref())
.or(table.font_metrics.as_ref().map(|m| m.as_ref()))
} else {
table.font_metrics.as_ref().map(|m| m.as_ref())
}
}
pub fn calculate_layout(table: &Table) -> Result<TableLayout> {
table.validate()?;
debug!(
"Calculating layout for table with {} rows",
table.rows.len()
);
let available_width = table.total_width.unwrap_or_else(|| {
estimate_total_width(table)
});
let column_widths = if let Some(ref width_specs) = table.column_widths {
resolve_column_widths(width_specs, available_width, table)?
} else {
calculate_column_widths(table)?
};
let row_heights = calculate_row_heights(table, &column_widths)?;
let total_width = column_widths.iter().sum();
let total_height = row_heights.iter().sum();
trace!("Layout calculated: {}x{}", total_width, total_height);
Ok(TableLayout {
column_widths,
row_heights,
total_width,
total_height,
})
}
fn estimate_total_width(_table: &Table) -> f32 {
LETTER_WIDTH - (DEFAULT_MARGIN * 2.0)
}
fn resolve_column_widths(
specs: &[ColumnWidth],
available_width: f32,
table: &Table,
) -> Result<Vec<f32>> {
let mut resolved_widths = vec![0.0; specs.len()];
let mut total_fixed_width = 0.0;
let mut total_percentage = 0.0;
let mut auto_columns = Vec::new();
for (i, spec) in specs.iter().enumerate() {
match spec {
ColumnWidth::Pixels(width) => {
resolved_widths[i] = *width;
total_fixed_width += width;
}
ColumnWidth::Percentage(percent) => {
total_percentage += percent;
}
ColumnWidth::Auto => {
auto_columns.push(i);
}
}
}
let percentage_width = available_width * (total_percentage / 100.0);
let remaining_width = available_width - total_fixed_width - percentage_width;
for (i, spec) in specs.iter().enumerate() {
if let ColumnWidth::Percentage(percent) = spec {
resolved_widths[i] = available_width * (percent / 100.0);
}
}
if !auto_columns.is_empty() {
if remaining_width > 0.0 {
let mut auto_proportions = vec![0.0; auto_columns.len()];
let mut total_proportion = 0.0;
for (idx, &col) in auto_columns.iter().enumerate() {
let max_content_width = estimate_column_content_width(table, col);
auto_proportions[idx] = max_content_width;
total_proportion += max_content_width;
}
for (idx, &col) in auto_columns.iter().enumerate() {
if total_proportion > 0.0 {
resolved_widths[col] =
remaining_width * (auto_proportions[idx] / total_proportion);
} else {
resolved_widths[col] = remaining_width / auto_columns.len() as f32;
}
resolved_widths[col] = resolved_widths[col].max(MIN_COLUMN_WIDTH);
}
} else {
for &col in &auto_columns {
resolved_widths[col] = MIN_COLUMN_WIDTH;
}
}
}
trace!("Resolved column widths: {:?}", resolved_widths);
Ok(resolved_widths)
}
fn estimate_column_content_width(table: &Table, col_idx: usize) -> f32 {
let mut max_width = 0.0;
for row in &table.rows {
if col_idx < row.cells.len() {
let cell = &row.cells[col_idx];
let font_size = cell
.style
.as_ref()
.and_then(|s| s.font_size)
.unwrap_or(table.style.default_font_size);
let estimated_width = if let Some(metrics) = metrics_for_cell(table, cell) {
crate::drawing_utils::estimate_text_width_with_metrics(
&cell.content,
font_size,
metrics,
)
} else {
crate::drawing_utils::estimate_text_width(&cell.content, font_size)
};
max_width = f32::max(max_width, estimated_width);
}
}
let padding = table.style.padding.left + table.style.padding.right;
max_width + padding
}
fn calculate_column_widths(table: &Table) -> Result<Vec<f32>> {
let col_count = table.column_count();
if col_count == 0 {
return Err(TableError::LayoutError("No columns in table".to_string()));
}
let mut max_widths = vec![0.0; col_count];
for row in &table.rows {
for (i, cell) in row.cells.iter().enumerate() {
if i >= col_count {
break;
}
let font_size = cell
.style
.as_ref()
.and_then(|s| s.font_size)
.unwrap_or(table.style.default_font_size);
let estimated_width = if let Some(metrics) = metrics_for_cell(table, cell) {
crate::drawing_utils::estimate_text_width_with_metrics(
&cell.content,
font_size,
metrics,
)
} else {
crate::drawing_utils::estimate_text_width(&cell.content, font_size)
};
max_widths[i] = f32::max(max_widths[i], estimated_width);
}
}
let padding = table.style.padding.left + table.style.padding.right;
for width in &mut max_widths {
*width += padding;
*width = width.max(MIN_COLUMN_WIDTH);
}
trace!("Calculated column widths: {:?}", max_widths);
Ok(max_widths)
}
fn single_image_content_height(image: &crate::table::CellImage, available_width: f32) -> f32 {
if image.width_px == 0 || image.height_px == 0 {
return 0.0;
}
let aspect = image.aspect_ratio();
let mut height = available_width / aspect;
if let Some(max_h) = image.max_render_height_pts {
height = height.min(max_h);
}
height
}
fn images_content_height(images: &[crate::table::CellImage], available_width: f32) -> f32 {
if images.is_empty() {
return 0.0;
}
if images.len() == 1 {
return single_image_content_height(&images[0], available_width);
}
const IMAGE_GAP: f32 = 4.0;
let total_gap = IMAGE_GAP * (images.len() as f32 - 1.0);
let slot_w = (available_width - total_gap) / images.len() as f32;
images
.iter()
.map(|img| single_image_content_height(img, slot_w))
.fold(0.0f32, f32::max)
}
fn calculate_row_heights(table: &Table, column_widths: &[f32]) -> Result<Vec<f32>> {
let mut heights = Vec::with_capacity(table.rows.len());
for row in &table.rows {
if let Some(height) = row.height {
heights.push(height);
} else {
let mut max_height = 0.0;
for (i, cell) in row.cells.iter().enumerate() {
if i >= column_widths.len() {
break;
}
let padding = cell
.style
.as_ref()
.and_then(|s| s.padding.as_ref())
.unwrap_or(&table.style.padding);
let font_size = cell
.style
.as_ref()
.and_then(|s| s.font_size)
.unwrap_or(table.style.default_font_size);
let available_width = column_widths[i] - padding.left - padding.right;
let text_height = if cell.text_wrap {
if let Some(metrics) = metrics_for_cell(table, cell) {
crate::text::calculate_wrapped_text_height_with_metrics(
&cell.content,
available_width,
font_size,
DEFAULT_LINE_HEIGHT_MULTIPLIER,
metrics,
)
} else {
crate::text::calculate_wrapped_text_height(
&cell.content,
available_width,
font_size,
DEFAULT_LINE_HEIGHT_MULTIPLIER,
)
}
} else if !cell.content.is_empty() {
font_size_to_height(font_size)
} else {
0.0
};
let img_height = images_content_height(&cell.images, available_width);
max_height = f32::max(max_height, f32::max(text_height, img_height));
}
max_height += table.style.padding.top + table.style.padding.bottom;
max_height = max_height.max(font_size_to_height(table.style.default_font_size));
heights.push(max_height);
}
}
trace!("Calculated row heights: {:?}", heights);
Ok(heights)
}
fn font_size_to_height(font_size: f32) -> f32 {
font_size * DEFAULT_LINE_HEIGHT_MULTIPLIER
}
#[cfg(test)]
mod tests {
use super::*;
use crate::table::{Cell, Row};
#[test]
fn test_layout_calculation() {
let table = Table::new()
.add_row(Row::new(vec![
Cell::new("Short"),
Cell::new("Medium text"),
Cell::new("This is a longer piece of text"),
]))
.add_row(Row::new(vec![
Cell::new("A"),
Cell::new("B"),
Cell::new("C"),
]));
let layout = calculate_layout(&table).unwrap();
assert_eq!(layout.column_widths.len(), 3);
assert_eq!(layout.row_heights.len(), 2);
assert!(layout.total_width > 0.0);
assert!(layout.total_height > 0.0);
}
}