use std::collections::BTreeMap;
use zenith_core::{
Diagnostic, FontProvider, Node, PropertyValue, ResolvedToken, Style, TableColumn, TableNode,
TableRow,
};
use zenith_layout::RustybuzzEngine;
use crate::ir::{Paint, SceneCommand};
use super::super::anchor::AnchorMap;
use super::super::chain::ChainAssignments;
use super::super::field::FieldCtx;
use super::super::paint::resolve_property_color;
use super::super::table_flow::TableFlowAssignments;
use super::super::text::{MeasureEnv, empty_md_blocks, measure_text_wrapped_height};
use super::super::util::{resolve_geometry_px, resolve_property_dimension_px};
use super::super::{ComponentMap, NodeCtx, RenderCtx, compile_node};
use super::collapse::{EdgeKey, EdgeStyle, accumulate_cell_edges, resolve_border_width};
use super::layout::{
GridDims, TableLayout, cached_families, compute_table_layout, header_styled_text,
};
use super::place::{CellRect, child_declared_box, place_cells};
#[derive(Clone, Copy)]
pub(in crate::compile) struct TableEmitCtx<'a> {
pub(in crate::compile) table: &'a TableNode,
pub(in crate::compile) resolved: &'a BTreeMap<String, ResolvedToken>,
pub(in crate::compile) style_map: &'a BTreeMap<&'a str, &'a Style>,
pub(in crate::compile) components: &'a ComponentMap<'a>,
pub(in crate::compile) fonts: &'a dyn FontProvider,
pub(in crate::compile) engine: &'a RustybuzzEngine,
pub(in crate::compile) chains: &'a ChainAssignments,
pub(in crate::compile) flows: &'a TableFlowAssignments,
pub(in crate::compile) anchors: &'a AnchorMap,
pub(in crate::compile) field_ctx: &'a FieldCtx<'a>,
}
impl<'a> TableEmitCtx<'a> {
fn measure_env(&self) -> MeasureEnv<'a> {
MeasureEnv {
resolved: self.resolved,
style_map: self.style_map,
fonts: self.fonts,
engine: self.engine,
}
}
fn node_ctx(&self) -> NodeCtx<'a> {
NodeCtx {
resolved: self.resolved,
style_map: self.style_map,
components: self.components,
fonts: self.fonts,
engine: self.engine,
chains: self.chains,
flows: self.flows,
anchors: self.anchors,
field_ctx: self.field_ctx,
md_blocks: empty_md_blocks(),
page_block_styles: &[],
doc_block_styles: &[],
}
}
}
#[derive(Clone, Copy)]
struct CellEmit {
pad: f64,
opacity: f64,
is_header: bool,
}
pub(in crate::compile) fn compile_table(
cx: TableEmitCtx,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
ctx: RenderCtx,
) {
let table = cx.table;
if table.visible == Some(false) {
return;
}
let flow = cx.flows.get(&table.id);
let rows: &[TableRow] = flow.map(|a| a.rows.as_slice()).unwrap_or(&table.rows);
let columns: &[TableColumn] = flow.map(|a| a.columns.as_slice()).unwrap_or(&table.columns);
let (Some(x_dim), Some(y_dim), Some(w_dim), Some(h_dim)) =
(&table.x, &table.y, &table.w, &table.h)
else {
diagnostics.push(Diagnostic::advisory(
"scene.missing_geometry",
format!(
"table '{}' is missing one or more geometry properties (x, y, w, h); skipped",
table.id
),
table.source_span,
Some(table.id.clone()),
));
return;
};
let (Some(table_x), Some(table_y), Some(table_w), Some(table_h)) = (
resolve_geometry_px(Some(x_dim), cx.resolved),
resolve_geometry_px(Some(y_dim), cx.resolved),
resolve_geometry_px(Some(w_dim), cx.resolved),
resolve_geometry_px(Some(h_dim), cx.resolved),
) else {
diagnostics.push(Diagnostic::advisory(
"scene.missing_geometry",
format!(
"table '{}' has an unresolvable geometry unit (x, y, w, h); skipped",
table.id
),
table.source_span,
Some(table.id.clone()),
));
return;
};
let origin_x = ctx.dx + table_x;
let origin_y = ctx.dy + table_y;
let gap = resolve_property_dimension_px(table.gap.as_ref(), cx.resolved, 0.0).max(0.0);
let pad = resolve_property_dimension_px(table.cell_padding.as_ref(), cx.resolved, 0.0).max(0.0);
let opacity = (table.opacity.unwrap_or(1.0).clamp(0.0, 1.0)) * ctx.opacity;
let col_count = columns.len().max(1);
let row_count = rows.len();
if row_count == 0 {
return;
}
let placed = place_cells(rows, col_count, row_count);
let env = cx.measure_env();
let header_rows = (table.header_rows.unwrap_or(0) as usize).min(row_count);
let TableLayout {
col_widths,
row_heights,
row_natural: _,
} = compute_table_layout(
columns,
&placed,
GridDims {
col_count,
row_count,
gap,
pad,
table_w,
table_h,
},
env,
diagnostics,
header_rows,
table.header_style.as_deref(),
);
let mut col_left: Vec<f64> = Vec::with_capacity(col_count);
let mut cursor = origin_x + pad;
for w in &col_widths {
col_left.push(cursor);
cursor += w + gap;
}
let mut row_top: Vec<f64> = Vec::with_capacity(row_count);
let mut rcursor = origin_y + pad;
for h in &row_heights {
row_top.push(rcursor);
rcursor += h + gap;
}
let collapse_mode = matches!(table.border_collapse.as_deref(), Some("collapse"));
let mut edge_acc: BTreeMap<EdgeKey, EdgeStyle> = BTreeMap::new();
for pc in &placed {
let left = col_left.get(pc.col).copied().unwrap_or(origin_x + pad);
let mut span_w = 0.0;
for c in pc.col..pc.col + pc.cs {
span_w += col_widths.get(c).copied().unwrap_or(0.0);
}
span_w += gap * (pc.cs.saturating_sub(1)) as f64;
let top = row_top.get(pc.row).copied().unwrap_or(origin_y + pad);
let mut span_h = 0.0;
for dr in 0..pc.rs {
span_h += row_heights.get(pc.row + dr).copied().unwrap_or(0.0);
}
span_h += gap * (pc.rs.saturating_sub(1)) as f64;
let rect = CellRect {
x: left,
y: top,
w: span_w.max(0.0),
h: span_h.max(0.0),
};
let cell_emit = CellEmit {
pad,
opacity,
is_header: pc.row < header_rows,
};
if collapse_mode {
emit_cell_no_border(pc.cell, &rect, cell_emit, cx, commands, diagnostics, ctx);
accumulate_cell_edges(
table,
pc.cell,
&rect,
opacity,
cx.resolved,
diagnostics,
&mut edge_acc,
);
} else {
emit_cell(pc.cell, &rect, cell_emit, cx, commands, diagnostics, ctx);
}
}
for edge in edge_acc.values() {
commands.push(SceneCommand::StrokeLine {
x1: edge.x1,
y1: edge.y1,
x2: edge.x2,
y2: edge.y2,
color: edge.color,
stroke_width: edge.stroke_width,
stroke_dash: None,
stroke_gap: None,
stroke_linecap: None,
});
}
}
fn emit_cell_no_border(
cell: &zenith_core::TableCell,
rect: &CellRect,
cell_emit: CellEmit,
cx: TableEmitCtx,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
ctx: RenderCtx,
) {
emit_cell_fill(cell, rect, cell_emit, cx, commands, diagnostics);
emit_cell_children(cell, rect, cell_emit, cx, commands, diagnostics, ctx);
}
fn emit_cell(
cell: &zenith_core::TableCell,
rect: &CellRect,
cell_emit: CellEmit,
cx: TableEmitCtx,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
ctx: RenderCtx,
) {
emit_cell_fill(cell, rect, cell_emit, cx, commands, diagnostics);
let table = cx.table;
let border_prop: Option<&PropertyValue> = cell.border.as_ref().or(table.border.as_ref());
if let Some(prop) = border_prop
&& let Some(mut color) = resolve_property_color(prop, cx.resolved, diagnostics, &table.id)
{
color.a = (color.a as f64 * cell_emit.opacity).round() as u8;
let bw = resolve_border_width(
cell.border_width.as_ref().or(table.border_width.as_ref()),
cx.resolved,
1.0,
)
.max(0.0);
if bw > 0.0 {
let x0 = rect.x;
let y0 = rect.y;
let x1 = rect.x + rect.w;
let y1 = rect.y + rect.h;
for (ax, ay, bx, by) in [
(x0, y0, x1, y0), (x0, y1, x1, y1), (x0, y0, x0, y1), (x1, y0, x1, y1), ] {
commands.push(SceneCommand::StrokeLine {
x1: ax,
y1: ay,
x2: bx,
y2: by,
color,
stroke_width: bw,
stroke_dash: None,
stroke_gap: None,
stroke_linecap: None,
});
}
}
}
emit_cell_children(cell, rect, cell_emit, cx, commands, diagnostics, ctx);
}
fn emit_cell_fill(
cell: &zenith_core::TableCell,
rect: &CellRect,
cell_emit: CellEmit,
cx: TableEmitCtx,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
) {
let table = cx.table;
let fill_prop: Option<&PropertyValue> = cell.fill.as_ref().or_else(|| {
if cell_emit.is_header {
table.header_fill.as_ref().or(table.fill.as_ref())
} else {
table.fill.as_ref()
}
});
if let Some(prop) = fill_prop
&& let Some(mut color) = resolve_property_color(prop, cx.resolved, diagnostics, &table.id)
{
color.a = (color.a as f64 * cell_emit.opacity).round() as u8;
commands.push(SceneCommand::FillRect {
x: rect.x,
y: rect.y,
w: rect.w,
h: rect.h,
paint: Paint::solid(color),
});
}
}
fn emit_cell_children(
cell: &zenith_core::TableCell,
rect: &CellRect,
cell_emit: CellEmit,
cx: TableEmitCtx,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
ctx: RenderCtx,
) {
let table = cx.table;
let node_cx = cx.node_ctx();
let pad = cell_emit.pad;
let opacity = cell_emit.opacity;
let is_header = cell_emit.is_header;
let content_x = rect.x + pad;
let content_y = rect.y + pad;
let content_w = (rect.w - 2.0 * pad).max(0.0);
let content_h = (rect.h - 2.0 * pad).max(0.0);
let h_align = cell
.h_align
.as_deref()
.or(table.h_align.as_deref())
.unwrap_or("start");
let v_align = cell
.v_align
.as_deref()
.or(table.v_align.as_deref())
.unwrap_or("top");
commands.push(SceneCommand::PushClip {
x: content_x,
y: content_y,
w: content_w,
h: content_h,
});
let env = cx.measure_env();
let mut family_cache: BTreeMap<String, Vec<String>> = BTreeMap::new();
for child in &cell.children {
if let Node::Text(t) = child {
let wrap_w = child_declared_box(child, cx.resolved)
.0
.unwrap_or(content_w);
let h_align_text = match h_align {
"center" => "center",
"end" => "end",
_ => "start",
};
let eff_align: &str = t.align.as_deref().unwrap_or(h_align_text);
let eff_hs = if is_header {
table.header_style.as_deref()
} else {
None
};
let styled = header_styled_text(t, eff_hs);
let families = cached_families(
&styled,
cx.resolved,
cx.style_map,
cx.fonts,
diagnostics,
&mut family_cache,
);
let nat_h = measure_text_wrapped_height(&styled, wrap_w, families, env, diagnostics)
.unwrap_or(0.0);
let v_offset = match v_align {
"middle" => ((content_h - nat_h) / 2.0).max(0.0),
"bottom" => (content_h - nat_h).max(0.0),
_ => 0.0,
};
let mut cloned = styled.into_owned();
if cloned.w.is_none() {
cloned.w = Some(super::super::util::px_prop(wrap_w));
}
if cloned.x.is_none() {
cloned.x = Some(super::super::util::px_prop(0.0));
}
if cloned.y.is_none() {
cloned.y = Some(super::super::util::px_prop(0.0));
}
if cloned.align.is_none() {
cloned.align = Some(eff_align.to_string());
}
let effective_child = zenith_core::Node::Text(Box::new(cloned));
let child_ctx = RenderCtx {
opacity,
dx: content_x,
dy: content_y + v_offset,
baseline_grid: ctx.baseline_grid,
};
let _ = compile_node(
&effective_child,
node_cx,
commands,
diagnostics,
&mut Vec::new(),
child_ctx,
);
continue;
}
let (cw, ch) = child_declared_box(child, cx.resolved);
let dx_align = match h_align {
"center" => ((content_w - cw.unwrap_or(content_w)) / 2.0).max(0.0),
"end" => (content_w - cw.unwrap_or(content_w)).max(0.0),
_ => 0.0,
};
let dy_align = match v_align {
"middle" => ((content_h - ch.unwrap_or(content_h)) / 2.0).max(0.0),
"bottom" => (content_h - ch.unwrap_or(content_h)).max(0.0),
_ => 0.0,
};
let child_ctx = RenderCtx {
opacity,
dx: content_x + dx_align,
dy: content_y + dy_align,
baseline_grid: ctx.baseline_grid,
};
let _ = compile_node(
child,
node_cx,
commands,
diagnostics,
&mut Vec::new(),
child_ctx,
);
}
commands.push(SceneCommand::PopClip);
}