use std::collections::BTreeMap;
use zenith_core::{
Diagnostic, FontProvider, Node, ResolvedToken, Style, TableColumn, TableNode, TableRow,
};
use zenith_layout::RustybuzzEngine;
use super::table::{GridDims, compute_table_layout, place_cells};
use super::text::MeasureEnv;
use super::util::{resolve_geometry_px, resolve_property_dimension_px};
pub(crate) struct TableFlowAssignment {
pub(super) columns: Vec<TableColumn>,
pub(super) rows: Vec<TableRow>,
}
pub(crate) type TableFlowAssignments = BTreeMap<String, TableFlowAssignment>;
struct Member<'a> {
table: &'a TableNode,
w: f64,
h: f64,
gap: f64,
pad: f64,
}
fn member_box(table: &TableNode, resolved: &BTreeMap<String, ResolvedToken>) -> Option<(f64, f64)> {
resolve_geometry_px(table.x.as_ref(), resolved)?;
resolve_geometry_px(table.y.as_ref(), resolved)?;
Some((
resolve_geometry_px(table.w.as_ref(), resolved)?,
resolve_geometry_px(table.h.as_ref(), resolved)?,
))
}
fn collect_flows<'a>(
nodes: &'a [Node],
resolved: &BTreeMap<String, ResolvedToken>,
members: &mut BTreeMap<String, Vec<Member<'a>>>,
) {
for node in nodes {
match node {
Node::Table(t) => {
if let Some(flow_id) = &t.flows
&& let Some((w, h)) = member_box(t, resolved)
{
let gap = resolve_property_dimension_px(t.gap.as_ref(), resolved, 0.0).max(0.0);
let pad = resolve_property_dimension_px(t.cell_padding.as_ref(), resolved, 0.0)
.max(0.0);
members.entry(flow_id.clone()).or_default().push(Member {
table: t,
w,
h,
gap,
pad,
});
}
for row in &t.rows {
for cell in &row.cells {
collect_flows(&cell.children, resolved, members);
}
}
}
Node::Frame(f) => collect_flows(&f.children, resolved, members),
Node::Group(g) => collect_flows(&g.children, resolved, members),
Node::Rect(_)
| Node::Ellipse(_)
| Node::Line(_)
| Node::Text(_)
| Node::Code(_)
| Node::Image(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Instance(_)
| Node::Field(_)
| Node::Footnote(_)
| Node::Toc(_)
| Node::Shape(_)
| Node::Connector(_)
| Node::Pattern(_)
| Node::Chart(_)
| Node::Unknown(_) => {}
}
}
}
pub(super) fn resolve_table_flows<'a>(
doc: &'a zenith_core::Document,
resolved: &BTreeMap<String, ResolvedToken>,
style_map: &BTreeMap<&str, &Style>,
fonts: &dyn FontProvider,
engine: &RustybuzzEngine,
diagnostics: &mut Vec<Diagnostic>,
) -> TableFlowAssignments {
let mut members: BTreeMap<String, Vec<Member<'a>>> = BTreeMap::new();
for page in &doc.body.pages {
collect_flows(&page.children, resolved, &mut members);
}
let env = MeasureEnv {
resolved,
style_map,
fonts,
engine,
};
let mut assignments: TableFlowAssignments = BTreeMap::new();
for flow_members in members.values() {
distribute_one_flow(flow_members, env, diagnostics, &mut assignments);
}
assignments
}
fn block_height(row_natural: &[f64], start: usize, count: usize, gap: f64) -> f64 {
if count == 0 {
return 0.0;
}
let mut h = 0.0;
for i in 0..count {
h += row_natural.get(start + i).copied().unwrap_or(0.0);
}
h + gap * (count.saturating_sub(1)) as f64
}
fn rowspan_extent(body: &[TableRow], i: usize, remaining: usize) -> usize {
let span = body
.get(i)
.map(|r| {
r.cells
.iter()
.map(|c| c.rowspan.max(1) as usize)
.max()
.unwrap_or(1)
})
.unwrap_or(1);
span.clamp(1, remaining.max(1))
}
fn distribute_one_flow(
flow_members: &[Member<'_>],
env: MeasureEnv,
diagnostics: &mut Vec<Diagnostic>,
assignments: &mut TableFlowAssignments,
) {
let Some(source) = flow_members.first() else {
return;
};
let src = source.table;
let columns: Vec<TableColumn> = src.columns.clone();
let header_rows = (src.header_rows.unwrap_or(0) as usize).min(src.rows.len());
let header: Vec<TableRow> = src.rows.get(..header_rows).unwrap_or(&[]).to_vec();
let body: &[TableRow] = src.rows.get(header_rows..).unwrap_or(&[]);
let last_index = flow_members.len().saturating_sub(1);
let mut cursor = 0usize;
for (mi, member) in flow_members.iter().enumerate() {
let is_last = mi == last_index;
let col_count = columns.len().max(1);
let remaining_body: &[TableRow] = body.get(cursor..).unwrap_or(&[]);
let take = if is_last {
remaining_body.len()
} else {
let mut candidate: Vec<TableRow> = header.clone();
candidate.extend_from_slice(remaining_body);
let row_count = candidate.len();
if row_count == 0 {
0
} else {
let placed = place_cells(&candidate, col_count, row_count);
let layout = compute_table_layout(
&columns,
&placed,
GridDims {
col_count,
row_count,
gap: member.gap,
pad: member.pad,
table_w: member.w,
table_h: member.h,
},
env,
diagnostics,
header_rows,
src.header_style.as_deref(),
);
let row_natural = &layout.row_natural;
let header_h = block_height(row_natural, 0, header_rows, member.gap);
let avail = (member.h - 2.0 * member.pad).max(0.0);
let after_header_gap = if header_rows > 0 { member.gap } else { 0.0 };
let mut used = header_h + after_header_gap;
greedy_take(
remaining_body,
row_natural,
GreedyCtx {
header_rows,
gap: member.gap,
avail,
table_id: &src.id,
source_span: src.source_span,
},
&mut used,
diagnostics,
)
}
};
let take = take.min(remaining_body.len());
let mut rows: Vec<TableRow> = header.clone();
rows.extend_from_slice(remaining_body.get(..take).unwrap_or(&[]));
assignments.insert(
member.table.id.clone(),
TableFlowAssignment {
columns: columns.clone(),
rows,
},
);
cursor = cursor.saturating_add(take);
}
}
#[derive(Clone, Copy)]
struct GreedyCtx<'a> {
header_rows: usize,
gap: f64,
avail: f64,
table_id: &'a str,
source_span: Option<zenith_core::Span>,
}
fn greedy_take(
body: &[TableRow],
row_natural: &[f64],
gc: GreedyCtx<'_>,
used: &mut f64,
diagnostics: &mut Vec<Diagnostic>,
) -> usize {
let GreedyCtx {
header_rows,
gap,
avail,
table_id,
source_span,
} = gc;
let mut taken = 0usize;
while taken < body.len() {
let remaining = body.len() - taken;
let extent = rowspan_extent(body, taken, remaining);
let group_h = block_height(row_natural, header_rows + taken, extent, gap);
let lead_gap = if taken > 0 { gap } else { 0.0 };
if *used + lead_gap + group_h <= avail || taken == 0 {
if taken == 0 && *used + lead_gap + group_h > avail {
diagnostics.push(Diagnostic::advisory(
"table.flow_overflow",
format!(
"table flow '{table_id}': a rowspan group is taller than a \
continuation box; placed and clipped"
),
source_span,
Some(table_id.to_owned()),
));
}
*used += lead_gap + group_h;
taken += extent;
} else {
break;
}
}
taken.min(body.len())
}