use blitz_traits::node_id::NodeId;
use std::{ops::Range, sync::Arc};
use style::logical_geometry::WritingMode;
use style::properties::ComputedValues;
use atomic_refcell::AtomicRefCell;
use markup5ever::local_name;
use style::properties::style_structs::Border;
use style::servo_arc::Arc as ServoArc;
use style::typed_om::NumericBaseType;
use style::values::computed::calc::CalcPercentageLeaf;
use style::values::computed::length_percentage::{
CalcLengthPercentage, CalcNode, ComputedLeaf, Unpacked as UnpackedLengthPercentage,
};
use style::values::computed::{Length, LengthPercentage};
use style::values::generics::Optional;
use style::values::specified::box_::{DisplayInside, DisplayOutside};
use style::{
Atom, computed_values::border_collapse::T as BorderCollapse,
computed_values::table_layout::T as TableLayout, values::computed::BorderStyle,
};
use style_traits::values::specified::AllowedNumericType;
use taffy::{
DetailedGridInfo, LayoutPartialTree as _, ResolveOrZero, TrackSizingFunction, style_helpers,
};
use crate::BaseDocument;
use crate::layout::LayoutPassState;
use super::damage::{CONSTRUCT_BOX, CONSTRUCT_DESCENDENT, CONSTRUCT_FC};
use super::resolve_calc_value;
pub struct TableTreeWrapper<'a, 'doc> {
pub(crate) doc: &'a mut LayoutPassState<'doc>,
pub(crate) ctx: Arc<TableContext>,
}
#[derive(Debug)]
pub struct TableContext {
pub style: taffy::Style<Atom>,
pub cells: Vec<TableCell>,
pub oof_children: Vec<(NodeId, taffy::Position)>,
pub rows: Vec<TableRow>,
pub columns: Vec<TableColumn>,
pub computed_grid_info: AtomicRefCell<Option<DetailedGridInfo<Atom>>>,
pub border_style: Option<ServoArc<Border>>,
pub border_collapse: BorderCollapse,
#[allow(dead_code)]
calc_values: Vec<LengthPercentage>,
}
#[derive(Debug, Clone)]
pub struct TableCell {
pub node_id: NodeId,
pub row: u16,
pub column: u16,
style: taffy::Style<Atom>,
}
#[derive(Debug, Default)]
struct ColumnCursor {
col: u16,
num_columns: u16,
rowspans: Vec<u16>,
}
impl ColumnCursor {
fn start_row(&mut self) {
self.col = 0;
for remaining in self.rowspans.iter_mut() {
*remaining = remaining.saturating_sub(1);
}
}
fn next_free(&mut self) -> u16 {
while self.rowspans.get(self.col as usize).is_some_and(|r| *r > 0) {
self.col += 1;
}
self.col
}
fn place(&mut self, colspan: u16, rowspan: u16) {
let end = self.col + colspan;
if self.rowspans.len() < end as usize {
self.rowspans.resize(end as usize, 0);
}
for remaining in &mut self.rowspans[self.col as usize..end as usize] {
*remaining = rowspan - 1;
}
self.col = end;
self.num_columns = self.num_columns.max(end);
}
}
#[derive(Debug, Clone)]
pub struct TableColumn {
pub node_id: NodeId,
pub group: Option<NodeId>,
}
#[derive(Debug, Clone)]
pub struct TableRow {
pub node_id: NodeId,
pub group: Option<NodeId>,
pub height: Option<f32>,
pub is_constrained: bool,
}
#[cfg_attr(not(feature = "writing-mode"), allow(unused_variables))]
fn table_wm(doc: &BaseDocument, table_root_node_id: NodeId) -> WritingMode {
#[cfg(feature = "writing-mode")]
{
if doc.try_root_element().map(|root| root.id) == Some(table_root_node_id) {
doc.root_layout_wm(table_root_node_id)
} else {
doc.nodes[table_root_node_id].writing_mode()
}
}
#[cfg(not(feature = "writing-mode"))]
{
WritingMode::empty()
}
}
fn table_taffy_style(style: &ComputedValues, table_wm: WritingMode) -> taffy::Style<Atom> {
stylo_taffy::to_taffy_style_in(style, table_wm)
}
fn side_width(width: app_units::Au, style: BorderStyle) -> f32 {
if style.none_or_hidden() {
0.0
} else {
width.to_f32_px()
}
}
fn percent_plus_length(
calc_values: &mut Vec<LengthPercentage>,
percent: f32,
length: f32,
) -> TrackSizingFunction {
let node = CalcNode::Sum(
vec![
CalcNode::Leaf(ComputedLeaf::Percentage(CalcPercentageLeaf::new(
percent,
Optional::Some(NumericBaseType::Length),
))),
CalcNode::Leaf(ComputedLeaf::Length(Length::new(length))),
]
.into(),
);
let value = LengthPercentage::new_calc(node, AllowedNumericType::NonNegative);
let dim = match value.unpack() {
UnpackedLengthPercentage::Calc(calc) => {
let ptr = calc as *const CalcLengthPercentage as *const ();
unsafe { taffy::Dimension::from_raw(taffy::CompactLength::calc(ptr)) }
}
UnpackedLengthPercentage::Length(len) => style_helpers::length(len.px()),
UnpackedLengthPercentage::Percentage(p) => style_helpers::percent(p.0),
};
calc_values.push(value);
dim.into()
}
pub(crate) fn build_table_context(
doc: &mut BaseDocument,
table_root_node_id: NodeId,
) -> (TableContext, Vec<NodeId>) {
let mut cells: Vec<TableCell> = Vec::new();
let mut oof_children: Vec<(NodeId, taffy::Position)> = Vec::new();
let mut rows: Vec<TableRow> = Vec::new();
let mut row = 0u16;
let mut cursor = ColumnCursor::default();
let wm = table_wm(doc, table_root_node_id);
let root_node = &mut doc.nodes[table_root_node_id];
let children = std::mem::take(&mut root_node.children);
let Some(stylo_styles) = root_node.primary_styles() else {
panic!("Ignoring table because it has no styles");
};
let mut style = table_taffy_style(&stylo_styles, wm);
style.item_is_table = true;
style.grid_auto_flow = taffy::GridAutoFlow::RowDense;
style.grid_auto_columns = Vec::new();
style.grid_auto_rows = Vec::new();
let is_fixed = match stylo_styles.clone_table_layout() {
TableLayout::Fixed => !style.size.width.is_auto(),
TableLayout::Auto => false,
};
let border_collapse = stylo_styles.clone_border_collapse();
let border_spacing = stylo_styles.clone_border_spacing().0;
drop(stylo_styles);
let mut columns: Vec<TableColumn> = Vec::new();
let mut column_sizes: Vec<taffy::TrackSizingFunction> = Vec::new();
for child_id in children.iter().copied() {
collect_columns(doc, child_id, wm, &mut columns, &mut column_sizes);
}
if !is_fixed {
for column in column_sizes.iter_mut() {
if column.max.into_raw().tag() == taffy::CompactLength::PERCENT_TAG {
*column = style_helpers::auto();
}
}
}
let mut first_cell_border: Option<ServoArc<Border>> = None;
let mut percent_columns: Vec<(u16, f32, f32)> = Vec::new();
let mut calc_values: Vec<LengthPercentage> = Vec::new();
let row_group_order = |doc: &BaseDocument, child_id: NodeId| -> u8 {
let display = doc.nodes[child_id]
.primary_styles()
.map(|s| s.clone_display());
match display.map(|d| d.inside()) {
Some(DisplayInside::TableHeaderGroup) => 0,
Some(DisplayInside::TableFooterGroup) => 2,
_ => 1,
}
};
for order in 0..3 {
for child_id in children.iter().copied() {
if row_group_order(doc, child_id) != order {
continue;
}
collect_table_cells(
doc,
child_id,
wm,
is_fixed,
border_collapse,
&mut row,
&mut cursor,
&mut cells,
&mut oof_children,
&mut rows,
&mut column_sizes,
&mut first_cell_border,
&mut percent_columns,
);
}
}
let remaining_column = if is_fixed {
style_helpers::minmax(style_helpers::length(0.0), style_helpers::fr(1.0))
} else {
style_helpers::auto()
};
while column_sizes.len() > cursor.num_columns as usize
&& column_sizes
.last()
.is_some_and(|c| c.min.into_raw().tag() == taffy::CompactLength::AUTO_TAG)
{
column_sizes.pop();
columns.pop();
}
let num_columns = cursor.num_columns.max(column_sizes.len() as u16);
column_sizes.resize(num_columns as usize, remaining_column);
if is_fixed {
let spacing_x = match border_collapse {
BorderCollapse::Separate => border_spacing.width.px(),
BorderCollapse::Collapse => 0.0,
};
let inner_spacing = spacing_x * num_columns.saturating_sub(1) as f32;
let mut percent_track = |percent: f32, extra: f32| -> TrackSizingFunction {
let extra = extra - percent * inner_spacing;
if extra == 0.0 {
style_helpers::percent(percent)
} else {
percent_plus_length(&mut calc_values, percent, extra)
}
};
for column in column_sizes.iter_mut() {
if column.max.is_auto() {
*column = remaining_column;
} else if column.max.into_raw().tag() == taffy::CompactLength::PERCENT_TAG {
*column = percent_track(column.max.into_raw().value(), 0.0);
}
}
for (col_idx, percent, extra) in percent_columns {
if let Some(column) = column_sizes.get_mut(col_idx as usize) {
*column = percent_track(percent, extra);
}
}
}
style.grid_template_columns = column_sizes.into_iter().map(|dim| dim.into()).collect();
let has_unconstrained_rows = rows.iter().any(|row| !row.is_constrained);
style.grid_template_rows = rows
.iter()
.map(|row| {
let min = match row.height {
Some(height) => style_helpers::length(height),
None => style_helpers::auto(),
};
let max = if row.is_constrained && has_unconstrained_rows {
style_helpers::max_content()
} else {
style_helpers::auto()
};
style_helpers::minmax(min, max)
})
.collect();
style.gap = match border_collapse {
BorderCollapse::Separate => {
let spacing_x = border_spacing.width.px();
let spacing_y = border_spacing.height.px();
let padding = style.padding.resolve_or_zero(None, resolve_calc_value);
style.padding = taffy::Rect {
left: style_helpers::length(padding.left + spacing_x),
right: style_helpers::length(padding.right + spacing_x),
top: style_helpers::length(padding.top + spacing_y),
bottom: style_helpers::length(padding.bottom + spacing_y),
};
taffy::Size {
width: style_helpers::length(spacing_x),
height: style_helpers::length(spacing_y),
}
}
BorderCollapse::Collapse => first_cell_border
.as_ref()
.map(|border| {
let x = side_width(border.border_left_width.0, border.border_left_style).max(
side_width(border.border_right_width.0, border.border_right_style),
);
let y = side_width(border.border_top_width.0, border.border_top_style).max(
side_width(border.border_bottom_width.0, border.border_bottom_style),
);
taffy::Size {
width: style_helpers::length(x),
height: style_helpers::length(y),
}
})
.unwrap_or(taffy::Size::ZERO.map(style_helpers::length)),
};
if border_collapse == BorderCollapse::Collapse {
style.border = taffy::Rect {
left: style.gap.width,
right: style.gap.width,
top: style.gap.height,
bottom: style.gap.height,
};
}
let layout_children = cells
.iter()
.map(|cell| cell.node_id)
.chain(oof_children.iter().map(|(node_id, _)| *node_id))
.collect();
let root_node = &mut doc.nodes[table_root_node_id];
root_node.children = children;
(
TableContext {
style,
cells,
oof_children,
rows,
columns,
computed_grid_info: AtomicRefCell::new(None),
border_collapse,
border_style: first_cell_border,
calc_values,
},
layout_children,
)
}
fn collect_columns(
doc: &mut BaseDocument,
node_id: NodeId,
wm: WritingMode,
columns: &mut Vec<TableColumn>,
column_sizes: &mut Vec<TrackSizingFunction>,
) {
let node = &doc.nodes[node_id];
if !node.is_element() {
return;
}
let Some(display) = node.display_style() else {
return;
};
match display.inside() {
DisplayInside::TableColumnGroup => {
let first_column = columns.len();
let children = std::mem::take(&mut doc.nodes[node_id].children);
for child_id in children.iter().copied() {
collect_columns(doc, child_id, wm, columns, column_sizes);
}
doc.nodes[node_id].children = children;
for column in &mut columns[first_column..] {
column.group.get_or_insert(node_id);
}
}
DisplayInside::TableColumn => {
let style = table_taffy_style(&node.primary_styles().unwrap(), wm);
let span: u16 = node
.attr(local_name!("span"))
.and_then(|val| val.parse::<u16>().ok())
.map(|v| v.max(1))
.unwrap_or(1);
let column: TrackSizingFunction = match style.size.width.tag() {
taffy::CompactLength::LENGTH_TAG => {
let mut width = style.size.width.value();
if style.max_size.width.into_raw().tag() == taffy::CompactLength::LENGTH_TAG {
width = width.min(style.max_size.width.into_raw().value());
}
if width > 0.0 {
style_helpers::length(width)
} else {
style_helpers::auto()
}
}
taffy::CompactLength::PERCENT_TAG => {
style_helpers::percent(style.size.width.value())
}
_ => style_helpers::auto(),
};
let column = match style.min_size.width.into_raw().tag() {
taffy::CompactLength::LENGTH_TAG if column.max.is_auto() => style_helpers::minmax(
style_helpers::length(style.min_size.width.into_raw().value()),
style_helpers::auto(),
),
_ => column,
};
for _ in 0..span {
columns.push(TableColumn {
node_id,
group: None,
});
column_sizes.push(column);
}
}
_ => {}
}
}
#[allow(clippy::too_many_arguments)]
fn collect_table_cells(
doc: &mut BaseDocument,
node_id: NodeId,
wm: WritingMode,
is_fixed: bool,
border_collapse: BorderCollapse,
row: &mut u16,
cursor: &mut ColumnCursor,
cells: &mut Vec<TableCell>,
oof_children: &mut Vec<(NodeId, taffy::Position)>,
rows: &mut Vec<TableRow>,
columns: &mut Vec<TrackSizingFunction>,
first_cell_border: &mut Option<ServoArc<Border>>,
percent_columns: &mut Vec<(u16, f32, f32)>,
) {
let node = &mut doc.nodes[node_id];
if !node.is_element() {
return;
}
let Some(display) = node.display_style() else {
#[cfg(feature = "tracing")]
tracing::info!("Ignoring table descendent because it has no styles");
return;
};
if display.outside() == DisplayOutside::None {
node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
return;
}
let position = node.primary_styles().unwrap().clone_position();
if position.is_absolutely_positioned() {
oof_children.push((node_id, stylo_taffy::convert::position(position)));
return;
}
match display.inside() {
DisplayInside::TableRowGroup
| DisplayInside::TableHeaderGroup
| DisplayInside::TableFooterGroup
| DisplayInside::Contents => {
let first_row = rows.len();
let children = std::mem::take(&mut doc.nodes[node_id].children);
for child_id in children.iter().copied() {
doc.nodes[child_id]
.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
collect_table_cells(
doc,
child_id,
wm,
is_fixed,
border_collapse,
row,
cursor,
cells,
oof_children,
rows,
columns,
first_cell_border,
percent_columns,
);
}
doc.nodes[node_id].children = children;
if display.inside() != DisplayInside::Contents {
for row in &mut rows[first_row..] {
row.group.get_or_insert(node_id);
}
}
}
DisplayInside::TableRow => {
node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
*row += 1;
cursor.start_row();
let height = node.primary_styles().and_then(|style| {
let height = stylo_taffy::convert::dimension(&style.clone_height());
(height.tag() == taffy::CompactLength::LENGTH_TAG).then(|| height.value())
});
rows.push(TableRow {
node_id,
group: None,
height,
is_constrained: height.is_some(),
});
let children = std::mem::take(&mut doc.nodes[node_id].children);
for child_id in children.iter().copied() {
collect_table_cells(
doc,
child_id,
wm,
is_fixed,
border_collapse,
row,
cursor,
cells,
oof_children,
rows,
columns,
first_cell_border,
percent_columns,
);
}
doc.nodes[node_id].children = children;
}
DisplayInside::TableCell => {
let stylo_style = &node.primary_styles().unwrap();
let colspan: u16 = node
.attr(local_name!("colspan"))
.and_then(|val| val.parse().ok())
.unwrap_or(1);
let rowspan: u16 = node
.attr(local_name!("rowspan"))
.and_then(|val| val.parse::<u16>().ok())
.map(|v| v.clamp(1, 65534))
.unwrap_or(1);
let mut style = table_taffy_style(stylo_style, wm);
let col = cursor.next_free();
if border_collapse == BorderCollapse::Collapse {
style.border = taffy::Rect::ZERO.map(style_helpers::length);
}
if first_cell_border.is_none() {
*first_cell_border = Some(stylo_style.clone_border());
}
if is_fixed {
style.min_size.width = style_helpers::length(0.0);
}
let col_needs_width =
(col as usize) >= columns.len() || columns[col as usize].max.is_auto();
if *row == 1 && col_needs_width {
let column: TrackSizingFunction = match style.size.width.tag() {
taffy::CompactLength::LENGTH_TAG => {
let len = style.size.width.value();
let padding = style.padding.resolve_or_zero(None, resolve_calc_value);
let border = style.border.resolve_or_zero(None, resolve_calc_value);
match style.box_sizing {
taffy::BoxSizing::ContentBox => style_helpers::length(
len + padding.left + padding.right + border.left + border.right,
),
taffy::BoxSizing::BorderBox => style_helpers::length(len),
}
}
taffy::CompactLength::PERCENT_TAG => {
if is_fixed {
let extra = match style.box_sizing {
taffy::BoxSizing::ContentBox => {
let padding =
style.padding.resolve_or_zero(None, resolve_calc_value);
let border =
style.border.resolve_or_zero(None, resolve_calc_value);
padding.left + padding.right + border.left + border.right
}
taffy::BoxSizing::BorderBox => 0.0,
};
percent_columns.push((col, style.size.width.value(), extra));
style_helpers::percent(style.size.width.value())
} else {
style_helpers::auto()
}
}
taffy::CompactLength::AUTO_TAG => style_helpers::auto(),
_ => style.size.width.into(),
};
if (col as usize) < columns.len() {
if !column.max.is_auto() {
columns[col as usize] = column;
}
} else {
columns.resize(col as usize, style_helpers::auto());
columns.push(column);
}
}
style.margin = taffy::Rect::ZERO.map(style_helpers::length);
style.grid_column = taffy::Line {
start: style_helpers::auto(),
end: style_helpers::span(colspan),
};
style.grid_row = taffy::Line {
start: style_helpers::line(*row as i16),
end: style_helpers::span(rowspan),
};
if rowspan == 1 && style.size.height.tag() == taffy::CompactLength::LENGTH_TAG {
if let Some(row) = rows.last_mut() {
row.is_constrained = true;
}
}
style.size.width = style_helpers::auto();
cells.push(TableCell {
node_id,
row: row.saturating_sub(1),
column: col,
style,
});
cursor.place(colspan, rowspan);
}
DisplayInside::Flow
| DisplayInside::FlowRoot
| DisplayInside::Flex
| DisplayInside::Grid => {
node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
}
DisplayInside::TableColumnGroup | DisplayInside::TableColumn | DisplayInside::Table => {
node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
}
DisplayInside::None => {
node.remove_damage(CONSTRUCT_DESCENDENT | CONSTRUCT_FC | CONSTRUCT_BOX);
}
}
}
pub struct RangeIter(Range<usize>);
impl Iterator for RangeIter {
type Item = taffy::NodeId;
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(taffy::NodeId::from)
}
}
impl taffy::TraversePartialTree for TableTreeWrapper<'_, '_> {
type ChildIter<'a>
= RangeIter
where
Self: 'a;
#[inline(always)]
fn child_ids(&self, _node_id: taffy::NodeId) -> Self::ChildIter<'_> {
RangeIter(0..self.ctx.cells.len())
}
#[inline(always)]
fn child_count(&self, _node_id: taffy::NodeId) -> usize {
self.ctx.cells.len()
}
#[inline(always)]
fn get_child_id(&self, _node_id: taffy::NodeId, index: usize) -> taffy::NodeId {
index.into()
}
}
impl taffy::TraverseTree for TableTreeWrapper<'_, '_> {}
impl taffy::LayoutPartialTree for TableTreeWrapper<'_, '_> {
type CoreContainerStyle<'a>
= &'a taffy::Style<Atom>
where
Self: 'a;
type CustomIdent = Atom;
fn get_core_container_style(&self, _node_id: taffy::NodeId) -> &taffy::Style<Atom> {
&self.ctx.style
}
fn resolve_calc_value(&self, calc_ptr: *const (), parent_size: f32) -> f32 {
resolve_calc_value(calc_ptr, parent_size)
}
fn set_unrounded_layout(&mut self, node_id: taffy::NodeId, layout: &taffy::Layout) {
let node_id = crate::taffy_node_id(self.ctx.cells[usize::from(node_id)].node_id);
self.doc.set_unrounded_layout(node_id, layout)
}
fn compute_child_layout(
&mut self,
node_id: taffy::NodeId,
inputs: taffy::tree::LayoutInput,
) -> taffy::LayoutOutput {
let cell = &self.ctx.cells[usize::from(node_id)];
let node_id = crate::taffy_node_id(cell.node_id);
self.doc.compute_child_layout(node_id, inputs)
}
}
impl taffy::LayoutGridContainer for TableTreeWrapper<'_, '_> {
type GridContainerStyle<'a>
= &'a taffy::Style<Atom>
where
Self: 'a;
type GridItemStyle<'a>
= &'a taffy::Style<Atom>
where
Self: 'a;
fn get_grid_container_style(&self, node_id: taffy::NodeId) -> Self::GridContainerStyle<'_> {
self.get_core_container_style(node_id)
}
fn get_grid_child_style(&self, child_node_id: taffy::NodeId) -> Self::GridItemStyle<'_> {
&self.ctx.cells[usize::from(child_node_id)].style
}
fn set_detailed_grid_info(
&mut self,
_node_id: taffy::NodeId,
detailed_grid_info: DetailedGridInfo<Atom>,
) {
*self.ctx.computed_grid_info.borrow_mut() = Some(detailed_grid_info);
}
}