servo: Merge #3609 - layout: Rewrite intrinsic inline-size and automatic table layout to match L. David Baron's work-in-progress specification (from pcwalton:tables); r=SimonSapin

http://dbaron.org/css/intrinsic/

Column spans are not yet supported.

This effectively adds support for percentage widths, and it also fixes
many bugs, improving the layout of Google and Wikipedia.

r? @SimonSapin

Source-Repo: https://github.com/servo/servo
Source-Revision: e2d7777c41135b71293c195d2a9d7a1bc2afd0ca
This commit is contained in:
Patrick Walton
2014-10-14 15:42:32 -06:00
parent 8adbc51eec
commit 9df551f68e
12 changed files with 998 additions and 613 deletions

View File

@@ -3,22 +3,30 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
//! CSS tables.
//!
//! This follows the "More Precise Definitions of Inline Layout and Table Layout" proposal written
//! by L. David Baron (Mozilla) here:
//!
//! http://dbaron.org/css/intrinsic/
//!
//! Hereafter this document is referred to as INTRINSIC.
#![deny(unsafe_block)]
use block::{BlockFlow, BlockNonReplaced, FloatNonReplaced, ISizeAndMarginsComputer};
use block::{ISizeConstraintInput, MarginsMayNotCollapse};
use block::{MarginsMayNotCollapse};
use construct::FlowConstructor;
use context::LayoutContext;
use floats::FloatKind;
use flow::{TableWrapperFlowClass, FlowClass, Flow, ImmutableFlowUtils};
use fragment::Fragment;
use model::{Specified, Auto, specified};
use table::ColumnInlineSize;
use wrapper::ThreadSafeLayoutNode;
use servo_util::geometry::Au;
use std::cmp::max;
use std::cmp::{max, min};
use std::fmt;
use style::CSSFloat;
use style::computed_values::{clear, float, table_layout};
#[deriving(Encodable)]
@@ -32,8 +40,8 @@ pub enum TableLayout {
pub struct TableWrapperFlow {
pub block_flow: BlockFlow,
/// Column inline-sizes
pub col_inline_sizes: Vec<Au>,
/// Inline-size information for each column.
pub column_inline_sizes: Vec<ColumnInlineSize>,
/// Table-layout property
pub table_layout: TableLayout,
@@ -52,7 +60,7 @@ impl TableWrapperFlow {
};
TableWrapperFlow {
block_flow: block_flow,
col_inline_sizes: vec!(),
column_inline_sizes: vec!(),
table_layout: table_layout
}
}
@@ -69,7 +77,7 @@ impl TableWrapperFlow {
};
TableWrapperFlow {
block_flow: block_flow,
col_inline_sizes: vec!(),
column_inline_sizes: vec!(),
table_layout: table_layout
}
}
@@ -87,7 +95,7 @@ impl TableWrapperFlow {
};
TableWrapperFlow {
block_flow: block_flow,
col_inline_sizes: vec!(),
column_inline_sizes: vec!(),
table_layout: table_layout
}
}
@@ -97,113 +105,87 @@ impl TableWrapperFlow {
self.block_flow.build_display_list_block(layout_context);
}
fn calculate_table_column_sizes(&mut self, mut input: ISizeConstraintInput)
-> ISizeConstraintInput {
let style = self.block_flow.fragment.style();
// Get inline-start and inline-end paddings, borders for table.
// We get these values from the fragment's style since table_wrapper doesn't have its own
// border or padding. input.available_inline_size is same as containing_block_inline_size
// in table_wrapper.
let padding = style.logical_padding();
let border = style.logical_border_width();
let padding_and_borders =
specified(padding.inline_start, input.available_inline_size) +
specified(padding.inline_end, input.available_inline_size) +
border.inline_start +
border.inline_end;
let computed_inline_size = match self.table_layout {
FixedLayout => {
let fixed_cells_inline_size = self.col_inline_sizes
.iter()
.fold(Au(0), |sum, inline_size| {
sum.add(inline_size)
});
let mut computed_inline_size = input.computed_inline_size.specified_or_zero();
// Compare border-edge inline-sizes. Because fixed_cells_inline_size indicates
// content-inline-size, padding and border values are added to
// fixed_cells_inline_size.
computed_inline_size = max(
fixed_cells_inline_size + padding_and_borders, computed_inline_size);
computed_inline_size
},
AutoLayout => {
// Automatic table layout is calculated according to CSS 2.1 § 17.5.2.2.
let mut cap_min = Au(0);
let mut cols_min = Au(0);
let mut cols_max = Au(0);
let mut col_min_inline_sizes = &vec!();
let mut col_pref_inline_sizes = &vec!();
for kid in self.block_flow.base.child_iter() {
if kid.is_table_caption() {
cap_min = kid.as_block().base.intrinsic_inline_sizes.minimum_inline_size;
} else {
assert!(kid.is_table());
cols_min = kid.as_block().base.intrinsic_inline_sizes.minimum_inline_size;
cols_max = kid.as_block()
.base
.intrinsic_inline_sizes
.preferred_inline_size;
col_min_inline_sizes = kid.col_min_inline_sizes();
col_pref_inline_sizes = kid.col_pref_inline_sizes();
}
}
// 'extra_inline-size': difference between the calculated table inline-size and
// minimum inline-size required by all columns. It will be distributed over the
// columns.
let (inline_size, extra_inline_size) = match input.computed_inline_size {
Auto => {
if input.available_inline_size > max(cols_max, cap_min) {
if cols_max > cap_min {
self.col_inline_sizes = col_pref_inline_sizes.clone();
(cols_max, Au(0))
} else {
(cap_min, cap_min - cols_min)
}
} else {
let max = if cols_min >= input.available_inline_size &&
cols_min >= cap_min {
self.col_inline_sizes = col_min_inline_sizes.clone();
cols_min
} else {
max(input.available_inline_size, cap_min)
};
(max, max - cols_min)
}
},
Specified(inline_size) => {
let max = if cols_min >= inline_size && cols_min >= cap_min {
self.col_inline_sizes = col_min_inline_sizes.clone();
cols_min
} else {
max(inline_size, cap_min)
};
(max, max - cols_min)
}
};
// The extra inline-size is distributed over the columns
if extra_inline_size > Au(0) {
let cell_len = self.col_inline_sizes.len() as f64;
self.col_inline_sizes = col_min_inline_sizes.iter()
.map(|inline_size| {
inline_size + extra_inline_size.scale_by(1.0 / cell_len)
}).collect();
}
inline_size + padding_and_borders
/// Calculates table column sizes for automatic layout per INTRINSIC § 4.3.
fn calculate_table_column_sizes_for_automatic_layout(&mut self) {
// Find the padding and border of our first child, which is the table itself.
//
// This is a little weird because we're computing border/padding/margins for our child,
// when normally the child computes it itself. But it has to be this way because the
// padding will affect where we place the child. This is an odd artifact of the way that
// tables are separated into table flows and table wrapper flows.
let available_inline_size = self.block_flow.fragment.border_box.size.inline;
let mut table_border_padding = Au(0);
for kid in self.block_flow.base.child_iter() {
if kid.is_table() {
let kid_block = kid.as_block();
kid_block.fragment.compute_border_and_padding(available_inline_size);
kid_block.fragment.compute_block_direction_margins(available_inline_size);
kid_block.fragment.compute_inline_direction_margins(available_inline_size);
table_border_padding = kid_block.fragment.border_padding.inline_start_end();
break
}
};
input.computed_inline_size = Specified(computed_inline_size);
input
}
// FIXME(pcwalton, spec): INTRINSIC § 8 does not properly define how to compute this, but
// says "the basic idea is the same as the shrink-to-fit width that CSS2.1 defines". So we
// just use the shrink-to-fit inline size.
let available_inline_size =
self.block_flow.get_shrink_to_fit_inline_size(available_inline_size);
// Compute all the guesses for the column sizes, and sum them.
let mut total_guess = AutoLayoutCandidateGuess::new();
let guesses: Vec<AutoLayoutCandidateGuess> =
self.column_inline_sizes.iter().map(|column_inline_size| {
let guess = AutoLayoutCandidateGuess::from_column_inline_size(
column_inline_size,
available_inline_size);
total_guess = total_guess + guess;
guess
}).collect();
// Assign inline sizes.
let selection = SelectedAutoLayoutCandidateGuess::select(&total_guess,
available_inline_size);
let mut total_used_inline_size = Au(0);
for (column_inline_size, guess) in self.column_inline_sizes
.iter_mut()
.zip(guesses.iter()) {
column_inline_size.minimum_length = guess.calculate(selection);
column_inline_size.percentage = 0.0;
total_used_inline_size = total_used_inline_size + column_inline_size.minimum_length
}
// Distribute excess inline-size if necessary per INTRINSIC § 4.4.
//
// FIXME(pcwalton, spec): How do I deal with fractional excess?
let excess_inline_size = available_inline_size - total_used_inline_size;
if excess_inline_size > Au(0) &&
selection == UsePreferredGuessAndDistributeExcessInlineSize {
let mut info = ExcessInlineSizeDistributionInfo::new();
for column_inline_size in self.column_inline_sizes.iter() {
info.update(column_inline_size)
}
let mut total_distributed_excess_size = Au(0);
for column_inline_size in self.column_inline_sizes.iter_mut() {
info.distribute_excess_inline_size_to_column(column_inline_size,
excess_inline_size,
&mut total_distributed_excess_size)
}
total_used_inline_size = available_inline_size
}
self.block_flow.fragment.border_box.size.inline = total_used_inline_size +
table_border_padding;
self.block_flow.base.position.size.inline = total_used_inline_size +
table_border_padding + self.block_flow.fragment.margin.inline_start_end();
}
fn compute_used_inline_size(&mut self,
layout_context: &LayoutContext,
parent_flow_inline_size: Au) {
// Delegate to the appropriate inline size computer to find the constraint inputs.
let mut input = if self.is_float() {
let input = if self.is_float() {
FloatNonReplaced.compute_inline_size_constraint_inputs(&mut self.block_flow,
parent_flow_inline_size,
layout_context)
@@ -213,9 +195,6 @@ impl TableWrapperFlow {
layout_context)
};
// Compute the inline sizes of the columns.
input = self.calculate_table_column_sizes(input);
// Delegate to the appropriate inline size computer to write the constraint solutions in.
if self.is_float() {
let solution = FloatNonReplaced.solve_inline_size_constraints(&mut self.block_flow,
@@ -261,12 +240,11 @@ impl Flow for TableWrapperFlow {
}
fn bubble_inline_sizes(&mut self) {
// get column inline-sizes info from table flow
// Get the column inline-sizes info from the table flow.
for kid in self.block_flow.base.child_iter() {
assert!(kid.is_table_caption() || kid.is_table());
debug_assert!(kid.is_table_caption() || kid.is_table());
if kid.is_table() {
self.col_inline_sizes.push_all(kid.as_table().col_inline_sizes.as_slice());
self.column_inline_sizes = kid.column_inline_sizes().clone()
}
}
@@ -296,18 +274,25 @@ impl Flow for TableWrapperFlow {
self.compute_used_inline_size(layout_context, containing_block_inline_size);
match self.table_layout {
FixedLayout => {}
AutoLayout => {
self.calculate_table_column_sizes_for_automatic_layout()
}
}
let inline_start_content_edge = self.block_flow.fragment.border_box.start.i;
let content_inline_size = self.block_flow.fragment.border_box.size.inline;
// In case of fixed layout, column inline-sizes are calculated in table flow.
let assigned_col_inline_sizes = match self.table_layout {
let assigned_column_inline_sizes = match self.table_layout {
FixedLayout => None,
AutoLayout => Some(self.col_inline_sizes.clone())
AutoLayout => Some(self.column_inline_sizes.as_slice())
};
self.block_flow.propagate_assigned_inline_size_to_children(inline_start_content_edge,
content_inline_size,
assigned_col_inline_sizes);
assigned_column_inline_sizes);
}
fn assign_block_size<'a>(&mut self, ctx: &'a LayoutContext<'a>) {
@@ -353,3 +338,230 @@ impl fmt::Show for TableWrapperFlow {
}
}
/// The layout "guesses" defined in INTRINSIC § 4.3.
struct AutoLayoutCandidateGuess {
/// The column inline-size assignment where each column is assigned its intrinsic minimum
/// inline-size.
minimum_guess: Au,
/// The column inline-size assignment where:
/// * A column with an intrinsic percentage inline-size greater than 0% is assigned the
/// larger of:
/// - Its intrinsic percentage inline-size times the assignable inline-size;
/// - Its intrinsic minimum inline-size;
/// * Other columns receive their intrinsic minimum inline-size.
minimum_percentage_guess: Au,
/// The column inline-size assignment where:
/// * Each column with an intrinsic percentage inline-size greater than 0% is assigned the
/// larger of:
/// - Its intrinsic percentage inline-size times the assignable inline-size;
/// - Its intrinsic minimum inline-size;
/// * Any other column that is constrained is assigned its intrinsic preferred inline-size;
/// * Other columns are assigned their intrinsic minimum inline-size.
minimum_specified_guess: Au,
/// The column inline-size assignment where:
/// * Each column with an intrinsic percentage inline-size greater than 0% is assigned the
/// larger of:
/// - Its intrinsic percentage inline-size times the assignable inline-size;
/// - Its intrinsic minimum inline-size;
/// * Other columns are assigned their intrinsic preferred inline-size.
preferred_guess: Au,
}
impl AutoLayoutCandidateGuess {
/// Creates a guess with all elements initialized to zero.
fn new() -> AutoLayoutCandidateGuess {
AutoLayoutCandidateGuess {
minimum_guess: Au(0),
minimum_percentage_guess: Au(0),
minimum_specified_guess: Au(0),
preferred_guess: Au(0),
}
}
/// Fills in the inline-size guesses for this column per INTRINSIC § 4.3.
fn from_column_inline_size(column_inline_size: &ColumnInlineSize, assignable_inline_size: Au)
-> AutoLayoutCandidateGuess {
let minimum_percentage_guess =
max(assignable_inline_size.scale_by(column_inline_size.percentage),
column_inline_size.minimum_length);
AutoLayoutCandidateGuess {
minimum_guess: column_inline_size.minimum_length,
minimum_percentage_guess: minimum_percentage_guess,
// FIXME(pcwalton): We need the notion of *constrainedness* per INTRINSIC § 4 to
// implement this one correctly.
minimum_specified_guess: if column_inline_size.percentage > 0.0 {
minimum_percentage_guess
} else if column_inline_size.constrained {
column_inline_size.preferred
} else {
column_inline_size.minimum_length
},
preferred_guess: if column_inline_size.percentage > 0.0 {
minimum_percentage_guess
} else {
column_inline_size.preferred
},
}
}
/// Calculates the inline-size, interpolating appropriately based on the value of `selection`.
///
/// This does *not* distribute excess inline-size. That must be done later if necessary.
fn calculate(&self, selection: SelectedAutoLayoutCandidateGuess) -> Au {
match selection {
UseMinimumGuess => self.minimum_guess,
InterpolateBetweenMinimumGuessAndMinimumPercentageGuess(weight) => {
interp(self.minimum_guess, self.minimum_percentage_guess, weight)
}
InterpolateBetweenMinimumPercentageGuessAndMinimumSpecifiedGuess(weight) => {
interp(self.minimum_percentage_guess, self.minimum_specified_guess, weight)
}
InterpolateBetweenMinimumSpecifiedGuessAndPreferredGuess(weight) => {
interp(self.minimum_specified_guess, self.preferred_guess, weight)
}
UsePreferredGuessAndDistributeExcessInlineSize => {
self.preferred_guess
}
}
}
}
impl Add<AutoLayoutCandidateGuess,AutoLayoutCandidateGuess> for AutoLayoutCandidateGuess {
#[inline]
fn add(&self, other: &AutoLayoutCandidateGuess) -> AutoLayoutCandidateGuess {
AutoLayoutCandidateGuess {
minimum_guess: self.minimum_guess + other.minimum_guess,
minimum_percentage_guess:
self.minimum_percentage_guess + other.minimum_percentage_guess,
minimum_specified_guess: self.minimum_specified_guess + other.minimum_specified_guess,
preferred_guess: self.preferred_guess + other.preferred_guess,
}
}
}
/// The `CSSFloat` member specifies the weight of the smaller of the two guesses, on a scale from
/// 0.0 to 1.0.
#[deriving(PartialEq, Show)]
enum SelectedAutoLayoutCandidateGuess {
UseMinimumGuess,
InterpolateBetweenMinimumGuessAndMinimumPercentageGuess(CSSFloat),
InterpolateBetweenMinimumPercentageGuessAndMinimumSpecifiedGuess(CSSFloat),
InterpolateBetweenMinimumSpecifiedGuessAndPreferredGuess(CSSFloat),
UsePreferredGuessAndDistributeExcessInlineSize,
}
impl SelectedAutoLayoutCandidateGuess {
/// See INTRINSIC § 4.3.
///
/// FIXME(pcwalton, INTRINSIC spec): INTRINSIC doesn't specify whether these are exclusive or
/// inclusive ranges.
fn select(guess: &AutoLayoutCandidateGuess, assignable_inline_size: Au)
-> SelectedAutoLayoutCandidateGuess {
if assignable_inline_size < guess.minimum_guess {
UseMinimumGuess
} else if assignable_inline_size < guess.minimum_percentage_guess {
let weight = weight(guess.minimum_guess,
assignable_inline_size,
guess.minimum_percentage_guess);
InterpolateBetweenMinimumGuessAndMinimumPercentageGuess(weight)
} else if assignable_inline_size < guess.minimum_specified_guess {
let weight = weight(guess.minimum_percentage_guess,
assignable_inline_size,
guess.minimum_specified_guess);
InterpolateBetweenMinimumPercentageGuessAndMinimumSpecifiedGuess(weight)
} else if assignable_inline_size < guess.preferred_guess {
let weight = weight(guess.minimum_specified_guess,
assignable_inline_size,
guess.preferred_guess);
InterpolateBetweenMinimumSpecifiedGuessAndPreferredGuess(weight)
} else {
UsePreferredGuessAndDistributeExcessInlineSize
}
}
}
/// Computes the weight needed to linearly interpolate `middle` between two guesses `low` and
/// `high` as specified by INTRINSIC § 4.3.
fn weight(low: Au, middle: Au, high: Au) -> CSSFloat {
(middle - low).to_subpx() / (high - low).to_subpx()
}
/// Linearly interpolates between two guesses, as specified by INTRINSIC § 4.3.
fn interp(low: Au, high: Au, weight: CSSFloat) -> Au {
low + (high - low).scale_by(weight)
}
struct ExcessInlineSizeDistributionInfo {
preferred_inline_size_of_nonconstrained_columns_with_no_percentage: Au,
count_of_nonconstrained_columns_with_no_percentage: u32,
preferred_inline_size_of_constrained_columns_with_no_percentage: Au,
total_percentage: CSSFloat,
column_count: u32,
}
impl ExcessInlineSizeDistributionInfo {
fn new() -> ExcessInlineSizeDistributionInfo {
ExcessInlineSizeDistributionInfo {
preferred_inline_size_of_nonconstrained_columns_with_no_percentage: Au(0),
count_of_nonconstrained_columns_with_no_percentage: 0,
preferred_inline_size_of_constrained_columns_with_no_percentage: Au(0),
total_percentage: 0.0,
column_count: 0,
}
}
fn update(&mut self, column_inline_size: &ColumnInlineSize) {
if !column_inline_size.constrained && column_inline_size.percentage == 0.0 {
self.preferred_inline_size_of_nonconstrained_columns_with_no_percentage =
self.preferred_inline_size_of_nonconstrained_columns_with_no_percentage +
column_inline_size.preferred;
self.count_of_nonconstrained_columns_with_no_percentage += 1
}
if column_inline_size.constrained && column_inline_size.percentage == 0.0 {
self.preferred_inline_size_of_constrained_columns_with_no_percentage =
self.preferred_inline_size_of_constrained_columns_with_no_percentage +
column_inline_size.preferred
}
self.total_percentage += column_inline_size.percentage;
self.column_count += 1
}
/// Based on the information here, distributes excess inline-size to the given column per
/// INTRINSIC § 4.4.
///
/// `#[inline]` so the compiler will hoist out the branch, which is loop-invariant.
#[inline]
fn distribute_excess_inline_size_to_column(&self,
column_inline_size: &mut ColumnInlineSize,
excess_inline_size: Au,
total_distributed_excess_size: &mut Au) {
let proportion =
if self.preferred_inline_size_of_nonconstrained_columns_with_no_percentage > Au(0) {
column_inline_size.preferred.to_subpx() /
self.preferred_inline_size_of_nonconstrained_columns_with_no_percentage
.to_subpx()
} else if self.count_of_nonconstrained_columns_with_no_percentage > 0 {
1.0 / (self.count_of_nonconstrained_columns_with_no_percentage as CSSFloat)
} else if self.preferred_inline_size_of_constrained_columns_with_no_percentage >
Au(0) {
column_inline_size.preferred.to_subpx() /
self.preferred_inline_size_of_constrained_columns_with_no_percentage.to_subpx()
} else if self.total_percentage > 0.0 {
column_inline_size.percentage / self.total_percentage
} else {
1.0 / (self.column_count as CSSFloat)
};
// The `min` here has the effect of throwing away fractional excess at the end of the
// table.
let amount_to_distribute = min(excess_inline_size.scale_by(proportion),
excess_inline_size - *total_distributed_excess_size);
*total_distributed_excess_size = *total_distributed_excess_size + amount_to_distribute;
column_inline_size.minimum_length = column_inline_size.minimum_length +
amount_to_distribute
}
}