mirror of
https://github.com/servo/servo.git
synced 2025-08-03 20:50:07 +01:00
Add an implementation of the core float and clear placement logic in layout
2020, not yet wired to the rest of layout. This commit implements an object that handles the 10 rules in CSS 2.1: https://www.w3.org/TR/CSS2/visuren.html#float-position The implementation strategy is that of a persistent balanced binary search tree of float bands. Binary search trees are commonly used for implementing float positioning; e.g. by WebKit. Persistence enables each object that interacts with floats to efficiently contain a snapshot of the float list at the time that object was laid out. That way, incremental layout can invalidate and start reflow at any point in a containing block. This commit features extensive use of [QuickCheck](https://github.com/BurntSushi/quickcheck) to ensure that the rules of the CSS specification are followed. Because this is not yet connected to layout, floats will not actually be laid out in Web pages yet. Note that unit tests as set up in Servo currently require types that they access to be public. Therefore, some internal layout 2020 types that were previously private have been made public. This is somewhat unfortunate. Part of #25167.
This commit is contained in:
parent
48bf169101
commit
5b36d211b4
10 changed files with 1354 additions and 20 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -2912,12 +2912,14 @@ dependencies = [
|
|||
"gfx_traits",
|
||||
"html5ever",
|
||||
"ipc-channel",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"mitochondria",
|
||||
"msg",
|
||||
"net_traits",
|
||||
"parking_lot 0.10.2",
|
||||
"quickcheck",
|
||||
"range",
|
||||
"rayon",
|
||||
"rayon_croissant",
|
||||
|
@ -4365,6 +4367,18 @@ version = "1.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eda5fe9b71976e62bc81b781206aaa076401769b2143379d3eb2118388babac4"
|
||||
|
||||
[[package]]
|
||||
name = "quickcheck"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"log",
|
||||
"rand",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "0.6.13"
|
||||
|
|
|
@ -44,3 +44,7 @@ style = { path = "../style", features = ["servo", "servo-layout-2020"] }
|
|||
style_traits = { path = "../style_traits" }
|
||||
unicode-script = { version = "0.3", features = ["harfbuzz"] }
|
||||
webrender_api = { git = "https://github.com/servo/webrender" }
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "1"
|
||||
quickcheck = "0.9"
|
||||
|
|
|
@ -2,29 +2,494 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
//! Float layout.
|
||||
//!
|
||||
//! See CSS 2.1 § 9.5.1: https://www.w3.org/TR/CSS2/visuren.html#float-position
|
||||
|
||||
use crate::context::LayoutContext;
|
||||
use crate::dom_traversal::{Contents, NodeAndStyleInfo, NodeExt};
|
||||
use crate::formatting_contexts::IndependentFormattingContext;
|
||||
use crate::geom::flow_relative::{Rect, Vec2};
|
||||
use crate::style_ext::DisplayInside;
|
||||
use euclid::num::Zero;
|
||||
use servo_arc::Arc;
|
||||
use std::f32;
|
||||
use std::ops::Range;
|
||||
use style::values::computed::Length;
|
||||
use style::values::specified::text::TextDecorationLine;
|
||||
|
||||
/// A floating box.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct FloatBox {
|
||||
/// The formatting context that makes up the content of this box.
|
||||
pub contents: IndependentFormattingContext,
|
||||
}
|
||||
|
||||
/// Data kept during layout about the floats in a given block formatting context.
|
||||
pub(crate) struct FloatContext {
|
||||
// TODO
|
||||
///
|
||||
/// This is a persistent data structure. Each float has its own private copy of the float context,
|
||||
/// although such copies may share portions of the `bands` tree.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FloatContext {
|
||||
/// A persistent AA tree of float bands.
|
||||
///
|
||||
/// This tree is immutable; modification operations return the new tree, which may share nodes
|
||||
/// with previous versions of the tree.
|
||||
pub bands: FloatBandTree,
|
||||
/// The logical width of this context. No floats may extend outside the width of this context
|
||||
/// unless they are as far (logically) left or right as possible.
|
||||
pub inline_size: Length,
|
||||
/// The current (logically) vertical position. No new floats may be placed (logically) above
|
||||
/// this line.
|
||||
pub ceiling: Length,
|
||||
}
|
||||
|
||||
impl FloatContext {
|
||||
pub fn new() -> Self {
|
||||
FloatContext {}
|
||||
/// Returns a new float context representing a containing block with the given content
|
||||
/// inline-size.
|
||||
pub fn new(inline_size: Length) -> Self {
|
||||
let mut bands = FloatBandTree::new();
|
||||
bands = bands.insert(FloatBand {
|
||||
top: Length::zero(),
|
||||
left: None,
|
||||
right: None,
|
||||
});
|
||||
bands = bands.insert(FloatBand {
|
||||
top: Length::new(f32::INFINITY),
|
||||
left: None,
|
||||
right: None,
|
||||
});
|
||||
FloatContext {
|
||||
bands,
|
||||
inline_size,
|
||||
ceiling: Length::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current ceiling value. No new floats may be placed (logically) above this line.
|
||||
pub fn ceiling(&self) -> Length {
|
||||
self.ceiling
|
||||
}
|
||||
|
||||
/// (Logically) lowers the ceiling to at least `new_ceiling` units.
|
||||
///
|
||||
/// If the ceiling is already logically lower (i.e. larger) than this, does nothing.
|
||||
pub fn lower_ceiling(&mut self, new_ceiling: Length) {
|
||||
self.ceiling = self.ceiling.max(new_ceiling);
|
||||
}
|
||||
|
||||
/// Returns the highest block position that is both logically below the current ceiling and
|
||||
/// clear of floats on the given side or sides.
|
||||
pub fn clearance(&self, side: ClearSide) -> Length {
|
||||
let mut band = self.bands.find(self.ceiling).unwrap();
|
||||
while !band.is_clear(side) {
|
||||
let next_band = self.bands.find_next(band.top).unwrap();
|
||||
if next_band.top.px().is_infinite() {
|
||||
break;
|
||||
}
|
||||
band = next_band;
|
||||
}
|
||||
band.top.max(self.ceiling)
|
||||
}
|
||||
|
||||
/// Places a new float and adds it to the list. Returns the start corner of its margin box.
|
||||
pub fn add_float(&mut self, new_float: FloatInfo) -> Vec2<Length> {
|
||||
// Find the first band this float fits in.
|
||||
let mut first_band = self.bands.find(self.ceiling).unwrap();
|
||||
while !first_band.float_fits(&new_float, self.inline_size) {
|
||||
let next_band = self.bands.find_next(first_band.top).unwrap();
|
||||
if next_band.top.px().is_infinite() {
|
||||
break;
|
||||
}
|
||||
first_band = next_band;
|
||||
}
|
||||
|
||||
// The float fits perfectly here. Place it.
|
||||
let (new_float_origin, new_float_extent);
|
||||
match new_float.side {
|
||||
FloatSide::Left => {
|
||||
new_float_origin = Vec2 {
|
||||
inline: first_band.left.unwrap_or(Length::zero()),
|
||||
block: first_band.top.max(self.ceiling),
|
||||
};
|
||||
new_float_extent = new_float_origin.inline + new_float.size.inline;
|
||||
},
|
||||
FloatSide::Right => {
|
||||
new_float_origin = Vec2 {
|
||||
inline: first_band.right.unwrap_or(self.inline_size) - new_float.size.inline,
|
||||
block: first_band.top.max(self.ceiling),
|
||||
};
|
||||
new_float_extent = new_float_origin.inline;
|
||||
},
|
||||
};
|
||||
let new_float_rect = Rect {
|
||||
start_corner: new_float_origin,
|
||||
size: new_float.size,
|
||||
};
|
||||
|
||||
// Split the first band if necessary.
|
||||
first_band.top = new_float_rect.start_corner.block;
|
||||
self.bands = self.bands.insert(first_band);
|
||||
|
||||
// Split the last band if necessary.
|
||||
let mut last_band = self
|
||||
.bands
|
||||
.find(new_float_rect.max_block_position())
|
||||
.unwrap();
|
||||
last_band.top = new_float_rect.max_block_position();
|
||||
self.bands = self.bands.insert(last_band);
|
||||
|
||||
// Update all bands that contain this float to reflect the new available size.
|
||||
let block_range = new_float_rect.start_corner.block..new_float_rect.max_block_position();
|
||||
self.bands = self
|
||||
.bands
|
||||
.set_range(&block_range, new_float.side, new_float_extent);
|
||||
|
||||
// CSS 2.1 § 9.5.1 rule 6: The outer top of a floating box may not be higher than the outer
|
||||
// top of any block or floated box generated by an element earlier in the source document.
|
||||
self.ceiling = self.ceiling.max(new_float_rect.start_corner.block);
|
||||
new_float_rect.start_corner
|
||||
}
|
||||
}
|
||||
|
||||
/// Information needed to place a float.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FloatInfo {
|
||||
/// The *margin* box size of the float.
|
||||
pub size: Vec2<Length>,
|
||||
/// Whether the float is left or right.
|
||||
pub side: FloatSide,
|
||||
/// Which side or sides to clear floats on.
|
||||
pub clear: ClearSide,
|
||||
}
|
||||
|
||||
/// Whether the float is left or right.
|
||||
///
|
||||
/// See CSS 2.1 § 9.5.1: https://www.w3.org/TR/CSS2/visuren.html#float-position
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum FloatSide {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Which side or sides to clear floats on.
|
||||
///
|
||||
/// See CSS 2.1 § 9.5.2: https://www.w3.org/TR/CSS2/visuren.html#flow-control
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ClearSide {
|
||||
None = 0,
|
||||
Left = 1,
|
||||
Right = 2,
|
||||
Both = 3,
|
||||
}
|
||||
|
||||
/// Internal data structure that describes a nonoverlapping vertical region in which floats may be
|
||||
/// placed. Floats must go between "left edge + `left`" and "right edge - `right`".
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct FloatBand {
|
||||
/// The logical vertical position of the top of this band.
|
||||
pub top: Length,
|
||||
/// The distance from the left edge to the first legal (logically) horizontal position where
|
||||
/// floats may be placed. If `None`, there are no floats to the left; distinguishing between
|
||||
/// the cases of "a zero-width float is present" and "no floats at all are present" is
|
||||
/// necessary to, for example, clear past zero-width floats.
|
||||
pub left: Option<Length>,
|
||||
/// The distance from the right edge to the first legal (logically) horizontal position where
|
||||
/// floats may be placed. If `None`, there are no floats to the right; distinguishing between
|
||||
/// the cases of "a zero-width float is present" and "no floats at all are present" is
|
||||
/// necessary to, for example, clear past zero-width floats.
|
||||
pub right: Option<Length>,
|
||||
}
|
||||
|
||||
impl FloatBand {
|
||||
fn is_clear(&self, side: ClearSide) -> bool {
|
||||
match (side, self.left, self.right) {
|
||||
(ClearSide::Left, Some(_), _) |
|
||||
(ClearSide::Right, _, Some(_)) |
|
||||
(ClearSide::Both, Some(_), _) |
|
||||
(ClearSide::Both, _, Some(_)) => false,
|
||||
(ClearSide::None, _, _) |
|
||||
(ClearSide::Left, None, _) |
|
||||
(ClearSide::Right, _, None) |
|
||||
(ClearSide::Both, None, None) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn float_fits(&self, new_float: &FloatInfo, container_inline_size: Length) -> bool {
|
||||
let available_space =
|
||||
self.right.unwrap_or(container_inline_size) - self.left.unwrap_or(Length::zero());
|
||||
self.is_clear(new_float.clear) &&
|
||||
(new_float.size.inline <= available_space ||
|
||||
(self.left.is_none() && self.right.is_none()))
|
||||
}
|
||||
}
|
||||
|
||||
// Float band storage
|
||||
|
||||
/// A persistent AA tree for float band storage.
|
||||
///
|
||||
/// Bands here are nonoverlapping, and there is guaranteed to be a band at block-position 0 and
|
||||
/// another band at block-position infinity.
|
||||
///
|
||||
/// AA trees were chosen for simplicity.
|
||||
///
|
||||
/// See: https://en.wikipedia.org/wiki/AA_tree
|
||||
/// https://arxiv.org/pdf/1412.4882.pdf
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FloatBandTree {
|
||||
pub root: FloatBandLink,
|
||||
}
|
||||
|
||||
/// A single edge (or lack thereof) in the float band tree.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FloatBandLink(pub Option<Arc<FloatBandNode>>);
|
||||
|
||||
/// A single node in the float band tree.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FloatBandNode {
|
||||
/// The actual band.
|
||||
pub band: FloatBand,
|
||||
/// The left child.
|
||||
pub left: FloatBandLink,
|
||||
/// The right child.
|
||||
pub right: FloatBandLink,
|
||||
/// The level, which increases as you go up the tree.
|
||||
///
|
||||
/// This value is needed for tree balancing.
|
||||
pub level: i32,
|
||||
}
|
||||
|
||||
impl FloatBandTree {
|
||||
/// Creates a new float band tree.
|
||||
pub fn new() -> FloatBandTree {
|
||||
FloatBandTree {
|
||||
root: FloatBandLink(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the first band whose top is less than or equal to the given `block_position`.
|
||||
pub fn find(&self, block_position: Length) -> Option<FloatBand> {
|
||||
self.root.find(block_position)
|
||||
}
|
||||
|
||||
/// Returns the first band whose top is strictly greater than to the given `block_position`.
|
||||
pub fn find_next(&self, block_position: Length) -> Option<FloatBand> {
|
||||
self.root.find_next(block_position)
|
||||
}
|
||||
|
||||
/// Sets the side values of all bands within the given half-open range to be at least
|
||||
/// `new_value`.
|
||||
#[must_use]
|
||||
pub fn set_range(
|
||||
&self,
|
||||
range: &Range<Length>,
|
||||
side: FloatSide,
|
||||
new_value: Length,
|
||||
) -> FloatBandTree {
|
||||
FloatBandTree {
|
||||
root: FloatBandLink(
|
||||
self.root
|
||||
.0
|
||||
.as_ref()
|
||||
.map(|root| root.set_range(range, side, new_value)),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a new band into the tree. If the band has the same level as a pre-existing one,
|
||||
/// replaces the existing band with the new one.
|
||||
#[must_use]
|
||||
pub fn insert(&self, band: FloatBand) -> FloatBandTree {
|
||||
FloatBandTree {
|
||||
root: self.root.insert(band),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FloatBandNode {
|
||||
fn new(band: FloatBand) -> FloatBandNode {
|
||||
FloatBandNode {
|
||||
band,
|
||||
left: FloatBandLink(None),
|
||||
right: FloatBandLink(None),
|
||||
level: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the side values of all bands within the given half-open range to be at least
|
||||
/// `new_value`.
|
||||
fn set_range(
|
||||
&self,
|
||||
range: &Range<Length>,
|
||||
side: FloatSide,
|
||||
new_value: Length,
|
||||
) -> Arc<FloatBandNode> {
|
||||
let mut new_band = self.band.clone();
|
||||
if self.band.top >= range.start && self.band.top < range.end {
|
||||
match side {
|
||||
FloatSide::Left => match new_band.left {
|
||||
None => new_band.left = Some(new_value),
|
||||
Some(ref mut old_value) => *old_value = old_value.max(new_value),
|
||||
},
|
||||
FloatSide::Right => match new_band.right {
|
||||
None => new_band.right = Some(new_value),
|
||||
Some(ref mut old_value) => *old_value = old_value.min(new_value),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let new_left = match self.left.0 {
|
||||
None => FloatBandLink(None),
|
||||
Some(ref old_left) if range.start < new_band.top => {
|
||||
FloatBandLink(Some(old_left.set_range(range, side, new_value)))
|
||||
},
|
||||
Some(ref old_left) => FloatBandLink(Some((*old_left).clone())),
|
||||
};
|
||||
|
||||
let new_right = match self.right.0 {
|
||||
None => FloatBandLink(None),
|
||||
Some(ref old_right) if range.end > new_band.top => {
|
||||
FloatBandLink(Some(old_right.set_range(range, side, new_value)))
|
||||
},
|
||||
Some(ref old_right) => FloatBandLink(Some((*old_right).clone())),
|
||||
};
|
||||
|
||||
Arc::new(FloatBandNode {
|
||||
band: new_band,
|
||||
left: new_left,
|
||||
right: new_right,
|
||||
level: self.level,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FloatBandLink {
|
||||
/// Returns the first band whose top is less than or equal to the given `block_position`.
|
||||
fn find(&self, block_position: Length) -> Option<FloatBand> {
|
||||
let this = match self.0 {
|
||||
None => return None,
|
||||
Some(ref node) => node,
|
||||
};
|
||||
|
||||
if block_position < this.band.top {
|
||||
return this.left.find(block_position);
|
||||
}
|
||||
|
||||
// It's somewhere in this subtree, but we aren't sure whether it's here or in the right
|
||||
// subtree.
|
||||
if let Some(band) = this.right.find(block_position) {
|
||||
return Some(band);
|
||||
}
|
||||
|
||||
Some(this.band.clone())
|
||||
}
|
||||
|
||||
/// Returns the first band whose top is strictly greater than the given `block_position`.
|
||||
fn find_next(&self, block_position: Length) -> Option<FloatBand> {
|
||||
let this = match self.0 {
|
||||
None => return None,
|
||||
Some(ref node) => node,
|
||||
};
|
||||
|
||||
if block_position >= this.band.top {
|
||||
return this.right.find_next(block_position);
|
||||
}
|
||||
|
||||
// It's somewhere in this subtree, but we aren't sure whether it's here or in the left
|
||||
// subtree.
|
||||
if let Some(band) = this.left.find_next(block_position) {
|
||||
return Some(band);
|
||||
}
|
||||
|
||||
Some(this.band.clone())
|
||||
}
|
||||
|
||||
// Inserts a new band into the tree. If the band has the same level as a pre-existing one,
|
||||
// replaces the existing band with the new one.
|
||||
fn insert(&self, band: FloatBand) -> FloatBandLink {
|
||||
let mut this = match self.0 {
|
||||
None => return FloatBandLink(Some(Arc::new(FloatBandNode::new(band)))),
|
||||
Some(ref this) => (**this).clone(),
|
||||
};
|
||||
|
||||
if band.top < this.band.top {
|
||||
this.left = this.left.insert(band);
|
||||
return FloatBandLink(Some(Arc::new(this))).skew().split();
|
||||
}
|
||||
if band.top > this.band.top {
|
||||
this.right = this.right.insert(band);
|
||||
return FloatBandLink(Some(Arc::new(this))).skew().split();
|
||||
}
|
||||
|
||||
this.band = band;
|
||||
FloatBandLink(Some(Arc::new(this)))
|
||||
}
|
||||
|
||||
// Corrects tree balance:
|
||||
//
|
||||
// T L
|
||||
// / \ / \
|
||||
// L R → A T if level(T) = level(L)
|
||||
// / \ / \
|
||||
// A B B R
|
||||
fn skew(&self) -> FloatBandLink {
|
||||
if let Some(ref this) = self.0 {
|
||||
if let Some(ref left) = this.left.0 {
|
||||
if this.level == left.level {
|
||||
return FloatBandLink(Some(Arc::new(FloatBandNode {
|
||||
level: this.level,
|
||||
left: left.left.clone(),
|
||||
band: left.band.clone(),
|
||||
right: FloatBandLink(Some(Arc::new(FloatBandNode {
|
||||
level: this.level,
|
||||
left: left.right.clone(),
|
||||
band: this.band.clone(),
|
||||
right: this.right.clone(),
|
||||
}))),
|
||||
})));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(*self).clone()
|
||||
}
|
||||
|
||||
// Corrects tree balance:
|
||||
//
|
||||
// T R
|
||||
// / \ / \
|
||||
// A R → T X if level(T) = level(X)
|
||||
// / \ / \
|
||||
// B X A B
|
||||
fn split(&self) -> FloatBandLink {
|
||||
if let Some(ref this) = self.0 {
|
||||
if let Some(ref right) = this.right.0 {
|
||||
if let Some(ref right_right) = right.right.0 {
|
||||
if this.level == right_right.level {
|
||||
return FloatBandLink(Some(Arc::new(FloatBandNode {
|
||||
level: this.level + 1,
|
||||
left: FloatBandLink(Some(Arc::new(FloatBandNode {
|
||||
level: this.level,
|
||||
left: this.left.clone(),
|
||||
band: this.band.clone(),
|
||||
right: right.left.clone(),
|
||||
}))),
|
||||
band: right.band.clone(),
|
||||
right: right.right.clone(),
|
||||
})));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(*self).clone()
|
||||
}
|
||||
}
|
||||
|
||||
// Float boxes
|
||||
|
||||
impl FloatBox {
|
||||
/// Creates a new float box.
|
||||
pub fn construct<'dom>(
|
||||
context: &LayoutContext,
|
||||
info: &NodeAndStyleInfo<impl NodeExt<'dom>>,
|
||||
|
|
|
@ -30,7 +30,7 @@ use style::values::computed::{Length, LengthOrAuto};
|
|||
use style::Zero;
|
||||
|
||||
mod construct;
|
||||
mod float;
|
||||
pub mod float;
|
||||
pub mod inline;
|
||||
mod root;
|
||||
|
||||
|
@ -80,7 +80,7 @@ impl BlockFormattingContext {
|
|||
) -> IndependentLayout {
|
||||
let mut float_context;
|
||||
let float_context = if self.contains_floats {
|
||||
float_context = FloatContext::new();
|
||||
float_context = FloatContext::new(containing_block.inline_size);
|
||||
Some(&mut float_context)
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -19,21 +19,21 @@ pub type PhysicalSides<U> = euclid::SideOffsets2D<U, CSSPixel>;
|
|||
pub type LengthOrAuto = AutoOr<Length>;
|
||||
pub type LengthPercentageOrAuto<'a> = AutoOr<&'a LengthPercentage>;
|
||||
|
||||
pub(crate) mod flow_relative {
|
||||
pub mod flow_relative {
|
||||
#[derive(Clone, Serialize)]
|
||||
pub(crate) struct Vec2<T> {
|
||||
pub struct Vec2<T> {
|
||||
pub inline: T,
|
||||
pub block: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub(crate) struct Rect<T> {
|
||||
pub struct Rect<T> {
|
||||
pub start_corner: Vec2<T>,
|
||||
pub size: Vec2<T>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub(crate) struct Sides<T> {
|
||||
pub struct Sides<T> {
|
||||
pub inline_start: T,
|
||||
pub inline_end: T,
|
||||
pub block_start: T,
|
||||
|
@ -325,6 +325,20 @@ where
|
|||
}
|
||||
|
||||
impl<T> flow_relative::Rect<T> {
|
||||
pub fn max_inline_position(&self) -> T
|
||||
where
|
||||
T: Add<Output = T> + Copy,
|
||||
{
|
||||
self.start_corner.inline + self.size.inline
|
||||
}
|
||||
|
||||
pub fn max_block_position(&self) -> T
|
||||
where
|
||||
T: Add<Output = T> + Copy,
|
||||
{
|
||||
self.start_corner.block + self.size.block
|
||||
}
|
||||
|
||||
pub fn inflate(&self, sides: &flow_relative::Sides<T>) -> Self
|
||||
where
|
||||
T: Add<Output = T> + Copy,
|
||||
|
|
|
@ -16,10 +16,10 @@ pub mod display_list;
|
|||
mod dom_traversal;
|
||||
pub mod element_data;
|
||||
mod flexbox;
|
||||
mod flow;
|
||||
pub mod flow;
|
||||
mod formatting_contexts;
|
||||
mod fragments;
|
||||
mod geom;
|
||||
pub mod geom;
|
||||
#[macro_use]
|
||||
pub mod layout_debug;
|
||||
mod opaque_node;
|
||||
|
@ -37,7 +37,7 @@ use crate::geom::flow_relative::Vec2;
|
|||
use style::properties::ComputedValues;
|
||||
use style::values::computed::{Length, LengthOrAuto};
|
||||
|
||||
struct ContainingBlock<'a> {
|
||||
pub struct ContainingBlock<'a> {
|
||||
inline_size: Length,
|
||||
block_size: LengthOrAuto,
|
||||
style: &'a ComputedValues,
|
||||
|
|
823
components/layout_2020/tests/floats.rs
Normal file
823
components/layout_2020/tests/floats.rs
Normal file
|
@ -0,0 +1,823 @@
|
|||
/* 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 https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
//! Property-based randomized testing for the core float layout algorithm.
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
use euclid::num::Zero;
|
||||
use layout::flow::float::{ClearSide, FloatBand, FloatBandNode, FloatBandTree, FloatContext};
|
||||
use layout::flow::float::{FloatInfo, FloatSide};
|
||||
use layout::geom::flow_relative::{Rect, Vec2};
|
||||
use quickcheck::{Arbitrary, Gen};
|
||||
use std::f32;
|
||||
use std::ops::Range;
|
||||
use std::panic::{self, PanicInfo};
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use std::thread;
|
||||
use std::u32;
|
||||
use style::values::computed::Length;
|
||||
|
||||
lazy_static! {
|
||||
static ref PANIC_HOOK_MUTEX: Mutex<()> = Mutex::new(());
|
||||
}
|
||||
|
||||
// Suppresses panic messages. Some tests need to fail and we don't want them to spam the console.
|
||||
// Note that, because the panic hook is process-wide, tests that are expected to fail might
|
||||
// suppress panic messages from other failing tests. To work around this, run failing tests one at
|
||||
// a time or use only a single test thread.
|
||||
struct PanicMsgSuppressor<'a> {
|
||||
#[allow(dead_code)]
|
||||
mutex_guard: MutexGuard<'a, ()>,
|
||||
prev_hook: Option<Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send>>,
|
||||
}
|
||||
|
||||
impl<'a> PanicMsgSuppressor<'a> {
|
||||
fn new(mutex_guard: MutexGuard<'a, ()>) -> PanicMsgSuppressor<'a> {
|
||||
let prev_hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(|_| ()));
|
||||
PanicMsgSuppressor {
|
||||
mutex_guard,
|
||||
prev_hook: Some(prev_hook),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for PanicMsgSuppressor<'a> {
|
||||
fn drop(&mut self) {
|
||||
panic::set_hook(self.prev_hook.take().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
// AA tree helpers
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FloatBandWrapper(FloatBand);
|
||||
|
||||
impl Arbitrary for FloatBandWrapper {
|
||||
fn arbitrary<G>(generator: &mut G) -> FloatBandWrapper
|
||||
where
|
||||
G: Gen,
|
||||
{
|
||||
let top: u32 = Arbitrary::arbitrary(generator);
|
||||
let left: Option<u32> = Arbitrary::arbitrary(generator);
|
||||
let right: Option<u32> = Arbitrary::arbitrary(generator);
|
||||
FloatBandWrapper(FloatBand {
|
||||
top: Length::new(top as f32),
|
||||
left: left.map(|value| Length::new(value as f32)),
|
||||
right: right.map(|value| Length::new(value as f32)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FloatRangeInput {
|
||||
start_index: u32,
|
||||
band_count: u32,
|
||||
side: FloatSide,
|
||||
length: u32,
|
||||
}
|
||||
|
||||
impl Arbitrary for FloatRangeInput {
|
||||
fn arbitrary<G>(generator: &mut G) -> FloatRangeInput
|
||||
where
|
||||
G: Gen,
|
||||
{
|
||||
let start_index: u32 = Arbitrary::arbitrary(generator);
|
||||
let band_count: u32 = Arbitrary::arbitrary(generator);
|
||||
let is_left: bool = Arbitrary::arbitrary(generator);
|
||||
let length: u32 = Arbitrary::arbitrary(generator);
|
||||
FloatRangeInput {
|
||||
start_index,
|
||||
band_count,
|
||||
side: if is_left {
|
||||
FloatSide::Left
|
||||
} else {
|
||||
FloatSide::Right
|
||||
},
|
||||
length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AA tree predicates
|
||||
|
||||
fn check_node_ordering(node: &FloatBandNode) {
|
||||
let mid = node.band.top;
|
||||
if let Some(ref left) = node.left.0 {
|
||||
assert!(left.band.top < mid);
|
||||
}
|
||||
if let Some(ref right) = node.right.0 {
|
||||
assert!(right.band.top > mid);
|
||||
}
|
||||
if let Some(ref left) = node.left.0 {
|
||||
check_node_ordering(left);
|
||||
}
|
||||
if let Some(ref right) = node.right.0 {
|
||||
check_node_ordering(right);
|
||||
}
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/AA_tree#Balancing_rotations
|
||||
fn check_node_balance(node: &FloatBandNode) {
|
||||
// 1. The level of every leaf node is one.
|
||||
if node.left.0.is_none() && node.right.0.is_none() {
|
||||
assert_eq!(node.level, 1);
|
||||
}
|
||||
// 2. The level of every left child is exactly one less than that of its parent.
|
||||
if let Some(ref left) = node.left.0 {
|
||||
assert_eq!(left.level, node.level - 1);
|
||||
}
|
||||
// 3. The level of every right child is equal to or one less than that of its parent.
|
||||
if let Some(ref right) = node.right.0 {
|
||||
assert!(right.level == node.level || right.level == node.level - 1);
|
||||
}
|
||||
// 4. The level of every right grandchild is strictly less than that of its grandparent.
|
||||
if let Some(ref right) = node.right.0 {
|
||||
if let Some(ref right_right) = right.right.0 {
|
||||
assert!(right_right.level < node.level);
|
||||
}
|
||||
}
|
||||
// 5. Every node of level greater than one has two children.
|
||||
if node.level > 1 {
|
||||
assert!(node.left.0.is_some() && node.right.0.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
fn check_tree_ordering(tree: FloatBandTree) {
|
||||
if let Some(ref root) = tree.root.0 {
|
||||
check_node_ordering(root);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_tree_balance(tree: FloatBandTree) {
|
||||
if let Some(ref root) = tree.root.0 {
|
||||
check_node_balance(root);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_tree_find(tree: &FloatBandTree, block_position: Length, sorted_bands: &[FloatBand]) {
|
||||
let found_band = tree
|
||||
.find(block_position)
|
||||
.expect("Couldn't find the band in the tree!");
|
||||
let reference_band_index = sorted_bands
|
||||
.iter()
|
||||
.position(|band| band.top > block_position)
|
||||
.expect("Couldn't find the reference band!") -
|
||||
1;
|
||||
let reference_band = &sorted_bands[reference_band_index];
|
||||
assert_eq!(found_band.top, reference_band.top);
|
||||
assert_eq!(found_band.left, reference_band.left);
|
||||
assert_eq!(found_band.right, reference_band.right);
|
||||
}
|
||||
|
||||
fn check_tree_find_next(tree: &FloatBandTree, block_position: Length, sorted_bands: &[FloatBand]) {
|
||||
let found_band = tree
|
||||
.find_next(block_position)
|
||||
.expect("Couldn't find the band in the tree!");
|
||||
let reference_band_index = sorted_bands
|
||||
.iter()
|
||||
.position(|band| band.top > block_position)
|
||||
.expect("Couldn't find the reference band!");
|
||||
let reference_band = &sorted_bands[reference_band_index];
|
||||
assert_eq!(found_band.top, reference_band.top);
|
||||
assert_eq!(found_band.left, reference_band.left);
|
||||
assert_eq!(found_band.right, reference_band.right);
|
||||
}
|
||||
|
||||
fn check_node_range_setting(
|
||||
node: &FloatBandNode,
|
||||
block_range: &Range<Length>,
|
||||
side: FloatSide,
|
||||
value: Length,
|
||||
) {
|
||||
if node.band.top >= block_range.start && node.band.top < block_range.end {
|
||||
match side {
|
||||
FloatSide::Left => assert!(node.band.left.unwrap() >= value),
|
||||
FloatSide::Right => assert!(node.band.right.unwrap() <= value),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref left) = node.left.0 {
|
||||
check_node_range_setting(left, block_range, side, value)
|
||||
}
|
||||
if let Some(ref right) = node.right.0 {
|
||||
check_node_range_setting(right, block_range, side, value)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_tree_range_setting(
|
||||
tree: &FloatBandTree,
|
||||
block_range: &Range<Length>,
|
||||
side: FloatSide,
|
||||
value: Length,
|
||||
) {
|
||||
if let Some(ref root) = tree.root.0 {
|
||||
check_node_range_setting(root, block_range, side, value)
|
||||
}
|
||||
}
|
||||
|
||||
// AA tree unit tests
|
||||
|
||||
// Tests that the tree is a properly-ordered binary tree.
|
||||
#[test]
|
||||
fn test_tree_ordering() {
|
||||
let f: fn(Vec<FloatBandWrapper>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(bands: Vec<FloatBandWrapper>) {
|
||||
let mut tree = FloatBandTree::new();
|
||||
for FloatBandWrapper(band) in bands {
|
||||
tree = tree.insert(band);
|
||||
}
|
||||
check_tree_ordering(tree);
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that the tree is balanced (i.e. AA tree invariants are maintained).
|
||||
#[test]
|
||||
fn test_tree_balance() {
|
||||
let f: fn(Vec<FloatBandWrapper>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(bands: Vec<FloatBandWrapper>) {
|
||||
let mut tree = FloatBandTree::new();
|
||||
for FloatBandWrapper(band) in bands {
|
||||
tree = tree.insert(band);
|
||||
}
|
||||
check_tree_balance(tree);
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that the `find()` method works.
|
||||
#[test]
|
||||
fn test_tree_find() {
|
||||
let f: fn(Vec<FloatBandWrapper>, Vec<u32>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(bands: Vec<FloatBandWrapper>, lookups: Vec<u32>) {
|
||||
let mut bands: Vec<FloatBand> = bands.into_iter().map(|band| band.0).collect();
|
||||
bands.push(FloatBand {
|
||||
top: Length::zero(),
|
||||
left: None,
|
||||
right: None,
|
||||
});
|
||||
bands.push(FloatBand {
|
||||
top: Length::new(f32::INFINITY),
|
||||
left: None,
|
||||
right: None,
|
||||
});
|
||||
let mut tree = FloatBandTree::new();
|
||||
for ref band in &bands {
|
||||
tree = tree.insert((*band).clone());
|
||||
}
|
||||
bands.sort_by(|a, b| a.top.partial_cmp(&b.top).unwrap());
|
||||
for lookup in lookups {
|
||||
check_tree_find(&tree, Length::new(lookup as f32), &bands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that the `find_next()` method works.
|
||||
#[test]
|
||||
fn test_tree_find_next() {
|
||||
let f: fn(Vec<FloatBandWrapper>, Vec<u32>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(bands: Vec<FloatBandWrapper>, lookups: Vec<u32>) {
|
||||
let mut bands: Vec<FloatBand> = bands.into_iter().map(|band| band.0).collect();
|
||||
bands.push(FloatBand {
|
||||
top: Length::zero(),
|
||||
left: None,
|
||||
right: None,
|
||||
});
|
||||
bands.push(FloatBand {
|
||||
top: Length::new(f32::INFINITY),
|
||||
left: None,
|
||||
right: None,
|
||||
});
|
||||
bands.sort_by(|a, b| a.top.partial_cmp(&b.top).unwrap());
|
||||
bands.dedup_by(|a, b| a.top == b.top);
|
||||
let mut tree = FloatBandTree::new();
|
||||
for ref band in &bands {
|
||||
tree = tree.insert((*band).clone());
|
||||
}
|
||||
for lookup in lookups {
|
||||
check_tree_find_next(&tree, Length::new(lookup as f32), &bands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that `set_range()` works.
|
||||
#[test]
|
||||
fn test_tree_range_setting() {
|
||||
let f: fn(Vec<FloatBandWrapper>, Vec<FloatRangeInput>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(bands: Vec<FloatBandWrapper>, ranges: Vec<FloatRangeInput>) {
|
||||
let mut tree = FloatBandTree::new();
|
||||
for FloatBandWrapper(ref band) in &bands {
|
||||
tree = tree.insert((*band).clone());
|
||||
}
|
||||
|
||||
let mut tops: Vec<Length> = bands.iter().map(|band| band.0.top).collect();
|
||||
tops.push(Length::new(f32::INFINITY));
|
||||
tops.sort_by(|a, b| a.px().partial_cmp(&b.px()).unwrap());
|
||||
|
||||
for range in ranges {
|
||||
let start = range.start_index.min(tops.len() as u32 - 1);
|
||||
let end = (range.start_index + range.length).min(tops.len() as u32 - 1);
|
||||
let block_range = tops[start as usize]..tops[end as usize];
|
||||
let length = Length::new(range.length as f32);
|
||||
let new_tree = tree.set_range(&block_range, range.side, length);
|
||||
check_tree_range_setting(&new_tree, &block_range, range.side, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Float predicates
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FloatInput {
|
||||
// Information needed to place the float.
|
||||
info: FloatInfo,
|
||||
// The float may be placed no higher than this line. This simulates the effect of line boxes
|
||||
// per CSS 2.1 § 9.5.1 rule 6.
|
||||
ceiling: u32,
|
||||
}
|
||||
|
||||
impl Arbitrary for FloatInput {
|
||||
fn arbitrary<G>(generator: &mut G) -> FloatInput
|
||||
where
|
||||
G: Gen,
|
||||
{
|
||||
let width: u32 = Arbitrary::arbitrary(generator);
|
||||
let height: u32 = Arbitrary::arbitrary(generator);
|
||||
let is_left: bool = Arbitrary::arbitrary(generator);
|
||||
let ceiling: u32 = Arbitrary::arbitrary(generator);
|
||||
let clear: u8 = Arbitrary::arbitrary(generator);
|
||||
FloatInput {
|
||||
info: FloatInfo {
|
||||
size: Vec2 {
|
||||
inline: Length::new(width as f32),
|
||||
block: Length::new(height as f32),
|
||||
},
|
||||
side: if is_left {
|
||||
FloatSide::Left
|
||||
} else {
|
||||
FloatSide::Right
|
||||
},
|
||||
clear: new_clear_side(clear),
|
||||
},
|
||||
ceiling,
|
||||
}
|
||||
}
|
||||
|
||||
fn shrink(&self) -> Box<dyn Iterator<Item = FloatInput>> {
|
||||
let mut this = (*self).clone();
|
||||
let mut shrunk = false;
|
||||
if let Some(inline_size) = self.info.size.inline.px().shrink().next() {
|
||||
this.info.size.inline = Length::new(inline_size);
|
||||
shrunk = true;
|
||||
}
|
||||
if let Some(block_size) = self.info.size.block.px().shrink().next() {
|
||||
this.info.size.block = Length::new(block_size);
|
||||
shrunk = true;
|
||||
}
|
||||
if let Some(clear_side) = (self.info.clear as u8).shrink().next() {
|
||||
this.info.clear = new_clear_side(clear_side);
|
||||
shrunk = true;
|
||||
}
|
||||
if let Some(ceiling) = self.ceiling.shrink().next() {
|
||||
this.ceiling = ceiling;
|
||||
shrunk = true;
|
||||
}
|
||||
if shrunk {
|
||||
quickcheck::single_shrinker(this)
|
||||
} else {
|
||||
quickcheck::empty_shrinker()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_clear_side(value: u8) -> ClearSide {
|
||||
match value & 3 {
|
||||
0 => ClearSide::None,
|
||||
1 => ClearSide::Left,
|
||||
2 => ClearSide::Right,
|
||||
_ => ClearSide::Both,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FloatPlacement {
|
||||
float_context: FloatContext,
|
||||
placed_floats: Vec<PlacedFloat>,
|
||||
}
|
||||
|
||||
// Information about the placement of a float.
|
||||
#[derive(Clone)]
|
||||
struct PlacedFloat {
|
||||
origin: Vec2<Length>,
|
||||
info: FloatInfo,
|
||||
ceiling: Length,
|
||||
}
|
||||
|
||||
impl Drop for FloatPlacement {
|
||||
fn drop(&mut self) {
|
||||
if !thread::panicking() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dump the float context for debugging.
|
||||
eprintln!(
|
||||
"Failing float placement (inline size: {:?}):",
|
||||
self.float_context.inline_size
|
||||
);
|
||||
for placed_float in &self.placed_floats {
|
||||
eprintln!(" * {:?} @ {:?}", placed_float.info, placed_float.origin);
|
||||
}
|
||||
eprintln!("Bands:\n{:?}\n", self.float_context.bands);
|
||||
}
|
||||
}
|
||||
|
||||
impl PlacedFloat {
|
||||
fn rect(&self) -> Rect<Length> {
|
||||
Rect {
|
||||
start_corner: self.origin.clone(),
|
||||
size: self.info.size.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FloatPlacement {
|
||||
fn place(inline_size: u32, floats: Vec<FloatInput>) -> FloatPlacement {
|
||||
let mut float_context = FloatContext::new(Length::new(inline_size as f32));
|
||||
let mut placed_floats = vec![];
|
||||
for float in floats {
|
||||
let ceiling = Length::new(float.ceiling as f32);
|
||||
float_context.lower_ceiling(ceiling);
|
||||
placed_floats.push(PlacedFloat {
|
||||
origin: float_context.add_float(float.info.clone()),
|
||||
info: float.info,
|
||||
ceiling,
|
||||
})
|
||||
}
|
||||
FloatPlacement {
|
||||
float_context,
|
||||
placed_floats,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From CSS 2.1 § 9.5.1 [1].
|
||||
//
|
||||
// [1]: https://www.w3.org/TR/CSS2/visuren.html#float-position
|
||||
|
||||
// 1. The left outer edge of a left-floating box may not be to the left of the left edge of its
|
||||
// containing block. An analogous rule holds for right-floating elements.
|
||||
fn check_floats_rule_1(placement: &FloatPlacement) {
|
||||
for placed_float in &placement.placed_floats {
|
||||
match placed_float.info.side {
|
||||
FloatSide::Left => assert!(placed_float.origin.inline >= Length::zero()),
|
||||
FloatSide::Right => assert!(
|
||||
placed_float.rect().max_inline_position() <= placement.float_context.inline_size
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If the current box is left-floating, and there are any left-floating boxes generated by
|
||||
// elements earlier in the source document, then for each such earlier box, either the left
|
||||
// outer edge of the current box must be to the right of the right outer edge of the earlier
|
||||
// box, or its top must be lower than the bottom of the earlier box. Analogous rules hold for
|
||||
// right-floating boxes.
|
||||
fn check_floats_rule_2(placement: &FloatPlacement) {
|
||||
for (this_float_index, this_float) in placement.placed_floats.iter().enumerate() {
|
||||
for prev_float in &placement.placed_floats[0..this_float_index] {
|
||||
match (this_float.info.side, prev_float.info.side) {
|
||||
(FloatSide::Left, FloatSide::Left) => {
|
||||
assert!(
|
||||
this_float.origin.inline >= prev_float.rect().max_inline_position() ||
|
||||
this_float.origin.block >= prev_float.rect().max_block_position()
|
||||
);
|
||||
},
|
||||
(FloatSide::Right, FloatSide::Right) => {
|
||||
assert!(
|
||||
this_float.rect().max_inline_position() <= prev_float.origin.inline ||
|
||||
this_float.origin.block >= prev_float.rect().max_block_position()
|
||||
);
|
||||
},
|
||||
(FloatSide::Left, FloatSide::Right) | (FloatSide::Right, FloatSide::Left) => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. The right outer edge of a left-floating box may not be to the right of the left outer edge of
|
||||
// any right-floating box that is next to it. Analogous rules hold for right-floating elements.
|
||||
fn check_floats_rule_3(placement: &FloatPlacement) {
|
||||
for (this_float_index, this_float) in placement.placed_floats.iter().enumerate() {
|
||||
for other_float in &placement.placed_floats[0..this_float_index] {
|
||||
// This logic to check intersection is complicated by the fact that we need to treat
|
||||
// zero-height floats later in the document as "next to" floats earlier in the
|
||||
// document. Otherwise we might end up with a situation like:
|
||||
//
|
||||
// <div id="a" style="float: left; width: 32px; height: 32px"></div>
|
||||
// <div id="b" style="float: right; width: 0px; height: 0px"></div>
|
||||
//
|
||||
// Where the top of `b` should probably be 32px per Rule 3, but unless this distinction
|
||||
// is made the top of `b` could legally be 0px.
|
||||
if this_float.origin.block >= other_float.rect().max_block_position() ||
|
||||
(this_float.info.size.block == Length::zero() &&
|
||||
this_float.rect().max_block_position() < other_float.origin.block) ||
|
||||
(this_float.info.size.block > Length::zero() &&
|
||||
this_float.rect().max_block_position() <= other_float.origin.block)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
match (this_float.info.side, other_float.info.side) {
|
||||
(FloatSide::Left, FloatSide::Right) => {
|
||||
assert!(this_float.rect().max_inline_position() <= other_float.origin.inline);
|
||||
},
|
||||
(FloatSide::Right, FloatSide::Left) => {
|
||||
assert!(this_float.origin.inline >= other_float.rect().max_inline_position());
|
||||
},
|
||||
(FloatSide::Left, FloatSide::Left) | (FloatSide::Right, FloatSide::Right) => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. A floating box's outer top may not be higher than the top of its containing block. When the
|
||||
// float occurs between two collapsing margins, the float is positioned as if it had an
|
||||
// otherwise empty anonymous block parent taking part in the flow. The position of such a parent
|
||||
// is defined by the rules in the section on margin collapsing.
|
||||
fn check_floats_rule_4(placement: &FloatPlacement) {
|
||||
for placed_float in &placement.placed_floats {
|
||||
assert!(placed_float.origin.block >= Length::zero());
|
||||
}
|
||||
}
|
||||
|
||||
// 5. The outer top of a floating box may not be higher than the outer top of any block or floated
|
||||
// box generated by an element earlier in the source document.
|
||||
fn check_floats_rule_5(placement: &FloatPlacement) {
|
||||
let mut block_position = Length::zero();
|
||||
for placed_float in &placement.placed_floats {
|
||||
assert!(placed_float.origin.block >= block_position);
|
||||
block_position = placed_float.origin.block;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. The outer top of an element's floating box may not be higher than the top of any line-box
|
||||
// containing a box generated by an element earlier in the source document.
|
||||
fn check_floats_rule_6(placement: &FloatPlacement) {
|
||||
for placed_float in &placement.placed_floats {
|
||||
assert!(placed_float.origin.block >= placed_float.ceiling);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. A left-floating box that has another left-floating box to its left may not have its right
|
||||
// outer edge to the right of its containing block's right edge. (Loosely: a left float may not
|
||||
// stick out at the right edge, unless it is already as far to the left as possible.) An
|
||||
// analogous rule holds for right-floating elements.
|
||||
fn check_floats_rule_7(placement: &FloatPlacement) {
|
||||
for (placed_float_index, placed_float) in placement.placed_floats.iter().enumerate() {
|
||||
// Only consider floats that stick out.
|
||||
match placed_float.info.side {
|
||||
FloatSide::Left => {
|
||||
if placed_float.rect().max_inline_position() <= placement.float_context.inline_size
|
||||
{
|
||||
continue;
|
||||
}
|
||||
},
|
||||
FloatSide::Right => {
|
||||
if placed_float.origin.inline >= Length::zero() {
|
||||
continue;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Make sure there are no previous floats to the left or right.
|
||||
for prev_float in &placement.placed_floats[0..placed_float_index] {
|
||||
assert!(
|
||||
prev_float.info.side != placed_float.info.side ||
|
||||
prev_float.rect().max_block_position() <= placed_float.origin.block ||
|
||||
prev_float.origin.block >= placed_float.rect().max_block_position()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. A floating box must be placed as high as possible.
|
||||
fn check_floats_rule_8(inline_size: u32, floats_and_perturbations: Vec<(FloatInput, u32)>) {
|
||||
let floats = floats_and_perturbations
|
||||
.iter()
|
||||
.map(|&(ref float, _)| (*float).clone())
|
||||
.collect();
|
||||
let placement = FloatPlacement::place(inline_size, floats);
|
||||
|
||||
for (float_index, &(_, perturbation)) in floats_and_perturbations.iter().enumerate() {
|
||||
if perturbation == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut placement = placement.clone();
|
||||
placement.placed_floats[float_index].origin.block =
|
||||
placement.placed_floats[float_index].origin.block - Length::new(perturbation as f32);
|
||||
|
||||
let result = {
|
||||
let mutex_guard = PANIC_HOOK_MUTEX.lock().unwrap();
|
||||
let _suppressor = PanicMsgSuppressor::new(mutex_guard);
|
||||
panic::catch_unwind(|| check_basic_float_rules(&placement))
|
||||
};
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
// 9. A left-floating box must be put as far to the left as possible, a right-floating box as far
|
||||
// to the right as possible. A higher position is preferred over one that is further to the
|
||||
// left/right.
|
||||
fn check_floats_rule_9(inline_size: u32, floats_and_perturbations: Vec<(FloatInput, u32)>) {
|
||||
let floats = floats_and_perturbations
|
||||
.iter()
|
||||
.map(|&(ref float, _)| (*float).clone())
|
||||
.collect();
|
||||
let placement = FloatPlacement::place(inline_size, floats);
|
||||
|
||||
for (float_index, &(_, perturbation)) in floats_and_perturbations.iter().enumerate() {
|
||||
if perturbation == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut placement = placement.clone();
|
||||
{
|
||||
let mut placed_float = &mut placement.placed_floats[float_index];
|
||||
let perturbation = Length::new(perturbation as f32);
|
||||
match placed_float.info.side {
|
||||
FloatSide::Left => {
|
||||
placed_float.origin.inline = placed_float.origin.inline - perturbation
|
||||
},
|
||||
FloatSide::Right => {
|
||||
placed_float.origin.inline = placed_float.origin.inline + perturbation
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let result = {
|
||||
let mutex_guard = PANIC_HOOK_MUTEX.lock().unwrap();
|
||||
let _suppressor = PanicMsgSuppressor::new(mutex_guard);
|
||||
panic::catch_unwind(|| check_basic_float_rules(&placement))
|
||||
};
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
// From CSS 2.1 § 9.5.2 (https://www.w3.org/TR/CSS2/visuren.html#propdef-clear):
|
||||
//
|
||||
// 10. The top outer edge of the float must be below the bottom outer edge of all earlier
|
||||
// left-floating boxes (in the case of 'clear: left'), or all earlier right-floating boxes (in
|
||||
// the case of 'clear: right'), or both ('clear: both').
|
||||
fn check_floats_rule_10(placement: &FloatPlacement) {
|
||||
let mut block_position = Length::zero();
|
||||
for placed_float in &placement.placed_floats {
|
||||
assert!(placed_float.origin.block >= block_position);
|
||||
block_position = placed_float.origin.block;
|
||||
}
|
||||
|
||||
for (this_float_index, this_float) in placement.placed_floats.iter().enumerate() {
|
||||
if this_float.info.clear == ClearSide::None {
|
||||
continue;
|
||||
}
|
||||
|
||||
for other_float in &placement.placed_floats[0..this_float_index] {
|
||||
// This logic to check intersection is complicated by the fact that we need to treat
|
||||
// zero-height floats later in the document as "next to" floats earlier in the
|
||||
// document. Otherwise we might end up with a situation like:
|
||||
//
|
||||
// <div id="a" style="float: left; width: 32px; height: 32px"></div>
|
||||
// <div id="b" style="float: right; width: 0px; height: 0px"></div>
|
||||
//
|
||||
// Where the top of `b` should probably be 32px per Rule 3, but unless this distinction
|
||||
// is made the top of `b` could legally be 0px.
|
||||
if this_float.origin.block >= other_float.rect().max_block_position() ||
|
||||
(this_float.info.size.block == Length::zero() &&
|
||||
this_float.rect().max_block_position() < other_float.origin.block) ||
|
||||
(this_float.info.size.block > Length::zero() &&
|
||||
this_float.rect().max_block_position() <= other_float.origin.block)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
match this_float.info.clear {
|
||||
ClearSide::Left => assert_ne!(other_float.info.side, FloatSide::Left),
|
||||
ClearSide::Right => assert_ne!(other_float.info.side, FloatSide::Right),
|
||||
ClearSide::Both => assert!(false),
|
||||
ClearSide::None => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checks that rule 1-7 and rule 10 hold (i.e. all rules that don't specify that floats are placed
|
||||
// "as far as possible" in some direction).
|
||||
fn check_basic_float_rules(placement: &FloatPlacement) {
|
||||
check_floats_rule_1(placement);
|
||||
check_floats_rule_2(placement);
|
||||
check_floats_rule_3(placement);
|
||||
check_floats_rule_4(placement);
|
||||
check_floats_rule_5(placement);
|
||||
check_floats_rule_6(placement);
|
||||
check_floats_rule_7(placement);
|
||||
check_floats_rule_10(placement);
|
||||
}
|
||||
|
||||
// Float unit tests
|
||||
|
||||
#[test]
|
||||
fn test_floats_rule_1() {
|
||||
let f: fn(u32, Vec<FloatInput>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(inline_size: u32, floats: Vec<FloatInput>) {
|
||||
check_floats_rule_1(&FloatPlacement::place(inline_size, floats));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floats_rule_2() {
|
||||
let f: fn(u32, Vec<FloatInput>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(inline_size: u32, floats: Vec<FloatInput>) {
|
||||
check_floats_rule_2(&FloatPlacement::place(inline_size, floats));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floats_rule_3() {
|
||||
let f: fn(u32, Vec<FloatInput>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(inline_size: u32, floats: Vec<FloatInput>) {
|
||||
check_floats_rule_3(&FloatPlacement::place(inline_size, floats));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floats_rule_4() {
|
||||
let f: fn(u32, Vec<FloatInput>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(inline_size: u32, floats: Vec<FloatInput>) {
|
||||
check_floats_rule_4(&FloatPlacement::place(inline_size, floats));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floats_rule_5() {
|
||||
let f: fn(u32, Vec<FloatInput>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(inline_size: u32, floats: Vec<FloatInput>) {
|
||||
check_floats_rule_5(&FloatPlacement::place(inline_size, floats));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floats_rule_6() {
|
||||
let f: fn(u32, Vec<FloatInput>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(inline_size: u32, floats: Vec<FloatInput>) {
|
||||
check_floats_rule_6(&FloatPlacement::place(inline_size, floats));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floats_rule_7() {
|
||||
let f: fn(u32, Vec<FloatInput>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(inline_size: u32, floats: Vec<FloatInput>) {
|
||||
check_floats_rule_7(&FloatPlacement::place(inline_size, floats));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floats_rule_8() {
|
||||
let f: fn(u32, Vec<(FloatInput, u32)>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(inline_size: u32, floats: Vec<(FloatInput, u32)>) {
|
||||
check_floats_rule_8(inline_size, floats);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floats_rule_9() {
|
||||
let f: fn(u32, Vec<(FloatInput, u32)>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(inline_size: u32, floats: Vec<(FloatInput, u32)>) {
|
||||
check_floats_rule_9(inline_size, floats);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floats_rule_10() {
|
||||
let f: fn(u32, Vec<FloatInput>) = check;
|
||||
quickcheck::quickcheck(f);
|
||||
fn check(inline_size: u32, floats: Vec<FloatInput>) {
|
||||
check_floats_rule_10(&FloatPlacement::place(inline_size, floats));
|
||||
}
|
||||
}
|
|
@ -222,7 +222,7 @@ class MachCommands(CommandBase):
|
|||
@CommandArgument('--nocapture', default=False, action="store_true",
|
||||
help="Run tests with nocapture ( show test stdout )")
|
||||
@CommandBase.build_like_command_arguments
|
||||
def test_unit(self, test_name=None, package=None, bench=False, nocapture=False, **kwargs):
|
||||
def test_unit(self, test_name=None, package=None, bench=False, nocapture=False, with_layout_2020=False, **kwargs):
|
||||
if test_name is None:
|
||||
test_name = []
|
||||
|
||||
|
@ -255,7 +255,6 @@ class MachCommands(CommandBase):
|
|||
self_contained_tests = [
|
||||
"background_hang_monitor",
|
||||
"gfx",
|
||||
"layout_2013",
|
||||
"msg",
|
||||
"net",
|
||||
"net_traits",
|
||||
|
@ -263,6 +262,10 @@ class MachCommands(CommandBase):
|
|||
"servo_config",
|
||||
"servo_remutex",
|
||||
]
|
||||
if with_layout_2020:
|
||||
self_contained_tests.append("layout_2020")
|
||||
else:
|
||||
self_contained_tests.append("layout_2013")
|
||||
if not packages:
|
||||
packages = set(os.listdir(path.join(self.context.topdir, "tests", "unit"))) - set(['.DS_Store'])
|
||||
packages |= set(self_contained_tests)
|
||||
|
@ -298,7 +301,11 @@ class MachCommands(CommandBase):
|
|||
if nocapture:
|
||||
args += ["--", "--nocapture"]
|
||||
|
||||
err = self.run_cargo_build_like_command("bench" if bench else "test", args, env=env, **kwargs)
|
||||
err = self.run_cargo_build_like_command("bench" if bench else "test",
|
||||
args,
|
||||
env=env,
|
||||
with_layout_2020=with_layout_2020,
|
||||
**kwargs)
|
||||
if err:
|
||||
return err
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ rand = [
|
|||
"hashglobe", # Only used in tests
|
||||
"ipc-channel",
|
||||
"phf_generator",
|
||||
"quickcheck", # Only used in tests
|
||||
"servo_rand",
|
||||
"tempfile",
|
||||
"uuid",
|
||||
|
|
|
@ -112,16 +112,22 @@ macro_rules! parse_longhand {
|
|||
};
|
||||
}
|
||||
|
||||
mod background;
|
||||
mod border;
|
||||
mod box_;
|
||||
mod column;
|
||||
mod effects;
|
||||
mod image;
|
||||
mod inherited_text;
|
||||
mod outline;
|
||||
mod selectors;
|
||||
mod supports;
|
||||
mod text_overflow;
|
||||
mod transition_duration;
|
||||
mod transition_timing_function;
|
||||
|
||||
// These tests test features that are only available in 2013 layout.
|
||||
#[cfg(feature = "layout_2013")]
|
||||
mod background;
|
||||
#[cfg(feature = "layout_2013")]
|
||||
mod border;
|
||||
#[cfg(feature = "layout_2013")]
|
||||
mod column;
|
||||
#[cfg(feature = "layout_2013")]
|
||||
mod text_overflow;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue