mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
1166 lines
46 KiB
Rust
1166 lines
46 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
//! CSS table formatting contexts.
|
|
|
|
use app_units::Au;
|
|
use crate::block::{BlockFlow, ISizeAndMarginsComputer};
|
|
use crate::context::LayoutContext;
|
|
use crate::display_list::{BlockFlowDisplayListBuilding, DisplayListBuildState};
|
|
use crate::display_list::{StackingContextCollectionFlags, StackingContextCollectionState};
|
|
use crate::flow::{
|
|
EarlyAbsolutePositionInfo, Flow, FlowClass, GetBaseFlow, ImmutableFlowUtils, OpaqueFlow,
|
|
};
|
|
use crate::flow_list::MutFlowListIterator;
|
|
use crate::fragment::{Fragment, FragmentBorderBoxIterator, Overflow};
|
|
use crate::layout_debug;
|
|
use crate::model::MaybeAuto;
|
|
use crate::table::{ColumnComputedInlineSize, ColumnIntrinsicInlineSize, InternalTable, VecExt};
|
|
use crate::table_cell::{CollapsedBordersForCell, TableCellFlow};
|
|
use euclid::Point2D;
|
|
use gfx_traits::print_tree::PrintTree;
|
|
use serde::{Serialize, Serializer};
|
|
use std::cmp::max;
|
|
use std::fmt;
|
|
use std::iter::{Enumerate, IntoIterator, Peekable};
|
|
use style::computed_values::border_collapse::T as BorderCollapse;
|
|
use style::computed_values::border_spacing::T as BorderSpacing;
|
|
use style::computed_values::border_top_style::T as BorderStyle;
|
|
use style::logical_geometry::{LogicalSize, PhysicalSide, WritingMode};
|
|
use style::properties::ComputedValues;
|
|
use style::values::computed::{Color, LengthOrPercentageOrAuto};
|
|
|
|
#[allow(unsafe_code)]
|
|
unsafe impl crate::flow::HasBaseFlow for TableRowFlow {}
|
|
|
|
/// A single row of a table.
|
|
#[repr(C)]
|
|
pub struct TableRowFlow {
|
|
/// Fields common to all block flows.
|
|
pub block_flow: BlockFlow,
|
|
|
|
/// Information about the intrinsic inline-sizes of each cell.
|
|
pub cell_intrinsic_inline_sizes: Vec<CellIntrinsicInlineSize>,
|
|
|
|
/// Information about the computed inline-sizes of each column.
|
|
pub column_computed_inline_sizes: Vec<ColumnComputedInlineSize>,
|
|
|
|
/// The number of remaining rows spanned by cells in previous rows, indexed by column.
|
|
///
|
|
/// Columns that are not included in this vector have the default rowspan of "1". If there are
|
|
/// no cells with rowspan != 1 in previous rows, this vector may be empty.
|
|
pub incoming_rowspan: Vec<u32>,
|
|
|
|
/// The spacing for this row, propagated down from the table during the inline-size assignment
|
|
/// phase.
|
|
pub spacing: BorderSpacing,
|
|
|
|
/// The direction of the columns, propagated down from the table during the inline-size
|
|
/// assignment phase.
|
|
pub table_writing_mode: WritingMode,
|
|
|
|
/// Information about the borders for each cell that we bubble up to our parent. This is only
|
|
/// computed if `border-collapse` is `collapse`.
|
|
pub preliminary_collapsed_borders: CollapsedBordersForRow,
|
|
|
|
/// Information about the borders for each cell, post-collapse. This is only computed if
|
|
/// `border-collapse` is `collapse`.
|
|
pub final_collapsed_borders: CollapsedBordersForRow,
|
|
|
|
/// The computed cell spacing widths post-collapse.
|
|
pub collapsed_border_spacing: CollapsedBorderSpacingForRow,
|
|
}
|
|
|
|
impl Serialize for TableRowFlow {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
self.block_flow.serialize(serializer)
|
|
}
|
|
}
|
|
|
|
/// Information about the column inline size and span for each cell.
|
|
#[derive(Clone, Copy, Serialize)]
|
|
pub struct CellIntrinsicInlineSize {
|
|
/// Inline sizes that this cell contributes to the column.
|
|
pub column_size: ColumnIntrinsicInlineSize,
|
|
/// The column span of this cell.
|
|
pub column_span: u32,
|
|
/// The row span of this cell.
|
|
pub row_span: u32,
|
|
}
|
|
|
|
impl TableRowFlow {
|
|
pub fn from_fragment(fragment: Fragment) -> TableRowFlow {
|
|
let writing_mode = fragment.style().writing_mode;
|
|
TableRowFlow {
|
|
block_flow: BlockFlow::from_fragment(fragment),
|
|
cell_intrinsic_inline_sizes: Vec::new(),
|
|
column_computed_inline_sizes: Vec::new(),
|
|
incoming_rowspan: Vec::new(),
|
|
spacing: BorderSpacing::zero(),
|
|
table_writing_mode: writing_mode,
|
|
preliminary_collapsed_borders: CollapsedBordersForRow::new(),
|
|
final_collapsed_borders: CollapsedBordersForRow::new(),
|
|
collapsed_border_spacing: CollapsedBorderSpacingForRow::new(),
|
|
}
|
|
}
|
|
|
|
/// Compute block-size for table-row flow.
|
|
///
|
|
/// TODO(pcwalton): This doesn't handle floats and positioned elements right.
|
|
///
|
|
/// Returns the block size
|
|
pub fn compute_block_size_table_row_base<'a>(
|
|
&'a mut self,
|
|
layout_context: &LayoutContext,
|
|
incoming_rowspan_data: &mut Vec<Au>,
|
|
border_info: &[TableRowSizeData],
|
|
row_index: usize,
|
|
) -> Au {
|
|
fn include_sizes_from_previous_rows(
|
|
col: &mut usize,
|
|
incoming_rowspan: &[u32],
|
|
incoming_rowspan_data: &mut Vec<Au>,
|
|
max_block_size: &mut Au,
|
|
) {
|
|
while let Some(span) = incoming_rowspan.get(*col) {
|
|
if *span == 1 {
|
|
break;
|
|
}
|
|
let incoming = if let Some(incoming) = incoming_rowspan_data.get(*col) {
|
|
*incoming
|
|
} else {
|
|
// This happens when we have a cell with both rowspan and colspan
|
|
// incoming_rowspan_data only records the data for the first column,
|
|
// but that's ok because we only need to account for each spanning cell
|
|
// once. So we skip ahead.
|
|
*col += 1;
|
|
continue;
|
|
};
|
|
*max_block_size = max(*max_block_size, incoming);
|
|
*col += 1;
|
|
}
|
|
}
|
|
// Per CSS 2.1 § 17.5.3, find max_y = max(computed `block-size`, minimum block-size of
|
|
// all cells).
|
|
let mut max_block_size = Au(0);
|
|
let thread_id = self.block_flow.base.thread_id;
|
|
let content_box = self.block_flow.base.position -
|
|
self.block_flow.fragment.border_padding -
|
|
self.block_flow.fragment.margin;
|
|
|
|
let mut col = 0;
|
|
for kid in self.block_flow.base.child_iter_mut() {
|
|
include_sizes_from_previous_rows(
|
|
&mut col,
|
|
&self.incoming_rowspan,
|
|
incoming_rowspan_data,
|
|
&mut max_block_size,
|
|
);
|
|
kid.place_float_if_applicable();
|
|
debug_assert!(
|
|
!kid.base().flags.is_float(),
|
|
"table cells should never float"
|
|
);
|
|
kid.assign_block_size_for_inorder_child_if_necessary(
|
|
layout_context,
|
|
thread_id,
|
|
content_box,
|
|
);
|
|
|
|
let mut row_span;
|
|
let column_span;
|
|
let cell_total;
|
|
{
|
|
let cell = kid.as_mut_table_cell();
|
|
row_span = cell.row_span;
|
|
column_span = cell.column_span as usize;
|
|
cell_total = cell.total_block_size();
|
|
}
|
|
let child_node = kid.mut_base();
|
|
child_node.position.start.b = Au(0);
|
|
let mut cell_block_size_pressure = max(cell_total, child_node.position.size.block);
|
|
|
|
if row_span != 1 {
|
|
if incoming_rowspan_data.len() <= col {
|
|
incoming_rowspan_data.resize(col + 1, Au(0));
|
|
}
|
|
let border_sizes_spanned =
|
|
get_spanned_border_size(border_info, row_index, &mut row_span);
|
|
|
|
cell_block_size_pressure -= border_sizes_spanned;
|
|
|
|
// XXXManishearth in case this row covers more than cell_block_size_pressure / row_span
|
|
// anyway, we should use that to reduce the pressure on future rows. This will
|
|
// require an extra slow-path loop, sadly.
|
|
cell_block_size_pressure /= row_span as i32;
|
|
incoming_rowspan_data[col] = cell_block_size_pressure;
|
|
}
|
|
|
|
max_block_size = max(max_block_size, cell_block_size_pressure);
|
|
col += column_span;
|
|
}
|
|
include_sizes_from_previous_rows(
|
|
&mut col,
|
|
&self.incoming_rowspan,
|
|
incoming_rowspan_data,
|
|
&mut max_block_size,
|
|
);
|
|
|
|
let mut block_size = max_block_size;
|
|
// TODO: Percentage block-size
|
|
block_size = match MaybeAuto::from_style(
|
|
self.block_flow.fragment.style().content_block_size(),
|
|
Au(0),
|
|
) {
|
|
MaybeAuto::Auto => block_size,
|
|
MaybeAuto::Specified(value) => max(value, block_size),
|
|
};
|
|
block_size
|
|
}
|
|
|
|
pub fn assign_block_size_to_self_and_children(
|
|
&mut self,
|
|
sizes: &[TableRowSizeData],
|
|
index: usize,
|
|
) {
|
|
// Assign the block-size of kid fragments, which is the same value as own block-size.
|
|
let block_size = sizes[index].size;
|
|
for kid in self.block_flow.base.child_iter_mut() {
|
|
let child_table_cell = kid.as_mut_table_cell();
|
|
let block_size = if child_table_cell.row_span != 1 {
|
|
let mut row_span = child_table_cell.row_span;
|
|
let border_sizes_spanned = get_spanned_border_size(sizes, index, &mut row_span);
|
|
let row_sizes = sizes[index..]
|
|
.iter()
|
|
.take(row_span as usize)
|
|
.fold(Au(0), |accum, r| accum + r.size);
|
|
row_sizes + border_sizes_spanned
|
|
} else {
|
|
block_size
|
|
};
|
|
{
|
|
let kid_fragment = child_table_cell.mut_fragment();
|
|
let mut position = kid_fragment.border_box;
|
|
position.size.block = block_size;
|
|
kid_fragment.border_box = position;
|
|
}
|
|
|
|
// Assign the child's block size.
|
|
child_table_cell.block_flow.base.position.size.block = block_size;
|
|
|
|
// Now we know the cell height, vertical align the cell's children.
|
|
child_table_cell.valign_children();
|
|
|
|
// Write in the size of the relative containing block for children. (This
|
|
// information is also needed to handle RTL.)
|
|
child_table_cell
|
|
.block_flow
|
|
.base
|
|
.early_absolute_position_info = EarlyAbsolutePositionInfo {
|
|
relative_containing_block_size: self.block_flow.fragment.content_box().size,
|
|
relative_containing_block_mode: self.block_flow.fragment.style().writing_mode,
|
|
};
|
|
}
|
|
|
|
// Assign the block-size of own fragment
|
|
let mut position = self.block_flow.fragment.border_box;
|
|
position.size.block = block_size;
|
|
self.block_flow.fragment.border_box = position;
|
|
self.block_flow.base.position.size.block = block_size;
|
|
}
|
|
|
|
pub fn populate_collapsed_border_spacing<'a, I>(
|
|
&mut self,
|
|
collapsed_inline_direction_border_widths_for_table: &[Au],
|
|
collapsed_block_direction_border_widths_for_table: &mut Peekable<I>,
|
|
) where
|
|
I: Iterator<Item = &'a Au>,
|
|
{
|
|
self.collapsed_border_spacing.inline.clear();
|
|
self.collapsed_border_spacing.inline.extend(
|
|
collapsed_inline_direction_border_widths_for_table
|
|
.into_iter()
|
|
.map(|x| *x),
|
|
);
|
|
|
|
if let Some(collapsed_block_direction_border_width_for_table) =
|
|
collapsed_block_direction_border_widths_for_table.next()
|
|
{
|
|
self.collapsed_border_spacing.block_start =
|
|
*collapsed_block_direction_border_width_for_table
|
|
}
|
|
if let Some(collapsed_block_direction_border_width_for_table) =
|
|
collapsed_block_direction_border_widths_for_table.peek()
|
|
{
|
|
self.collapsed_border_spacing.block_end =
|
|
**collapsed_block_direction_border_width_for_table
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct TableRowSizeData {
|
|
/// The block-size of the row.
|
|
pub size: Au,
|
|
/// Border spacing up to this row (not including spacing below the row)
|
|
pub cumulative_border_spacing: Au,
|
|
/// The "segment" of the table it is in. Tables containing
|
|
/// both row groups and rows have the bare rows grouped in
|
|
/// segments separated by row groups. It's helpful to look
|
|
/// at these as if they are rowgroups themselves.
|
|
///
|
|
/// This is enough information for us to be able to check whether we
|
|
/// are in a case where we are overflowing a rowgroup with rowspan,
|
|
/// however calculating the amount of overflow requires lookahead.
|
|
pub rowgroup_id: u32,
|
|
}
|
|
|
|
/// Given an array of (_, cumulative_border_size), the index of the
|
|
/// current row, and the >1 row_span of the cell, calculate the amount of
|
|
/// border-spacing spanned by the row. In case the rowspan was larger
|
|
/// than required, this will fix it up.
|
|
fn get_spanned_border_size(sizes: &[TableRowSizeData], row_index: usize, row_span: &mut u32) -> Au {
|
|
// A zero rowspan is functionally equivalent to rowspan=infinity
|
|
if *row_span == 0 || row_index + *row_span as usize > sizes.len() {
|
|
*row_span = (sizes.len() - row_index) as u32;
|
|
}
|
|
let mut last_row_idx = row_index + *row_span as usize - 1;
|
|
// This is a slow path and should be rare -- this should only get triggered
|
|
// when you use `rowspan=0` or an overlarge rowspan in a table with
|
|
// mixed rows + rowgroups
|
|
if sizes[last_row_idx].rowgroup_id != sizes[row_index].rowgroup_id {
|
|
// XXXManishearth this loop can be avoided by also storing
|
|
// a "last_rowgroup_at" index so we can leapfrog back quickly
|
|
*row_span = sizes[row_index..last_row_idx + 1]
|
|
.iter()
|
|
.position(|s| s.rowgroup_id != sizes[row_index].rowgroup_id)
|
|
.unwrap() as u32;
|
|
last_row_idx = row_index + *row_span as usize - 1;
|
|
}
|
|
sizes[last_row_idx].cumulative_border_spacing - sizes[row_index].cumulative_border_spacing
|
|
}
|
|
|
|
impl Flow for TableRowFlow {
|
|
fn class(&self) -> FlowClass {
|
|
FlowClass::TableRow
|
|
}
|
|
|
|
fn as_mut_table_row(&mut self) -> &mut TableRowFlow {
|
|
self
|
|
}
|
|
|
|
fn as_table_row(&self) -> &TableRowFlow {
|
|
self
|
|
}
|
|
|
|
fn as_mut_block(&mut self) -> &mut BlockFlow {
|
|
&mut self.block_flow
|
|
}
|
|
|
|
fn as_block(&self) -> &BlockFlow {
|
|
&self.block_flow
|
|
}
|
|
|
|
/// Recursively (bottom-up) determines the context's preferred and minimum inline-sizes. When
|
|
/// called on this context, all child contexts have had their min/pref inline-sizes set. This
|
|
/// function must decide min/pref inline-sizes based on child context inline-sizes and
|
|
/// dimensions of any fragments it is responsible for flowing.
|
|
/// Min/pref inline-sizes set by this function are used in automatic table layout calculation.
|
|
/// The specified column inline-sizes of children cells are used in fixed table layout
|
|
/// calculation.
|
|
fn bubble_inline_sizes(&mut self) {
|
|
let _scope = layout_debug_scope!(
|
|
"table_row::bubble_inline_sizes {:x}",
|
|
self.block_flow.base.debug_id()
|
|
);
|
|
|
|
// Bubble up the specified inline-sizes from child table cells.
|
|
let (mut min_inline_size, mut pref_inline_size) = (Au(0), Au(0));
|
|
let collapsing_borders = self
|
|
.block_flow
|
|
.fragment
|
|
.style()
|
|
.get_inherited_table()
|
|
.border_collapse ==
|
|
BorderCollapse::Collapse;
|
|
let row_style = &*self.block_flow.fragment.style;
|
|
self.preliminary_collapsed_borders
|
|
.reset(CollapsedBorder::inline_start(
|
|
&row_style,
|
|
CollapsedBorderProvenance::FromTableRow,
|
|
));
|
|
|
|
{
|
|
let children_count = self.block_flow.base.children.len();
|
|
let mut iterator = self.block_flow.base.child_iter_mut().enumerate().peekable();
|
|
while let Some((i, kid)) = iterator.next() {
|
|
assert!(kid.is_table_cell());
|
|
|
|
// Collect the specified column inline-size of the cell. This is used in both
|
|
// fixed and automatic table layout calculation.
|
|
let child_specified_inline_size;
|
|
let child_column_span;
|
|
let child_row_span;
|
|
{
|
|
let child_table_cell = kid.as_mut_table_cell();
|
|
child_specified_inline_size = child_table_cell
|
|
.block_flow
|
|
.fragment
|
|
.style
|
|
.content_inline_size();
|
|
child_column_span = child_table_cell.column_span;
|
|
child_row_span = child_table_cell.row_span;
|
|
|
|
// Perform border collapse if necessary.
|
|
if collapsing_borders {
|
|
perform_inline_direction_border_collapse_for_row(
|
|
row_style,
|
|
children_count,
|
|
i,
|
|
child_table_cell,
|
|
&mut iterator,
|
|
&mut self.preliminary_collapsed_borders,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Collect minimum and preferred inline-sizes of the cell for automatic table layout
|
|
// calculation.
|
|
let child_base = kid.mut_base();
|
|
let child_column_inline_size = ColumnIntrinsicInlineSize {
|
|
minimum_length: match child_specified_inline_size {
|
|
LengthOrPercentageOrAuto::Auto |
|
|
LengthOrPercentageOrAuto::Calc(_) |
|
|
LengthOrPercentageOrAuto::Percentage(_) => {
|
|
child_base.intrinsic_inline_sizes.minimum_inline_size
|
|
},
|
|
LengthOrPercentageOrAuto::Length(length) => Au::from(length),
|
|
},
|
|
percentage: match child_specified_inline_size {
|
|
LengthOrPercentageOrAuto::Auto |
|
|
LengthOrPercentageOrAuto::Calc(_) |
|
|
LengthOrPercentageOrAuto::Length(_) => 0.0,
|
|
LengthOrPercentageOrAuto::Percentage(percentage) => percentage.0,
|
|
},
|
|
preferred: child_base.intrinsic_inline_sizes.preferred_inline_size,
|
|
constrained: match child_specified_inline_size {
|
|
LengthOrPercentageOrAuto::Length(_) => true,
|
|
LengthOrPercentageOrAuto::Auto |
|
|
LengthOrPercentageOrAuto::Calc(_) |
|
|
LengthOrPercentageOrAuto::Percentage(_) => false,
|
|
},
|
|
};
|
|
min_inline_size = min_inline_size + child_column_inline_size.minimum_length;
|
|
pref_inline_size = pref_inline_size + child_column_inline_size.preferred;
|
|
self.cell_intrinsic_inline_sizes
|
|
.push(CellIntrinsicInlineSize {
|
|
column_size: child_column_inline_size,
|
|
column_span: child_column_span,
|
|
row_span: child_row_span,
|
|
});
|
|
}
|
|
}
|
|
|
|
self.block_flow
|
|
.base
|
|
.intrinsic_inline_sizes
|
|
.minimum_inline_size = min_inline_size;
|
|
self.block_flow
|
|
.base
|
|
.intrinsic_inline_sizes
|
|
.preferred_inline_size = max(min_inline_size, pref_inline_size);
|
|
}
|
|
|
|
fn assign_inline_sizes(&mut self, layout_context: &LayoutContext) {
|
|
let _scope = layout_debug_scope!(
|
|
"table_row::assign_inline_sizes {:x}",
|
|
self.block_flow.base.debug_id()
|
|
);
|
|
debug!(
|
|
"assign_inline_sizes({}): assigning inline_size for flow",
|
|
"table_row"
|
|
);
|
|
|
|
let shared_context = layout_context.shared_context();
|
|
// The position was set to the containing block by the flow's parent.
|
|
let containing_block_inline_size = self.block_flow.base.block_container_inline_size;
|
|
// FIXME: In case of border-collapse: collapse, inline_start_content_edge should be
|
|
// border_inline_start.
|
|
let inline_start_content_edge = Au(0);
|
|
let inline_end_content_edge = Au(0);
|
|
|
|
let inline_size_computer = InternalTable;
|
|
inline_size_computer.compute_used_inline_size(
|
|
&mut self.block_flow,
|
|
shared_context,
|
|
containing_block_inline_size,
|
|
);
|
|
|
|
// Spread out the completed inline sizes among columns with spans > 1.
|
|
let num_columns = self.column_computed_inline_sizes.len();
|
|
let mut computed_inline_size_for_cells = Vec::with_capacity(num_columns);
|
|
let mut col = 0;
|
|
|
|
for cell_intrinsic_inline_size in &self.cell_intrinsic_inline_sizes {
|
|
// Skip any column occupied by a cell from a previous row.
|
|
while col < self.incoming_rowspan.len() && self.incoming_rowspan[col] != 1 {
|
|
let size = match self.column_computed_inline_sizes.get(col) {
|
|
Some(column_computed_inline_size) => *column_computed_inline_size,
|
|
None => ColumnComputedInlineSize { size: Au(0) }, // See FIXME below.
|
|
};
|
|
computed_inline_size_for_cells.push(size);
|
|
col += 1;
|
|
}
|
|
// Start with the computed inline size for the first column in the span.
|
|
let mut column_computed_inline_size = match self.column_computed_inline_sizes.get(col) {
|
|
Some(column_computed_inline_size) => *column_computed_inline_size,
|
|
None => {
|
|
// We're in fixed layout mode and there are more cells in this row than
|
|
// columns we know about. According to CSS 2.1 § 17.5.2.1, the behavior is
|
|
// now undefined. So just use zero.
|
|
//
|
|
// FIXME(pcwalton): $10 says this isn't Web compatible.
|
|
ColumnComputedInlineSize { size: Au(0) }
|
|
},
|
|
};
|
|
col += 1;
|
|
|
|
// Add in computed inline sizes for any extra columns in the span.
|
|
for _ in 1..cell_intrinsic_inline_size.column_span {
|
|
let extra_column_computed_inline_size =
|
|
match self.column_computed_inline_sizes.get(col) {
|
|
Some(column_computed_inline_size) => column_computed_inline_size,
|
|
None => break,
|
|
};
|
|
column_computed_inline_size.size = column_computed_inline_size.size +
|
|
extra_column_computed_inline_size.size +
|
|
self.spacing.horizontal();
|
|
col += 1;
|
|
}
|
|
|
|
computed_inline_size_for_cells.push(column_computed_inline_size)
|
|
}
|
|
|
|
// Set up border collapse info.
|
|
let border_collapse_info = match self
|
|
.block_flow
|
|
.fragment
|
|
.style()
|
|
.get_inherited_table()
|
|
.border_collapse
|
|
{
|
|
BorderCollapse::Collapse => Some(BorderCollapseInfoForChildTableCell {
|
|
collapsed_borders_for_row: &self.final_collapsed_borders,
|
|
collapsed_border_spacing_for_row: &self.collapsed_border_spacing,
|
|
}),
|
|
BorderCollapse::Separate => None,
|
|
};
|
|
|
|
// Push those inline sizes down to the cells.
|
|
let spacing = self.spacing;
|
|
let row_writing_mode = self.block_flow.base.writing_mode;
|
|
let table_writing_mode = self.table_writing_mode;
|
|
let incoming_rowspan = &self.incoming_rowspan;
|
|
let mut column_index = 0;
|
|
|
|
self.block_flow.propagate_assigned_inline_size_to_children(
|
|
shared_context,
|
|
inline_start_content_edge,
|
|
inline_end_content_edge,
|
|
containing_block_inline_size,
|
|
|child_flow,
|
|
child_index,
|
|
content_inline_size,
|
|
_writing_mode,
|
|
inline_start_margin_edge,
|
|
inline_end_margin_edge| {
|
|
set_inline_position_of_child_flow(
|
|
child_flow,
|
|
child_index,
|
|
&mut column_index,
|
|
incoming_rowspan,
|
|
row_writing_mode,
|
|
table_writing_mode,
|
|
&computed_inline_size_for_cells,
|
|
&spacing,
|
|
&border_collapse_info,
|
|
content_inline_size,
|
|
inline_start_margin_edge,
|
|
inline_end_margin_edge,
|
|
);
|
|
},
|
|
)
|
|
}
|
|
|
|
fn assign_block_size(&mut self, _: &LayoutContext) {
|
|
// the surrounding table or rowgroup does this
|
|
}
|
|
|
|
fn compute_stacking_relative_position(&mut self, layout_context: &LayoutContext) {
|
|
self.block_flow
|
|
.compute_stacking_relative_position(layout_context)
|
|
}
|
|
|
|
fn update_late_computed_inline_position_if_necessary(&mut self, inline_position: Au) {
|
|
self.block_flow
|
|
.update_late_computed_inline_position_if_necessary(inline_position)
|
|
}
|
|
|
|
fn update_late_computed_block_position_if_necessary(&mut self, block_position: Au) {
|
|
self.block_flow
|
|
.update_late_computed_block_position_if_necessary(block_position)
|
|
}
|
|
|
|
fn build_display_list(&mut self, _: &mut DisplayListBuildState) {
|
|
use style::servo::restyle_damage::ServoRestyleDamage;
|
|
// handled in TableCellStyleInfo::build_display_list
|
|
// we skip setting the damage in TableCellStyleInfo::build_display_list()
|
|
// because we only have immutable access
|
|
self.block_flow
|
|
.fragment
|
|
.restyle_damage
|
|
.remove(ServoRestyleDamage::REPAINT);
|
|
}
|
|
|
|
fn collect_stacking_contexts(&mut self, state: &mut StackingContextCollectionState) {
|
|
self.block_flow
|
|
.collect_stacking_contexts_for_block(state, StackingContextCollectionFlags::empty());
|
|
}
|
|
|
|
fn repair_style(&mut self, new_style: &crate::ServoArc<ComputedValues>) {
|
|
self.block_flow.repair_style(new_style)
|
|
}
|
|
|
|
fn compute_overflow(&self) -> Overflow {
|
|
self.block_flow.compute_overflow()
|
|
}
|
|
|
|
fn contains_roots_of_absolute_flow_tree(&self) -> bool {
|
|
self.block_flow.contains_roots_of_absolute_flow_tree()
|
|
}
|
|
|
|
fn is_absolute_containing_block(&self) -> bool {
|
|
self.block_flow.is_absolute_containing_block()
|
|
}
|
|
|
|
fn generated_containing_block_size(&self, flow: OpaqueFlow) -> LogicalSize<Au> {
|
|
self.block_flow.generated_containing_block_size(flow)
|
|
}
|
|
|
|
fn iterate_through_fragment_border_boxes(
|
|
&self,
|
|
iterator: &mut FragmentBorderBoxIterator,
|
|
level: i32,
|
|
stacking_context_position: &Point2D<Au>,
|
|
) {
|
|
self.block_flow.iterate_through_fragment_border_boxes(
|
|
iterator,
|
|
level,
|
|
stacking_context_position,
|
|
)
|
|
}
|
|
|
|
fn mutate_fragments(&mut self, mutator: &mut FnMut(&mut Fragment)) {
|
|
self.block_flow.mutate_fragments(mutator)
|
|
}
|
|
|
|
fn print_extra_flow_children(&self, print_tree: &mut PrintTree) {
|
|
self.block_flow.print_extra_flow_children(print_tree);
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for TableRowFlow {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(f, "TableRowFlow: {:?}", self.block_flow)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct CollapsedBordersForRow {
|
|
/// The size of this vector should be equal to the number of cells plus one.
|
|
pub inline: Vec<CollapsedBorder>,
|
|
/// The size of this vector should be equal to the number of cells.
|
|
pub block_start: Vec<CollapsedBorder>,
|
|
/// The size of this vector should be equal to the number of cells.
|
|
pub block_end: Vec<CollapsedBorder>,
|
|
}
|
|
|
|
impl CollapsedBordersForRow {
|
|
pub fn new() -> CollapsedBordersForRow {
|
|
CollapsedBordersForRow {
|
|
inline: Vec::new(),
|
|
block_start: Vec::new(),
|
|
block_end: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn reset(&mut self, first_inline_border: CollapsedBorder) {
|
|
self.inline.clear();
|
|
self.inline.push(first_inline_border);
|
|
self.block_start.clear();
|
|
self.block_end.clear()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct CollapsedBorderSpacingForRow {
|
|
/// The spacing in between each column.
|
|
inline: Vec<Au>,
|
|
/// The spacing above this row.
|
|
pub block_start: Au,
|
|
/// The spacing below this row.
|
|
block_end: Au,
|
|
}
|
|
|
|
impl CollapsedBorderSpacingForRow {
|
|
fn new() -> CollapsedBorderSpacingForRow {
|
|
CollapsedBorderSpacingForRow {
|
|
inline: Vec::new(),
|
|
block_start: Au(0),
|
|
block_end: Au(0),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// All aspects of a border that can collapse with adjacent borders. See CSS 2.1 § 17.6.2.1.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct CollapsedBorder {
|
|
/// The style of the border.
|
|
pub style: BorderStyle,
|
|
/// The width of the border.
|
|
pub width: Au,
|
|
/// The color of the border.
|
|
pub color: Color,
|
|
/// The type of item that this border comes from.
|
|
pub provenance: CollapsedBorderProvenance,
|
|
}
|
|
|
|
impl Serialize for CollapsedBorder {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
serializer.serialize_unit()
|
|
}
|
|
}
|
|
|
|
/// Where a border style comes from.
|
|
///
|
|
/// The integer values here correspond to the border conflict resolution rules in CSS 2.1 §
|
|
/// 17.6.2.1. Higher values override lower values.
|
|
// FIXME(#8586): FromTableRow, FromTableRowGroup, FromTableColumn,
|
|
// FromTableColumnGroup are unused
|
|
#[allow(dead_code)]
|
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
|
|
pub enum CollapsedBorderProvenance {
|
|
FromPreviousTableCell = 6,
|
|
FromNextTableCell = 5,
|
|
FromTableRow = 4,
|
|
FromTableRowGroup = 3,
|
|
FromTableColumn = 2,
|
|
FromTableColumnGroup = 1,
|
|
FromTable = 0,
|
|
}
|
|
|
|
impl CollapsedBorder {
|
|
/// Creates a collapsible border style for no border.
|
|
pub fn new() -> CollapsedBorder {
|
|
CollapsedBorder {
|
|
style: BorderStyle::None,
|
|
width: Au(0),
|
|
color: Color::transparent(),
|
|
provenance: CollapsedBorderProvenance::FromTable,
|
|
}
|
|
}
|
|
|
|
/// Creates a collapsed border from the block-start border described in the given CSS style
|
|
/// object.
|
|
fn top(css_style: &ComputedValues, provenance: CollapsedBorderProvenance) -> CollapsedBorder {
|
|
CollapsedBorder {
|
|
style: css_style.get_border().border_top_style,
|
|
width: Au::from(css_style.get_border().border_top_width),
|
|
color: css_style.get_border().border_top_color,
|
|
provenance: provenance,
|
|
}
|
|
}
|
|
|
|
/// Creates a collapsed border style from the right border described in the given CSS style
|
|
/// object.
|
|
fn right(css_style: &ComputedValues, provenance: CollapsedBorderProvenance) -> CollapsedBorder {
|
|
CollapsedBorder {
|
|
style: css_style.get_border().border_right_style,
|
|
width: Au::from(css_style.get_border().border_right_width),
|
|
color: css_style.get_border().border_right_color,
|
|
provenance: provenance,
|
|
}
|
|
}
|
|
|
|
/// Creates a collapsed border style from the bottom border described in the given CSS style
|
|
/// object.
|
|
fn bottom(
|
|
css_style: &ComputedValues,
|
|
provenance: CollapsedBorderProvenance,
|
|
) -> CollapsedBorder {
|
|
CollapsedBorder {
|
|
style: css_style.get_border().border_bottom_style,
|
|
width: Au::from(css_style.get_border().border_bottom_width),
|
|
color: css_style.get_border().border_bottom_color,
|
|
provenance: provenance,
|
|
}
|
|
}
|
|
|
|
/// Creates a collapsed border style from the left border described in the given CSS style
|
|
/// object.
|
|
fn left(css_style: &ComputedValues, provenance: CollapsedBorderProvenance) -> CollapsedBorder {
|
|
CollapsedBorder {
|
|
style: css_style.get_border().border_left_style,
|
|
width: Au::from(css_style.get_border().border_left_width),
|
|
color: css_style.get_border().border_left_color,
|
|
provenance: provenance,
|
|
}
|
|
}
|
|
|
|
/// Creates a collapsed border style from the given physical side.
|
|
fn from_side(
|
|
side: PhysicalSide,
|
|
css_style: &ComputedValues,
|
|
provenance: CollapsedBorderProvenance,
|
|
) -> CollapsedBorder {
|
|
match side {
|
|
PhysicalSide::Top => CollapsedBorder::top(css_style, provenance),
|
|
PhysicalSide::Right => CollapsedBorder::right(css_style, provenance),
|
|
PhysicalSide::Bottom => CollapsedBorder::bottom(css_style, provenance),
|
|
PhysicalSide::Left => CollapsedBorder::left(css_style, provenance),
|
|
}
|
|
}
|
|
|
|
/// Creates a collapsed border style from the inline-start border described in the given CSS
|
|
/// style object.
|
|
pub fn inline_start(
|
|
css_style: &ComputedValues,
|
|
provenance: CollapsedBorderProvenance,
|
|
) -> CollapsedBorder {
|
|
CollapsedBorder::from_side(
|
|
css_style.writing_mode.inline_start_physical_side(),
|
|
css_style,
|
|
provenance,
|
|
)
|
|
}
|
|
|
|
/// Creates a collapsed border style from the inline-start border described in the given CSS
|
|
/// style object.
|
|
pub fn inline_end(
|
|
css_style: &ComputedValues,
|
|
provenance: CollapsedBorderProvenance,
|
|
) -> CollapsedBorder {
|
|
CollapsedBorder::from_side(
|
|
css_style.writing_mode.inline_end_physical_side(),
|
|
css_style,
|
|
provenance,
|
|
)
|
|
}
|
|
|
|
/// Creates a collapsed border style from the block-start border described in the given CSS
|
|
/// style object.
|
|
pub fn block_start(
|
|
css_style: &ComputedValues,
|
|
provenance: CollapsedBorderProvenance,
|
|
) -> CollapsedBorder {
|
|
CollapsedBorder::from_side(
|
|
css_style.writing_mode.block_start_physical_side(),
|
|
css_style,
|
|
provenance,
|
|
)
|
|
}
|
|
|
|
/// Creates a collapsed border style from the block-end border described in the given CSS style
|
|
/// object.
|
|
pub fn block_end(
|
|
css_style: &ComputedValues,
|
|
provenance: CollapsedBorderProvenance,
|
|
) -> CollapsedBorder {
|
|
CollapsedBorder::from_side(
|
|
css_style.writing_mode.block_end_physical_side(),
|
|
css_style,
|
|
provenance,
|
|
)
|
|
}
|
|
|
|
/// If `other` has a higher priority per CSS 2.1 § 17.6.2.1, replaces `self` with it.
|
|
pub fn combine(&mut self, other: &CollapsedBorder) {
|
|
match (self.style, other.style) {
|
|
// Step 1.
|
|
(BorderStyle::Hidden, _) => {},
|
|
(_, BorderStyle::Hidden) => *self = *other,
|
|
// Step 2.
|
|
(BorderStyle::None, _) => *self = *other,
|
|
(_, BorderStyle::None) => {},
|
|
// Step 3.
|
|
_ if self.width > other.width => {},
|
|
_ if self.width < other.width => *self = *other,
|
|
(this_style, other_style) if this_style > other_style => {},
|
|
(this_style, other_style) if this_style < other_style => *self = *other,
|
|
// Step 4.
|
|
_ if (self.provenance as i8) >= other.provenance as i8 => {},
|
|
_ => *self = *other,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Pushes column inline size, incoming rowspan, and border collapse info down to a child.
|
|
pub fn propagate_column_inline_sizes_to_child(
|
|
child_flow: &mut Flow,
|
|
table_writing_mode: WritingMode,
|
|
column_computed_inline_sizes: &[ColumnComputedInlineSize],
|
|
border_spacing: &BorderSpacing,
|
|
incoming_rowspan: &mut Vec<u32>,
|
|
) {
|
|
// If the child is a row group or a row, the column inline-size and rowspan info should be copied from its
|
|
// parent.
|
|
//
|
|
// FIXME(pcwalton): This seems inefficient. Reference count it instead?
|
|
match child_flow.class() {
|
|
FlowClass::TableRowGroup => {
|
|
incoming_rowspan.clear();
|
|
let child_table_rowgroup_flow = child_flow.as_mut_table_rowgroup();
|
|
child_table_rowgroup_flow.spacing = *border_spacing;
|
|
for kid in child_table_rowgroup_flow.block_flow.base.child_iter_mut() {
|
|
propagate_column_inline_sizes_to_child(
|
|
kid,
|
|
table_writing_mode,
|
|
column_computed_inline_sizes,
|
|
border_spacing,
|
|
incoming_rowspan,
|
|
);
|
|
}
|
|
},
|
|
FlowClass::TableRow => {
|
|
let child_table_row_flow = child_flow.as_mut_table_row();
|
|
child_table_row_flow.column_computed_inline_sizes =
|
|
column_computed_inline_sizes.to_vec();
|
|
child_table_row_flow.spacing = *border_spacing;
|
|
child_table_row_flow.table_writing_mode = table_writing_mode;
|
|
child_table_row_flow.incoming_rowspan = incoming_rowspan.clone();
|
|
|
|
// Update the incoming rowspan for the next row.
|
|
let mut col = 0;
|
|
for cell in &child_table_row_flow.cell_intrinsic_inline_sizes {
|
|
// Skip any column occupied by a cell from a previous row.
|
|
while col < incoming_rowspan.len() && incoming_rowspan[col] != 1 {
|
|
if incoming_rowspan[col] > 1 {
|
|
incoming_rowspan[col] -= 1;
|
|
}
|
|
col += 1;
|
|
}
|
|
for _ in 0..cell.column_span {
|
|
if col < incoming_rowspan.len() && incoming_rowspan[col] > 1 {
|
|
incoming_rowspan[col] -= 1;
|
|
}
|
|
// If this cell spans later rows, record its rowspan.
|
|
if cell.row_span != 1 {
|
|
if incoming_rowspan.len() < col + 1 {
|
|
incoming_rowspan.resize(col + 1, 1);
|
|
}
|
|
// HTML § 4.9.11: For rowspan, the value 0 means the cell is to span all
|
|
// the remaining rows in the rowgroup.
|
|
if cell.row_span > incoming_rowspan[col] || cell.row_span == 0 {
|
|
incoming_rowspan[col] = cell.row_span;
|
|
}
|
|
}
|
|
col += 1;
|
|
}
|
|
}
|
|
},
|
|
c => warn!("unexpected flow in table {:?}", c),
|
|
}
|
|
}
|
|
|
|
/// Lay out table cells inline according to the computer column sizes.
|
|
fn set_inline_position_of_child_flow(
|
|
child_flow: &mut Flow,
|
|
child_index: usize,
|
|
column_index: &mut usize,
|
|
incoming_rowspan: &[u32],
|
|
row_writing_mode: WritingMode,
|
|
table_writing_mode: WritingMode,
|
|
column_computed_inline_sizes: &[ColumnComputedInlineSize],
|
|
border_spacing: &BorderSpacing,
|
|
border_collapse_info: &Option<BorderCollapseInfoForChildTableCell>,
|
|
parent_content_inline_size: Au,
|
|
inline_start_margin_edge: &mut Au,
|
|
inline_end_margin_edge: &mut Au,
|
|
) {
|
|
if !child_flow.is_table_cell() {
|
|
return;
|
|
}
|
|
|
|
let reverse_column_order = table_writing_mode.is_bidi_ltr() != row_writing_mode.is_bidi_ltr();
|
|
|
|
// Advance past any column occupied by a cell from a previous row.
|
|
while *column_index < incoming_rowspan.len() && incoming_rowspan[*column_index] != 1 {
|
|
let column_inline_size = column_computed_inline_sizes[*column_index].size;
|
|
let border_inline_size = match *border_collapse_info {
|
|
Some(_) => Au(0), // FIXME: Make collapsed borders account for colspan/rowspan.
|
|
None => border_spacing.horizontal(),
|
|
};
|
|
if reverse_column_order {
|
|
*inline_end_margin_edge += column_inline_size + border_inline_size;
|
|
} else {
|
|
*inline_start_margin_edge += column_inline_size + border_inline_size;
|
|
}
|
|
*column_index += 1;
|
|
}
|
|
|
|
// Handle border collapsing, if necessary.
|
|
let child_table_cell = child_flow.as_mut_table_cell();
|
|
match *border_collapse_info {
|
|
Some(ref border_collapse_info) => {
|
|
// Write in the child's border collapse state.
|
|
child_table_cell.collapsed_borders = CollapsedBordersForCell {
|
|
inline_start_border: border_collapse_info
|
|
.collapsed_borders_for_row
|
|
.inline
|
|
.get(child_index)
|
|
.map_or(CollapsedBorder::new(), |x| *x),
|
|
inline_end_border: border_collapse_info
|
|
.collapsed_borders_for_row
|
|
.inline
|
|
.get(child_index + 1)
|
|
.map_or(CollapsedBorder::new(), |x| *x),
|
|
block_start_border: border_collapse_info
|
|
.collapsed_borders_for_row
|
|
.block_start
|
|
.get(child_index)
|
|
.map_or(CollapsedBorder::new(), |x| *x),
|
|
block_end_border: border_collapse_info
|
|
.collapsed_borders_for_row
|
|
.block_end
|
|
.get(child_index)
|
|
.map_or(CollapsedBorder::new(), |x| *x),
|
|
inline_start_width: border_collapse_info
|
|
.collapsed_border_spacing_for_row
|
|
.inline
|
|
.get(child_index)
|
|
.map_or(Au(0), |x| *x),
|
|
inline_end_width: border_collapse_info
|
|
.collapsed_border_spacing_for_row
|
|
.inline
|
|
.get(child_index + 1)
|
|
.map_or(Au(0), |x| *x),
|
|
block_start_width: border_collapse_info
|
|
.collapsed_border_spacing_for_row
|
|
.block_start,
|
|
block_end_width: border_collapse_info
|
|
.collapsed_border_spacing_for_row
|
|
.block_end,
|
|
};
|
|
|
|
// Move over past the collapsed border.
|
|
if reverse_column_order {
|
|
*inline_end_margin_edge += child_table_cell.collapsed_borders.inline_start_width;
|
|
} else {
|
|
*inline_start_margin_edge += child_table_cell.collapsed_borders.inline_start_width;
|
|
}
|
|
},
|
|
None => {
|
|
// Take spacing into account.
|
|
if reverse_column_order {
|
|
*inline_end_margin_edge += border_spacing.horizontal();
|
|
} else {
|
|
*inline_start_margin_edge += border_spacing.horizontal();
|
|
}
|
|
},
|
|
}
|
|
|
|
let column_inline_size = column_computed_inline_sizes[*column_index].size;
|
|
*column_index += 1;
|
|
|
|
let kid_base = &mut child_table_cell.block_flow.base;
|
|
kid_base.block_container_inline_size = column_inline_size;
|
|
|
|
if reverse_column_order {
|
|
// Columns begin from the inline-end edge.
|
|
kid_base.position.start.i =
|
|
parent_content_inline_size - *inline_end_margin_edge - column_inline_size;
|
|
*inline_end_margin_edge += column_inline_size;
|
|
} else {
|
|
// Columns begin from the inline-start edge.
|
|
kid_base.position.start.i = *inline_start_margin_edge;
|
|
*inline_start_margin_edge += column_inline_size;
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub struct BorderCollapseInfoForChildTableCell<'a> {
|
|
collapsed_borders_for_row: &'a CollapsedBordersForRow,
|
|
collapsed_border_spacing_for_row: &'a CollapsedBorderSpacingForRow,
|
|
}
|
|
|
|
/// Performs border-collapse in the inline direction for all the cells' inside borders in the
|
|
/// inline-direction cells and propagates the outside borders (the far left and right) up to the
|
|
/// table row. This is done eagerly here so that at least the inline inside border collapse
|
|
/// computations can be parallelized across all the rows of the table.
|
|
fn perform_inline_direction_border_collapse_for_row(
|
|
row_style: &ComputedValues,
|
|
children_count: usize,
|
|
child_index: usize,
|
|
child_table_cell: &mut TableCellFlow,
|
|
iterator: &mut Peekable<Enumerate<MutFlowListIterator>>,
|
|
preliminary_collapsed_borders: &mut CollapsedBordersForRow,
|
|
) {
|
|
// In the first cell, combine its border with the one coming from the row.
|
|
if child_index == 0 {
|
|
let first_inline_border = &mut preliminary_collapsed_borders.inline[0];
|
|
first_inline_border.combine(&CollapsedBorder::inline_start(
|
|
&*child_table_cell.block_flow.fragment.style,
|
|
CollapsedBorderProvenance::FromNextTableCell,
|
|
));
|
|
}
|
|
|
|
let inline_collapsed_border = preliminary_collapsed_borders.inline.push_or_set(
|
|
child_index + 1,
|
|
CollapsedBorder::inline_end(
|
|
&*child_table_cell.block_flow.fragment.style,
|
|
CollapsedBorderProvenance::FromPreviousTableCell,
|
|
),
|
|
);
|
|
|
|
if let Some(&(_, ref next_child_flow)) = iterator.peek() {
|
|
let next_child_flow = next_child_flow.as_block();
|
|
inline_collapsed_border.combine(&CollapsedBorder::inline_start(
|
|
&*next_child_flow.fragment.style,
|
|
CollapsedBorderProvenance::FromNextTableCell,
|
|
))
|
|
};
|
|
|
|
// In the last cell, also take into account the border that may
|
|
// come from the row.
|
|
if child_index + 1 == children_count {
|
|
inline_collapsed_border.combine(&CollapsedBorder::inline_end(
|
|
&row_style,
|
|
CollapsedBorderProvenance::FromTableRow,
|
|
));
|
|
}
|
|
|
|
let mut block_start_border = CollapsedBorder::block_start(
|
|
&*child_table_cell.block_flow.fragment.style,
|
|
CollapsedBorderProvenance::FromNextTableCell,
|
|
);
|
|
block_start_border.combine(&CollapsedBorder::block_start(
|
|
row_style,
|
|
CollapsedBorderProvenance::FromTableRow,
|
|
));
|
|
preliminary_collapsed_borders
|
|
.block_start
|
|
.push_or_set(child_index, block_start_border);
|
|
let mut block_end_border = CollapsedBorder::block_end(
|
|
&*child_table_cell.block_flow.fragment.style,
|
|
CollapsedBorderProvenance::FromPreviousTableCell,
|
|
);
|
|
block_end_border.combine(&CollapsedBorder::block_end(
|
|
row_style,
|
|
CollapsedBorderProvenance::FromTableRow,
|
|
));
|
|
|
|
preliminary_collapsed_borders
|
|
.block_end
|
|
.push_or_set(child_index, block_end_border);
|
|
}
|