diff options
author | Anthony Ramine <n.oxyde@gmail.com> | 2019-07-23 15:44:56 +0200 |
---|---|---|
committer | Anthony Ramine <n.oxyde@gmail.com> | 2019-07-31 17:09:16 +0200 |
commit | 4846d76e82e2d60875472fb8ea375e22d40a0800 (patch) | |
tree | 073ff8aa7ec1c65ad51c2b08bfe55007c6f442e7 /components | |
parent | 87e7e3d429f2122ffa9ef016ba5659a3b21be91b (diff) | |
download | servo-4846d76e82e2d60875472fb8ea375e22d40a0800.tar.gz servo-4846d76e82e2d60875472fb8ea375e22d40a0800.zip |
Make layout_2020 be layout_2013
Diffstat (limited to 'components')
48 files changed, 34529 insertions, 22 deletions
diff --git a/components/layout_2020/Cargo.toml b/components/layout_2020/Cargo.toml index bff14135888..daa5170b23a 100644 --- a/components/layout_2020/Cargo.toml +++ b/components/layout_2020/Cargo.toml @@ -7,9 +7,53 @@ edition = "2018" publish = false [lib] +name = "layout" path = "lib.rs" test = false doctest = false [dependencies] +app_units = "0.7" +atomic_refcell = "0.1" +bitflags = "1.0" +canvas_traits = {path = "../canvas_traits"} +crossbeam-channel = "0.3" +embedder_traits = {path = "../embedder_traits"} +euclid = "0.19" +fnv = "1.0" +fxhash = "0.2" +gfx = {path = "../gfx"} +gfx_traits = {path = "../gfx_traits"} +html5ever = "0.23" +ipc-channel = "0.11" +libc = "0.2" +log = "0.4" +malloc_size_of = { path = "../malloc_size_of" } +msg = {path = "../msg"} +net_traits = {path = "../net_traits"} +num-traits = "0.2" +ordered-float = "1.0" +parking_lot = "0.8" +profile_traits = {path = "../profile_traits"} +range = {path = "../range"} +rayon = "1" +script_layout_interface = {path = "../script_layout_interface"} +script_traits = {path = "../script_traits"} +selectors = { path = "../selectors" } +serde = "1.0" +servo_arc = {path = "../servo_arc"} +servo_atoms = {path = "../atoms"} +servo_geometry = {path = "../geometry"} +serde_json = "1.0" +servo_config = {path = "../config"} +servo_url = {path = "../url"} +smallvec = { version = "0.6", features = ["std", "union"] } style = {path = "../style", features = ["servo", "servo-layout-2020"]} +style_traits = {path = "../style_traits"} +unicode-bidi = {version = "0.3", features = ["with_serde"]} +unicode-script = {version = "0.3", features = ["harfbuzz"]} +webrender_api = {git = "https://github.com/servo/webrender", features = ["ipc"]} +xi-unicode = "0.1.0" + +[dev-dependencies] +size_of_test = {path = "../size_of_test"} diff --git a/components/layout_2020/animation.rs b/components/layout_2020/animation.rs new file mode 100644 index 00000000000..96e4801fa4e --- /dev/null +++ b/components/layout_2020/animation.rs @@ -0,0 +1,211 @@ +/* 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/. */ + +//! CSS transitions and animations. + +use crate::context::LayoutContext; +use crate::display_list::items::OpaqueNode; +use crate::flow::{Flow, GetBaseFlow}; +use crate::opaque_node::OpaqueNodeMethods; +use crossbeam_channel::Receiver; +use fxhash::{FxHashMap, FxHashSet}; +use ipc_channel::ipc::IpcSender; +use msg::constellation_msg::PipelineId; +use script_traits::UntrustedNodeAddress; +use script_traits::{AnimationState, ConstellationControlMsg, LayoutMsg as ConstellationMsg}; +use style::animation::{update_style_for_animation, Animation}; +use style::dom::TElement; +use style::font_metrics::ServoMetricsProvider; +use style::selector_parser::RestyleDamage; +use style::timer::Timer; + +/// Processes any new animations that were discovered after style recalculation. +/// Also expire any old animations that have completed, inserting them into +/// `expired_animations`. +pub fn update_animation_state<E>( + constellation_chan: &IpcSender<ConstellationMsg>, + script_chan: &IpcSender<ConstellationControlMsg>, + running_animations: &mut FxHashMap<OpaqueNode, Vec<Animation>>, + expired_animations: &mut FxHashMap<OpaqueNode, Vec<Animation>>, + mut keys_to_remove: FxHashSet<OpaqueNode>, + mut newly_transitioning_nodes: Option<&mut Vec<UntrustedNodeAddress>>, + new_animations_receiver: &Receiver<Animation>, + pipeline_id: PipelineId, + timer: &Timer, +) where + E: TElement, +{ + let mut new_running_animations = vec![]; + while let Ok(animation) = new_animations_receiver.try_recv() { + let mut should_push = true; + if let Animation::Keyframes(ref node, _, ref name, ref state) = animation { + // If the animation was already present in the list for the + // node, just update its state, else push the new animation to + // run. + if let Some(ref mut animations) = running_animations.get_mut(node) { + // TODO: This being linear is probably not optimal. + for anim in animations.iter_mut() { + if let Animation::Keyframes(_, _, ref anim_name, ref mut anim_state) = *anim { + if *name == *anim_name { + debug!("update_animation_state: Found other animation {}", name); + anim_state.update_from_other(&state, timer); + should_push = false; + break; + } + } + } + } + } + + if should_push { + new_running_animations.push(animation); + } + } + + if running_animations.is_empty() && new_running_animations.is_empty() { + // Nothing to do. Return early so we don't flood the compositor with + // `ChangeRunningAnimationsState` messages. + return; + } + + let now = timer.seconds(); + // Expire old running animations. + // + // TODO: Do not expunge Keyframes animations, since we need that state if + // the animation gets re-triggered. Probably worth splitting in two + // different maps, or at least using a linked list? + for (key, running_animations) in running_animations.iter_mut() { + let mut animations_still_running = vec![]; + for mut running_animation in running_animations.drain(..) { + let still_running = !running_animation.is_expired() && + match running_animation { + Animation::Transition(_, started_at, ref frame) => { + now < started_at + frame.duration + }, + Animation::Keyframes(_, _, _, ref mut state) => { + // This animation is still running, or we need to keep + // iterating. + now < state.started_at + state.duration || state.tick() + }, + }; + + debug!( + "update_animation_state({:?}): {:?}", + still_running, running_animation + ); + + if still_running { + animations_still_running.push(running_animation); + continue; + } + + if let Animation::Transition(node, _, ref frame) = running_animation { + script_chan + .send(ConstellationControlMsg::TransitionEnd( + node.to_untrusted_node_address(), + frame.property_animation.property_name().into(), + frame.duration, + )) + .unwrap(); + } + + expired_animations + .entry(*key) + .or_insert_with(Vec::new) + .push(running_animation); + } + + if animations_still_running.is_empty() { + keys_to_remove.insert(*key); + } else { + *running_animations = animations_still_running + } + } + + for key in keys_to_remove { + running_animations.remove(&key).unwrap(); + } + + // Add new running animations. + for new_running_animation in new_running_animations { + if new_running_animation.is_transition() { + match newly_transitioning_nodes { + Some(ref mut nodes) => { + nodes.push(new_running_animation.node().to_untrusted_node_address()); + }, + None => { + warn!("New transition encountered from compositor-initiated layout."); + }, + } + } + + running_animations + .entry(*new_running_animation.node()) + .or_insert_with(Vec::new) + .push(new_running_animation) + } + + let animation_state = if running_animations.is_empty() { + AnimationState::NoAnimationsPresent + } else { + AnimationState::AnimationsPresent + }; + + constellation_chan + .send(ConstellationMsg::ChangeRunningAnimationsState( + pipeline_id, + animation_state, + )) + .unwrap(); +} + +/// Recalculates style for a set of animations. This does *not* run with the DOM +/// lock held. Returns a set of nodes associated with animations that are no longer +/// valid. +pub fn recalc_style_for_animations<E>( + context: &LayoutContext, + flow: &mut dyn Flow, + animations: &FxHashMap<OpaqueNode, Vec<Animation>>, +) -> FxHashSet<OpaqueNode> +where + E: TElement, +{ + let mut invalid_nodes = animations.keys().cloned().collect(); + do_recalc_style_for_animations::<E>(context, flow, animations, &mut invalid_nodes); + invalid_nodes +} + +fn do_recalc_style_for_animations<E>( + context: &LayoutContext, + flow: &mut dyn Flow, + animations: &FxHashMap<OpaqueNode, Vec<Animation>>, + invalid_nodes: &mut FxHashSet<OpaqueNode>, +) where + E: TElement, +{ + let mut damage = RestyleDamage::empty(); + flow.mutate_fragments(&mut |fragment| { + if let Some(ref animations) = animations.get(&fragment.node) { + invalid_nodes.remove(&fragment.node); + for animation in animations.iter() { + let old_style = fragment.style.clone(); + update_style_for_animation::<E>( + &context.style_context, + animation, + &mut fragment.style, + &ServoMetricsProvider, + ); + let difference = + RestyleDamage::compute_style_difference(&old_style, &fragment.style); + damage |= difference.damage; + } + } + }); + + let base = flow.mut_base(); + base.restyle_damage.insert(damage); + for kid in base.children.iter_mut() { + do_recalc_style_for_animations::<E>(context, kid, animations, invalid_nodes) + } +} diff --git a/components/layout_2020/block.rs b/components/layout_2020/block.rs new file mode 100644 index 00000000000..24128ca2ae7 --- /dev/null +++ b/components/layout_2020/block.rs @@ -0,0 +1,3697 @@ +/* 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/. */ + +//! Layout for CSS block-level elements. +//! +//! As a terminology note, the term *absolute positioning* here refers to elements with position +//! `absolute` or `fixed`. The term *positioned element* refers to elements with position +//! `relative`, `absolute`, and `fixed`. The term *containing block* (occasionally abbreviated as +//! *CB*) is the containing block for the current flow, which differs from the static containing +//! block if the flow is absolutely-positioned. +//! +//! "CSS 2.1" or "CSS 2.2" refers to the editor's draft of the W3C "Cascading Style Sheets Level 2 +//! Revision 2 (CSS 2.2) Specification" available here: +//! +//! http://dev.w3.org/csswg/css2/ +//! +//! "INTRINSIC" refers to L. David Baron's "More Precise Definitions of Inline Layout and Table +//! Layout" available here: +//! +//! http://dbaron.org/css/intrinsic/ +//! +//! "CSS-SIZING" refers to the W3C "CSS Intrinsic & Extrinsic Sizing Module Level 3" document +//! available here: +//! +//! http://dev.w3.org/csswg/css-sizing/ + +use crate::context::LayoutContext; +use crate::display_list::items::DisplayListSection; +use crate::display_list::{ + BorderPaintingMode, DisplayListBuildState, StackingContextCollectionFlags, + StackingContextCollectionState, +}; +use crate::floats::{ClearType, FloatKind, Floats, PlacementInfo}; +use crate::flow::{ + BaseFlow, EarlyAbsolutePositionInfo, Flow, FlowClass, ForceNonfloatedFlag, GetBaseFlow, +}; +use crate::flow::{ + FlowFlags, FragmentationContext, ImmutableFlowUtils, LateAbsolutePositionInfo, OpaqueFlow, +}; +use crate::flow_list::FlowList; +use crate::fragment::{ + CoordinateSystem, Fragment, FragmentBorderBoxIterator, FragmentFlags, Overflow, +}; +use crate::incremental::RelayoutMode; +use crate::layout_debug; +use crate::model::{ + AdjoiningMargins, CollapsibleMargins, IntrinsicISizes, MarginCollapseInfo, MaybeAuto, +}; +use crate::sequential; +use crate::traversal::PreorderFlowTraversal; +use app_units::{Au, MAX_AU}; +use euclid::{Point2D, Rect, SideOffsets2D, Size2D}; +use gfx_traits::print_tree::PrintTree; +use serde::{Serialize, Serializer}; +use servo_geometry::MaxRect; +use std::cmp::{max, min}; +use std::fmt; +use std::sync::Arc; +use style::computed_values::box_sizing::T as BoxSizing; +use style::computed_values::display::T as Display; +use style::computed_values::float::T as Float; +use style::computed_values::overflow_x::T as StyleOverflow; +use style::computed_values::position::T as Position; +use style::computed_values::text_align::T as TextAlign; +use style::context::SharedStyleContext; +use style::logical_geometry::{LogicalMargin, LogicalPoint, LogicalRect, LogicalSize, WritingMode}; +use style::properties::ComputedValues; +use style::servo::restyle_damage::ServoRestyleDamage; +use style::values::computed::{LengthPercentageOrAuto, MaxSize, Size}; + +/// Information specific to floated blocks. +#[derive(Clone, Serialize)] +pub struct FloatedBlockInfo { + /// The amount of inline size that is available for the float. + pub containing_inline_size: Au, + + /// The float ceiling, relative to `BaseFlow::position::cur_b` (i.e. the top part of the border + /// box). + pub float_ceiling: Au, + + /// Left or right? + pub float_kind: FloatKind, +} + +impl FloatedBlockInfo { + pub fn new(float_kind: FloatKind) -> FloatedBlockInfo { + FloatedBlockInfo { + containing_inline_size: Au(0), + float_ceiling: Au(0), + float_kind: float_kind, + } + } +} + +/// The solutions for the block-size-and-margins constraint equation. +#[derive(Clone, Copy)] +struct BSizeConstraintSolution { + block_start: Au, + block_size: Au, + margin_block_start: Au, + margin_block_end: Au, +} + +impl BSizeConstraintSolution { + fn new( + block_start: Au, + block_size: Au, + margin_block_start: Au, + margin_block_end: Au, + ) -> BSizeConstraintSolution { + BSizeConstraintSolution { + block_start: block_start, + block_size: block_size, + margin_block_start: margin_block_start, + margin_block_end: margin_block_end, + } + } + + /// Solve the vertical constraint equation for absolute non-replaced elements. + /// + /// CSS Section 10.6.4 + /// Constraint equation: + /// block-start + block-end + block-size + margin-block-start + margin-block-end + /// = absolute containing block block-size - (vertical padding and border) + /// [aka available_block-size] + /// + /// Return the solution for the equation. + fn solve_vertical_constraints_abs_nonreplaced( + block_size: MaybeAuto, + block_start_margin: MaybeAuto, + block_end_margin: MaybeAuto, + block_start: MaybeAuto, + block_end: MaybeAuto, + content_block_size: Au, + available_block_size: Au, + ) -> BSizeConstraintSolution { + let (block_start, block_size, margin_block_start, margin_block_end) = + match (block_start, block_end, block_size) { + (MaybeAuto::Auto, MaybeAuto::Auto, MaybeAuto::Auto) => { + let margin_block_start = block_start_margin.specified_or_zero(); + let margin_block_end = block_end_margin.specified_or_zero(); + // Now it is the same situation as block-start Specified and block-end + // and block-size Auto. + let block_size = content_block_size; + // Use a dummy value for `block_start`, since it has the static position. + (Au(0), block_size, margin_block_start, margin_block_end) + }, + ( + MaybeAuto::Specified(block_start), + MaybeAuto::Specified(block_end), + MaybeAuto::Specified(block_size), + ) => { + match (block_start_margin, block_end_margin) { + (MaybeAuto::Auto, MaybeAuto::Auto) => { + let total_margin_val = + available_block_size - block_start - block_end - block_size; + ( + block_start, + block_size, + total_margin_val.scale_by(0.5), + total_margin_val.scale_by(0.5), + ) + }, + (MaybeAuto::Specified(margin_block_start), MaybeAuto::Auto) => { + let sum = block_start + block_end + block_size + margin_block_start; + ( + block_start, + block_size, + margin_block_start, + available_block_size - sum, + ) + }, + (MaybeAuto::Auto, MaybeAuto::Specified(margin_block_end)) => { + let sum = block_start + block_end + block_size + margin_block_end; + ( + block_start, + block_size, + available_block_size - sum, + margin_block_end, + ) + }, + ( + MaybeAuto::Specified(margin_block_start), + MaybeAuto::Specified(margin_block_end), + ) => { + // Values are over-constrained. Ignore value for 'block-end'. + ( + block_start, + block_size, + margin_block_start, + margin_block_end, + ) + }, + } + }, + + // For the rest of the cases, auto values for margin are set to 0 + + // If only one is Auto, solve for it + ( + MaybeAuto::Auto, + MaybeAuto::Specified(block_end), + MaybeAuto::Specified(block_size), + ) => { + let margin_block_start = block_start_margin.specified_or_zero(); + let margin_block_end = block_end_margin.specified_or_zero(); + let sum = block_end + block_size + margin_block_start + margin_block_end; + ( + available_block_size - sum, + block_size, + margin_block_start, + margin_block_end, + ) + }, + ( + MaybeAuto::Specified(block_start), + MaybeAuto::Auto, + MaybeAuto::Specified(block_size), + ) => { + let margin_block_start = block_start_margin.specified_or_zero(); + let margin_block_end = block_end_margin.specified_or_zero(); + ( + block_start, + block_size, + margin_block_start, + margin_block_end, + ) + }, + ( + MaybeAuto::Specified(block_start), + MaybeAuto::Specified(block_end), + MaybeAuto::Auto, + ) => { + let margin_block_start = block_start_margin.specified_or_zero(); + let margin_block_end = block_end_margin.specified_or_zero(); + let sum = block_start + block_end + margin_block_start + margin_block_end; + ( + block_start, + available_block_size - sum, + margin_block_start, + margin_block_end, + ) + }, + + // If block-size is auto, then block-size is content block-size. Solve for the + // non-auto value. + (MaybeAuto::Specified(block_start), MaybeAuto::Auto, MaybeAuto::Auto) => { + let margin_block_start = block_start_margin.specified_or_zero(); + let margin_block_end = block_end_margin.specified_or_zero(); + let block_size = content_block_size; + ( + block_start, + block_size, + margin_block_start, + margin_block_end, + ) + }, + (MaybeAuto::Auto, MaybeAuto::Specified(block_end), MaybeAuto::Auto) => { + let margin_block_start = block_start_margin.specified_or_zero(); + let margin_block_end = block_end_margin.specified_or_zero(); + let block_size = content_block_size; + let sum = block_end + block_size + margin_block_start + margin_block_end; + ( + available_block_size - sum, + block_size, + margin_block_start, + margin_block_end, + ) + }, + + (MaybeAuto::Auto, MaybeAuto::Auto, MaybeAuto::Specified(block_size)) => { + let margin_block_start = block_start_margin.specified_or_zero(); + let margin_block_end = block_end_margin.specified_or_zero(); + // Use a dummy value for `block_start`, since it has the static position. + (Au(0), block_size, margin_block_start, margin_block_end) + }, + }; + + BSizeConstraintSolution::new( + block_start, + block_size, + margin_block_start, + margin_block_end, + ) + } + + /// Solve the vertical constraint equation for absolute replaced elements. + /// + /// Assumption: The used value for block-size has already been calculated. + /// + /// CSS Section 10.6.5 + /// Constraint equation: + /// block-start + block-end + block-size + margin-block-start + margin-block-end + /// = absolute containing block block-size - (vertical padding and border) + /// [aka available block-size] + /// + /// Return the solution for the equation. + fn solve_vertical_constraints_abs_replaced( + block_size: Au, + block_start_margin: MaybeAuto, + block_end_margin: MaybeAuto, + block_start: MaybeAuto, + block_end: MaybeAuto, + _: Au, + available_block_size: Au, + ) -> BSizeConstraintSolution { + let (block_start, block_size, margin_block_start, margin_block_end) = + match (block_start, block_end) { + (MaybeAuto::Auto, MaybeAuto::Auto) => { + let margin_block_start = block_start_margin.specified_or_zero(); + let margin_block_end = block_end_margin.specified_or_zero(); + // Use a dummy value for `block_start`, since it has the static position. + (Au(0), block_size, margin_block_start, margin_block_end) + }, + (MaybeAuto::Specified(block_start), MaybeAuto::Specified(block_end)) => { + match (block_start_margin, block_end_margin) { + (MaybeAuto::Auto, MaybeAuto::Auto) => { + let total_margin_val = + available_block_size - block_start - block_end - block_size; + ( + block_start, + block_size, + total_margin_val.scale_by(0.5), + total_margin_val.scale_by(0.5), + ) + }, + (MaybeAuto::Specified(margin_block_start), MaybeAuto::Auto) => { + let sum = block_start + block_end + block_size + margin_block_start; + ( + block_start, + block_size, + margin_block_start, + available_block_size - sum, + ) + }, + (MaybeAuto::Auto, MaybeAuto::Specified(margin_block_end)) => { + let sum = block_start + block_end + block_size + margin_block_end; + ( + block_start, + block_size, + available_block_size - sum, + margin_block_end, + ) + }, + ( + MaybeAuto::Specified(margin_block_start), + MaybeAuto::Specified(margin_block_end), + ) => { + // Values are over-constrained. Ignore value for 'block-end'. + ( + block_start, + block_size, + margin_block_start, + margin_block_end, + ) + }, + } + }, + + // If only one is Auto, solve for it + (MaybeAuto::Auto, MaybeAuto::Specified(block_end)) => { + let margin_block_start = block_start_margin.specified_or_zero(); + let margin_block_end = block_end_margin.specified_or_zero(); + let sum = block_end + block_size + margin_block_start + margin_block_end; + ( + available_block_size - sum, + block_size, + margin_block_start, + margin_block_end, + ) + }, + (MaybeAuto::Specified(block_start), MaybeAuto::Auto) => { + let margin_block_start = block_start_margin.specified_or_zero(); + let margin_block_end = block_end_margin.specified_or_zero(); + ( + block_start, + block_size, + margin_block_start, + margin_block_end, + ) + }, + }; + BSizeConstraintSolution::new( + block_start, + block_size, + margin_block_start, + margin_block_end, + ) + } +} + +/// Performs block-size calculations potentially multiple times, taking +/// (assuming an horizontal writing mode) `height`, `min-height`, and `max-height` +/// into account. After each call to `next()`, the caller must call `.try()` with the +/// current calculated value of `height`. +/// +/// See CSS 2.1 § 10.7. +pub struct CandidateBSizeIterator { + block_size: MaybeAuto, + max_block_size: Option<Au>, + min_block_size: Au, + pub candidate_value: Au, + status: CandidateBSizeIteratorStatus, +} + +impl CandidateBSizeIterator { + /// Creates a new candidate block-size iterator. `block_container_block-size` is `None` if the block-size + /// of the block container has not been determined yet. It will always be `Some` in the case of + /// absolutely-positioned containing blocks. + pub fn new( + fragment: &Fragment, + block_container_block_size: Option<Au>, + ) -> CandidateBSizeIterator { + // Per CSS 2.1 § 10.7, (assuming an horizontal writing mode,) + // percentages in `min-height` and `max-height` refer to the height of + // the containing block. + // If that is not determined yet by the time we need to resolve + // `min-height` and `max-height`, percentage values are ignored. + + let block_size = match fragment.style.content_block_size() { + Size::Auto => MaybeAuto::Auto, + Size::LengthPercentage(ref lp) => { + MaybeAuto::from_option(lp.maybe_to_used_value(block_container_block_size)) + }, + }; + + let max_block_size = match fragment.style.max_block_size() { + MaxSize::None => None, + MaxSize::LengthPercentage(ref lp) => lp.maybe_to_used_value(block_container_block_size), + }; + + let min_block_size = match fragment.style.min_block_size() { + Size::Auto => MaybeAuto::Auto, + Size::LengthPercentage(ref lp) => { + MaybeAuto::from_option(lp.maybe_to_used_value(block_container_block_size)) + }, + } + .specified_or_zero(); + + // If the style includes `box-sizing: border-box`, subtract the border and padding. + let adjustment_for_box_sizing = match fragment.style.get_position().box_sizing { + BoxSizing::BorderBox => fragment.border_padding.block_start_end(), + BoxSizing::ContentBox => Au(0), + }; + + return CandidateBSizeIterator { + block_size: block_size.map(|size| adjust(size, adjustment_for_box_sizing)), + max_block_size: max_block_size.map(|size| adjust(size, adjustment_for_box_sizing)), + min_block_size: adjust(min_block_size, adjustment_for_box_sizing), + candidate_value: Au(0), + status: CandidateBSizeIteratorStatus::Initial, + }; + + fn adjust(size: Au, delta: Au) -> Au { + max(size - delta, Au(0)) + } + } +} + +impl Iterator for CandidateBSizeIterator { + type Item = MaybeAuto; + fn next(&mut self) -> Option<MaybeAuto> { + self.status = match self.status { + CandidateBSizeIteratorStatus::Initial => CandidateBSizeIteratorStatus::Trying, + CandidateBSizeIteratorStatus::Trying => match self.max_block_size { + Some(max_block_size) if self.candidate_value > max_block_size => { + CandidateBSizeIteratorStatus::TryingMax + }, + _ if self.candidate_value < self.min_block_size => { + CandidateBSizeIteratorStatus::TryingMin + }, + _ => CandidateBSizeIteratorStatus::Found, + }, + CandidateBSizeIteratorStatus::TryingMax => { + if self.candidate_value < self.min_block_size { + CandidateBSizeIteratorStatus::TryingMin + } else { + CandidateBSizeIteratorStatus::Found + } + }, + CandidateBSizeIteratorStatus::TryingMin | CandidateBSizeIteratorStatus::Found => { + CandidateBSizeIteratorStatus::Found + }, + }; + + match self.status { + CandidateBSizeIteratorStatus::Trying => Some(self.block_size), + CandidateBSizeIteratorStatus::TryingMax => { + Some(MaybeAuto::Specified(self.max_block_size.unwrap())) + }, + CandidateBSizeIteratorStatus::TryingMin => { + Some(MaybeAuto::Specified(self.min_block_size)) + }, + CandidateBSizeIteratorStatus::Found => None, + CandidateBSizeIteratorStatus::Initial => panic!(), + } + } +} + +enum CandidateBSizeIteratorStatus { + Initial, + Trying, + TryingMax, + TryingMin, + Found, +} + +// A helper function used in block-size calculation. +fn translate_including_floats(cur_b: &mut Au, delta: Au, floats: &mut Floats) { + *cur_b = *cur_b + delta; + let writing_mode = floats.writing_mode; + floats.translate(LogicalSize::new(writing_mode, Au(0), -delta)); +} + +/// The real assign-block-sizes traversal for flows with position 'absolute'. +/// +/// This is a traversal of an Absolute Flow tree. +/// - Relatively positioned flows and the Root flow start new Absolute flow trees. +/// - The kids of a flow in this tree will be the flows for which it is the +/// absolute Containing Block. +/// - Thus, leaf nodes and inner non-root nodes are all Absolute Flows. +/// +/// A Flow tree can have several Absolute Flow trees (depending on the number +/// of relatively positioned flows it has). +/// +/// Note that flows with position 'fixed' just form a flat list as they all +/// have the Root flow as their CB. +pub struct AbsoluteAssignBSizesTraversal<'a>(pub &'a SharedStyleContext<'a>); + +impl<'a> PreorderFlowTraversal for AbsoluteAssignBSizesTraversal<'a> { + #[inline] + fn process(&self, flow: &mut dyn Flow) { + if !flow.is_block_like() { + return; + } + + // This flow might not be an absolutely positioned flow if it is the root of the tree. + let block = flow.as_mut_block(); + if !block + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + return; + } + + if !block + .base + .restyle_damage + .intersects(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW) + { + return; + } + + block.calculate_absolute_block_size_and_margins(self.0); + } +} + +pub enum BlockType { + Replaced, + NonReplaced, + AbsoluteReplaced, + AbsoluteNonReplaced, + FloatReplaced, + FloatNonReplaced, + InlineBlockReplaced, + InlineBlockNonReplaced, + InlineFlexItem, +} + +#[derive(Clone, PartialEq)] +pub enum MarginsMayCollapseFlag { + MarginsMayCollapse, + MarginsMayNotCollapse, +} + +#[derive(Debug, PartialEq)] +pub enum FormattingContextType { + None, + Block, + Other, +} + +#[allow(unsafe_code)] +unsafe impl crate::flow::HasBaseFlow for BlockFlow {} + +// A block formatting context. +#[derive(Serialize)] +#[repr(C)] +pub struct BlockFlow { + /// Data common to all flows. + pub base: BaseFlow, + + /// The associated fragment. + pub fragment: Fragment, + + /// Additional floating flow members. + pub float: Option<Box<FloatedBlockInfo>>, + + /// Various flags. + flags: BlockFlowFlags, +} + +bitflags! { + struct BlockFlowFlags: u8 { + #[doc = "If this is set, then this block flow is the root flow."] + const IS_ROOT = 0b0000_0001; + #[doc = "If this is set, then this block flow has overflow and it will scroll."] + const HAS_SCROLLING_OVERFLOW = 0b0000_0010; + } +} + +impl Serialize for BlockFlowFlags { + fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + self.bits().serialize(serializer) + } +} + +impl BlockFlow { + pub fn from_fragment(fragment: Fragment) -> BlockFlow { + BlockFlow::from_fragment_and_float_kind(fragment, None) + } + + pub fn from_fragment_and_float_kind( + fragment: Fragment, + float_kind: Option<FloatKind>, + ) -> BlockFlow { + let writing_mode = fragment.style().writing_mode; + BlockFlow { + base: BaseFlow::new( + Some(fragment.style()), + writing_mode, + match float_kind { + Some(_) => ForceNonfloatedFlag::FloatIfNecessary, + None => ForceNonfloatedFlag::ForceNonfloated, + }, + ), + fragment: fragment, + float: float_kind.map(|kind| Box::new(FloatedBlockInfo::new(kind))), + flags: BlockFlowFlags::empty(), + } + } + + /// Return the type of this block. + /// + /// This determines the algorithm used to calculate inline-size, block-size, and the + /// relevant margins for this Block. + pub fn block_type(&self) -> BlockType { + if self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + if self.fragment.is_replaced() { + BlockType::AbsoluteReplaced + } else { + BlockType::AbsoluteNonReplaced + } + } else if self.is_inline_flex_item() { + BlockType::InlineFlexItem + } else if self.base.flags.is_float() { + if self.fragment.is_replaced() { + BlockType::FloatReplaced + } else { + BlockType::FloatNonReplaced + } + } else if self.is_inline_block_or_inline_flex() { + if self.fragment.is_replaced() { + BlockType::InlineBlockReplaced + } else { + BlockType::InlineBlockNonReplaced + } + } else { + if self.fragment.is_replaced() { + BlockType::Replaced + } else { + BlockType::NonReplaced + } + } + } + + /// Compute the actual inline size and position for this block. + pub fn compute_used_inline_size( + &mut self, + shared_context: &SharedStyleContext, + containing_block_inline_size: Au, + ) { + let block_type = self.block_type(); + match block_type { + BlockType::AbsoluteReplaced => { + let inline_size_computer = AbsoluteReplaced; + inline_size_computer.compute_used_inline_size( + self, + shared_context, + containing_block_inline_size, + ); + }, + BlockType::AbsoluteNonReplaced => { + let inline_size_computer = AbsoluteNonReplaced; + inline_size_computer.compute_used_inline_size( + self, + shared_context, + containing_block_inline_size, + ); + }, + BlockType::FloatReplaced => { + let inline_size_computer = FloatReplaced; + inline_size_computer.compute_used_inline_size( + self, + shared_context, + containing_block_inline_size, + ); + }, + BlockType::FloatNonReplaced => { + let inline_size_computer = FloatNonReplaced; + inline_size_computer.compute_used_inline_size( + self, + shared_context, + containing_block_inline_size, + ); + }, + BlockType::InlineBlockReplaced => { + let inline_size_computer = InlineBlockReplaced; + inline_size_computer.compute_used_inline_size( + self, + shared_context, + containing_block_inline_size, + ); + }, + BlockType::InlineBlockNonReplaced => { + let inline_size_computer = InlineBlockNonReplaced; + inline_size_computer.compute_used_inline_size( + self, + shared_context, + containing_block_inline_size, + ); + }, + BlockType::Replaced => { + let inline_size_computer = BlockReplaced; + inline_size_computer.compute_used_inline_size( + self, + shared_context, + containing_block_inline_size, + ); + }, + BlockType::NonReplaced => { + let inline_size_computer = BlockNonReplaced; + inline_size_computer.compute_used_inline_size( + self, + shared_context, + containing_block_inline_size, + ); + }, + BlockType::InlineFlexItem => { + let inline_size_computer = InlineFlexItem; + inline_size_computer.compute_used_inline_size( + self, + shared_context, + containing_block_inline_size, + ); + }, + } + } + + /// Return this flow's fragment. + pub fn fragment(&mut self) -> &mut Fragment { + &mut self.fragment + } + + pub fn stacking_relative_border_box(&self, coor: CoordinateSystem) -> Rect<Au> { + return self.fragment.stacking_relative_border_box( + &self.base.stacking_relative_position, + &self + .base + .early_absolute_position_info + .relative_containing_block_size, + self.base + .early_absolute_position_info + .relative_containing_block_mode, + coor, + ); + } + + /// Return the size of the containing block for the given immediate absolute descendant of this + /// flow. + /// + /// Right now, this only gets the containing block size for absolutely positioned elements. + /// Note: We assume this is called in a top-down traversal, so it is ok to reference the CB. + #[inline] + pub fn containing_block_size( + &self, + viewport_size: &Size2D<Au>, + descendant: OpaqueFlow, + ) -> LogicalSize<Au> { + debug_assert!(self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED)); + if self.is_fixed() || self.is_root() { + // Initial containing block is the CB for the root + LogicalSize::from_physical(self.base.writing_mode, *viewport_size) + } else { + self.base + .absolute_cb + .generated_containing_block_size(descendant) + } + } + + /// Return shrink-to-fit inline-size. + /// + /// This is where we use the preferred inline-sizes and minimum inline-sizes + /// calculated in the bubble-inline-sizes traversal. + pub fn get_shrink_to_fit_inline_size(&self, available_inline_size: Au) -> Au { + let content_intrinsic_inline_sizes = self.content_intrinsic_inline_sizes(); + min( + content_intrinsic_inline_sizes.preferred_inline_size, + max( + content_intrinsic_inline_sizes.minimum_inline_size, + available_inline_size, + ), + ) + } + + /// If this is the root flow, shifts all kids down and adjusts our size to account for + /// root flow margins, which should never be collapsed according to CSS § 8.3.1. + /// + /// TODO(#2017, pcwalton): This is somewhat inefficient (traverses kids twice); can we do + /// better? + fn adjust_fragments_for_collapsed_margins_if_root( + &mut self, + shared_context: &SharedStyleContext, + ) { + if !self.is_root() { + return; + } + + let (block_start_margin_value, block_end_margin_value) = match self.base.collapsible_margins + { + CollapsibleMargins::CollapseThrough(_) => { + panic!("Margins unexpectedly collapsed through root flow.") + }, + CollapsibleMargins::Collapse(block_start_margin, block_end_margin) => { + (block_start_margin.collapse(), block_end_margin.collapse()) + }, + CollapsibleMargins::None(block_start, block_end) => (block_start, block_end), + }; + + // Shift all kids down (or up, if margins are negative) if necessary. + if block_start_margin_value != Au(0) { + for kid in self.base.child_iter_mut() { + let kid_base = kid.mut_base(); + kid_base.position.start.b = kid_base.position.start.b + block_start_margin_value + } + } + + // FIXME(#2003, pcwalton): The max is taken here so that you can scroll the page, but this + // is not correct behavior according to CSS 2.1 § 10.5. Instead I think we should treat the + // root element as having `overflow: scroll` and use the layers-based scrolling + // infrastructure to make it scrollable. + let viewport_size = LogicalSize::from_physical( + self.fragment.style.writing_mode, + shared_context.viewport_size(), + ); + let block_size = max( + viewport_size.block, + self.fragment.border_box.size.block + block_start_margin_value + block_end_margin_value, + ); + + self.base.position.size.block = block_size; + self.fragment.border_box.size.block = block_size; + } + + // FIXME: Record enough info to deal with fragmented decorations. + // See https://drafts.csswg.org/css-break/#break-decoration + // For borders, this might be `enum FragmentPosition { First, Middle, Last }` + fn clone_with_children(&self, new_children: FlowList) -> BlockFlow { + BlockFlow { + base: self.base.clone_with_children(new_children), + fragment: self.fragment.clone(), + float: self.float.clone(), + ..*self + } + } + + /// Writes in the size of the relative containing block for children. (This information + /// is also needed to handle RTL.) + fn propagate_early_absolute_position_info_to_children(&mut self) { + for kid in self.base.child_iter_mut() { + kid.mut_base().early_absolute_position_info = EarlyAbsolutePositionInfo { + relative_containing_block_size: self.fragment.content_box().size, + relative_containing_block_mode: self.fragment.style().writing_mode, + } + } + } + + /// Assign block-size for current flow. + /// + /// * Collapse margins for flow's children and set in-flow child flows' block offsets now that + /// we know their block-sizes. + /// * Calculate and set the block-size of the current flow. + /// * Calculate block-size, vertical margins, and block offset for the flow's box using CSS § + /// 10.6.7. + /// + /// For absolute flows, we store the calculated content block-size for the flow. We defer the + /// calculation of the other values until a later traversal. + /// + /// When `fragmentation_context` is given (not `None`), this should fit as much of the content + /// as possible within the available block size. + /// If there is more content (that doesn’t fit), this flow is *fragmented* + /// with the extra content moved to another fragment (a flow like this one) which is returned. + /// See `Flow::fragment`. + /// + /// The return value is always `None` when `fragmentation_context` is `None`. + /// + /// `inline(always)` because this is only ever called by in-order or non-in-order top-level + /// methods. + #[inline(always)] + pub fn assign_block_size_block_base( + &mut self, + layout_context: &LayoutContext, + mut fragmentation_context: Option<FragmentationContext>, + margins_may_collapse: MarginsMayCollapseFlag, + ) -> Option<Arc<dyn Flow>> { + let _scope = layout_debug_scope!("assign_block_size_block_base {:x}", self.base.debug_id()); + + let mut break_at = None; + let content_box = self.fragment.content_box(); + if self + .base + .restyle_damage + .contains(ServoRestyleDamage::REFLOW) + { + // Our current border-box position. + let mut cur_b = Au(0); + + // Absolute positioning establishes a block formatting context. Don't propagate floats + // in or out. (But do propagate them between kids.) + if self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) || + margins_may_collapse != MarginsMayCollapseFlag::MarginsMayCollapse + { + self.base.floats = Floats::new(self.fragment.style.writing_mode); + } + + let writing_mode = self.base.floats.writing_mode; + self.base.floats.translate(LogicalSize::new( + writing_mode, + -self.fragment.inline_start_offset(), + Au(0), + )); + + // The sum of our block-start border and block-start padding. + let block_start_offset = self.fragment.border_padding.block_start; + translate_including_floats(&mut cur_b, block_start_offset, &mut self.base.floats); + + let can_collapse_block_start_margin_with_kids = margins_may_collapse == + MarginsMayCollapseFlag::MarginsMayCollapse && + !self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) && + self.fragment.border_padding.block_start == Au(0); + let mut margin_collapse_info = MarginCollapseInfo::initialize_block_start_margin( + &self.fragment, + can_collapse_block_start_margin_with_kids, + ); + + // At this point, `cur_b` is at the content edge of our box. Now iterate over children. + let mut floats = self.base.floats.clone(); + let thread_id = self.base.thread_id; + let (mut had_floated_children, mut had_children_with_clearance) = (false, false); + for (child_index, kid) in self.base.child_iter_mut().enumerate() { + if kid + .base() + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + // Assume that the *hypothetical box* for an absolute flow starts immediately + // after the margin-end border edge of the previous flow. + if kid + .base() + .flags + .contains(FlowFlags::BLOCK_POSITION_IS_STATIC) + { + let previous_bottom_margin = margin_collapse_info.current_float_ceiling(); + + kid.mut_base().position.start.b = cur_b + + kid.base() + .collapsible_margins + .block_start_margin_for_noncollapsible_context() + + previous_bottom_margin + } + kid.place_float_if_applicable(); + if !kid.base().flags.is_float() { + kid.assign_block_size_for_inorder_child_if_necessary( + layout_context, + thread_id, + content_box, + ); + } + + // Skip the collapsing and float processing for absolute flow kids and continue + // with the next flow. + continue; + } + + let previous_b = cur_b; + if let Some(ctx) = fragmentation_context { + let child_ctx = FragmentationContext { + available_block_size: ctx.available_block_size - cur_b, + this_fragment_is_empty: ctx.this_fragment_is_empty, + }; + if let Some(remaining) = kid.fragment(layout_context, Some(child_ctx)) { + break_at = Some((child_index + 1, Some(remaining))); + } + } + + // Assign block-size now for the child if it might have floats in and we couldn't + // before. + kid.mut_base().floats = floats.clone(); + if kid.base().flags.is_float() { + had_floated_children = true; + kid.mut_base().position.start.b = cur_b; + { + let kid_block = kid.as_mut_block(); + let float_ceiling = margin_collapse_info.current_float_ceiling(); + kid_block.float.as_mut().unwrap().float_ceiling = float_ceiling + } + kid.place_float_if_applicable(); + + let kid_base = kid.mut_base(); + floats = kid_base.floats.clone(); + continue; + } + + // If we have clearance, assume there are no floats in. + // + // FIXME(#2008, pcwalton): This could be wrong if we have `clear: left` or `clear: + // right` and there are still floats to impact, of course. But this gets + // complicated with margin collapse. Possibly the right thing to do is to lay out + // the block again in this rare case. (Note that WebKit can lay blocks out twice; + // this may be related, although I haven't looked into it closely.) + if kid.base().flags.clears_floats() { + kid.mut_base().floats = Floats::new(self.fragment.style.writing_mode) + } + + // Lay the child out if this was an in-order traversal. + let need_to_process_child_floats = kid + .assign_block_size_for_inorder_child_if_necessary( + layout_context, + thread_id, + content_box, + ); + + if !had_children_with_clearance && + floats.is_present() && + (kid.base().flags.contains(FlowFlags::CLEARS_LEFT) || + kid.base().flags.contains(FlowFlags::CLEARS_RIGHT)) + { + had_children_with_clearance = true + } + + // Handle any (possibly collapsed) top margin. + let delta = margin_collapse_info.advance_block_start_margin( + &kid.base().collapsible_margins, + !had_children_with_clearance, + ); + translate_including_floats(&mut cur_b, delta, &mut floats); + + // Collapse-through margins should be placed at the top edge, + // so we'll handle the delta after the bottom margin is processed + if let CollapsibleMargins::CollapseThrough(_) = kid.base().collapsible_margins { + cur_b = cur_b - delta; + } + + // Clear past the floats that came in, if necessary. + let clearance = match ( + kid.base().flags.contains(FlowFlags::CLEARS_LEFT), + kid.base().flags.contains(FlowFlags::CLEARS_RIGHT), + ) { + (false, false) => Au(0), + (true, false) => floats.clearance(ClearType::Left), + (false, true) => floats.clearance(ClearType::Right), + (true, true) => floats.clearance(ClearType::Both), + }; + translate_including_floats(&mut cur_b, clearance, &mut floats); + + // At this point, `cur_b` is at the border edge of the child. + kid.mut_base().position.start.b = cur_b; + + // Now pull out the child's outgoing floats. We didn't do this immediately after + // the `assign_block_size_for_inorder_child_if_necessary` call because clearance on + // a block operates on the floats that come *in*, not the floats that go *out*. + if need_to_process_child_floats { + floats = kid.mut_base().floats.clone() + } + + // Move past the child's border box. Do not use the `translate_including_floats` + // function here because the child has already translated floats past its border + // box. + let kid_base = kid.mut_base(); + cur_b = cur_b + kid_base.position.size.block; + + // Handle any (possibly collapsed) block-end margin. + let delta = + margin_collapse_info.advance_block_end_margin(&kid_base.collapsible_margins); + translate_including_floats(&mut cur_b, delta, &mut floats); + + // Collapse-through margin should be placed at the top edge of the flow. + let collapse_delta = match kid_base.collapsible_margins { + CollapsibleMargins::CollapseThrough(_) => { + let delta = margin_collapse_info.current_float_ceiling(); + cur_b = cur_b + delta; + kid_base.position.start.b = kid_base.position.start.b + delta; + delta + }, + _ => Au(0), + }; + + if break_at.is_some() { + break; + } + + if let Some(ref mut ctx) = fragmentation_context { + if cur_b > ctx.available_block_size && !ctx.this_fragment_is_empty { + break_at = Some((child_index, None)); + cur_b = previous_b; + break; + } + ctx.this_fragment_is_empty = false + } + + // For consecutive collapse-through flows, their top margin should be calculated + // from the same baseline. + cur_b = cur_b - collapse_delta; + } + + // Add in our block-end margin and compute our collapsible margins. + let can_collapse_block_end_margin_with_kids = margins_may_collapse == + MarginsMayCollapseFlag::MarginsMayCollapse && + !self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) && + self.fragment.border_padding.block_end == Au(0); + let (collapsible_margins, delta) = margin_collapse_info + .finish_and_compute_collapsible_margins( + &self.fragment, + self.base.block_container_explicit_block_size, + can_collapse_block_end_margin_with_kids, + !had_floated_children, + ); + self.base.collapsible_margins = collapsible_margins; + translate_including_floats(&mut cur_b, delta, &mut floats); + + let mut block_size = cur_b - block_start_offset; + let is_root = self.is_root(); + + if is_root || + self.formatting_context_type() != FormattingContextType::None || + self.base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + // The content block-size includes all the floats per CSS 2.1 § 10.6.7. The easiest + // way to handle this is to just treat it as clearance. + block_size = block_size + floats.clearance(ClearType::Both); + } + + if self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + // FIXME(#2003, pcwalton): The max is taken here so that you can scroll the page, + // but this is not correct behavior according to CSS 2.1 § 10.5. Instead I think we + // should treat the root element as having `overflow: scroll` and use the layers- + // based scrolling infrastructure to make it scrollable. + if is_root { + let viewport_size = LogicalSize::from_physical( + self.fragment.style.writing_mode, + layout_context.shared_context().viewport_size(), + ); + block_size = max(viewport_size.block, block_size) + } + + // Store the content block-size for use in calculating the absolute flow's + // dimensions later. + // + // FIXME(pcwalton): This looks not idempotent. Is it? + self.fragment.border_box.size.block = block_size; + } + + if self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + self.propagate_early_absolute_position_info_to_children(); + return None; + } + + // Compute any explicitly-specified block size. + // Can't use `for` because we assign to `candidate_block_size_iterator.candidate_value`. + let mut candidate_block_size_iterator = CandidateBSizeIterator::new( + &self.fragment, + self.base.block_container_explicit_block_size, + ); + while let Some(candidate_block_size) = candidate_block_size_iterator.next() { + candidate_block_size_iterator.candidate_value = match candidate_block_size { + MaybeAuto::Auto => block_size, + MaybeAuto::Specified(value) => value, + } + } + + // Adjust `cur_b` as necessary to account for the explicitly-specified block-size. + block_size = candidate_block_size_iterator.candidate_value; + let delta = block_size - (cur_b - block_start_offset); + translate_including_floats(&mut cur_b, delta, &mut floats); + + // Take border and padding into account. + let block_end_offset = self.fragment.border_padding.block_end; + translate_including_floats(&mut cur_b, block_end_offset, &mut floats); + + // Now that `cur_b` is at the block-end of the border box, compute the final border box + // position. + self.fragment.border_box.size.block = cur_b; + self.fragment.border_box.start.b = Au(0); + self.base.position.size.block = cur_b; + + self.propagate_early_absolute_position_info_to_children(); + + // Translate the current set of floats back into the parent coordinate system in the + // inline direction, and store them in the flow so that flows that come later in the + // document can access them. + floats.translate(LogicalSize::new( + writing_mode, + self.fragment.inline_start_offset(), + Au(0), + )); + self.base.floats = floats; + self.adjust_fragments_for_collapsed_margins_if_root(layout_context.shared_context()); + } else { + // We don't need to reflow, but we still need to perform in-order traversals if + // necessary. + let thread_id = self.base.thread_id; + for kid in self.base.child_iter_mut() { + kid.assign_block_size_for_inorder_child_if_necessary( + layout_context, + thread_id, + content_box, + ); + } + } + + if (&*self as &dyn Flow).contains_roots_of_absolute_flow_tree() { + // Assign block-sizes for all flows in this absolute flow tree. + // This is preorder because the block-size of an absolute flow may depend on + // the block-size of its containing block, which may also be an absolute flow. + let assign_abs_b_sizes = AbsoluteAssignBSizesTraversal(layout_context.shared_context()); + assign_abs_b_sizes.traverse_absolute_flows(&mut *self); + } + + // Don't remove the dirty bits yet if we're absolutely-positioned, since our final size + // has not been calculated yet. (See `calculate_absolute_block_size_and_margins` for that.) + // Also don't remove the dirty bits if we're a block formatting context since our inline + // size has not yet been computed. (See `assign_inline_position_for_formatting_context()`.) + if (self.base.flags.is_float() || + self.formatting_context_type() == FormattingContextType::None) && + !self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + self.base + .restyle_damage + .remove(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + self.fragment + .restyle_damage + .remove(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + } + + break_at.and_then(|(i, child_remaining)| { + if i == self.base.children.len() && child_remaining.is_none() { + None + } else { + let mut children = self.base.children.split_off(i); + if let Some(child) = child_remaining { + children.push_front_arc(child); + } + Some(Arc::new(self.clone_with_children(children)) as Arc<dyn Flow>) + } + }) + } + + /// Add placement information about current float flow for use by the parent. + /// + /// Also, use information given by parent about other floats to find out our relative position. + /// + /// This does not give any information about any float descendants because they do not affect + /// elements outside of the subtree rooted at this float. + /// + /// This function is called on a kid flow by a parent. Therefore, `assign_block_size_float` was + /// already called on this kid flow by the traversal function. So, the values used are + /// well-defined. + pub fn place_float(&mut self) { + let block_size = self.fragment.border_box.size.block; + let clearance = match self.fragment.clear() { + None => Au(0), + Some(clear) => self.base.floats.clearance(clear), + }; + + let float_info: FloatedBlockInfo = (**self.float.as_ref().unwrap()).clone(); + + // Our `position` field accounts for positive margins, but not negative margins. (See + // calculation of `extra_inline_size_from_margin` below.) Negative margins must be taken + // into account for float placement, however. So we add them in here. + let inline_size_for_float_placement = + self.base.position.size.inline + min(Au(0), self.fragment.margin.inline_start_end()); + + let info = PlacementInfo { + size: LogicalSize::new( + self.fragment.style.writing_mode, + inline_size_for_float_placement, + block_size + self.fragment.margin.block_start_end(), + ) + .convert( + self.fragment.style.writing_mode, + self.base.floats.writing_mode, + ), + ceiling: clearance + float_info.float_ceiling, + max_inline_size: float_info.containing_inline_size, + kind: float_info.float_kind, + }; + + // Place the float and return the `Floats` back to the parent flow. + // After, grab the position and use that to set our position. + self.base.floats.add_float(&info); + + // FIXME (mbrubeck) Get the correct container size for self.base.floats; + let container_size = Size2D::new(self.base.block_container_inline_size, Au(0)); + + // Move in from the margin edge, as per CSS 2.1 § 9.5, floats may not overlap anything on + // their margin edges. + let float_offset = self + .base + .floats + .last_float_pos() + .unwrap() + .convert( + self.base.floats.writing_mode, + self.base.writing_mode, + container_size, + ) + .start; + let margin_offset = LogicalPoint::new( + self.base.writing_mode, + Au(0), + self.fragment.margin.block_start, + ); + + let mut origin = LogicalPoint::new( + self.base.writing_mode, + self.base.position.start.i, + self.base.position.start.b, + ); + origin = origin.add_point(&float_offset).add_point(&margin_offset); + self.base.position = + LogicalRect::from_point_size(self.base.writing_mode, origin, self.base.position.size); + } + + pub fn explicit_block_containing_size( + &self, + shared_context: &SharedStyleContext, + ) -> Option<Au> { + if self.is_root() || self.is_fixed() { + let viewport_size = LogicalSize::from_physical( + self.fragment.style.writing_mode, + shared_context.viewport_size(), + ); + Some(viewport_size.block) + } else if self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) && + self.base.block_container_explicit_block_size.is_none() + { + self.base + .absolute_cb + .explicit_block_containing_size(shared_context) + } else { + self.base.block_container_explicit_block_size + } + } + + pub fn explicit_block_size(&self, containing_block_size: Option<Au>) -> Option<Au> { + let content_block_size = self.fragment.style().content_block_size(); + + match content_block_size { + Size::Auto => { + let container_size = containing_block_size?; + let (block_start, block_end) = { + let position = self.fragment.style().logical_position(); + ( + MaybeAuto::from_style(position.block_start, container_size), + MaybeAuto::from_style(position.block_end, container_size), + ) + }; + + match (block_start, block_end) { + (MaybeAuto::Specified(block_start), MaybeAuto::Specified(block_end)) => { + let available_block_size = + container_size - self.fragment.border_padding.block_start_end(); + + // Non-auto margin-block-start and margin-block-end values have already been + // calculated during assign-inline-size. + let margin = self.fragment.style().logical_margin(); + let margin_block_start = match margin.block_start { + LengthPercentageOrAuto::Auto => MaybeAuto::Auto, + _ => MaybeAuto::Specified(self.fragment.margin.block_start), + }; + let margin_block_end = match margin.block_end { + LengthPercentageOrAuto::Auto => MaybeAuto::Auto, + _ => MaybeAuto::Specified(self.fragment.margin.block_end), + }; + + let margin_block_start = margin_block_start.specified_or_zero(); + let margin_block_end = margin_block_end.specified_or_zero(); + let sum = block_start + block_end + margin_block_start + margin_block_end; + Some(available_block_size - sum) + }, + (_, _) => None, + } + }, + Size::LengthPercentage(ref lp) => lp.maybe_to_used_value(containing_block_size), + } + } + + fn calculate_absolute_block_size_and_margins(&mut self, shared_context: &SharedStyleContext) { + let opaque_self = OpaqueFlow::from_flow(self); + let containing_block_block_size = self + .containing_block_size(&shared_context.viewport_size(), opaque_self) + .block; + + // This is the stored content block-size value from assign-block-size + let content_block_size = self.fragment.border_box.size.block; + + let mut solution = None; + { + // Non-auto margin-block-start and margin-block-end values have already been + // calculated during assign-inline-size. + let margin = self.fragment.style().logical_margin(); + let margin_block_start = match margin.block_start { + LengthPercentageOrAuto::Auto => MaybeAuto::Auto, + _ => MaybeAuto::Specified(self.fragment.margin.block_start), + }; + let margin_block_end = match margin.block_end { + LengthPercentageOrAuto::Auto => MaybeAuto::Auto, + _ => MaybeAuto::Specified(self.fragment.margin.block_end), + }; + + let block_start; + let block_end; + { + let position = self.fragment.style().logical_position(); + block_start = + MaybeAuto::from_style(position.block_start, containing_block_block_size); + block_end = MaybeAuto::from_style(position.block_end, containing_block_block_size); + } + + let available_block_size = + containing_block_block_size - self.fragment.border_padding.block_start_end(); + if self.fragment.is_replaced() { + // Calculate used value of block-size just like we do for inline replaced elements. + // TODO: Pass in the containing block block-size when Fragment's + // assign-block-size can handle it correctly. + self.fragment.assign_replaced_block_size_if_necessary(); + // TODO: Right now, this content block-size value includes the + // margin because of erroneous block-size calculation in fragment. + // Check this when that has been fixed. + let block_size_used_val = self.fragment.border_box.size.block - + self.fragment.border_padding.block_start_end(); + solution = Some( + BSizeConstraintSolution::solve_vertical_constraints_abs_replaced( + block_size_used_val, + margin_block_start, + margin_block_end, + block_start, + block_end, + content_block_size, + available_block_size, + ), + ) + } else { + let mut candidate_block_size_iterator = + CandidateBSizeIterator::new(&self.fragment, Some(containing_block_block_size)); + + // Can't use `for` because we assign to + // `candidate_block_size_iterator.candidate_value`. + while let Some(block_size_used_val) = candidate_block_size_iterator.next() { + solution = Some( + BSizeConstraintSolution::solve_vertical_constraints_abs_nonreplaced( + block_size_used_val, + margin_block_start, + margin_block_end, + block_start, + block_end, + content_block_size, + available_block_size, + ), + ); + + candidate_block_size_iterator.candidate_value = solution.unwrap().block_size; + } + } + } + + let solution = solution.unwrap(); + self.fragment.margin.block_start = solution.margin_block_start; + self.fragment.margin.block_end = solution.margin_block_end; + self.fragment.border_box.start.b = Au(0); + + if !self + .base + .flags + .contains(FlowFlags::BLOCK_POSITION_IS_STATIC) + { + self.base.position.start.b = solution.block_start + self.fragment.margin.block_start + } + + let block_size = solution.block_size + self.fragment.border_padding.block_start_end(); + + self.fragment.border_box.size.block = block_size; + self.base.position.size.block = block_size; + + self.base + .restyle_damage + .remove(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + self.fragment + .restyle_damage + .remove(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + } + + /// Compute inline size based using the `block_container_inline_size` set by the parent flow. + /// + /// This is run in the `AssignISizes` traversal. + fn propagate_and_compute_used_inline_size(&mut self, shared_context: &SharedStyleContext) { + let containing_block_inline_size = self.base.block_container_inline_size; + self.compute_used_inline_size(shared_context, containing_block_inline_size); + if self.base.flags.is_float() { + self.float.as_mut().unwrap().containing_inline_size = containing_block_inline_size + } + } + + /// Assigns the computed inline-start content edge and inline-size to all the children of this + /// block flow. The given `callback`, if supplied, will be called once per child; it is + /// currently used to push down column sizes for tables. + /// + /// `#[inline(always)]` because this is called only from block or table inline-size assignment + /// and the code for block layout is significantly simpler. + #[inline(always)] + pub fn propagate_assigned_inline_size_to_children<F>( + &mut self, + shared_context: &SharedStyleContext, + inline_start_content_edge: Au, + inline_end_content_edge: Au, + content_inline_size: Au, + mut callback: F, + ) where + F: FnMut(&mut dyn Flow, usize, Au, WritingMode, &mut Au, &mut Au), + { + let flags = self.base.flags.clone(); + + let opaque_self = OpaqueFlow::from_flow(self); + + // Calculate non-auto block size to pass to children. + let box_border = match self.fragment.style().get_position().box_sizing { + BoxSizing::BorderBox => self.fragment.border_padding.block_start_end(), + BoxSizing::ContentBox => Au(0), + }; + let parent_container_size = self.explicit_block_containing_size(shared_context); + // https://drafts.csswg.org/css-ui-3/#box-sizing + let mut explicit_content_size = self.explicit_block_size(parent_container_size).map(|x| { + if x < box_border { + Au(0) + } else { + x - box_border + } + }); + if self.is_root() { + explicit_content_size = max(parent_container_size, explicit_content_size); + } + // Calculate containing block inline size. + let containing_block_size = if flags.contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) { + self.containing_block_size(&shared_context.viewport_size(), opaque_self) + .inline + } else { + content_inline_size + }; + // FIXME (mbrubeck): Get correct mode for absolute containing block + let containing_block_mode = self.base.writing_mode; + + let mut inline_start_margin_edge = inline_start_content_edge; + let mut inline_end_margin_edge = inline_end_content_edge; + + let mut iterator = self.base.child_iter_mut().enumerate().peekable(); + while let Some((i, kid)) = iterator.next() { + kid.mut_base().block_container_explicit_block_size = explicit_content_size; + + // The inline-start margin edge of the child flow is at our inline-start content edge, + // and its inline-size is our content inline-size. + let kid_mode = kid.base().writing_mode; + { + // Don't assign positions to children unless they're going to be reflowed. + // Otherwise, the position we assign might be incorrect and never fixed up. (Issue + // #13704.) + // + // For instance, floats have their true inline position calculated in + // `assign_block_size()`, which won't do anything unless `REFLOW` is set. So, if a + // float child does not have `REFLOW` set, we must be careful to avoid touching its + // inline position, as no logic will run afterward to set its true value. + let kid_base = kid.mut_base(); + let reflow_damage = if kid_base.flags.contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + ServoRestyleDamage::REFLOW_OUT_OF_FLOW + } else { + ServoRestyleDamage::REFLOW + }; + if kid_base + .flags + .contains(FlowFlags::INLINE_POSITION_IS_STATIC) && + kid_base.restyle_damage.contains(reflow_damage) + { + kid_base.position.start.i = + if kid_mode.is_bidi_ltr() == containing_block_mode.is_bidi_ltr() { + inline_start_content_edge + } else { + // The kid's inline 'start' is at the parent's 'end' + inline_end_content_edge + }; + } + kid_base.block_container_inline_size = content_inline_size; + kid_base.block_container_writing_mode = containing_block_mode; + } + + // Call the callback to propagate extra inline size information down to the child. This + // is currently used for tables. + callback( + kid, + i, + content_inline_size, + containing_block_mode, + &mut inline_start_margin_edge, + &mut inline_end_margin_edge, + ); + + // Per CSS 2.1 § 16.3.1, text alignment propagates to all children in flow. + // + // TODO(#2265, pcwalton): Do this in the cascade instead. + let containing_block_text_align = self.fragment.style().get_inherited_text().text_align; + kid.mut_base() + .flags + .set_text_align(containing_block_text_align); + + // Handle `text-indent` on behalf of any inline children that we have. This is + // necessary because any percentages are relative to the containing block, which only + // we know. + if kid.is_inline_flow() { + kid.as_mut_inline().first_line_indentation = self + .fragment + .style() + .get_inherited_text() + .text_indent + .to_used_value(containing_block_size); + } + } + } + + /// Determines the type of formatting context this is. See the definition of + /// `FormattingContextType`. + pub fn formatting_context_type(&self) -> FormattingContextType { + if self.is_inline_flex_item() || self.is_block_flex_item() { + return FormattingContextType::Other; + } + let style = self.fragment.style(); + if style.get_box().float != Float::None { + return FormattingContextType::Other; + } + match style.get_box().display { + Display::TableCell | + Display::TableCaption | + Display::TableRowGroup | + Display::Table | + Display::InlineBlock | + Display::Flex => FormattingContextType::Other, + _ if style.get_box().overflow_x != StyleOverflow::Visible || + style.get_box().overflow_y != StyleOverflow::Visible || + style.is_multicol() => + { + FormattingContextType::Block + }, + _ => FormattingContextType::None, + } + } + + /// Per CSS 2.1 § 9.5, block formatting contexts' inline widths and positions are affected by + /// the presence of floats. This is the part of the assign-heights traversal that computes + /// the final inline position and width for such flows. + /// + /// Note that this is part of the assign-block-sizes traversal, not the assign-inline-sizes + /// traversal as one might expect. That is because, in general, float placement cannot occur + /// until heights are assigned. To work around this unfortunate circular dependency, by the + /// time we get here we have already estimated the width of the block formatting context based + /// on the floats we could see at the time of inline-size assignment. The job of this function, + /// therefore, is not only to assign the final size but also to perform the layout again for + /// this block formatting context if our speculation was wrong. + fn assign_inline_position_for_formatting_context( + &mut self, + layout_context: &LayoutContext, + content_box: LogicalRect<Au>, + ) { + debug_assert_ne!(self.formatting_context_type(), FormattingContextType::None); + + if !self + .base + .restyle_damage + .intersects(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW) + { + return; + } + + // We do this first to avoid recomputing our inline size when we propagate it. + self.base + .restyle_damage + .remove(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + self.fragment + .restyle_damage + .remove(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + + // The code below would completely wreck the layout if run on a flex item, however: + // * Flex items are always the children of flex containers. + // * Flex containers only contain flex items. + // * Floats cannot intrude into flex containers. + // * Floats cannot escape flex items. + // * Flex items cannot also be floats. + // Therefore, a flex item cannot be impacted by a float. + // See also: https://www.w3.org/TR/css-flexbox-1/#flex-containers + if !self.base.might_have_floats_in() { + return; + } + + // If you remove the might_have_floats_in conditional, this will go off. + debug_assert!(!self.is_inline_flex_item()); + + // Compute the available space for us, based on the actual floats. + let rect = self.base.floats.available_rect( + Au(0), + self.fragment.border_box.size.block, + content_box.size.inline, + ); + let available_inline_size = if let Some(rect) = rect { + // Offset our position by whatever displacement is needed to not impact the floats. + // Also, account for margins sliding behind floats. + let inline_offset = if self.fragment.margin.inline_start < rect.start.i { + // Do not do anything for negative margins; those are handled separately. + rect.start.i - max(Au(0), self.fragment.margin.inline_start) + } else { + Au(0) + }; + self.base.position.start.i = content_box.start.i + inline_offset; + // Handle the end margin sliding behind the float. + let end = content_box.size.inline - rect.start.i - rect.size.inline; + let inline_end_offset = if self.fragment.margin.inline_end < end { + end - max(Au(0), self.fragment.margin.inline_end) + } else { + Au(0) + }; + content_box.size.inline - inline_offset - inline_end_offset + } else { + content_box.size.inline + } - self.fragment.margin.inline_start_end(); + let max_inline_size = self + .fragment + .style() + .max_inline_size() + .to_used_value(self.base.block_container_inline_size) + .unwrap_or(MAX_AU); + let min_inline_size = self + .fragment + .style() + .min_inline_size() + .to_used_value(self.base.block_container_inline_size) + .unwrap_or(Au(0)); + let specified_inline_size = self.fragment.style().content_inline_size(); + let container_size = self.base.block_container_inline_size; + let inline_size = match specified_inline_size.to_used_value(container_size) { + Some(size) => match self.fragment.style().get_position().box_sizing { + BoxSizing::BorderBox => size, + BoxSizing::ContentBox => size + self.fragment.border_padding.inline_start_end(), + }, + None => max(min_inline_size, min(available_inline_size, max_inline_size)), + }; + self.base.position.size.inline = inline_size + self.fragment.margin.inline_start_end(); + + // If float speculation failed, fixup our layout, and re-layout all the children. + if self.fragment.margin_box_inline_size() != self.base.position.size.inline { + debug!("assign_inline_position_for_formatting_context: float speculation failed"); + // Fix-up our own layout. + // We can't just traverse_flow_tree_preorder ourself, because that would re-run + // float speculation, instead of acting on the actual results. + self.fragment.border_box.size.inline = inline_size; + // Assign final-final inline sizes on all our children. + self.assign_inline_sizes(layout_context); + // Re-run layout on our children. + for child in self.base.child_iter_mut() { + sequential::reflow(child, layout_context, RelayoutMode::Force); + } + // Assign our final-final block size. + self.assign_block_size(layout_context); + } + + debug_assert_eq!( + self.fragment.margin_box_inline_size(), + self.base.position.size.inline + ); + } + + fn is_inline_block_or_inline_flex(&self) -> bool { + self.fragment.style().get_box().display == Display::InlineBlock || + self.fragment.style().get_box().display == Display::InlineFlex + } + + /// Computes the content portion (only) of the intrinsic inline sizes of this flow. This is + /// used for calculating shrink-to-fit width. Assumes that intrinsic sizes have already been + /// computed for this flow. + fn content_intrinsic_inline_sizes(&self) -> IntrinsicISizes { + let (border_padding, margin) = self.fragment.surrounding_intrinsic_inline_size(); + IntrinsicISizes { + minimum_inline_size: self.base.intrinsic_inline_sizes.minimum_inline_size - + border_padding - + margin, + preferred_inline_size: self.base.intrinsic_inline_sizes.preferred_inline_size - + border_padding - + margin, + } + } + + /// Computes intrinsic inline sizes for a block. + pub fn bubble_inline_sizes_for_block(&mut self, consult_children: bool) { + let _scope = layout_debug_scope!("block::bubble_inline_sizes {:x}", self.base.debug_id()); + + let mut flags = self.base.flags; + if self.definitely_has_zero_block_size() { + // This is kind of a hack for Acid2. But it's a harmless one, because (a) this behavior + // is unspecified; (b) it matches the behavior one would intuitively expect, since + // floats don't flow around blocks that take up no space in the block direction. + flags.remove(FlowFlags::CONTAINS_TEXT_OR_REPLACED_FRAGMENTS); + } else if self.fragment.is_text_or_replaced() { + flags.insert(FlowFlags::CONTAINS_TEXT_OR_REPLACED_FRAGMENTS); + } else { + flags.remove(FlowFlags::CONTAINS_TEXT_OR_REPLACED_FRAGMENTS); + for kid in self.base.children.iter() { + if kid + .base() + .flags + .contains(FlowFlags::CONTAINS_TEXT_OR_REPLACED_FRAGMENTS) + { + flags.insert(FlowFlags::CONTAINS_TEXT_OR_REPLACED_FRAGMENTS); + break; + } + } + } + + // Find the maximum inline-size from children. + // + // See: https://lists.w3.org/Archives/Public/www-style/2014Nov/0085.html + // + // FIXME(pcwalton): This doesn't exactly follow that algorithm at the moment. + // FIXME(pcwalton): This should consider all float descendants, not just children. + let mut computation = self.fragment.compute_intrinsic_inline_sizes(); + let (mut left_float_width, mut right_float_width) = (Au(0), Au(0)); + let (mut left_float_width_accumulator, mut right_float_width_accumulator) = (Au(0), Au(0)); + let mut preferred_inline_size_of_children_without_text_or_replaced_fragments = Au(0); + for kid in self.base.child_iter_mut() { + if kid + .base() + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) || + !consult_children + { + continue; + } + + let child_base = kid.mut_base(); + let float_kind = child_base.flags.float_kind(); + computation.content_intrinsic_sizes.minimum_inline_size = max( + computation.content_intrinsic_sizes.minimum_inline_size, + child_base.intrinsic_inline_sizes.minimum_inline_size, + ); + + if child_base.flags.contains(FlowFlags::CLEARS_LEFT) { + left_float_width = max(left_float_width, left_float_width_accumulator); + left_float_width_accumulator = Au(0) + } + if child_base.flags.contains(FlowFlags::CLEARS_RIGHT) { + right_float_width = max(right_float_width, right_float_width_accumulator); + right_float_width_accumulator = Au(0) + } + + match ( + float_kind, + child_base + .flags + .contains(FlowFlags::CONTAINS_TEXT_OR_REPLACED_FRAGMENTS), + ) { + (Float::None, true) => { + computation.content_intrinsic_sizes.preferred_inline_size = max( + computation.content_intrinsic_sizes.preferred_inline_size, + child_base.intrinsic_inline_sizes.preferred_inline_size, + ); + }, + (Float::None, false) => { + preferred_inline_size_of_children_without_text_or_replaced_fragments = max( + preferred_inline_size_of_children_without_text_or_replaced_fragments, + child_base.intrinsic_inline_sizes.preferred_inline_size, + ) + }, + (Float::Left, _) => { + left_float_width_accumulator = left_float_width_accumulator + + child_base.intrinsic_inline_sizes.preferred_inline_size; + }, + (Float::Right, _) => { + right_float_width_accumulator = right_float_width_accumulator + + child_base.intrinsic_inline_sizes.preferred_inline_size; + }, + } + } + + left_float_width = max(left_float_width, left_float_width_accumulator); + right_float_width = max(right_float_width, right_float_width_accumulator); + + computation.content_intrinsic_sizes.preferred_inline_size = + computation.content_intrinsic_sizes.preferred_inline_size + + left_float_width + + right_float_width; + computation.content_intrinsic_sizes.preferred_inline_size = max( + computation.content_intrinsic_sizes.preferred_inline_size, + preferred_inline_size_of_children_without_text_or_replaced_fragments, + ); + + self.base.intrinsic_inline_sizes = computation.finish(); + self.base.flags = flags + } + + pub fn overflow_style_may_require_clip_scroll_node(&self) -> bool { + match ( + self.fragment.style().get_box().overflow_x, + self.fragment.style().get_box().overflow_y, + ) { + (StyleOverflow::Auto, _) | + (StyleOverflow::Scroll, _) | + (StyleOverflow::Hidden, _) | + (_, StyleOverflow::Auto) | + (_, StyleOverflow::Scroll) | + (_, StyleOverflow::Hidden) => true, + (_, _) => false, + } + } + + pub fn compute_inline_sizes(&mut self, shared_context: &SharedStyleContext) { + if !self + .base + .restyle_damage + .intersects(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW) + { + return; + } + + debug!( + "assign_inline_sizes({}): assigning inline_size for flow", + if self.base.flags.is_float() { + "float" + } else { + "block" + } + ); + + self.base.floats = Floats::new(self.base.writing_mode); + + self.initialize_container_size_for_root(shared_context); + + // Our inline-size was set to the inline-size of the containing block by the flow's parent. + // Now compute the real value. + self.propagate_and_compute_used_inline_size(shared_context); + + self.guess_inline_size_for_block_formatting_context_if_necessary() + } + + /// If this is the root flow, initialize values that would normally be set by the parent. + /// + /// Should be called during `assign_inline_sizes` for flows that may be the root. + pub fn initialize_container_size_for_root(&mut self, shared_context: &SharedStyleContext) { + if self.is_root() { + debug!("Setting root position"); + self.base.position.start = LogicalPoint::zero(self.base.writing_mode); + self.base.block_container_inline_size = + LogicalSize::from_physical(self.base.writing_mode, shared_context.viewport_size()) + .inline; + self.base.block_container_writing_mode = self.base.writing_mode; + } + } + + fn guess_inline_size_for_block_formatting_context_if_necessary(&mut self) { + // We don't need to guess anything unless this is a block formatting context. + if self.formatting_context_type() != FormattingContextType::Block { + return; + } + + // If `max-width` is set, then don't perform this speculation. We guess that the + // page set `max-width` in order to avoid hitting floats. The search box on Google + // SERPs falls into this category. + if self.fragment.style.max_inline_size() != MaxSize::None { + return; + } + + // At this point, we know we can't precisely compute the inline-size of this block now, + // because floats might affect it. Speculate that its inline-size is equal to the + // inline-size computed above minus the inline-size of the previous left and/or right + // floats. + let speculated_left_float_size = if self.fragment.margin.inline_start >= Au(0) && + self.base.speculated_float_placement_in.left > self.fragment.margin.inline_start + { + self.base.speculated_float_placement_in.left - self.fragment.margin.inline_start + } else { + Au(0) + }; + let speculated_right_float_size = if self.fragment.margin.inline_end >= Au(0) && + self.base.speculated_float_placement_in.right > self.fragment.margin.inline_end + { + self.base.speculated_float_placement_in.right - self.fragment.margin.inline_end + } else { + Au(0) + }; + self.fragment.border_box.size.inline = self.fragment.border_box.size.inline - + speculated_left_float_size - + speculated_right_float_size + } + + fn definitely_has_zero_block_size(&self) -> bool { + if !self + .fragment + .style + .content_block_size() + .is_definitely_zero() + { + return false; + } + let border_width = self.fragment.border_width(); + if border_width.block_start != Au(0) || border_width.block_end != Au(0) { + return false; + } + let padding = self.fragment.style.logical_padding(); + padding.block_start.is_definitely_zero() && padding.block_end.is_definitely_zero() + } + + pub fn is_inline_flex_item(&self) -> bool { + self.fragment + .flags + .contains(FragmentFlags::IS_INLINE_FLEX_ITEM) + } + + pub fn is_block_flex_item(&self) -> bool { + self.fragment + .flags + .contains(FragmentFlags::IS_BLOCK_FLEX_ITEM) + } + + pub fn mark_scrolling_overflow(&mut self, has_scrolling_overflow: bool) { + if has_scrolling_overflow { + self.flags.insert(BlockFlowFlags::HAS_SCROLLING_OVERFLOW); + } else { + self.flags.remove(BlockFlowFlags::HAS_SCROLLING_OVERFLOW); + } + } + + pub fn has_scrolling_overflow(&self) -> bool { + self.flags.contains(BlockFlowFlags::HAS_SCROLLING_OVERFLOW) + } + + // Return offset from original position because of `position: sticky`. + pub fn sticky_position(&self) -> SideOffsets2D<MaybeAuto> { + let containing_block_size = &self + .base + .early_absolute_position_info + .relative_containing_block_size; + let writing_mode = self + .base + .early_absolute_position_info + .relative_containing_block_mode; + let offsets = self.fragment.style().logical_position(); + let as_margins = LogicalMargin::new( + writing_mode, + MaybeAuto::from_style(offsets.block_start, containing_block_size.inline), + MaybeAuto::from_style(offsets.inline_end, containing_block_size.inline), + MaybeAuto::from_style(offsets.block_end, containing_block_size.inline), + MaybeAuto::from_style(offsets.inline_start, containing_block_size.inline), + ); + as_margins.to_physical(writing_mode) + } + + pub fn background_border_section(&self) -> DisplayListSection { + if self.base.flags.is_float() { + DisplayListSection::BackgroundAndBorders + } else if self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + if self.fragment.establishes_stacking_context() { + DisplayListSection::BackgroundAndBorders + } else { + DisplayListSection::BlockBackgroundsAndBorders + } + } else { + DisplayListSection::BlockBackgroundsAndBorders + } + } +} + +impl Flow for BlockFlow { + fn class(&self) -> FlowClass { + FlowClass::Block + } + + fn as_mut_block(&mut self) -> &mut BlockFlow { + self + } + + fn as_block(&self) -> &BlockFlow { + self + } + + /// Pass 1 of reflow: computes minimum and preferred inline-sizes. + /// + /// Recursively (bottom-up) determine the flow's minimum and preferred inline-sizes. When + /// called on this flow, all child flows have had their minimum and preferred inline-sizes set. + /// This function must decide minimum/preferred inline-sizes based on its children's + /// inline-sizes and the dimensions of any fragments it is responsible for flowing. + fn bubble_inline_sizes(&mut self) { + // If this block has a fixed width, just use that for the minimum and preferred width, + // rather than bubbling up children inline width. + // FIXME(emilio): This should probably be writing-mode-aware. + let consult_children = match self.fragment.style().get_position().width { + Size::Auto => true, + Size::LengthPercentage(ref lp) => lp.maybe_to_used_value(None).is_none(), + }; + self.bubble_inline_sizes_for_block(consult_children); + self.fragment + .restyle_damage + .remove(ServoRestyleDamage::BUBBLE_ISIZES); + } + + /// Recursively (top-down) determines the actual inline-size of child contexts and fragments. + /// When called on this context, the context has had its inline-size set by the parent context. + /// + /// Dual fragments consume some inline-size first, and the remainder is assigned to all child + /// (block) contexts. + fn assign_inline_sizes(&mut self, layout_context: &LayoutContext) { + let _scope = layout_debug_scope!("block::assign_inline_sizes {:x}", self.base.debug_id()); + + let shared_context = layout_context.shared_context(); + self.compute_inline_sizes(shared_context); + + // Move in from the inline-start border edge. + let inline_start_content_edge = + self.fragment.border_box.start.i + self.fragment.border_padding.inline_start; + + let padding_and_borders = self.fragment.border_padding.inline_start_end(); + + // Distance from the inline-end margin edge to the inline-end content edge. + let inline_end_content_edge = + self.fragment.margin.inline_end + self.fragment.border_padding.inline_end; + + let content_inline_size = self.fragment.border_box.size.inline - padding_and_borders; + + self.propagate_assigned_inline_size_to_children( + shared_context, + inline_start_content_edge, + inline_end_content_edge, + content_inline_size, + |_, _, _, _, _, _| {}, + ); + } + + fn place_float_if_applicable<'a>(&mut self) { + if self.base.flags.is_float() { + self.place_float(); + } + } + + fn assign_block_size_for_inorder_child_if_necessary( + &mut self, + layout_context: &LayoutContext, + parent_thread_id: u8, + content_box: LogicalRect<Au>, + ) -> bool { + if self.base.flags.is_float() { + return false; + } + + let is_formatting_context = self.formatting_context_type() != FormattingContextType::None; + if !self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) && + is_formatting_context + { + self.assign_inline_position_for_formatting_context(layout_context, content_box); + } + + if (self as &dyn Flow).floats_might_flow_through() { + self.base.thread_id = parent_thread_id; + if self + .base + .restyle_damage + .intersects(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW) + { + self.assign_block_size(layout_context); + // Don't remove the restyle damage; `assign_block_size` decides whether that is + // appropriate (which in the case of e.g. absolutely-positioned flows, it is not). + } + return true; + } + + if is_formatting_context { + // If this is a formatting context and definitely did not have floats in, then we must + // translate the floats past us. + let writing_mode = self.base.floats.writing_mode; + let delta = self.base.position.size.block; + self.base + .floats + .translate(LogicalSize::new(writing_mode, Au(0), -delta)); + return true; + } + + false + } + + fn assign_block_size(&mut self, ctx: &LayoutContext) { + let remaining = Flow::fragment(self, ctx, None); + debug_assert!(remaining.is_none()); + } + + fn fragment( + &mut self, + layout_context: &LayoutContext, + fragmentation_context: Option<FragmentationContext>, + ) -> Option<Arc<dyn Flow>> { + if self.fragment.is_replaced() { + let _scope = layout_debug_scope!( + "assign_replaced_block_size_if_necessary {:x}", + self.base.debug_id() + ); + + // Assign block-size for fragment if it is an image fragment. + self.fragment.assign_replaced_block_size_if_necessary(); + if !self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + self.base.position.size.block = self.fragment.border_box.size.block; + let mut block_start = + AdjoiningMargins::from_margin(self.fragment.margin.block_start); + let block_end = AdjoiningMargins::from_margin(self.fragment.margin.block_end); + if self.fragment.border_box.size.block == Au(0) { + block_start.union(block_end); + self.base.collapsible_margins = + CollapsibleMargins::CollapseThrough(block_start); + } else { + self.base.collapsible_margins = + CollapsibleMargins::Collapse(block_start, block_end); + } + self.base + .restyle_damage + .remove(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + self.fragment + .restyle_damage + .remove(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + } + None + } else if self.is_root() || + self.formatting_context_type() != FormattingContextType::None || + self.base.flags.contains(FlowFlags::MARGINS_CANNOT_COLLAPSE) + { + // Root element margins should never be collapsed according to CSS § 8.3.1. + debug!( + "assign_block_size: assigning block_size for root flow {:?}", + self.base().debug_id() + ); + self.assign_block_size_block_base( + layout_context, + fragmentation_context, + MarginsMayCollapseFlag::MarginsMayNotCollapse, + ) + } else { + debug!( + "assign_block_size: assigning block_size for block {:?}", + self.base().debug_id() + ); + self.assign_block_size_block_base( + layout_context, + fragmentation_context, + MarginsMayCollapseFlag::MarginsMayCollapse, + ) + } + } + + fn compute_stacking_relative_position(&mut self, _layout_context: &LayoutContext) { + // FIXME (mbrubeck): Get the real container size, taking the container writing mode into + // account. Must handle vertical writing modes. + let container_size = Size2D::new(self.base.block_container_inline_size, Au(0)); + + if self.is_root() { + self.base.clip = Rect::max_rect(); + } + + if self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + let position_start = self + .base + .position + .start + .to_physical(self.base.writing_mode, container_size); + + // Compute our position relative to the nearest ancestor stacking context. This will be + // passed down later as part of containing block details for absolute descendants. + let absolute_stacking_relative_position = if self.is_fixed() { + // The viewport is initially at (0, 0). + position_start + } else { + // Absolute position of the containing block + position of absolute + // flow w.r.t. the containing block. + self.base + .late_absolute_position_info + .stacking_relative_position_of_absolute_containing_block + + position_start.to_vector() + }; + + if !self.base.writing_mode.is_vertical() { + if !self + .base + .flags + .contains(FlowFlags::INLINE_POSITION_IS_STATIC) + { + self.base.stacking_relative_position.x = absolute_stacking_relative_position.x + } + if !self + .base + .flags + .contains(FlowFlags::BLOCK_POSITION_IS_STATIC) + { + self.base.stacking_relative_position.y = absolute_stacking_relative_position.y + } + } else { + if !self + .base + .flags + .contains(FlowFlags::INLINE_POSITION_IS_STATIC) + { + self.base.stacking_relative_position.y = absolute_stacking_relative_position.y + } + if !self + .base + .flags + .contains(FlowFlags::BLOCK_POSITION_IS_STATIC) + { + self.base.stacking_relative_position.x = absolute_stacking_relative_position.x + } + } + } + + // For relatively-positioned descendants, the containing block formed by a block is just + // the content box. The containing block for absolutely-positioned descendants, on the + // other hand, is established in other circumstances (see `is_absolute_containing_block'). + let relative_offset = self.fragment.relative_position( + &self + .base + .early_absolute_position_info + .relative_containing_block_size, + ); + if self.is_absolute_containing_block() { + let border_box_origin = + (self.fragment.border_box - self.fragment.style.logical_border_width()).start; + self.base + .late_absolute_position_info + .stacking_relative_position_of_absolute_containing_block = + self.base.stacking_relative_position.to_point() + + (border_box_origin + relative_offset) + .to_physical(self.base.writing_mode, container_size) + .to_vector() + } + + // Compute absolute position info for children. + let stacking_relative_position_of_absolute_containing_block_for_children = + if self.fragment.establishes_stacking_context() { + let logical_border_width = self.fragment.style().logical_border_width(); + let position = LogicalPoint::new( + self.base.writing_mode, + logical_border_width.inline_start, + logical_border_width.block_start, + ); + let position = position.to_physical(self.base.writing_mode, container_size); + + // Some blocks establish a stacking context, but not a containing block for + // absolutely positioned elements. An example of this might be a block that has + // `position: static` and `opacity` set. In these cases, absolutely-positioned + // children will not be positioned relative to us but will instead be positioned + // relative to our containing block. + if self.is_absolute_containing_block() { + position + } else { + position - self.base.stacking_relative_position + } + } else { + self.base + .late_absolute_position_info + .stacking_relative_position_of_absolute_containing_block + }; + let late_absolute_position_info_for_children = LateAbsolutePositionInfo { + stacking_relative_position_of_absolute_containing_block: + stacking_relative_position_of_absolute_containing_block_for_children, + }; + let container_size_for_children = + self.base.position.size.to_physical(self.base.writing_mode); + + // Compute the origin and clipping rectangle for children. + let relative_offset = relative_offset + .to_physical(self.base.writing_mode) + .to_vector(); + let is_stacking_context = self.fragment.establishes_stacking_context(); + let origin_for_children = if is_stacking_context { + // We establish a stacking context, so the position of our children is vertically + // correct, but has to be adjusted to accommodate horizontal margins. (Note the + // calculation involving `position` below and recall that inline-direction flow + // positions are relative to the edges of the margin box.) + // + // FIXME(pcwalton): Is this vertical-writing-direction-safe? + let margin = self.fragment.margin.to_physical(self.base.writing_mode); + Point2D::new(-margin.left, Au(0)) + } else { + self.base.stacking_relative_position.to_point() + relative_offset + }; + + // Process children. + for kid in self.base.child_iter_mut() { + if kid + .base() + .flags + .contains(FlowFlags::INLINE_POSITION_IS_STATIC) || + kid.base() + .flags + .contains(FlowFlags::BLOCK_POSITION_IS_STATIC) + { + let kid_base = kid.mut_base(); + let physical_position = kid_base + .position + .to_physical(kid_base.writing_mode, container_size_for_children); + + // Set the inline and block positions as necessary. + if !kid_base.writing_mode.is_vertical() { + if kid_base + .flags + .contains(FlowFlags::INLINE_POSITION_IS_STATIC) + { + kid_base.stacking_relative_position.x = + origin_for_children.x + physical_position.origin.x + } + if kid_base.flags.contains(FlowFlags::BLOCK_POSITION_IS_STATIC) { + kid_base.stacking_relative_position.y = + origin_for_children.y + physical_position.origin.y + } + } else { + if kid_base + .flags + .contains(FlowFlags::INLINE_POSITION_IS_STATIC) + { + kid_base.stacking_relative_position.y = + origin_for_children.y + physical_position.origin.y + } + if kid_base.flags.contains(FlowFlags::BLOCK_POSITION_IS_STATIC) { + kid_base.stacking_relative_position.x = + origin_for_children.x + physical_position.origin.x + } + } + } + + kid.mut_base().late_absolute_position_info = late_absolute_position_info_for_children; + } + } + + fn mark_as_root(&mut self) { + self.flags.insert(BlockFlowFlags::IS_ROOT) + } + + fn is_root(&self) -> bool { + self.flags.contains(BlockFlowFlags::IS_ROOT) + } + + /// The 'position' property of this flow. + fn positioning(&self) -> Position { + self.fragment.style.get_box().position + } + + /// Return the dimensions of the containing block generated by this flow for absolutely- + /// positioned descendants. For block flows, this is the padding box. + fn generated_containing_block_size(&self, _: OpaqueFlow) -> LogicalSize<Au> { + (self.fragment.border_box - self.fragment.style().logical_border_width()).size + } + + /// Returns true if this flow contains fragments that are roots of an absolute flow tree. + fn contains_roots_of_absolute_flow_tree(&self) -> bool { + self.contains_relatively_positioned_fragments() || + self.is_root() || + self.fragment.has_filter_transform_or_perspective() + } + + /// Returns true if this is an absolute containing block. + fn is_absolute_containing_block(&self) -> bool { + self.contains_positioned_fragments() || self.fragment.has_filter_transform_or_perspective() + } + + fn update_late_computed_inline_position_if_necessary(&mut self, inline_position: Au) { + if self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) && + self.fragment.style().logical_position().inline_start == LengthPercentageOrAuto::Auto && + self.fragment.style().logical_position().inline_end == LengthPercentageOrAuto::Auto + { + self.base.position.start.i = inline_position + } + } + + fn update_late_computed_block_position_if_necessary(&mut self, block_position: Au) { + if self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) && + self.fragment.style().logical_position().block_start == LengthPercentageOrAuto::Auto && + self.fragment.style().logical_position().block_end == LengthPercentageOrAuto::Auto + { + self.base.position.start.b = block_position + } + } + + fn collect_stacking_contexts(&mut self, state: &mut StackingContextCollectionState) { + self.collect_stacking_contexts_for_block(state, StackingContextCollectionFlags::empty()); + } + + fn build_display_list(&mut self, state: &mut DisplayListBuildState) { + self.build_display_list_for_block(state, BorderPaintingMode::Separate); + } + + fn repair_style(&mut self, new_style: &crate::ServoArc<ComputedValues>) { + self.fragment.repair_style(new_style) + } + + fn compute_overflow(&self) -> Overflow { + let flow_size = self.base.position.size.to_physical(self.base.writing_mode); + let overflow = self.fragment.compute_overflow( + &flow_size, + &self + .base + .early_absolute_position_info + .relative_containing_block_size, + ); + overflow + } + + fn iterate_through_fragment_border_boxes( + &self, + iterator: &mut dyn FragmentBorderBoxIterator, + level: i32, + stacking_context_position: &Point2D<Au>, + ) { + if !iterator.should_process(&self.fragment) { + return; + } + + iterator.process( + &self.fragment, + level, + &self + .fragment + .stacking_relative_border_box( + &self.base.stacking_relative_position, + &self + .base + .early_absolute_position_info + .relative_containing_block_size, + self.base + .early_absolute_position_info + .relative_containing_block_mode, + CoordinateSystem::Own, + ) + .translate(&stacking_context_position.to_vector()), + ); + } + + fn mutate_fragments(&mut self, mutator: &mut dyn FnMut(&mut Fragment)) { + (*mutator)(&mut self.fragment) + } + + fn print_extra_flow_children(&self, print_tree: &mut PrintTree) { + print_tree.add_item(format!("↑↑ Fragment for block:{:?}", self.fragment)); + } +} + +impl fmt::Debug for BlockFlow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{:?}({:x}) {:?}", + self.class(), + self.base.debug_id(), + self.base + ) + } +} + +/// The inputs for the inline-sizes-and-margins constraint equation. +#[derive(Clone, Copy, Debug)] +pub struct ISizeConstraintInput { + pub computed_inline_size: MaybeAuto, + pub inline_start_margin: MaybeAuto, + pub inline_end_margin: MaybeAuto, + pub inline_start: MaybeAuto, + pub inline_end: MaybeAuto, + pub text_align: TextAlign, + pub available_inline_size: Au, +} + +impl ISizeConstraintInput { + pub fn new( + computed_inline_size: MaybeAuto, + inline_start_margin: MaybeAuto, + inline_end_margin: MaybeAuto, + inline_start: MaybeAuto, + inline_end: MaybeAuto, + text_align: TextAlign, + available_inline_size: Au, + ) -> ISizeConstraintInput { + ISizeConstraintInput { + computed_inline_size: computed_inline_size, + inline_start_margin: inline_start_margin, + inline_end_margin: inline_end_margin, + inline_start: inline_start, + inline_end: inline_end, + text_align: text_align, + available_inline_size: available_inline_size, + } + } +} + +/// The solutions for the inline-size-and-margins constraint equation. +#[derive(Clone, Copy, Debug)] +pub struct ISizeConstraintSolution { + pub inline_start: Au, + pub inline_size: Au, + pub margin_inline_start: Au, + pub margin_inline_end: Au, +} + +impl ISizeConstraintSolution { + pub fn new( + inline_size: Au, + margin_inline_start: Au, + margin_inline_end: Au, + ) -> ISizeConstraintSolution { + ISizeConstraintSolution { + inline_start: Au(0), + inline_size: inline_size, + margin_inline_start: margin_inline_start, + margin_inline_end: margin_inline_end, + } + } + + fn for_absolute_flow( + inline_start: Au, + inline_size: Au, + margin_inline_start: Au, + margin_inline_end: Au, + ) -> ISizeConstraintSolution { + ISizeConstraintSolution { + inline_start: inline_start, + inline_size: inline_size, + margin_inline_start: margin_inline_start, + margin_inline_end: margin_inline_end, + } + } +} + +// Trait to encapsulate the ISize and Margin calculation. +// +// CSS Section 10.3 +pub trait ISizeAndMarginsComputer { + /// Instructs the fragment to compute its border and padding. + fn compute_border_and_padding(&self, block: &mut BlockFlow, containing_block_inline_size: Au) { + block + .fragment + .compute_border_and_padding(containing_block_inline_size); + } + + /// Compute the inputs for the ISize constraint equation. + /// + /// This is called only once to compute the initial inputs. For calculations involving + /// minimum and maximum inline-size, we don't need to recompute these. + fn compute_inline_size_constraint_inputs( + &self, + block: &mut BlockFlow, + parent_flow_inline_size: Au, + shared_context: &SharedStyleContext, + ) -> ISizeConstraintInput { + let containing_block_inline_size = + self.containing_block_inline_size(block, parent_flow_inline_size, shared_context); + + block + .fragment + .compute_block_direction_margins(containing_block_inline_size); + block + .fragment + .compute_inline_direction_margins(containing_block_inline_size); + self.compute_border_and_padding(block, containing_block_inline_size); + + let mut computed_inline_size = + self.initial_computed_inline_size(block, parent_flow_inline_size, shared_context); + let style = block.fragment.style(); + match (computed_inline_size, style.get_position().box_sizing) { + (MaybeAuto::Specified(size), BoxSizing::BorderBox) => { + computed_inline_size = + MaybeAuto::Specified(size - block.fragment.border_padding.inline_start_end()) + }, + (MaybeAuto::Auto, BoxSizing::BorderBox) | (_, BoxSizing::ContentBox) => {}, + } + + let margin = style.logical_margin(); + let position = style.logical_position(); + + let available_inline_size = + containing_block_inline_size - block.fragment.border_padding.inline_start_end(); + ISizeConstraintInput::new( + computed_inline_size, + MaybeAuto::from_style(margin.inline_start, containing_block_inline_size), + MaybeAuto::from_style(margin.inline_end, containing_block_inline_size), + MaybeAuto::from_style(position.inline_start, containing_block_inline_size), + MaybeAuto::from_style(position.inline_end, containing_block_inline_size), + style.get_inherited_text().text_align, + available_inline_size, + ) + } + + /// Set the used values for inline-size and margins from the relevant constraint equation. + /// This is called only once. + /// + /// Set: + /// * Used values for content inline-size, inline-start margin, and inline-end margin for this + /// flow's box; + /// * Inline-start coordinate of this flow's box; + /// * Inline-start coordinate of the flow with respect to its containing block (if this is an + /// absolute flow). + fn set_inline_size_constraint_solutions( + &self, + block: &mut BlockFlow, + solution: ISizeConstraintSolution, + ) { + let inline_size; + let extra_inline_size_from_margin; + { + let block_mode = block.base.writing_mode; + + // FIXME (mbrubeck): Get correct containing block for positioned blocks? + let container_mode = block.base.block_container_writing_mode; + let container_size = block.base.block_container_inline_size; + + let fragment = block.fragment(); + fragment.margin.inline_start = solution.margin_inline_start; + fragment.margin.inline_end = solution.margin_inline_end; + + // The associated fragment has the border box of this flow. + inline_size = solution.inline_size + fragment.border_padding.inline_start_end(); + fragment.border_box.size.inline = inline_size; + + // Start border edge. + // FIXME (mbrubeck): Handle vertical writing modes. + fragment.border_box.start.i = + if container_mode.is_bidi_ltr() == block_mode.is_bidi_ltr() { + fragment.margin.inline_start + } else { + // The parent's "start" direction is the child's "end" direction. + container_size - inline_size - fragment.margin.inline_end + }; + + // To calculate the total size of this block, we also need to account for any + // additional size contribution from positive margins. Negative margins means the block + // isn't made larger at all by the margin. + extra_inline_size_from_margin = + max(Au(0), fragment.margin.inline_start) + max(Au(0), fragment.margin.inline_end); + } + + // We also resize the block itself, to ensure that overflow is not calculated + // as the inline-size of our parent. We might be smaller and we might be larger if we + // overflow. + block.mut_base().position.size.inline = inline_size + extra_inline_size_from_margin; + } + + /// Set the inline coordinate of the given flow if it is absolutely positioned. + fn set_inline_position_of_flow_if_necessary( + &self, + _: &mut BlockFlow, + _: ISizeConstraintSolution, + ) { + } + + /// Solve the inline-size and margins constraints for this block flow. + fn solve_inline_size_constraints( + &self, + block: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution; + + fn initial_computed_inline_size( + &self, + block: &mut BlockFlow, + parent_flow_inline_size: Au, + shared_context: &SharedStyleContext, + ) -> MaybeAuto { + MaybeAuto::from_option( + block + .fragment() + .style() + .content_inline_size() + .to_used_value(self.containing_block_inline_size( + block, + parent_flow_inline_size, + shared_context, + )), + ) + } + + fn containing_block_inline_size( + &self, + _: &mut BlockFlow, + parent_flow_inline_size: Au, + _: &SharedStyleContext, + ) -> Au { + parent_flow_inline_size + } + + /// Compute the used value of inline-size, taking care of min-inline-size and max-inline-size. + /// + /// CSS Section 10.4: Minimum and Maximum inline-sizes + fn compute_used_inline_size( + &self, + block: &mut BlockFlow, + shared_context: &SharedStyleContext, + parent_flow_inline_size: Au, + ) { + let mut input = self.compute_inline_size_constraint_inputs( + block, + parent_flow_inline_size, + shared_context, + ); + + let containing_block_inline_size = + self.containing_block_inline_size(block, parent_flow_inline_size, shared_context); + + let mut solution = self.solve_inline_size_constraints(block, &input); + + // If the tentative used inline-size is greater than 'max-inline-size', inline-size should + // be recalculated, but this time using the computed value of 'max-inline-size' as the + // computed value for 'inline-size'. + match block + .fragment() + .style() + .max_inline_size() + .to_used_value(containing_block_inline_size) + { + Some(max_inline_size) if max_inline_size < solution.inline_size => { + input.computed_inline_size = MaybeAuto::Specified(max_inline_size); + solution = self.solve_inline_size_constraints(block, &input); + }, + _ => {}, + } + + // If the resulting inline-size is smaller than 'min-inline-size', inline-size should be + // recalculated, but this time using the value of 'min-inline-size' as the computed value + // for 'inline-size'. + let computed_min_inline_size = block + .fragment() + .style() + .min_inline_size() + .to_used_value(containing_block_inline_size) + .unwrap_or(Au(0)); + if computed_min_inline_size > solution.inline_size { + input.computed_inline_size = MaybeAuto::Specified(computed_min_inline_size); + solution = self.solve_inline_size_constraints(block, &input); + } + + self.set_inline_size_constraint_solutions(block, solution); + self.set_inline_position_of_flow_if_necessary(block, solution); + } + + /// Computes inline-start and inline-end margins and inline-size. + /// + /// This is used by both replaced and non-replaced Blocks. + /// + /// CSS 2.1 Section 10.3.3. + /// Constraint Equation: margin-inline-start + margin-inline-end + inline-size = + /// available_inline-size + /// where available_inline-size = CB inline-size - (horizontal border + padding) + fn solve_block_inline_size_constraints( + &self, + block: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + let (computed_inline_size, inline_start_margin, inline_end_margin, available_inline_size) = ( + input.computed_inline_size, + input.inline_start_margin, + input.inline_end_margin, + input.available_inline_size, + ); + + // Check for direction of parent flow (NOT Containing Block) + let block_mode = block.base.writing_mode; + let container_mode = block.base.block_container_writing_mode; + let block_align = block.base.flags.text_align(); + + // FIXME (mbrubeck): Handle vertical writing modes. + let parent_has_same_direction = container_mode.is_bidi_ltr() == block_mode.is_bidi_ltr(); + + // If inline-size is not 'auto', and inline-size + margins > available_inline-size, all + // 'auto' margins are treated as 0. + let (inline_start_margin, inline_end_margin) = match computed_inline_size { + MaybeAuto::Auto => (inline_start_margin, inline_end_margin), + MaybeAuto::Specified(inline_size) => { + let inline_start = inline_start_margin.specified_or_zero(); + let inline_end = inline_end_margin.specified_or_zero(); + + if (inline_start + inline_end + inline_size) > available_inline_size { + ( + MaybeAuto::Specified(inline_start), + MaybeAuto::Specified(inline_end), + ) + } else { + (inline_start_margin, inline_end_margin) + } + }, + }; + + // Invariant: inline-start_margin + inline-size + inline-end_margin == + // available_inline-size + let (inline_start_margin, inline_size, inline_end_margin) = + match (inline_start_margin, computed_inline_size, inline_end_margin) { + // If all have a computed value other than 'auto', the system is over-constrained. + ( + MaybeAuto::Specified(margin_start), + MaybeAuto::Specified(inline_size), + MaybeAuto::Specified(margin_end), + ) => { + // servo_left, servo_right, and servo_center are used to implement + // the "align descendants" rule in HTML5 § 14.2. + if block_align == TextAlign::ServoCenter { + // Ignore any existing margins, and make the inline-start and + // inline-end margins equal. + let margin = (available_inline_size - inline_size).scale_by(0.5); + (margin, inline_size, margin) + } else { + let ignore_end_margin = match block_align { + TextAlign::ServoLeft => block_mode.is_bidi_ltr(), + TextAlign::ServoRight => !block_mode.is_bidi_ltr(), + _ => parent_has_same_direction, + }; + if ignore_end_margin { + ( + margin_start, + inline_size, + available_inline_size - (margin_start + inline_size), + ) + } else { + ( + available_inline_size - (margin_end + inline_size), + inline_size, + margin_end, + ) + } + } + }, + // If exactly one value is 'auto', solve for it + ( + MaybeAuto::Auto, + MaybeAuto::Specified(inline_size), + MaybeAuto::Specified(margin_end), + ) => ( + available_inline_size - (inline_size + margin_end), + inline_size, + margin_end, + ), + ( + MaybeAuto::Specified(margin_start), + MaybeAuto::Auto, + MaybeAuto::Specified(margin_end), + ) => ( + margin_start, + available_inline_size - (margin_start + margin_end), + margin_end, + ), + ( + MaybeAuto::Specified(margin_start), + MaybeAuto::Specified(inline_size), + MaybeAuto::Auto, + ) => ( + margin_start, + inline_size, + available_inline_size - (margin_start + inline_size), + ), + + // If inline-size is set to 'auto', any other 'auto' value becomes '0', + // and inline-size is solved for + (MaybeAuto::Auto, MaybeAuto::Auto, MaybeAuto::Specified(margin_end)) => { + (Au(0), available_inline_size - margin_end, margin_end) + }, + (MaybeAuto::Specified(margin_start), MaybeAuto::Auto, MaybeAuto::Auto) => { + (margin_start, available_inline_size - margin_start, Au(0)) + }, + (MaybeAuto::Auto, MaybeAuto::Auto, MaybeAuto::Auto) => { + (Au(0), available_inline_size, Au(0)) + }, + + // If inline-start and inline-end margins are auto, they become equal + (MaybeAuto::Auto, MaybeAuto::Specified(inline_size), MaybeAuto::Auto) => { + let margin = (available_inline_size - inline_size).scale_by(0.5); + (margin, inline_size, margin) + }, + }; + + ISizeConstraintSolution::new(inline_size, inline_start_margin, inline_end_margin) + } +} + +/// The different types of Blocks. +/// +/// They mainly differ in the way inline-size and block-sizes and margins are calculated +/// for them. +pub struct AbsoluteNonReplaced; +pub struct AbsoluteReplaced; +pub struct BlockNonReplaced; +pub struct BlockReplaced; +pub struct FloatNonReplaced; +pub struct FloatReplaced; +pub struct InlineBlockNonReplaced; +pub struct InlineBlockReplaced; +pub struct InlineFlexItem; + +impl ISizeAndMarginsComputer for AbsoluteNonReplaced { + /// Solve the horizontal constraint equation for absolute non-replaced elements. + /// + /// CSS Section 10.3.7 + /// Constraint equation: + /// inline-start + inline-end + inline-size + margin-inline-start + margin-inline-end + /// = absolute containing block inline-size - (horizontal padding and border) + /// [aka available inline-size] + /// + /// Return the solution for the equation. + fn solve_inline_size_constraints( + &self, + block: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + let &ISizeConstraintInput { + computed_inline_size, + inline_start_margin, + inline_end_margin, + inline_start, + inline_end, + available_inline_size, + .. + } = input; + + // Check for direction of parent flow (NOT Containing Block) + let block_mode = block.base.writing_mode; + let container_mode = block.base.block_container_writing_mode; + + // FIXME (mbrubeck): Handle vertical writing modes. + let parent_has_same_direction = container_mode.is_bidi_ltr() == block_mode.is_bidi_ltr(); + + let (inline_start, inline_size, margin_inline_start, margin_inline_end) = + match (inline_start, inline_end, computed_inline_size) { + (MaybeAuto::Auto, MaybeAuto::Auto, MaybeAuto::Auto) => { + let margin_start = inline_start_margin.specified_or_zero(); + let margin_end = inline_end_margin.specified_or_zero(); + // Now it is the same situation as inline-start Specified and inline-end + // and inline-size Auto. + + // Set inline-end to zero to calculate inline-size. + let inline_size = block.get_shrink_to_fit_inline_size( + available_inline_size - (margin_start + margin_end), + ); + (Au(0), inline_size, margin_start, margin_end) + }, + ( + MaybeAuto::Specified(inline_start), + MaybeAuto::Specified(inline_end), + MaybeAuto::Specified(inline_size), + ) => { + match (inline_start_margin, inline_end_margin) { + (MaybeAuto::Auto, MaybeAuto::Auto) => { + let total_margin_val = + available_inline_size - inline_start - inline_end - inline_size; + if total_margin_val < Au(0) { + if parent_has_same_direction { + // margin-inline-start becomes 0 + (inline_start, inline_size, Au(0), total_margin_val) + } else { + // margin-inline-end becomes 0, because it's toward the parent's + // inline-start edge. + (inline_start, inline_size, total_margin_val, Au(0)) + } + } else { + // Equal margins + ( + inline_start, + inline_size, + total_margin_val.scale_by(0.5), + total_margin_val.scale_by(0.5), + ) + } + }, + (MaybeAuto::Specified(margin_start), MaybeAuto::Auto) => { + let sum = inline_start + inline_end + inline_size + margin_start; + ( + inline_start, + inline_size, + margin_start, + available_inline_size - sum, + ) + }, + (MaybeAuto::Auto, MaybeAuto::Specified(margin_end)) => { + let sum = inline_start + inline_end + inline_size + margin_end; + ( + inline_start, + inline_size, + available_inline_size - sum, + margin_end, + ) + }, + (MaybeAuto::Specified(margin_start), MaybeAuto::Specified(margin_end)) => { + // Values are over-constrained. + let sum = inline_start + inline_size + margin_start + margin_end; + if parent_has_same_direction { + // Ignore value for 'inline-end' + (inline_start, inline_size, margin_start, margin_end) + } else { + // Ignore value for 'inline-start' + ( + available_inline_size - sum, + inline_size, + margin_start, + margin_end, + ) + } + }, + } + }, + // For the rest of the cases, auto values for margin are set to 0 + + // If only one is Auto, solve for it + ( + MaybeAuto::Auto, + MaybeAuto::Specified(inline_end), + MaybeAuto::Specified(inline_size), + ) => { + let margin_start = inline_start_margin.specified_or_zero(); + let margin_end = inline_end_margin.specified_or_zero(); + let sum = inline_end + inline_size + margin_start + margin_end; + ( + available_inline_size - sum, + inline_size, + margin_start, + margin_end, + ) + }, + ( + MaybeAuto::Specified(inline_start), + MaybeAuto::Auto, + MaybeAuto::Specified(inline_size), + ) => { + let margin_start = inline_start_margin.specified_or_zero(); + let margin_end = inline_end_margin.specified_or_zero(); + (inline_start, inline_size, margin_start, margin_end) + }, + ( + MaybeAuto::Specified(inline_start), + MaybeAuto::Specified(inline_end), + MaybeAuto::Auto, + ) => { + let margin_start = inline_start_margin.specified_or_zero(); + let margin_end = inline_end_margin.specified_or_zero(); + let sum = inline_start + inline_end + margin_start + margin_end; + ( + inline_start, + available_inline_size - sum, + margin_start, + margin_end, + ) + }, + + // If inline-size is auto, then inline-size is shrink-to-fit. Solve for the + // non-auto value. + (MaybeAuto::Specified(inline_start), MaybeAuto::Auto, MaybeAuto::Auto) => { + let margin_start = inline_start_margin.specified_or_zero(); + let margin_end = inline_end_margin.specified_or_zero(); + // Set inline-end to zero to calculate inline-size + let inline_size = block.get_shrink_to_fit_inline_size( + available_inline_size - (margin_start + margin_end), + ); + (inline_start, inline_size, margin_start, margin_end) + }, + (MaybeAuto::Auto, MaybeAuto::Specified(inline_end), MaybeAuto::Auto) => { + let margin_start = inline_start_margin.specified_or_zero(); + let margin_end = inline_end_margin.specified_or_zero(); + // Set inline-start to zero to calculate inline-size + let inline_size = block.get_shrink_to_fit_inline_size( + available_inline_size - (margin_start + margin_end), + ); + let sum = inline_end + inline_size + margin_start + margin_end; + ( + available_inline_size - sum, + inline_size, + margin_start, + margin_end, + ) + }, + + (MaybeAuto::Auto, MaybeAuto::Auto, MaybeAuto::Specified(inline_size)) => { + let margin_start = inline_start_margin.specified_or_zero(); + let margin_end = inline_end_margin.specified_or_zero(); + // Setting 'inline-start' to static position because direction is 'ltr'. + // TODO: Handle 'rtl' when it is implemented. + (Au(0), inline_size, margin_start, margin_end) + }, + }; + ISizeConstraintSolution::for_absolute_flow( + inline_start, + inline_size, + margin_inline_start, + margin_inline_end, + ) + } + + fn containing_block_inline_size( + &self, + block: &mut BlockFlow, + _: Au, + shared_context: &SharedStyleContext, + ) -> Au { + let opaque_block = OpaqueFlow::from_flow(block); + block + .containing_block_size(&shared_context.viewport_size(), opaque_block) + .inline + } + + fn set_inline_position_of_flow_if_necessary( + &self, + block: &mut BlockFlow, + solution: ISizeConstraintSolution, + ) { + // Set the inline position of the absolute flow wrt to its containing block. + if !block + .base + .flags + .contains(FlowFlags::INLINE_POSITION_IS_STATIC) + { + block.base.position.start.i = solution.inline_start; + } + } +} + +impl ISizeAndMarginsComputer for AbsoluteReplaced { + /// Solve the horizontal constraint equation for absolute replaced elements. + /// + /// CSS Section 10.3.8 + /// Constraint equation: + /// inline-start + inline-end + inline-size + margin-inline-start + margin-inline-end + /// = absolute containing block inline-size - (horizontal padding and border) + /// [aka available_inline-size] + /// + /// Return the solution for the equation. + fn solve_inline_size_constraints( + &self, + _: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + let &ISizeConstraintInput { + computed_inline_size, + inline_start_margin, + inline_end_margin, + inline_start, + inline_end, + available_inline_size, + .. + } = input; + // TODO: Check for direction of static-position Containing Block (aka + // parent flow, _not_ the actual Containing Block) when right-to-left + // is implemented + // Assume direction is 'ltr' for now + // TODO: Handle all the cases for 'rtl' direction. + + let inline_size = match computed_inline_size { + MaybeAuto::Specified(w) => w, + _ => panic!( + "{} {}", + "The used value for inline_size for absolute replaced flow", + "should have already been calculated by now." + ), + }; + + let (inline_start, inline_size, margin_inline_start, margin_inline_end) = + match (inline_start, inline_end) { + (MaybeAuto::Auto, MaybeAuto::Auto) => { + let margin_start = inline_start_margin.specified_or_zero(); + let margin_end = inline_end_margin.specified_or_zero(); + (Au(0), inline_size, margin_start, margin_end) + }, + // If only one is Auto, solve for it + (MaybeAuto::Auto, MaybeAuto::Specified(inline_end)) => { + let margin_start = inline_start_margin.specified_or_zero(); + let margin_end = inline_end_margin.specified_or_zero(); + let sum = inline_end + inline_size + margin_start + margin_end; + ( + available_inline_size - sum, + inline_size, + margin_start, + margin_end, + ) + }, + (MaybeAuto::Specified(inline_start), MaybeAuto::Auto) => { + let margin_start = inline_start_margin.specified_or_zero(); + let margin_end = inline_end_margin.specified_or_zero(); + (inline_start, inline_size, margin_start, margin_end) + }, + (MaybeAuto::Specified(inline_start), MaybeAuto::Specified(inline_end)) => { + match (inline_start_margin, inline_end_margin) { + (MaybeAuto::Auto, MaybeAuto::Auto) => { + let total_margin_val = + available_inline_size - inline_start - inline_end - inline_size; + if total_margin_val < Au(0) { + // margin-inline-start becomes 0 because direction is 'ltr'. + (inline_start, inline_size, Au(0), total_margin_val) + } else { + // Equal margins + ( + inline_start, + inline_size, + total_margin_val.scale_by(0.5), + total_margin_val.scale_by(0.5), + ) + } + }, + (MaybeAuto::Specified(margin_start), MaybeAuto::Auto) => { + let sum = inline_start + inline_end + inline_size + margin_start; + ( + inline_start, + inline_size, + margin_start, + available_inline_size - sum, + ) + }, + (MaybeAuto::Auto, MaybeAuto::Specified(margin_end)) => { + let sum = inline_start + inline_end + inline_size + margin_end; + ( + inline_start, + inline_size, + available_inline_size - sum, + margin_end, + ) + }, + (MaybeAuto::Specified(margin_start), MaybeAuto::Specified(margin_end)) => { + // Values are over-constrained. + // Ignore value for 'inline-end' cos direction is 'ltr'. + (inline_start, inline_size, margin_start, margin_end) + }, + } + }, + }; + ISizeConstraintSolution::for_absolute_flow( + inline_start, + inline_size, + margin_inline_start, + margin_inline_end, + ) + } + + /// Calculate used value of inline-size just like we do for inline replaced elements. + fn initial_computed_inline_size( + &self, + block: &mut BlockFlow, + _: Au, + shared_context: &SharedStyleContext, + ) -> MaybeAuto { + let opaque_block = OpaqueFlow::from_flow(block); + let containing_block_inline_size = block + .containing_block_size(&shared_context.viewport_size(), opaque_block) + .inline; + let container_block_size = block.explicit_block_containing_size(shared_context); + let fragment = block.fragment(); + fragment.assign_replaced_inline_size_if_necessary( + containing_block_inline_size, + container_block_size, + ); + // For replaced absolute flow, the rest of the constraint solving will + // take inline-size to be specified as the value computed here. + MaybeAuto::Specified(fragment.content_box().size.inline) + } + + fn containing_block_inline_size( + &self, + block: &mut BlockFlow, + _: Au, + shared_context: &SharedStyleContext, + ) -> Au { + let opaque_block = OpaqueFlow::from_flow(block); + block + .containing_block_size(&shared_context.viewport_size(), opaque_block) + .inline + } + + fn set_inline_position_of_flow_if_necessary( + &self, + block: &mut BlockFlow, + solution: ISizeConstraintSolution, + ) { + // Set the x-coordinate of the absolute flow wrt to its containing block. + block.base.position.start.i = solution.inline_start; + } +} + +impl ISizeAndMarginsComputer for BlockNonReplaced { + /// Compute inline-start and inline-end margins and inline-size. + fn solve_inline_size_constraints( + &self, + block: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + self.solve_block_inline_size_constraints(block, input) + } +} + +impl ISizeAndMarginsComputer for BlockReplaced { + /// Compute inline-start and inline-end margins and inline-size. + /// + /// ISize has already been calculated. We now calculate the margins just + /// like for non-replaced blocks. + fn solve_inline_size_constraints( + &self, + block: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + match input.computed_inline_size { + MaybeAuto::Specified(_) => {}, + MaybeAuto::Auto => { + panic!("BlockReplaced: inline_size should have been computed by now") + }, + }; + self.solve_block_inline_size_constraints(block, input) + } + + /// Calculate used value of inline-size just like we do for inline replaced elements. + fn initial_computed_inline_size( + &self, + block: &mut BlockFlow, + parent_flow_inline_size: Au, + shared_context: &SharedStyleContext, + ) -> MaybeAuto { + let container_block_size = block.explicit_block_containing_size(shared_context); + let fragment = block.fragment(); + fragment.assign_replaced_inline_size_if_necessary( + parent_flow_inline_size, + container_block_size, + ); + // For replaced block flow, the rest of the constraint solving will + // take inline-size to be specified as the value computed here. + MaybeAuto::Specified(fragment.content_box().size.inline) + } +} + +impl ISizeAndMarginsComputer for FloatNonReplaced { + /// CSS Section 10.3.5 + /// + /// If inline-size is computed as 'auto', the used value is the 'shrink-to-fit' inline-size. + fn solve_inline_size_constraints( + &self, + block: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + let (computed_inline_size, inline_start_margin, inline_end_margin, available_inline_size) = ( + input.computed_inline_size, + input.inline_start_margin, + input.inline_end_margin, + input.available_inline_size, + ); + let margin_inline_start = inline_start_margin.specified_or_zero(); + let margin_inline_end = inline_end_margin.specified_or_zero(); + let available_inline_size_float = + available_inline_size - margin_inline_start - margin_inline_end; + let shrink_to_fit = block.get_shrink_to_fit_inline_size(available_inline_size_float); + let inline_size = computed_inline_size.specified_or_default(shrink_to_fit); + debug!( + "assign_inline_sizes_float -- inline_size: {:?}", + inline_size + ); + ISizeConstraintSolution::new(inline_size, margin_inline_start, margin_inline_end) + } +} + +impl ISizeAndMarginsComputer for FloatReplaced { + /// CSS Section 10.3.5 + /// + /// If inline-size is computed as 'auto', the used value is the 'shrink-to-fit' inline-size. + fn solve_inline_size_constraints( + &self, + _: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + let (computed_inline_size, inline_start_margin, inline_end_margin) = ( + input.computed_inline_size, + input.inline_start_margin, + input.inline_end_margin, + ); + let margin_inline_start = inline_start_margin.specified_or_zero(); + let margin_inline_end = inline_end_margin.specified_or_zero(); + let inline_size = match computed_inline_size { + MaybeAuto::Specified(w) => w, + MaybeAuto::Auto => { + panic!("FloatReplaced: inline_size should have been computed by now") + }, + }; + debug!( + "assign_inline_sizes_float -- inline_size: {:?}", + inline_size + ); + ISizeConstraintSolution::new(inline_size, margin_inline_start, margin_inline_end) + } + + /// Calculate used value of inline-size just like we do for inline replaced elements. + fn initial_computed_inline_size( + &self, + block: &mut BlockFlow, + parent_flow_inline_size: Au, + shared_context: &SharedStyleContext, + ) -> MaybeAuto { + let container_block_size = block.explicit_block_containing_size(shared_context); + let fragment = block.fragment(); + fragment.assign_replaced_inline_size_if_necessary( + parent_flow_inline_size, + container_block_size, + ); + // For replaced block flow, the rest of the constraint solving will + // take inline-size to be specified as the value computed here. + MaybeAuto::Specified(fragment.content_box().size.inline) + } +} + +impl ISizeAndMarginsComputer for InlineBlockNonReplaced { + /// Compute inline-start and inline-end margins and inline-size. + fn solve_inline_size_constraints( + &self, + block: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + let (computed_inline_size, inline_start_margin, inline_end_margin, available_inline_size) = ( + input.computed_inline_size, + input.inline_start_margin, + input.inline_end_margin, + input.available_inline_size, + ); + + // For inline-blocks, `auto` margins compute to 0. + let inline_start_margin = inline_start_margin.specified_or_zero(); + let inline_end_margin = inline_end_margin.specified_or_zero(); + + // If inline-size is set to 'auto', and this is an inline block, use the + // shrink to fit algorithm (see CSS 2.1 § 10.3.9) + let inline_size = match computed_inline_size { + MaybeAuto::Auto => block.get_shrink_to_fit_inline_size( + available_inline_size - (inline_start_margin + inline_end_margin), + ), + MaybeAuto::Specified(inline_size) => inline_size, + }; + + ISizeConstraintSolution::new(inline_size, inline_start_margin, inline_end_margin) + } +} + +impl ISizeAndMarginsComputer for InlineBlockReplaced { + /// Compute inline-start and inline-end margins and inline-size. + /// + /// ISize has already been calculated. We now calculate the margins just + /// like for non-replaced blocks. + fn solve_inline_size_constraints( + &self, + block: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + debug_assert!(match input.computed_inline_size { + MaybeAuto::Specified(_) => true, + MaybeAuto::Auto => false, + }); + + let (computed_inline_size, inline_start_margin, inline_end_margin, available_inline_size) = ( + input.computed_inline_size, + input.inline_start_margin, + input.inline_end_margin, + input.available_inline_size, + ); + + // For inline-blocks, `auto` margins compute to 0. + let inline_start_margin = inline_start_margin.specified_or_zero(); + let inline_end_margin = inline_end_margin.specified_or_zero(); + + // If inline-size is set to 'auto', and this is an inline block, use the + // shrink to fit algorithm (see CSS 2.1 § 10.3.9) + let inline_size = match computed_inline_size { + MaybeAuto::Auto => block.get_shrink_to_fit_inline_size( + available_inline_size - (inline_start_margin + inline_end_margin), + ), + MaybeAuto::Specified(inline_size) => inline_size, + }; + + ISizeConstraintSolution::new(inline_size, inline_start_margin, inline_end_margin) + } + + /// Calculate used value of inline-size just like we do for inline replaced elements. + fn initial_computed_inline_size( + &self, + block: &mut BlockFlow, + parent_flow_inline_size: Au, + shared_context: &SharedStyleContext, + ) -> MaybeAuto { + let container_block_size = block.explicit_block_containing_size(shared_context); + let fragment = block.fragment(); + fragment.assign_replaced_inline_size_if_necessary( + parent_flow_inline_size, + container_block_size, + ); + // For replaced block flow, the rest of the constraint solving will + // take inline-size to be specified as the value computed here. + MaybeAuto::Specified(fragment.content_box().size.inline) + } +} + +impl ISizeAndMarginsComputer for InlineFlexItem { + // Replace the default method directly to prevent recalculating and setting margins again + // which has already been set by its parent. + fn compute_used_inline_size( + &self, + block: &mut BlockFlow, + shared_context: &SharedStyleContext, + parent_flow_inline_size: Au, + ) { + let container_block_size = block.explicit_block_containing_size(shared_context); + block.fragment.assign_replaced_inline_size_if_necessary( + parent_flow_inline_size, + container_block_size, + ); + } + + // The used inline size and margins are set by parent flex flow, do nothing here. + fn solve_inline_size_constraints( + &self, + block: &mut BlockFlow, + _: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + let fragment = block.fragment(); + ISizeConstraintSolution::new( + fragment.border_box.size.inline, + fragment.margin.inline_start, + fragment.margin.inline_end, + ) + } +} diff --git a/components/layout_2020/construct.rs b/components/layout_2020/construct.rs new file mode 100644 index 00000000000..072413995b2 --- /dev/null +++ b/components/layout_2020/construct.rs @@ -0,0 +1,2443 @@ +/* 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/. */ + +//! Creates flows and fragments from a DOM tree via a bottom-up, incremental traversal of the DOM. +//! +//! Each step of the traversal considers the node and existing flow, if there is one. If a node is +//! not dirty and an existing flow exists, then the traversal reuses that flow. Otherwise, it +//! proceeds to construct either a flow or a `ConstructionItem`. A construction item is a piece of +//! intermediate data that goes with a DOM node and hasn't found its "home" yet-maybe it's a box, +//! maybe it's an absolute or fixed position thing that hasn't found its containing block yet. +//! Construction items bubble up the tree from children to parents until they find their homes. + +use crate::block::BlockFlow; +use crate::context::{with_thread_local_font_context, LayoutContext}; +use crate::data::{LayoutData, LayoutDataFlags}; +use crate::display_list::items::OpaqueNode; +use crate::flex::FlexFlow; +use crate::floats::FloatKind; +use crate::flow::{AbsoluteDescendants, Flow, FlowClass, GetBaseFlow, ImmutableFlowUtils}; +use crate::flow::{FlowFlags, MutableFlowUtils, MutableOwnedFlowUtils}; +use crate::flow_ref::FlowRef; +use crate::fragment::{ + CanvasFragmentInfo, Fragment, FragmentFlags, GeneratedContentInfo, IframeFragmentInfo, +}; +use crate::fragment::{ + ImageFragmentInfo, InlineAbsoluteFragmentInfo, InlineAbsoluteHypotheticalFragmentInfo, +}; +use crate::fragment::{ + InlineBlockFragmentInfo, MediaFragmentInfo, SpecificFragmentInfo, SvgFragmentInfo, +}; +use crate::fragment::{ + TableColumnFragmentInfo, UnscannedTextFragmentInfo, WhitespaceStrippingResult, +}; +use crate::inline::{InlineFlow, InlineFragmentNodeFlags, InlineFragmentNodeInfo}; +use crate::linked_list::prepend_from; +use crate::list_item::{ListItemFlow, ListStyleTypeContent}; +use crate::multicol::{MulticolColumnFlow, MulticolFlow}; +use crate::parallel; +use crate::table::TableFlow; +use crate::table_caption::TableCaptionFlow; +use crate::table_cell::TableCellFlow; +use crate::table_colgroup::TableColGroupFlow; +use crate::table_row::TableRowFlow; +use crate::table_rowgroup::TableRowGroupFlow; +use crate::table_wrapper::TableWrapperFlow; +use crate::text::TextRunScanner; +use crate::traversal::PostorderNodeMutTraversal; +use crate::wrapper::{LayoutNodeLayoutData, TextContent, ThreadSafeLayoutNodeHelpers}; +use crate::ServoArc; +use script_layout_interface::wrapper_traits::{ + PseudoElementType, ThreadSafeLayoutElement, ThreadSafeLayoutNode, +}; +use script_layout_interface::{LayoutElementType, LayoutNodeType}; +use servo_config::opts; +use servo_url::ServoUrl; +use std::collections::LinkedList; +use std::marker::PhantomData; +use std::mem; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use style::computed_values::caption_side::T as CaptionSide; +use style::computed_values::display::T as Display; +use style::computed_values::empty_cells::T as EmptyCells; +use style::computed_values::float::T as Float; +use style::computed_values::list_style_position::T as ListStylePosition; +use style::computed_values::position::T as Position; +use style::context::SharedStyleContext; +use style::dom::TElement; +use style::logical_geometry::Direction; +use style::properties::ComputedValues; +use style::selector_parser::{PseudoElement, RestyleDamage}; +use style::servo::restyle_damage::ServoRestyleDamage; +use style::values::generics::counters::ContentItem; +use style::values::generics::url::UrlOrNone as ImageUrlOrNone; + +/// The results of flow construction for a DOM node. +#[derive(Clone)] +pub enum ConstructionResult { + /// This node contributes nothing at all (`display: none`). Alternately, this is what newly + /// created nodes have their `ConstructionResult` set to. + None, + + /// This node contributed a flow at the proper position in the tree. + /// Nothing more needs to be done for this node. It has bubbled up fixed + /// and absolute descendant flows that have a containing block above it. + Flow(FlowRef, AbsoluteDescendants), + + /// This node contributed some object or objects that will be needed to construct a proper flow + /// later up the tree, but these objects have not yet found their home. + ConstructionItem(ConstructionItem), +} + +impl ConstructionResult { + pub fn get(&mut self) -> ConstructionResult { + // FIXME(pcwalton): Stop doing this with inline fragments. Cloning fragments is very + // inefficient! + (*self).clone() + } + + pub fn debug_id(&self) -> usize { + match *self { + ConstructionResult::None => 0, + ConstructionResult::ConstructionItem(_) => 0, + ConstructionResult::Flow(ref flow_ref, _) => flow_ref.base().debug_id(), + } + } +} + +/// Represents the output of flow construction for a DOM node that has not yet resulted in a +/// complete flow. Construction items bubble up the tree until they find a `Flow` to be attached +/// to. +#[derive(Clone)] +pub enum ConstructionItem { + /// Inline fragments and associated {ib} splits that have not yet found flows. + InlineFragments(InlineFragmentsConstructionResult), + /// Potentially ignorable whitespace. + /// + /// FIXME(emilio): How could whitespace have any PseudoElementType other + /// than Normal? + Whitespace( + OpaqueNode, + PseudoElementType, + ServoArc<ComputedValues>, + RestyleDamage, + ), + /// TableColumn Fragment + TableColumnFragment(Fragment), +} + +/// Represents inline fragments and {ib} splits that are bubbling up from an inline. +#[derive(Clone)] +pub struct InlineFragmentsConstructionResult { + /// Any {ib} splits that we're bubbling up. + pub splits: LinkedList<InlineBlockSplit>, + + /// Any fragments that succeed the {ib} splits. + pub fragments: IntermediateInlineFragments, +} + +/// Represents an {ib} split that has not yet found the containing block that it belongs to. This +/// is somewhat tricky. An example may be helpful. For this DOM fragment: +/// +/// ```html +/// <span> +/// A +/// <div>B</div> +/// C +/// </span> +/// ``` +/// +/// The resulting `ConstructionItem` for the outer `span` will be: +/// +/// ```rust,ignore +/// ConstructionItem::InlineFragments( +/// InlineFragmentsConstructionResult { +/// splits: linked_list![ +/// InlineBlockSplit { +/// predecessors: IntermediateInlineFragments { +/// fragments: linked_list![A], +/// absolute_descendents: AbsoluteDescendents { +/// descendant_links: vec![] +/// }, +/// }, +/// flow: B, +/// } +/// ], +/// fragments: linked_list![C], +/// }, +/// ) +/// ``` +#[derive(Clone)] +pub struct InlineBlockSplit { + /// The inline fragments that precede the flow. + pub predecessors: IntermediateInlineFragments, + + /// The flow that caused this {ib} split. + pub flow: FlowRef, +} + +impl InlineBlockSplit { + /// Flushes the given accumulator to the new split and makes a new accumulator to hold any + /// subsequent fragments. + fn new<ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode>( + fragment_accumulator: &mut InlineFragmentsAccumulator, + node: &ConcreteThreadSafeLayoutNode, + style_context: &SharedStyleContext, + flow: FlowRef, + ) -> InlineBlockSplit { + fragment_accumulator.enclosing_node.as_mut().expect( + "enclosing_node is None; Are {ib} splits being generated outside of an inline node?" + ).flags.remove(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT); + + let split = InlineBlockSplit { + predecessors: mem::replace( + fragment_accumulator, + InlineFragmentsAccumulator::from_inline_node(node, style_context), + ) + .to_intermediate_inline_fragments::<ConcreteThreadSafeLayoutNode>(style_context), + flow: flow, + }; + + fragment_accumulator + .enclosing_node + .as_mut() + .unwrap() + .flags + .remove(InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT); + + split + } +} + +/// Holds inline fragments and absolute descendants. +#[derive(Clone)] +pub struct IntermediateInlineFragments { + /// The list of fragments. + pub fragments: LinkedList<Fragment>, + + /// The list of absolute descendants of those inline fragments. + pub absolute_descendants: AbsoluteDescendants, +} + +impl IntermediateInlineFragments { + fn new() -> IntermediateInlineFragments { + IntermediateInlineFragments { + fragments: LinkedList::new(), + absolute_descendants: AbsoluteDescendants::new(), + } + } + + fn is_empty(&self) -> bool { + self.fragments.is_empty() && self.absolute_descendants.is_empty() + } + + fn push_all(&mut self, mut other: IntermediateInlineFragments) { + self.fragments.append(&mut other.fragments); + self.absolute_descendants + .push_descendants(other.absolute_descendants); + } +} + +/// Holds inline fragments that we're gathering for children of an inline node. +struct InlineFragmentsAccumulator { + /// The list of fragments. + fragments: IntermediateInlineFragments, + + /// Information about the inline box directly enclosing the fragments being gathered, if any. + /// + /// `inline::InlineFragmentNodeInfo` also stores flags indicating whether a fragment is the + /// first and/or last of the corresponding inline box. This `InlineFragmentsAccumulator` may + /// represent only one side of an {ib} split, so we store these flags as if it represented only + /// one fragment. `to_intermediate_inline_fragments` later splits this hypothetical fragment + /// into pieces, leaving the `FIRST_FRAGMENT_OF_ELEMENT` and `LAST_FRAGMENT_OF_ELEMENT` flags, + /// if present, on the first and last fragments of the output. + enclosing_node: Option<InlineFragmentNodeInfo>, + + /// Restyle damage to use for fragments created in this node. + restyle_damage: RestyleDamage, + + /// Bidi control characters to insert before and after these fragments. + bidi_control_chars: Option<(&'static str, &'static str)>, +} + +impl InlineFragmentsAccumulator { + fn new() -> InlineFragmentsAccumulator { + InlineFragmentsAccumulator { + fragments: IntermediateInlineFragments::new(), + enclosing_node: None, + bidi_control_chars: None, + restyle_damage: RestyleDamage::empty(), + } + } + + fn from_inline_node<N>( + node: &N, + style_context: &SharedStyleContext, + ) -> InlineFragmentsAccumulator + where + N: ThreadSafeLayoutNode, + { + InlineFragmentsAccumulator { + fragments: IntermediateInlineFragments::new(), + enclosing_node: Some(InlineFragmentNodeInfo { + address: node.opaque(), + pseudo: node.get_pseudo_element_type(), + style: node.style(style_context), + selected_style: node.selected_style(), + flags: InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT | + InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT, + }), + bidi_control_chars: None, + restyle_damage: node.restyle_damage(), + } + } + + fn push(&mut self, fragment: Fragment) { + self.fragments.fragments.push_back(fragment) + } + + fn push_all(&mut self, mut fragments: IntermediateInlineFragments) { + self.fragments.fragments.append(&mut fragments.fragments); + self.fragments + .absolute_descendants + .push_descendants(fragments.absolute_descendants); + } + + fn to_intermediate_inline_fragments<N>( + self, + context: &SharedStyleContext, + ) -> IntermediateInlineFragments + where + N: ThreadSafeLayoutNode, + { + let InlineFragmentsAccumulator { + mut fragments, + enclosing_node, + bidi_control_chars, + restyle_damage, + } = self; + if let Some(mut enclosing_node) = enclosing_node { + let fragment_count = fragments.fragments.len(); + for (index, fragment) in fragments.fragments.iter_mut().enumerate() { + let mut enclosing_node = enclosing_node.clone(); + if index != 0 { + enclosing_node + .flags + .remove(InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT) + } + if index != fragment_count - 1 { + enclosing_node + .flags + .remove(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT) + } + fragment.add_inline_context_style(enclosing_node); + } + + // Control characters are later discarded in transform_text, so they don't affect the + // is_first/is_last styles above. + enclosing_node.flags.remove( + InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT | + InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT, + ); + + if let Some((start, end)) = bidi_control_chars { + fragments + .fragments + .push_front(control_chars_to_fragment::<N::ConcreteElement>( + &enclosing_node, + context, + start, + restyle_damage, + )); + fragments + .fragments + .push_back(control_chars_to_fragment::<N::ConcreteElement>( + &enclosing_node, + context, + end, + restyle_damage, + )); + } + } + fragments + } +} + +/// An object that knows how to create flows. +pub struct FlowConstructor<'a, N: ThreadSafeLayoutNode> { + /// The layout context. + pub layout_context: &'a LayoutContext<'a>, + /// Satisfy the compiler about the unused parameters, which we use to improve the ergonomics of + /// the ensuing impl {} by removing the need to parameterize all the methods individually. + phantom2: PhantomData<N>, +} + +impl<'a, ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode> + FlowConstructor<'a, ConcreteThreadSafeLayoutNode> +{ + /// Creates a new flow constructor. + pub fn new(layout_context: &'a LayoutContext<'a>) -> Self { + FlowConstructor { + layout_context: layout_context, + phantom2: PhantomData, + } + } + + #[inline] + fn style_context(&self) -> &SharedStyleContext { + self.layout_context.shared_context() + } + + #[inline] + fn set_flow_construction_result( + &self, + node: &ConcreteThreadSafeLayoutNode, + result: ConstructionResult, + ) { + node.set_flow_construction_result(result); + } + + /// Builds the fragment for the given block or subclass thereof. + fn build_fragment_for_block(&self, node: &ConcreteThreadSafeLayoutNode) -> Fragment { + let specific_fragment_info = match node.type_id() { + Some(LayoutNodeType::Element(LayoutElementType::HTMLIFrameElement)) => { + SpecificFragmentInfo::Iframe(IframeFragmentInfo::new(node)) + }, + Some(LayoutNodeType::Element(LayoutElementType::HTMLImageElement)) => { + let image_info = Box::new(ImageFragmentInfo::new( + node.image_url(), + node.image_density(), + node, + &self.layout_context, + )); + SpecificFragmentInfo::Image(image_info) + }, + Some(LayoutNodeType::Element(LayoutElementType::HTMLMediaElement)) => { + let data = node.media_data().unwrap(); + SpecificFragmentInfo::Media(Box::new(MediaFragmentInfo::new(data))) + }, + Some(LayoutNodeType::Element(LayoutElementType::HTMLObjectElement)) => { + let elem = node.as_element().unwrap(); + let type_and_data = ( + elem.get_attr(&ns!(), &local_name!("type")), + elem.get_attr(&ns!(), &local_name!("data")), + ); + let object_data = match type_and_data { + (None, Some(uri)) if is_image_data(uri) => ServoUrl::parse(uri).ok(), + _ => None, + }; + let image_info = Box::new(ImageFragmentInfo::new( + object_data, + None, + node, + &self.layout_context, + )); + SpecificFragmentInfo::Image(image_info) + }, + Some(LayoutNodeType::Element(LayoutElementType::HTMLTableElement)) => { + SpecificFragmentInfo::TableWrapper + }, + Some(LayoutNodeType::Element(LayoutElementType::HTMLTableColElement)) => { + SpecificFragmentInfo::TableColumn(TableColumnFragmentInfo::new(node)) + }, + Some(LayoutNodeType::Element(LayoutElementType::HTMLTableCellElement)) => { + SpecificFragmentInfo::TableCell + }, + Some(LayoutNodeType::Element(LayoutElementType::HTMLTableRowElement)) | + Some(LayoutNodeType::Element(LayoutElementType::HTMLTableSectionElement)) => { + SpecificFragmentInfo::TableRow + }, + Some(LayoutNodeType::Element(LayoutElementType::HTMLCanvasElement)) => { + let data = node.canvas_data().unwrap(); + SpecificFragmentInfo::Canvas(Box::new(CanvasFragmentInfo::new(data))) + }, + Some(LayoutNodeType::Element(LayoutElementType::SVGSVGElement)) => { + let data = node.svg_data().unwrap(); + SpecificFragmentInfo::Svg(Box::new(SvgFragmentInfo::new(data))) + }, + _ => { + // This includes pseudo-elements. + SpecificFragmentInfo::Generic + }, + }; + + Fragment::new(node, specific_fragment_info, self.layout_context) + } + + /// Creates an inline flow from a set of inline fragments, then adds it as a child of the given + /// flow or pushes it onto the given flow list. + /// + /// `#[inline(always)]` because this is performance critical and LLVM will not inline it + /// otherwise. + #[inline(always)] + fn flush_inline_fragments_to_flow( + &mut self, + fragment_accumulator: InlineFragmentsAccumulator, + flow: &mut FlowRef, + absolute_descendants: &mut AbsoluteDescendants, + legalizer: &mut Legalizer, + node: &ConcreteThreadSafeLayoutNode, + ) { + let mut fragments = fragment_accumulator + .to_intermediate_inline_fragments::<ConcreteThreadSafeLayoutNode>(self.style_context()); + if fragments.is_empty() { + return; + }; + + strip_ignorable_whitespace_from_start(&mut fragments.fragments); + strip_ignorable_whitespace_from_end(&mut fragments.fragments); + if fragments.fragments.is_empty() { + absolute_descendants.push_descendants(fragments.absolute_descendants); + return; + } + + // Build a list of all the inline-block fragments before fragments is moved. + let mut inline_block_flows = vec![]; + for fragment in &fragments.fragments { + match fragment.specific { + SpecificFragmentInfo::InlineBlock(ref info) => { + inline_block_flows.push(info.flow_ref.clone()) + }, + SpecificFragmentInfo::InlineAbsoluteHypothetical(ref info) => { + inline_block_flows.push(info.flow_ref.clone()) + }, + SpecificFragmentInfo::InlineAbsolute(ref info) => { + inline_block_flows.push(info.flow_ref.clone()) + }, + _ => {}, + } + } + + // We must scan for runs before computing minimum ascent and descent because scanning + // for runs might collapse so much whitespace away that only hypothetical fragments + // remain. In that case the inline flow will compute its ascent and descent to be zero. + let scanned_fragments = + with_thread_local_font_context(self.layout_context, |font_context| { + TextRunScanner::new().scan_for_runs( + font_context, + mem::replace(&mut fragments.fragments, LinkedList::new()), + ) + }); + let mut inline_flow_ref = FlowRef::new(Arc::new(InlineFlow::from_fragments( + scanned_fragments, + node.style(self.style_context()).writing_mode, + ))); + + // Add all the inline-block fragments as children of the inline flow. + for inline_block_flow in &inline_block_flows { + inline_flow_ref.add_new_child(inline_block_flow.clone()); + } + + // Set up absolute descendants as necessary. + // + // The inline flow itself may need to become the containing block for absolute descendants + // in order to handle cases like: + // + // <div> + // <span style="position: relative"> + // <span style="position: absolute; ..."></span> + // </span> + // </div> + // + // See the comment above `flow::AbsoluteDescendantInfo` for more information. + inline_flow_ref.take_applicable_absolute_descendants(&mut fragments.absolute_descendants); + absolute_descendants.push_descendants(fragments.absolute_descendants); + + { + // FIXME(#6503): Use Arc::get_mut().unwrap() here. + let inline_flow = FlowRef::deref_mut(&mut inline_flow_ref).as_mut_inline(); + inline_flow.minimum_line_metrics = + with_thread_local_font_context(self.layout_context, |font_context| { + inline_flow + .minimum_line_metrics(font_context, &node.style(self.style_context())) + }); + } + + inline_flow_ref.finish(); + legalizer.add_child::<ConcreteThreadSafeLayoutNode::ConcreteElement>( + self.style_context(), + flow, + inline_flow_ref, + ) + } + + fn build_block_flow_using_construction_result_of_child( + &mut self, + flow: &mut FlowRef, + node: &ConcreteThreadSafeLayoutNode, + kid: ConcreteThreadSafeLayoutNode, + inline_fragment_accumulator: &mut InlineFragmentsAccumulator, + abs_descendants: &mut AbsoluteDescendants, + legalizer: &mut Legalizer, + ) { + match kid.get_construction_result() { + ConstructionResult::None => {}, + ConstructionResult::Flow(kid_flow, kid_abs_descendants) => { + // If kid_flow is TableCaptionFlow, kid_flow should be added under + // TableWrapperFlow. + if flow.is_table() && kid_flow.is_table_caption() { + let construction_result = + ConstructionResult::Flow(kid_flow, AbsoluteDescendants::new()); + self.set_flow_construction_result(&kid, construction_result) + } else { + if !kid_flow + .base() + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + // Flush any inline fragments that we were gathering up. This allows us to + // handle {ib} splits. + let old_inline_fragment_accumulator = mem::replace( + inline_fragment_accumulator, + InlineFragmentsAccumulator::new(), + ); + self.flush_inline_fragments_to_flow( + old_inline_fragment_accumulator, + flow, + abs_descendants, + legalizer, + node, + ); + } + legalizer.add_child::<ConcreteThreadSafeLayoutNode::ConcreteElement>( + self.style_context(), + flow, + kid_flow, + ) + } + abs_descendants.push_descendants(kid_abs_descendants); + }, + ConstructionResult::ConstructionItem(ConstructionItem::InlineFragments( + InlineFragmentsConstructionResult { + splits, + fragments: successor_fragments, + }, + )) => { + // Add any {ib} splits. + for split in splits { + // Pull apart the {ib} split object and push its predecessor fragments + // onto the list. + let InlineBlockSplit { + predecessors, + flow: kid_flow, + } = split; + inline_fragment_accumulator.push_all(predecessors); + + // Flush any inline fragments that we were gathering up. + debug!( + "flushing {} inline box(es) to flow A", + inline_fragment_accumulator.fragments.fragments.len() + ); + let old_inline_fragment_accumulator = mem::replace( + inline_fragment_accumulator, + InlineFragmentsAccumulator::new(), + ); + let absolute_descendants = + &mut inline_fragment_accumulator.fragments.absolute_descendants; + self.flush_inline_fragments_to_flow( + old_inline_fragment_accumulator, + flow, + absolute_descendants, + legalizer, + node, + ); + + // Push the flow generated by the {ib} split onto our list of flows. + legalizer.add_child::<ConcreteThreadSafeLayoutNode::ConcreteElement>( + self.style_context(), + flow, + kid_flow, + ) + } + + // Add the fragments to the list we're maintaining. + inline_fragment_accumulator.push_all(successor_fragments); + }, + ConstructionResult::ConstructionItem(ConstructionItem::Whitespace( + whitespace_node, + whitespace_pseudo, + whitespace_style, + whitespace_damage, + )) => { + // Add whitespace results. They will be stripped out later on when + // between block elements, and retained when between inline elements. + let fragment_info = SpecificFragmentInfo::UnscannedText(Box::new( + UnscannedTextFragmentInfo::new(Box::<str>::from(" "), None), + )); + let fragment = Fragment::from_opaque_node_and_style( + whitespace_node, + whitespace_pseudo, + whitespace_style, + node.selected_style(), + whitespace_damage, + fragment_info, + ); + inline_fragment_accumulator + .fragments + .fragments + .push_back(fragment); + }, + ConstructionResult::ConstructionItem(ConstructionItem::TableColumnFragment(_)) => { + // TODO: Implement anonymous table objects for missing parents + // CSS 2.1 § 17.2.1, step 3-2 + }, + } + } + + /// Constructs a block flow, beginning with the given `initial_fragments` if present and then + /// appending the construction results of children to the child list of the block flow. {ib} + /// splits and absolutely-positioned descendants are handled correctly. + fn build_flow_for_block_starting_with_fragments( + &mut self, + mut flow: FlowRef, + node: &ConcreteThreadSafeLayoutNode, + initial_fragments: IntermediateInlineFragments, + ) -> ConstructionResult { + // Gather up fragments for the inline flows we might need to create. + let mut inline_fragment_accumulator = InlineFragmentsAccumulator::new(); + + inline_fragment_accumulator + .fragments + .push_all(initial_fragments); + + // List of absolute descendants, in tree order. + let mut abs_descendants = AbsoluteDescendants::new(); + let mut legalizer = Legalizer::new(); + let is_media_element_with_widget = node.type_id() == + Some(LayoutNodeType::Element(LayoutElementType::HTMLMediaElement)) && + node.as_element().unwrap().is_shadow_host(); + if !node.is_replaced_content() || is_media_element_with_widget { + for kid in node.children() { + if kid.get_pseudo_element_type() != PseudoElementType::Normal { + if node.is_replaced_content() { + // Replaced elements don't have pseudo-elements per spec. + continue; + } + self.process(&kid); + } + + self.build_block_flow_using_construction_result_of_child( + &mut flow, + node, + kid, + &mut inline_fragment_accumulator, + &mut abs_descendants, + &mut legalizer, + ); + } + } + + // Perform a final flush of any inline fragments that we were gathering up to handle {ib} + // splits, after stripping ignorable whitespace. + self.flush_inline_fragments_to_flow( + inline_fragment_accumulator, + &mut flow, + &mut abs_descendants, + &mut legalizer, + node, + ); + + // The flow is done. + legalizer.finish(&mut flow); + flow.finish(); + + // Set up the absolute descendants. + if flow.is_absolute_containing_block() { + // This is the containing block for all the absolute descendants. + flow.set_absolute_descendants(abs_descendants); + + abs_descendants = AbsoluteDescendants::new(); + if flow + .base() + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + // This is now the only absolute flow in the subtree which hasn't yet + // reached its CB. + abs_descendants.push(flow.clone()); + } + } + ConstructionResult::Flow(flow, abs_descendants) + } + + /// Constructs a flow for the given block node and its children. This method creates an + /// initial fragment as appropriate and then dispatches to + /// `build_flow_for_block_starting_with_fragments`. Currently the following kinds of flows get + /// initial content: + /// + /// * Generated content gets the initial content specified by the `content` attribute of the + /// CSS. + /// * `<input>` and `<textarea>` elements get their content. + /// + /// FIXME(pcwalton): It is not clear to me that there isn't a cleaner way to handle + /// `<textarea>`. + fn build_flow_for_block_like( + &mut self, + flow: FlowRef, + node: &ConcreteThreadSafeLayoutNode, + ) -> ConstructionResult { + let mut fragments = IntermediateInlineFragments::new(); + let node_is_input_or_text_area = node.type_id() == + Some(LayoutNodeType::Element(LayoutElementType::HTMLInputElement)) || + node.type_id() == + Some(LayoutNodeType::Element( + LayoutElementType::HTMLTextAreaElement, + )); + if node.get_pseudo_element_type().is_replaced_content() || node_is_input_or_text_area { + // A TextArea's text contents are displayed through the input text + // box, so don't construct them. + if node.type_id() == + Some(LayoutNodeType::Element( + LayoutElementType::HTMLTextAreaElement, + )) + { + for kid in node.children() { + self.set_flow_construction_result(&kid, ConstructionResult::None) + } + } + + let context = self.style_context(); + let mut style = node.style(context); + style = context + .stylist + .style_for_anonymous::<ConcreteThreadSafeLayoutNode::ConcreteElement>( + &context.guards, + &PseudoElement::ServoText, + &style, + ); + if node_is_input_or_text_area { + style = context + .stylist + .style_for_anonymous::<ConcreteThreadSafeLayoutNode::ConcreteElement>( + &context.guards, + &PseudoElement::ServoInputText, + &style, + ) + } + + self.create_fragments_for_node_text_content(&mut fragments, node, &style) + } + self.build_flow_for_block_starting_with_fragments(flow, node, fragments) + } + + /// Pushes fragments appropriate for the content of the given node onto the given list. + fn create_fragments_for_node_text_content( + &self, + fragments: &mut IntermediateInlineFragments, + node: &ConcreteThreadSafeLayoutNode, + style: &ServoArc<ComputedValues>, + ) { + // Fast path: If there is no text content, return immediately. + let text_content = node.text_content(); + if text_content.is_empty() { + return; + } + + let style = (*style).clone(); + let selected_style = node.selected_style(); + + match text_content { + TextContent::Text(string) => { + let info = Box::new(UnscannedTextFragmentInfo::new(string, node.selection())); + let specific_fragment_info = SpecificFragmentInfo::UnscannedText(info); + fragments + .fragments + .push_back(Fragment::from_opaque_node_and_style( + node.opaque(), + node.get_pseudo_element_type(), + style, + selected_style, + node.restyle_damage(), + specific_fragment_info, + )) + }, + TextContent::GeneratedContent(content_items) => { + for content_item in content_items.into_iter() { + let specific_fragment_info = match content_item { + ContentItem::String(string) => { + let info = Box::new(UnscannedTextFragmentInfo::new(string, None)); + SpecificFragmentInfo::UnscannedText(info) + }, + content_item => { + let content_item = + Box::new(GeneratedContentInfo::ContentItem(content_item)); + SpecificFragmentInfo::GeneratedContent(content_item) + }, + }; + fragments + .fragments + .push_back(Fragment::from_opaque_node_and_style( + node.opaque(), + node.get_pseudo_element_type(), + style.clone(), + selected_style.clone(), + node.restyle_damage(), + specific_fragment_info, + )) + } + }, + } + } + + /// Builds a flow for a node with `display: block`. This yields a `BlockFlow` with possibly + /// other `BlockFlow`s or `InlineFlow`s underneath it, depending on whether {ib} splits needed + /// to happen. + fn build_flow_for_block( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + float_kind: Option<FloatKind>, + ) -> ConstructionResult { + if node.style(self.style_context()).is_multicol() { + return self.build_flow_for_multicol(node, float_kind); + } + + let fragment = self.build_fragment_for_block(node); + let flow = FlowRef::new(Arc::new(BlockFlow::from_fragment_and_float_kind( + fragment, float_kind, + ))); + self.build_flow_for_block_like(flow, node) + } + + /// Bubbles up {ib} splits. + fn accumulate_inline_block_splits( + &mut self, + splits: LinkedList<InlineBlockSplit>, + node: &ConcreteThreadSafeLayoutNode, + fragment_accumulator: &mut InlineFragmentsAccumulator, + opt_inline_block_splits: &mut LinkedList<InlineBlockSplit>, + ) { + for split in splits { + let InlineBlockSplit { + predecessors, + flow: kid_flow, + } = split; + fragment_accumulator.push_all(predecessors); + + opt_inline_block_splits.push_back(InlineBlockSplit::new( + fragment_accumulator, + node, + self.style_context(), + kid_flow, + )); + } + } + + /// Concatenates the fragments of kids, adding in our own borders/padding/margins if necessary. + /// Returns the `InlineFragmentsConstructionResult`, if any. There will be no + /// `InlineFragmentsConstructionResult` if this node consisted entirely of ignorable + /// whitespace. + fn build_fragments_for_nonreplaced_inline_content( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + ) -> ConstructionResult { + let mut opt_inline_block_splits: LinkedList<InlineBlockSplit> = LinkedList::new(); + let mut fragment_accumulator = + InlineFragmentsAccumulator::from_inline_node(node, self.style_context()); + fragment_accumulator.bidi_control_chars = + bidi_control_chars(&node.style(self.style_context())); + + let mut abs_descendants = AbsoluteDescendants::new(); + + // Concatenate all the fragments of our kids, creating {ib} splits as necessary. + let mut is_empty = true; + for kid in node.children() { + is_empty = false; + if kid.get_pseudo_element_type() != PseudoElementType::Normal { + self.process(&kid); + } + match kid.get_construction_result() { + ConstructionResult::None => {}, + ConstructionResult::Flow(flow, kid_abs_descendants) => { + if !flow + .base() + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + opt_inline_block_splits.push_back(InlineBlockSplit::new( + &mut fragment_accumulator, + node, + self.style_context(), + flow, + )); + abs_descendants.push_descendants(kid_abs_descendants); + } else { + // Push the absolutely-positioned kid as an inline containing block. + let kid_node = flow.as_block().fragment.node; + let kid_pseudo = flow.as_block().fragment.pseudo.clone(); + let kid_style = flow.as_block().fragment.style.clone(); + let kid_selected_style = flow.as_block().fragment.selected_style.clone(); + let kid_restyle_damage = flow.as_block().fragment.restyle_damage; + let fragment_info = SpecificFragmentInfo::InlineAbsolute( + InlineAbsoluteFragmentInfo::new(flow), + ); + fragment_accumulator.push(Fragment::from_opaque_node_and_style( + kid_node, + kid_pseudo, + kid_style, + kid_selected_style, + kid_restyle_damage, + fragment_info, + )); + fragment_accumulator + .fragments + .absolute_descendants + .push_descendants(kid_abs_descendants); + } + }, + ConstructionResult::ConstructionItem(ConstructionItem::InlineFragments( + InlineFragmentsConstructionResult { + splits, + fragments: successors, + }, + )) => { + // Bubble up {ib} splits. + self.accumulate_inline_block_splits( + splits, + node, + &mut fragment_accumulator, + &mut opt_inline_block_splits, + ); + + // Push residual fragments. + fragment_accumulator.push_all(successors); + }, + ConstructionResult::ConstructionItem(ConstructionItem::Whitespace( + whitespace_node, + whitespace_pseudo, + whitespace_style, + whitespace_damage, + )) => { + // Instantiate the whitespace fragment. + let fragment_info = SpecificFragmentInfo::UnscannedText(Box::new( + UnscannedTextFragmentInfo::new(Box::<str>::from(" "), None), + )); + let fragment = Fragment::from_opaque_node_and_style( + whitespace_node, + whitespace_pseudo, + whitespace_style, + node.selected_style(), + whitespace_damage, + fragment_info, + ); + fragment_accumulator.fragments.fragments.push_back(fragment) + }, + ConstructionResult::ConstructionItem(ConstructionItem::TableColumnFragment(_)) => { + // TODO: Implement anonymous table objects for missing parents + // CSS 2.1 § 17.2.1, step 3-2 + }, + } + } + + let node_style = node.style(self.style_context()); + if is_empty && has_padding_or_border(&node_style) { + // An empty inline box needs at least one fragment to draw its background and borders. + let info = SpecificFragmentInfo::UnscannedText(Box::new( + UnscannedTextFragmentInfo::new(Box::<str>::from(""), None), + )); + let fragment = Fragment::from_opaque_node_and_style( + node.opaque(), + node.get_pseudo_element_type(), + node_style.clone(), + node.selected_style(), + node.restyle_damage(), + info, + ); + fragment_accumulator.fragments.fragments.push_back(fragment) + } + + // Finally, make a new construction result. + if opt_inline_block_splits.len() > 0 || + !fragment_accumulator.fragments.is_empty() || + abs_descendants.len() > 0 + { + fragment_accumulator + .fragments + .absolute_descendants + .push_descendants(abs_descendants); + + // If the node is positioned, then it's the containing block for all absolutely- + // positioned descendants. + if node_style.get_box().position != Position::Static { + fragment_accumulator + .fragments + .absolute_descendants + .mark_as_having_reached_containing_block(); + } + + let construction_item = + ConstructionItem::InlineFragments(InlineFragmentsConstructionResult { + splits: opt_inline_block_splits, + fragments: fragment_accumulator + .to_intermediate_inline_fragments::<ConcreteThreadSafeLayoutNode>( + self.style_context(), + ), + }); + ConstructionResult::ConstructionItem(construction_item) + } else { + ConstructionResult::None + } + } + + /// Creates an `InlineFragmentsConstructionResult` for replaced content. Replaced content + /// doesn't render its children, so this just nukes a child's fragments and creates a + /// `Fragment`. + fn build_fragments_for_replaced_inline_content( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + ) -> ConstructionResult { + for kid in node.children() { + self.set_flow_construction_result(&kid, ConstructionResult::None) + } + + let context = self.style_context(); + let style = node.style(context); + // If this node is ignorable whitespace, bail out now. + if node.is_ignorable_whitespace(context) { + return ConstructionResult::ConstructionItem(ConstructionItem::Whitespace( + node.opaque(), + node.get_pseudo_element_type(), + context + .stylist + .style_for_anonymous::<ConcreteThreadSafeLayoutNode::ConcreteElement>( + &context.guards, + &PseudoElement::ServoText, + &style, + ), + node.restyle_damage(), + )); + } + + // If this is generated content, then we need to initialize the accumulator with the + // fragment corresponding to that content. Otherwise, just initialize with the ordinary + // fragment that needs to be generated for this inline node. + let mut fragments = IntermediateInlineFragments::new(); + match (node.get_pseudo_element_type(), node.type_id()) { + (_, Some(LayoutNodeType::Text)) => { + let text_style = context + .stylist + .style_for_anonymous::<ConcreteThreadSafeLayoutNode::ConcreteElement>( + &context.guards, + &PseudoElement::ServoText, + &style, + ); + self.create_fragments_for_node_text_content(&mut fragments, node, &text_style) + }, + (PseudoElementType::Normal, _) => { + fragments + .fragments + .push_back(self.build_fragment_for_block(node)); + }, + (_, _) => self.create_fragments_for_node_text_content(&mut fragments, node, &style), + } + + let construction_item = + ConstructionItem::InlineFragments(InlineFragmentsConstructionResult { + splits: LinkedList::new(), + fragments: fragments, + }); + ConstructionResult::ConstructionItem(construction_item) + } + + /// Build the fragment for an inline-block or inline-flex, based on the `display` flag + fn build_fragment_for_inline_block_or_inline_flex( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + display: Display, + ) -> ConstructionResult { + let block_flow_result = match display { + Display::InlineBlock => self.build_flow_for_block(node, None), + Display::InlineFlex => self.build_flow_for_flex(node, None), + _ => panic!("The flag should be inline-block or inline-flex"), + }; + let (block_flow, abs_descendants) = match block_flow_result { + ConstructionResult::Flow(block_flow, abs_descendants) => (block_flow, abs_descendants), + _ => unreachable!(), + }; + + let context = self.style_context(); + let style = node.style(context); + let style = context + .stylist + .style_for_anonymous::<ConcreteThreadSafeLayoutNode::ConcreteElement>( + &context.guards, + &PseudoElement::ServoInlineBlockWrapper, + &style, + ); + let fragment_info = + SpecificFragmentInfo::InlineBlock(InlineBlockFragmentInfo::new(block_flow)); + let fragment = Fragment::from_opaque_node_and_style( + node.opaque(), + node.get_pseudo_element_type(), + style, + node.selected_style(), + node.restyle_damage(), + fragment_info, + ); + + let mut fragment_accumulator = InlineFragmentsAccumulator::new(); + fragment_accumulator.fragments.fragments.push_back(fragment); + fragment_accumulator + .fragments + .absolute_descendants + .push_descendants(abs_descendants); + + let construction_item = + ConstructionItem::InlineFragments(InlineFragmentsConstructionResult { + splits: LinkedList::new(), + fragments: fragment_accumulator + .to_intermediate_inline_fragments::<ConcreteThreadSafeLayoutNode>(context), + }); + ConstructionResult::ConstructionItem(construction_item) + } + + /// This is an annoying case, because the computed `display` value is `block`, but the + /// hypothetical box is inline. + fn build_fragment_for_absolutely_positioned_inline( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + ) -> ConstructionResult { + let block_flow_result = self.build_flow_for_block(node, None); + let (block_flow, abs_descendants) = match block_flow_result { + ConstructionResult::Flow(block_flow, abs_descendants) => (block_flow, abs_descendants), + _ => unreachable!(), + }; + + let fragment_info = SpecificFragmentInfo::InlineAbsoluteHypothetical( + InlineAbsoluteHypotheticalFragmentInfo::new(block_flow), + ); + let style_context = self.style_context(); + let style = node.style(style_context); + let style = style_context + .stylist + .style_for_anonymous::<ConcreteThreadSafeLayoutNode::ConcreteElement>( + &style_context.guards, + &PseudoElement::ServoInlineAbsolute, + &style, + ); + let fragment = Fragment::from_opaque_node_and_style( + node.opaque(), + PseudoElementType::Normal, + style, + node.selected_style(), + node.restyle_damage(), + fragment_info, + ); + + let mut fragment_accumulator = + InlineFragmentsAccumulator::from_inline_node(node, self.style_context()); + fragment_accumulator.fragments.fragments.push_back(fragment); + fragment_accumulator + .fragments + .absolute_descendants + .push_descendants(abs_descendants); + + let construction_item = + ConstructionItem::InlineFragments(InlineFragmentsConstructionResult { + splits: LinkedList::new(), + fragments: fragment_accumulator + .to_intermediate_inline_fragments::<ConcreteThreadSafeLayoutNode>( + style_context, + ), + }); + ConstructionResult::ConstructionItem(construction_item) + } + + /// Builds one or more fragments for a node with `display: inline`. This yields an + /// `InlineFragmentsConstructionResult`. + fn build_fragments_for_inline( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + ) -> ConstructionResult { + // Is this node replaced content? + if !node.is_replaced_content() { + // Go to a path that concatenates our kids' fragments. + self.build_fragments_for_nonreplaced_inline_content(node) + } else { + // Otherwise, just nuke our kids' fragments, create our fragment if any, and be done + // with it. + self.build_fragments_for_replaced_inline_content(node) + } + } + + /// Places any table captions found under the given table wrapper, if the value of their + /// `caption-side` property is equal to the given `side`. + fn place_table_caption_under_table_wrapper_on_side( + &mut self, + table_wrapper_flow: &mut FlowRef, + node: &ConcreteThreadSafeLayoutNode, + side: CaptionSide, + ) { + // Only flows that are table captions are matched here. + for kid in node.children() { + match kid.get_construction_result() { + ConstructionResult::Flow(kid_flow, _) => { + if kid_flow.is_table_caption() && + kid_flow + .as_block() + .fragment + .style() + .get_inherited_table() + .caption_side == + side + { + table_wrapper_flow.add_new_child(kid_flow); + } + }, + ConstructionResult::None | ConstructionResult::ConstructionItem(_) => {}, + } + } + } + + /// Builds a flow for a node with `column-count` or `column-width` non-`auto`. + /// This yields a `MulticolFlow` with a single `MulticolColumnFlow` underneath it. + fn build_flow_for_multicol( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + float_kind: Option<FloatKind>, + ) -> ConstructionResult { + let fragment = Fragment::new(node, SpecificFragmentInfo::Multicol, self.layout_context); + let mut flow = FlowRef::new(Arc::new(MulticolFlow::from_fragment(fragment, float_kind))); + + let column_fragment = Fragment::new( + node, + SpecificFragmentInfo::MulticolColumn, + self.layout_context, + ); + let column_flow = + FlowRef::new(Arc::new(MulticolColumnFlow::from_fragment(column_fragment))); + + // First populate the column flow with its children. + let construction_result = self.build_flow_for_block_like(column_flow, node); + + let mut abs_descendants = AbsoluteDescendants::new(); + + if let ConstructionResult::Flow(column_flow, column_abs_descendants) = construction_result { + flow.add_new_child(column_flow); + abs_descendants.push_descendants(column_abs_descendants); + } + + // The flow is done. + flow.finish(); + if flow.is_absolute_containing_block() { + // This is the containing block for all the absolute descendants. + flow.set_absolute_descendants(abs_descendants); + + abs_descendants = AbsoluteDescendants::new(); + + if flow + .base() + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + // This is now the only absolute flow in the subtree which hasn't yet + // reached its containing block. + abs_descendants.push(flow.clone()); + } + } + + ConstructionResult::Flow(flow, abs_descendants) + } + + /// Builds a flow for a node with `display: table`. This yields a `TableWrapperFlow` with + /// possibly other `TableCaptionFlow`s or `TableFlow`s underneath it. + fn build_flow_for_table( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + float_value: Float, + ) -> ConstructionResult { + let mut legalizer = Legalizer::new(); + + let table_style; + let wrapper_style; + { + let context = self.style_context(); + table_style = node.style(context); + wrapper_style = context + .stylist + .style_for_anonymous::<ConcreteThreadSafeLayoutNode::ConcreteElement>( + &context.guards, + &PseudoElement::ServoTableWrapper, + &table_style, + ); + } + let wrapper_fragment = Fragment::from_opaque_node_and_style( + node.opaque(), + PseudoElementType::Normal, + wrapper_style, + node.selected_style(), + node.restyle_damage(), + SpecificFragmentInfo::TableWrapper, + ); + let wrapper_float_kind = FloatKind::from_property(float_value); + let mut wrapper_flow = FlowRef::new(Arc::new( + TableWrapperFlow::from_fragment_and_float_kind(wrapper_fragment, wrapper_float_kind), + )); + + let table_fragment = Fragment::new(node, SpecificFragmentInfo::Table, self.layout_context); + let table_flow = FlowRef::new(Arc::new(TableFlow::from_fragment(table_fragment))); + + // First populate the table flow with its children. + let construction_result = self.build_flow_for_block_like(table_flow, node); + + let mut abs_descendants = AbsoluteDescendants::new(); + + // The order of the caption and the table are not necessarily the same order as in the DOM + // tree. All caption blocks are placed before or after the table flow, depending on the + // value of `caption-side`. + self.place_table_caption_under_table_wrapper_on_side( + &mut wrapper_flow, + node, + CaptionSide::Top, + ); + + if let ConstructionResult::Flow(table_flow, table_abs_descendants) = construction_result { + legalizer.add_child::<ConcreteThreadSafeLayoutNode::ConcreteElement>( + self.style_context(), + &mut wrapper_flow, + table_flow, + ); + abs_descendants.push_descendants(table_abs_descendants); + } + + // If the value of `caption-side` is `bottom`, place it now. + self.place_table_caption_under_table_wrapper_on_side( + &mut wrapper_flow, + node, + CaptionSide::Bottom, + ); + + // The flow is done. + legalizer.finish(&mut wrapper_flow); + wrapper_flow.finish(); + + if wrapper_flow.is_absolute_containing_block() { + // This is the containing block for all the absolute descendants. + wrapper_flow.set_absolute_descendants(abs_descendants); + + abs_descendants = AbsoluteDescendants::new(); + + if wrapper_flow + .base() + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + // This is now the only absolute flow in the subtree which hasn't yet + // reached its containing block. + abs_descendants.push(wrapper_flow.clone()); + } + } + + ConstructionResult::Flow(wrapper_flow, abs_descendants) + } + + /// Builds a flow for a node with `display: table-caption`. This yields a `TableCaptionFlow` + /// with possibly other `BlockFlow`s or `InlineFlow`s underneath it. + fn build_flow_for_table_caption( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + ) -> ConstructionResult { + let fragment = self.build_fragment_for_block(node); + let flow = FlowRef::new(Arc::new(TableCaptionFlow::from_fragment(fragment))); + self.build_flow_for_block_like(flow, node) + } + + /// Builds a flow for a node with `display: table-row-group`. This yields a `TableRowGroupFlow` + /// with possibly other `TableRowFlow`s underneath it. + fn build_flow_for_table_rowgroup( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + ) -> ConstructionResult { + let fragment = Fragment::new(node, SpecificFragmentInfo::TableRow, self.layout_context); + let flow = FlowRef::new(Arc::new(TableRowGroupFlow::from_fragment(fragment))); + self.build_flow_for_block_like(flow, node) + } + + /// Builds a flow for a node with `display: table-row`. This yields a `TableRowFlow` with + /// possibly other `TableCellFlow`s underneath it. + fn build_flow_for_table_row( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + ) -> ConstructionResult { + let fragment = Fragment::new(node, SpecificFragmentInfo::TableRow, self.layout_context); + let flow = FlowRef::new(Arc::new(TableRowFlow::from_fragment(fragment))); + self.build_flow_for_block_like(flow, node) + } + + /// Builds a flow for a node with `display: table-cell`. This yields a `TableCellFlow` with + /// possibly other `BlockFlow`s or `InlineFlow`s underneath it. + fn build_flow_for_table_cell( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + ) -> ConstructionResult { + let fragment = Fragment::new(node, SpecificFragmentInfo::TableCell, self.layout_context); + + // Determine if the table cell should be hidden. Per CSS 2.1 § 17.6.1.1, this will be true + // if the cell has any in-flow elements (even empty ones!) and has `empty-cells` set to + // `hide`. + let hide = node + .style(self.style_context()) + .get_inherited_table() + .empty_cells == + EmptyCells::Hide && + node.children().all(|kid| { + let position = kid.style(self.style_context()).get_box().position; + !kid.is_content() || position == Position::Absolute || position == Position::Fixed + }); + + let flow = FlowRef::new(Arc::new( + TableCellFlow::from_node_fragment_and_visibility_flag(node, fragment, !hide), + )); + self.build_flow_for_block_like(flow, node) + } + + /// Builds a flow for a node with `display: list-item`. This yields a `ListItemFlow` with + /// possibly other `BlockFlow`s or `InlineFlow`s underneath it. + fn build_flow_for_list_item( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + flotation: Float, + ) -> ConstructionResult { + let flotation = FloatKind::from_property(flotation); + let marker_fragments = match node.style(self.style_context()).get_list().list_style_image { + ImageUrlOrNone::Url(ref url_value) => { + let image_info = Box::new(ImageFragmentInfo::new( + url_value.url().map(|u| u.clone()), + None, + node, + &self.layout_context, + )); + vec![Fragment::new( + node, + SpecificFragmentInfo::Image(image_info), + self.layout_context, + )] + }, + ImageUrlOrNone::None => match ListStyleTypeContent::from_list_style_type( + node.style(self.style_context()).get_list().list_style_type, + ) { + ListStyleTypeContent::None => Vec::new(), + ListStyleTypeContent::StaticText(ch) => { + let text = format!("{}\u{a0}", ch); + let mut unscanned_marker_fragments = LinkedList::new(); + unscanned_marker_fragments.push_back(Fragment::new( + node, + SpecificFragmentInfo::UnscannedText(Box::new( + UnscannedTextFragmentInfo::new(Box::<str>::from(text), None), + )), + self.layout_context, + )); + let marker_fragments = + with_thread_local_font_context(self.layout_context, |mut font_context| { + TextRunScanner::new() + .scan_for_runs(&mut font_context, unscanned_marker_fragments) + }); + marker_fragments.fragments + }, + ListStyleTypeContent::GeneratedContent(info) => vec![Fragment::new( + node, + SpecificFragmentInfo::GeneratedContent(info), + self.layout_context, + )], + }, + }; + + // If the list marker is outside, it becomes the special "outside fragment" that list item + // flows have. If it's inside, it's just a plain old fragment. Note that this means that + // we adopt Gecko's behavior rather than WebKit's when the marker causes an {ib} split, + // which has caused some malaise (Bugzilla #36854) but CSS 2.1 § 12.5.1 lets me do it, so + // there. + let mut initial_fragments = IntermediateInlineFragments::new(); + let main_fragment = self.build_fragment_for_block(node); + let flow = match node + .style(self.style_context()) + .get_list() + .list_style_position + { + ListStylePosition::Outside => Arc::new(ListItemFlow::from_fragments_and_flotation( + main_fragment, + marker_fragments, + flotation, + )), + ListStylePosition::Inside => { + for marker_fragment in marker_fragments { + initial_fragments.fragments.push_back(marker_fragment) + } + Arc::new(ListItemFlow::from_fragments_and_flotation( + main_fragment, + vec![], + flotation, + )) + }, + }; + + self.build_flow_for_block_starting_with_fragments( + FlowRef::new(flow), + node, + initial_fragments, + ) + } + + /// Creates a fragment for a node with `display: table-column`. + fn build_fragments_for_table_column( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + ) -> ConstructionResult { + // CSS 2.1 § 17.2.1. Treat all child fragments of a `table-column` as `display: none`. + for kid in node.children() { + self.set_flow_construction_result(&kid, ConstructionResult::None) + } + + let specific = SpecificFragmentInfo::TableColumn(TableColumnFragmentInfo::new(node)); + let construction_item = ConstructionItem::TableColumnFragment(Fragment::new( + node, + specific, + self.layout_context, + )); + ConstructionResult::ConstructionItem(construction_item) + } + + /// Builds a flow for a node with `display: table-column-group`. + /// This yields a `TableColGroupFlow`. + fn build_flow_for_table_colgroup( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + ) -> ConstructionResult { + let fragment = Fragment::new( + node, + SpecificFragmentInfo::TableColumn(TableColumnFragmentInfo::new(node)), + self.layout_context, + ); + let mut col_fragments = vec![]; + for kid in node.children() { + // CSS 2.1 § 17.2.1. Treat all non-column child fragments of `table-column-group` + // as `display: none`. + if let ConstructionResult::ConstructionItem(ConstructionItem::TableColumnFragment( + fragment, + )) = kid.get_construction_result() + { + col_fragments.push(fragment) + } + } + if col_fragments.is_empty() { + debug!("add SpecificFragmentInfo::TableColumn for empty colgroup"); + let specific = SpecificFragmentInfo::TableColumn(TableColumnFragmentInfo::new(node)); + col_fragments.push(Fragment::new(node, specific, self.layout_context)); + } + let mut flow = FlowRef::new(Arc::new(TableColGroupFlow::from_fragments( + fragment, + col_fragments, + ))); + flow.finish(); + + ConstructionResult::Flow(flow, AbsoluteDescendants::new()) + } + + /// Builds a flow for a node with 'display: flex'. + fn build_flow_for_flex( + &mut self, + node: &ConcreteThreadSafeLayoutNode, + float_kind: Option<FloatKind>, + ) -> ConstructionResult { + let fragment = self.build_fragment_for_block(node); + let flow = FlowRef::new(Arc::new(FlexFlow::from_fragment(fragment, float_kind))); + self.build_flow_for_block_like(flow, node) + } + + /// Attempts to perform incremental repair to account for recent changes to this node. This + /// can fail and return false, indicating that flows will need to be reconstructed. + /// + /// TODO(pcwalton): Add some more fast paths, like toggling `display: none`, adding block kids + /// to block parents with no {ib} splits, adding out-of-flow kids, etc. + pub fn repair_if_possible(&mut self, node: &ConcreteThreadSafeLayoutNode) -> bool { + // We can skip reconstructing the flow if we don't have to reconstruct and none of our kids + // did either. + // + // We visit the kids first and reset their HAS_NEWLY_CONSTRUCTED_FLOW flags after checking + // them. NOTE: Make sure not to bail out early before resetting all the flags! + let mut need_to_reconstruct = false; + + // If the node has display: none, it's possible that we haven't even + // styled the children once, so we need to bailout early here. + if node.style(self.style_context()).get_box().clone_display() == Display::None { + return false; + } + + for kid in node.children() { + if kid + .flags() + .contains(LayoutDataFlags::HAS_NEWLY_CONSTRUCTED_FLOW) + { + kid.remove_flags(LayoutDataFlags::HAS_NEWLY_CONSTRUCTED_FLOW); + need_to_reconstruct = true + } + } + + if need_to_reconstruct { + return false; + } + + if node + .restyle_damage() + .contains(ServoRestyleDamage::RECONSTRUCT_FLOW) + { + return false; + } + + let mut set_has_newly_constructed_flow_flag = false; + let result = { + let style = node.style(self.style_context()); + + if style.can_be_fragmented() || style.is_multicol() { + return false; + } + + let damage = node.restyle_damage(); + let mut data = node.mutate_layout_data().unwrap(); + + match *node.construction_result_mut(&mut *data) { + ConstructionResult::None => true, + ConstructionResult::Flow(ref mut flow, _) => { + // The node's flow is of the same type and has the same set of children and can + // therefore be repaired by simply propagating damage and style to the flow. + if !flow.is_block_flow() { + return false; + } + + let flow = FlowRef::deref_mut(flow); + flow.mut_base().restyle_damage.insert(damage); + flow.repair_style_and_bubble_inline_sizes(&style); + true + }, + ConstructionResult::ConstructionItem(ConstructionItem::InlineFragments( + ref mut inline_fragments_construction_result, + )) => { + if !inline_fragments_construction_result.splits.is_empty() { + return false; + } + + for fragment in inline_fragments_construction_result + .fragments + .fragments + .iter_mut() + { + // Only mutate the styles of fragments that represent the dirty node (including + // pseudo-element). + if fragment.node != node.opaque() { + continue; + } + if fragment.pseudo != node.get_pseudo_element_type() { + continue; + } + + match fragment.specific { + SpecificFragmentInfo::InlineBlock(ref mut inline_block_fragment) => { + let flow_ref = + FlowRef::deref_mut(&mut inline_block_fragment.flow_ref); + flow_ref.mut_base().restyle_damage.insert(damage); + // FIXME(pcwalton): Fragment restyle damage too? + flow_ref.repair_style_and_bubble_inline_sizes(&style); + }, + SpecificFragmentInfo::InlineAbsoluteHypothetical( + ref mut inline_absolute_hypothetical_fragment, + ) => { + let flow_ref = FlowRef::deref_mut( + &mut inline_absolute_hypothetical_fragment.flow_ref, + ); + flow_ref.mut_base().restyle_damage.insert(damage); + // FIXME(pcwalton): Fragment restyle damage too? + flow_ref.repair_style_and_bubble_inline_sizes(&style); + }, + SpecificFragmentInfo::InlineAbsolute( + ref mut inline_absolute_fragment, + ) => { + let flow_ref = + FlowRef::deref_mut(&mut inline_absolute_fragment.flow_ref); + flow_ref.mut_base().restyle_damage.insert(damage); + // FIXME(pcwalton): Fragment restyle damage too? + flow_ref.repair_style_and_bubble_inline_sizes(&style); + }, + SpecificFragmentInfo::ScannedText(_) => { + // Text fragments in ConstructionResult haven't been scanned yet + unreachable!() + }, + SpecificFragmentInfo::GeneratedContent(_) | + SpecificFragmentInfo::UnscannedText(_) => { + // We can't repair this unscanned text; we need to update the + // scanned text fragments. + // + // TODO: Add code to find and repair the ScannedText fragments? + return false; + }, + _ => { + fragment.repair_style(&style); + set_has_newly_constructed_flow_flag = true; + }, + } + } + true + }, + ConstructionResult::ConstructionItem(_) => false, + } + }; + if set_has_newly_constructed_flow_flag { + node.insert_flags(LayoutDataFlags::HAS_NEWLY_CONSTRUCTED_FLOW); + } + return result; + } +} + +impl<'a, ConcreteThreadSafeLayoutNode> PostorderNodeMutTraversal<ConcreteThreadSafeLayoutNode> + for FlowConstructor<'a, ConcreteThreadSafeLayoutNode> +where + ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode, +{ + // Construct Flow based on 'display', 'position', and 'float' values. + // + // CSS 2.1 Section 9.7 + // + // TODO: This should actually consult the table in that section to get the + // final computed value for 'display'. + fn process(&mut self, node: &ConcreteThreadSafeLayoutNode) { + node.insert_flags(LayoutDataFlags::HAS_NEWLY_CONSTRUCTED_FLOW); + + let style = node.style(self.style_context()); + + // Bail out if this node is display: none. The style system guarantees + // that we don't arrive here for children of those. + if style.get_box().display.is_none() { + self.set_flow_construction_result(node, ConstructionResult::None); + return; + } + + // Get the `display` property for this node, and determine whether this node is floated. + let (display, float, positioning) = match node.type_id() { + None => { + // Pseudo-element. + ( + style.get_box().display, + style.get_box().float, + style.get_box().position, + ) + }, + Some(LayoutNodeType::Element(_)) => { + let original_display = style.get_box().original_display; + // FIXME(emilio, #19771): This munged_display business is pretty + // wrong. After we fix this we should be able to unify the + // pseudo-element path too. + let munged_display = match original_display { + Display::Inline | Display::InlineBlock => original_display, + _ => style.get_box().display, + }; + ( + munged_display, + style.get_box().float, + style.get_box().position, + ) + }, + Some(LayoutNodeType::Text) => (Display::Inline, Float::None, Position::Static), + }; + + debug!( + "building flow for node: {:?} {:?} {:?} {:?}", + display, + float, + positioning, + node.type_id() + ); + + // Switch on display and floatedness. + match (display, float, positioning) { + // `display: none` contributes no flow construction result. + (Display::None, _, _) => { + self.set_flow_construction_result(node, ConstructionResult::None); + }, + + // Table items contribute table flow construction results. + (Display::Table, float_value, _) => { + let construction_result = self.build_flow_for_table(node, float_value); + self.set_flow_construction_result(node, construction_result) + }, + + // Absolutely positioned elements will have computed value of + // `float` as 'none' and `display` as per the table. + // Only match here for block items. If an item is absolutely + // positioned, but inline we shouldn't try to construct a block + // flow here - instead, let it match the inline case + // below. + (Display::Block, _, Position::Absolute) | (Display::Block, _, Position::Fixed) => { + let construction_result = self.build_flow_for_block(node, None); + self.set_flow_construction_result(node, construction_result) + }, + + // List items contribute their own special flows. + (Display::ListItem, float_value, _) => { + let construction_result = self.build_flow_for_list_item(node, float_value); + self.set_flow_construction_result(node, construction_result) + }, + + // Inline items that are absolutely-positioned contribute inline fragment construction + // results with a hypothetical fragment. + (Display::Inline, _, Position::Absolute) | + (Display::InlineBlock, _, Position::Absolute) => { + let construction_result = + self.build_fragment_for_absolutely_positioned_inline(node); + self.set_flow_construction_result(node, construction_result) + }, + + // Inline items contribute inline fragment construction results. + // + // FIXME(pcwalton, #3307): This is not sufficient to handle floated generated content. + (Display::Inline, Float::None, _) => { + let construction_result = self.build_fragments_for_inline(node); + self.set_flow_construction_result(node, construction_result) + }, + + // Inline-block items contribute inline fragment construction results. + (Display::InlineBlock, Float::None, _) => { + let construction_result = + self.build_fragment_for_inline_block_or_inline_flex(node, Display::InlineBlock); + self.set_flow_construction_result(node, construction_result) + }, + + // Table items contribute table flow construction results. + (Display::TableCaption, _, _) => { + let construction_result = self.build_flow_for_table_caption(node); + self.set_flow_construction_result(node, construction_result) + }, + + // Table items contribute table flow construction results. + (Display::TableColumnGroup, _, _) => { + let construction_result = self.build_flow_for_table_colgroup(node); + self.set_flow_construction_result(node, construction_result) + }, + + // Table items contribute table flow construction results. + (Display::TableColumn, _, _) => { + let construction_result = self.build_fragments_for_table_column(node); + self.set_flow_construction_result(node, construction_result) + }, + + // Table items contribute table flow construction results. + (Display::TableRowGroup, _, _) | + (Display::TableHeaderGroup, _, _) | + (Display::TableFooterGroup, _, _) => { + let construction_result = self.build_flow_for_table_rowgroup(node); + self.set_flow_construction_result(node, construction_result) + }, + + // Table items contribute table flow construction results. + (Display::TableRow, _, _) => { + let construction_result = self.build_flow_for_table_row(node); + self.set_flow_construction_result(node, construction_result) + }, + + // Table items contribute table flow construction results. + (Display::TableCell, _, _) => { + let construction_result = self.build_flow_for_table_cell(node); + self.set_flow_construction_result(node, construction_result) + }, + + // Flex items contribute flex flow construction results. + (Display::Flex, float_value, _) => { + let float_kind = FloatKind::from_property(float_value); + let construction_result = self.build_flow_for_flex(node, float_kind); + self.set_flow_construction_result(node, construction_result) + }, + + (Display::InlineFlex, _, _) => { + let construction_result = + self.build_fragment_for_inline_block_or_inline_flex(node, Display::InlineFlex); + self.set_flow_construction_result(node, construction_result) + }, + + // Block flows that are not floated contribute block flow construction results. + // + // TODO(pcwalton): Make this only trigger for blocks and handle the other `display` + // properties separately. + (_, float_value, _) => { + let float_kind = FloatKind::from_property(float_value); + let construction_result = self.build_flow_for_block(node, float_kind); + self.set_flow_construction_result(node, construction_result) + }, + } + } +} + +/// A utility trait with some useful methods for node queries. +trait NodeUtils { + /// Returns true if this node doesn't render its kids and false otherwise. + fn is_replaced_content(&self) -> bool; + + fn construction_result_mut(self, layout_data: &mut LayoutData) -> &mut ConstructionResult; + + /// Sets the construction result of a flow. + fn set_flow_construction_result(self, result: ConstructionResult); + + /// Returns the construction result for this node. + fn get_construction_result(self) -> ConstructionResult; +} + +impl<ConcreteThreadSafeLayoutNode> NodeUtils for ConcreteThreadSafeLayoutNode +where + ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode, +{ + fn is_replaced_content(&self) -> bool { + match self.type_id() { + Some(LayoutNodeType::Text) | + Some(LayoutNodeType::Element(LayoutElementType::HTMLImageElement)) | + Some(LayoutNodeType::Element(LayoutElementType::HTMLMediaElement)) | + Some(LayoutNodeType::Element(LayoutElementType::HTMLIFrameElement)) | + Some(LayoutNodeType::Element(LayoutElementType::HTMLCanvasElement)) | + Some(LayoutNodeType::Element(LayoutElementType::SVGSVGElement)) => true, + Some(LayoutNodeType::Element(LayoutElementType::HTMLObjectElement)) => { + let elem = self.as_element().unwrap(); + let type_and_data = ( + elem.get_attr(&ns!(), &local_name!("type")), + elem.get_attr(&ns!(), &local_name!("data")), + ); + match type_and_data { + (None, Some(uri)) => is_image_data(uri), + _ => false, + } + }, + Some(LayoutNodeType::Element(_)) => false, + None => self.get_pseudo_element_type().is_replaced_content(), + } + } + + fn construction_result_mut(self, data: &mut LayoutData) -> &mut ConstructionResult { + match self.get_pseudo_element_type() { + PseudoElementType::Before => &mut data.before_flow_construction_result, + PseudoElementType::After => &mut data.after_flow_construction_result, + PseudoElementType::DetailsSummary => &mut data.details_summary_flow_construction_result, + PseudoElementType::DetailsContent => &mut data.details_content_flow_construction_result, + PseudoElementType::Normal => &mut data.flow_construction_result, + } + } + + #[inline(always)] + fn set_flow_construction_result(self, result: ConstructionResult) { + let mut layout_data = self.mutate_layout_data().unwrap(); + let dst = self.construction_result_mut(&mut *layout_data); + *dst = result; + } + + #[inline(always)] + fn get_construction_result(self) -> ConstructionResult { + let mut layout_data = self.mutate_layout_data().unwrap(); + self.construction_result_mut(&mut *layout_data).get() + } +} + +impl FlowRef { + /// Adds a new flow as a child of this flow. Fails if this flow is marked as a leaf. + fn add_new_child(&mut self, mut new_child: FlowRef) { + { + let kid_base = FlowRef::deref_mut(&mut new_child).mut_base(); + kid_base.parallel.parent = parallel::mut_owned_flow_to_unsafe_flow(self); + } + + let base = FlowRef::deref_mut(self).mut_base(); + base.children.push_back(new_child); + let _ = base.parallel.children_count.fetch_add(1, Ordering::Relaxed); + } + + /// Finishes a flow. Once a flow is finished, no more child flows or fragments may be added to + /// it. This will normally run the bubble-inline-sizes (minimum and preferred -- i.e. intrinsic + /// -- inline-size) calculation, unless the global `bubble_inline-sizes_separately` flag is on. + /// + /// All flows must be finished at some point, or they will not have their intrinsic inline-sizes + /// properly computed. (This is not, however, a memory safety problem.) + fn finish(&mut self) { + if !opts::get().bubble_inline_sizes_separately { + FlowRef::deref_mut(self).bubble_inline_sizes(); + FlowRef::deref_mut(self) + .mut_base() + .restyle_damage + .remove(ServoRestyleDamage::BUBBLE_ISIZES); + } + } +} + +/// Strips ignorable whitespace from the start of a list of fragments. +pub fn strip_ignorable_whitespace_from_start(this: &mut LinkedList<Fragment>) { + if this.is_empty() { + return; // Fast path. + } + + let mut leading_fragments_consisting_of_solely_bidi_control_characters = LinkedList::new(); + while !this.is_empty() { + match this + .front_mut() + .as_mut() + .unwrap() + .strip_leading_whitespace_if_necessary() + { + WhitespaceStrippingResult::RetainFragment => break, + WhitespaceStrippingResult::FragmentContainedOnlyBidiControlCharacters => { + leading_fragments_consisting_of_solely_bidi_control_characters + .push_back(this.pop_front().unwrap()) + }, + WhitespaceStrippingResult::FragmentContainedOnlyWhitespace => { + let removed_fragment = this.pop_front().unwrap(); + if let Some(ref mut remaining_fragment) = this.front_mut() { + remaining_fragment.meld_with_prev_inline_fragment(&removed_fragment); + } + }, + } + } + prepend_from( + this, + &mut leading_fragments_consisting_of_solely_bidi_control_characters, + ); +} + +/// Strips ignorable whitespace from the end of a list of fragments. +pub fn strip_ignorable_whitespace_from_end(this: &mut LinkedList<Fragment>) { + if this.is_empty() { + return; + } + + let mut trailing_fragments_consisting_of_solely_bidi_control_characters = LinkedList::new(); + while !this.is_empty() { + match this + .back_mut() + .as_mut() + .unwrap() + .strip_trailing_whitespace_if_necessary() + { + WhitespaceStrippingResult::RetainFragment => break, + WhitespaceStrippingResult::FragmentContainedOnlyBidiControlCharacters => { + trailing_fragments_consisting_of_solely_bidi_control_characters + .push_front(this.pop_back().unwrap()) + }, + WhitespaceStrippingResult::FragmentContainedOnlyWhitespace => { + let removed_fragment = this.pop_back().unwrap(); + if let Some(ref mut remaining_fragment) = this.back_mut() { + remaining_fragment.meld_with_next_inline_fragment(&removed_fragment); + } + }, + } + } + this.append(&mut trailing_fragments_consisting_of_solely_bidi_control_characters); +} + +/// If the 'unicode-bidi' property has a value other than 'normal', return the bidi control codes +/// to inject before and after the text content of the element. +fn bidi_control_chars(style: &ServoArc<ComputedValues>) -> Option<(&'static str, &'static str)> { + use style::computed_values::direction::T::*; + use style::computed_values::unicode_bidi::T::*; + + let unicode_bidi = style.get_text().unicode_bidi; + let direction = style.get_inherited_box().direction; + + // See the table in http://dev.w3.org/csswg/css-writing-modes/#unicode-bidi + match (unicode_bidi, direction) { + (Normal, _) => None, + (Embed, Ltr) => Some(("\u{202A}", "\u{202C}")), + (Embed, Rtl) => Some(("\u{202B}", "\u{202C}")), + (Isolate, Ltr) => Some(("\u{2066}", "\u{2069}")), + (Isolate, Rtl) => Some(("\u{2067}", "\u{2069}")), + (BidiOverride, Ltr) => Some(("\u{202D}", "\u{202C}")), + (BidiOverride, Rtl) => Some(("\u{202E}", "\u{202C}")), + (IsolateOverride, Ltr) => Some(("\u{2068}\u{202D}", "\u{202C}\u{2069}")), + (IsolateOverride, Rtl) => Some(("\u{2068}\u{202E}", "\u{202C}\u{2069}")), + (Plaintext, _) => Some(("\u{2068}", "\u{2069}")), + } +} + +fn control_chars_to_fragment<E>( + node: &InlineFragmentNodeInfo, + context: &SharedStyleContext, + text: &str, + restyle_damage: RestyleDamage, +) -> Fragment +where + E: TElement, +{ + let info = SpecificFragmentInfo::UnscannedText(Box::new(UnscannedTextFragmentInfo::new( + Box::<str>::from(text), + None, + ))); + let text_style = context.stylist.style_for_anonymous::<E>( + &context.guards, + &PseudoElement::ServoText, + &node.style, + ); + + Fragment::from_opaque_node_and_style( + node.address, + node.pseudo, + text_style, + node.selected_style.clone(), + restyle_damage, + info, + ) +} + +/// Returns true if this node has non-zero padding or border. +fn has_padding_or_border(values: &ComputedValues) -> bool { + let padding = values.get_padding(); + let border = values.get_border(); + + !padding.padding_top.is_definitely_zero() || + !padding.padding_right.is_definitely_zero() || + !padding.padding_bottom.is_definitely_zero() || + !padding.padding_left.is_definitely_zero() || + border.border_top_width.px() != 0. || + border.border_right_width.px() != 0. || + border.border_bottom_width.px() != 0. || + border.border_left_width.px() != 0. +} + +/// Maintains a stack of anonymous boxes needed to ensure that the flow tree is *legal*. The tree +/// is legal if it follows the rules in CSS 2.1 § 17.2.1. +/// +/// As an example, the legalizer makes sure that table row flows contain only table cells. If the +/// flow constructor attempts to place, say, a block flow directly underneath the table row, the +/// legalizer generates an anonymous table cell in between to hold the block. +/// +/// Generally, the flow constructor should use `Legalizer::add_child()` instead of calling +/// `Flow::add_new_child()` directly. This ensures that the flow tree remains legal at all times +/// and centralizes the anonymous flow generation logic in one place. +struct Legalizer { + /// A stack of anonymous flows that have yet to be finalized (i.e. that still could acquire new + /// children). + stack: Vec<FlowRef>, +} + +impl Legalizer { + /// Creates a new legalizer. + fn new() -> Legalizer { + Legalizer { stack: vec![] } + } + + /// Makes the `child` flow a new child of `parent`. Anonymous flows are automatically inserted + /// to keep the tree legal. + fn add_child<E>( + &mut self, + context: &SharedStyleContext, + parent: &mut FlowRef, + mut child: FlowRef, + ) where + E: TElement, + { + while !self.stack.is_empty() { + if self.try_to_add_child::<E>(context, parent, &mut child) { + return; + } + self.flush_top_of_stack(parent) + } + + while !self.try_to_add_child::<E>(context, parent, &mut child) { + self.push_next_anonymous_flow::<E>(context, parent) + } + } + + /// Flushes all flows we've been gathering up. + fn finish(mut self, parent: &mut FlowRef) { + while !self.stack.is_empty() { + self.flush_top_of_stack(parent) + } + } + + /// Attempts to make `child` a child of `parent`. On success, this returns true. If this would + /// make the tree illegal, this method does nothing and returns false. + /// + /// This method attempts to create anonymous blocks in between `parent` and `child` if and only + /// if those blocks will only ever have `child` as their sole child. At present, this is only + /// true for anonymous block children of flex flows. + fn try_to_add_child<E>( + &mut self, + context: &SharedStyleContext, + parent: &mut FlowRef, + child: &mut FlowRef, + ) -> bool + where + E: TElement, + { + let parent = self.stack.last_mut().unwrap_or(parent); + let (parent_class, child_class) = (parent.class(), child.class()); + match (parent_class, child_class) { + (FlowClass::TableWrapper, FlowClass::Table) | + (FlowClass::Table, FlowClass::TableColGroup) | + (FlowClass::Table, FlowClass::TableRowGroup) | + (FlowClass::Table, FlowClass::TableRow) | + (FlowClass::Table, FlowClass::TableCaption) | + (FlowClass::TableRowGroup, FlowClass::TableRow) | + (FlowClass::TableRow, FlowClass::TableCell) => { + parent.add_new_child((*child).clone()); + true + }, + + (FlowClass::TableWrapper, _) | + (FlowClass::Table, _) | + (FlowClass::TableRowGroup, _) | + (FlowClass::TableRow, _) | + (_, FlowClass::Table) | + (_, FlowClass::TableColGroup) | + (_, FlowClass::TableRowGroup) | + (_, FlowClass::TableRow) | + (_, FlowClass::TableCaption) | + (_, FlowClass::TableCell) => false, + + (FlowClass::Flex, FlowClass::Inline) => { + FlowRef::deref_mut(child) + .mut_base() + .flags + .insert(FlowFlags::MARGINS_CANNOT_COLLAPSE); + let mut block_wrapper = Legalizer::create_anonymous_flow::<E, _>( + context, + parent, + &[PseudoElement::ServoAnonymousBlock], + SpecificFragmentInfo::Generic, + BlockFlow::from_fragment, + ); + + { + let flag = if parent.as_flex().main_mode() == Direction::Inline { + FragmentFlags::IS_INLINE_FLEX_ITEM + } else { + FragmentFlags::IS_BLOCK_FLEX_ITEM + }; + let block = FlowRef::deref_mut(&mut block_wrapper).as_mut_block(); + block.base.flags.insert(FlowFlags::MARGINS_CANNOT_COLLAPSE); + block.fragment.flags.insert(flag); + } + block_wrapper.add_new_child((*child).clone()); + block_wrapper.finish(); + parent.add_new_child(block_wrapper); + true + }, + + (FlowClass::Flex, _) => { + { + let flag = if parent.as_flex().main_mode() == Direction::Inline { + FragmentFlags::IS_INLINE_FLEX_ITEM + } else { + FragmentFlags::IS_BLOCK_FLEX_ITEM + }; + let block = FlowRef::deref_mut(child).as_mut_block(); + block.base.flags.insert(FlowFlags::MARGINS_CANNOT_COLLAPSE); + block.fragment.flags.insert(flag); + } + parent.add_new_child((*child).clone()); + true + }, + + _ => { + parent.add_new_child((*child).clone()); + true + }, + } + } + + /// Finalizes the flow on the top of the stack. + fn flush_top_of_stack(&mut self, parent: &mut FlowRef) { + let mut child = self.stack.pop().expect("flush_top_of_stack(): stack empty"); + child.finish(); + self.stack.last_mut().unwrap_or(parent).add_new_child(child) + } + + /// Adds the anonymous flow that would be necessary to make an illegal child of `parent` legal + /// to the stack. + fn push_next_anonymous_flow<E>(&mut self, context: &SharedStyleContext, parent: &FlowRef) + where + E: TElement, + { + let parent_class = self.stack.last().unwrap_or(parent).class(); + match parent_class { + FlowClass::TableRow => self.push_new_anonymous_flow::<E, _>( + context, + parent, + &[PseudoElement::ServoAnonymousTableCell], + SpecificFragmentInfo::TableCell, + TableCellFlow::from_fragment, + ), + FlowClass::Table | FlowClass::TableRowGroup => self.push_new_anonymous_flow::<E, _>( + context, + parent, + &[PseudoElement::ServoAnonymousTableRow], + SpecificFragmentInfo::TableRow, + TableRowFlow::from_fragment, + ), + FlowClass::TableWrapper => self.push_new_anonymous_flow::<E, _>( + context, + parent, + &[PseudoElement::ServoAnonymousTable], + SpecificFragmentInfo::Table, + TableFlow::from_fragment, + ), + _ => self.push_new_anonymous_flow::<E, _>( + context, + parent, + &[ + PseudoElement::ServoTableWrapper, + PseudoElement::ServoAnonymousTableWrapper, + ], + SpecificFragmentInfo::TableWrapper, + TableWrapperFlow::from_fragment, + ), + } + } + + /// Creates an anonymous flow and pushes it onto the stack. + fn push_new_anonymous_flow<E, F>( + &mut self, + context: &SharedStyleContext, + reference: &FlowRef, + pseudos: &[PseudoElement], + specific_fragment_info: SpecificFragmentInfo, + constructor: fn(Fragment) -> F, + ) where + E: TElement, + F: Flow, + { + let new_flow = Self::create_anonymous_flow::<E, _>( + context, + reference, + pseudos, + specific_fragment_info, + constructor, + ); + self.stack.push(new_flow) + } + + /// Creates a new anonymous flow. The new flow is identical to `reference` except with all + /// styles applying to every pseudo-element in `pseudos` applied. + /// + /// This method invokes the supplied constructor function on the given specific fragment info + /// in order to actually generate the flow. + fn create_anonymous_flow<E, F>( + context: &SharedStyleContext, + reference: &FlowRef, + pseudos: &[PseudoElement], + specific_fragment_info: SpecificFragmentInfo, + constructor: fn(Fragment) -> F, + ) -> FlowRef + where + E: TElement, + F: Flow, + { + let reference_block = reference.as_block(); + let mut new_style = reference_block.fragment.style.clone(); + for pseudo in pseudos { + new_style = + context + .stylist + .style_for_anonymous::<E>(&context.guards, pseudo, &new_style); + } + let fragment = reference_block + .fragment + .create_similar_anonymous_fragment(new_style, specific_fragment_info); + FlowRef::new(Arc::new(constructor(fragment))) + } +} + +pub fn is_image_data(uri: &str) -> bool { + static TYPES: &'static [&'static str] = + &["data:image/png", "data:image/gif", "data:image/jpeg"]; + TYPES.iter().any(|&type_| uri.starts_with(type_)) +} diff --git a/components/layout_2020/context.rs b/components/layout_2020/context.rs new file mode 100644 index 00000000000..015cc9ac40d --- /dev/null +++ b/components/layout_2020/context.rs @@ -0,0 +1,201 @@ +/* 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/. */ + +//! Data needed by the layout thread. + +use crate::display_list::items::{OpaqueNode, WebRenderImageInfo}; +use crate::opaque_node::OpaqueNodeMethods; +use fnv::FnvHasher; +use gfx::font_cache_thread::FontCacheThread; +use gfx::font_context::FontContext; +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use msg::constellation_msg::PipelineId; +use net_traits::image_cache::{CanRequestImages, ImageCache, ImageState}; +use net_traits::image_cache::{ImageOrMetadataAvailable, UsePlaceholder}; +use parking_lot::RwLock; +use script_layout_interface::{PendingImage, PendingImageState}; +use script_traits::Painter; +use script_traits::UntrustedNodeAddress; +use servo_atoms::Atom; +use servo_url::ServoUrl; +use std::cell::{RefCell, RefMut}; +use std::collections::HashMap; +use std::hash::BuildHasherDefault; +use std::sync::{Arc, Mutex}; +use std::thread; +use style::context::RegisteredSpeculativePainter; +use style::context::SharedStyleContext; + +pub type LayoutFontContext = FontContext<FontCacheThread>; + +thread_local!(static FONT_CONTEXT_KEY: RefCell<Option<LayoutFontContext>> = RefCell::new(None)); + +pub fn with_thread_local_font_context<F, R>(layout_context: &LayoutContext, f: F) -> R +where + F: FnOnce(&mut LayoutFontContext) -> R, +{ + FONT_CONTEXT_KEY.with(|k| { + let mut font_context = k.borrow_mut(); + if font_context.is_none() { + let font_cache_thread = layout_context.font_cache_thread.lock().unwrap().clone(); + *font_context = Some(FontContext::new(font_cache_thread)); + } + f(&mut RefMut::map(font_context, |x| x.as_mut().unwrap())) + }) +} + +pub fn malloc_size_of_persistent_local_context(ops: &mut MallocSizeOfOps) -> usize { + FONT_CONTEXT_KEY.with(|r| { + if let Some(ref context) = *r.borrow() { + context.size_of(ops) + } else { + 0 + } + }) +} + +/// Layout information shared among all workers. This must be thread-safe. +pub struct LayoutContext<'a> { + /// The pipeline id of this LayoutContext. + pub id: PipelineId, + + /// Bits shared by the layout and style system. + pub style_context: SharedStyleContext<'a>, + + /// Reference to the script thread image cache. + pub image_cache: Arc<dyn ImageCache>, + + /// Interface to the font cache thread. + pub font_cache_thread: Mutex<FontCacheThread>, + + /// A cache of WebRender image info. + pub webrender_image_cache: Arc< + RwLock< + HashMap<(ServoUrl, UsePlaceholder), WebRenderImageInfo, BuildHasherDefault<FnvHasher>>, + >, + >, + + /// Paint worklets + pub registered_painters: &'a dyn RegisteredPainters, + + /// A list of in-progress image loads to be shared with the script thread. + /// A None value means that this layout was not initiated by the script thread. + pub pending_images: Option<Mutex<Vec<PendingImage>>>, + + /// A list of nodes that have just initiated a CSS transition. + /// A None value means that this layout was not initiated by the script thread. + pub newly_transitioning_nodes: Option<Mutex<Vec<UntrustedNodeAddress>>>, +} + +impl<'a> Drop for LayoutContext<'a> { + fn drop(&mut self) { + if !thread::panicking() { + if let Some(ref pending_images) = self.pending_images { + assert!(pending_images.lock().unwrap().is_empty()); + } + } + } +} + +impl<'a> LayoutContext<'a> { + #[inline(always)] + pub fn shared_context(&self) -> &SharedStyleContext { + &self.style_context + } + + pub fn get_or_request_image_or_meta( + &self, + node: OpaqueNode, + url: ServoUrl, + use_placeholder: UsePlaceholder, + ) -> Option<ImageOrMetadataAvailable> { + //XXXjdm For cases where we do not request an image, we still need to + // ensure the node gets another script-initiated reflow or it + // won't be requested at all. + let can_request = if self.pending_images.is_some() { + CanRequestImages::Yes + } else { + CanRequestImages::No + }; + + // See if the image is already available + let result = + self.image_cache + .find_image_or_metadata(url.clone(), use_placeholder, can_request); + match result { + Ok(image_or_metadata) => Some(image_or_metadata), + // Image failed to load, so just return nothing + Err(ImageState::LoadError) => None, + // Not yet requested - request image or metadata from the cache + Err(ImageState::NotRequested(id)) => { + let image = PendingImage { + state: PendingImageState::Unrequested(url), + node: node.to_untrusted_node_address(), + id: id, + }; + self.pending_images + .as_ref() + .unwrap() + .lock() + .unwrap() + .push(image); + None + }, + // Image has been requested, is still pending. Return no image for this paint loop. + // When the image loads it will trigger a reflow and/or repaint. + Err(ImageState::Pending(id)) => { + //XXXjdm if self.pending_images is not available, we should make sure that + // this node gets marked dirty again so it gets a script-initiated + // reflow that deals with this properly. + if let Some(ref pending_images) = self.pending_images { + let image = PendingImage { + state: PendingImageState::PendingResponse, + node: node.to_untrusted_node_address(), + id: id, + }; + pending_images.lock().unwrap().push(image); + } + None + }, + } + } + + pub fn get_webrender_image_for_url( + &self, + node: OpaqueNode, + url: ServoUrl, + use_placeholder: UsePlaceholder, + ) -> Option<WebRenderImageInfo> { + if let Some(existing_webrender_image) = self + .webrender_image_cache + .read() + .get(&(url.clone(), use_placeholder)) + { + return Some((*existing_webrender_image).clone()); + } + + match self.get_or_request_image_or_meta(node, url.clone(), use_placeholder) { + Some(ImageOrMetadataAvailable::ImageAvailable(image, _)) => { + let image_info = WebRenderImageInfo::from_image(&*image); + if image_info.key.is_none() { + Some(image_info) + } else { + let mut webrender_image_cache = self.webrender_image_cache.write(); + webrender_image_cache.insert((url, use_placeholder), image_info); + Some(image_info) + } + }, + None | Some(ImageOrMetadataAvailable::MetadataAvailable(_)) => None, + } + } +} + +/// A registered painter +pub trait RegisteredPainter: RegisteredSpeculativePainter + Painter {} + +/// A set of registered painters +pub trait RegisteredPainters: Sync { + /// Look up a painter + fn get(&self, name: &Atom) -> Option<&dyn RegisteredPainter>; +} diff --git a/components/layout_2020/data.rs b/components/layout_2020/data.rs new file mode 100644 index 00000000000..5f6672d1488 --- /dev/null +++ b/components/layout_2020/data.rs @@ -0,0 +1,68 @@ +/* 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/. */ + +use crate::construct::ConstructionResult; +use atomic_refcell::AtomicRefCell; +use script_layout_interface::StyleData; + +#[repr(C)] +pub struct StyleAndLayoutData { + /// Data accessed by script_layout_interface. This must be first to allow + /// casting between StyleAndLayoutData and StyleData. + pub style_data: StyleData, + /// The layout data associated with a node. + pub layout_data: AtomicRefCell<LayoutData>, +} + +impl StyleAndLayoutData { + pub fn new() -> Self { + Self { + style_data: StyleData::new(), + layout_data: AtomicRefCell::new(LayoutData::new()), + } + } +} + +/// Data that layout associates with a node. +#[repr(C)] +pub struct LayoutData { + /// The current results of flow construction for this node. This is either a + /// flow or a `ConstructionItem`. See comments in `construct.rs` for more + /// details. + pub flow_construction_result: ConstructionResult, + + pub before_flow_construction_result: ConstructionResult, + + pub after_flow_construction_result: ConstructionResult, + + pub details_summary_flow_construction_result: ConstructionResult, + + pub details_content_flow_construction_result: ConstructionResult, + + /// Various flags. + pub flags: LayoutDataFlags, +} + +impl LayoutData { + /// Creates new layout data. + pub fn new() -> LayoutData { + Self { + flow_construction_result: ConstructionResult::None, + before_flow_construction_result: ConstructionResult::None, + after_flow_construction_result: ConstructionResult::None, + details_summary_flow_construction_result: ConstructionResult::None, + details_content_flow_construction_result: ConstructionResult::None, + flags: LayoutDataFlags::empty(), + } + } +} + +bitflags! { + pub struct LayoutDataFlags: u8 { + #[doc = "Whether a flow has been newly constructed."] + const HAS_NEWLY_CONSTRUCTED_FLOW = 0x01; + #[doc = "Whether this node has been traversed by layout."] + const HAS_BEEN_TRAVERSED = 0x02; + } +} diff --git a/components/layout_2020/display_list/background.rs b/components/layout_2020/display_list/background.rs new file mode 100644 index 00000000000..c6fa1691e46 --- /dev/null +++ b/components/layout_2020/display_list/background.rs @@ -0,0 +1,336 @@ +/* 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/. */ + +use crate::display_list::border; +use app_units::Au; +use euclid::{Point2D, Rect, SideOffsets2D, Size2D}; +use style::computed_values::background_attachment::single_value::T as BackgroundAttachment; +use style::computed_values::background_clip::single_value::T as BackgroundClip; +use style::computed_values::background_origin::single_value::T as BackgroundOrigin; +use style::properties::style_structs::Background; +use style::values::computed::{BackgroundSize, NonNegativeLengthPercentageOrAuto}; +use style::values::specified::background::BackgroundRepeatKeyword; +use webrender_api::BorderRadius; + +/// Placment information for both image and gradient backgrounds. +#[derive(Clone, Copy, Debug)] +pub struct BackgroundPlacement { + /// Rendering bounds. The background will start in the uppper-left corner + /// and fill the whole area. + pub bounds: Rect<Au>, + /// Background tile size. Some backgrounds are repeated. These are the + /// dimensions of a single image of the background. + pub tile_size: Size2D<Au>, + /// Spacing between tiles. Some backgrounds are not repeated seamless + /// but have seams between them like tiles in real life. + pub tile_spacing: Size2D<Au>, + /// A clip area. While the background is rendered according to all the + /// measures above it is only shown within these bounds. + pub clip_rect: Rect<Au>, + /// Rounded corners for the clip_rect. + pub clip_radii: BorderRadius, + /// Whether or not the background is fixed to the viewport. + pub fixed: bool, +} + +/// Access element at index modulo the array length. +/// +/// Obviously it does not work with empty arrays. +/// +/// This is used for multiple layered background images. +/// See: https://drafts.csswg.org/css-backgrounds-3/#layering +pub fn get_cyclic<T>(arr: &[T], index: usize) -> &T { + &arr[index % arr.len()] +} + +/// For a given area and an image compute how big the +/// image should be displayed on the background. +fn compute_background_image_size( + bg_size: BackgroundSize, + bounds_size: Size2D<Au>, + intrinsic_size: Option<Size2D<Au>>, +) -> Size2D<Au> { + match intrinsic_size { + None => match bg_size { + BackgroundSize::Cover | BackgroundSize::Contain => bounds_size, + BackgroundSize::ExplicitSize { width, height } => Size2D::new( + width + .to_used_value(bounds_size.width) + .unwrap_or(bounds_size.width), + height + .to_used_value(bounds_size.height) + .unwrap_or(bounds_size.height), + ), + }, + Some(own_size) => { + // If `image_aspect_ratio` < `bounds_aspect_ratio`, the image is tall; otherwise, it is + // wide. + let image_aspect_ratio = own_size.width.to_f32_px() / own_size.height.to_f32_px(); + let bounds_aspect_ratio = + bounds_size.width.to_f32_px() / bounds_size.height.to_f32_px(); + match (bg_size, image_aspect_ratio < bounds_aspect_ratio) { + (BackgroundSize::Contain, false) | (BackgroundSize::Cover, true) => Size2D::new( + bounds_size.width, + bounds_size.width.scale_by(image_aspect_ratio.recip()), + ), + (BackgroundSize::Contain, true) | (BackgroundSize::Cover, false) => Size2D::new( + bounds_size.height.scale_by(image_aspect_ratio), + bounds_size.height, + ), + ( + BackgroundSize::ExplicitSize { + width, + height: NonNegativeLengthPercentageOrAuto::Auto, + }, + _, + ) => { + let width = width + .to_used_value(bounds_size.width) + .unwrap_or(own_size.width); + Size2D::new(width, width.scale_by(image_aspect_ratio.recip())) + }, + ( + BackgroundSize::ExplicitSize { + width: NonNegativeLengthPercentageOrAuto::Auto, + height, + }, + _, + ) => { + let height = height + .to_used_value(bounds_size.height) + .unwrap_or(own_size.height); + Size2D::new(height.scale_by(image_aspect_ratio), height) + }, + (BackgroundSize::ExplicitSize { width, height }, _) => Size2D::new( + width + .to_used_value(bounds_size.width) + .unwrap_or(own_size.width), + height + .to_used_value(bounds_size.height) + .unwrap_or(own_size.height), + ), + } + }, + } +} + +/// Compute a rounded clip rect for the background. +pub fn clip( + bg_clip: BackgroundClip, + absolute_bounds: Rect<Au>, + border: SideOffsets2D<Au>, + border_padding: SideOffsets2D<Au>, + border_radii: BorderRadius, +) -> (Rect<Au>, BorderRadius) { + match bg_clip { + BackgroundClip::BorderBox => (absolute_bounds, border_radii), + BackgroundClip::PaddingBox => ( + absolute_bounds.inner_rect(border), + border::inner_radii(border_radii, border), + ), + BackgroundClip::ContentBox => ( + absolute_bounds.inner_rect(border_padding), + border::inner_radii(border_radii, border_padding), + ), + } +} + +/// Determines where to place an element background image or gradient. +/// +/// Photos have their resolution as intrinsic size while gradients have +/// no intrinsic size. +pub fn placement( + bg: &Background, + viewport_size: Size2D<Au>, + absolute_bounds: Rect<Au>, + intrinsic_size: Option<Size2D<Au>>, + border: SideOffsets2D<Au>, + border_padding: SideOffsets2D<Au>, + border_radii: BorderRadius, + index: usize, +) -> BackgroundPlacement { + let bg_attachment = *get_cyclic(&bg.background_attachment.0, index); + let bg_clip = *get_cyclic(&bg.background_clip.0, index); + let bg_origin = *get_cyclic(&bg.background_origin.0, index); + let bg_position_x = get_cyclic(&bg.background_position_x.0, index); + let bg_position_y = get_cyclic(&bg.background_position_y.0, index); + let bg_repeat = get_cyclic(&bg.background_repeat.0, index); + let bg_size = *get_cyclic(&bg.background_size.0, index); + + let (clip_rect, clip_radii) = clip( + bg_clip, + absolute_bounds, + border, + border_padding, + border_radii, + ); + + let mut fixed = false; + let mut bounds = match bg_attachment { + BackgroundAttachment::Scroll => match bg_origin { + BackgroundOrigin::BorderBox => absolute_bounds, + BackgroundOrigin::PaddingBox => absolute_bounds.inner_rect(border), + BackgroundOrigin::ContentBox => absolute_bounds.inner_rect(border_padding), + }, + BackgroundAttachment::Fixed => { + fixed = true; + Rect::new(Point2D::origin(), viewport_size) + }, + }; + + let mut tile_size = compute_background_image_size(bg_size, bounds.size, intrinsic_size); + + let mut tile_spacing = Size2D::zero(); + let own_position = bounds.size - tile_size; + let pos_x = bg_position_x.to_used_value(own_position.width); + let pos_y = bg_position_y.to_used_value(own_position.height); + tile_image_axis( + bg_repeat.0, + &mut bounds.origin.x, + &mut bounds.size.width, + &mut tile_size.width, + &mut tile_spacing.width, + pos_x, + clip_rect.origin.x, + clip_rect.size.width, + ); + tile_image_axis( + bg_repeat.1, + &mut bounds.origin.y, + &mut bounds.size.height, + &mut tile_size.height, + &mut tile_spacing.height, + pos_y, + clip_rect.origin.y, + clip_rect.size.height, + ); + + BackgroundPlacement { + bounds, + tile_size, + tile_spacing, + clip_rect, + clip_radii, + fixed, + } +} + +fn tile_image_round( + position: &mut Au, + size: &mut Au, + absolute_anchor_origin: Au, + image_size: &mut Au, +) { + if *size == Au(0) || *image_size == Au(0) { + *position = Au(0); + *size = Au(0); + return; + } + + let number_of_tiles = (size.to_f32_px() / image_size.to_f32_px()).round().max(1.0); + *image_size = *size / (number_of_tiles as i32); + tile_image(position, size, absolute_anchor_origin, *image_size); +} + +fn tile_image_spaced( + position: &mut Au, + size: &mut Au, + tile_spacing: &mut Au, + absolute_anchor_origin: Au, + image_size: Au, +) { + if *size == Au(0) || image_size == Au(0) { + *position = Au(0); + *size = Au(0); + *tile_spacing = Au(0); + return; + } + + // Per the spec, if the space available is not enough for two images, just tile as + // normal but only display a single tile. + if image_size * 2 >= *size { + tile_image(position, size, absolute_anchor_origin, image_size); + *tile_spacing = Au(0); + *size = image_size; + return; + } + + // Take the box size, remove room for two tiles on the edges, and then calculate how many + // other tiles fit in between them. + let size_remaining = *size - (image_size * 2); + let num_middle_tiles = (size_remaining.to_f32_px() / image_size.to_f32_px()).floor() as i32; + + // Allocate the remaining space as padding between tiles. background-position is ignored + // as per the spec, so the position is just the box origin. We are also ignoring + // background-attachment here, which seems unspecced when combined with + // background-repeat: space. + let space_for_middle_tiles = image_size * num_middle_tiles; + *tile_spacing = (size_remaining - space_for_middle_tiles) / (num_middle_tiles + 1); +} + +/// Tile an image +fn tile_image(position: &mut Au, size: &mut Au, absolute_anchor_origin: Au, image_size: Au) { + // Avoid division by zero below! + // Images with a zero width or height are not displayed. + // Therefore the positions do not matter and can be left unchanged. + // NOTE: A possible optimization is not to build + // display items in this case at all. + if image_size == Au(0) { + return; + } + + let delta_pixels = absolute_anchor_origin - *position; + let image_size_px = image_size.to_f32_px(); + let tile_count = ((delta_pixels.to_f32_px() + image_size_px - 1.0) / image_size_px).floor(); + let offset = image_size * (tile_count as i32); + let new_position = absolute_anchor_origin - offset; + *size = *position - new_position + *size; + *position = new_position; +} + +/// For either the x or the y axis ajust various values to account for tiling. +/// +/// This is done separately for both axes because the repeat keywords may differ. +fn tile_image_axis( + repeat: BackgroundRepeatKeyword, + position: &mut Au, + size: &mut Au, + tile_size: &mut Au, + tile_spacing: &mut Au, + offset: Au, + clip_origin: Au, + clip_size: Au, +) { + let absolute_anchor_origin = *position + offset; + match repeat { + BackgroundRepeatKeyword::NoRepeat => { + *position += offset; + *size = *tile_size; + }, + BackgroundRepeatKeyword::Repeat => { + *position = clip_origin; + *size = clip_size; + tile_image(position, size, absolute_anchor_origin, *tile_size); + }, + BackgroundRepeatKeyword::Space => { + tile_image_spaced( + position, + size, + tile_spacing, + absolute_anchor_origin, + *tile_size, + ); + let combined_tile_size = *tile_size + *tile_spacing; + *position = clip_origin; + *size = clip_size; + tile_image(position, size, absolute_anchor_origin, combined_tile_size); + }, + BackgroundRepeatKeyword::Round => { + tile_image_round(position, size, absolute_anchor_origin, tile_size); + *position = clip_origin; + *size = clip_size; + tile_image(position, size, absolute_anchor_origin, *tile_size); + }, + } +} diff --git a/components/layout_2020/display_list/border.rs b/components/layout_2020/display_list/border.rs new file mode 100644 index 00000000000..fff18fcf6c0 --- /dev/null +++ b/components/layout_2020/display_list/border.rs @@ -0,0 +1,199 @@ +/* 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/. */ + +use crate::display_list::ToLayout; +use app_units::Au; +use euclid::{Rect, SideOffsets2D, Size2D}; +use style::computed_values::border_image_outset::T as BorderImageOutset; +use style::properties::style_structs::Border; +use style::values::computed::NumberOrPercentage; +use style::values::computed::{BorderCornerRadius, BorderImageWidth}; +use style::values::computed::{BorderImageSideWidth, NonNegativeLengthOrNumber}; +use style::values::generics::rect::Rect as StyleRect; +use style::values::generics::NonNegative; +use webrender_api::units::{LayoutSideOffsets, LayoutSize}; +use webrender_api::{BorderRadius, BorderSide, BorderStyle, ColorF, NormalBorder}; + +/// Computes a border radius size against the containing size. +/// +/// Note that percentages in `border-radius` are resolved against the relevant +/// box dimension instead of only against the width per [1]: +/// +/// > Percentages: Refer to corresponding dimension of the border box. +/// +/// [1]: https://drafts.csswg.org/css-backgrounds-3/#border-radius +fn corner_radius(radius: BorderCornerRadius, containing_size: Size2D<Au>) -> Size2D<Au> { + let w = radius.0.width().to_used_value(containing_size.width); + let h = radius.0.height().to_used_value(containing_size.height); + Size2D::new(w, h) +} + +fn scaled_radii(radii: BorderRadius, factor: f32) -> BorderRadius { + BorderRadius { + top_left: radii.top_left * factor, + top_right: radii.top_right * factor, + bottom_left: radii.bottom_left * factor, + bottom_right: radii.bottom_right * factor, + } +} + +fn overlapping_radii(size: LayoutSize, radii: BorderRadius) -> BorderRadius { + // No two corners' border radii may add up to more than the length of the edge + // between them. To prevent that, all radii are scaled down uniformly. + fn scale_factor(radius_a: f32, radius_b: f32, edge_length: f32) -> f32 { + let required = radius_a + radius_b; + + if required <= edge_length { + 1.0 + } else { + edge_length / required + } + } + + let top_factor = scale_factor(radii.top_left.width, radii.top_right.width, size.width); + let bottom_factor = scale_factor( + radii.bottom_left.width, + radii.bottom_right.width, + size.width, + ); + let left_factor = scale_factor(radii.top_left.height, radii.bottom_left.height, size.height); + let right_factor = scale_factor( + radii.top_right.height, + radii.bottom_right.height, + size.height, + ); + let min_factor = top_factor + .min(bottom_factor) + .min(left_factor) + .min(right_factor); + if min_factor < 1.0 { + scaled_radii(radii, min_factor) + } else { + radii + } +} + +/// Determine the four corner radii of a border. +/// +/// Radii may either be absolute or relative to the absolute bounds. +/// Each corner radius has a width and a height which may differ. +/// Lastly overlapping radii are shrank so they don't collide anymore. +pub fn radii(abs_bounds: Rect<Au>, border_style: &Border) -> BorderRadius { + // TODO(cgaebel): Support border radii even in the case of multiple border widths. + // This is an extension of supporting elliptical radii. For now, all percentage + // radii will be relative to the width. + + overlapping_radii( + abs_bounds.size.to_layout(), + BorderRadius { + top_left: corner_radius(border_style.border_top_left_radius, abs_bounds.size) + .to_layout(), + top_right: corner_radius(border_style.border_top_right_radius, abs_bounds.size) + .to_layout(), + bottom_right: corner_radius(border_style.border_bottom_right_radius, abs_bounds.size) + .to_layout(), + bottom_left: corner_radius(border_style.border_bottom_left_radius, abs_bounds.size) + .to_layout(), + }, + ) +} + +/// Calculates radii for the inner side. +/// +/// Radii usually describe the outer side of a border but for the lines to look nice +/// the inner radii need to be smaller depending on the line width. +/// +/// This is used to determine clipping areas. +pub fn inner_radii(mut radii: BorderRadius, offsets: SideOffsets2D<Au>) -> BorderRadius { + fn inner_length(x: f32, offset: Au) -> f32 { + 0.0_f32.max(x - offset.to_f32_px()) + } + radii.top_left.width = inner_length(radii.top_left.width, offsets.left); + radii.bottom_left.width = inner_length(radii.bottom_left.width, offsets.left); + + radii.top_right.width = inner_length(radii.top_right.width, offsets.right); + radii.bottom_right.width = inner_length(radii.bottom_right.width, offsets.right); + + radii.top_left.height = inner_length(radii.top_left.height, offsets.top); + radii.top_right.height = inner_length(radii.top_right.height, offsets.top); + + radii.bottom_left.height = inner_length(radii.bottom_left.height, offsets.bottom); + radii.bottom_right.height = inner_length(radii.bottom_right.height, offsets.bottom); + radii +} + +/// Creates a four-sided border with square corners and uniform color and width. +pub fn simple(color: ColorF, style: BorderStyle) -> NormalBorder { + let side = BorderSide { color, style }; + NormalBorder { + left: side, + right: side, + top: side, + bottom: side, + radius: BorderRadius::zero(), + do_aa: true, + } +} + +fn side_image_outset(outset: NonNegativeLengthOrNumber, border_width: Au) -> Au { + match outset { + NonNegativeLengthOrNumber::Length(length) => length.into(), + NonNegativeLengthOrNumber::Number(factor) => border_width.scale_by(factor.0), + } +} + +/// Compute the additional border-image area. +pub fn image_outset(outset: BorderImageOutset, border: SideOffsets2D<Au>) -> SideOffsets2D<Au> { + SideOffsets2D::new( + side_image_outset(outset.0, border.top), + side_image_outset(outset.1, border.right), + side_image_outset(outset.2, border.bottom), + side_image_outset(outset.3, border.left), + ) +} + +fn side_image_width( + border_image_width: BorderImageSideWidth, + border_width: f32, + total_length: Au, +) -> f32 { + match border_image_width { + BorderImageSideWidth::LengthPercentage(v) => v.to_used_value(total_length).to_f32_px(), + BorderImageSideWidth::Number(x) => border_width * x.0, + BorderImageSideWidth::Auto => border_width, + } +} + +pub fn image_width( + width: &BorderImageWidth, + border: LayoutSideOffsets, + border_area: Size2D<Au>, +) -> LayoutSideOffsets { + LayoutSideOffsets::new( + side_image_width(width.0, border.top, border_area.height), + side_image_width(width.1, border.right, border_area.width), + side_image_width(width.2, border.bottom, border_area.height), + side_image_width(width.3, border.left, border_area.width), + ) +} + +fn resolve_percentage(value: NonNegative<NumberOrPercentage>, length: i32) -> i32 { + match value.0 { + NumberOrPercentage::Percentage(p) => (p.0 * length as f32).round() as i32, + NumberOrPercentage::Number(n) => n.round() as i32, + } +} + +pub fn image_slice( + border_image_slice: &StyleRect<NonNegative<NumberOrPercentage>>, + width: i32, + height: i32, +) -> SideOffsets2D<i32> { + SideOffsets2D::new( + resolve_percentage(border_image_slice.0, height), + resolve_percentage(border_image_slice.1, width), + resolve_percentage(border_image_slice.2, height), + resolve_percentage(border_image_slice.3, width), + ) +} diff --git a/components/layout_2020/display_list/builder.rs b/components/layout_2020/display_list/builder.rs new file mode 100644 index 00000000000..e6ead2505ca --- /dev/null +++ b/components/layout_2020/display_list/builder.rs @@ -0,0 +1,3024 @@ +/* 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/. */ + +//! Builds display lists from flows and fragments. +//! +//! Other browser engines sometimes call this "painting", but it is more accurately called display +//! list building, as the actual painting does not happen here—only deciding *what* we're going to +//! paint. + +use crate::block::BlockFlow; +use crate::context::LayoutContext; +use crate::display_list::background::{self, get_cyclic}; +use crate::display_list::border; +use crate::display_list::gradient; +use crate::display_list::items::{self, BaseDisplayItem, ClipScrollNode}; +use crate::display_list::items::{ClipScrollNodeIndex, ClipScrollNodeType, ClippingAndScrolling}; +use crate::display_list::items::{ClippingRegion, DisplayItem, DisplayItemMetadata, DisplayList}; +use crate::display_list::items::{CommonDisplayItem, DisplayListSection}; +use crate::display_list::items::{IframeDisplayItem, OpaqueNode}; +use crate::display_list::items::{PopAllTextShadowsDisplayItem, PushTextShadowDisplayItem}; +use crate::display_list::items::{StackingContext, StackingContextType, StickyFrameData}; +use crate::display_list::items::{TextOrientation, WebRenderImageInfo}; +use crate::display_list::ToLayout; +use crate::flow::{BaseFlow, Flow, FlowFlags}; +use crate::flow_ref::FlowRef; +use crate::fragment::SpecificFragmentInfo; +use crate::fragment::{CanvasFragmentSource, CoordinateSystem, Fragment, ScannedTextFragmentInfo}; +use crate::inline::InlineFragmentNodeFlags; +use crate::model::MaybeAuto; +use crate::table_cell::CollapsedBordersForCell; +use app_units::{Au, AU_PER_PX}; +use canvas_traits::canvas::{CanvasMsg, FromLayoutMsg}; +use embedder_traits::Cursor; +use euclid::{rect, Point2D, Rect, SideOffsets2D, Size2D, TypedRect, TypedSize2D}; +use fnv::FnvHashMap; +use gfx::text::glyph::ByteIndex; +use gfx::text::TextRun; +use gfx_traits::{combine_id_with_fragment_type, FragmentType, StackingContextId}; +use ipc_channel::ipc; +use msg::constellation_msg::PipelineId; +use net_traits::image_cache::UsePlaceholder; +use range::Range; +use script_traits::IFrameSize; +use servo_config::opts; +use servo_geometry::{self, MaxRect}; +use std::default::Default; +use std::f32; +use std::mem; +use std::sync::Arc; +use style::computed_values::border_style::T as BorderStyle; +use style::computed_values::overflow_x::T as StyleOverflow; +use style::computed_values::pointer_events::T as PointerEvents; +use style::computed_values::position::T as StylePosition; +use style::computed_values::visibility::T as Visibility; +use style::logical_geometry::{LogicalMargin, LogicalPoint, LogicalRect}; +use style::properties::{style_structs, ComputedValues}; +use style::servo::restyle_damage::ServoRestyleDamage; +use style::values::computed::effects::SimpleShadow; +use style::values::computed::image::{Image, ImageLayer}; +use style::values::computed::{Gradient, LengthOrAuto}; +use style::values::generics::background::BackgroundSize; +use style::values::generics::image::{GradientKind, PaintWorklet}; +use style::values::specified::ui::CursorKind; +use style::values::{Either, RGBA}; +use style_traits::ToCss; +use webrender_api::units::{LayoutRect, LayoutSize, LayoutTransform, LayoutVector2D}; +use webrender_api::{self, BorderDetails, BorderRadius, BorderSide, BoxShadowClipMode, ColorF}; +use webrender_api::{ColorU, ExternalScrollId, FilterOp, GlyphInstance, ImageRendering, LineStyle}; +use webrender_api::{NinePatchBorder, NinePatchBorderSource, NormalBorder}; +use webrender_api::{ScrollSensitivity, StickyOffsetBounds}; + +static THREAD_TINT_COLORS: [ColorF; 8] = [ + ColorF { + r: 6.0 / 255.0, + g: 153.0 / 255.0, + b: 198.0 / 255.0, + a: 0.7, + }, + ColorF { + r: 255.0 / 255.0, + g: 212.0 / 255.0, + b: 83.0 / 255.0, + a: 0.7, + }, + ColorF { + r: 116.0 / 255.0, + g: 29.0 / 255.0, + b: 109.0 / 255.0, + a: 0.7, + }, + ColorF { + r: 204.0 / 255.0, + g: 158.0 / 255.0, + b: 199.0 / 255.0, + a: 0.7, + }, + ColorF { + r: 242.0 / 255.0, + g: 46.0 / 255.0, + b: 121.0 / 255.0, + a: 0.7, + }, + ColorF { + r: 116.0 / 255.0, + g: 203.0 / 255.0, + b: 196.0 / 255.0, + a: 0.7, + }, + ColorF { + r: 255.0 / 255.0, + g: 249.0 / 255.0, + b: 201.0 / 255.0, + a: 0.7, + }, + ColorF { + r: 137.0 / 255.0, + g: 196.0 / 255.0, + b: 78.0 / 255.0, + a: 0.7, + }, +]; + +pub struct InlineNodeBorderInfo { + is_first_fragment_of_element: bool, + is_last_fragment_of_element: bool, +} + +#[derive(Debug)] +struct StackingContextInfo { + children: Vec<StackingContext>, + clip_scroll_nodes: Vec<ClipScrollNodeIndex>, + real_stacking_context_id: StackingContextId, +} + +impl StackingContextInfo { + fn new(real_stacking_context_id: StackingContextId) -> StackingContextInfo { + StackingContextInfo { + children: Vec::new(), + clip_scroll_nodes: Vec::new(), + real_stacking_context_id, + } + } + + fn take_children(&mut self) -> Vec<StackingContext> { + mem::replace(&mut self.children, Vec::new()) + } +} + +pub struct StackingContextCollectionState { + /// The PipelineId of this stacking context collection. + pub pipeline_id: PipelineId, + + /// The root of the StackingContext tree. + pub root_stacking_context: StackingContext, + + /// StackingContext and ClipScrollNode children for each StackingContext. + stacking_context_info: FnvHashMap<StackingContextId, StackingContextInfo>, + + pub clip_scroll_nodes: Vec<ClipScrollNode>, + + /// The current stacking context id, used to keep track of state when building. + /// recursively building and processing the display list. + pub current_stacking_context_id: StackingContextId, + + /// The current reference frame ClipScrollNodeIndex. + pub current_real_stacking_context_id: StackingContextId, + + /// The next stacking context id that we will assign to a stacking context. + pub next_stacking_context_id: StackingContextId, + + /// The current reference frame id. This is used to assign items to the parent + /// reference frame when we encounter a fixed position stacking context. + pub current_parent_reference_frame_id: ClipScrollNodeIndex, + + /// The current clip and scroll info, used to keep track of state when + /// recursively building and processing the display list. + pub current_clipping_and_scrolling: ClippingAndScrolling, + + /// The clip and scroll info of the first ancestor which defines a containing block. + /// This is necessary because absolutely positioned items should be clipped + /// by their containing block's scroll root. + pub containing_block_clipping_and_scrolling: ClippingAndScrolling, + + /// A stack of clips used to cull display list entries that are outside the + /// rendered region. + pub clip_stack: Vec<Rect<Au>>, + + /// A stack of clips used to cull display list entries that are outside the + /// rendered region, but only collected at containing block boundaries. + pub containing_block_clip_stack: Vec<Rect<Au>>, + + /// The flow parent's content box, used to calculate sticky constraints. + parent_stacking_relative_content_box: Rect<Au>, +} + +impl StackingContextCollectionState { + pub fn new(pipeline_id: PipelineId) -> StackingContextCollectionState { + let root_clip_indices = + ClippingAndScrolling::simple(ClipScrollNodeIndex::root_scroll_node()); + + let mut stacking_context_info = FnvHashMap::default(); + stacking_context_info.insert( + StackingContextId::root(), + StackingContextInfo::new(StackingContextId::root()), + ); + + // We add two empty nodes to represent the WebRender root reference frame and + // root scroll nodes. WebRender adds these automatically and we add them here + // so that the ids in the array match up with the ones we assign during display + // list building. We ignore these two nodes during conversion to WebRender + // display lists. + let clip_scroll_nodes = vec![ClipScrollNode::placeholder(), ClipScrollNode::placeholder()]; + + StackingContextCollectionState { + pipeline_id: pipeline_id, + root_stacking_context: StackingContext::root(), + stacking_context_info, + clip_scroll_nodes, + current_stacking_context_id: StackingContextId::root(), + current_real_stacking_context_id: StackingContextId::root(), + next_stacking_context_id: StackingContextId::root().next(), + current_parent_reference_frame_id: ClipScrollNodeIndex::root_reference_frame(), + current_clipping_and_scrolling: root_clip_indices, + containing_block_clipping_and_scrolling: root_clip_indices, + clip_stack: Vec::new(), + containing_block_clip_stack: Vec::new(), + parent_stacking_relative_content_box: Rect::zero(), + } + } + + fn allocate_stacking_context_info( + &mut self, + stacking_context_type: StackingContextType, + ) -> StackingContextId { + let next_stacking_context_id = self.next_stacking_context_id.next(); + let allocated_id = + mem::replace(&mut self.next_stacking_context_id, next_stacking_context_id); + + let real_stacking_context_id = match stacking_context_type { + StackingContextType::Real => allocated_id, + _ => self.current_real_stacking_context_id, + }; + + self.stacking_context_info.insert( + allocated_id, + StackingContextInfo::new(real_stacking_context_id), + ); + + allocated_id + } + + fn add_stacking_context( + &mut self, + parent_id: StackingContextId, + stacking_context: StackingContext, + ) { + self.stacking_context_info + .get_mut(&parent_id) + .unwrap() + .children + .push(stacking_context); + } + + fn add_clip_scroll_node(&mut self, clip_scroll_node: ClipScrollNode) -> ClipScrollNodeIndex { + let is_placeholder = clip_scroll_node.is_placeholder(); + + self.clip_scroll_nodes.push(clip_scroll_node); + let index = ClipScrollNodeIndex::new(self.clip_scroll_nodes.len() - 1); + + // If this node is a placeholder node (currently just reference frames), then don't add + // it to the stacking context list. Placeholder nodes are created automatically by + // WebRender and we don't want to explicitly create them in the display list. The node + // is just there to take up a spot in the global list of ClipScrollNodes. + if !is_placeholder { + // We want the scroll root to be defined before any possible item that could use it, + // so we make sure that it is added to the beginning of the parent "real" (non-pseudo) + // stacking context. This ensures that item reordering will not result in an item using + // the scroll root before it is defined. + self.stacking_context_info + .get_mut(&self.current_real_stacking_context_id) + .unwrap() + .clip_scroll_nodes + .push(index); + } + + index + } +} + +pub struct DisplayListBuildState<'a> { + /// A LayoutContext reference important for creating WebRender images. + pub layout_context: &'a LayoutContext<'a>, + + /// The root of the StackingContext tree. + pub root_stacking_context: StackingContext, + + /// StackingContext and ClipScrollNode children for each StackingContext. + stacking_context_info: FnvHashMap<StackingContextId, StackingContextInfo>, + + /// A vector of ClipScrollNodes which will be given ids during WebRender DL conversion. + pub clip_scroll_nodes: Vec<ClipScrollNode>, + + /// The items in this display list. + pub items: FnvHashMap<StackingContextId, Vec<DisplayItem>>, + + /// Whether or not we are processing an element that establishes scrolling overflow. Used + /// to determine what ClipScrollNode to place backgrounds and borders into. + pub processing_scrolling_overflow_element: bool, + + /// The current stacking context id, used to keep track of state when building. + /// recursively building and processing the display list. + pub current_stacking_context_id: StackingContextId, + + /// The current clip and scroll info, used to keep track of state when + /// recursively building and processing the display list. + pub current_clipping_and_scrolling: ClippingAndScrolling, + + /// Vector containing iframe sizes, used to inform the constellation about + /// new iframe sizes + pub iframe_sizes: Vec<IFrameSize>, + + /// Stores text runs to answer text queries used to place a cursor inside text. + pub indexable_text: IndexableText, +} + +impl<'a> DisplayListBuildState<'a> { + pub fn new( + layout_context: &'a LayoutContext, + state: StackingContextCollectionState, + ) -> DisplayListBuildState<'a> { + DisplayListBuildState { + layout_context: layout_context, + root_stacking_context: state.root_stacking_context, + items: FnvHashMap::default(), + stacking_context_info: state.stacking_context_info, + clip_scroll_nodes: state.clip_scroll_nodes, + processing_scrolling_overflow_element: false, + current_stacking_context_id: StackingContextId::root(), + current_clipping_and_scrolling: ClippingAndScrolling::simple( + ClipScrollNodeIndex::root_scroll_node(), + ), + iframe_sizes: Vec::new(), + indexable_text: IndexableText::default(), + } + } + + pub fn add_display_item(&mut self, display_item: DisplayItem) { + let items = self + .items + .entry(display_item.stacking_context_id()) + .or_insert(Vec::new()); + items.push(display_item); + } + + fn add_image_item(&mut self, base: BaseDisplayItem, item: webrender_api::ImageDisplayItem) { + if item.stretch_size == LayoutSize::zero() { + return; + } + self.add_display_item(DisplayItem::Image(CommonDisplayItem::new(base, item))) + } + + fn parent_clip_scroll_node_index(&self, index: ClipScrollNodeIndex) -> ClipScrollNodeIndex { + if index.is_root_scroll_node() { + return index; + } + + self.clip_scroll_nodes[index.to_index()].parent_index + } + + fn is_background_or_border_of_clip_scroll_node(&self, section: DisplayListSection) -> bool { + (section == DisplayListSection::BackgroundAndBorders || + section == DisplayListSection::BlockBackgroundsAndBorders) && + self.processing_scrolling_overflow_element + } + + pub fn create_base_display_item( + &self, + clip_rect: Rect<Au>, + node: OpaqueNode, + cursor: Option<Cursor>, + section: DisplayListSection, + ) -> BaseDisplayItem { + let clipping_and_scrolling = if self.is_background_or_border_of_clip_scroll_node(section) { + ClippingAndScrolling::simple( + self.parent_clip_scroll_node_index(self.current_clipping_and_scrolling.scrolling), + ) + } else { + self.current_clipping_and_scrolling + }; + self.create_base_display_item_with_clipping_and_scrolling( + clip_rect, + node, + cursor, + section, + clipping_and_scrolling, + ) + } + + fn create_base_display_item_with_clipping_and_scrolling( + &self, + clip_rect: Rect<Au>, + node: OpaqueNode, + cursor: Option<Cursor>, + section: DisplayListSection, + clipping_and_scrolling: ClippingAndScrolling, + ) -> BaseDisplayItem { + BaseDisplayItem::new( + DisplayItemMetadata { + node, + // Store cursor id in display list. + pointing: cursor.map(|x| x as u16), + }, + clip_rect.to_layout(), + section, + self.current_stacking_context_id, + clipping_and_scrolling, + ) + } + + fn add_late_clip_node(&mut self, rect: LayoutRect, radii: BorderRadius) -> ClipScrollNodeIndex { + let mut clip = ClippingRegion::from_rect(rect); + clip.intersect_with_rounded_rect(rect, radii); + + let node = ClipScrollNode { + parent_index: self.current_clipping_and_scrolling.scrolling, + clip, + content_rect: LayoutRect::zero(), // content_rect isn't important for clips. + node_type: ClipScrollNodeType::Clip, + }; + + // We want the scroll root to be defined before any possible item that could use it, + // so we make sure that it is added to the beginning of the parent "real" (non-pseudo) + // stacking context. This ensures that item reordering will not result in an item using + // the scroll root before it is defined. + self.clip_scroll_nodes.push(node); + let index = ClipScrollNodeIndex::new(self.clip_scroll_nodes.len() - 1); + let real_stacking_context_id = + self.stacking_context_info[&self.current_stacking_context_id].real_stacking_context_id; + self.stacking_context_info + .get_mut(&real_stacking_context_id) + .unwrap() + .clip_scroll_nodes + .push(index); + + index + } + + pub fn to_display_list(mut self) -> DisplayList { + let mut list = Vec::new(); + let root_context = mem::replace(&mut self.root_stacking_context, StackingContext::root()); + + self.to_display_list_for_stacking_context(&mut list, root_context); + + DisplayList { + list: list, + clip_scroll_nodes: self.clip_scroll_nodes, + } + } + + fn to_display_list_for_stacking_context( + &mut self, + list: &mut Vec<DisplayItem>, + stacking_context: StackingContext, + ) { + let mut child_items = self + .items + .remove(&stacking_context.id) + .unwrap_or(Vec::new()); + child_items.sort_by(|a, b| a.base().section.cmp(&b.base().section)); + child_items.reverse(); + + let mut info = self + .stacking_context_info + .remove(&stacking_context.id) + .unwrap(); + + info.children.sort(); + + if stacking_context.context_type != StackingContextType::Real { + list.extend( + info.clip_scroll_nodes + .into_iter() + .map(|index| index.to_define_item()), + ); + self.to_display_list_for_items(list, child_items, info.children); + } else { + let (push_item, pop_item) = stacking_context.to_display_list_items(); + list.push(push_item); + list.extend( + info.clip_scroll_nodes + .into_iter() + .map(|index| index.to_define_item()), + ); + self.to_display_list_for_items(list, child_items, info.children); + list.push(pop_item); + } + } + + fn to_display_list_for_items( + &mut self, + list: &mut Vec<DisplayItem>, + mut child_items: Vec<DisplayItem>, + child_stacking_contexts: Vec<StackingContext>, + ) { + // Properly order display items that make up a stacking context. "Steps" here + // refer to the steps in CSS 2.1 Appendix E. + // Steps 1 and 2: Borders and background for the root. + while child_items.last().map_or(false, |child| { + child.section() == DisplayListSection::BackgroundAndBorders + }) { + list.push(child_items.pop().unwrap()); + } + + // Step 3: Positioned descendants with negative z-indices. + let mut child_stacking_contexts = child_stacking_contexts.into_iter().peekable(); + while child_stacking_contexts + .peek() + .map_or(false, |child| child.z_index < 0) + { + let context = child_stacking_contexts.next().unwrap(); + self.to_display_list_for_stacking_context(list, context); + } + + // Step 4: Block backgrounds and borders. + while child_items.last().map_or(false, |child| { + child.section() == DisplayListSection::BlockBackgroundsAndBorders + }) { + list.push(child_items.pop().unwrap()); + } + + // Step 5: Floats. + while child_stacking_contexts.peek().map_or(false, |child| { + child.context_type == StackingContextType::PseudoFloat + }) { + let context = child_stacking_contexts.next().unwrap(); + self.to_display_list_for_stacking_context(list, context); + } + + // Step 6 & 7: Content and inlines that generate stacking contexts. + while child_items.last().map_or(false, |child| { + child.section() == DisplayListSection::Content + }) { + list.push(child_items.pop().unwrap()); + } + + // Step 8 & 9: Positioned descendants with nonnegative, numeric z-indices. + for child in child_stacking_contexts { + self.to_display_list_for_stacking_context(list, child); + } + + // Step 10: Outlines. + for item in child_items.drain(..) { + list.push(item); + } + } + + fn clipping_and_scrolling_scope<R, F: FnOnce(&mut Self) -> R>(&mut self, function: F) -> R { + let previous_clipping_and_scrolling = self.current_clipping_and_scrolling; + let ret = function(self); + self.current_clipping_and_scrolling = previous_clipping_and_scrolling; + ret + } +} + +/// The logical width of an insertion point: at the moment, a one-pixel-wide line. +const INSERTION_POINT_LOGICAL_WIDTH: Au = Au(1 * AU_PER_PX); + +/// Get the border radius for the rectangle inside of a rounded border. This is useful +/// for building the clip for the content inside the border. +fn build_border_radius_for_inner_rect( + outer_rect: Rect<Au>, + style: &ComputedValues, +) -> BorderRadius { + let radii = border::radii(outer_rect, style.get_border()); + if radii.is_zero() { + return radii; + } + + // Since we are going to using the inner rectangle (outer rectangle minus + // border width), we need to adjust to border radius so that we are smaller + // rectangle with the same border curve. + let border_widths = style.logical_border_width().to_physical(style.writing_mode); + border::inner_radii(radii, border_widths) +} + +impl Fragment { + pub fn collect_stacking_contexts_for_blocklike_fragment( + &mut self, + state: &mut StackingContextCollectionState, + ) -> bool { + match self.specific { + SpecificFragmentInfo::InlineBlock(ref mut block_flow) => { + let block_flow = FlowRef::deref_mut(&mut block_flow.flow_ref); + block_flow.collect_stacking_contexts(state); + true + }, + SpecificFragmentInfo::InlineAbsoluteHypothetical(ref mut block_flow) => { + let block_flow = FlowRef::deref_mut(&mut block_flow.flow_ref); + block_flow.collect_stacking_contexts(state); + true + }, + SpecificFragmentInfo::InlineAbsolute(ref mut block_flow) => { + let block_flow = FlowRef::deref_mut(&mut block_flow.flow_ref); + block_flow.collect_stacking_contexts(state); + true + }, + // FIXME: In the future, if #15144 is fixed we can remove this case. See #18510. + SpecificFragmentInfo::TruncatedFragment(ref mut info) => info + .full + .collect_stacking_contexts_for_blocklike_fragment(state), + _ => false, + } + } + + pub fn create_stacking_context_for_inline_block( + &mut self, + base: &BaseFlow, + state: &mut StackingContextCollectionState, + ) -> bool { + self.stacking_context_id = state.allocate_stacking_context_info(StackingContextType::Real); + + let established_reference_frame = if self.can_establish_reference_frame() { + // WebRender currently creates reference frames automatically, so just add + // a placeholder node to allocate a ClipScrollNodeIndex for this reference frame. + self.established_reference_frame = + Some(state.add_clip_scroll_node(ClipScrollNode::placeholder())); + self.established_reference_frame + } else { + None + }; + + let current_stacking_context_id = state.current_stacking_context_id; + let stacking_context = self.create_stacking_context( + self.stacking_context_id, + &base, + StackingContextType::Real, + established_reference_frame, + state.current_clipping_and_scrolling, + ); + state.add_stacking_context(current_stacking_context_id, stacking_context); + true + } + + /// Adds the display items necessary to paint the background of this fragment to the display + /// list if necessary. + fn build_display_list_for_background_if_applicable( + &self, + state: &mut DisplayListBuildState, + style: &ComputedValues, + display_list_section: DisplayListSection, + absolute_bounds: Rect<Au>, + ) { + let background = style.get_background(); + let background_color = style.resolve_color(background.background_color); + // XXXManishearth the below method should ideally use an iterator over + // backgrounds + self.build_display_list_for_background_if_applicable_with_background( + state, + style, + background, + background_color, + display_list_section, + absolute_bounds, + ) + } + + /// Same as build_display_list_for_background_if_applicable, but lets you + /// override the actual background used + fn build_display_list_for_background_if_applicable_with_background( + &self, + state: &mut DisplayListBuildState, + style: &ComputedValues, + background: &style_structs::Background, + background_color: RGBA, + display_list_section: DisplayListSection, + absolute_bounds: Rect<Au>, + ) { + // FIXME: This causes a lot of background colors to be displayed when they are clearly not + // needed. We could use display list optimization to clean this up, but it still seems + // inefficient. What we really want is something like "nearest ancestor element that + // doesn't have a fragment". + + // Quote from CSS Backgrounds and Borders Module Level 3: + // + // > The background color is clipped according to the background-clip value associated + // > with the bottom-most background image layer. + let last_background_image_index = background.background_image.0.len() - 1; + let color_clip = *get_cyclic(&background.background_clip.0, last_background_image_index); + let (bounds, border_radii) = background::clip( + color_clip, + absolute_bounds, + style.logical_border_width().to_physical(style.writing_mode), + self.border_padding.to_physical(self.style.writing_mode), + border::radii(absolute_bounds, style.get_border()), + ); + + state.clipping_and_scrolling_scope(|state| { + if !border_radii.is_zero() { + let clip_id = state.add_late_clip_node(bounds.to_layout(), border_radii); + state.current_clipping_and_scrolling = ClippingAndScrolling::simple(clip_id); + } + + let base = state.create_base_display_item( + bounds, + self.node, + get_cursor(&style, Cursor::Default), + display_list_section, + ); + state.add_display_item(DisplayItem::Rectangle(CommonDisplayItem::new( + base, + webrender_api::RectangleDisplayItem { + color: background_color.to_layout(), + common: items::empty_common_item_properties(), + }, + ))); + }); + + // The background image is painted on top of the background color. + // Implements background image, per spec: + // http://www.w3.org/TR/CSS21/colors.html#background + let background = style.get_background(); + for (i, background_image) in background.background_image.0.iter().enumerate().rev() { + let background_image = match *background_image { + ImageLayer::None => continue, + ImageLayer::Image(ref image) => image, + }; + + match *background_image { + Image::Gradient(ref gradient) => { + self.build_display_list_for_background_gradient( + state, + display_list_section, + absolute_bounds, + gradient, + style, + i, + ); + }, + Image::Url(ref image_url) => { + if let Some(url) = image_url.url() { + let webrender_image = state.layout_context.get_webrender_image_for_url( + self.node, + url.clone(), + UsePlaceholder::No, + ); + if let Some(webrender_image) = webrender_image { + self.build_display_list_for_webrender_image( + state, + style, + display_list_section, + absolute_bounds, + webrender_image, + i, + ); + } + } + }, + Image::PaintWorklet(ref paint_worklet) => { + let bounding_box = self.border_box - style.logical_border_width(); + let bounding_box_size = bounding_box.size.to_physical(style.writing_mode); + let background_size = + get_cyclic(&style.get_background().background_size.0, i).clone(); + let size = match background_size { + BackgroundSize::ExplicitSize { width, height } => Size2D::new( + width + .to_used_value(bounding_box_size.width) + .unwrap_or(bounding_box_size.width), + height + .to_used_value(bounding_box_size.height) + .unwrap_or(bounding_box_size.height), + ), + _ => bounding_box_size, + }; + let webrender_image = self.get_webrender_image_for_paint_worklet( + state, + style, + paint_worklet, + size, + ); + if let Some(webrender_image) = webrender_image { + self.build_display_list_for_webrender_image( + state, + style, + display_list_section, + absolute_bounds, + webrender_image, + i, + ); + } + }, + Image::Rect(_) => { + // TODO: Implement `-moz-image-rect` + }, + Image::Element(_) => { + // TODO: Implement `-moz-element` + }, + } + } + } + + /// Adds the display items necessary to paint a webrender image of this fragment to the + /// appropriate section of the display list. + fn build_display_list_for_webrender_image( + &self, + state: &mut DisplayListBuildState, + style: &ComputedValues, + display_list_section: DisplayListSection, + absolute_bounds: Rect<Au>, + webrender_image: WebRenderImageInfo, + index: usize, + ) { + debug!("(building display list) building background image"); + if webrender_image.key.is_none() { + return; + } + + let image = Size2D::new( + Au::from_px(webrender_image.width as i32), + Au::from_px(webrender_image.height as i32), + ); + let placement = background::placement( + style.get_background(), + state.layout_context.shared_context().viewport_size(), + absolute_bounds, + Some(image), + style.logical_border_width().to_physical(style.writing_mode), + self.border_padding.to_physical(self.style.writing_mode), + border::radii(absolute_bounds, style.get_border()), + index, + ); + + state.clipping_and_scrolling_scope(|state| { + if !placement.clip_radii.is_zero() { + let clip_id = + state.add_late_clip_node(placement.clip_rect.to_layout(), placement.clip_radii); + state.current_clipping_and_scrolling = ClippingAndScrolling::simple(clip_id); + } + + // Create the image display item. + let base = state.create_base_display_item( + placement.clip_rect, + self.node, + get_cursor(&style, Cursor::Default), + display_list_section, + ); + + debug!("(building display list) adding background image."); + state.add_image_item( + base, + webrender_api::ImageDisplayItem { + bounds: placement.bounds.to_f32_px(), + common: items::empty_common_item_properties(), + image_key: webrender_image.key.unwrap(), + stretch_size: placement.tile_size.to_layout(), + tile_spacing: placement.tile_spacing.to_layout(), + image_rendering: style.get_inherited_box().image_rendering.to_layout(), + alpha_type: webrender_api::AlphaType::PremultipliedAlpha, + color: webrender_api::ColorF::WHITE, + }, + ); + }); + } + + /// Calculates the webrender image for a paint worklet. + /// Returns None if the worklet is not registered. + /// If the worklet has missing image URLs, it passes them to the image cache for loading. + fn get_webrender_image_for_paint_worklet( + &self, + state: &mut DisplayListBuildState, + style: &ComputedValues, + paint_worklet: &PaintWorklet, + size_in_au: Size2D<Au>, + ) -> Option<WebRenderImageInfo> { + let device_pixel_ratio = state.layout_context.style_context.device_pixel_ratio(); + let size_in_px = + TypedSize2D::new(size_in_au.width.to_f32_px(), size_in_au.height.to_f32_px()); + + // TODO: less copying. + let name = paint_worklet.name.clone(); + let arguments = paint_worklet + .arguments + .iter() + .map(|argument| argument.to_css_string()) + .collect(); + + let draw_result = match state.layout_context.registered_painters.get(&name) { + Some(painter) => { + debug!( + "Drawing a paint image {}({},{}).", + name, size_in_px.width, size_in_px.height + ); + let properties = painter + .properties() + .iter() + .filter_map(|(name, id)| id.as_shorthand().err().map(|id| (name, id))) + .map(|(name, id)| (name.clone(), style.computed_value_to_string(id))) + .collect(); + painter.draw_a_paint_image(size_in_px, device_pixel_ratio, properties, arguments) + }, + None => { + debug!("Worklet {} called before registration.", name); + return None; + }, + }; + + if let Ok(draw_result) = draw_result { + let webrender_image = WebRenderImageInfo { + width: draw_result.width, + height: draw_result.height, + key: draw_result.image_key, + }; + + for url in draw_result.missing_image_urls.into_iter() { + debug!("Requesting missing image URL {}.", url); + state.layout_context.get_webrender_image_for_url( + self.node, + url, + UsePlaceholder::No, + ); + } + Some(webrender_image) + } else { + None + } + } + + /// Adds the display items necessary to paint the background linear gradient of this fragment + /// to the appropriate section of the display list. + fn build_display_list_for_background_gradient( + &self, + state: &mut DisplayListBuildState, + display_list_section: DisplayListSection, + absolute_bounds: Rect<Au>, + gradient: &Gradient, + style: &ComputedValues, + index: usize, + ) { + let placement = background::placement( + style.get_background(), + state.layout_context.shared_context().viewport_size(), + absolute_bounds, + None, + style.logical_border_width().to_physical(style.writing_mode), + self.border_padding.to_physical(self.style.writing_mode), + border::radii(absolute_bounds, style.get_border()), + index, + ); + + state.clipping_and_scrolling_scope(|state| { + if !placement.clip_radii.is_zero() { + let clip_id = + state.add_late_clip_node(placement.clip_rect.to_layout(), placement.clip_radii); + state.current_clipping_and_scrolling = ClippingAndScrolling::simple(clip_id); + } + + let base = state.create_base_display_item( + placement.clip_rect, + self.node, + get_cursor(&style, Cursor::Default), + display_list_section, + ); + + let display_item = match gradient.kind { + GradientKind::Linear(angle_or_corner) => { + let (gradient, stops) = gradient::linear( + style, + placement.tile_size, + &gradient.items[..], + angle_or_corner, + gradient.repeating, + ); + let item = webrender_api::GradientDisplayItem { + gradient, + bounds: placement.bounds.to_f32_px(), + common: items::empty_common_item_properties(), + tile_size: placement.tile_size.to_layout(), + tile_spacing: placement.tile_spacing.to_layout(), + }; + DisplayItem::Gradient(CommonDisplayItem::with_data(base, item, stops)) + }, + GradientKind::Radial(shape, center) => { + let (gradient, stops) = gradient::radial( + style, + placement.tile_size, + &gradient.items[..], + shape, + center, + gradient.repeating, + ); + let item = webrender_api::RadialGradientDisplayItem { + gradient, + bounds: placement.bounds.to_f32_px(), + common: items::empty_common_item_properties(), + tile_size: placement.tile_size.to_layout(), + tile_spacing: placement.tile_spacing.to_layout(), + }; + DisplayItem::RadialGradient(CommonDisplayItem::with_data(base, item, stops)) + }, + }; + state.add_display_item(display_item); + }); + } + + /// Adds the display items necessary to paint the box shadow of this fragment to the display + /// list if necessary. + fn build_display_list_for_box_shadow_if_applicable( + &self, + state: &mut DisplayListBuildState, + style: &ComputedValues, + display_list_section: DisplayListSection, + absolute_bounds: Rect<Au>, + clip: Rect<Au>, + ) { + // NB: According to CSS-BACKGROUNDS, box shadows render in *reverse* order (front to back). + for box_shadow in style.get_effects().box_shadow.0.iter().rev() { + let base = state.create_base_display_item( + clip, + self.node, + get_cursor(&style, Cursor::Default), + display_list_section, + ); + let border_radius = border::radii(absolute_bounds, style.get_border()); + state.add_display_item(DisplayItem::BoxShadow(CommonDisplayItem::new( + base, + webrender_api::BoxShadowDisplayItem { + common: items::empty_common_item_properties(), + box_bounds: absolute_bounds.to_layout(), + color: style.resolve_color(box_shadow.base.color).to_layout(), + offset: LayoutVector2D::new( + box_shadow.base.horizontal.px(), + box_shadow.base.vertical.px(), + ), + blur_radius: box_shadow.base.blur.px(), + spread_radius: box_shadow.spread.px(), + border_radius: border_radius, + clip_mode: if box_shadow.inset { + BoxShadowClipMode::Inset + } else { + BoxShadowClipMode::Outset + }, + }, + ))); + } + } + + /// Adds the display items necessary to paint the borders of this fragment to a display list if + /// necessary. + fn build_display_list_for_borders_if_applicable( + &self, + state: &mut DisplayListBuildState, + style: &ComputedValues, + inline_info: Option<InlineNodeBorderInfo>, + border_painting_mode: BorderPaintingMode, + mut bounds: Rect<Au>, + display_list_section: DisplayListSection, + clip: Rect<Au>, + ) { + let mut border = style.logical_border_width(); + + if let Some(inline_info) = inline_info { + modify_border_width_for_inline_sides(&mut border, inline_info); + } + + match border_painting_mode { + BorderPaintingMode::Separate => {}, + BorderPaintingMode::Collapse(collapsed_borders) => { + collapsed_borders.adjust_border_widths_for_painting(&mut border) + }, + BorderPaintingMode::Hidden => return, + } + + let border_style_struct = style.get_border(); + let mut colors = SideOffsets2D::new( + border_style_struct.border_top_color, + border_style_struct.border_right_color, + border_style_struct.border_bottom_color, + border_style_struct.border_left_color, + ); + let mut border_style = SideOffsets2D::new( + border_style_struct.border_top_style, + border_style_struct.border_right_style, + border_style_struct.border_bottom_style, + border_style_struct.border_left_style, + ); + + if let BorderPaintingMode::Collapse(collapsed_borders) = border_painting_mode { + collapsed_borders.adjust_border_colors_and_styles_for_painting( + &mut colors, + &mut border_style, + style.writing_mode, + ); + } + + // If this border collapses, then we draw outside the boundaries we were given. + if let BorderPaintingMode::Collapse(collapsed_borders) = border_painting_mode { + collapsed_borders.adjust_border_bounds_for_painting(&mut bounds, style.writing_mode) + } + + // Append the border to the display list. + let base = state.create_base_display_item( + clip, + self.node, + get_cursor(&style, Cursor::Default), + display_list_section, + ); + + let border_radius = border::radii(bounds, border_style_struct); + let border_widths = border.to_physical(style.writing_mode); + + if let ImageLayer::Image(ref image) = border_style_struct.border_image_source { + if self + .build_display_list_for_border_image( + state, + style, + base.clone(), + bounds, + image, + border_widths, + ) + .is_some() + { + return; + } + // Fallback to rendering a solid border. + } + if border_widths == SideOffsets2D::zero() { + return; + } + let details = BorderDetails::Normal(NormalBorder { + left: BorderSide { + color: style.resolve_color(colors.left).to_layout(), + style: border_style.left.to_layout(), + }, + right: BorderSide { + color: style.resolve_color(colors.right).to_layout(), + style: border_style.right.to_layout(), + }, + top: BorderSide { + color: style.resolve_color(colors.top).to_layout(), + style: border_style.top.to_layout(), + }, + bottom: BorderSide { + color: style.resolve_color(colors.bottom).to_layout(), + style: border_style.bottom.to_layout(), + }, + radius: border_radius, + do_aa: true, + }); + state.add_display_item(DisplayItem::Border(CommonDisplayItem::with_data( + base, + webrender_api::BorderDisplayItem { + bounds: bounds.to_layout(), + common: items::empty_common_item_properties(), + widths: border_widths.to_layout(), + details, + }, + Vec::new(), + ))); + } + + /// Add display item for image border. + /// + /// Returns `Some` if the addition was successful. + fn build_display_list_for_border_image( + &self, + state: &mut DisplayListBuildState, + style: &ComputedValues, + base: BaseDisplayItem, + bounds: Rect<Au>, + image: &Image, + border_width: SideOffsets2D<Au>, + ) -> Option<()> { + let border_style_struct = style.get_border(); + let border_image_outset = + border::image_outset(border_style_struct.border_image_outset, border_width); + let border_image_area = bounds.outer_rect(border_image_outset).size; + let border_image_width = border::image_width( + &border_style_struct.border_image_width, + border_width.to_layout(), + border_image_area, + ); + let border_image_repeat = &border_style_struct.border_image_repeat; + let border_image_fill = border_style_struct.border_image_slice.fill; + let border_image_slice = &border_style_struct.border_image_slice.offsets; + + let mut stops = Vec::new(); + let mut width = border_image_area.width.to_px() as u32; + let mut height = border_image_area.height.to_px() as u32; + let source = match image { + Image::Url(ref image_url) => { + let url = image_url.url()?; + let image = state.layout_context.get_webrender_image_for_url( + self.node, + url.clone(), + UsePlaceholder::No, + )?; + width = image.width; + height = image.height; + NinePatchBorderSource::Image(image.key?) + }, + Image::PaintWorklet(ref paint_worklet) => { + let image = self.get_webrender_image_for_paint_worklet( + state, + style, + paint_worklet, + border_image_area, + )?; + width = image.width; + height = image.height; + NinePatchBorderSource::Image(image.key?) + }, + Image::Gradient(ref gradient) => match gradient.kind { + GradientKind::Linear(angle_or_corner) => { + let (wr_gradient, linear_stops) = gradient::linear( + style, + border_image_area, + &gradient.items[..], + angle_or_corner, + gradient.repeating, + ); + stops = linear_stops; + NinePatchBorderSource::Gradient(wr_gradient) + }, + GradientKind::Radial(shape, center) => { + let (wr_gradient, radial_stops) = gradient::radial( + style, + border_image_area, + &gradient.items[..], + shape, + center, + gradient.repeating, + ); + stops = radial_stops; + NinePatchBorderSource::RadialGradient(wr_gradient) + }, + }, + _ => return None, + }; + + let details = BorderDetails::NinePatch(NinePatchBorder { + source, + width: width as i32, + height: height as i32, + slice: border::image_slice(border_image_slice, width as i32, height as i32), + fill: border_image_fill, + repeat_horizontal: border_image_repeat.0.to_layout(), + repeat_vertical: border_image_repeat.1.to_layout(), + outset: SideOffsets2D::new( + border_image_outset.top.to_f32_px(), + border_image_outset.right.to_f32_px(), + border_image_outset.bottom.to_f32_px(), + border_image_outset.left.to_f32_px(), + ), + }); + state.add_display_item(DisplayItem::Border(CommonDisplayItem::with_data( + base, + webrender_api::BorderDisplayItem { + bounds: bounds.to_layout(), + common: items::empty_common_item_properties(), + widths: border_image_width, + details, + }, + stops, + ))); + Some(()) + } + + /// Adds the display items necessary to paint the outline of this fragment to the display list + /// if necessary. + fn build_display_list_for_outline_if_applicable( + &self, + state: &mut DisplayListBuildState, + style: &ComputedValues, + mut bounds: Rect<Au>, + clip: Rect<Au>, + ) { + use style::values::specified::outline::OutlineStyle; + + let width = Au::from(style.get_outline().outline_width); + if width == Au(0) { + return; + } + + let outline_style = match style.get_outline().outline_style { + OutlineStyle::Auto => BorderStyle::Solid, + // FIXME(emilio): I don't think this border-style check is + // necessary, since border-style: none implies an outline-width of + // zero at computed value time. + OutlineStyle::BorderStyle(BorderStyle::None) => return, + OutlineStyle::BorderStyle(s) => s, + }; + + // Outlines are not accounted for in the dimensions of the border box, so adjust the + // absolute bounds. + let offset = width + Au::from(style.get_outline().outline_offset); + bounds = bounds.inflate(offset, offset); + + // Append the outline to the display list. + let color = style + .resolve_color(style.get_outline().outline_color) + .to_layout(); + let base = state.create_base_display_item( + clip, + self.node, + get_cursor(&style, Cursor::Default), + DisplayListSection::Outlines, + ); + state.add_display_item(DisplayItem::Border(CommonDisplayItem::with_data( + base, + webrender_api::BorderDisplayItem { + bounds: bounds.to_layout(), + common: items::empty_common_item_properties(), + widths: SideOffsets2D::new_all_same(width).to_layout(), + details: BorderDetails::Normal(border::simple(color, outline_style.to_layout())), + }, + Vec::new(), + ))); + } + + /// Adds display items necessary to draw debug boxes around a scanned text fragment. + fn build_debug_borders_around_text_fragments( + &self, + state: &mut DisplayListBuildState, + style: &ComputedValues, + stacking_relative_border_box: Rect<Au>, + stacking_relative_content_box: Rect<Au>, + text_fragment: &ScannedTextFragmentInfo, + clip: Rect<Au>, + ) { + // FIXME(pcwalton, #2795): Get the real container size. + let container_size = Size2D::zero(); + + // Compute the text fragment bounds and draw a border surrounding them. + let base = state.create_base_display_item( + clip, + self.node, + get_cursor(&style, Cursor::Default), + DisplayListSection::Content, + ); + state.add_display_item(DisplayItem::Border(CommonDisplayItem::with_data( + base, + webrender_api::BorderDisplayItem { + bounds: stacking_relative_border_box.to_layout(), + common: items::empty_common_item_properties(), + widths: SideOffsets2D::new_all_same(Au::from_px(1)).to_layout(), + details: BorderDetails::Normal(border::simple( + ColorU::new(0, 0, 200, 1).into(), + webrender_api::BorderStyle::Solid, + )), + }, + Vec::new(), + ))); + + // Draw a rectangle representing the baselines. + let mut baseline = LogicalRect::from_physical( + self.style.writing_mode, + stacking_relative_content_box, + container_size, + ); + baseline.start.b = baseline.start.b + text_fragment.run.ascent(); + baseline.size.block = Au(0); + let baseline = baseline.to_physical(self.style.writing_mode, container_size); + + let base = state.create_base_display_item( + clip, + self.node, + get_cursor(&style, Cursor::Default), + DisplayListSection::Content, + ); + // TODO(gw): Use a better estimate for wavy line thickness. + let area = baseline.to_layout(); + let wavy_line_thickness = (0.33 * area.size.height).ceil(); + state.add_display_item(DisplayItem::Line(CommonDisplayItem::new( + base, + webrender_api::LineDisplayItem { + common: items::empty_common_item_properties(), + area, + orientation: webrender_api::LineOrientation::Horizontal, + wavy_line_thickness, + color: ColorU::new(0, 200, 0, 1).into(), + style: LineStyle::Dashed, + }, + ))); + } + + /// Adds display items necessary to draw debug boxes around this fragment. + fn build_debug_borders_around_fragment( + &self, + state: &mut DisplayListBuildState, + stacking_relative_border_box: Rect<Au>, + clip: Rect<Au>, + ) { + // This prints a debug border around the border of this fragment. + let base = state.create_base_display_item( + clip, + self.node, + get_cursor(&self.style, Cursor::Default), + DisplayListSection::Content, + ); + state.add_display_item(DisplayItem::Border(CommonDisplayItem::with_data( + base, + webrender_api::BorderDisplayItem { + bounds: stacking_relative_border_box.to_layout(), + common: items::empty_common_item_properties(), + widths: SideOffsets2D::new_all_same(Au::from_px(1)).to_layout(), + details: BorderDetails::Normal(border::simple( + ColorU::new(0, 0, 200, 1).into(), + webrender_api::BorderStyle::Solid, + )), + }, + Vec::new(), + ))); + } + + /// Builds the display items necessary to paint the selection and/or caret for this fragment, + /// if any. + fn build_display_items_for_selection_if_necessary( + &self, + state: &mut DisplayListBuildState, + stacking_relative_border_box: Rect<Au>, + display_list_section: DisplayListSection, + ) { + let scanned_text_fragment_info = match self.specific { + SpecificFragmentInfo::ScannedText(ref scanned_text_fragment_info) => { + scanned_text_fragment_info + }, + _ => return, + }; + + // Draw a highlighted background if the text is selected. + // + // TODO: Allow non-text fragments to be selected too. + if scanned_text_fragment_info.selected() { + let style = self.selected_style(); + let background_color = style.resolve_color(style.get_background().background_color); + let base = state.create_base_display_item( + stacking_relative_border_box, + self.node, + get_cursor(&self.style, Cursor::Default), + display_list_section, + ); + state.add_display_item(DisplayItem::Rectangle(CommonDisplayItem::new( + base, + webrender_api::RectangleDisplayItem { + common: items::empty_common_item_properties(), + color: background_color.to_layout(), + }, + ))); + } + + // Draw a caret at the insertion point. + let insertion_point_index = match scanned_text_fragment_info.insertion_point { + Some(insertion_point_index) => insertion_point_index, + None => return, + }; + let range = Range::new( + scanned_text_fragment_info.range.begin(), + insertion_point_index - scanned_text_fragment_info.range.begin(), + ); + let advance = scanned_text_fragment_info.run.advance_for_range(&range); + + let insertion_point_bounds; + let cursor; + if !self.style.writing_mode.is_vertical() { + insertion_point_bounds = rect( + stacking_relative_border_box.origin.x + advance, + stacking_relative_border_box.origin.y, + INSERTION_POINT_LOGICAL_WIDTH, + stacking_relative_border_box.size.height, + ); + cursor = Cursor::Text; + } else { + insertion_point_bounds = rect( + stacking_relative_border_box.origin.x, + stacking_relative_border_box.origin.y + advance, + stacking_relative_border_box.size.width, + INSERTION_POINT_LOGICAL_WIDTH, + ); + cursor = Cursor::VerticalText; + }; + + let base = state.create_base_display_item( + insertion_point_bounds, + self.node, + get_cursor(&self.style, cursor), + display_list_section, + ); + state.add_display_item(DisplayItem::Rectangle(CommonDisplayItem::new( + base, + webrender_api::RectangleDisplayItem { + common: items::empty_common_item_properties(), + color: self.style().get_inherited_text().color.to_layout(), + }, + ))); + } + + /// Adds the display items for this fragment to the given display list. + /// + /// Arguments: + /// + /// * `state`: The display building state, including the display list currently + /// under construction and other metadata useful for constructing it. + /// * `dirty`: The dirty rectangle in the coordinate system of the owning flow. + /// * `clip`: The region to clip the display items to. + /// * `overflow_content_size`: The size of content associated with this fragment + /// that must have overflow handling applied to it. For a scrollable block + /// flow, it is expected that this is the size of the child boxes. + pub fn build_display_list( + &mut self, + state: &mut DisplayListBuildState, + stacking_relative_border_box: Rect<Au>, + border_painting_mode: BorderPaintingMode, + display_list_section: DisplayListSection, + clip: Rect<Au>, + overflow_content_size: Option<Size2D<Au>>, + ) { + let previous_clipping_and_scrolling = state.current_clipping_and_scrolling; + if let Some(index) = self.established_reference_frame { + state.current_clipping_and_scrolling = ClippingAndScrolling::simple(index); + } + + self.restyle_damage.remove(ServoRestyleDamage::REPAINT); + self.build_display_list_no_damage( + state, + stacking_relative_border_box, + border_painting_mode, + display_list_section, + clip, + overflow_content_size, + ); + + state.current_clipping_and_scrolling = previous_clipping_and_scrolling; + } + + /// build_display_list, but don't update the restyle damage + /// + /// Must be paired with a self.restyle_damage.remove(REPAINT) somewhere + fn build_display_list_no_damage( + &self, + state: &mut DisplayListBuildState, + stacking_relative_border_box: Rect<Au>, + border_painting_mode: BorderPaintingMode, + display_list_section: DisplayListSection, + clip: Rect<Au>, + overflow_content_size: Option<Size2D<Au>>, + ) { + if self.style().get_inherited_box().visibility != Visibility::Visible { + return; + } + + debug!( + "Fragment::build_display_list at rel={:?}, abs={:?}: {:?}", + self.border_box, stacking_relative_border_box, self + ); + + // Check the clip rect. If there's nothing to render at all, don't even construct display + // list items. + let empty_rect = !clip.intersects(&stacking_relative_border_box); + if self.is_primary_fragment() && !empty_rect { + // Add shadows, background, borders, and outlines, if applicable. + if let Some(ref inline_context) = self.inline_context { + for node in inline_context.nodes.iter().rev() { + self.build_display_list_for_background_if_applicable( + state, + &*node.style, + display_list_section, + stacking_relative_border_box, + ); + + self.build_display_list_for_box_shadow_if_applicable( + state, + &*node.style, + display_list_section, + stacking_relative_border_box, + clip, + ); + + self.build_display_list_for_borders_if_applicable( + state, + &*node.style, + Some(InlineNodeBorderInfo { + is_first_fragment_of_element: node + .flags + .contains(InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT), + is_last_fragment_of_element: node + .flags + .contains(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT), + }), + border_painting_mode, + stacking_relative_border_box, + display_list_section, + clip, + ); + + // FIXME(emilio): Why does outline not do the same width + // fixup as border? + self.build_display_list_for_outline_if_applicable( + state, + &*node.style, + stacking_relative_border_box, + clip, + ); + } + } + + if !self.is_scanned_text_fragment() { + self.build_display_list_for_background_if_applicable( + state, + &*self.style, + display_list_section, + stacking_relative_border_box, + ); + + self.build_display_list_for_box_shadow_if_applicable( + state, + &*self.style, + display_list_section, + stacking_relative_border_box, + clip, + ); + + self.build_display_list_for_borders_if_applicable( + state, + &*self.style, + /* inline_node_info = */ None, + border_painting_mode, + stacking_relative_border_box, + display_list_section, + clip, + ); + + self.build_display_list_for_outline_if_applicable( + state, + &*self.style, + stacking_relative_border_box, + clip, + ); + } + } + + if self.is_primary_fragment() { + // Paint the selection point if necessary. Even an empty text fragment may have an + // insertion point, so we do this even if `empty_rect` is true. + self.build_display_items_for_selection_if_necessary( + state, + stacking_relative_border_box, + display_list_section, + ); + } + + if empty_rect { + return; + } + + debug!("Fragment::build_display_list: intersected. Adding display item..."); + + if let Some(content_size) = overflow_content_size { + // Create a transparent rectangle for hit-testing purposes that exists in front + // of this fragment's background but behind its content. This ensures that any + // hit tests inside the content box but not on actual content target the current + // scrollable ancestor. + let content_size = TypedRect::new(stacking_relative_border_box.origin, content_size); + let base = state.create_base_display_item_with_clipping_and_scrolling( + content_size, + self.node, + // FIXME(emilio): Why does this ignore pointer-events? + get_cursor(&self.style, Cursor::Default).or(Some(Cursor::Default)), + display_list_section, + state.current_clipping_and_scrolling, + ); + state.add_display_item(DisplayItem::Rectangle(CommonDisplayItem::new( + base, + webrender_api::RectangleDisplayItem { + common: items::empty_common_item_properties(), + color: ColorF::TRANSPARENT, + }, + ))); + } + + // Create special per-fragment-type display items. + state.clipping_and_scrolling_scope(|state| { + self.build_fragment_type_specific_display_items( + state, + stacking_relative_border_box, + clip, + ); + }); + + if opts::get().show_debug_fragment_borders { + self.build_debug_borders_around_fragment(state, stacking_relative_border_box, clip) + } + } + + /// A helper method that `build_display_list` calls to create per-fragment-type display items. + fn build_fragment_type_specific_display_items( + &self, + state: &mut DisplayListBuildState, + stacking_relative_border_box: Rect<Au>, + clip: Rect<Au>, + ) { + // Compute the context box position relative to the parent stacking context. + let stacking_relative_content_box = + self.stacking_relative_content_box(stacking_relative_border_box); + + let create_base_display_item = |state: &mut DisplayListBuildState| { + // Adjust the clipping region as necessary to account for `border-radius`. + let radii = + build_border_radius_for_inner_rect(stacking_relative_border_box, &self.style); + + if !radii.is_zero() { + let clip_id = + state.add_late_clip_node(stacking_relative_border_box.to_layout(), radii); + state.current_clipping_and_scrolling = ClippingAndScrolling::simple(clip_id); + } + + state.create_base_display_item( + stacking_relative_border_box, + self.node, + get_cursor(&self.style, Cursor::Default), + DisplayListSection::Content, + ) + }; + + match self.specific { + SpecificFragmentInfo::TruncatedFragment(ref truncated_fragment) + if truncated_fragment.text_info.is_some() => + { + let text_fragment = truncated_fragment.text_info.as_ref().unwrap(); + // Create the main text display item. + self.build_display_list_for_text_fragment( + state, + &text_fragment, + stacking_relative_content_box, + &self.style.get_inherited_text().text_shadow.0, + clip, + ); + + if opts::get().show_debug_fragment_borders { + self.build_debug_borders_around_text_fragments( + state, + self.style(), + stacking_relative_border_box, + stacking_relative_content_box, + &text_fragment, + clip, + ); + } + } + SpecificFragmentInfo::ScannedText(ref text_fragment) => { + // Create the main text display item. + self.build_display_list_for_text_fragment( + state, + &text_fragment, + stacking_relative_content_box, + &self.style.get_inherited_text().text_shadow.0, + clip, + ); + + if opts::get().show_debug_fragment_borders { + self.build_debug_borders_around_text_fragments( + state, + self.style(), + stacking_relative_border_box, + stacking_relative_content_box, + &text_fragment, + clip, + ); + } + }, + SpecificFragmentInfo::Generic | + SpecificFragmentInfo::GeneratedContent(..) | + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TableWrapper | + SpecificFragmentInfo::Multicol | + SpecificFragmentInfo::MulticolColumn | + SpecificFragmentInfo::InlineBlock(_) | + SpecificFragmentInfo::InlineAbsoluteHypothetical(_) | + SpecificFragmentInfo::InlineAbsolute(_) | + SpecificFragmentInfo::TruncatedFragment(_) | + SpecificFragmentInfo::Svg(_) => { + if opts::get().show_debug_fragment_borders { + self.build_debug_borders_around_fragment( + state, + stacking_relative_border_box, + clip, + ); + } + }, + SpecificFragmentInfo::Iframe(ref fragment_info) => { + if !stacking_relative_content_box.is_empty() { + let browsing_context_id = match fragment_info.browsing_context_id { + Some(browsing_context_id) => browsing_context_id, + None => return warn!("No browsing context id for iframe."), + }; + let pipeline_id = match fragment_info.pipeline_id { + Some(pipeline_id) => pipeline_id, + None => return warn!("No pipeline id for iframe {}.", browsing_context_id), + }; + + let base = create_base_display_item(state); + let bounds = stacking_relative_content_box.to_layout(); + let item = DisplayItem::Iframe(Box::new(IframeDisplayItem { + base, + bounds, + iframe: pipeline_id, + })); + + // XXXjdm: This sleight-of-hand to convert LayoutRect -> Size2D<CSSPixel> + // looks bogus. + let size = Size2D::new(bounds.size.width, bounds.size.height); + state.iframe_sizes.push(IFrameSize { + id: browsing_context_id, + size: TypedSize2D::from_untyped(&size), + }); + + state.add_display_item(item); + } + }, + SpecificFragmentInfo::Image(ref image_fragment) => { + // Place the image into the display list. + if let Some(ref image) = image_fragment.image { + if let Some(id) = image.id { + let base = create_base_display_item(state); + state.add_image_item( + base, + webrender_api::ImageDisplayItem { + bounds: stacking_relative_content_box.to_layout(), + common: items::empty_common_item_properties(), + image_key: id, + stretch_size: stacking_relative_content_box.size.to_layout(), + tile_spacing: LayoutSize::zero(), + image_rendering: self + .style + .get_inherited_box() + .image_rendering + .to_layout(), + alpha_type: webrender_api::AlphaType::PremultipliedAlpha, + color: webrender_api::ColorF::WHITE, + }, + ); + } + } + }, + SpecificFragmentInfo::Media(ref fragment_info) => { + if let Some((ref image_key, _, _)) = fragment_info.current_frame { + let base = create_base_display_item(state); + state.add_image_item( + base, + webrender_api::ImageDisplayItem { + bounds: stacking_relative_content_box.to_layout(), + common: items::empty_common_item_properties(), + image_key: *image_key, + stretch_size: stacking_relative_border_box.size.to_layout(), + tile_spacing: LayoutSize::zero(), + image_rendering: ImageRendering::Auto, + alpha_type: webrender_api::AlphaType::PremultipliedAlpha, + color: webrender_api::ColorF::WHITE, + }, + ); + } + }, + SpecificFragmentInfo::Canvas(ref canvas_fragment_info) => { + let image_key = match canvas_fragment_info.source { + CanvasFragmentSource::WebGL(image_key) => image_key, + CanvasFragmentSource::Image(ref ipc_renderer) => match *ipc_renderer { + Some(ref ipc_renderer) => { + let ipc_renderer = ipc_renderer.lock().unwrap(); + let (sender, receiver) = ipc::channel().unwrap(); + ipc_renderer + .send(CanvasMsg::FromLayout( + FromLayoutMsg::SendData(sender), + canvas_fragment_info.canvas_id.clone(), + )) + .unwrap(); + receiver.recv().unwrap().image_key + }, + None => return, + }, + }; + + let base = create_base_display_item(state); + let display_item = webrender_api::ImageDisplayItem { + bounds: stacking_relative_border_box.to_layout(), + common: items::empty_common_item_properties(), + image_key, + stretch_size: stacking_relative_content_box.size.to_layout(), + tile_spacing: LayoutSize::zero(), + image_rendering: ImageRendering::Auto, + alpha_type: webrender_api::AlphaType::PremultipliedAlpha, + color: webrender_api::ColorF::WHITE, + }; + + state.add_image_item(base, display_item); + }, + SpecificFragmentInfo::UnscannedText(_) => { + panic!("Shouldn't see unscanned fragments here.") + }, + SpecificFragmentInfo::TableColumn(_) => { + panic!("Shouldn't see table column fragments here.") + }, + } + } + + /// Creates a stacking context for associated fragment. + fn create_stacking_context( + &self, + id: StackingContextId, + base_flow: &BaseFlow, + context_type: StackingContextType, + established_reference_frame: Option<ClipScrollNodeIndex>, + parent_clipping_and_scrolling: ClippingAndScrolling, + ) -> StackingContext { + let border_box = self.stacking_relative_border_box( + &base_flow.stacking_relative_position, + &base_flow + .early_absolute_position_info + .relative_containing_block_size, + base_flow + .early_absolute_position_info + .relative_containing_block_mode, + CoordinateSystem::Parent, + ); + // First, compute the offset of our border box (including relative positioning) + // from our flow origin, since that is what `BaseFlow::overflow` is relative to. + let border_box_offset = border_box + .translate(&-base_flow.stacking_relative_position) + .origin; + // Then, using that, compute our overflow region relative to our border box. + let overflow = base_flow + .overflow + .paint + .translate(&-border_box_offset.to_vector()); + + // Create the filter pipeline. + let effects = self.style().get_effects(); + let mut filters: Vec<FilterOp> = effects.filter.0.iter().map(ToLayout::to_layout).collect(); + if effects.opacity != 1.0 { + filters.push(FilterOp::Opacity(effects.opacity.into(), effects.opacity)); + } + + StackingContext::new( + id, + context_type, + border_box.to_layout(), + overflow.to_layout(), + self.effective_z_index(), + self.style().get_box()._servo_top_layer, + filters, + self.style().get_effects().mix_blend_mode.to_layout(), + self.transform_matrix(&border_box), + self.style().get_used_transform_style().to_layout(), + self.perspective_matrix(&border_box), + parent_clipping_and_scrolling, + established_reference_frame, + ) + } + + /// Creates the text display item for one text fragment. This can be called multiple times for + /// one fragment if there are text shadows. + /// + /// `text_shadow` will be `Some` if this is rendering a shadow. + fn build_display_list_for_text_fragment( + &self, + state: &mut DisplayListBuildState, + text_fragment: &ScannedTextFragmentInfo, + stacking_relative_content_box: Rect<Au>, + text_shadows: &[SimpleShadow], + clip: Rect<Au>, + ) { + // NB: The order for painting text components (CSS Text Decoration Module Level 3) is: + // shadows, underline, overline, text, text-emphasis, and then line-through. + + // TODO(emilio): Allow changing more properties by ::selection + // Paint the text with the color as described in its styling. + let text_color = if text_fragment.selected() { + self.selected_style().get_inherited_text().color + } else { + self.style().get_inherited_text().color + }; + + // Determine the orientation and cursor to use. + let (_orientation, cursor) = if self.style.writing_mode.is_vertical() { + // TODO: Distinguish between 'sideways-lr' and 'sideways-rl' writing modes in CSS + // Writing Modes Level 4. + (TextOrientation::SidewaysRight, Cursor::VerticalText) + } else { + (TextOrientation::Upright, Cursor::Text) + }; + + // Compute location of the baseline. + // + // FIXME(pcwalton): Get the real container size. + let container_size = Size2D::zero(); + let metrics = &text_fragment.run.font_metrics; + let baseline_origin = stacking_relative_content_box.origin + + LogicalPoint::new(self.style.writing_mode, Au(0), metrics.ascent) + .to_physical(self.style.writing_mode, container_size) + .to_vector(); + + // Base item for all text/shadows + let base = state.create_base_display_item( + clip, + self.node, + get_cursor(&self.style, cursor), + DisplayListSection::Content, + ); + + // NB: According to CSS-BACKGROUNDS, text shadows render in *reverse* order (front + // to back). + + // Shadows + for shadow in text_shadows.iter().rev() { + state.add_display_item(DisplayItem::PushTextShadow(Box::new( + PushTextShadowDisplayItem { + base: base.clone(), + shadow: webrender_api::Shadow { + offset: LayoutVector2D::new(shadow.horizontal.px(), shadow.vertical.px()), + color: self.style.resolve_color(shadow.color).to_layout(), + blur_radius: shadow.blur.px(), + }, + }, + ))); + } + + // Create display items for text decorations. + let text_decorations = self.style().get_inherited_text().text_decorations_in_effect; + + let logical_stacking_relative_content_box = LogicalRect::from_physical( + self.style.writing_mode, + stacking_relative_content_box, + container_size, + ); + + // Underline + if text_decorations.underline { + let mut stacking_relative_box = logical_stacking_relative_content_box; + stacking_relative_box.start.b = logical_stacking_relative_content_box.start.b + + metrics.ascent - + metrics.underline_offset; + stacking_relative_box.size.block = metrics.underline_size; + self.build_display_list_for_text_decoration( + state, + &text_color, + &stacking_relative_box, + clip, + ); + } + + // Overline + if text_decorations.overline { + let mut stacking_relative_box = logical_stacking_relative_content_box; + stacking_relative_box.size.block = metrics.underline_size; + self.build_display_list_for_text_decoration( + state, + &text_color, + &stacking_relative_box, + clip, + ); + } + + // Text + let glyphs = convert_text_run_to_glyphs( + text_fragment.run.clone(), + text_fragment.range, + baseline_origin, + ); + if !glyphs.is_empty() { + let indexable_text = IndexableTextItem { + origin: stacking_relative_content_box.origin, + text_run: text_fragment.run.clone(), + range: text_fragment.range, + baseline_origin, + }; + state.indexable_text.insert(self.node, indexable_text); + + state.add_display_item(DisplayItem::Text(CommonDisplayItem::with_data( + base.clone(), + webrender_api::TextDisplayItem { + bounds: stacking_relative_content_box.to_layout(), + common: items::empty_common_item_properties(), + font_key: text_fragment.run.font_key, + color: text_color.to_layout(), + glyph_options: None, + }, + glyphs, + ))); + } + + // TODO(#17715): emit text-emphasis marks here. + // (just push another TextDisplayItem?) + + // Line-Through + if text_decorations.line_through { + let mut stacking_relative_box = logical_stacking_relative_content_box; + stacking_relative_box.start.b = + stacking_relative_box.start.b + metrics.ascent - metrics.strikeout_offset; + stacking_relative_box.size.block = metrics.strikeout_size; + self.build_display_list_for_text_decoration( + state, + &text_color, + &stacking_relative_box, + clip, + ); + } + + // Pop all the PushTextShadows + if !text_shadows.is_empty() { + state.add_display_item(DisplayItem::PopAllTextShadows(Box::new( + PopAllTextShadowsDisplayItem { base }, + ))); + } + } + + /// Creates the display item for a text decoration: underline, overline, or line-through. + fn build_display_list_for_text_decoration( + &self, + state: &mut DisplayListBuildState, + color: &RGBA, + stacking_relative_box: &LogicalRect<Au>, + clip: Rect<Au>, + ) { + // FIXME(pcwalton, #2795): Get the real container size. + let container_size = Size2D::zero(); + let stacking_relative_box = + stacking_relative_box.to_physical(self.style.writing_mode, container_size); + let base = state.create_base_display_item( + clip, + self.node, + get_cursor(&self.style, Cursor::Default), + DisplayListSection::Content, + ); + + // TODO(gw): Use a better estimate for wavy line thickness. + let area = stacking_relative_box.to_layout(); + let wavy_line_thickness = (0.33 * area.size.height).ceil(); + state.add_display_item(DisplayItem::Line(CommonDisplayItem::new( + base, + webrender_api::LineDisplayItem { + common: items::empty_common_item_properties(), + area, + orientation: webrender_api::LineOrientation::Horizontal, + wavy_line_thickness, + color: color.to_layout(), + style: LineStyle::Solid, + }, + ))); + } + + fn unique_id(&self) -> u64 { + let fragment_type = self.fragment_type(); + let id = self.node.id() as usize; + combine_id_with_fragment_type(id, fragment_type) as u64 + } + + fn fragment_type(&self) -> FragmentType { + self.pseudo.fragment_type() + } +} + +bitflags! { + pub struct StackingContextCollectionFlags: u8 { + /// This flow never establishes a containing block. + const POSITION_NEVER_CREATES_CONTAINING_BLOCK = 0b001; + /// This flow never creates a ClipScrollNode. + const NEVER_CREATES_CLIP_SCROLL_NODE = 0b010; + /// This flow never creates a stacking context. + const NEVER_CREATES_STACKING_CONTEXT = 0b100; + } +} + +/// This structure manages ensuring that modification to StackingContextCollectionState is +/// only temporary. It's useful for moving recursively down the flow tree and ensuring +/// that the state is restored for siblings. To use this structure, we must call +/// SavedStackingContextCollectionState::restore in order to restore the state. +/// TODO(mrobinson): It would be nice to use RAII here to avoid having to call restore. +pub struct SavedStackingContextCollectionState { + stacking_context_id: StackingContextId, + real_stacking_context_id: StackingContextId, + parent_reference_frame_id: ClipScrollNodeIndex, + clipping_and_scrolling: ClippingAndScrolling, + containing_block_clipping_and_scrolling: ClippingAndScrolling, + clips_pushed: usize, + containing_block_clips_pushed: usize, + stacking_relative_content_box: Rect<Au>, +} + +impl SavedStackingContextCollectionState { + fn new(state: &mut StackingContextCollectionState) -> SavedStackingContextCollectionState { + SavedStackingContextCollectionState { + stacking_context_id: state.current_stacking_context_id, + real_stacking_context_id: state.current_real_stacking_context_id, + parent_reference_frame_id: state.current_parent_reference_frame_id, + clipping_and_scrolling: state.current_clipping_and_scrolling, + containing_block_clipping_and_scrolling: state.containing_block_clipping_and_scrolling, + clips_pushed: 0, + containing_block_clips_pushed: 0, + stacking_relative_content_box: state.parent_stacking_relative_content_box, + } + } + + fn switch_to_containing_block_clip(&mut self, state: &mut StackingContextCollectionState) { + let clip = state + .containing_block_clip_stack + .last() + .cloned() + .unwrap_or_else(MaxRect::max_rect); + state.clip_stack.push(clip); + self.clips_pushed += 1; + } + + fn restore(self, state: &mut StackingContextCollectionState) { + state.current_stacking_context_id = self.stacking_context_id; + state.current_real_stacking_context_id = self.real_stacking_context_id; + state.current_parent_reference_frame_id = self.parent_reference_frame_id; + state.current_clipping_and_scrolling = self.clipping_and_scrolling; + state.containing_block_clipping_and_scrolling = + self.containing_block_clipping_and_scrolling; + state.parent_stacking_relative_content_box = self.stacking_relative_content_box; + + let truncate_length = state.clip_stack.len() - self.clips_pushed; + state.clip_stack.truncate(truncate_length); + + let truncate_length = + state.containing_block_clip_stack.len() - self.containing_block_clips_pushed; + state.containing_block_clip_stack.truncate(truncate_length); + } + + fn push_clip( + &mut self, + state: &mut StackingContextCollectionState, + mut clip: Rect<Au>, + positioning: StylePosition, + ) { + if positioning != StylePosition::Fixed { + if let Some(old_clip) = state.clip_stack.last() { + clip = old_clip.intersection(&clip).unwrap_or_else(Rect::zero); + } + } + + state.clip_stack.push(clip); + self.clips_pushed += 1; + + if StylePosition::Absolute == positioning { + state.containing_block_clip_stack.push(clip); + self.containing_block_clips_pushed += 1; + } + } +} + +impl BlockFlow { + fn transform_clip_to_coordinate_space( + &mut self, + state: &mut StackingContextCollectionState, + preserved_state: &mut SavedStackingContextCollectionState, + ) { + if state.clip_stack.is_empty() { + return; + } + let border_box = self.stacking_relative_border_box(CoordinateSystem::Parent); + let transform = match self.fragment.transform_matrix(&border_box) { + Some(transform) => transform, + None => return, + }; + + let perspective = self + .fragment + .perspective_matrix(&border_box) + .unwrap_or(LayoutTransform::identity()); + let transform = transform.pre_mul(&perspective).inverse(); + + let origin = border_box.origin; + let transform_clip = |clip: Rect<Au>| { + if clip == Rect::max_rect() { + return clip; + } + + match transform { + Some(transform) if transform.m13 != 0.0 || transform.m23 != 0.0 => { + // We cannot properly handle perspective transforms, because there may be a + // situation where an element is transformed from outside the clip into the + // clip region. Here we don't have enough information to detect when that is + // happening. For the moment we just punt on trying to optimize the display + // list for those cases. + Rect::max_rect() + }, + Some(transform) => { + let clip = rect( + (clip.origin.x - origin.x).to_f32_px(), + (clip.origin.y - origin.y).to_f32_px(), + clip.size.width.to_f32_px(), + clip.size.height.to_f32_px(), + ); + + let clip = transform.transform_rect(&clip).unwrap(); + + rect( + Au::from_f32_px(clip.origin.x), + Au::from_f32_px(clip.origin.y), + Au::from_f32_px(clip.size.width), + Au::from_f32_px(clip.size.height), + ) + }, + None => Rect::zero(), + } + }; + + if let Some(clip) = state.clip_stack.last().cloned() { + state.clip_stack.push(transform_clip(clip)); + preserved_state.clips_pushed += 1; + } + + if let Some(clip) = state.containing_block_clip_stack.last().cloned() { + state.containing_block_clip_stack.push(transform_clip(clip)); + preserved_state.containing_block_clips_pushed += 1; + } + } + + /// Returns true if this fragment may establish a reference frame and this block + /// creates a stacking context. Both are necessary in order to establish a reference + /// frame. + fn is_reference_frame(&self, context_type: Option<StackingContextType>) -> bool { + match context_type { + Some(StackingContextType::Real) => self.fragment.can_establish_reference_frame(), + _ => false, + } + } + + pub fn collect_stacking_contexts_for_block( + &mut self, + state: &mut StackingContextCollectionState, + flags: StackingContextCollectionFlags, + ) { + let mut preserved_state = SavedStackingContextCollectionState::new(state); + + let stacking_context_type = self.stacking_context_type(flags); + self.base.stacking_context_id = match stacking_context_type { + None => state.current_stacking_context_id, + Some(sc_type) => state.allocate_stacking_context_info(sc_type), + }; + state.current_stacking_context_id = self.base.stacking_context_id; + + if stacking_context_type == Some(StackingContextType::Real) { + state.current_real_stacking_context_id = self.base.stacking_context_id; + } + + let established_reference_frame = if self.is_reference_frame(stacking_context_type) { + // WebRender currently creates reference frames automatically, so just add + // a placeholder node to allocate a ClipScrollNodeIndex for this reference frame. + Some(state.add_clip_scroll_node(ClipScrollNode::placeholder())) + } else { + None + }; + + // We are getting the id of the scroll root that contains us here, not the id of + // any scroll root that we create. If we create a scroll root, its index will be + // stored in state.current_clipping_and_scrolling. If we create a stacking context, + // we don't want it to be contained by its own scroll root. + let containing_clipping_and_scrolling = self.setup_clipping_for_block( + state, + &mut preserved_state, + stacking_context_type, + established_reference_frame, + flags, + ); + + let creates_containing_block = !flags + .contains(StackingContextCollectionFlags::POSITION_NEVER_CREATES_CONTAINING_BLOCK); + let abspos_containing_block = established_reference_frame.is_some() || + (creates_containing_block && self.positioning() != StylePosition::Static); + if abspos_containing_block { + state.containing_block_clipping_and_scrolling = state.current_clipping_and_scrolling; + } + + match stacking_context_type { + None => self.base.collect_stacking_contexts_for_children(state), + Some(StackingContextType::Real) => { + self.create_real_stacking_context_for_block( + preserved_state.stacking_context_id, + containing_clipping_and_scrolling, + established_reference_frame, + state, + ); + }, + Some(stacking_context_type) => { + self.create_pseudo_stacking_context_for_block( + stacking_context_type, + preserved_state.stacking_context_id, + containing_clipping_and_scrolling, + state, + ); + }, + } + + preserved_state.restore(state); + } + + fn setup_clipping_for_block( + &mut self, + state: &mut StackingContextCollectionState, + preserved_state: &mut SavedStackingContextCollectionState, + stacking_context_type: Option<StackingContextType>, + established_reference_frame: Option<ClipScrollNodeIndex>, + flags: StackingContextCollectionFlags, + ) -> ClippingAndScrolling { + // If this block is absolutely positioned, we should be clipped and positioned by + // the scroll root of our nearest ancestor that establishes a containing block. + let containing_clipping_and_scrolling = match self.positioning() { + StylePosition::Absolute => { + preserved_state.switch_to_containing_block_clip(state); + state.current_clipping_and_scrolling = + state.containing_block_clipping_and_scrolling; + state.containing_block_clipping_and_scrolling + }, + StylePosition::Fixed => { + // If we are a fixed positioned stacking context, we want to be scrolled by + // our reference frame instead of the clip scroll node that we are inside. + preserved_state.push_clip(state, Rect::max_rect(), StylePosition::Fixed); + state.current_clipping_and_scrolling.scrolling = + state.current_parent_reference_frame_id; + state.current_clipping_and_scrolling + }, + _ => state.current_clipping_and_scrolling, + }; + self.base.clipping_and_scrolling = Some(containing_clipping_and_scrolling); + + if let Some(reference_frame_index) = established_reference_frame { + let clipping_and_scrolling = ClippingAndScrolling::simple(reference_frame_index); + state.current_clipping_and_scrolling = clipping_and_scrolling; + self.base.clipping_and_scrolling = Some(clipping_and_scrolling); + } + + let stacking_relative_border_box = if self.fragment.establishes_stacking_context() { + self.stacking_relative_border_box(CoordinateSystem::Own) + } else { + self.stacking_relative_border_box(CoordinateSystem::Parent) + }; + + if stacking_context_type == Some(StackingContextType::Real) { + self.transform_clip_to_coordinate_space(state, preserved_state); + } + + if !flags.contains(StackingContextCollectionFlags::NEVER_CREATES_CLIP_SCROLL_NODE) { + self.setup_clip_scroll_node_for_position(state, stacking_relative_border_box); + self.setup_clip_scroll_node_for_overflow(state, stacking_relative_border_box); + self.setup_clip_scroll_node_for_css_clip( + state, + preserved_state, + stacking_relative_border_box, + ); + } + self.base.clip = state + .clip_stack + .last() + .cloned() + .unwrap_or_else(Rect::max_rect); + + // We keep track of our position so that any stickily positioned elements can + // properly determine the extent of their movement relative to scrolling containers. + if !flags.contains(StackingContextCollectionFlags::POSITION_NEVER_CREATES_CONTAINING_BLOCK) + { + let border_box = if self.fragment.establishes_stacking_context() { + stacking_relative_border_box + } else { + self.stacking_relative_border_box(CoordinateSystem::Own) + }; + state.parent_stacking_relative_content_box = + self.fragment.stacking_relative_content_box(border_box) + } + + match self.positioning() { + StylePosition::Absolute | StylePosition::Relative | StylePosition::Fixed => { + state.containing_block_clipping_and_scrolling = state.current_clipping_and_scrolling + }, + _ => {}, + } + + containing_clipping_and_scrolling + } + + fn setup_clip_scroll_node_for_position( + &mut self, + state: &mut StackingContextCollectionState, + border_box: Rect<Au>, + ) { + if self.positioning() != StylePosition::Sticky { + return; + } + + let sticky_position = self.sticky_position(); + if sticky_position.left == MaybeAuto::Auto && + sticky_position.right == MaybeAuto::Auto && + sticky_position.top == MaybeAuto::Auto && + sticky_position.bottom == MaybeAuto::Auto + { + return; + } + + // Since position: sticky elements always establish a stacking context, we will + // have previously calculated our border box in our own coordinate system. In + // order to properly calculate max offsets we need to compare our size and + // position in our parent's coordinate system. + let border_box_in_parent = self.stacking_relative_border_box(CoordinateSystem::Parent); + let margins = self.fragment.margin.to_physical( + self.base + .early_absolute_position_info + .relative_containing_block_mode, + ); + + // Position:sticky elements are always restricted based on the size and position of + // their containing block, which for sticky items is like relative and statically + // positioned items: just the parent block. + let constraint_rect = state.parent_stacking_relative_content_box; + + let to_offset_bound = |constraint_edge: Au, moving_edge: Au| -> f32 { + (constraint_edge - moving_edge).to_f32_px() + }; + + // This is the minimum negative offset and then the maximum positive offset. We just + // specify every edge, but if the corresponding margin is None, that offset has no effect. + let vertical_offset_bounds = StickyOffsetBounds::new( + to_offset_bound( + constraint_rect.min_y(), + border_box_in_parent.min_y() - margins.top, + ), + to_offset_bound(constraint_rect.max_y(), border_box_in_parent.max_y()), + ); + let horizontal_offset_bounds = StickyOffsetBounds::new( + to_offset_bound( + constraint_rect.min_x(), + border_box_in_parent.min_x() - margins.left, + ), + to_offset_bound(constraint_rect.max_x(), border_box_in_parent.max_x()), + ); + + // The margins control which edges have sticky behavior. + let sticky_frame_data = StickyFrameData { + margins: SideOffsets2D::new( + sticky_position.top.to_option().map(|v| v.to_f32_px()), + sticky_position.right.to_option().map(|v| v.to_f32_px()), + sticky_position.bottom.to_option().map(|v| v.to_f32_px()), + sticky_position.left.to_option().map(|v| v.to_f32_px()), + ), + vertical_offset_bounds, + horizontal_offset_bounds, + }; + + let new_clip_scroll_index = state.add_clip_scroll_node(ClipScrollNode { + parent_index: self.clipping_and_scrolling().scrolling, + clip: ClippingRegion::from_rect(border_box.to_layout()), + content_rect: LayoutRect::zero(), + node_type: ClipScrollNodeType::StickyFrame(sticky_frame_data), + }); + + let new_clipping_and_scrolling = ClippingAndScrolling::simple(new_clip_scroll_index); + self.base.clipping_and_scrolling = Some(new_clipping_and_scrolling); + state.current_clipping_and_scrolling = new_clipping_and_scrolling; + } + + fn setup_clip_scroll_node_for_overflow( + &mut self, + state: &mut StackingContextCollectionState, + border_box: Rect<Au>, + ) { + if !self.overflow_style_may_require_clip_scroll_node() { + return; + } + + let content_box = self.fragment.stacking_relative_content_box(border_box); + let has_scrolling_overflow = self.base.overflow.scroll.origin != Point2D::zero() || + self.base.overflow.scroll.size.width > content_box.size.width || + self.base.overflow.scroll.size.height > content_box.size.height || + StyleOverflow::Hidden == self.fragment.style.get_box().overflow_x || + StyleOverflow::Hidden == self.fragment.style.get_box().overflow_y; + + self.mark_scrolling_overflow(has_scrolling_overflow); + if !has_scrolling_overflow { + return; + } + + let sensitivity = if StyleOverflow::Hidden == self.fragment.style.get_box().overflow_x && + StyleOverflow::Hidden == self.fragment.style.get_box().overflow_y + { + ScrollSensitivity::Script + } else { + ScrollSensitivity::ScriptAndInputEvents + }; + + let border_widths = self + .fragment + .style + .logical_border_width() + .to_physical(self.fragment.style.writing_mode); + let clip_rect = border_box.inner_rect(border_widths); + + let mut clip = ClippingRegion::from_rect(clip_rect.to_layout()); + let radii = build_border_radius_for_inner_rect(border_box, &self.fragment.style); + if !radii.is_zero() { + clip.intersect_with_rounded_rect(clip_rect.to_layout(), radii) + } + + let content_size = self.base.overflow.scroll.origin + self.base.overflow.scroll.size; + let content_size = Size2D::new(content_size.x, content_size.y); + + let external_id = + ExternalScrollId(self.fragment.unique_id(), state.pipeline_id.to_webrender()); + let new_clip_scroll_index = state.add_clip_scroll_node(ClipScrollNode { + parent_index: self.clipping_and_scrolling().scrolling, + clip: clip, + content_rect: Rect::new(content_box.origin, content_size).to_layout(), + node_type: ClipScrollNodeType::ScrollFrame(sensitivity, external_id), + }); + + let new_clipping_and_scrolling = ClippingAndScrolling::simple(new_clip_scroll_index); + self.base.clipping_and_scrolling = Some(new_clipping_and_scrolling); + state.current_clipping_and_scrolling = new_clipping_and_scrolling; + } + + /// Adds a scroll root for a block to take the `clip` property into account + /// per CSS 2.1 § 11.1.2. + fn setup_clip_scroll_node_for_css_clip( + &mut self, + state: &mut StackingContextCollectionState, + preserved_state: &mut SavedStackingContextCollectionState, + stacking_relative_border_box: Rect<Au>, + ) { + // Account for `clip` per CSS 2.1 § 11.1.2. + let style_clip_rect = match self.fragment.style().get_effects().clip { + Either::First(style_clip_rect) => style_clip_rect, + _ => return, + }; + + // CSS `clip` should only apply to position:absolute or positione:fixed elements. + // CSS Masking Appendix A: "Applies to: Absolutely positioned elements." + match self.positioning() { + StylePosition::Absolute | StylePosition::Fixed => {}, + _ => return, + } + + fn extract_clip_component(p: &LengthOrAuto) -> Option<Au> { + match *p { + LengthOrAuto::Auto => None, + LengthOrAuto::LengthPercentage(ref length) => Some(Au::from(*length)), + } + } + + let clip_origin = Point2D::new( + stacking_relative_border_box.origin.x + + extract_clip_component(&style_clip_rect.left).unwrap_or_default(), + stacking_relative_border_box.origin.y + + extract_clip_component(&style_clip_rect.top).unwrap_or_default(), + ); + let right = extract_clip_component(&style_clip_rect.right) + .unwrap_or(stacking_relative_border_box.size.width); + let bottom = extract_clip_component(&style_clip_rect.bottom) + .unwrap_or(stacking_relative_border_box.size.height); + let clip_size = Size2D::new(right - clip_origin.x, bottom - clip_origin.y); + + let clip_rect = Rect::new(clip_origin, clip_size); + preserved_state.push_clip(state, clip_rect, self.positioning()); + + let new_index = state.add_clip_scroll_node(ClipScrollNode { + parent_index: self.clipping_and_scrolling().scrolling, + clip: ClippingRegion::from_rect(clip_rect.to_layout()), + content_rect: LayoutRect::zero(), // content_rect isn't important for clips. + node_type: ClipScrollNodeType::Clip, + }); + + let new_indices = ClippingAndScrolling::new(new_index, new_index); + self.base.clipping_and_scrolling = Some(new_indices); + state.current_clipping_and_scrolling = new_indices; + } + + fn create_pseudo_stacking_context_for_block( + &mut self, + stacking_context_type: StackingContextType, + parent_stacking_context_id: StackingContextId, + parent_clipping_and_scrolling: ClippingAndScrolling, + state: &mut StackingContextCollectionState, + ) { + let new_context = self.fragment.create_stacking_context( + self.base.stacking_context_id, + &self.base, + stacking_context_type, + None, + parent_clipping_and_scrolling, + ); + state.add_stacking_context(parent_stacking_context_id, new_context); + + self.base.collect_stacking_contexts_for_children(state); + + let children = state + .stacking_context_info + .get_mut(&self.base.stacking_context_id) + .map(|info| info.take_children()); + if let Some(children) = children { + for child in children { + if child.context_type == StackingContextType::PseudoFloat { + state.add_stacking_context(self.base.stacking_context_id, child); + } else { + state.add_stacking_context(parent_stacking_context_id, child); + } + } + } + } + + fn create_real_stacking_context_for_block( + &mut self, + parent_stacking_context_id: StackingContextId, + parent_clipping_and_scrolling: ClippingAndScrolling, + established_reference_frame: Option<ClipScrollNodeIndex>, + state: &mut StackingContextCollectionState, + ) { + let stacking_context = self.fragment.create_stacking_context( + self.base.stacking_context_id, + &self.base, + StackingContextType::Real, + established_reference_frame, + parent_clipping_and_scrolling, + ); + + state.add_stacking_context(parent_stacking_context_id, stacking_context); + self.base.collect_stacking_contexts_for_children(state); + } + + pub fn build_display_list_for_block_no_damage( + &self, + state: &mut DisplayListBuildState, + border_painting_mode: BorderPaintingMode, + ) { + let background_border_section = self.background_border_section(); + + state.processing_scrolling_overflow_element = self.has_scrolling_overflow(); + + let content_size = if state.processing_scrolling_overflow_element { + let content_size = self.base.overflow.scroll.origin + self.base.overflow.scroll.size; + Some(Size2D::new(content_size.x, content_size.y)) + } else { + None + }; + + let stacking_relative_border_box = self + .base + .stacking_relative_border_box_for_display_list(&self.fragment); + // Add the box that starts the block context. + self.fragment.build_display_list_no_damage( + state, + stacking_relative_border_box, + border_painting_mode, + background_border_section, + self.base.clip, + content_size, + ); + + self.base + .build_display_items_for_debugging_tint(state, self.fragment.node); + + state.processing_scrolling_overflow_element = false; + } + + pub fn build_display_list_for_block( + &mut self, + state: &mut DisplayListBuildState, + border_painting_mode: BorderPaintingMode, + ) { + self.fragment + .restyle_damage + .remove(ServoRestyleDamage::REPAINT); + self.build_display_list_for_block_no_damage(state, border_painting_mode); + } + + pub fn build_display_list_for_background_if_applicable_with_background( + &self, + state: &mut DisplayListBuildState, + background: &style_structs::Background, + background_color: RGBA, + ) { + let stacking_relative_border_box = self + .base + .stacking_relative_border_box_for_display_list(&self.fragment); + let background_border_section = self.background_border_section(); + + self.fragment + .build_display_list_for_background_if_applicable_with_background( + state, + self.fragment.style(), + background, + background_color, + background_border_section, + stacking_relative_border_box, + ) + } + + #[inline] + fn stacking_context_type( + &self, + flags: StackingContextCollectionFlags, + ) -> Option<StackingContextType> { + if flags.contains(StackingContextCollectionFlags::NEVER_CREATES_STACKING_CONTEXT) { + return None; + } + + if self.fragment.establishes_stacking_context() { + return Some(StackingContextType::Real); + } + + if self + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + return Some(StackingContextType::PseudoPositioned); + } + + if self.fragment.style.get_box().position != StylePosition::Static { + return Some(StackingContextType::PseudoPositioned); + } + + if self.base.flags.is_float() { + return Some(StackingContextType::PseudoFloat); + } + + None + } +} + +impl BaseFlow { + pub fn build_display_items_for_debugging_tint( + &self, + state: &mut DisplayListBuildState, + node: OpaqueNode, + ) { + if !opts::get().show_debug_parallel_layout { + return; + } + + let thread_id = self.thread_id; + let stacking_context_relative_bounds = Rect::new( + self.stacking_relative_position.to_point(), + self.position.size.to_physical(self.writing_mode), + ); + + let mut color = THREAD_TINT_COLORS[thread_id as usize % THREAD_TINT_COLORS.len()]; + color.a = 1.0; + let base = + state.create_base_display_item(self.clip, node, None, DisplayListSection::Content); + let bounds = stacking_context_relative_bounds.inflate(Au::from_px(2), Au::from_px(2)); + state.add_display_item(DisplayItem::Border(CommonDisplayItem::with_data( + base, + webrender_api::BorderDisplayItem { + bounds: bounds.to_layout(), + common: items::empty_common_item_properties(), + widths: SideOffsets2D::new_all_same(Au::from_px(2)).to_layout(), + details: BorderDetails::Normal(border::simple( + color, + webrender_api::BorderStyle::Solid, + )), + }, + Vec::new(), + ))); + } +} + +/// Gets the cursor to use given the specific ComputedValues. `default_cursor` specifies +/// the cursor to use if `cursor` is `auto`. Typically, this will be `PointerCursor`, but for +/// text display items it may be `TextCursor` or `VerticalTextCursor`. +#[inline] +fn get_cursor(values: &ComputedValues, default_cursor: Cursor) -> Option<Cursor> { + let inherited_ui = values.get_inherited_ui(); + if inherited_ui.pointer_events == PointerEvents::None { + return None; + } + + Some(match inherited_ui.cursor.keyword { + CursorKind::Auto => default_cursor, + CursorKind::None => Cursor::None, + CursorKind::Default => Cursor::Default, + CursorKind::Pointer => Cursor::Pointer, + CursorKind::ContextMenu => Cursor::ContextMenu, + CursorKind::Help => Cursor::Help, + CursorKind::Progress => Cursor::Progress, + CursorKind::Wait => Cursor::Wait, + CursorKind::Cell => Cursor::Cell, + CursorKind::Crosshair => Cursor::Crosshair, + CursorKind::Text => Cursor::Text, + CursorKind::VerticalText => Cursor::VerticalText, + CursorKind::Alias => Cursor::Alias, + CursorKind::Copy => Cursor::Copy, + CursorKind::Move => Cursor::Move, + CursorKind::NoDrop => Cursor::NoDrop, + CursorKind::NotAllowed => Cursor::NotAllowed, + CursorKind::Grab => Cursor::Grab, + CursorKind::Grabbing => Cursor::Grabbing, + CursorKind::EResize => Cursor::EResize, + CursorKind::NResize => Cursor::NResize, + CursorKind::NeResize => Cursor::NeResize, + CursorKind::NwResize => Cursor::NwResize, + CursorKind::SResize => Cursor::SResize, + CursorKind::SeResize => Cursor::SeResize, + CursorKind::SwResize => Cursor::SwResize, + CursorKind::WResize => Cursor::WResize, + CursorKind::EwResize => Cursor::EwResize, + CursorKind::NsResize => Cursor::NsResize, + CursorKind::NeswResize => Cursor::NeswResize, + CursorKind::NwseResize => Cursor::NwseResize, + CursorKind::ColResize => Cursor::ColResize, + CursorKind::RowResize => Cursor::RowResize, + CursorKind::AllScroll => Cursor::AllScroll, + CursorKind::ZoomIn => Cursor::ZoomIn, + CursorKind::ZoomOut => Cursor::ZoomOut, + }) +} + +/// Adjusts borders as appropriate to account for a fragment's status as the +/// first or last fragment within the range of an element. +/// +/// Specifically, this function sets border widths to zero on the sides for +/// which the fragment is not outermost. +fn modify_border_width_for_inline_sides( + border_width: &mut LogicalMargin<Au>, + inline_border_info: InlineNodeBorderInfo, +) { + if !inline_border_info.is_first_fragment_of_element { + border_width.inline_start = Au(0); + } + + if !inline_border_info.is_last_fragment_of_element { + border_width.inline_end = Au(0); + } +} + +/// Describes how to paint the borders. +#[derive(Clone, Copy)] +pub enum BorderPaintingMode<'a> { + /// Paint borders separately (`border-collapse: separate`). + Separate, + /// Paint collapsed borders. + Collapse(&'a CollapsedBordersForCell), + /// Paint no borders. + Hidden, +} + +fn convert_text_run_to_glyphs( + text_run: Arc<TextRun>, + range: Range<ByteIndex>, + mut origin: Point2D<Au>, +) -> Vec<GlyphInstance> { + let mut glyphs = vec![]; + + for slice in text_run.natural_word_slices_in_visual_order(&range) { + for glyph in slice.glyphs.iter_glyphs_for_byte_range(&slice.range) { + let glyph_advance = if glyph.char_is_space() { + glyph.advance() + text_run.extra_word_spacing + } else { + glyph.advance() + }; + if !slice.glyphs.is_whitespace() { + let glyph_offset = glyph.offset().unwrap_or(Point2D::zero()); + let point = origin + glyph_offset.to_vector(); + let glyph = GlyphInstance { + index: glyph.id(), + point: point.to_layout(), + }; + glyphs.push(glyph); + } + origin.x += glyph_advance; + } + } + return glyphs; +} + +pub struct IndexableTextItem { + /// The placement of the text item on the plane. + pub origin: Point2D<Au>, + /// The text run. + pub text_run: Arc<TextRun>, + /// The range of text within the text run. + pub range: Range<ByteIndex>, + /// The position of the start of the baseline of this text. + pub baseline_origin: Point2D<Au>, +} + +#[derive(Default)] +pub struct IndexableText { + inner: FnvHashMap<OpaqueNode, Vec<IndexableTextItem>>, +} + +impl IndexableText { + fn insert(&mut self, node: OpaqueNode, item: IndexableTextItem) { + let entries = self.inner.entry(node).or_insert(Vec::new()); + entries.push(item); + } + + pub fn get(&self, node: OpaqueNode) -> Option<&[IndexableTextItem]> { + self.inner.get(&node).map(|x| x.as_slice()) + } + + // Returns the text index within a node for the point of interest. + pub fn text_index(&self, node: OpaqueNode, point_in_item: Point2D<Au>) -> Option<usize> { + let item = self.inner.get(&node)?; + // TODO(#20020): access all elements + let point = point_in_item + item[0].origin.to_vector(); + let offset = point - item[0].baseline_origin; + Some( + item[0] + .text_run + .range_index_of_advance(&item[0].range, offset.x), + ) + } +} + +trait ToF32Px { + type Output; + fn to_f32_px(&self) -> Self::Output; +} + +impl ToF32Px for TypedRect<Au> { + type Output = LayoutRect; + fn to_f32_px(&self) -> LayoutRect { + LayoutRect::from_untyped(&servo_geometry::au_rect_to_f32_rect(*self)) + } +} diff --git a/components/layout_2020/display_list/conversions.rs b/components/layout_2020/display_list/conversions.rs new file mode 100644 index 00000000000..4624b5f8dd0 --- /dev/null +++ b/components/layout_2020/display_list/conversions.rs @@ -0,0 +1,167 @@ +/* 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/. */ + +use app_units::Au; +use euclid::{Point2D, Rect, SideOffsets2D, Size2D, Vector2D}; +use style::computed_values::image_rendering::T as ImageRendering; +use style::computed_values::mix_blend_mode::T as MixBlendMode; +use style::computed_values::transform_style::T as TransformStyle; +use style::values::computed::{BorderStyle, Filter}; +use style::values::specified::border::BorderImageRepeatKeyword; +use style::values::RGBA; +use webrender_api as wr; + +pub trait ToLayout { + type Type; + fn to_layout(&self) -> Self::Type; +} + +impl ToLayout for BorderStyle { + type Type = wr::BorderStyle; + fn to_layout(&self) -> Self::Type { + match *self { + BorderStyle::None => wr::BorderStyle::None, + BorderStyle::Solid => wr::BorderStyle::Solid, + BorderStyle::Double => wr::BorderStyle::Double, + BorderStyle::Dotted => wr::BorderStyle::Dotted, + BorderStyle::Dashed => wr::BorderStyle::Dashed, + BorderStyle::Hidden => wr::BorderStyle::Hidden, + BorderStyle::Groove => wr::BorderStyle::Groove, + BorderStyle::Ridge => wr::BorderStyle::Ridge, + BorderStyle::Inset => wr::BorderStyle::Inset, + BorderStyle::Outset => wr::BorderStyle::Outset, + } + } +} + +impl ToLayout for Filter { + type Type = wr::FilterOp; + fn to_layout(&self) -> Self::Type { + match *self { + Filter::Blur(radius) => wr::FilterOp::Blur(radius.px()), + Filter::Brightness(amount) => wr::FilterOp::Brightness(amount.0), + Filter::Contrast(amount) => wr::FilterOp::Contrast(amount.0), + Filter::Grayscale(amount) => wr::FilterOp::Grayscale(amount.0), + Filter::HueRotate(angle) => wr::FilterOp::HueRotate(angle.radians()), + Filter::Invert(amount) => wr::FilterOp::Invert(amount.0), + Filter::Opacity(amount) => wr::FilterOp::Opacity(amount.0.into(), amount.0), + Filter::Saturate(amount) => wr::FilterOp::Saturate(amount.0), + Filter::Sepia(amount) => wr::FilterOp::Sepia(amount.0), + // Statically check that DropShadow is impossible. + Filter::DropShadow(ref shadow) => match *shadow {}, + // Statically check that Url is impossible. + Filter::Url(ref url) => match *url {}, + } + } +} + +impl ToLayout for ImageRendering { + type Type = wr::ImageRendering; + fn to_layout(&self) -> Self::Type { + match *self { + ImageRendering::Auto => wr::ImageRendering::Auto, + ImageRendering::CrispEdges => wr::ImageRendering::CrispEdges, + ImageRendering::Pixelated => wr::ImageRendering::Pixelated, + } + } +} + +impl ToLayout for MixBlendMode { + type Type = wr::MixBlendMode; + fn to_layout(&self) -> Self::Type { + match *self { + MixBlendMode::Normal => wr::MixBlendMode::Normal, + MixBlendMode::Multiply => wr::MixBlendMode::Multiply, + MixBlendMode::Screen => wr::MixBlendMode::Screen, + MixBlendMode::Overlay => wr::MixBlendMode::Overlay, + MixBlendMode::Darken => wr::MixBlendMode::Darken, + MixBlendMode::Lighten => wr::MixBlendMode::Lighten, + MixBlendMode::ColorDodge => wr::MixBlendMode::ColorDodge, + MixBlendMode::ColorBurn => wr::MixBlendMode::ColorBurn, + MixBlendMode::HardLight => wr::MixBlendMode::HardLight, + MixBlendMode::SoftLight => wr::MixBlendMode::SoftLight, + MixBlendMode::Difference => wr::MixBlendMode::Difference, + MixBlendMode::Exclusion => wr::MixBlendMode::Exclusion, + MixBlendMode::Hue => wr::MixBlendMode::Hue, + MixBlendMode::Saturation => wr::MixBlendMode::Saturation, + MixBlendMode::Color => wr::MixBlendMode::Color, + MixBlendMode::Luminosity => wr::MixBlendMode::Luminosity, + } + } +} + +impl ToLayout for TransformStyle { + type Type = wr::TransformStyle; + fn to_layout(&self) -> Self::Type { + match *self { + TransformStyle::Auto | TransformStyle::Flat => wr::TransformStyle::Flat, + TransformStyle::Preserve3d => wr::TransformStyle::Preserve3D, + } + } +} + +impl ToLayout for RGBA { + type Type = wr::ColorF; + fn to_layout(&self) -> Self::Type { + wr::ColorF::new( + self.red_f32(), + self.green_f32(), + self.blue_f32(), + self.alpha_f32(), + ) + } +} + +impl ToLayout for Point2D<Au> { + type Type = wr::units::LayoutPoint; + fn to_layout(&self) -> Self::Type { + wr::units::LayoutPoint::new(self.x.to_f32_px(), self.y.to_f32_px()) + } +} + +impl ToLayout for Rect<Au> { + type Type = wr::units::LayoutRect; + fn to_layout(&self) -> Self::Type { + wr::units::LayoutRect::new(self.origin.to_layout(), self.size.to_layout()) + } +} + +impl ToLayout for SideOffsets2D<Au> { + type Type = wr::units::LayoutSideOffsets; + fn to_layout(&self) -> Self::Type { + wr::units::LayoutSideOffsets::new( + self.top.to_f32_px(), + self.right.to_f32_px(), + self.bottom.to_f32_px(), + self.left.to_f32_px(), + ) + } +} + +impl ToLayout for Size2D<Au> { + type Type = wr::units::LayoutSize; + fn to_layout(&self) -> Self::Type { + wr::units::LayoutSize::new(self.width.to_f32_px(), self.height.to_f32_px()) + } +} + +impl ToLayout for Vector2D<Au> { + type Type = wr::units::LayoutVector2D; + fn to_layout(&self) -> Self::Type { + wr::units::LayoutVector2D::new(self.x.to_f32_px(), self.y.to_f32_px()) + } +} + +impl ToLayout for BorderImageRepeatKeyword { + type Type = wr::RepeatMode; + + fn to_layout(&self) -> Self::Type { + match *self { + BorderImageRepeatKeyword::Stretch => wr::RepeatMode::Stretch, + BorderImageRepeatKeyword::Repeat => wr::RepeatMode::Repeat, + BorderImageRepeatKeyword::Round => wr::RepeatMode::Round, + BorderImageRepeatKeyword::Space => wr::RepeatMode::Space, + } + } +} diff --git a/components/layout_2020/display_list/gradient.rs b/components/layout_2020/display_list/gradient.rs new file mode 100644 index 00000000000..ab56b1d6c9c --- /dev/null +++ b/components/layout_2020/display_list/gradient.rs @@ -0,0 +1,323 @@ +/* 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/. */ + +use crate::display_list::ToLayout; +use app_units::Au; +use euclid::{Point2D, Size2D, Vector2D}; +use style::properties::ComputedValues; +use style::values::computed::image::{EndingShape, LineDirection}; +use style::values::computed::{Angle, GradientItem, LengthPercentage, Percentage, Position}; +use style::values::generics::image::{Circle, ColorStop, Ellipse, ShapeExtent}; +use webrender_api::{ExtendMode, Gradient, GradientBuilder, GradientStop, RadialGradient}; + +/// A helper data structure for gradients. +#[derive(Clone, Copy)] +struct StopRun { + start_offset: f32, + end_offset: f32, + start_index: usize, + stop_count: usize, +} + +/// Determines the radius of a circle if it was not explictly provided. +/// <https://drafts.csswg.org/css-images-3/#typedef-size> +fn circle_size_keyword( + keyword: ShapeExtent, + size: &Size2D<Au>, + center: &Point2D<Au>, +) -> Size2D<Au> { + let radius = match keyword { + ShapeExtent::ClosestSide | ShapeExtent::Contain => { + let dist = distance_to_sides(size, center, ::std::cmp::min); + ::std::cmp::min(dist.width, dist.height) + }, + ShapeExtent::FarthestSide => { + let dist = distance_to_sides(size, center, ::std::cmp::max); + ::std::cmp::max(dist.width, dist.height) + }, + ShapeExtent::ClosestCorner => distance_to_corner(size, center, ::std::cmp::min), + ShapeExtent::FarthestCorner | ShapeExtent::Cover => { + distance_to_corner(size, center, ::std::cmp::max) + }, + }; + Size2D::new(radius, radius) +} + +/// Returns the radius for an ellipse with the same ratio as if it was matched to the sides. +fn ellipse_radius<F>(size: &Size2D<Au>, center: &Point2D<Au>, cmp: F) -> Size2D<Au> +where + F: Fn(Au, Au) -> Au, +{ + let dist = distance_to_sides(size, center, cmp); + Size2D::new( + dist.width.scale_by(::std::f32::consts::FRAC_1_SQRT_2 * 2.0), + dist.height + .scale_by(::std::f32::consts::FRAC_1_SQRT_2 * 2.0), + ) +} + +/// Determines the radius of an ellipse if it was not explictly provided. +/// <https://drafts.csswg.org/css-images-3/#typedef-size> +fn ellipse_size_keyword( + keyword: ShapeExtent, + size: &Size2D<Au>, + center: &Point2D<Au>, +) -> Size2D<Au> { + match keyword { + ShapeExtent::ClosestSide | ShapeExtent::Contain => { + distance_to_sides(size, center, ::std::cmp::min) + }, + ShapeExtent::FarthestSide => distance_to_sides(size, center, ::std::cmp::max), + ShapeExtent::ClosestCorner => ellipse_radius(size, center, ::std::cmp::min), + ShapeExtent::FarthestCorner | ShapeExtent::Cover => { + ellipse_radius(size, center, ::std::cmp::max) + }, + } +} + +fn convert_gradient_stops( + style: &ComputedValues, + gradient_items: &[GradientItem], + total_length: Au, +) -> GradientBuilder { + // Determine the position of each stop per CSS-IMAGES § 3.4. + + // Only keep the color stops, discard the color interpolation hints. + let mut stop_items = gradient_items + .iter() + .filter_map(|item| match *item { + GradientItem::SimpleColorStop(color) => Some(ColorStop { + color, + position: None, + }), + GradientItem::ComplexColorStop { color, position } => Some(ColorStop { + color, + position: Some(position), + }), + _ => None, + }) + .collect::<Vec<_>>(); + + assert!(stop_items.len() >= 2); + + // Run the algorithm from + // https://drafts.csswg.org/css-images-3/#color-stop-syntax + + // Step 1: + // If the first color stop does not have a position, set its position to 0%. + { + let first = stop_items.first_mut().unwrap(); + if first.position.is_none() { + first.position = Some(LengthPercentage::new_percent(Percentage(0.))); + } + } + // If the last color stop does not have a position, set its position to 100%. + { + let last = stop_items.last_mut().unwrap(); + if last.position.is_none() { + last.position = Some(LengthPercentage::new_percent(Percentage(1.0))); + } + } + + // Step 2: Move any stops placed before earlier stops to the + // same position as the preceding stop. + let mut last_stop_position = stop_items.first().unwrap().position.unwrap(); + for stop in stop_items.iter_mut().skip(1) { + if let Some(pos) = stop.position { + if position_to_offset(last_stop_position, total_length) > + position_to_offset(pos, total_length) + { + stop.position = Some(last_stop_position); + } + last_stop_position = stop.position.unwrap(); + } + } + + // Step 3: Evenly space stops without position. + let mut stops = GradientBuilder::new(); + let mut stop_run = None; + for (i, stop) in stop_items.iter().enumerate() { + let offset = match stop.position { + None => { + if stop_run.is_none() { + // Initialize a new stop run. + // `unwrap()` here should never fail because this is the beginning of + // a stop run, which is always bounded by a length or percentage. + let start_offset = + position_to_offset(stop_items[i - 1].position.unwrap(), total_length); + // `unwrap()` here should never fail because this is the end of + // a stop run, which is always bounded by a length or percentage. + let (end_index, end_stop) = stop_items[(i + 1)..] + .iter() + .enumerate() + .find(|&(_, ref stop)| stop.position.is_some()) + .unwrap(); + let end_offset = position_to_offset(end_stop.position.unwrap(), total_length); + stop_run = Some(StopRun { + start_offset, + end_offset, + start_index: i - 1, + stop_count: end_index, + }) + } + + let stop_run = stop_run.unwrap(); + let stop_run_length = stop_run.end_offset - stop_run.start_offset; + stop_run.start_offset + + stop_run_length * (i - stop_run.start_index) as f32 / + ((2 + stop_run.stop_count) as f32) + }, + Some(position) => { + stop_run = None; + position_to_offset(position, total_length) + }, + }; + assert!(offset.is_finite()); + stops.push(GradientStop { + offset: offset, + color: style.resolve_color(stop.color).to_layout(), + }) + } + stops +} + +fn extend_mode(repeating: bool) -> ExtendMode { + if repeating { + ExtendMode::Repeat + } else { + ExtendMode::Clamp + } +} +/// Returns the the distance to the nearest or farthest corner depending on the comperator. +fn distance_to_corner<F>(size: &Size2D<Au>, center: &Point2D<Au>, cmp: F) -> Au +where + F: Fn(Au, Au) -> Au, +{ + let dist = distance_to_sides(size, center, cmp); + Au::from_f32_px(dist.width.to_f32_px().hypot(dist.height.to_f32_px())) +} + +/// Returns the distance to the nearest or farthest sides depending on the comparator. +/// +/// The first return value is horizontal distance the second vertical distance. +fn distance_to_sides<F>(size: &Size2D<Au>, center: &Point2D<Au>, cmp: F) -> Size2D<Au> +where + F: Fn(Au, Au) -> Au, +{ + let top_side = center.y; + let right_side = size.width - center.x; + let bottom_side = size.height - center.y; + let left_side = center.x; + Size2D::new(cmp(left_side, right_side), cmp(top_side, bottom_side)) +} + +fn position_to_offset(position: LengthPercentage, total_length: Au) -> f32 { + if total_length == Au(0) { + return 0.0; + } + position.to_used_value(total_length).0 as f32 / total_length.0 as f32 +} + +pub fn linear( + style: &ComputedValues, + size: Size2D<Au>, + stops: &[GradientItem], + direction: LineDirection, + repeating: bool, +) -> (Gradient, Vec<GradientStop>) { + use style::values::specified::position::HorizontalPositionKeyword::*; + use style::values::specified::position::VerticalPositionKeyword::*; + let angle = match direction { + LineDirection::Angle(angle) => angle.radians(), + LineDirection::Horizontal(x) => match x { + Left => Angle::from_degrees(270.).radians(), + Right => Angle::from_degrees(90.).radians(), + }, + LineDirection::Vertical(y) => match y { + Top => Angle::from_degrees(0.).radians(), + Bottom => Angle::from_degrees(180.).radians(), + }, + LineDirection::Corner(horizontal, vertical) => { + // This the angle for one of the diagonals of the box. Our angle + // will either be this one, this one + PI, or one of the other + // two perpendicular angles. + let atan = (size.height.to_f32_px() / size.width.to_f32_px()).atan(); + match (horizontal, vertical) { + (Right, Bottom) => ::std::f32::consts::PI - atan, + (Left, Bottom) => ::std::f32::consts::PI + atan, + (Right, Top) => atan, + (Left, Top) => -atan, + } + }, + }; + + // Get correct gradient line length, based on: + // https://drafts.csswg.org/css-images-3/#linear-gradients + let dir = Point2D::new(angle.sin(), -angle.cos()); + + let line_length = + (dir.x * size.width.to_f32_px()).abs() + (dir.y * size.height.to_f32_px()).abs(); + + let inv_dir_length = 1.0 / (dir.x * dir.x + dir.y * dir.y).sqrt(); + + // This is the vector between the center and the ending point; i.e. half + // of the distance between the starting point and the ending point. + let delta = Vector2D::new( + Au::from_f32_px(dir.x * inv_dir_length * line_length / 2.0), + Au::from_f32_px(dir.y * inv_dir_length * line_length / 2.0), + ); + + // This is the length of the gradient line. + let length = Au::from_f32_px((delta.x.to_f32_px() * 2.0).hypot(delta.y.to_f32_px() * 2.0)); + + let mut builder = convert_gradient_stops(style, stops, length); + + let center = Point2D::new(size.width / 2, size.height / 2); + + ( + builder.gradient( + (center - delta).to_layout(), + (center + delta).to_layout(), + extend_mode(repeating), + ), + builder.into_stops(), + ) +} + +pub fn radial( + style: &ComputedValues, + size: Size2D<Au>, + stops: &[GradientItem], + shape: EndingShape, + center: Position, + repeating: bool, +) -> (RadialGradient, Vec<GradientStop>) { + let center = Point2D::new( + center.horizontal.to_used_value(size.width), + center.vertical.to_used_value(size.height), + ); + let radius = match shape { + EndingShape::Circle(Circle::Radius(length)) => { + let length = Au::from(length); + Size2D::new(length, length) + }, + EndingShape::Circle(Circle::Extent(extent)) => circle_size_keyword(extent, &size, ¢er), + EndingShape::Ellipse(Ellipse::Radii(x, y)) => { + Size2D::new(x.to_used_value(size.width), y.to_used_value(size.height)) + }, + EndingShape::Ellipse(Ellipse::Extent(extent)) => { + ellipse_size_keyword(extent, &size, ¢er) + }, + }; + + let mut builder = convert_gradient_stops(style, stops, radius.width); + ( + builder.radial_gradient( + center.to_layout(), + radius.to_layout(), + extend_mode(repeating), + ), + builder.into_stops(), + ) +} diff --git a/components/layout_2020/display_list/items.rs b/components/layout_2020/display_list/items.rs new file mode 100644 index 00000000000..b6674d972fe --- /dev/null +++ b/components/layout_2020/display_list/items.rs @@ -0,0 +1,795 @@ +/* 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/. */ + +//! Servo heavily uses display lists, which are retained-mode lists of painting commands to +//! perform. Using a list instead of painting elements in immediate mode allows transforms, hit +//! testing, and invalidation to be performed using the same primitives as painting. It also allows +//! Servo to aggressively cull invisible and out-of-bounds painting elements, to reduce overdraw. +//! +//! Display items describe relatively high-level drawing operations (for example, entire borders +//! and shadows instead of lines and blur operations), to reduce the amount of allocation required. +//! They are therefore not exactly analogous to constructs like Skia pictures, which consist of +//! low-level drawing primitives. + +use euclid::{SideOffsets2D, Vector2D}; +use gfx_traits::print_tree::PrintTree; +use gfx_traits::{self, StackingContextId}; +use msg::constellation_msg::PipelineId; +use net_traits::image::base::Image; +use servo_geometry::MaxRect; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::f32; +use std::fmt; +use style::computed_values::_servo_top_layer::T as InTopLayer; +use webrender_api as wr; +use webrender_api::units::{LayoutPoint, LayoutRect, LayoutSize, LayoutTransform}; +use webrender_api::{BorderRadius, ClipId, ClipMode, CommonItemProperties, ComplexClipRegion}; +use webrender_api::{ExternalScrollId, FilterOp, GlyphInstance, GradientStop, ImageKey}; +use webrender_api::{MixBlendMode, ScrollSensitivity, Shadow, SpatialId}; +use webrender_api::{StickyOffsetBounds, TransformStyle}; + +pub use style::dom::OpaqueNode; + +/// The factor that we multiply the blur radius by in order to inflate the boundaries of display +/// items that involve a blur. This ensures that the display item boundaries include all the ink. +pub static BLUR_INFLATION_FACTOR: i32 = 3; + +/// An index into the vector of ClipScrollNodes. During WebRender conversion these nodes +/// are given ClipIds. +#[derive(Clone, Copy, Debug, PartialEq, Serialize)] +pub struct ClipScrollNodeIndex(usize); + +impl ClipScrollNodeIndex { + pub fn root_scroll_node() -> ClipScrollNodeIndex { + ClipScrollNodeIndex(1) + } + + pub fn root_reference_frame() -> ClipScrollNodeIndex { + ClipScrollNodeIndex(0) + } + + pub fn new(index: usize) -> ClipScrollNodeIndex { + assert_ne!(index, 0, "Use the root_reference_frame constructor"); + assert_ne!(index, 1, "Use the root_scroll_node constructor"); + ClipScrollNodeIndex(index) + } + + pub fn is_root_scroll_node(&self) -> bool { + *self == Self::root_scroll_node() + } + + pub fn to_define_item(&self) -> DisplayItem { + DisplayItem::DefineClipScrollNode(Box::new(DefineClipScrollNodeItem { + base: BaseDisplayItem::empty(), + node_index: *self, + })) + } + + pub fn to_index(self) -> usize { + self.0 + } +} + +/// A set of indices into the clip scroll node vector for a given item. +#[derive(Clone, Copy, Debug, PartialEq, Serialize)] +pub struct ClippingAndScrolling { + pub scrolling: ClipScrollNodeIndex, + pub clipping: Option<ClipScrollNodeIndex>, +} + +impl ClippingAndScrolling { + pub fn simple(scrolling: ClipScrollNodeIndex) -> ClippingAndScrolling { + ClippingAndScrolling { + scrolling, + clipping: None, + } + } + + pub fn new(scrolling: ClipScrollNodeIndex, clipping: ClipScrollNodeIndex) -> Self { + ClippingAndScrolling { + scrolling, + clipping: Some(clipping), + } + } +} + +#[derive(Serialize)] +pub struct DisplayList { + pub list: Vec<DisplayItem>, + pub clip_scroll_nodes: Vec<ClipScrollNode>, +} + +impl DisplayList { + /// Return the bounds of this display list based on the dimensions of the root + /// stacking context. + pub fn bounds(&self) -> LayoutRect { + match self.list.get(0) { + Some(&DisplayItem::PushStackingContext(ref item)) => item.stacking_context.bounds, + Some(_) => unreachable!("Root element of display list not stacking context."), + None => LayoutRect::zero(), + } + } + + pub fn print(&self) { + let mut print_tree = PrintTree::new("Display List".to_owned()); + self.print_with_tree(&mut print_tree); + } + + pub fn print_with_tree(&self, print_tree: &mut PrintTree) { + print_tree.new_level("ClipScrollNodes".to_owned()); + for node in &self.clip_scroll_nodes { + print_tree.add_item(format!("{:?}", node)); + } + print_tree.end_level(); + + print_tree.new_level("Items".to_owned()); + for item in &self.list { + print_tree.add_item(format!( + "{:?} StackingContext: {:?} {:?}", + item, + item.base().stacking_context_id, + item.clipping_and_scrolling() + )); + } + print_tree.end_level(); + } +} + +impl gfx_traits::DisplayList for DisplayList { + /// Analyze the display list to figure out if this may be the first + /// contentful paint (i.e. the display list contains items of type text, + /// image, non-white canvas or SVG). Used by metrics. + fn is_contentful(&self) -> bool { + for item in &self.list { + match item { + &DisplayItem::Text(_) | &DisplayItem::Image(_) => return true, + _ => (), + } + } + + false + } +} + +/// Display list sections that make up a stacking context. Each section here refers +/// to the steps in CSS 2.1 Appendix E. +/// +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub enum DisplayListSection { + BackgroundAndBorders, + BlockBackgroundsAndBorders, + Content, + Outlines, +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub enum StackingContextType { + Real, + PseudoPositioned, + PseudoFloat, +} + +#[derive(Clone, Serialize)] +/// Represents one CSS stacking context, which may or may not have a hardware layer. +pub struct StackingContext { + /// The ID of this StackingContext for uniquely identifying it. + pub id: StackingContextId, + + /// The type of this StackingContext. Used for collecting and sorting. + pub context_type: StackingContextType, + + /// The position and size of this stacking context. + pub bounds: LayoutRect, + + /// The overflow rect for this stacking context in its coordinate system. + pub overflow: LayoutRect, + + /// The `z-index` for this stacking context. + pub z_index: i32, + + /// Whether this is the top layer. + pub in_top_layer: InTopLayer, + + /// CSS filters to be applied to this stacking context (including opacity). + pub filters: Vec<FilterOp>, + + /// The blend mode with which this stacking context blends with its backdrop. + pub mix_blend_mode: MixBlendMode, + + /// A transform to be applied to this stacking context. + pub transform: Option<LayoutTransform>, + + /// The transform style of this stacking context. + pub transform_style: TransformStyle, + + /// The perspective matrix to be applied to children. + pub perspective: Option<LayoutTransform>, + + /// The clip and scroll info for this StackingContext. + pub parent_clipping_and_scrolling: ClippingAndScrolling, + + /// The index of the reference frame that this stacking context estalishes. + pub established_reference_frame: Option<ClipScrollNodeIndex>, +} + +impl StackingContext { + /// Creates a new stacking context. + #[inline] + pub fn new( + id: StackingContextId, + context_type: StackingContextType, + bounds: LayoutRect, + overflow: LayoutRect, + z_index: i32, + in_top_layer: InTopLayer, + filters: Vec<FilterOp>, + mix_blend_mode: MixBlendMode, + transform: Option<LayoutTransform>, + transform_style: TransformStyle, + perspective: Option<LayoutTransform>, + parent_clipping_and_scrolling: ClippingAndScrolling, + established_reference_frame: Option<ClipScrollNodeIndex>, + ) -> StackingContext { + StackingContext { + id, + context_type, + bounds, + overflow, + z_index, + in_top_layer, + filters, + mix_blend_mode, + transform, + transform_style, + perspective, + parent_clipping_and_scrolling, + established_reference_frame, + } + } + + #[inline] + pub fn root() -> StackingContext { + StackingContext::new( + StackingContextId::root(), + StackingContextType::Real, + LayoutRect::zero(), + LayoutRect::zero(), + 0, + InTopLayer::None, + vec![], + MixBlendMode::Normal, + None, + TransformStyle::Flat, + None, + ClippingAndScrolling::simple(ClipScrollNodeIndex::root_scroll_node()), + None, + ) + } + + pub fn to_display_list_items(self) -> (DisplayItem, DisplayItem) { + let mut base_item = BaseDisplayItem::empty(); + base_item.stacking_context_id = self.id; + base_item.clipping_and_scrolling = self.parent_clipping_and_scrolling; + + let pop_item = DisplayItem::PopStackingContext(Box::new(PopStackingContextItem { + base: base_item.clone(), + stacking_context_id: self.id, + })); + + let push_item = DisplayItem::PushStackingContext(Box::new(PushStackingContextItem { + base: base_item, + stacking_context: self, + })); + + (push_item, pop_item) + } +} + +impl Ord for StackingContext { + fn cmp(&self, other: &Self) -> Ordering { + if self.in_top_layer == InTopLayer::Top { + if other.in_top_layer == InTopLayer::Top { + return Ordering::Equal; + } else { + return Ordering::Greater; + } + } else if other.in_top_layer == InTopLayer::Top { + return Ordering::Less; + } + + if self.z_index != 0 || other.z_index != 0 { + return self.z_index.cmp(&other.z_index); + } + + match (self.context_type, other.context_type) { + (StackingContextType::PseudoFloat, StackingContextType::PseudoFloat) => Ordering::Equal, + (StackingContextType::PseudoFloat, _) => Ordering::Less, + (_, StackingContextType::PseudoFloat) => Ordering::Greater, + (_, _) => Ordering::Equal, + } + } +} + +impl PartialOrd for StackingContext { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Eq for StackingContext {} +impl PartialEq for StackingContext { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl fmt::Debug for StackingContext { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let type_string = if self.context_type == StackingContextType::Real { + "StackingContext" + } else { + "Pseudo-StackingContext" + }; + + write!( + f, + "{} at {:?} with overflow {:?}: {:?}", + type_string, self.bounds, self.overflow, self.id + ) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct StickyFrameData { + pub margins: SideOffsets2D<Option<f32>>, + pub vertical_offset_bounds: StickyOffsetBounds, + pub horizontal_offset_bounds: StickyOffsetBounds, +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub enum ClipScrollNodeType { + Placeholder, + ScrollFrame(ScrollSensitivity, ExternalScrollId), + StickyFrame(StickyFrameData), + Clip, +} + +/// Defines a clip scroll node. +#[derive(Clone, Debug, Serialize)] +pub struct ClipScrollNode { + /// The index of the parent of this ClipScrollNode. + pub parent_index: ClipScrollNodeIndex, + + /// The position of this scroll root's frame in the parent stacking context. + pub clip: ClippingRegion, + + /// The rect of the contents that can be scrolled inside of the scroll root. + pub content_rect: LayoutRect, + + /// The type of this ClipScrollNode. + pub node_type: ClipScrollNodeType, +} + +impl ClipScrollNode { + pub fn placeholder() -> ClipScrollNode { + ClipScrollNode { + parent_index: ClipScrollNodeIndex(0), + clip: ClippingRegion::from_rect(LayoutRect::zero()), + content_rect: LayoutRect::zero(), + node_type: ClipScrollNodeType::Placeholder, + } + } + + pub fn is_placeholder(&self) -> bool { + self.node_type == ClipScrollNodeType::Placeholder + } +} + +/// One drawing command in the list. +#[derive(Clone, Serialize)] +pub enum DisplayItem { + Rectangle(Box<CommonDisplayItem<wr::RectangleDisplayItem>>), + Text(Box<CommonDisplayItem<wr::TextDisplayItem, Vec<GlyphInstance>>>), + Image(Box<CommonDisplayItem<wr::ImageDisplayItem>>), + Border(Box<CommonDisplayItem<wr::BorderDisplayItem, Vec<GradientStop>>>), + Gradient(Box<CommonDisplayItem<wr::GradientDisplayItem, Vec<GradientStop>>>), + RadialGradient(Box<CommonDisplayItem<wr::RadialGradientDisplayItem, Vec<GradientStop>>>), + Line(Box<CommonDisplayItem<wr::LineDisplayItem>>), + BoxShadow(Box<CommonDisplayItem<wr::BoxShadowDisplayItem>>), + PushTextShadow(Box<PushTextShadowDisplayItem>), + PopAllTextShadows(Box<PopAllTextShadowsDisplayItem>), + Iframe(Box<IframeDisplayItem>), + PushStackingContext(Box<PushStackingContextItem>), + PopStackingContext(Box<PopStackingContextItem>), + DefineClipScrollNode(Box<DefineClipScrollNodeItem>), +} + +/// Information common to all display items. +#[derive(Clone, Serialize)] +pub struct BaseDisplayItem { + /// Metadata attached to this display item. + pub metadata: DisplayItemMetadata, + + /// The clip rectangle to use for this item. + pub clip_rect: LayoutRect, + + /// The section of the display list that this item belongs to. + pub section: DisplayListSection, + + /// The id of the stacking context this item belongs to. + pub stacking_context_id: StackingContextId, + + /// The clip and scroll info for this item. + pub clipping_and_scrolling: ClippingAndScrolling, +} + +impl BaseDisplayItem { + #[inline(always)] + pub fn new( + metadata: DisplayItemMetadata, + clip_rect: LayoutRect, + section: DisplayListSection, + stacking_context_id: StackingContextId, + clipping_and_scrolling: ClippingAndScrolling, + ) -> BaseDisplayItem { + BaseDisplayItem { + metadata, + clip_rect, + section, + stacking_context_id, + clipping_and_scrolling, + } + } + + #[inline(always)] + pub fn empty() -> BaseDisplayItem { + BaseDisplayItem { + metadata: DisplayItemMetadata { + node: OpaqueNode(0), + pointing: None, + }, + // Create a rectangle of maximal size. + clip_rect: LayoutRect::max_rect(), + section: DisplayListSection::Content, + stacking_context_id: StackingContextId::root(), + clipping_and_scrolling: ClippingAndScrolling::simple( + ClipScrollNodeIndex::root_scroll_node(), + ), + } + } +} + +pub fn empty_common_item_properties() -> CommonItemProperties { + CommonItemProperties { + clip_rect: LayoutRect::max_rect(), + clip_id: ClipId::root(wr::PipelineId::dummy()), + spatial_id: SpatialId::root_scroll_node(wr::PipelineId::dummy()), + hit_info: None, + is_backface_visible: false, + } +} + +/// A clipping region for a display item. Currently, this can describe rectangles, rounded +/// rectangles (for `border-radius`), or arbitrary intersections of the two. Arbitrary transforms +/// are not supported because those are handled by the higher-level `StackingContext` abstraction. +#[derive(Clone, PartialEq, Serialize)] +pub struct ClippingRegion { + /// The main rectangular region. This does not include any corners. + pub main: LayoutRect, + /// Any complex regions. + /// + /// TODO(pcwalton): Atomically reference count these? Not sure if it's worth the trouble. + /// Measure and follow up. + pub complex: Vec<ComplexClipRegion>, +} + +impl ClippingRegion { + /// Returns an empty clipping region that, if set, will result in no pixels being visible. + #[inline] + pub fn empty() -> ClippingRegion { + ClippingRegion { + main: LayoutRect::zero(), + complex: Vec::new(), + } + } + + /// Returns an all-encompassing clipping region that clips no pixels out. + #[inline] + pub fn max() -> ClippingRegion { + ClippingRegion { + main: LayoutRect::max_rect(), + complex: Vec::new(), + } + } + + /// Returns a clipping region that represents the given rectangle. + #[inline] + pub fn from_rect(rect: LayoutRect) -> ClippingRegion { + ClippingRegion { + main: rect, + complex: Vec::new(), + } + } + + /// Intersects this clipping region with the given rounded rectangle. + #[inline] + pub fn intersect_with_rounded_rect(&mut self, rect: LayoutRect, radii: BorderRadius) { + let new_complex_region = ComplexClipRegion { + rect, + radii, + mode: ClipMode::Clip, + }; + + // FIXME(pcwalton): This is O(n²) worst case for disjoint clipping regions. Is that OK? + // They're slow anyway… + // + // Possibly relevant if we want to do better: + // + // http://www.inrg.csie.ntu.edu.tw/algorithm2014/presentation/D&C%20Lee-84.pdf + for existing_complex_region in &mut self.complex { + if completely_encloses(&existing_complex_region, &new_complex_region) { + *existing_complex_region = new_complex_region; + return; + } + if completely_encloses(&new_complex_region, &existing_complex_region) { + return; + } + } + + self.complex.push(new_complex_region); + } +} + +impl fmt::Debug for ClippingRegion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if *self == ClippingRegion::max() { + write!(f, "ClippingRegion::Max") + } else if *self == ClippingRegion::empty() { + write!(f, "ClippingRegion::Empty") + } else if self.main == LayoutRect::max_rect() { + write!(f, "ClippingRegion(Complex={:?})", self.complex) + } else { + write!( + f, + "ClippingRegion(Rect={:?}, Complex={:?})", + self.main, self.complex + ) + } + } +} + +// TODO(pcwalton): This could be more aggressive by considering points that touch the inside of +// the border radius ellipse. +fn completely_encloses(this: &ComplexClipRegion, other: &ComplexClipRegion) -> bool { + let left = this.radii.top_left.width.max(this.radii.bottom_left.width); + let top = this.radii.top_left.height.max(this.radii.top_right.height); + let right = this + .radii + .top_right + .width + .max(this.radii.bottom_right.width); + let bottom = this + .radii + .bottom_left + .height + .max(this.radii.bottom_right.height); + let interior = LayoutRect::new( + LayoutPoint::new(this.rect.origin.x + left, this.rect.origin.y + top), + LayoutSize::new( + this.rect.size.width - left - right, + this.rect.size.height - top - bottom, + ), + ); + interior.origin.x <= other.rect.origin.x && + interior.origin.y <= other.rect.origin.y && + interior.max_x() >= other.rect.max_x() && + interior.max_y() >= other.rect.max_y() +} + +/// Metadata attached to each display item. This is useful for performing auxiliary threads with +/// the display list involving hit testing: finding the originating DOM node and determining the +/// cursor to use when the element is hovered over. +#[derive(Clone, Copy, Serialize)] +pub struct DisplayItemMetadata { + /// The DOM node from which this display item originated. + pub node: OpaqueNode, + /// The value of the `cursor` property when the mouse hovers over this display item. If `None`, + /// this display item is ineligible for pointer events (`pointer-events: none`). + pub pointing: Option<u16>, +} + +#[derive(Clone, Eq, PartialEq, Serialize)] +pub enum TextOrientation { + Upright, + SidewaysLeft, + SidewaysRight, +} + +/// Paints an iframe. +#[derive(Clone, Serialize)] +pub struct IframeDisplayItem { + pub base: BaseDisplayItem, + pub iframe: PipelineId, + pub bounds: LayoutRect, +} + +#[derive(Clone, Serialize)] +pub struct CommonDisplayItem<T, U = ()> { + pub base: BaseDisplayItem, + pub item: T, + pub data: U, +} + +impl<T> CommonDisplayItem<T> { + pub fn new(base: BaseDisplayItem, item: T) -> Box<CommonDisplayItem<T>> { + Box::new(CommonDisplayItem { + base, + item, + data: (), + }) + } +} + +impl<T, U> CommonDisplayItem<T, U> { + pub fn with_data(base: BaseDisplayItem, item: T, data: U) -> Box<CommonDisplayItem<T, U>> { + Box::new(CommonDisplayItem { base, item, data }) + } +} + +/// Defines a text shadow that affects all items until the paired PopTextShadow. +#[derive(Clone, Serialize)] +pub struct PushTextShadowDisplayItem { + /// Fields common to all display items. + pub base: BaseDisplayItem, + + pub shadow: Shadow, +} + +/// Defines a text shadow that affects all items until the next PopTextShadow. +#[derive(Clone, Serialize)] +pub struct PopAllTextShadowsDisplayItem { + /// Fields common to all display items. + pub base: BaseDisplayItem, +} + +/// Defines a stacking context. +#[derive(Clone, Serialize)] +pub struct PushStackingContextItem { + /// Fields common to all display items. + pub base: BaseDisplayItem, + + pub stacking_context: StackingContext, +} + +/// Defines a stacking context. +#[derive(Clone, Serialize)] +pub struct PopStackingContextItem { + /// Fields common to all display items. + pub base: BaseDisplayItem, + + pub stacking_context_id: StackingContextId, +} + +/// Starts a group of items inside a particular scroll root. +#[derive(Clone, Serialize)] +pub struct DefineClipScrollNodeItem { + /// Fields common to all display items. + pub base: BaseDisplayItem, + + /// The scroll root that this item starts. + pub node_index: ClipScrollNodeIndex, +} + +impl DisplayItem { + pub fn base(&self) -> &BaseDisplayItem { + match *self { + DisplayItem::Rectangle(ref rect) => &rect.base, + DisplayItem::Text(ref text) => &text.base, + DisplayItem::Image(ref image_item) => &image_item.base, + DisplayItem::Border(ref border) => &border.base, + DisplayItem::Gradient(ref gradient) => &gradient.base, + DisplayItem::RadialGradient(ref gradient) => &gradient.base, + DisplayItem::Line(ref line) => &line.base, + DisplayItem::BoxShadow(ref box_shadow) => &box_shadow.base, + DisplayItem::PushTextShadow(ref push_text_shadow) => &push_text_shadow.base, + DisplayItem::PopAllTextShadows(ref pop_text_shadow) => &pop_text_shadow.base, + DisplayItem::Iframe(ref iframe) => &iframe.base, + DisplayItem::PushStackingContext(ref stacking_context) => &stacking_context.base, + DisplayItem::PopStackingContext(ref item) => &item.base, + DisplayItem::DefineClipScrollNode(ref item) => &item.base, + } + } + + pub fn clipping_and_scrolling(&self) -> ClippingAndScrolling { + self.base().clipping_and_scrolling + } + + pub fn stacking_context_id(&self) -> StackingContextId { + self.base().stacking_context_id + } + + pub fn section(&self) -> DisplayListSection { + self.base().section + } + + pub fn bounds(&self) -> LayoutRect { + match *self { + DisplayItem::Rectangle(ref item) => item.item.common.clip_rect, + DisplayItem::Text(ref item) => item.item.bounds, + DisplayItem::Image(ref item) => item.item.bounds, + DisplayItem::Border(ref item) => item.item.bounds, + DisplayItem::Gradient(ref item) => item.item.bounds, + DisplayItem::RadialGradient(ref item) => item.item.bounds, + DisplayItem::Line(ref item) => item.item.area, + DisplayItem::BoxShadow(ref item) => item.item.box_bounds, + DisplayItem::PushTextShadow(_) => LayoutRect::zero(), + DisplayItem::PopAllTextShadows(_) => LayoutRect::zero(), + DisplayItem::Iframe(ref item) => item.bounds, + DisplayItem::PushStackingContext(ref item) => item.stacking_context.bounds, + DisplayItem::PopStackingContext(_) => LayoutRect::zero(), + DisplayItem::DefineClipScrollNode(_) => LayoutRect::zero(), + } + } +} + +impl fmt::Debug for DisplayItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let DisplayItem::PushStackingContext(ref item) = *self { + return write!(f, "PushStackingContext({:?})", item.stacking_context); + } + + if let DisplayItem::PopStackingContext(ref item) = *self { + return write!(f, "PopStackingContext({:?}", item.stacking_context_id); + } + + if let DisplayItem::DefineClipScrollNode(ref item) = *self { + return write!(f, "DefineClipScrollNode({:?}", item.node_index); + } + + write!( + f, + "{} @ {:?} {:?}", + match *self { + DisplayItem::Rectangle(_) => "Rectangle".to_owned(), + DisplayItem::Text(_) => "Text".to_owned(), + DisplayItem::Image(_) => "Image".to_owned(), + DisplayItem::Border(_) => "Border".to_owned(), + DisplayItem::Gradient(_) => "Gradient".to_owned(), + DisplayItem::RadialGradient(_) => "RadialGradient".to_owned(), + DisplayItem::Line(_) => "Line".to_owned(), + DisplayItem::BoxShadow(_) => "BoxShadow".to_owned(), + DisplayItem::PushTextShadow(_) => "PushTextShadow".to_owned(), + DisplayItem::PopAllTextShadows(_) => "PopTextShadow".to_owned(), + DisplayItem::Iframe(_) => "Iframe".to_owned(), + DisplayItem::PushStackingContext(_) | + DisplayItem::PopStackingContext(_) | + DisplayItem::DefineClipScrollNode(_) => "".to_owned(), + }, + self.bounds(), + self.base().clip_rect + ) + } +} + +#[derive(Clone, Copy, Serialize)] +pub struct WebRenderImageInfo { + pub width: u32, + pub height: u32, + pub key: Option<ImageKey>, +} + +impl WebRenderImageInfo { + #[inline] + pub fn from_image(image: &Image) -> WebRenderImageInfo { + WebRenderImageInfo { + width: image.width, + height: image.height, + key: image.id, + } + } +} + +/// The type of the scroll offset list. This is only populated if WebRender is in use. +pub type ScrollOffsetMap = HashMap<ExternalScrollId, Vector2D<f32>>; diff --git a/components/layout_2020/display_list/mod.rs b/components/layout_2020/display_list/mod.rs new file mode 100644 index 00000000000..302728fbf22 --- /dev/null +++ b/components/layout_2020/display_list/mod.rs @@ -0,0 +1,19 @@ +/* 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/. */ + +pub use self::builder::BorderPaintingMode; +pub use self::builder::DisplayListBuildState; +pub use self::builder::IndexableText; +pub use self::builder::StackingContextCollectionFlags; +pub use self::builder::StackingContextCollectionState; +pub use self::conversions::ToLayout; +pub use self::webrender_helpers::WebRenderDisplayListConverter; + +mod background; +mod border; +mod builder; +mod conversions; +mod gradient; +pub mod items; +mod webrender_helpers; diff --git a/components/layout_2020/display_list/webrender_helpers.rs b/components/layout_2020/display_list/webrender_helpers.rs new file mode 100644 index 00000000000..6bdb1bfcfdc --- /dev/null +++ b/components/layout_2020/display_list/webrender_helpers.rs @@ -0,0 +1,321 @@ +/* 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/. */ + +// TODO(gw): This contains helper traits and implementations for converting Servo display lists +// into WebRender display lists. In the future, this step should be completely removed. +// This might be achieved by sharing types between WR and Servo display lists, or +// completely converting layout to directly generate WebRender display lists, for example. + +use crate::display_list::items::{BaseDisplayItem, ClipScrollNode, ClipScrollNodeType}; +use crate::display_list::items::{DisplayItem, DisplayList, StackingContextType}; +use msg::constellation_msg::PipelineId; +use webrender_api::units::LayoutPoint; +use webrender_api::{self, ClipId, CommonItemProperties, DisplayItem as WrDisplayItem}; +use webrender_api::{DisplayListBuilder, PropertyBinding, PushStackingContextDisplayItem}; +use webrender_api::{ + RasterSpace, ReferenceFrameKind, SpaceAndClipInfo, SpatialId, StackingContext, +}; + +pub trait WebRenderDisplayListConverter { + fn convert_to_webrender(&mut self, pipeline_id: PipelineId) -> DisplayListBuilder; +} + +struct ClipScrollState { + clip_ids: Vec<Option<ClipId>>, + spatial_ids: Vec<Option<SpatialId>>, + active_clip_id: ClipId, + active_spatial_id: SpatialId, +} + +trait WebRenderDisplayItemConverter { + fn convert_to_webrender( + &mut self, + clip_scroll_nodes: &[ClipScrollNode], + state: &mut ClipScrollState, + builder: &mut DisplayListBuilder, + ); +} + +impl WebRenderDisplayListConverter for DisplayList { + fn convert_to_webrender(&mut self, pipeline_id: PipelineId) -> DisplayListBuilder { + let mut clip_ids = vec![None; self.clip_scroll_nodes.len()]; + let mut spatial_ids = vec![None; self.clip_scroll_nodes.len()]; + + // We need to add the WebRender root reference frame and root scroll node ids + // here manually, because WebRender creates these automatically. + // We also follow the "old" WebRender API for clip/scroll for now, + // hence both arrays are initialized based on FIRST_SPATIAL_NODE_INDEX, + // while FIRST_CLIP_NODE_INDEX is not taken into account. + + let webrender_pipeline = pipeline_id.to_webrender(); + clip_ids[0] = Some(ClipId::root(webrender_pipeline)); + clip_ids[1] = Some(ClipId::root(webrender_pipeline)); + spatial_ids[0] = Some(SpatialId::root_reference_frame(webrender_pipeline)); + spatial_ids[1] = Some(SpatialId::root_scroll_node(webrender_pipeline)); + + let mut state = ClipScrollState { + clip_ids, + spatial_ids, + active_clip_id: ClipId::root(webrender_pipeline), + active_spatial_id: SpatialId::root_scroll_node(webrender_pipeline), + }; + + let mut builder = DisplayListBuilder::with_capacity( + webrender_pipeline, + self.bounds().size, + 1024 * 1024, // 1 MB of space + ); + + for item in &mut self.list { + item.convert_to_webrender(&self.clip_scroll_nodes, &mut state, &mut builder); + } + + builder + } +} + +impl WebRenderDisplayItemConverter for DisplayItem { + fn convert_to_webrender( + &mut self, + clip_scroll_nodes: &[ClipScrollNode], + state: &mut ClipScrollState, + builder: &mut DisplayListBuilder, + ) { + // Note: for each time of a display item, if we register one of `clip_ids` or `spatial_ids`, + // we also register the other one as inherited from the current state or the stack. + // This is not an ideal behavior, but it is compatible with the old WebRender model + // of the clip-scroll tree. + + let clip_and_scroll_indices = self.base().clipping_and_scrolling; + trace!("converting {:?}", clip_and_scroll_indices); + + let cur_spatial_id = state.spatial_ids[clip_and_scroll_indices.scrolling.to_index()] + .expect("Tried to use WebRender SpatialId before it was defined."); + if cur_spatial_id != state.active_spatial_id { + state.active_spatial_id = cur_spatial_id; + } + + let internal_clip_id = clip_and_scroll_indices + .clipping + .unwrap_or(clip_and_scroll_indices.scrolling); + let cur_clip_id = state.clip_ids[internal_clip_id.to_index()] + .expect("Tried to use WebRender ClipId before it was defined."); + if cur_clip_id != state.active_clip_id { + state.active_clip_id = cur_clip_id; + } + + match *self { + DisplayItem::Rectangle(ref mut item) => { + item.item.common = build_common_item_properties(&item.base, state); + builder.push_item(&WrDisplayItem::Rectangle(item.item)); + }, + DisplayItem::Text(ref mut item) => { + item.item.common = build_common_item_properties(&item.base, state); + builder.push_item(&WrDisplayItem::Text(item.item)); + builder.push_iter(item.data.iter()); + }, + DisplayItem::Image(ref mut item) => { + item.item.common = build_common_item_properties(&item.base, state); + builder.push_item(&WrDisplayItem::Image(item.item)); + }, + DisplayItem::Border(ref mut item) => { + item.item.common = build_common_item_properties(&item.base, state); + if !item.data.is_empty() { + builder.push_stops(item.data.as_ref()); + } + builder.push_item(&WrDisplayItem::Border(item.item)); + }, + DisplayItem::Gradient(ref mut item) => { + item.item.common = build_common_item_properties(&item.base, state); + builder.push_stops(item.data.as_ref()); + builder.push_item(&WrDisplayItem::Gradient(item.item)); + }, + DisplayItem::RadialGradient(ref mut item) => { + item.item.common = build_common_item_properties(&item.base, state); + builder.push_stops(item.data.as_ref()); + builder.push_item(&WrDisplayItem::RadialGradient(item.item)); + }, + DisplayItem::Line(ref mut item) => { + item.item.common = build_common_item_properties(&item.base, state); + builder.push_item(&WrDisplayItem::Line(item.item)); + }, + DisplayItem::BoxShadow(ref mut item) => { + item.item.common = build_common_item_properties(&item.base, state); + builder.push_item(&WrDisplayItem::BoxShadow(item.item)); + }, + DisplayItem::PushTextShadow(ref mut item) => { + let common = build_common_item_properties(&item.base, state); + builder.push_shadow( + &SpaceAndClipInfo { + spatial_id: common.spatial_id, + clip_id: common.clip_id, + }, + item.shadow, + true, + ); + }, + DisplayItem::PopAllTextShadows(_) => { + builder.push_item(&WrDisplayItem::PopAllShadows); + }, + DisplayItem::Iframe(ref mut item) => { + let common = build_common_item_properties(&item.base, state); + builder.push_iframe( + item.bounds, + common.clip_rect, + &SpaceAndClipInfo { + spatial_id: common.spatial_id, + clip_id: common.clip_id, + }, + item.iframe.to_webrender(), + true, + ); + }, + DisplayItem::PushStackingContext(ref mut item) => { + let stacking_context = &item.stacking_context; + debug_assert_eq!(stacking_context.context_type, StackingContextType::Real); + + //let mut info = webrender_api::LayoutPrimitiveInfo::new(stacking_context.bounds); + let mut bounds = stacking_context.bounds; + let spatial_id = + if let Some(frame_index) = stacking_context.established_reference_frame { + let (transform, ref_frame) = + match (stacking_context.transform, stacking_context.perspective) { + (None, Some(p)) => ( + p, + ReferenceFrameKind::Perspective { + scrolling_relative_to: None, + }, + ), + (Some(t), None) => (t, ReferenceFrameKind::Transform), + (Some(t), Some(p)) => ( + t.pre_mul(&p), + ReferenceFrameKind::Perspective { + scrolling_relative_to: None, + }, + ), + (None, None) => unreachable!(), + }; + + let spatial_id = builder.push_reference_frame( + stacking_context.bounds.origin, + state.active_spatial_id, + stacking_context.transform_style, + PropertyBinding::Value(transform), + ref_frame, + ); + + state.spatial_ids[frame_index.to_index()] = Some(spatial_id); + state.clip_ids[frame_index.to_index()] = Some(cur_clip_id); + + bounds.origin = LayoutPoint::zero(); + spatial_id + } else { + state.active_spatial_id + }; + + if !stacking_context.filters.is_empty() { + builder.push_item(&WrDisplayItem::SetFilterOps); + builder.push_iter(&stacking_context.filters); + } + + let wr_item = PushStackingContextDisplayItem { + origin: bounds.origin, + spatial_id, + is_backface_visible: true, + stacking_context: StackingContext { + transform_style: stacking_context.transform_style, + mix_blend_mode: stacking_context.mix_blend_mode, + clip_id: None, + raster_space: RasterSpace::Screen, + // TODO(pcwalton): Enable picture caching? + cache_tiles: false, + }, + }; + + builder.push_item(&WrDisplayItem::PushStackingContext(wr_item)); + }, + DisplayItem::PopStackingContext(_) => builder.pop_stacking_context(), + DisplayItem::DefineClipScrollNode(ref mut item) => { + let node = &clip_scroll_nodes[item.node_index.to_index()]; + let item_rect = node.clip.main; + + let parent_spatial_id = state.spatial_ids[node.parent_index.to_index()] + .expect("Tried to use WebRender parent SpatialId before it was defined."); + let parent_clip_id = state.clip_ids[node.parent_index.to_index()] + .expect("Tried to use WebRender parent ClipId before it was defined."); + + match node.node_type { + ClipScrollNodeType::Clip => { + let id = builder.define_clip( + &SpaceAndClipInfo { + clip_id: parent_clip_id, + spatial_id: parent_spatial_id, + }, + item_rect, + node.clip.complex.clone(), + None, + ); + + state.spatial_ids[item.node_index.to_index()] = Some(parent_spatial_id); + state.clip_ids[item.node_index.to_index()] = Some(id); + }, + ClipScrollNodeType::ScrollFrame(scroll_sensitivity, external_id) => { + let space_clip_info = builder.define_scroll_frame( + &SpaceAndClipInfo { + clip_id: parent_clip_id, + spatial_id: parent_spatial_id, + }, + Some(external_id), + node.content_rect, + node.clip.main, + node.clip.complex.clone(), + None, + scroll_sensitivity, + webrender_api::units::LayoutVector2D::zero(), + ); + + state.clip_ids[item.node_index.to_index()] = Some(space_clip_info.clip_id); + state.spatial_ids[item.node_index.to_index()] = + Some(space_clip_info.spatial_id); + }, + ClipScrollNodeType::StickyFrame(ref sticky_data) => { + // TODO: Add define_sticky_frame_with_parent to WebRender. + let id = builder.define_sticky_frame( + parent_spatial_id, + item_rect, + sticky_data.margins, + sticky_data.vertical_offset_bounds, + sticky_data.horizontal_offset_bounds, + webrender_api::units::LayoutVector2D::zero(), + ); + + state.spatial_ids[item.node_index.to_index()] = Some(id); + state.clip_ids[item.node_index.to_index()] = Some(parent_clip_id); + }, + ClipScrollNodeType::Placeholder => { + unreachable!("Found DefineClipScrollNode for Placeholder type node."); + }, + }; + }, + } + } +} + +fn build_common_item_properties( + base: &BaseDisplayItem, + state: &ClipScrollState, +) -> CommonItemProperties { + let tag = match base.metadata.pointing { + Some(cursor) => Some((base.metadata.node.0 as u64, cursor)), + None => None, + }; + CommonItemProperties { + clip_rect: base.clip_rect, + spatial_id: state.active_spatial_id, + clip_id: state.active_clip_id, + // TODO(gw): Make use of the WR backface visibility functionality. + is_backface_visible: true, + hit_info: tag, + } +} diff --git a/components/layout_2020/flex.rs b/components/layout_2020/flex.rs new file mode 100644 index 00000000000..e6aa2f7b2c8 --- /dev/null +++ b/components/layout_2020/flex.rs @@ -0,0 +1,1128 @@ +/* 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/. */ + +//! Layout for elements with a CSS `display` property of `flex`. + +use crate::block::{AbsoluteAssignBSizesTraversal, BlockFlow, MarginsMayCollapseFlag}; +use crate::context::LayoutContext; +use crate::display_list::{ + BorderPaintingMode, DisplayListBuildState, StackingContextCollectionState, +}; +use crate::floats::FloatKind; +use crate::flow::{Flow, FlowClass, FlowFlags, GetBaseFlow, ImmutableFlowUtils, OpaqueFlow}; +use crate::fragment::{Fragment, FragmentBorderBoxIterator, Overflow}; +use crate::layout_debug; +use crate::model::{self, AdjoiningMargins, CollapsibleMargins}; +use crate::model::{IntrinsicISizes, MaybeAuto, SizeConstraint}; +use crate::traversal::PreorderFlowTraversal; +use app_units::{Au, MAX_AU}; +use euclid::Point2D; +use std::cmp::{max, min}; +use std::ops::Range; +use style::computed_values::align_content::T as AlignContent; +use style::computed_values::align_self::T as AlignSelf; +use style::computed_values::flex_direction::T as FlexDirection; +use style::computed_values::flex_wrap::T as FlexWrap; +use style::computed_values::justify_content::T as JustifyContent; +use style::logical_geometry::{Direction, LogicalSize}; +use style::properties::ComputedValues; +use style::servo::restyle_damage::ServoRestyleDamage; +use style::values::computed::flex::FlexBasis; +use style::values::computed::{MaxSize, Size}; + +/// The size of an axis. May be a specified size, a min/max +/// constraint, or an unlimited size +#[derive(Debug, Serialize)] +enum AxisSize { + Definite(Au), + MinMax(SizeConstraint), + Infinite, +} + +impl AxisSize { + /// Generate a new available cross or main axis size from the specified size of the container, + /// containing block size, min constraint, and max constraint + pub fn new(size: Size, content_size: Option<Au>, min: Size, max: MaxSize) -> AxisSize { + match size { + Size::Auto => AxisSize::MinMax(SizeConstraint::new(content_size, min, max, None)), + Size::LengthPercentage(ref lp) => match lp.maybe_to_used_value(content_size) { + Some(length) => AxisSize::Definite(length), + None => AxisSize::Infinite, + }, + } + } +} + +/// This function accepts the flex-basis and the size property in main direction from style, +/// and the container size, then return the used value of flex basis. it can be used to help +/// determining the flex base size and to indicate whether the main size of the item +/// is definite after flex size resolving. +fn from_flex_basis(flex_basis: FlexBasis, main_length: Size, containing_length: Au) -> MaybeAuto { + let width = match flex_basis { + FlexBasis::Content => return MaybeAuto::Auto, + FlexBasis::Size(width) => width, + }; + + let width = match width { + Size::Auto => main_length, + _ => width, + }; + + match width { + Size::Auto => MaybeAuto::Auto, + Size::LengthPercentage(ref lp) => MaybeAuto::Specified(lp.to_used_value(containing_length)), + } +} + +/// Represents a child in a flex container. Most fields here are used in +/// flex size resolving, and items are sorted by the 'order' property. +#[derive(Debug, Serialize)] +struct FlexItem { + /// Main size of a flex item, used to store results of flexible length calcuation. + pub main_size: Au, + /// Used flex base size. + pub base_size: Au, + /// The minimal size in main direction. + pub min_size: Au, + /// The maximal main size. If this property is not actually set by style + /// It will be the largest size available for code reuse. + pub max_size: Au, + /// The index of the actual flow in our child list. + pub index: usize, + /// The 'flex-grow' property of this item. + pub flex_grow: f32, + /// The 'flex-shrink' property of this item. + pub flex_shrink: f32, + /// The 'order' property of this item. + pub order: i32, + /// Whether the main size has met its constraint. + pub is_frozen: bool, + /// True if this flow has property 'visibility::collapse'. + pub is_strut: bool, +} + +impl FlexItem { + pub fn new(index: usize, flow: &dyn Flow) -> FlexItem { + let style = &flow.as_block().fragment.style; + let flex_grow = style.get_position().flex_grow; + let flex_shrink = style.get_position().flex_shrink; + let order = style.get_position().order; + // TODO(stshine): for item with 'visibility:collapse', set is_strut to true. + + FlexItem { + main_size: Au(0), + base_size: Au(0), + min_size: Au(0), + max_size: MAX_AU, + index: index, + flex_grow: flex_grow.into(), + flex_shrink: flex_shrink.into(), + order: order, + is_frozen: false, + is_strut: false, + } + } + + /// Initialize the used flex base size, minimal main size and maximal main size. + /// For block mode container this method should be called in assign_block_size() + /// pass so that the item has already been layouted. + pub fn init_sizes(&mut self, flow: &mut dyn Flow, containing_length: Au, direction: Direction) { + let block = flow.as_mut_block(); + match direction { + // TODO(stshine): the definition of min-{width, height} in style component + // should change to LengthPercentageOrAuto for automatic implied minimal size. + // https://drafts.csswg.org/css-flexbox-1/#min-size-auto + Direction::Inline => { + let basis = from_flex_basis( + block.fragment.style.get_position().flex_basis, + block.fragment.style.content_inline_size(), + containing_length, + ); + + // These methods compute auto margins to zero length, which is exactly what we want. + block.fragment.compute_border_and_padding(containing_length); + block + .fragment + .compute_inline_direction_margins(containing_length); + block + .fragment + .compute_block_direction_margins(containing_length); + + let (border_padding, margin) = block.fragment.surrounding_intrinsic_inline_size(); + let content_size = block.base.intrinsic_inline_sizes.preferred_inline_size - + border_padding - + margin + + block.fragment.box_sizing_boundary(direction); + self.base_size = basis.specified_or_default(content_size); + self.max_size = block + .fragment + .style + .max_inline_size() + .to_used_value(containing_length) + .unwrap_or(MAX_AU); + self.min_size = block + .fragment + .style + .min_inline_size() + .to_used_value(containing_length) + .unwrap_or(Au(0)); + }, + Direction::Block => { + let basis = from_flex_basis( + block.fragment.style.get_position().flex_basis, + block.fragment.style.content_block_size(), + containing_length, + ); + let content_size = block.fragment.border_box.size.block - + block.fragment.border_padding.block_start_end() + + block.fragment.box_sizing_boundary(direction); + self.base_size = basis.specified_or_default(content_size); + self.max_size = block + .fragment + .style + .max_block_size() + .to_used_value(containing_length) + .unwrap_or(MAX_AU); + self.min_size = block + .fragment + .style + .min_block_size() + .to_used_value(containing_length) + .unwrap_or(Au(0)); + }, + } + } + + /// Returns the outer main size of the item, including paddings and margins, + /// clamped by max and min size. + pub fn outer_main_size(&self, flow: &dyn Flow, direction: Direction) -> Au { + let ref fragment = flow.as_block().fragment; + let outer_width = match direction { + Direction::Inline => { + fragment.border_padding.inline_start_end() + fragment.margin.inline_start_end() + }, + Direction::Block => { + fragment.border_padding.block_start_end() + fragment.margin.block_start_end() + }, + }; + max(self.min_size, min(self.base_size, self.max_size)) - + fragment.box_sizing_boundary(direction) + + outer_width + } + + /// Returns the number of auto margins in given direction. + pub fn auto_margin_count(&self, flow: &dyn Flow, direction: Direction) -> i32 { + let margin = flow.as_block().fragment.style.logical_margin(); + let mut margin_count = 0; + match direction { + Direction::Inline => { + if margin.inline_start.is_auto() { + margin_count += 1; + } + if margin.inline_end.is_auto() { + margin_count += 1; + } + }, + Direction::Block => { + if margin.block_start.is_auto() { + margin_count += 1; + } + if margin.block_end.is_auto() { + margin_count += 1; + } + }, + } + margin_count + } +} + +/// A line in a flex container. +// TODO(stshine): More fields are required to handle collapsed items and baseline alignment. +#[derive(Debug, Serialize)] +struct FlexLine { + /// Range of items belong to this line in 'self.items'. + pub range: Range<usize>, + /// Remaining free space of this line, items will grow or shrink based on it being positive or negative. + pub free_space: Au, + /// The number of auto margins of items. + pub auto_margin_count: i32, + /// Line size in the block direction. + pub cross_size: Au, +} + +impl FlexLine { + pub fn new(range: Range<usize>, free_space: Au, auto_margin_count: i32) -> FlexLine { + FlexLine { + range: range, + auto_margin_count: auto_margin_count, + free_space: free_space, + cross_size: Au(0), + } + } + + /// This method implements the flexible lengths resolving algorithm. + /// The 'collapse' parameter is used to indicate whether items with 'visibility: collapse' + /// is included in length resolving. The result main size is stored in 'item.main_size'. + /// <https://drafts.csswg.org/css-flexbox/#resolve-flexible-lengths> + pub fn flex_resolve(&mut self, items: &mut [FlexItem], collapse: bool) { + let mut total_grow = 0.0; + let mut total_shrink = 0.0; + let mut total_scaled = 0.0; + let mut active_count = 0; + // Iterate through items, collect total factors and freeze those that have already met + // their constraints or won't grow/shrink in corresponding scenario. + // https://drafts.csswg.org/css-flexbox/#resolve-flexible-lengths + for item in items.iter_mut().filter(|i| !(i.is_strut && collapse)) { + item.main_size = max(item.min_size, min(item.base_size, item.max_size)); + if (self.free_space > Au(0) && + (item.flex_grow == 0.0 || item.base_size >= item.max_size)) || + (self.free_space < Au(0) && + (item.flex_shrink == 0.0 || item.base_size <= item.min_size)) + { + item.is_frozen = true; + } else { + item.is_frozen = false; + total_grow += item.flex_grow; + total_shrink += item.flex_shrink; + // The scaled factor is used to calculate flex shrink + total_scaled += item.flex_shrink * item.base_size.0 as f32; + active_count += 1; + } + } + + let initial_free_space = self.free_space; + let mut total_variation = Au(1); + // If there is no remaining free space or all items are frozen, stop loop. + while total_variation != Au(0) && self.free_space != Au(0) && active_count > 0 { + self.free_space = + // https://drafts.csswg.org/css-flexbox/#remaining-free-space + if self.free_space > Au(0) { + min(initial_free_space.scale_by(total_grow), self.free_space) + } else { + max(initial_free_space.scale_by(total_shrink), self.free_space) + }; + + total_variation = Au(0); + for item in items + .iter_mut() + .filter(|i| !i.is_frozen) + .filter(|i| !(i.is_strut && collapse)) + { + // Use this and the 'abs()' below to make the code work in both grow and shrink scenarios. + let (factor, end_size) = if self.free_space > Au(0) { + (item.flex_grow / total_grow, item.max_size) + } else { + ( + item.flex_shrink * item.base_size.0 as f32 / total_scaled, + item.min_size, + ) + }; + let variation = self.free_space.scale_by(factor); + if variation.0.abs() >= (end_size - item.main_size).0.abs() { + // Use constraint as the target main size, and freeze item. + total_variation += end_size - item.main_size; + item.main_size = end_size; + item.is_frozen = true; + active_count -= 1; + total_shrink -= item.flex_shrink; + total_grow -= item.flex_grow; + total_scaled -= item.flex_shrink * item.base_size.0 as f32; + } else { + total_variation += variation; + item.main_size += variation; + } + } + self.free_space -= total_variation; + } + } +} + +#[allow(unsafe_code)] +unsafe impl crate::flow::HasBaseFlow for FlexFlow {} + +/// A block with the CSS `display` property equal to `flex`. +#[derive(Debug, Serialize)] +#[repr(C)] +pub struct FlexFlow { + /// Data common to all block flows. + block_flow: BlockFlow, + /// The logical axis which the main axis will be parallel with. + /// The cross axis will be parallel with the opposite logical axis. + main_mode: Direction, + /// The available main axis size + available_main_size: AxisSize, + /// The available cross axis size + available_cross_size: AxisSize, + /// List of flex lines in the container. + lines: Vec<FlexLine>, + /// List of flex-items that belong to this flex-container + items: Vec<FlexItem>, + /// True if the flex-direction is *-reversed + main_reverse: bool, + /// True if this flex container can be multiline. + is_wrappable: bool, + /// True if the cross direction is reversed. + cross_reverse: bool, +} + +impl FlexFlow { + pub fn from_fragment(fragment: Fragment, flotation: Option<FloatKind>) -> FlexFlow { + let main_mode; + let main_reverse; + let is_wrappable; + let cross_reverse; + { + let style = fragment.style(); + let (mode, reverse) = match style.get_position().flex_direction { + FlexDirection::Row => (Direction::Inline, false), + FlexDirection::RowReverse => (Direction::Inline, true), + FlexDirection::Column => (Direction::Block, false), + FlexDirection::ColumnReverse => (Direction::Block, true), + }; + main_mode = mode; + main_reverse = reverse == style.writing_mode.is_bidi_ltr(); + let (wrappable, reverse) = match fragment.style.get_position().flex_wrap { + FlexWrap::Nowrap => (false, false), + FlexWrap::Wrap => (true, false), + FlexWrap::WrapReverse => (true, true), + }; + is_wrappable = wrappable; + // TODO(stshine): Handle vertical writing mode. + cross_reverse = reverse; + } + + FlexFlow { + block_flow: BlockFlow::from_fragment_and_float_kind(fragment, flotation), + main_mode: main_mode, + available_main_size: AxisSize::Infinite, + available_cross_size: AxisSize::Infinite, + lines: Vec::new(), + items: Vec::new(), + main_reverse: main_reverse, + is_wrappable: is_wrappable, + cross_reverse: cross_reverse, + } + } + + pub fn main_mode(&self) -> Direction { + self.main_mode + } + + /// Returns a line start after the last item that is already in a line. + /// Note that when the container main size is infinite(i.e. A column flexbox with auto height), + /// we do not need to do flex resolving and this can be considered as a fast-path, so the + /// 'container_size' param does not need to be 'None'. A line has to contain at least one item; + /// (except this) if the container can be multi-line the sum of outer main size of items should + /// be less than the container size; a line should be filled by items as much as possible. + /// After been collected in a line a item should have its main sizes initialized. + fn get_flex_line(&mut self, container_size: Au) -> Option<FlexLine> { + let start = self.lines.last().map(|line| line.range.end).unwrap_or(0); + if start == self.items.len() { + return None; + } + let mut end = start; + let mut total_line_size = Au(0); + let mut margin_count = 0; + + let items = &mut self.items[start..]; + let mut children = self.block_flow.base.children.random_access_mut(); + for item in items { + let kid = children.get(item.index); + item.init_sizes(kid, container_size, self.main_mode); + let outer_main_size = item.outer_main_size(kid, self.main_mode); + if total_line_size + outer_main_size > container_size && + end != start && + self.is_wrappable + { + break; + } + margin_count += item.auto_margin_count(kid, self.main_mode); + total_line_size += outer_main_size; + end += 1; + } + + let line = FlexLine::new(start..end, container_size - total_line_size, margin_count); + Some(line) + } + + // TODO(zentner): This function should use flex-basis. + // Currently, this is the core of BlockFlow::bubble_inline_sizes() with all float logic + // stripped out, and max replaced with union_nonbreaking_inline. + fn inline_mode_bubble_inline_sizes(&mut self) { + // FIXME(emilio): This doesn't handle at all writing-modes. + let fixed_width = + !model::style_length(self.block_flow.fragment.style().get_position().width, None) + .is_auto(); + + let mut computation = self.block_flow.fragment.compute_intrinsic_inline_sizes(); + if !fixed_width { + for kid in self.block_flow.base.children.iter_mut() { + let base = kid.mut_base(); + let is_absolutely_positioned = + base.flags.contains(FlowFlags::IS_ABSOLUTELY_POSITIONED); + if !is_absolutely_positioned { + let flex_item_inline_sizes = IntrinsicISizes { + minimum_inline_size: base.intrinsic_inline_sizes.minimum_inline_size, + preferred_inline_size: base.intrinsic_inline_sizes.preferred_inline_size, + }; + computation.union_nonbreaking_inline(&flex_item_inline_sizes); + } + } + } + self.block_flow.base.intrinsic_inline_sizes = computation.finish(); + } + + // TODO(zentner): This function should use flex-basis. + // Currently, this is the core of BlockFlow::bubble_inline_sizes() with all float logic + // stripped out. + fn block_mode_bubble_inline_sizes(&mut self) { + let fixed_width = + !model::style_length(self.block_flow.fragment.style().get_position().width, None) + .is_auto(); + + let mut computation = self.block_flow.fragment.compute_intrinsic_inline_sizes(); + if !fixed_width { + for kid in self.block_flow.base.children.iter_mut() { + let base = kid.mut_base(); + let is_absolutely_positioned = + base.flags.contains(FlowFlags::IS_ABSOLUTELY_POSITIONED); + if !is_absolutely_positioned { + computation.content_intrinsic_sizes.minimum_inline_size = max( + computation.content_intrinsic_sizes.minimum_inline_size, + base.intrinsic_inline_sizes.minimum_inline_size, + ); + + computation.content_intrinsic_sizes.preferred_inline_size = max( + computation.content_intrinsic_sizes.preferred_inline_size, + base.intrinsic_inline_sizes.preferred_inline_size, + ); + } + } + } + self.block_flow.base.intrinsic_inline_sizes = computation.finish(); + } + + // TODO(zentner): This function needs to be radically different for multi-line flexbox. + // Currently, this is the core of BlockFlow::propagate_assigned_inline_size_to_children() with + // all float and table logic stripped out. + fn block_mode_assign_inline_sizes( + &mut self, + _layout_context: &LayoutContext, + inline_start_content_edge: Au, + inline_end_content_edge: Au, + content_inline_size: Au, + ) { + let _scope = layout_debug_scope!("flex::block_mode_assign_inline_sizes"); + debug!("flex::block_mode_assign_inline_sizes"); + + // FIXME (mbrubeck): Get correct mode for absolute containing block + let containing_block_mode = self.block_flow.base.writing_mode; + + let container_block_size = match self.available_main_size { + AxisSize::Definite(length) => Some(length), + _ => None, + }; + let container_inline_size = match self.available_cross_size { + AxisSize::Definite(length) => length, + AxisSize::MinMax(ref constraint) => constraint.clamp(content_inline_size), + AxisSize::Infinite => content_inline_size, + }; + + let mut children = self.block_flow.base.children.random_access_mut(); + for kid in &mut self.items { + let kid_base = children.get(kid.index).mut_base(); + kid_base.block_container_explicit_block_size = container_block_size; + if kid_base + .flags + .contains(FlowFlags::INLINE_POSITION_IS_STATIC) + { + // The inline-start margin edge of the child flow is at our inline-start content + // edge, and its inline-size is our content inline-size. + kid_base.position.start.i = + if kid_base.writing_mode.is_bidi_ltr() == containing_block_mode.is_bidi_ltr() { + inline_start_content_edge + } else { + // The kid's inline 'start' is at the parent's 'end' + inline_end_content_edge + }; + } + kid_base.block_container_inline_size = container_inline_size; + kid_base.block_container_writing_mode = containing_block_mode; + kid_base.position.start.i = inline_start_content_edge; + } + } + + fn inline_mode_assign_inline_sizes( + &mut self, + layout_context: &LayoutContext, + inline_start_content_edge: Au, + _inline_end_content_edge: Au, + content_inline_size: Au, + ) { + let _scope = layout_debug_scope!("flex::inline_mode_assign_inline_sizes"); + debug!("inline_mode_assign_inline_sizes"); + + debug!("content_inline_size = {:?}", content_inline_size); + + let child_count = ImmutableFlowUtils::child_count(self as &dyn Flow) as i32; + debug!("child_count = {:?}", child_count); + if child_count == 0 { + return; + } + + let inline_size = match self.available_main_size { + AxisSize::Definite(length) => length, + AxisSize::MinMax(ref constraint) => constraint.clamp(content_inline_size), + AxisSize::Infinite => content_inline_size, + }; + + let container_mode = self.block_flow.base.block_container_writing_mode; + self.block_flow.base.position.size.inline = inline_size; + + // Calculate non-auto block size to pass to children. + let box_border = self + .block_flow + .fragment + .box_sizing_boundary(Direction::Block); + + let parent_container_size = self + .block_flow + .explicit_block_containing_size(layout_context.shared_context()); + // https://drafts.csswg.org/css-ui-3/#box-sizing + let explicit_content_size = self + .block_flow + .explicit_block_size(parent_container_size) + .map(|x| max(x - box_border, Au(0))); + let containing_block_text_align = self + .block_flow + .fragment + .style() + .get_inherited_text() + .text_align; + + while let Some(mut line) = self.get_flex_line(inline_size) { + let items = &mut self.items[line.range.clone()]; + line.flex_resolve(items, false); + // TODO(stshine): if this flex line contain children that have + // property visibility:collapse, exclude them and resolve again. + + let item_count = items.len() as i32; + let mut cur_i = inline_start_content_edge; + let item_interval = if line.free_space >= Au(0) && line.auto_margin_count == 0 { + match self + .block_flow + .fragment + .style() + .get_position() + .justify_content + { + JustifyContent::SpaceBetween => { + if item_count == 1 { + Au(0) + } else { + line.free_space / (item_count - 1) + } + }, + JustifyContent::SpaceAround => line.free_space / item_count, + _ => Au(0), + } + } else { + Au(0) + }; + + match self + .block_flow + .fragment + .style() + .get_position() + .justify_content + { + // Overflow equally in both ends of line. + JustifyContent::Center | JustifyContent::SpaceAround => { + cur_i += (line.free_space - item_interval * (item_count - 1)) / 2; + }, + JustifyContent::FlexEnd => { + cur_i += line.free_space; + }, + _ => {}, + } + + let mut children = self.block_flow.base.children.random_access_mut(); + for item in items.iter_mut() { + let block = children.get(item.index).as_mut_block(); + + block.base.block_container_writing_mode = container_mode; + block.base.block_container_inline_size = inline_size; + block.base.block_container_explicit_block_size = explicit_content_size; + // Per CSS 2.1 § 16.3.1, text alignment propagates to all children in flow. + // + // TODO(#2265, pcwalton): Do this in the cascade instead. + block.base.flags.set_text_align(containing_block_text_align); + + let margin = block.fragment.style().logical_margin(); + let auto_len = if line.auto_margin_count == 0 || line.free_space <= Au(0) { + Au(0) + } else { + line.free_space / line.auto_margin_count + }; + let margin_inline_start = MaybeAuto::from_style(margin.inline_start, inline_size) + .specified_or_default(auto_len); + let margin_inline_end = MaybeAuto::from_style(margin.inline_end, inline_size) + .specified_or_default(auto_len); + let item_inline_size = item.main_size - + block.fragment.box_sizing_boundary(self.main_mode) + + block.fragment.border_padding.inline_start_end(); + let item_outer_size = item_inline_size + block.fragment.margin.inline_start_end(); + + block.fragment.margin.inline_start = margin_inline_start; + block.fragment.margin.inline_end = margin_inline_end; + block.fragment.border_box.start.i = margin_inline_start; + block.fragment.border_box.size.inline = item_inline_size; + block.base.position.start.i = if !self.main_reverse { + cur_i + } else { + inline_start_content_edge * 2 + content_inline_size - cur_i - item_outer_size + }; + block.base.position.size.inline = item_outer_size; + cur_i += item_outer_size + item_interval; + } + self.lines.push(line); + } + } + + // TODO(zentner): This function should actually flex elements! + fn block_mode_assign_block_size(&mut self) { + let mut cur_b = if !self.main_reverse { + self.block_flow.fragment.border_padding.block_start + } else { + self.block_flow.fragment.border_box.size.block + }; + + let mut children = self.block_flow.base.children.random_access_mut(); + for item in &mut self.items { + let base = children.get(item.index).mut_base(); + if !self.main_reverse { + base.position.start.b = cur_b; + cur_b = cur_b + base.position.size.block; + } else { + cur_b = cur_b - base.position.size.block; + base.position.start.b = cur_b; + } + } + } + + fn inline_mode_assign_block_size(&mut self, layout_context: &LayoutContext) { + let _scope = layout_debug_scope!("flex::inline_mode_assign_block_size"); + + let line_count = self.lines.len() as i32; + let line_align = self + .block_flow + .fragment + .style() + .get_position() + .align_content; + let mut cur_b = self.block_flow.fragment.border_padding.block_start; + let mut total_cross_size = Au(0); + let mut line_interval = Au(0); + + { + let mut children = self.block_flow.base.children.random_access_mut(); + for line in self.lines.iter_mut() { + for item in &self.items[line.range.clone()] { + let fragment = &children.get(item.index).as_block().fragment; + line.cross_size = max( + line.cross_size, + fragment.border_box.size.block + fragment.margin.block_start_end(), + ); + } + total_cross_size += line.cross_size; + } + } + + let box_border = self + .block_flow + .fragment + .box_sizing_boundary(Direction::Block); + let parent_container_size = self + .block_flow + .explicit_block_containing_size(layout_context.shared_context()); + // https://drafts.csswg.org/css-ui-3/#box-sizing + let explicit_content_size = self + .block_flow + .explicit_block_size(parent_container_size) + .map(|x| max(x - box_border, Au(0))); + + if let Some(container_block_size) = explicit_content_size { + let free_space = container_block_size - total_cross_size; + total_cross_size = container_block_size; + + if line_align == AlignContent::Stretch && free_space > Au(0) { + for line in self.lines.iter_mut() { + line.cross_size += free_space / line_count; + } + } + + line_interval = match line_align { + AlignContent::SpaceBetween => { + if line_count <= 1 { + Au(0) + } else { + free_space / (line_count - 1) + } + }, + AlignContent::SpaceAround => { + if line_count == 0 { + Au(0) + } else { + free_space / line_count + } + }, + _ => Au(0), + }; + + match line_align { + AlignContent::Center | AlignContent::SpaceAround => { + cur_b += (free_space - line_interval * (line_count - 1)) / 2; + }, + AlignContent::FlexEnd => { + cur_b += free_space; + }, + _ => {}, + } + } + + let mut children = self.block_flow.base.children.random_access_mut(); + for line in &self.lines { + for item in self.items[line.range.clone()].iter_mut() { + let block = children.get(item.index).as_mut_block(); + let auto_margin_count = item.auto_margin_count(block, Direction::Block); + let margin = block.fragment.style().logical_margin(); + + let mut margin_block_start = block.fragment.margin.block_start; + let mut margin_block_end = block.fragment.margin.block_end; + let mut free_space = line.cross_size - + block.base.position.size.block - + block.fragment.margin.block_start_end(); + + // The spec is a little vague here, but if I understand it correctly, the outer + // cross size of item should equal to the line size if any auto margin exists. + // https://drafts.csswg.org/css-flexbox/#algo-cross-margins + if auto_margin_count > 0 { + if margin.block_start.is_auto() { + margin_block_start = if free_space < Au(0) { + Au(0) + } else { + free_space / auto_margin_count + }; + } + margin_block_end = + line.cross_size - margin_block_start - block.base.position.size.block; + free_space = Au(0); + } + + let self_align = block.fragment.style().get_position().align_self; + if self_align == AlignSelf::Stretch && + block.fragment.style().content_block_size().is_auto() + { + free_space = Au(0); + block.base.block_container_explicit_block_size = Some(line.cross_size); + block.base.position.size.block = + line.cross_size - margin_block_start - margin_block_end; + block.fragment.border_box.size.block = block.base.position.size.block; + // FIXME(stshine): item with 'align-self: stretch' and auto cross size should act + // as if it has a fixed cross size, all child blocks should resolve against it. + // block.assign_block_size(layout_context); + } + block.base.position.start.b = margin_block_start + + if !self.cross_reverse { + cur_b + } else { + self.block_flow.fragment.border_padding.block_start * 2 + total_cross_size - + cur_b - + line.cross_size + }; + // TODO(stshine): support baseline alignment. + if free_space != Au(0) { + let flex_cross = match self_align { + AlignSelf::FlexEnd => free_space, + AlignSelf::Center => free_space / 2, + _ => Au(0), + }; + block.base.position.start.b += if !self.cross_reverse { + flex_cross + } else { + free_space - flex_cross + }; + } + } + cur_b += line_interval + line.cross_size; + } + let total_block_size = + total_cross_size + self.block_flow.fragment.border_padding.block_start_end(); + self.block_flow.fragment.border_box.size.block = total_block_size; + self.block_flow.base.position.size.block = total_block_size; + } +} + +impl Flow for FlexFlow { + fn class(&self) -> FlowClass { + FlowClass::Flex + } + + fn as_flex(&self) -> &FlexFlow { + self + } + + fn as_block(&self) -> &BlockFlow { + &self.block_flow + } + + fn as_mut_block(&mut self) -> &mut BlockFlow { + &mut self.block_flow + } + + fn mark_as_root(&mut self) { + self.block_flow.mark_as_root(); + } + + fn bubble_inline_sizes(&mut self) { + let _scope = layout_debug_scope!( + "flex::bubble_inline_sizes {:x}", + self.block_flow.base.debug_id() + ); + + // Flexbox Section 9.0: Generate anonymous flex items: + // This part was handled in the flow constructor. + + // Flexbox Section 9.1: Re-order flex items according to their order. + // FIXME(stshine): This should be done during flow construction. + let mut items: Vec<FlexItem> = self + .block_flow + .base + .children + .iter() + .enumerate() + .filter(|&(_, flow)| { + !flow + .as_block() + .base + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + }) + .map(|(index, flow)| FlexItem::new(index, flow)) + .collect(); + + items.sort_by_key(|item| item.order); + self.items = items; + + match self.main_mode { + Direction::Inline => self.inline_mode_bubble_inline_sizes(), + Direction::Block => self.block_mode_bubble_inline_sizes(), + } + } + + fn assign_inline_sizes(&mut self, layout_context: &LayoutContext) { + let _scope = layout_debug_scope!( + "flex::assign_inline_sizes {:x}", + self.block_flow.base.debug_id() + ); + debug!("assign_inline_sizes"); + + if !self + .block_flow + .base + .restyle_damage + .intersects(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW) + { + return; + } + + self.block_flow + .initialize_container_size_for_root(layout_context.shared_context()); + + // Our inline-size was set to the inline-size of the containing block by the flow's parent. + // Now compute the real value. + let containing_block_inline_size = self.block_flow.base.block_container_inline_size; + self.block_flow.compute_used_inline_size( + layout_context.shared_context(), + containing_block_inline_size, + ); + if self.block_flow.base.flags.is_float() { + self.block_flow + .float + .as_mut() + .unwrap() + .containing_inline_size = containing_block_inline_size + } + + let (available_block_size, available_inline_size) = { + let style = &self.block_flow.fragment.style; + let (specified_block_size, specified_inline_size) = if style.writing_mode.is_vertical() + { + (style.get_position().width, style.get_position().height) + } else { + (style.get_position().height, style.get_position().width) + }; + + let available_inline_size = AxisSize::new( + specified_inline_size, + Some(self.block_flow.base.block_container_inline_size), + style.min_inline_size(), + style.max_inline_size(), + ); + + let available_block_size = AxisSize::new( + specified_block_size, + self.block_flow.base.block_container_explicit_block_size, + style.min_block_size(), + style.max_block_size(), + ); + (available_block_size, available_inline_size) + }; + + // Move in from the inline-start border edge. + let inline_start_content_edge = self.block_flow.fragment.border_box.start.i + + self.block_flow.fragment.border_padding.inline_start; + + debug!( + "inline_start_content_edge = {:?}", + inline_start_content_edge + ); + + let padding_and_borders = self.block_flow.fragment.border_padding.inline_start_end(); + + // Distance from the inline-end margin edge to the inline-end content edge. + let inline_end_content_edge = self.block_flow.fragment.margin.inline_end + + self.block_flow.fragment.border_padding.inline_end; + + debug!("padding_and_borders = {:?}", padding_and_borders); + debug!( + "self.block_flow.fragment.border_box.size.inline = {:?}", + self.block_flow.fragment.border_box.size.inline + ); + let content_inline_size = + self.block_flow.fragment.border_box.size.inline - padding_and_borders; + + match self.main_mode { + Direction::Inline => { + self.available_main_size = available_inline_size; + self.available_cross_size = available_block_size; + self.inline_mode_assign_inline_sizes( + layout_context, + inline_start_content_edge, + inline_end_content_edge, + content_inline_size, + ) + }, + Direction::Block => { + self.available_main_size = available_block_size; + self.available_cross_size = available_inline_size; + self.block_mode_assign_inline_sizes( + layout_context, + inline_start_content_edge, + inline_end_content_edge, + content_inline_size, + ) + }, + } + } + + fn assign_block_size(&mut self, layout_context: &LayoutContext) { + match self.main_mode { + Direction::Inline => { + self.inline_mode_assign_block_size(layout_context); + let block_start = + AdjoiningMargins::from_margin(self.block_flow.fragment.margin.block_start); + let block_end = + AdjoiningMargins::from_margin(self.block_flow.fragment.margin.block_end); + self.block_flow.base.collapsible_margins = + CollapsibleMargins::Collapse(block_start, block_end); + + // TODO(stshine): assign proper static position for absolute descendants. + if (&*self as &dyn Flow).contains_roots_of_absolute_flow_tree() { + // Assign block-sizes for all flows in this absolute flow tree. + // This is preorder because the block-size of an absolute flow may depend on + // the block-size of its containing block, which may also be an absolute flow. + let assign_abs_b_sizes = + AbsoluteAssignBSizesTraversal(layout_context.shared_context()); + assign_abs_b_sizes.traverse_absolute_flows(&mut *self); + } + }, + Direction::Block => { + self.block_flow.assign_block_size_block_base( + layout_context, + None, + MarginsMayCollapseFlag::MarginsMayNotCollapse, + ); + self.block_mode_assign_block_size(); + }, + } + } + + fn compute_stacking_relative_position(&mut self, layout_context: &LayoutContext) { + self.block_flow + .compute_stacking_relative_position(layout_context) + } + + fn place_float_if_applicable<'a>(&mut self) { + self.block_flow.place_float_if_applicable() + } + + 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, state: &mut DisplayListBuildState) { + // Draw the rest of the block. + self.as_mut_block() + .build_display_list_for_block(state, BorderPaintingMode::Separate) + } + + fn collect_stacking_contexts(&mut self, state: &mut StackingContextCollectionState) { + self.block_flow.collect_stacking_contexts(state); + } + + 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 dyn 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 dyn FnMut(&mut Fragment)) { + self.block_flow.mutate_fragments(mutator); + } +} diff --git a/components/layout_2020/floats.rs b/components/layout_2020/floats.rs new file mode 100644 index 00000000000..42df4e07dfd --- /dev/null +++ b/components/layout_2020/floats.rs @@ -0,0 +1,605 @@ +/* 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/. */ + +use crate::block::FormattingContextType; +use crate::flow::{Flow, FlowFlags, GetBaseFlow, ImmutableFlowUtils}; +use crate::persistent_list::PersistentList; +use app_units::{Au, MAX_AU}; +use std::cmp::{max, min}; +use std::fmt; +use style::computed_values::float::T as StyleFloat; +use style::logical_geometry::{LogicalRect, LogicalSize, WritingMode}; +use style::values::computed::Size; + +/// The kind of float: left or right. +#[derive(Clone, Copy, Debug, Serialize)] +pub enum FloatKind { + Left, + Right, +} + +impl FloatKind { + pub fn from_property(property: StyleFloat) -> Option<FloatKind> { + match property { + StyleFloat::None => None, + StyleFloat::Left => Some(FloatKind::Left), + StyleFloat::Right => Some(FloatKind::Right), + } + } +} + +/// The kind of clearance: left, right, or both. +#[derive(Clone, Copy)] +pub enum ClearType { + Left, + Right, + Both, +} + +/// Information about a single float. +#[derive(Clone, Copy)] +struct Float { + /// The boundaries of this float. + bounds: LogicalRect<Au>, + /// The kind of float: left or right. + kind: FloatKind, +} + +impl fmt::Debug for Float { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "bounds={:?} kind={:?}", self.bounds, self.kind) + } +} + +/// Information about the floats next to a flow. +#[derive(Clone)] +struct FloatList { + /// Information about each of the floats here. + floats: PersistentList<Float>, + /// Cached copy of the maximum block-start offset of the float. + max_block_start: Option<Au>, +} + +impl FloatList { + fn new() -> FloatList { + FloatList { + floats: PersistentList::new(), + max_block_start: None, + } + } + + /// Returns true if the list is allocated and false otherwise. If false, there are guaranteed + /// not to be any floats. + fn is_present(&self) -> bool { + self.floats.len() > 0 + } +} + +impl fmt::Debug for FloatList { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "max_block_start={:?} floats={}", + self.max_block_start, + self.floats.len() + )?; + for float in self.floats.iter() { + write!(f, " {:?}", float)?; + } + Ok(()) + } +} + +/// All the information necessary to place a float. +pub struct PlacementInfo { + /// The dimensions of the float. + pub size: LogicalSize<Au>, + /// The minimum block-start of the float, as determined by earlier elements. + pub ceiling: Au, + /// The maximum inline-end position of the float, generally determined by the containing block. + pub max_inline_size: Au, + /// The kind of float. + pub kind: FloatKind, +} + +impl fmt::Debug for PlacementInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "size={:?} ceiling={:?} max_inline_size={:?} kind={:?}", + self.size, self.ceiling, self.max_inline_size, self.kind + ) + } +} + +fn range_intersect( + block_start_1: Au, + block_end_1: Au, + block_start_2: Au, + block_end_2: Au, +) -> (Au, Au) { + ( + max(block_start_1, block_start_2), + min(block_end_1, block_end_2), + ) +} + +/// Encapsulates information about floats. This is optimized to avoid allocation if there are +/// no floats, and to avoid copying when translating the list of floats downward. +#[derive(Clone)] +pub struct Floats { + /// The list of floats. + list: FloatList, + /// The offset of the flow relative to the first float. + offset: LogicalSize<Au>, + /// The writing mode of these floats. + pub writing_mode: WritingMode, +} + +impl fmt::Debug for Floats { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if !self.list.is_present() { + write!(f, "[empty]") + } else { + write!(f, "offset={:?} floats={:?}", self.offset, self.list) + } + } +} + +impl Floats { + /// Creates a new `Floats` object. + pub fn new(writing_mode: WritingMode) -> Floats { + Floats { + list: FloatList::new(), + offset: LogicalSize::zero(writing_mode), + writing_mode: writing_mode, + } + } + + /// Adjusts the recorded offset of the flow relative to the first float. + pub fn translate(&mut self, delta: LogicalSize<Au>) { + self.offset = self.offset + delta + } + + /// Returns the position of the last float in flow coordinates. + pub fn last_float_pos(&self) -> Option<LogicalRect<Au>> { + match self.list.floats.front() { + None => None, + Some(float) => Some(float.bounds.translate_by_size(self.offset)), + } + } + + /// Returns a rectangle that encloses the region from block-start to block-start + block-size, + /// with inline-size small enough that it doesn't collide with any floats. max_x is the + /// inline-size beyond which floats have no effect. (Generally this is the containing block + /// inline-size.) + pub fn available_rect( + &self, + block_start: Au, + block_size: Au, + max_x: Au, + ) -> Option<LogicalRect<Au>> { + let list = &self.list; + let block_start = block_start - self.offset.block; + + debug!("available_rect: trying to find space at {:?}", block_start); + + // Relevant dimensions for the inline-end-most inline-start float + let mut max_inline_start = Au(0) - self.offset.inline; + let mut l_block_start = None; + let mut l_block_end = None; + // Relevant dimensions for the inline-start-most inline-end float + let mut min_inline_end = max_x - self.offset.inline; + let mut r_block_start = None; + let mut r_block_end = None; + + // Find the float collisions for the given range in the block direction. + for float in list.floats.iter() { + debug!("available_rect: Checking for collision against float"); + let float_pos = float.bounds.start; + let float_size = float.bounds.size; + + debug!("float_pos: {:?}, float_size: {:?}", float_pos, float_size); + match float.kind { + FloatKind::Left + if float_pos.i + float_size.inline > max_inline_start && + float_pos.b + float_size.block > block_start && + float_pos.b < block_start + block_size => + { + max_inline_start = float_pos.i + float_size.inline; + + l_block_start = Some(float_pos.b); + l_block_end = Some(float_pos.b + float_size.block); + + debug!( + "available_rect: collision with inline_start float: new \ + max_inline_start is {:?}", + max_inline_start + ); + } + FloatKind::Right + if float_pos.i < min_inline_end && + float_pos.b + float_size.block > block_start && + float_pos.b < block_start + block_size => + { + min_inline_end = float_pos.i; + + r_block_start = Some(float_pos.b); + r_block_end = Some(float_pos.b + float_size.block); + debug!( + "available_rect: collision with inline_end float: new min_inline_end \ + is {:?}", + min_inline_end + ); + } + FloatKind::Left | FloatKind::Right => {}, + } + } + + // Extend the vertical range of the rectangle to the closest floats. + // If there are floats on both sides, take the intersection of the + // two areas. Also make sure we never return a block-start smaller than the + // given upper bound. + let (block_start, block_end) = + match (r_block_start, r_block_end, l_block_start, l_block_end) { + ( + Some(r_block_start), + Some(r_block_end), + Some(l_block_start), + Some(l_block_end), + ) => range_intersect( + max(block_start, r_block_start), + r_block_end, + max(block_start, l_block_start), + l_block_end, + ), + (None, None, Some(l_block_start), Some(l_block_end)) => { + (max(block_start, l_block_start), l_block_end) + }, + (Some(r_block_start), Some(r_block_end), None, None) => { + (max(block_start, r_block_start), r_block_end) + }, + (None, None, None, None) => return None, + _ => panic!("Reached unreachable state when computing float area"), + }; + + // FIXME(eatkinson): This assertion is too strong and fails in some cases. It is OK to + // return negative inline-sizes since we check against that inline-end away, but we should + // still understand why they occur and add a stronger assertion here. + // assert!(max_inline-start < min_inline-end); + + assert!(block_start <= block_end, "Float position error"); + + Some(LogicalRect::new( + self.writing_mode, + max_inline_start + self.offset.inline, + block_start + self.offset.block, + min_inline_end - max_inline_start, + block_end - block_start, + )) + } + + /// Adds a new float to the list. + pub fn add_float(&mut self, info: &PlacementInfo) { + let new_info = PlacementInfo { + size: info.size, + ceiling: match self.list.max_block_start { + None => info.ceiling, + Some(max_block_start) => max(info.ceiling, max_block_start + self.offset.block), + }, + max_inline_size: info.max_inline_size, + kind: info.kind, + }; + + debug!("add_float: added float with info {:?}", new_info); + + let new_float = Float { + bounds: LogicalRect::from_point_size( + self.writing_mode, + self.place_between_floats(&new_info).start - self.offset, + info.size, + ), + kind: info.kind, + }; + + self.list.floats = self.list.floats.prepend_elem(new_float); + self.list.max_block_start = match self.list.max_block_start { + None => Some(new_float.bounds.start.b), + Some(max_block_start) => Some(max(max_block_start, new_float.bounds.start.b)), + } + } + + /// Given the three sides of the bounding rectangle in the block-start direction, finds the + /// largest block-size that will result in the rectangle not colliding with any floats. Returns + /// `None` if that block-size is infinite. + fn max_block_size_for_bounds( + &self, + inline_start: Au, + block_start: Au, + inline_size: Au, + ) -> Option<Au> { + let list = &self.list; + + let block_start = block_start - self.offset.block; + let inline_start = inline_start - self.offset.inline; + let mut max_block_size = None; + + for float in list.floats.iter() { + if float.bounds.start.b + float.bounds.size.block > block_start && + float.bounds.start.i + float.bounds.size.inline > inline_start && + float.bounds.start.i < inline_start + inline_size + { + let new_y = float.bounds.start.b; + max_block_size = Some(min(max_block_size.unwrap_or(new_y), new_y)); + } + } + + max_block_size.map(|h| h + self.offset.block) + } + + /// Given placement information, finds the closest place a fragment can be positioned without + /// colliding with any floats. + pub fn place_between_floats(&self, info: &PlacementInfo) -> LogicalRect<Au> { + debug!("place_between_floats: Placing object with {:?}", info.size); + + // If no floats, use this fast path. + if !self.list.is_present() { + match info.kind { + FloatKind::Left => { + return LogicalRect::new( + self.writing_mode, + Au(0), + info.ceiling, + info.max_inline_size, + MAX_AU, + ); + }, + FloatKind::Right => { + return LogicalRect::new( + self.writing_mode, + info.max_inline_size - info.size.inline, + info.ceiling, + info.max_inline_size, + MAX_AU, + ); + }, + } + } + + // Can't go any higher than previous floats or previous elements in the document. + let mut float_b = info.ceiling; + loop { + let maybe_location = + self.available_rect(float_b, info.size.block, info.max_inline_size); + debug!( + "place_float: got available rect: {:?} for block-pos: {:?}", + maybe_location, float_b + ); + match maybe_location { + // If there are no floats blocking us, return the current location + // TODO(eatkinson): integrate with overflow + None => { + return match info.kind { + FloatKind::Left => LogicalRect::new( + self.writing_mode, + Au(0), + float_b, + info.max_inline_size, + MAX_AU, + ), + FloatKind::Right => LogicalRect::new( + self.writing_mode, + info.max_inline_size - info.size.inline, + float_b, + info.max_inline_size, + MAX_AU, + ), + }; + }, + Some(rect) => { + assert_ne!( + rect.start.b + rect.size.block, + float_b, + "Non-terminating float placement" + ); + + // Place here if there is enough room + if rect.size.inline >= info.size.inline { + let block_size = self.max_block_size_for_bounds( + rect.start.i, + rect.start.b, + rect.size.inline, + ); + let block_size = block_size.unwrap_or(MAX_AU); + return match info.kind { + FloatKind::Left => LogicalRect::new( + self.writing_mode, + rect.start.i, + float_b, + rect.size.inline, + block_size, + ), + FloatKind::Right => LogicalRect::new( + self.writing_mode, + rect.start.i + rect.size.inline - info.size.inline, + float_b, + rect.size.inline, + block_size, + ), + }; + } + + // Try to place at the next-lowest location. + // Need to be careful of fencepost errors. + float_b = rect.start.b + rect.size.block; + }, + } + } + } + + pub fn clearance(&self, clear: ClearType) -> Au { + let list = &self.list; + let mut clearance = Au(0); + for float in list.floats.iter() { + match (clear, float.kind) { + (ClearType::Left, FloatKind::Left) | + (ClearType::Right, FloatKind::Right) | + (ClearType::Both, _) => { + let b = self.offset.block + float.bounds.start.b + float.bounds.size.block; + clearance = max(clearance, b); + }, + _ => {}, + } + } + clearance + } + + pub fn is_present(&self) -> bool { + self.list.is_present() + } +} + +/// The speculated inline sizes of floats flowing through or around a flow (depending on whether +/// the flow is a block formatting context). These speculations are always *upper bounds*; the +/// actual inline sizes might be less. Note that this implies that a speculated value of zero is a +/// guarantee that there will be no floats on that side. +/// +/// This is used for two purposes: (a) determining whether we can lay out blocks in parallel; (b) +/// guessing the inline-sizes of block formatting contexts in an effort to lay them out in +/// parallel. +#[derive(Clone, Copy)] +pub struct SpeculatedFloatPlacement { + /// The estimated inline size (an upper bound) of the left floats flowing through this flow. + pub left: Au, + /// The estimated inline size (an upper bound) of the right floats flowing through this flow. + pub right: Au, +} + +impl fmt::Debug for SpeculatedFloatPlacement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "L {:?} R {:?}", self.left, self.right) + } +} + +impl SpeculatedFloatPlacement { + /// Returns a `SpeculatedFloatPlacement` objects with both left and right speculated inline + /// sizes initialized to zero. + pub fn zero() -> SpeculatedFloatPlacement { + SpeculatedFloatPlacement { + left: Au(0), + right: Au(0), + } + } + + /// Given the speculated inline size of the floats out for the inorder predecessor of this + /// flow, computes the speculated inline size of the floats flowing in. + pub fn compute_floats_in(&mut self, flow: &mut dyn Flow) { + let base_flow = flow.base(); + if base_flow.flags.contains(FlowFlags::CLEARS_LEFT) { + self.left = Au(0) + } + if base_flow.flags.contains(FlowFlags::CLEARS_RIGHT) { + self.right = Au(0) + } + } + + /// Given the speculated inline size of the floats out for this flow's last child, computes the + /// speculated inline size of the floats out for this flow. + pub fn compute_floats_out(&mut self, flow: &mut dyn Flow) { + if flow.is_block_like() { + let block_flow = flow.as_block(); + if block_flow.formatting_context_type() != FormattingContextType::None { + *self = block_flow.base.speculated_float_placement_in; + } else { + if self.left > Au(0) || self.right > Au(0) { + let speculated_inline_content_edge_offsets = + block_flow.fragment.guess_inline_content_edge_offsets(); + if self.left > Au(0) && speculated_inline_content_edge_offsets.start > Au(0) { + self.left = self.left + speculated_inline_content_edge_offsets.start + } + if self.right > Au(0) && speculated_inline_content_edge_offsets.end > Au(0) { + self.right = self.right + speculated_inline_content_edge_offsets.end + } + } + + self.left = max( + self.left, + block_flow.base.speculated_float_placement_in.left, + ); + self.right = max( + self.right, + block_flow.base.speculated_float_placement_in.right, + ); + } + } + + let base_flow = flow.base(); + if !base_flow.flags.is_float() { + return; + } + + let mut float_inline_size = base_flow.intrinsic_inline_sizes.preferred_inline_size; + if float_inline_size == Au(0) { + if flow.is_block_like() { + // Hack: If the size of the float is not fixed, then there's no + // way we can guess at its size now. So just pick an arbitrary + // nonzero value (in this case, 1px) so that the layout + // traversal logic will know that objects later in the document + // might flow around this float. + let inline_size = flow.as_block().fragment.style.content_inline_size(); + let fixed = match inline_size { + Size::Auto => false, + Size::LengthPercentage(ref lp) => { + lp.0.is_definitely_zero() || lp.0.maybe_to_used_value(None).is_some() + }, + }; + if !fixed { + float_inline_size = Au::from_px(1) + } + } + } + + match base_flow.flags.float_kind() { + StyleFloat::None => {}, + StyleFloat::Left => self.left = self.left + float_inline_size, + StyleFloat::Right => self.right = self.right + float_inline_size, + } + } + + /// Given a flow, computes the speculated inline size of the floats in of its first child. + pub fn compute_floats_in_for_first_child( + parent_flow: &mut dyn Flow, + ) -> SpeculatedFloatPlacement { + if !parent_flow.is_block_like() { + return parent_flow.base().speculated_float_placement_in; + } + + let parent_block_flow = parent_flow.as_block(); + if parent_block_flow.formatting_context_type() != FormattingContextType::None { + return SpeculatedFloatPlacement::zero(); + } + + let mut placement = parent_block_flow.base.speculated_float_placement_in; + let speculated_inline_content_edge_offsets = parent_block_flow + .fragment + .guess_inline_content_edge_offsets(); + + if speculated_inline_content_edge_offsets.start > Au(0) { + placement.left = if placement.left > speculated_inline_content_edge_offsets.start { + placement.left - speculated_inline_content_edge_offsets.start + } else { + Au(0) + } + } + if speculated_inline_content_edge_offsets.end > Au(0) { + placement.right = if placement.right > speculated_inline_content_edge_offsets.end { + placement.right - speculated_inline_content_edge_offsets.end + } else { + Au(0) + } + } + + placement + } +} diff --git a/components/layout_2020/flow.rs b/components/layout_2020/flow.rs new file mode 100644 index 00000000000..d7ab73cd8d3 --- /dev/null +++ b/components/layout_2020/flow.rs @@ -0,0 +1,1458 @@ +/* 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/. */ + +//! Servo's experimental layout system builds a tree of `Flow` and `Fragment` objects and solves +//! layout constraints to obtain positions and display attributes of tree nodes. Positions are +//! computed in several tree traversals driven by the fundamental data dependencies required by +//! inline and block layout. +//! +//! Flows are interior nodes in the layout tree and correspond closely to *flow contexts* in the +//! CSS specification. Flows are responsible for positioning their child flow contexts and +//! fragments. Flows have purpose-specific fields, such as auxiliary line structs, out-of-flow +//! child lists, and so on. +//! +//! Currently, the important types of flows are: +//! +//! * `BlockFlow`: A flow that establishes a block context. It has several child flows, each of +//! which are positioned according to block formatting context rules (CSS block boxes). Block +//! flows also contain a single box to represent their rendered borders, padding, etc. +//! The BlockFlow at the root of the tree has special behavior: it stretches to the boundaries of +//! the viewport. +//! +//! * `InlineFlow`: A flow that establishes an inline context. It has a flat list of child +//! fragments/flows that are subject to inline layout and line breaking and structs to represent +//! line breaks and mapping to CSS boxes, for the purpose of handling `getClientRects()` and +//! similar methods. + +use crate::block::{BlockFlow, FormattingContextType}; +use crate::context::LayoutContext; +use crate::display_list::items::ClippingAndScrolling; +use crate::display_list::{DisplayListBuildState, StackingContextCollectionState}; +use crate::flex::FlexFlow; +use crate::floats::{Floats, SpeculatedFloatPlacement}; +use crate::flow_list::{FlowList, FlowListIterator, MutFlowListIterator}; +use crate::flow_ref::{FlowRef, WeakFlowRef}; +use crate::fragment::{CoordinateSystem, Fragment, FragmentBorderBoxIterator, Overflow}; +use crate::inline::InlineFlow; +use crate::model::{CollapsibleMargins, IntrinsicISizes}; +use crate::parallel::FlowParallelInfo; +use crate::table::TableFlow; +use crate::table_cell::TableCellFlow; +use crate::table_colgroup::TableColGroupFlow; +use crate::table_row::TableRowFlow; +use crate::table_rowgroup::TableRowGroupFlow; +use crate::table_wrapper::TableWrapperFlow; +use app_units::Au; +use euclid::{Point2D, Rect, Size2D, Vector2D}; +use gfx_traits::print_tree::PrintTree; +use gfx_traits::StackingContextId; +use num_traits::cast::FromPrimitive; +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use servo_geometry::{au_rect_to_f32_rect, f32_rect_to_au_rect, MaxRect}; +use std::fmt; +use std::slice::IterMut; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use style::computed_values::clear::T as Clear; +use style::computed_values::float::T as Float; +use style::computed_values::overflow_x::T as StyleOverflow; +use style::computed_values::position::T as Position; +use style::computed_values::text_align::T as TextAlign; +use style::context::SharedStyleContext; +use style::logical_geometry::{LogicalRect, LogicalSize, WritingMode}; +use style::properties::ComputedValues; +use style::selector_parser::RestyleDamage; +use style::servo::restyle_damage::ServoRestyleDamage; +use style::values::computed::LengthPercentageOrAuto; +use webrender_api::units::LayoutTransform; + +/// This marker trait indicates that a type is a struct with `#[repr(C)]` whose first field +/// is of type `BaseFlow` or some type that also implements this trait. +/// +/// In other words, the memory representation of `BaseFlow` must be a prefix +/// of the memory representation of types implementing `HasBaseFlow`. +#[allow(unsafe_code)] +pub unsafe trait HasBaseFlow {} + +/// Methods to get the `BaseFlow` from any `HasBaseFlow` type. +pub trait GetBaseFlow { + fn base(&self) -> &BaseFlow; + fn mut_base(&mut self) -> &mut BaseFlow; +} + +impl<T: HasBaseFlow + ?Sized> GetBaseFlow for T { + #[inline(always)] + #[allow(unsafe_code)] + fn base(&self) -> &BaseFlow { + let ptr: *const Self = self; + let ptr = ptr as *const BaseFlow; + unsafe { &*ptr } + } + + #[inline(always)] + #[allow(unsafe_code)] + fn mut_base(&mut self) -> &mut BaseFlow { + let ptr: *mut Self = self; + let ptr = ptr as *mut BaseFlow; + unsafe { &mut *ptr } + } +} + +/// Virtual methods that make up a float context. +/// +/// Note that virtual methods have a cost; we should not overuse them in Servo. Consider adding +/// methods to `ImmutableFlowUtils` or `MutableFlowUtils` before adding more methods here. +pub trait Flow: HasBaseFlow + fmt::Debug + Sync + Send + 'static { + // RTTI + // + // TODO(pcwalton): Use Rust's RTTI, once that works. + + /// Returns the class of flow that this is. + fn class(&self) -> FlowClass; + + /// If this is a block flow, returns the underlying object. Fails otherwise. + fn as_block(&self) -> &BlockFlow { + panic!("called as_block() on a non-block flow") + } + + /// If this is a block flow, returns the underlying object, borrowed mutably. Fails otherwise. + fn as_mut_block(&mut self) -> &mut BlockFlow { + debug!("called as_mut_block() on a flow of type {:?}", self.class()); + panic!("called as_mut_block() on a non-block flow") + } + + /// If this is a flex flow, returns the underlying object. Fails otherwise. + fn as_flex(&self) -> &FlexFlow { + panic!("called as_flex() on a non-flex flow") + } + + /// If this is an inline flow, returns the underlying object. Fails otherwise. + fn as_inline(&self) -> &InlineFlow { + panic!("called as_inline() on a non-inline flow") + } + + /// If this is an inline flow, returns the underlying object, borrowed mutably. Fails + /// otherwise. + fn as_mut_inline(&mut self) -> &mut InlineFlow { + panic!("called as_mut_inline() on a non-inline flow") + } + + /// If this is a table wrapper flow, returns the underlying object. Fails otherwise. + fn as_table_wrapper(&self) -> &TableWrapperFlow { + panic!("called as_table_wrapper() on a non-tablewrapper flow") + } + + /// If this is a table flow, returns the underlying object, borrowed mutably. Fails otherwise. + fn as_mut_table(&mut self) -> &mut TableFlow { + panic!("called as_mut_table() on a non-table flow") + } + + /// If this is a table flow, returns the underlying object. Fails otherwise. + fn as_table(&self) -> &TableFlow { + panic!("called as_table() on a non-table flow") + } + + /// If this is a table colgroup flow, returns the underlying object, borrowed mutably. Fails + /// otherwise. + fn as_mut_table_colgroup(&mut self) -> &mut TableColGroupFlow { + panic!("called as_mut_table_colgroup() on a non-tablecolgroup flow") + } + + /// If this is a table colgroup flow, returns the underlying object. Fails + /// otherwise. + fn as_table_colgroup(&self) -> &TableColGroupFlow { + panic!("called as_table_colgroup() on a non-tablecolgroup flow") + } + + /// If this is a table rowgroup flow, returns the underlying object, borrowed mutably. Fails + /// otherwise. + fn as_mut_table_rowgroup(&mut self) -> &mut TableRowGroupFlow { + panic!("called as_mut_table_rowgroup() on a non-tablerowgroup flow") + } + + /// If this is a table rowgroup flow, returns the underlying object. Fails otherwise. + fn as_table_rowgroup(&self) -> &TableRowGroupFlow { + panic!("called as_table_rowgroup() on a non-tablerowgroup flow") + } + + /// If this is a table row flow, returns the underlying object, borrowed mutably. Fails + /// otherwise. + fn as_mut_table_row(&mut self) -> &mut TableRowFlow { + panic!("called as_mut_table_row() on a non-tablerow flow") + } + + /// If this is a table row flow, returns the underlying object. Fails otherwise. + fn as_table_row(&self) -> &TableRowFlow { + panic!("called as_table_row() on a non-tablerow flow") + } + + /// If this is a table cell flow, returns the underlying object, borrowed mutably. Fails + /// otherwise. + fn as_mut_table_cell(&mut self) -> &mut TableCellFlow { + panic!("called as_mut_table_cell() on a non-tablecell flow") + } + + /// If this is a table cell flow, returns the underlying object. Fails otherwise. + fn as_table_cell(&self) -> &TableCellFlow { + panic!("called as_table_cell() on a non-tablecell flow") + } + + // Main methods + + /// Pass 1 of reflow: computes minimum and preferred inline-sizes. + /// + /// Recursively (bottom-up) determine the flow's minimum and preferred inline-sizes. When + /// called on this flow, all child flows have had their minimum and preferred inline-sizes set. + /// This function must decide minimum/preferred inline-sizes based on its children's inline- + /// sizes and the dimensions of any boxes it is responsible for flowing. + fn bubble_inline_sizes(&mut self) { + panic!("bubble_inline_sizes not yet implemented") + } + + /// Pass 2 of reflow: computes inline-size. + fn assign_inline_sizes(&mut self, _ctx: &LayoutContext) { + panic!("assign_inline_sizes not yet implemented") + } + + /// Pass 3a of reflow: computes block-size. + fn assign_block_size(&mut self, _ctx: &LayoutContext) { + panic!("assign_block_size not yet implemented") + } + + /// Like `assign_block_size`, but is recurses explicitly into descendants. + /// Fit as much content as possible within `available_block_size`. + /// If that’s not all of it, truncate the contents of `self` + /// and return a new flow similar to `self` with the rest of the content. + /// + /// The default is to make a flow "atomic": it can not be fragmented. + fn fragment( + &mut self, + layout_context: &LayoutContext, + _fragmentation_context: Option<FragmentationContext>, + ) -> Option<Arc<dyn Flow>> { + fn recursive_assign_block_size<F: ?Sized + Flow + GetBaseFlow>( + flow: &mut F, + ctx: &LayoutContext, + ) { + for child in flow.mut_base().child_iter_mut() { + recursive_assign_block_size(child, ctx) + } + flow.assign_block_size(ctx); + } + recursive_assign_block_size(self, layout_context); + None + } + + fn collect_stacking_contexts(&mut self, state: &mut StackingContextCollectionState); + + /// If this is a float, places it. The default implementation does nothing. + fn place_float_if_applicable<'a>(&mut self) {} + + /// Assigns block-sizes in-order; or, if this is a float, places the float. The default + /// implementation simply assigns block-sizes if this flow might have floats in. Returns true + /// if it was determined that this child might have had floats in or false otherwise. + /// + /// `parent_thread_id` is the thread ID of the parent. This is used for the layout tinting + /// debug mode; if the block size of this flow was determined by its parent, we should treat + /// it as laid out by its parent. + fn assign_block_size_for_inorder_child_if_necessary( + &mut self, + layout_context: &LayoutContext, + parent_thread_id: u8, + _content_box: LogicalRect<Au>, + ) -> bool { + let might_have_floats_in_or_out = + self.base().might_have_floats_in() || self.base().might_have_floats_out(); + if might_have_floats_in_or_out { + self.mut_base().thread_id = parent_thread_id; + self.assign_block_size(layout_context); + self.mut_base() + .restyle_damage + .remove(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + } + might_have_floats_in_or_out + } + + fn get_overflow_in_parent_coordinates(&self) -> Overflow { + // FIXME(#2795): Get the real container size. + let container_size = Size2D::zero(); + let position = self + .base() + .position + .to_physical(self.base().writing_mode, container_size); + + let mut overflow = self.base().overflow; + + match self.class() { + FlowClass::Block | FlowClass::TableCaption | FlowClass::TableCell => {}, + _ => { + overflow.translate(&position.origin.to_vector()); + return overflow; + }, + } + + let border_box = self.as_block().fragment.stacking_relative_border_box( + &self.base().stacking_relative_position, + &self + .base() + .early_absolute_position_info + .relative_containing_block_size, + self.base() + .early_absolute_position_info + .relative_containing_block_mode, + CoordinateSystem::Own, + ); + if StyleOverflow::Visible != self.as_block().fragment.style.get_box().overflow_x { + overflow.paint.origin.x = Au(0); + overflow.paint.size.width = border_box.size.width; + overflow.scroll.origin.x = Au(0); + overflow.scroll.size.width = border_box.size.width; + } + if StyleOverflow::Visible != self.as_block().fragment.style.get_box().overflow_y { + overflow.paint.origin.y = Au(0); + overflow.paint.size.height = border_box.size.height; + overflow.scroll.origin.y = Au(0); + overflow.scroll.size.height = border_box.size.height; + } + + if !self.as_block().fragment.establishes_stacking_context() || + self.as_block() + .fragment + .style + .get_box() + .transform + .0 + .is_empty() + { + overflow.translate(&position.origin.to_vector()); + return overflow; + } + + // TODO: Take into account 3d transforms, even though it's a fairly + // uncommon case. + let transform_2d = self + .as_block() + .fragment + .transform_matrix(&position) + .unwrap_or(LayoutTransform::identity()) + .to_2d() + .to_untyped(); + let transformed_overflow = Overflow { + paint: f32_rect_to_au_rect( + transform_2d.transform_rect(&au_rect_to_f32_rect(overflow.paint)), + ), + scroll: f32_rect_to_au_rect( + transform_2d.transform_rect(&au_rect_to_f32_rect(overflow.scroll)), + ), + }; + + // TODO: We are taking the union of the overflow and transformed overflow here, which + // happened implicitly in the previous version of this code. This will probably be + // unnecessary once we are taking into account 3D transformations above. + overflow.union(&transformed_overflow); + + overflow.translate(&position.origin.to_vector()); + overflow + } + + /// + /// CSS Section 11.1 + /// This is the union of rectangles of the flows for which we define the + /// Containing Block. + /// + /// FIXME(pcwalton): This should not be a virtual method, but currently is due to a compiler + /// bug ("the trait `Sized` is not implemented for `self`"). + /// + /// Assumption: This is called in a bottom-up traversal, so kids' overflows have + /// already been set. + /// Assumption: Absolute descendants have had their overflow calculated. + fn store_overflow(&mut self, _: &LayoutContext) { + // Calculate overflow on a per-fragment basis. + let mut overflow = self.compute_overflow(); + match self.class() { + FlowClass::Block | FlowClass::TableCaption | FlowClass::TableCell => { + for kid in self.mut_base().children.iter_mut() { + overflow.union(&kid.get_overflow_in_parent_coordinates()); + } + }, + _ => {}, + } + self.mut_base().overflow = overflow + } + + /// Phase 4 of reflow: Compute the stacking-relative position (origin of the content box, + /// in coordinates relative to the nearest ancestor stacking context). + fn compute_stacking_relative_position(&mut self, _: &LayoutContext) { + // The default implementation is a no-op. + } + + /// Phase 5 of reflow: builds display lists. + fn build_display_list(&mut self, state: &mut DisplayListBuildState); + + /// Returns the union of all overflow rects of all of this flow's fragments. + fn compute_overflow(&self) -> Overflow; + + /// Iterates through border boxes of all of this flow's fragments. + /// Level provides a zero based index indicating the current + /// depth of the flow tree during fragment iteration. + fn iterate_through_fragment_border_boxes( + &self, + iterator: &mut dyn FragmentBorderBoxIterator, + level: i32, + stacking_context_position: &Point2D<Au>, + ); + + /// Mutably iterates through fragments in this flow. + fn mutate_fragments(&mut self, mutator: &mut dyn FnMut(&mut Fragment)); + + /// Marks this flow as the root flow. The default implementation is a no-op. + fn mark_as_root(&mut self) { + debug!("called mark_as_root() on a flow of type {:?}", self.class()); + panic!("called mark_as_root() on an unhandled flow"); + } + + // Note that the following functions are mostly called using static method + // dispatch, so it's ok to have them in this trait. Plus, they have + // different behaviour for different types of Flow, so they can't go into + // the Immutable / Mutable Flow Utils traits without additional casts. + + fn is_root(&self) -> bool { + false + } + + /// The 'position' property of this flow. + fn positioning(&self) -> Position { + Position::Static + } + + /// Return true if this flow has position 'fixed'. + fn is_fixed(&self) -> bool { + self.positioning() == Position::Fixed + } + + fn contains_positioned_fragments(&self) -> bool { + self.contains_relatively_positioned_fragments() || + self.base() + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + } + + fn contains_relatively_positioned_fragments(&self) -> bool { + self.positioning() == Position::Relative + } + + /// Returns true if this is an absolute containing block. + fn is_absolute_containing_block(&self) -> bool { + self.contains_positioned_fragments() + } + + /// Returns true if this flow contains fragments that are roots of an absolute flow tree. + fn contains_roots_of_absolute_flow_tree(&self) -> bool { + self.contains_relatively_positioned_fragments() || self.is_root() + } + + /// Updates the inline position of a child flow during the assign-height traversal. At present, + /// this is only used for absolutely-positioned inline-blocks. + fn update_late_computed_inline_position_if_necessary(&mut self, inline_position: Au); + + /// Updates the block position of a child flow during the assign-height traversal. At present, + /// this is only used for absolutely-positioned inline-blocks. + fn update_late_computed_block_position_if_necessary(&mut self, block_position: Au); + + /// Return the size of the containing block generated by this flow for the absolutely- + /// positioned descendant referenced by `for_flow`. For block flows, this is the padding box. + /// + /// NB: Do not change this `&self` to `&mut self` under any circumstances! It has security + /// implications because this can be called on parents concurrently from descendants! + fn generated_containing_block_size(&self, _: OpaqueFlow) -> LogicalSize<Au>; + + /// Attempts to perform incremental fixup of this flow by replacing its fragment's style with + /// the new style. This can only succeed if the flow has exactly one fragment. + fn repair_style(&mut self, new_style: &crate::ServoArc<ComputedValues>); + + /// Print any extra children (such as fragments) contained in this Flow + /// for debugging purposes. Any items inserted into the tree will become + /// children of this flow. + fn print_extra_flow_children(&self, _: &mut PrintTree) {} + + fn clipping_and_scrolling(&self) -> ClippingAndScrolling { + match self.base().clipping_and_scrolling { + Some(info) => info, + None => unreachable!("Tried to access scroll root id on Flow before assignment"), + } + } +} + +pub trait ImmutableFlowUtils { + // Convenience functions + + /// Returns true if this flow is a block flow or subclass thereof. + fn is_block_like(self) -> bool; + + /// Returns true if this flow is a table flow. + fn is_table(self) -> bool; + + /// Returns true if this flow is a table caption flow. + fn is_table_caption(self) -> bool; + + /// Returns true if this flow is a table row flow. + fn is_table_row(self) -> bool; + + /// Returns true if this flow is a table cell flow. + fn is_table_cell(self) -> bool; + + /// Returns true if this flow is a table colgroup flow. + fn is_table_colgroup(self) -> bool; + + /// Returns true if this flow is a table rowgroup flow. + fn is_table_rowgroup(self) -> bool; + + /// Returns the number of children that this flow possesses. + fn child_count(self) -> usize; + + /// Returns true if this flow is a block flow. + fn is_block_flow(self) -> bool; + + /// Returns true if this flow is an inline flow. + fn is_inline_flow(self) -> bool; + + /// Dumps the flow tree for debugging. + fn print(self, title: String); + + /// Dumps the flow tree for debugging into the given PrintTree. + fn print_with_tree(self, print_tree: &mut PrintTree); + + /// Returns true if floats might flow through this flow, as determined by the float placement + /// speculation pass. + fn floats_might_flow_through(self) -> bool; + + fn baseline_offset_of_last_line_box_in_flow(self) -> Option<Au>; +} + +pub trait MutableFlowUtils { + /// Calls `repair_style` and `bubble_inline_sizes`. You should use this method instead of + /// calling them individually, since there is no reason not to perform both operations. + fn repair_style_and_bubble_inline_sizes(self, style: &crate::ServoArc<ComputedValues>); +} + +pub trait MutableOwnedFlowUtils { + /// Set absolute descendants for this flow. + /// + /// Set this flow as the Containing Block for all the absolute descendants. + fn set_absolute_descendants(&mut self, abs_descendants: AbsoluteDescendants); + + /// Sets the flow as the containing block for all absolute descendants that have been marked + /// as having reached their containing block. This is needed in order to handle cases like: + /// + /// ```html + /// <div> + /// <span style="position: relative"> + /// <span style="position: absolute; ..."></span> + /// </span> + /// </div> + /// ``` + fn take_applicable_absolute_descendants( + &mut self, + absolute_descendants: &mut AbsoluteDescendants, + ); +} + +#[derive(Clone, Copy, Debug, PartialEq, Serialize)] +pub enum FlowClass { + Block, + Inline, + ListItem, + TableWrapper, + Table, + TableColGroup, + TableRowGroup, + TableRow, + TableCaption, + TableCell, + Multicol, + MulticolColumn, + Flex, +} + +impl FlowClass { + fn is_block_like(self) -> bool { + match self { + FlowClass::Block | + FlowClass::ListItem | + FlowClass::Table | + FlowClass::TableRowGroup | + FlowClass::TableRow | + FlowClass::TableCaption | + FlowClass::TableCell | + FlowClass::TableWrapper | + FlowClass::Flex => true, + _ => false, + } + } +} + +bitflags! { + #[doc = "Flags used in flows."] + pub struct FlowFlags: u32 { + // text align flags + #[doc = "Whether this flow is absolutely positioned. This is checked all over layout, so a"] + #[doc = "virtual call is too expensive."] + const IS_ABSOLUTELY_POSITIONED = 0b0000_0000_0000_0000_0100_0000; + #[doc = "Whether this flow clears to the left. This is checked all over layout, so a"] + #[doc = "virtual call is too expensive."] + const CLEARS_LEFT = 0b0000_0000_0000_0000_1000_0000; + #[doc = "Whether this flow clears to the right. This is checked all over layout, so a"] + #[doc = "virtual call is too expensive."] + const CLEARS_RIGHT = 0b0000_0000_0000_0001_0000_0000; + #[doc = "Whether this flow is left-floated. This is checked all over layout, so a"] + #[doc = "virtual call is too expensive."] + const FLOATS_LEFT = 0b0000_0000_0000_0010_0000_0000; + #[doc = "Whether this flow is right-floated. This is checked all over layout, so a"] + #[doc = "virtual call is too expensive."] + const FLOATS_RIGHT = 0b0000_0000_0000_0100_0000_0000; + #[doc = "Text alignment. \ + + NB: If you update this, update `TEXT_ALIGN_SHIFT` below."] + const TEXT_ALIGN = 0b0000_0000_0111_1000_0000_0000; + #[doc = "Whether this flow has a fragment with `counter-reset` or `counter-increment` \ + styles."] + const AFFECTS_COUNTERS = 0b0000_0000_1000_0000_0000_0000; + #[doc = "Whether this flow's descendants have fragments that affect `counter-reset` or \ + `counter-increment` styles."] + const HAS_COUNTER_AFFECTING_CHILDREN = 0b0000_0001_0000_0000_0000_0000; + #[doc = "Whether this flow behaves as though it had `position: static` for the purposes \ + of positioning in the inline direction. This is set for flows with `position: \ + static` and `position: relative` as well as absolutely-positioned flows with \ + unconstrained positions in the inline direction."] + const INLINE_POSITION_IS_STATIC = 0b0000_0010_0000_0000_0000_0000; + #[doc = "Whether this flow behaves as though it had `position: static` for the purposes \ + of positioning in the block direction. This is set for flows with `position: \ + static` and `position: relative` as well as absolutely-positioned flows with \ + unconstrained positions in the block direction."] + const BLOCK_POSITION_IS_STATIC = 0b0000_0100_0000_0000_0000_0000; + + /// Whether any ancestor is a fragmentation container + const CAN_BE_FRAGMENTED = 0b0000_1000_0000_0000_0000_0000; + + /// Whether this flow contains any text and/or replaced fragments. + const CONTAINS_TEXT_OR_REPLACED_FRAGMENTS = 0b0001_0000_0000_0000_0000_0000; + + /// Whether margins are prohibited from collapsing with this flow. + const MARGINS_CANNOT_COLLAPSE = 0b0010_0000_0000_0000_0000_0000; + } +} + +/// The number of bits we must shift off to handle the text alignment field. +/// +/// NB: If you update this, update `TEXT_ALIGN` above. +static TEXT_ALIGN_SHIFT: usize = 11; + +impl FlowFlags { + #[inline] + pub fn text_align(self) -> TextAlign { + TextAlign::from_u32((self & FlowFlags::TEXT_ALIGN).bits() >> TEXT_ALIGN_SHIFT).unwrap() + } + + #[inline] + pub fn set_text_align(&mut self, value: TextAlign) { + *self = (*self & !FlowFlags::TEXT_ALIGN) | + FlowFlags::from_bits((value as u32) << TEXT_ALIGN_SHIFT).unwrap(); + } + + #[inline] + pub fn float_kind(&self) -> Float { + if self.contains(FlowFlags::FLOATS_LEFT) { + Float::Left + } else if self.contains(FlowFlags::FLOATS_RIGHT) { + Float::Right + } else { + Float::None + } + } + + #[inline] + pub fn is_float(&self) -> bool { + self.contains(FlowFlags::FLOATS_LEFT) || self.contains(FlowFlags::FLOATS_RIGHT) + } + + #[inline] + pub fn clears_floats(&self) -> bool { + self.contains(FlowFlags::CLEARS_LEFT) || self.contains(FlowFlags::CLEARS_RIGHT) + } +} + +/// Absolutely-positioned descendants of this flow. +#[derive(Clone)] +pub struct AbsoluteDescendants { + /// Links to every descendant. This must be private because it is unsafe to leak `FlowRef`s to + /// layout. + descendant_links: Vec<AbsoluteDescendantInfo>, +} + +impl AbsoluteDescendants { + pub fn new() -> AbsoluteDescendants { + AbsoluteDescendants { + descendant_links: Vec::new(), + } + } + + pub fn len(&self) -> usize { + self.descendant_links.len() + } + + pub fn is_empty(&self) -> bool { + self.descendant_links.is_empty() + } + + pub fn push(&mut self, given_descendant: FlowRef) { + self.descendant_links.push(AbsoluteDescendantInfo { + flow: given_descendant, + has_reached_containing_block: false, + }); + } + + /// Push the given descendants on to the existing descendants. + /// + /// Ignore any static y offsets, because they are None before layout. + pub fn push_descendants(&mut self, given_descendants: AbsoluteDescendants) { + for elem in given_descendants.descendant_links { + self.descendant_links.push(elem); + } + } + + /// Return an iterator over the descendant flows. + pub fn iter(&mut self) -> AbsoluteDescendantIter { + AbsoluteDescendantIter { + iter: self.descendant_links.iter_mut(), + } + } + + /// Mark these descendants as having reached their containing block. + pub fn mark_as_having_reached_containing_block(&mut self) { + for descendant_info in self.descendant_links.iter_mut() { + descendant_info.has_reached_containing_block = true + } + } +} + +/// Information about each absolutely-positioned descendant of the given flow. +#[derive(Clone)] +pub struct AbsoluteDescendantInfo { + /// The absolute descendant flow in question. + flow: FlowRef, + + /// Whether the absolute descendant has reached its containing block. This exists so that we + /// can handle cases like the following: + /// + /// ```html + /// <div> + /// <span id=a style="position: absolute; ...">foo</span> + /// <span style="position: relative"> + /// <span id=b style="position: absolute; ...">bar</span> + /// </span> + /// </div> + /// ``` + /// + /// When we go to create the `InlineFlow` for the outer `div`, our absolute descendants will + /// be `a` and `b`. At this point, we need a way to distinguish between the two, because the + /// containing block for `a` will be different from the containing block for `b`. Specifically, + /// the latter's containing block is the inline flow itself, while the former's containing + /// block is going to be some parent of the outer `div`. Hence we need this flag as a way to + /// distinguish the two; it will be false for `a` and true for `b`. + has_reached_containing_block: bool, +} + +pub struct AbsoluteDescendantIter<'a> { + iter: IterMut<'a, AbsoluteDescendantInfo>, +} + +impl<'a> Iterator for AbsoluteDescendantIter<'a> { + type Item = &'a mut dyn Flow; + fn next(&mut self) -> Option<&'a mut dyn Flow> { + self.iter + .next() + .map(|info| FlowRef::deref_mut(&mut info.flow)) + } + + fn size_hint(&self) -> (usize, Option<usize>) { + self.iter.size_hint() + } +} + +/// Information needed to compute absolute (i.e. viewport-relative) flow positions (not to be +/// confused with absolutely-positioned flows) that is computed during block-size assignment. +#[derive(Clone, Copy)] +pub struct EarlyAbsolutePositionInfo { + /// The size of the containing block for relatively-positioned descendants. + pub relative_containing_block_size: LogicalSize<Au>, + + /// The writing mode for `relative_containing_block_size`. + pub relative_containing_block_mode: WritingMode, +} + +impl EarlyAbsolutePositionInfo { + pub fn new(writing_mode: WritingMode) -> EarlyAbsolutePositionInfo { + // FIXME(pcwalton): The initial relative containing block-size should be equal to the size + // of the root layer. + EarlyAbsolutePositionInfo { + relative_containing_block_size: LogicalSize::zero(writing_mode), + relative_containing_block_mode: writing_mode, + } + } +} + +/// Information needed to compute absolute (i.e. viewport-relative) flow positions (not to be +/// confused with absolutely-positioned flows) that is computed during final position assignment. +#[derive(Clone, Copy, Serialize)] +pub struct LateAbsolutePositionInfo { + /// The position of the absolute containing block relative to the nearest ancestor stacking + /// context. If the absolute containing block establishes the stacking context for this flow, + /// and this flow is not itself absolutely-positioned, then this is (0, 0). + pub stacking_relative_position_of_absolute_containing_block: Point2D<Au>, +} + +impl LateAbsolutePositionInfo { + pub fn new() -> LateAbsolutePositionInfo { + LateAbsolutePositionInfo { + stacking_relative_position_of_absolute_containing_block: Point2D::zero(), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct FragmentationContext { + pub available_block_size: Au, + pub this_fragment_is_empty: bool, +} + +/// Data common to all flows. +pub struct BaseFlow { + pub restyle_damage: RestyleDamage, + + /// The children of this flow. + pub children: FlowList, + + /// Intrinsic inline sizes for this flow. + pub intrinsic_inline_sizes: IntrinsicISizes, + + /// The upper left corner of the box representing this flow, relative to the box representing + /// its parent flow. + /// + /// For absolute flows, this represents the position with respect to its *containing block*. + /// + /// This does not include margins in the block flow direction, because those can collapse. So + /// for the block direction (usually vertical), this represents the *border box*. For the + /// inline direction (usually horizontal), this represents the *margin box*. + pub position: LogicalRect<Au>, + + /// The amount of overflow of this flow, relative to the containing block. Must include all the + /// pixels of all the display list items for correct invalidation. + pub overflow: Overflow, + + /// Data used during parallel traversals. + /// + /// TODO(pcwalton): Group with other transient data to save space. + pub parallel: FlowParallelInfo, + + /// The floats next to this flow. + pub floats: Floats, + + /// Metrics for floats in computed during the float metrics speculation phase. + pub speculated_float_placement_in: SpeculatedFloatPlacement, + + /// Metrics for floats out computed during the float metrics speculation phase. + pub speculated_float_placement_out: SpeculatedFloatPlacement, + + /// The collapsible margins for this flow, if any. + pub collapsible_margins: CollapsibleMargins, + + /// The position of this flow relative to the start of the nearest ancestor stacking context. + /// This is computed during the top-down pass of display list construction. + pub stacking_relative_position: Vector2D<Au>, + + /// Details about descendants with position 'absolute' or 'fixed' for which we are the + /// containing block. This is in tree order. This includes any direct children. + pub abs_descendants: AbsoluteDescendants, + + /// The inline-size of the block container of this flow. Used for computing percentage and + /// automatic values for `width`. + pub block_container_inline_size: Au, + + /// The writing mode of the block container of this flow. + /// + /// FIXME (mbrubeck): Combine this and block_container_inline_size and maybe + /// block_container_explicit_block_size into a struct, to guarantee they are set at the same + /// time? Or just store a link to the containing block flow. + pub block_container_writing_mode: WritingMode, + + /// The block-size of the block container of this flow, if it is an explicit size (does not + /// depend on content heights). Used for computing percentage values for `height`. + pub block_container_explicit_block_size: Option<Au>, + + /// Reference to the Containing Block, if this flow is absolutely positioned. + pub absolute_cb: ContainingBlockLink, + + /// Information needed to compute absolute (i.e. viewport-relative) flow positions (not to be + /// confused with absolutely-positioned flows) that is computed during block-size assignment. + pub early_absolute_position_info: EarlyAbsolutePositionInfo, + + /// Information needed to compute absolute (i.e. viewport-relative) flow positions (not to be + /// confused with absolutely-positioned flows) that is computed during final position + /// assignment. + pub late_absolute_position_info: LateAbsolutePositionInfo, + + /// The clipping rectangle for this flow and its descendants, in the coordinate system of the + /// nearest ancestor stacking context. If this flow itself represents a stacking context, then + /// this is in the flow's own coordinate system. + pub clip: Rect<Au>, + + /// The writing mode for this flow. + pub writing_mode: WritingMode, + + /// For debugging and profiling, the identifier of the thread that laid out this fragment. + pub thread_id: u8, + + /// Various flags for flows, tightly packed to save space. + pub flags: FlowFlags, + + /// The ID of the StackingContext that contains this flow. This is initialized + /// to 0, but it assigned during the collect_stacking_contexts phase of display + /// list construction. + pub stacking_context_id: StackingContextId, + + /// The indices of this Flow's ClipScrollNode. This is used to place the node's + /// display items into scrolling frames and clipping nodes. + pub clipping_and_scrolling: Option<ClippingAndScrolling>, +} + +impl fmt::Debug for BaseFlow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let child_count = self.parallel.children_count.load(Ordering::SeqCst); + let child_count_string = if child_count > 0 { + format!("\nchildren={}", child_count) + } else { + "".to_owned() + }; + + let absolute_descendants_string = if self.abs_descendants.len() > 0 { + format!("\nabs-descendents={}", self.abs_descendants.len()) + } else { + "".to_owned() + }; + + let damage_string = if self.restyle_damage != RestyleDamage::empty() { + format!("\ndamage={:?}", self.restyle_damage) + } else { + "".to_owned() + }; + + write!( + f, + "\nsc={:?}\ + \npos={:?}{}{}\ + \nfloatspec-in={:?}\ + \nfloatspec-out={:?}\ + \noverflow={:?}{}{}{}", + self.stacking_context_id, + self.position, + if self.flags.contains(FlowFlags::FLOATS_LEFT) { + "FL" + } else { + "" + }, + if self.flags.contains(FlowFlags::FLOATS_RIGHT) { + "FR" + } else { + "" + }, + self.speculated_float_placement_in, + self.speculated_float_placement_out, + self.overflow, + child_count_string, + absolute_descendants_string, + damage_string + ) + } +} + +impl Serialize for BaseFlow { + fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + let mut serializer = serializer.serialize_struct("base", 5)?; + serializer.serialize_field("id", &self.debug_id())?; + serializer.serialize_field( + "stacking_relative_position", + &self.stacking_relative_position, + )?; + serializer.serialize_field("intrinsic_inline_sizes", &self.intrinsic_inline_sizes)?; + serializer.serialize_field("position", &self.position)?; + serializer.serialize_field("children", &self.children)?; + serializer.end() + } +} + +/// Whether a base flow should be forced to be nonfloated. This can affect e.g. `TableFlow`, which +/// is never floated because the table wrapper flow is the floated one. +#[derive(Clone, PartialEq)] +pub enum ForceNonfloatedFlag { + /// The flow should be floated if the node has a `float` property. + FloatIfNecessary, + /// The flow should be forced to be nonfloated. + ForceNonfloated, +} + +impl BaseFlow { + #[inline] + pub fn new( + style: Option<&ComputedValues>, + writing_mode: WritingMode, + force_nonfloated: ForceNonfloatedFlag, + ) -> BaseFlow { + let mut flags = FlowFlags::empty(); + match style { + Some(style) => { + if style.can_be_fragmented() { + flags.insert(FlowFlags::CAN_BE_FRAGMENTED); + } + + match style.get_box().position { + Position::Absolute | Position::Fixed => { + flags.insert(FlowFlags::IS_ABSOLUTELY_POSITIONED); + + let logical_position = style.logical_position(); + if logical_position.inline_start == LengthPercentageOrAuto::Auto && + logical_position.inline_end == LengthPercentageOrAuto::Auto + { + flags.insert(FlowFlags::INLINE_POSITION_IS_STATIC); + } + if logical_position.block_start == LengthPercentageOrAuto::Auto && + logical_position.block_end == LengthPercentageOrAuto::Auto + { + flags.insert(FlowFlags::BLOCK_POSITION_IS_STATIC); + } + }, + _ => flags.insert( + FlowFlags::BLOCK_POSITION_IS_STATIC | FlowFlags::INLINE_POSITION_IS_STATIC, + ), + } + + if force_nonfloated == ForceNonfloatedFlag::FloatIfNecessary { + match style.get_box().float { + Float::None => {}, + Float::Left => flags.insert(FlowFlags::FLOATS_LEFT), + Float::Right => flags.insert(FlowFlags::FLOATS_RIGHT), + } + } + + match style.get_box().clear { + Clear::None => {}, + Clear::Left => flags.insert(FlowFlags::CLEARS_LEFT), + Clear::Right => flags.insert(FlowFlags::CLEARS_RIGHT), + Clear::Both => { + flags.insert(FlowFlags::CLEARS_LEFT); + flags.insert(FlowFlags::CLEARS_RIGHT); + }, + } + + if !style.get_counters().counter_reset.is_empty() || + !style.get_counters().counter_increment.is_empty() + { + flags.insert(FlowFlags::AFFECTS_COUNTERS) + } + }, + None => flags + .insert(FlowFlags::BLOCK_POSITION_IS_STATIC | FlowFlags::INLINE_POSITION_IS_STATIC), + } + + // New flows start out as fully damaged. + let mut damage = RestyleDamage::rebuild_and_reflow(); + damage.remove(ServoRestyleDamage::RECONSTRUCT_FLOW); + + BaseFlow { + restyle_damage: damage, + children: FlowList::new(), + intrinsic_inline_sizes: IntrinsicISizes::new(), + position: LogicalRect::zero(writing_mode), + overflow: Overflow::new(), + parallel: FlowParallelInfo::new(), + floats: Floats::new(writing_mode), + collapsible_margins: CollapsibleMargins::new(), + stacking_relative_position: Vector2D::zero(), + abs_descendants: AbsoluteDescendants::new(), + speculated_float_placement_in: SpeculatedFloatPlacement::zero(), + speculated_float_placement_out: SpeculatedFloatPlacement::zero(), + block_container_inline_size: Au(0), + block_container_writing_mode: writing_mode, + block_container_explicit_block_size: None, + absolute_cb: ContainingBlockLink::new(), + early_absolute_position_info: EarlyAbsolutePositionInfo::new(writing_mode), + late_absolute_position_info: LateAbsolutePositionInfo::new(), + clip: MaxRect::max_rect(), + flags: flags, + writing_mode: writing_mode, + thread_id: 0, + stacking_context_id: StackingContextId::root(), + clipping_and_scrolling: None, + } + } + + /// Update the 'flags' field when computed styles have changed. + /// + /// These flags are initially set during flow construction. They only need to be updated here + /// if they are based on properties that can change without triggering `RECONSTRUCT_FLOW`. + pub fn update_flags_if_needed(&mut self, style: &ComputedValues) { + // For absolutely-positioned flows, changes to top/bottom/left/right can cause these flags + // to get out of date: + if self + .restyle_damage + .contains(ServoRestyleDamage::REFLOW_OUT_OF_FLOW) + { + // Note: We don't need to check whether IS_ABSOLUTELY_POSITIONED has changed, because + // changes to the 'position' property trigger flow reconstruction. + if self.flags.contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) { + let logical_position = style.logical_position(); + self.flags.set( + FlowFlags::INLINE_POSITION_IS_STATIC, + logical_position.inline_start == LengthPercentageOrAuto::Auto && + logical_position.inline_end == LengthPercentageOrAuto::Auto, + ); + self.flags.set( + FlowFlags::BLOCK_POSITION_IS_STATIC, + logical_position.block_start == LengthPercentageOrAuto::Auto && + logical_position.block_end == LengthPercentageOrAuto::Auto, + ); + } + } + } + + /// Return a new BaseFlow like this one but with the given children list + pub fn clone_with_children(&self, children: FlowList) -> BaseFlow { + BaseFlow { + children: children, + restyle_damage: self.restyle_damage | + ServoRestyleDamage::REPAINT | + ServoRestyleDamage::REFLOW_OUT_OF_FLOW | + ServoRestyleDamage::REFLOW, + parallel: FlowParallelInfo::new(), + floats: self.floats.clone(), + abs_descendants: self.abs_descendants.clone(), + absolute_cb: self.absolute_cb.clone(), + clip: self.clip.clone(), + + ..*self + } + } + + /// Iterates over the children of this immutable flow. + pub fn child_iter(&self) -> FlowListIterator { + self.children.iter() + } + + pub fn child_iter_mut(&mut self) -> MutFlowListIterator { + self.children.iter_mut() + } + + pub fn debug_id(&self) -> usize { + let p = self as *const _; + p as usize + } + + pub fn collect_stacking_contexts_for_children( + &mut self, + state: &mut StackingContextCollectionState, + ) { + for kid in self.children.iter_mut() { + kid.collect_stacking_contexts(state); + } + } + + #[inline] + pub fn might_have_floats_in(&self) -> bool { + self.speculated_float_placement_in.left > Au(0) || + self.speculated_float_placement_in.right > Au(0) + } + + #[inline] + pub fn might_have_floats_out(&self) -> bool { + self.speculated_float_placement_out.left > Au(0) || + self.speculated_float_placement_out.right > Au(0) + } + + /// Compute the fragment position relative to the parent stacking context. If the fragment + /// itself establishes a stacking context, then the origin of its position will be (0, 0) + /// for the purposes of this computation. + pub fn stacking_relative_border_box_for_display_list(&self, fragment: &Fragment) -> Rect<Au> { + fragment.stacking_relative_border_box( + &self.stacking_relative_position, + &self + .early_absolute_position_info + .relative_containing_block_size, + self.early_absolute_position_info + .relative_containing_block_mode, + CoordinateSystem::Own, + ) + } +} + +impl<'a> ImmutableFlowUtils for &'a dyn Flow { + /// Returns true if this flow is a block flow or subclass thereof. + fn is_block_like(self) -> bool { + self.class().is_block_like() + } + + /// Returns true if this flow is a table row flow. + fn is_table_row(self) -> bool { + match self.class() { + FlowClass::TableRow => true, + _ => false, + } + } + + /// Returns true if this flow is a table cell flow. + fn is_table_cell(self) -> bool { + match self.class() { + FlowClass::TableCell => true, + _ => false, + } + } + + /// Returns true if this flow is a table colgroup flow. + fn is_table_colgroup(self) -> bool { + match self.class() { + FlowClass::TableColGroup => true, + _ => false, + } + } + + /// Returns true if this flow is a table flow. + fn is_table(self) -> bool { + match self.class() { + FlowClass::Table => true, + _ => false, + } + } + + /// Returns true if this flow is a table caption flow. + fn is_table_caption(self) -> bool { + match self.class() { + FlowClass::TableCaption => true, + _ => false, + } + } + + /// Returns true if this flow is a table rowgroup flow. + fn is_table_rowgroup(self) -> bool { + match self.class() { + FlowClass::TableRowGroup => true, + _ => false, + } + } + + /// Returns the number of children that this flow possesses. + fn child_count(self) -> usize { + self.base().children.len() + } + + /// Returns true if this flow is a block flow. + fn is_block_flow(self) -> bool { + match self.class() { + FlowClass::Block => true, + _ => false, + } + } + + /// Returns true if this flow is an inline flow. + fn is_inline_flow(self) -> bool { + match self.class() { + FlowClass::Inline => true, + _ => false, + } + } + + /// Dumps the flow tree for debugging. + fn print(self, title: String) { + let mut print_tree = PrintTree::new(title); + self.print_with_tree(&mut print_tree); + } + + /// Dumps the flow tree for debugging into the given PrintTree. + fn print_with_tree(self, print_tree: &mut PrintTree) { + print_tree.new_level(format!("{:?}", self)); + self.print_extra_flow_children(print_tree); + for kid in self.base().child_iter() { + kid.print_with_tree(print_tree); + } + print_tree.end_level(); + } + + fn floats_might_flow_through(self) -> bool { + if !self.base().might_have_floats_in() && !self.base().might_have_floats_out() { + return false; + } + if self.is_root() { + return false; + } + if !self.is_block_like() { + return true; + } + self.as_block().formatting_context_type() == FormattingContextType::None + } + + fn baseline_offset_of_last_line_box_in_flow(self) -> Option<Au> { + for kid in self.base().children.iter().rev() { + if kid.is_inline_flow() { + if let Some(baseline_offset) = kid.as_inline().baseline_offset_of_last_line() { + return Some(kid.base().position.start.b + baseline_offset); + } + } + if kid.is_block_like() && + !kid.base() + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + if let Some(baseline_offset) = kid.baseline_offset_of_last_line_box_in_flow() { + return Some(kid.base().position.start.b + baseline_offset); + } + } + } + None + } +} + +impl<'a> MutableFlowUtils for &'a mut dyn Flow { + /// Calls `repair_style` and `bubble_inline_sizes`. You should use this method instead of + /// calling them individually, since there is no reason not to perform both operations. + fn repair_style_and_bubble_inline_sizes(self, style: &crate::ServoArc<ComputedValues>) { + self.repair_style(style); + self.mut_base().update_flags_if_needed(style); + self.bubble_inline_sizes(); + } +} + +impl MutableOwnedFlowUtils for FlowRef { + /// Set absolute descendants for this flow. + /// + /// Set yourself as the Containing Block for all the absolute descendants. + /// + /// This is called during flow construction, so nothing else can be accessing the descendant + /// flows. This is enforced by the fact that we have a mutable `FlowRef`, which only flow + /// construction is allowed to possess. + fn set_absolute_descendants(&mut self, abs_descendants: AbsoluteDescendants) { + let this = self.clone(); + let base = FlowRef::deref_mut(self).mut_base(); + base.abs_descendants = abs_descendants; + for descendant_link in base.abs_descendants.descendant_links.iter_mut() { + debug_assert!(!descendant_link.has_reached_containing_block); + let descendant_base = FlowRef::deref_mut(&mut descendant_link.flow).mut_base(); + descendant_base.absolute_cb.set(this.clone()); + } + } + + /// Sets the flow as the containing block for all absolute descendants that have been marked + /// as having reached their containing block. This is needed in order to handle cases like: + /// + /// ```html + /// <div> + /// <span style="position: relative"> + /// <span style="position: absolute; ..."></span> + /// </span> + /// </div> + /// ``` + fn take_applicable_absolute_descendants( + &mut self, + absolute_descendants: &mut AbsoluteDescendants, + ) { + let mut applicable_absolute_descendants = AbsoluteDescendants::new(); + for absolute_descendant in absolute_descendants.descendant_links.iter() { + if absolute_descendant.has_reached_containing_block { + applicable_absolute_descendants.push(absolute_descendant.flow.clone()); + } + } + absolute_descendants + .descendant_links + .retain(|descendant| !descendant.has_reached_containing_block); + + let this = self.clone(); + let base = FlowRef::deref_mut(self).mut_base(); + base.abs_descendants = applicable_absolute_descendants; + for descendant_link in base.abs_descendants.iter() { + let descendant_base = descendant_link.mut_base(); + descendant_base.absolute_cb.set(this.clone()); + } + } +} + +/// A link to a flow's containing block. +/// +/// This cannot safely be a `Flow` pointer because this is a pointer *up* the tree, not *down* the +/// tree. A pointer up the tree is unsafe during layout because it can be used to access a node +/// with an immutable reference while that same node is being laid out, causing possible iterator +/// invalidation and use-after-free. +/// +/// FIXME(pcwalton): I think this would be better with a borrow flag instead of `unsafe`. +#[derive(Clone)] +pub struct ContainingBlockLink { + /// The pointer up to the containing block. + link: Option<WeakFlowRef>, +} + +impl ContainingBlockLink { + fn new() -> ContainingBlockLink { + ContainingBlockLink { link: None } + } + + fn set(&mut self, link: FlowRef) { + self.link = Some(FlowRef::downgrade(&link)) + } + + #[inline] + pub fn generated_containing_block_size(&self, for_flow: OpaqueFlow) -> LogicalSize<Au> { + match self.link { + None => panic!( + "Link to containing block not established; perhaps you forgot to call \ + `set_absolute_descendants`?" + ), + Some(ref link) => { + let flow = link.upgrade().unwrap(); + flow.generated_containing_block_size(for_flow) + }, + } + } + + #[inline] + pub fn explicit_block_containing_size( + &self, + shared_context: &SharedStyleContext, + ) -> Option<Au> { + match self.link { + None => panic!( + "Link to containing block not established; perhaps you forgot to call \ + `set_absolute_descendants`?" + ), + Some(ref link) => { + let flow = link.upgrade().unwrap(); + if flow.is_block_like() { + flow.as_block() + .explicit_block_containing_size(shared_context) + } else if flow.is_inline_flow() { + Some(flow.as_inline().minimum_line_metrics.space_above_baseline) + } else { + None + } + }, + } + } +} + +/// A wrapper for the pointer address of a flow. These pointer addresses may only be compared for +/// equality with other such pointer addresses, never dereferenced. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct OpaqueFlow(pub usize); + +impl OpaqueFlow { + pub fn from_flow(flow: &dyn Flow) -> OpaqueFlow { + let object_ptr: *const dyn Flow = flow; + let data_ptr = object_ptr as *const (); + OpaqueFlow(data_ptr as usize) + } +} diff --git a/components/layout_2020/flow_list.rs b/components/layout_2020/flow_list.rs new file mode 100644 index 00000000000..a27db03920a --- /dev/null +++ b/components/layout_2020/flow_list.rs @@ -0,0 +1,197 @@ +/* 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/. */ + +use crate::flow::{Flow, FlowClass}; +use crate::flow_ref::FlowRef; +use serde::ser::{Serialize, SerializeSeq, Serializer}; +use serde_json::{to_value, Map, Value}; +use std::collections::{linked_list, LinkedList}; +use std::ops::Deref; +use std::sync::Arc; + +/// This needs to be reworked now that we have dynamically-sized types in Rust. +/// Until then, it's just a wrapper around LinkedList. +/// +/// SECURITY-NOTE(pcwalton): It is very important that `FlowRef` values not leak directly to +/// layout. Layout code must only interact with `&Flow` or `&mut Flow` values. Otherwise, layout +/// could stash `FlowRef` values in random places unknown to the system and thereby cause data +/// races. Those data races can lead to memory safety problems, potentially including arbitrary +/// remote code execution! In general, do not add new methods to this file (e.g. new ways of +/// iterating over flows) unless you are *very* sure of what you are doing. +pub struct FlowList { + flows: LinkedList<FlowRef>, +} + +impl Serialize for FlowList { + fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + let mut serializer = serializer.serialize_seq(Some(self.len()))?; + for f in self.iter() { + let mut flow_val = Map::new(); + flow_val.insert("class".to_owned(), to_value(f.class()).unwrap()); + let data = match f.class() { + FlowClass::Block => to_value(f.as_block()).unwrap(), + FlowClass::Inline => to_value(f.as_inline()).unwrap(), + FlowClass::Table => to_value(f.as_table()).unwrap(), + FlowClass::TableWrapper => to_value(f.as_table_wrapper()).unwrap(), + FlowClass::TableRowGroup => to_value(f.as_table_rowgroup()).unwrap(), + FlowClass::TableRow => to_value(f.as_table_row()).unwrap(), + FlowClass::TableCell => to_value(f.as_table_cell()).unwrap(), + FlowClass::Flex => to_value(f.as_flex()).unwrap(), + FlowClass::ListItem | + FlowClass::TableColGroup | + FlowClass::TableCaption | + FlowClass::Multicol | + FlowClass::MulticolColumn => { + Value::Null // Not implemented yet + }, + }; + flow_val.insert("data".to_owned(), data); + serializer.serialize_element(&flow_val)?; + } + serializer.end() + } +} + +pub struct MutFlowListIterator<'a> { + it: linked_list::IterMut<'a, FlowRef>, +} + +pub struct FlowListIterator<'a> { + it: linked_list::Iter<'a, FlowRef>, +} + +impl FlowList { + /// Add an element last in the list + /// + /// O(1) + pub fn push_back(&mut self, new_tail: FlowRef) { + self.flows.push_back(new_tail); + } + + pub fn push_back_arc(&mut self, new_head: Arc<dyn Flow>) { + self.flows.push_back(FlowRef::new(new_head)); + } + + pub fn push_front_arc(&mut self, new_head: Arc<dyn Flow>) { + self.flows.push_front(FlowRef::new(new_head)); + } + + pub fn pop_front_arc(&mut self) -> Option<Arc<dyn Flow>> { + self.flows.pop_front().map(FlowRef::into_arc) + } + + /// Create an empty list + #[inline] + pub fn new() -> FlowList { + FlowList { + flows: LinkedList::new(), + } + } + + /// Provide a forward iterator. + /// + /// SECURITY-NOTE(pcwalton): This does not hand out `FlowRef`s by design. Do not add a method + /// to do so! See the comment above in `FlowList`. + #[inline] + pub fn iter<'a>(&'a self) -> FlowListIterator { + FlowListIterator { + it: self.flows.iter(), + } + } + + /// Provide a forward iterator with mutable references + /// + /// SECURITY-NOTE(pcwalton): This does not hand out `FlowRef`s by design. Do not add a method + /// to do so! See the comment above in `FlowList`. + #[inline] + pub fn iter_mut(&mut self) -> MutFlowListIterator { + MutFlowListIterator { + it: self.flows.iter_mut(), + } + } + + /// Provides a caching random-access iterator that yields mutable references. This is + /// guaranteed to perform no more than O(n) pointer chases. + /// + /// SECURITY-NOTE(pcwalton): This does not hand out `FlowRef`s by design. Do not add a method + /// to do so! See the comment above in `FlowList`. + #[inline] + pub fn random_access_mut(&mut self) -> FlowListRandomAccessMut { + let length = self.flows.len(); + FlowListRandomAccessMut { + iterator: self.flows.iter_mut(), + cache: Vec::with_capacity(length), + } + } + + /// O(1) + #[inline] + pub fn len(&self) -> usize { + self.flows.len() + } + + #[inline] + pub fn split_off(&mut self, i: usize) -> Self { + FlowList { + flows: self.flows.split_off(i), + } + } +} + +impl<'a> DoubleEndedIterator for FlowListIterator<'a> { + fn next_back(&mut self) -> Option<&'a dyn Flow> { + self.it.next_back().map(Deref::deref) + } +} + +impl<'a> DoubleEndedIterator for MutFlowListIterator<'a> { + fn next_back(&mut self) -> Option<&'a mut dyn Flow> { + self.it.next_back().map(FlowRef::deref_mut) + } +} + +impl<'a> Iterator for FlowListIterator<'a> { + type Item = &'a dyn Flow; + #[inline] + fn next(&mut self) -> Option<&'a dyn Flow> { + self.it.next().map(Deref::deref) + } + + #[inline] + fn size_hint(&self) -> (usize, Option<usize>) { + self.it.size_hint() + } +} + +impl<'a> Iterator for MutFlowListIterator<'a> { + type Item = &'a mut dyn Flow; + #[inline] + fn next(&mut self) -> Option<&'a mut dyn Flow> { + self.it.next().map(FlowRef::deref_mut) + } + + #[inline] + fn size_hint(&self) -> (usize, Option<usize>) { + self.it.size_hint() + } +} + +/// A caching random-access iterator that yields mutable references. This is guaranteed to perform +/// no more than O(n) pointer chases. +pub struct FlowListRandomAccessMut<'a> { + iterator: linked_list::IterMut<'a, FlowRef>, + cache: Vec<FlowRef>, +} + +impl<'a> FlowListRandomAccessMut<'a> { + pub fn get<'b>(&'b mut self, index: usize) -> &'b mut dyn Flow { + while index >= self.cache.len() { + match self.iterator.next() { + None => panic!("Flow index out of range!"), + Some(next_flow) => self.cache.push((*next_flow).clone()), + } + } + FlowRef::deref_mut(&mut self.cache[index]) + } +} diff --git a/components/layout_2020/flow_ref.rs b/components/layout_2020/flow_ref.rs new file mode 100644 index 00000000000..f00740aa4ed --- /dev/null +++ b/components/layout_2020/flow_ref.rs @@ -0,0 +1,64 @@ +/* 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/. */ + +//! Reference-counted pointers to flows. +//! +//! Eventually, with dynamically sized types in Rust, much of this code will +//! be superfluous. This design is largely duplicating logic of Arc<T> and +//! Weak<T>; please see comments there for details. + +use crate::flow::Flow; +use std::ops::Deref; +use std::sync::{Arc, Weak}; + +#[derive(Clone, Debug)] +pub struct FlowRef(Arc<dyn Flow>); + +impl Deref for FlowRef { + type Target = dyn Flow; + fn deref(&self) -> &dyn Flow { + self.0.deref() + } +} + +impl FlowRef { + /// `FlowRef`s can only be made available to the traversal code. + /// See https://github.com/servo/servo/issues/14014 for more details. + pub fn new(mut r: Arc<dyn Flow>) -> Self { + // This assertion checks that this `FlowRef` does not alias normal `Arc`s. + // If that happens, we're in trouble. + assert!(Arc::get_mut(&mut r).is_some()); + FlowRef(r) + } + pub fn get_mut(this: &mut FlowRef) -> Option<&mut dyn Flow> { + Arc::get_mut(&mut this.0) + } + pub fn downgrade(this: &FlowRef) -> WeakFlowRef { + WeakFlowRef(Arc::downgrade(&this.0)) + } + pub fn into_arc(mut this: FlowRef) -> Arc<dyn Flow> { + // This assertion checks that this `FlowRef` does not alias normal `Arc`s. + // If that happens, we're in trouble. + assert!(FlowRef::get_mut(&mut this).is_some()); + this.0 + } + /// WARNING: This should only be used when there is no aliasing: + /// when the traversal ensures that no other threads accesses the same flow at the same time. + /// See https://github.com/servo/servo/issues/6503 + /// Use Arc::get_mut instead when possible (e.g. on an Arc that was just created). + #[allow(unsafe_code)] + pub fn deref_mut(this: &mut FlowRef) -> &mut dyn Flow { + let ptr: *const dyn Flow = &*this.0; + unsafe { &mut *(ptr as *mut dyn Flow) } + } +} + +#[derive(Clone, Debug)] +pub struct WeakFlowRef(Weak<dyn Flow>); + +impl WeakFlowRef { + pub fn upgrade(&self) -> Option<FlowRef> { + self.0.upgrade().map(FlowRef) + } +} diff --git a/components/layout_2020/fragment.rs b/components/layout_2020/fragment.rs new file mode 100644 index 00000000000..879e2af2f64 --- /dev/null +++ b/components/layout_2020/fragment.rs @@ -0,0 +1,3455 @@ +/* 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/. */ + +//! The `Fragment` type, which represents the leaves of the layout tree. + +use crate::context::{with_thread_local_font_context, LayoutContext}; +use crate::display_list::items::{ClipScrollNodeIndex, OpaqueNode, BLUR_INFLATION_FACTOR}; +use crate::display_list::ToLayout; +use crate::floats::ClearType; +use crate::flow::{GetBaseFlow, ImmutableFlowUtils}; +use crate::flow_ref::FlowRef; +use crate::inline::{InlineFragmentContext, InlineFragmentNodeFlags, InlineFragmentNodeInfo}; +use crate::inline::{InlineMetrics, LineMetrics}; +#[cfg(debug_assertions)] +use crate::layout_debug; +use crate::model::style_length; +use crate::model::{self, IntrinsicISizes, IntrinsicISizesContribution, MaybeAuto, SizeConstraint}; +use crate::text; +use crate::text::TextRunScanner; +use crate::wrapper::ThreadSafeLayoutNodeHelpers; +use crate::ServoArc; +use app_units::Au; +use canvas_traits::canvas::{CanvasId, CanvasMsg}; +use euclid::{Point2D, Rect, Size2D, Vector2D}; +use gfx::text::glyph::ByteIndex; +use gfx::text::text_run::{TextRun, TextRunSlice}; +use gfx_traits::StackingContextId; +use ipc_channel::ipc::IpcSender; +use msg::constellation_msg::{BrowsingContextId, PipelineId}; +use net_traits::image::base::{Image, ImageMetadata}; +use net_traits::image_cache::{ImageOrMetadataAvailable, UsePlaceholder}; +use range::*; +use script_layout_interface::wrapper_traits::{ + PseudoElementType, ThreadSafeLayoutElement, ThreadSafeLayoutNode, +}; +use script_layout_interface::{HTMLCanvasData, HTMLCanvasDataSource, HTMLMediaData, SVGSVGData}; +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use servo_url::ServoUrl; +use std::borrow::ToOwned; +use std::cmp::{max, min, Ordering}; +use std::collections::LinkedList; +use std::sync::{Arc, Mutex}; +use std::{f32, fmt}; +use style::computed_values::border_collapse::T as BorderCollapse; +use style::computed_values::box_sizing::T as BoxSizing; +use style::computed_values::clear::T as Clear; +use style::computed_values::color::T as Color; +use style::computed_values::display::T as Display; +use style::computed_values::mix_blend_mode::T as MixBlendMode; +use style::computed_values::overflow_wrap::T as OverflowWrap; +use style::computed_values::overflow_x::T as StyleOverflow; +use style::computed_values::position::T as Position; +use style::computed_values::text_decoration_line::T as TextDecorationLine; +use style::computed_values::transform_style::T as TransformStyle; +use style::computed_values::white_space::T as WhiteSpace; +use style::computed_values::word_break::T as WordBreak; +use style::logical_geometry::{Direction, LogicalMargin, LogicalRect, LogicalSize, WritingMode}; +use style::properties::ComputedValues; +use style::selector_parser::RestyleDamage; +use style::servo::restyle_damage::ServoRestyleDamage; +use style::str::char_is_whitespace; +use style::values::computed::counters::ContentItem; +use style::values::computed::{LengthPercentage, LengthPercentageOrAuto, Size, VerticalAlign}; +use style::values::generics::box_::{Perspective, VerticalAlignKeyword}; +use style::values::generics::transform; +use style::Zero; +use webrender_api; +use webrender_api::units::LayoutTransform; + +// From gfxFontConstants.h in Firefox. +static FONT_SUBSCRIPT_OFFSET_RATIO: f32 = 0.20; +static FONT_SUPERSCRIPT_OFFSET_RATIO: f32 = 0.34; + +// https://drafts.csswg.org/css-images/#default-object-size +static DEFAULT_REPLACED_WIDTH: i32 = 300; +static DEFAULT_REPLACED_HEIGHT: i32 = 150; + +/// Fragments (`struct Fragment`) are the leaves of the layout tree. They cannot position +/// themselves. In general, fragments do not have a simple correspondence with CSS fragments in the +/// specification: +/// +/// * Several fragments may correspond to the same CSS box or DOM node. For example, a CSS text box +/// broken across two lines is represented by two fragments. +/// +/// * Some CSS fragments are not created at all, such as some anonymous block fragments induced by +/// inline fragments with block-level sibling fragments. In that case, Servo uses an `InlineFlow` +/// with `BlockFlow` siblings; the `InlineFlow` is block-level, but not a block container. It is +/// positioned as if it were a block fragment, but its children are positioned according to +/// inline flow. +/// +/// A `SpecificFragmentInfo::Generic` is an empty fragment that contributes only borders, margins, +/// padding, and backgrounds. It is analogous to a CSS nonreplaced content box. +/// +/// A fragment's type influences how its styles are interpreted during layout. For example, +/// replaced content such as images are resized differently from tables, text, or other content. +/// Different types of fragments may also contain custom data; for example, text fragments contain +/// text. +/// +/// Do not add fields to this structure unless they're really really mega necessary! Fragments get +/// moved around a lot and thus their size impacts performance of layout quite a bit. +/// +/// FIXME(#2260, pcwalton): This can be slimmed down some by (at least) moving `inline_context` +/// to be on `InlineFlow` only. +#[derive(Clone)] +pub struct Fragment { + /// An opaque reference to the DOM node that this `Fragment` originates from. + pub node: OpaqueNode, + + /// The CSS style of this fragment. + pub style: ServoArc<ComputedValues>, + + /// The CSS style of this fragment when it's selected + pub selected_style: ServoArc<ComputedValues>, + + /// The position of this fragment relative to its owning flow. The size includes padding and + /// border, but not margin. + /// + /// NB: This does not account for relative positioning. + /// NB: Collapsed borders are not included in this. + pub border_box: LogicalRect<Au>, + + /// The sum of border and padding; i.e. the distance from the edge of the border box to the + /// content edge of the fragment. + pub border_padding: LogicalMargin<Au>, + + /// The margin of the content box. + pub margin: LogicalMargin<Au>, + + /// Info specific to the kind of fragment. Keep this enum small. + pub specific: SpecificFragmentInfo, + + /// Holds the style context information for fragments that are part of an inline formatting + /// context. + pub inline_context: Option<InlineFragmentContext>, + + /// How damaged this fragment is since last reflow. + pub restyle_damage: RestyleDamage, + + /// The pseudo-element that this fragment represents. + pub pseudo: PseudoElementType, + + /// Various flags for this fragment. + pub flags: FragmentFlags, + + /// A debug ID that is consistent for the life of this fragment (via transform etc). + /// This ID should not be considered stable across multiple layouts or fragment + /// manipulations. + debug_id: DebugId, + + /// The ID of the StackingContext that contains this fragment. This is initialized + /// to 0, but it assigned during the collect_stacking_contexts phase of display + /// list construction. + pub stacking_context_id: StackingContextId, + + /// The indices of this Fragment's ClipScrollNode. If this fragment doesn't have a + /// `established_reference_frame` assigned, it will use the `clipping_and_scrolling` of the + /// parent block. + pub established_reference_frame: Option<ClipScrollNodeIndex>, +} + +impl Serialize for Fragment { + fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + let mut serializer = serializer.serialize_struct("fragment", 3)?; + serializer.serialize_field("id", &self.debug_id)?; + serializer.serialize_field("border_box", &self.border_box)?; + serializer.serialize_field("margin", &self.margin)?; + serializer.end() + } +} + +/// Info specific to the kind of fragment. +/// +/// Keep this enum small. As in, no more than one word. Or pcwalton will yell at you. +#[derive(Clone)] +pub enum SpecificFragmentInfo { + Generic, + + /// A piece of generated content that cannot be resolved into `ScannedText` until the generated + /// content resolution phase (e.g. an ordered list item marker). + GeneratedContent(Box<GeneratedContentInfo>), + + Iframe(IframeFragmentInfo), + Image(Box<ImageFragmentInfo>), + Media(Box<MediaFragmentInfo>), + Canvas(Box<CanvasFragmentInfo>), + Svg(Box<SvgFragmentInfo>), + + /// A hypothetical box (see CSS 2.1 § 10.3.7) for an absolutely-positioned block that was + /// declared with `display: inline;`. + InlineAbsoluteHypothetical(InlineAbsoluteHypotheticalFragmentInfo), + + InlineBlock(InlineBlockFragmentInfo), + + /// An inline fragment that establishes an absolute containing block for its descendants (i.e. + /// a positioned inline fragment). + InlineAbsolute(InlineAbsoluteFragmentInfo), + + ScannedText(Box<ScannedTextFragmentInfo>), + Table, + TableCell, + TableColumn(TableColumnFragmentInfo), + TableRow, + TableWrapper, + Multicol, + MulticolColumn, + UnscannedText(Box<UnscannedTextFragmentInfo>), + + /// A container for a fragment that got truncated by text-overflow. + /// "Totally truncated fragments" are not rendered at all. + /// Text fragments may be partially truncated (in which case this renders like a text fragment). + /// Other fragments can only be totally truncated or not truncated at all. + TruncatedFragment(Box<TruncatedFragmentInfo>), +} + +impl SpecificFragmentInfo { + fn restyle_damage(&self) -> RestyleDamage { + let flow = match *self { + SpecificFragmentInfo::Canvas(_) | + SpecificFragmentInfo::GeneratedContent(_) | + SpecificFragmentInfo::Iframe(_) | + SpecificFragmentInfo::Image(_) | + SpecificFragmentInfo::Media(_) | + SpecificFragmentInfo::ScannedText(_) | + SpecificFragmentInfo::Svg(_) | + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableColumn(_) | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TableWrapper | + SpecificFragmentInfo::Multicol | + SpecificFragmentInfo::MulticolColumn | + SpecificFragmentInfo::UnscannedText(_) | + SpecificFragmentInfo::TruncatedFragment(_) | + SpecificFragmentInfo::Generic => return RestyleDamage::empty(), + SpecificFragmentInfo::InlineAbsoluteHypothetical(ref info) => &info.flow_ref, + SpecificFragmentInfo::InlineAbsolute(ref info) => &info.flow_ref, + SpecificFragmentInfo::InlineBlock(ref info) => &info.flow_ref, + }; + + flow.base().restyle_damage + } + + pub fn get_type(&self) -> &'static str { + match *self { + SpecificFragmentInfo::Canvas(_) => "SpecificFragmentInfo::Canvas", + SpecificFragmentInfo::Media(_) => "SpecificFragmentInfo::Media", + SpecificFragmentInfo::Generic => "SpecificFragmentInfo::Generic", + SpecificFragmentInfo::GeneratedContent(_) => "SpecificFragmentInfo::GeneratedContent", + SpecificFragmentInfo::Iframe(_) => "SpecificFragmentInfo::Iframe", + SpecificFragmentInfo::Image(_) => "SpecificFragmentInfo::Image", + SpecificFragmentInfo::InlineAbsolute(_) => "SpecificFragmentInfo::InlineAbsolute", + SpecificFragmentInfo::InlineAbsoluteHypothetical(_) => { + "SpecificFragmentInfo::InlineAbsoluteHypothetical" + }, + SpecificFragmentInfo::InlineBlock(_) => "SpecificFragmentInfo::InlineBlock", + SpecificFragmentInfo::ScannedText(_) => "SpecificFragmentInfo::ScannedText", + SpecificFragmentInfo::Svg(_) => "SpecificFragmentInfo::Svg", + SpecificFragmentInfo::Table => "SpecificFragmentInfo::Table", + SpecificFragmentInfo::TableCell => "SpecificFragmentInfo::TableCell", + SpecificFragmentInfo::TableColumn(_) => "SpecificFragmentInfo::TableColumn", + SpecificFragmentInfo::TableRow => "SpecificFragmentInfo::TableRow", + SpecificFragmentInfo::TableWrapper => "SpecificFragmentInfo::TableWrapper", + SpecificFragmentInfo::Multicol => "SpecificFragmentInfo::Multicol", + SpecificFragmentInfo::MulticolColumn => "SpecificFragmentInfo::MulticolColumn", + SpecificFragmentInfo::UnscannedText(_) => "SpecificFragmentInfo::UnscannedText", + SpecificFragmentInfo::TruncatedFragment(_) => "SpecificFragmentInfo::TruncatedFragment", + } + } +} + +impl fmt::Debug for SpecificFragmentInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + SpecificFragmentInfo::ScannedText(ref info) => write!(f, "{:?}", info.text()), + SpecificFragmentInfo::UnscannedText(ref info) => write!(f, "{:?}", info.text), + _ => Ok(()), + } + } +} + +/// Information for generated content. +#[derive(Clone)] +pub enum GeneratedContentInfo { + ListItem, + ContentItem(ContentItem), + /// Placeholder for elements with generated content that did not generate any fragments. + Empty, +} + +/// A hypothetical box (see CSS 2.1 § 10.3.7) for an absolutely-positioned block that was declared +/// with `display: inline;`. +/// +/// FIXME(pcwalton): Stop leaking this `FlowRef` to layout; that is not memory safe because layout +/// can clone it. +#[derive(Clone)] +pub struct InlineAbsoluteHypotheticalFragmentInfo { + pub flow_ref: FlowRef, +} + +impl InlineAbsoluteHypotheticalFragmentInfo { + pub fn new(flow_ref: FlowRef) -> InlineAbsoluteHypotheticalFragmentInfo { + InlineAbsoluteHypotheticalFragmentInfo { flow_ref: flow_ref } + } +} + +/// A fragment that represents an inline-block element. +/// +/// FIXME(pcwalton): Stop leaking this `FlowRef` to layout; that is not memory safe because layout +/// can clone it. +#[derive(Clone)] +pub struct InlineBlockFragmentInfo { + pub flow_ref: FlowRef, +} + +impl InlineBlockFragmentInfo { + pub fn new(flow_ref: FlowRef) -> InlineBlockFragmentInfo { + InlineBlockFragmentInfo { flow_ref: flow_ref } + } +} + +/// An inline fragment that establishes an absolute containing block for its descendants (i.e. +/// a positioned inline fragment). +/// +/// FIXME(pcwalton): Stop leaking this `FlowRef` to layout; that is not memory safe because layout +/// can clone it. +#[derive(Clone)] +pub struct InlineAbsoluteFragmentInfo { + pub flow_ref: FlowRef, +} + +impl InlineAbsoluteFragmentInfo { + pub fn new(flow_ref: FlowRef) -> InlineAbsoluteFragmentInfo { + InlineAbsoluteFragmentInfo { flow_ref: flow_ref } + } +} + +#[derive(Clone)] +pub enum CanvasFragmentSource { + WebGL(webrender_api::ImageKey), + Image(Option<Arc<Mutex<IpcSender<CanvasMsg>>>>), +} + +#[derive(Clone)] +pub struct CanvasFragmentInfo { + pub source: CanvasFragmentSource, + pub dom_width: Au, + pub dom_height: Au, + pub canvas_id: CanvasId, +} + +impl CanvasFragmentInfo { + pub fn new(data: HTMLCanvasData) -> CanvasFragmentInfo { + let source = match data.source { + HTMLCanvasDataSource::WebGL(texture_id) => CanvasFragmentSource::WebGL(texture_id), + HTMLCanvasDataSource::Image(ipc_sender) => CanvasFragmentSource::Image( + ipc_sender.map(|renderer| Arc::new(Mutex::new(renderer))), + ), + }; + + CanvasFragmentInfo { + source: source, + dom_width: Au::from_px(data.width as i32), + dom_height: Au::from_px(data.height as i32), + canvas_id: data.canvas_id, + } + } +} + +#[derive(Clone)] +pub struct MediaFragmentInfo { + pub current_frame: Option<(webrender_api::ImageKey, i32, i32)>, +} + +impl MediaFragmentInfo { + pub fn new(data: HTMLMediaData) -> MediaFragmentInfo { + MediaFragmentInfo { + current_frame: data.current_frame, + } + } +} + +#[derive(Clone)] +pub struct SvgFragmentInfo { + pub dom_width: Au, + pub dom_height: Au, +} + +impl SvgFragmentInfo { + pub fn new(data: SVGSVGData) -> SvgFragmentInfo { + SvgFragmentInfo { + dom_width: Au::from_px(data.width as i32), + dom_height: Au::from_px(data.height as i32), + } + } +} + +/// A fragment that represents a replaced content image and its accompanying borders, shadows, etc. +#[derive(Clone)] +pub struct ImageFragmentInfo { + pub image: Option<Arc<Image>>, + pub metadata: Option<ImageMetadata>, +} + +enum ImageOrMetadata { + Image(Arc<Image>), + Metadata(ImageMetadata), +} + +impl ImageFragmentInfo { + /// Creates a new image fragment from the given URL and local image cache. + /// + /// FIXME(pcwalton): The fact that image fragments store the cache in the fragment makes little + /// sense to me. + pub fn new<N: ThreadSafeLayoutNode>( + url: Option<ServoUrl>, + density: Option<f64>, + node: &N, + layout_context: &LayoutContext, + ) -> ImageFragmentInfo { + // First use any image data present in the element... + let image_or_metadata = node + .image_data() + .and_then(|(image, metadata)| match (image, metadata) { + (Some(image), _) => Some(ImageOrMetadata::Image(image)), + (None, Some(metadata)) => Some(ImageOrMetadata::Metadata(metadata)), + _ => None, + }) + .or_else(|| { + url.and_then(|url| { + // Otherwise query the image cache for anything known about the associated source URL. + layout_context + .get_or_request_image_or_meta(node.opaque(), url, UsePlaceholder::Yes) + .map(|result| match result { + ImageOrMetadataAvailable::ImageAvailable(i, _) => { + ImageOrMetadata::Image(i) + }, + ImageOrMetadataAvailable::MetadataAvailable(m) => { + ImageOrMetadata::Metadata(m) + }, + }) + }) + }); + + let current_pixel_density = density.unwrap_or(1f64); + + let (image, metadata) = match image_or_metadata { + Some(ImageOrMetadata::Image(i)) => { + let height = (i.height as f64 / current_pixel_density) as u32; + let width = (i.width as f64 / current_pixel_density) as u32; + ( + Some(Arc::new(Image { + height: height, + width: width, + ..(*i).clone() + })), + Some(ImageMetadata { + height: height, + width: width, + }), + ) + }, + Some(ImageOrMetadata::Metadata(m)) => ( + None, + Some(ImageMetadata { + height: (m.height as f64 / current_pixel_density) as u32, + width: (m.width as f64 / current_pixel_density) as u32, + }), + ), + None => (None, None), + }; + + ImageFragmentInfo { + image: image, + metadata: metadata, + } + } +} + +/// A fragment that represents an inline frame (iframe). This stores the frame ID so that the +/// size of this iframe can be communicated via the constellation to the iframe's own layout thread. +#[derive(Clone)] +pub struct IframeFragmentInfo { + /// The frame ID of this iframe. None if there is no nested browsing context. + pub browsing_context_id: Option<BrowsingContextId>, + /// The pipelineID of this iframe. None if there is no nested browsing context. + pub pipeline_id: Option<PipelineId>, +} + +impl IframeFragmentInfo { + /// Creates the information specific to an iframe fragment. + pub fn new<N: ThreadSafeLayoutNode>(node: &N) -> IframeFragmentInfo { + let browsing_context_id = node.iframe_browsing_context_id(); + let pipeline_id = node.iframe_pipeline_id(); + IframeFragmentInfo { + browsing_context_id: browsing_context_id, + pipeline_id: pipeline_id, + } + } +} + +/// A scanned text fragment represents a single run of text with a distinct style. A `TextFragment` +/// may be split into two or more fragments across line breaks. Several `TextFragment`s may +/// correspond to a single DOM text node. Split text fragments are implemented by referring to +/// subsets of a single `TextRun` object. +#[derive(Clone)] +pub struct ScannedTextFragmentInfo { + /// The text run that this represents. + pub run: Arc<TextRun>, + + /// The intrinsic size of the text fragment. + pub content_size: LogicalSize<Au>, + + /// The byte offset of the insertion point, if any. + pub insertion_point: Option<ByteIndex>, + + /// The range within the above text run that this represents. + pub range: Range<ByteIndex>, + + /// The endpoint of the above range, including whitespace that was stripped out. This exists + /// so that we can restore the range to its original value (before line breaking occurred) when + /// performing incremental reflow. + pub range_end_including_stripped_whitespace: ByteIndex, + + pub flags: ScannedTextFlags, +} + +bitflags! { + pub struct ScannedTextFlags: u8 { + /// Whether a line break is required after this fragment if wrapping on newlines (e.g. if + /// `white-space: pre` is in effect). + const REQUIRES_LINE_BREAK_AFTERWARD_IF_WRAPPING_ON_NEWLINES = 0x01; + + /// Is this fragment selected? + const SELECTED = 0x02; + + /// Suppress line breaking between this and the previous fragment + /// + /// This handles cases like Foo<span>bar</span> + const SUPPRESS_LINE_BREAK_BEFORE = 0x04; + } +} + +impl ScannedTextFragmentInfo { + /// Creates the information specific to a scanned text fragment from a range and a text run. + pub fn new( + run: Arc<TextRun>, + range: Range<ByteIndex>, + content_size: LogicalSize<Au>, + insertion_point: Option<ByteIndex>, + flags: ScannedTextFlags, + ) -> ScannedTextFragmentInfo { + ScannedTextFragmentInfo { + run: run, + range: range, + insertion_point: insertion_point, + content_size: content_size, + range_end_including_stripped_whitespace: range.end(), + flags: flags, + } + } + + pub fn text(&self) -> &str { + &self.run.text[self.range.begin().to_usize()..self.range.end().to_usize()] + } + + pub fn requires_line_break_afterward_if_wrapping_on_newlines(&self) -> bool { + self.flags + .contains(ScannedTextFlags::REQUIRES_LINE_BREAK_AFTERWARD_IF_WRAPPING_ON_NEWLINES) + } + + pub fn selected(&self) -> bool { + self.flags.contains(ScannedTextFlags::SELECTED) + } +} + +/// Describes how to split a fragment. This is used during line breaking as part of the return +/// value of `find_split_info_for_inline_size()`. +#[derive(Clone, Debug)] +pub struct SplitInfo { + // TODO(bjz): this should only need to be a single character index, but both values are + // currently needed for splitting in the `inline::try_append_*` functions. + pub range: Range<ByteIndex>, + pub inline_size: Au, +} + +impl SplitInfo { + fn new(range: Range<ByteIndex>, info: &ScannedTextFragmentInfo) -> SplitInfo { + let inline_size = info.run.advance_for_range(&range); + SplitInfo { + range: range, + inline_size: inline_size, + } + } +} + +/// Describes how to split a fragment into two. This contains up to two `SplitInfo`s. +pub struct SplitResult { + /// The part of the fragment that goes on the first line. + pub inline_start: Option<SplitInfo>, + /// The part of the fragment that goes on the second line. + pub inline_end: Option<SplitInfo>, + /// The text run which is being split. + pub text_run: Arc<TextRun>, +} + +/// Describes how a fragment should be truncated. +struct TruncationResult { + /// The part of the fragment remaining after truncation. + split: SplitInfo, + /// The text run which is being truncated. + text_run: Arc<TextRun>, +} + +/// Data for an unscanned text fragment. Unscanned text fragments are the results of flow +/// construction that have not yet had their inline-size determined. +#[derive(Clone)] +pub struct UnscannedTextFragmentInfo { + /// The text inside the fragment. + pub text: Box<str>, + + /// The selected text range. An empty range represents the insertion point. + pub selection: Option<Range<ByteIndex>>, +} + +impl UnscannedTextFragmentInfo { + /// Creates a new instance of `UnscannedTextFragmentInfo` from the given text. + #[inline] + pub fn new(text: Box<str>, selection: Option<Range<ByteIndex>>) -> UnscannedTextFragmentInfo { + UnscannedTextFragmentInfo { + text: text, + selection: selection, + } + } +} + +/// A fragment that represents a table column. +#[derive(Clone, Copy)] +pub struct TableColumnFragmentInfo { + /// the number of columns a <col> element should span + pub span: u32, +} + +impl TableColumnFragmentInfo { + /// Create the information specific to an table column fragment. + pub fn new<N: ThreadSafeLayoutNode>(node: &N) -> TableColumnFragmentInfo { + let element = node.as_element().unwrap(); + let span = element + .get_attr(&ns!(), &local_name!("span")) + .and_then(|string| string.parse().ok()) + .unwrap_or(0); + TableColumnFragmentInfo { span: span } + } +} + +/// A wrapper for fragments that have been truncated by the `text-overflow` property. +/// This may have an associated text node, or, if the fragment was completely truncated, +/// it may act as an invisible marker for incremental reflow. +#[derive(Clone)] +pub struct TruncatedFragmentInfo { + pub text_info: Option<ScannedTextFragmentInfo>, + pub full: Fragment, +} + +impl Fragment { + /// Constructs a new `Fragment` instance. + pub fn new<N: ThreadSafeLayoutNode>( + node: &N, + specific: SpecificFragmentInfo, + ctx: &LayoutContext, + ) -> Fragment { + let shared_context = ctx.shared_context(); + let style = node.style(shared_context); + let writing_mode = style.writing_mode; + + let mut restyle_damage = node.restyle_damage(); + restyle_damage.remove(ServoRestyleDamage::RECONSTRUCT_FLOW); + + Fragment { + node: node.opaque(), + style: style, + selected_style: node.selected_style(), + restyle_damage: restyle_damage, + border_box: LogicalRect::zero(writing_mode), + border_padding: LogicalMargin::zero(writing_mode), + margin: LogicalMargin::zero(writing_mode), + specific: specific, + inline_context: None, + pseudo: node.get_pseudo_element_type(), + flags: FragmentFlags::empty(), + debug_id: DebugId::new(), + stacking_context_id: StackingContextId::root(), + established_reference_frame: None, + } + } + + /// Constructs a new `Fragment` instance from an opaque node. + pub fn from_opaque_node_and_style( + node: OpaqueNode, + pseudo: PseudoElementType, + style: ServoArc<ComputedValues>, + selected_style: ServoArc<ComputedValues>, + mut restyle_damage: RestyleDamage, + specific: SpecificFragmentInfo, + ) -> Fragment { + let writing_mode = style.writing_mode; + + restyle_damage.remove(ServoRestyleDamage::RECONSTRUCT_FLOW); + + Fragment { + node: node, + style: style, + selected_style: selected_style, + restyle_damage: restyle_damage, + border_box: LogicalRect::zero(writing_mode), + border_padding: LogicalMargin::zero(writing_mode), + margin: LogicalMargin::zero(writing_mode), + specific: specific, + inline_context: None, + pseudo: pseudo, + flags: FragmentFlags::empty(), + debug_id: DebugId::new(), + stacking_context_id: StackingContextId::root(), + established_reference_frame: None, + } + } + + /// Creates an anonymous fragment just like this one but with the given style and fragment + /// type. For the new anonymous fragment, layout-related values (border box, etc.) are reset to + /// initial values. + pub fn create_similar_anonymous_fragment( + &self, + style: ServoArc<ComputedValues>, + specific: SpecificFragmentInfo, + ) -> Fragment { + let writing_mode = style.writing_mode; + Fragment { + node: self.node, + style: style, + selected_style: self.selected_style.clone(), + restyle_damage: self.restyle_damage, + border_box: LogicalRect::zero(writing_mode), + border_padding: LogicalMargin::zero(writing_mode), + margin: LogicalMargin::zero(writing_mode), + specific: specific, + inline_context: None, + pseudo: self.pseudo, + flags: FragmentFlags::empty(), + debug_id: DebugId::new(), + stacking_context_id: StackingContextId::root(), + established_reference_frame: None, + } + } + + /// Transforms this fragment into another fragment of the given type, with the given size, + /// preserving all the other data. + pub fn transform(&self, size: LogicalSize<Au>, info: SpecificFragmentInfo) -> Fragment { + let new_border_box = + LogicalRect::from_point_size(self.style.writing_mode, self.border_box.start, size); + + let mut restyle_damage = RestyleDamage::rebuild_and_reflow(); + restyle_damage.remove(ServoRestyleDamage::RECONSTRUCT_FLOW); + + Fragment { + node: self.node, + style: self.style.clone(), + selected_style: self.selected_style.clone(), + restyle_damage: restyle_damage, + border_box: new_border_box, + border_padding: self.border_padding, + margin: self.margin, + specific: info, + inline_context: self.inline_context.clone(), + pseudo: self.pseudo.clone(), + flags: FragmentFlags::empty(), + debug_id: self.debug_id.clone(), + stacking_context_id: StackingContextId::root(), + established_reference_frame: None, + } + } + + /// Transforms this fragment using the given `SplitInfo`, preserving all the other data. + /// + /// If this is the first half of a split, `first` is true + pub fn transform_with_split_info( + &self, + split: &SplitInfo, + text_run: Arc<TextRun>, + first: bool, + ) -> Fragment { + let size = LogicalSize::new( + self.style.writing_mode, + split.inline_size, + self.border_box.size.block, + ); + // Preserve the insertion point if it is in this fragment's range or it is at line end. + let (mut flags, insertion_point) = match self.specific { + SpecificFragmentInfo::ScannedText(ref info) => match info.insertion_point { + Some(index) if split.range.contains(index) => (info.flags, info.insertion_point), + Some(index) + if index == ByteIndex(text_run.text.chars().count() as isize - 1) && + index == split.range.end() => + { + (info.flags, info.insertion_point) + }, + _ => (info.flags, None), + }, + _ => (ScannedTextFlags::empty(), None), + }; + + if !first { + flags.set(ScannedTextFlags::SUPPRESS_LINE_BREAK_BEFORE, false); + } + + let info = Box::new(ScannedTextFragmentInfo::new( + text_run, + split.range, + size, + insertion_point, + flags, + )); + self.transform(size, SpecificFragmentInfo::ScannedText(info)) + } + + /// Transforms this fragment into an ellipsis fragment, preserving all the other data. + pub fn transform_into_ellipsis( + &self, + layout_context: &LayoutContext, + text_overflow_string: String, + ) -> Fragment { + let mut unscanned_ellipsis_fragments = LinkedList::new(); + let mut ellipsis_fragment = self.transform( + self.border_box.size, + SpecificFragmentInfo::UnscannedText(Box::new(UnscannedTextFragmentInfo::new( + text_overflow_string.into_boxed_str(), + None, + ))), + ); + unscanned_ellipsis_fragments.push_back(ellipsis_fragment); + let ellipsis_fragments = with_thread_local_font_context(layout_context, |font_context| { + TextRunScanner::new().scan_for_runs(font_context, unscanned_ellipsis_fragments) + }); + debug_assert_eq!(ellipsis_fragments.len(), 1); + ellipsis_fragment = ellipsis_fragments.fragments.into_iter().next().unwrap(); + ellipsis_fragment.flags |= FragmentFlags::IS_ELLIPSIS; + ellipsis_fragment + } + + pub fn restyle_damage(&self) -> RestyleDamage { + self.restyle_damage | self.specific.restyle_damage() + } + + pub fn contains_node(&self, node_address: OpaqueNode) -> bool { + node_address == self.node || + self.inline_context + .as_ref() + .map_or(false, |ctx| ctx.contains_node(node_address)) + } + + /// Adds a style to the inline context for this fragment. If the inline context doesn't exist + /// yet, it will be created. + pub fn add_inline_context_style(&mut self, node_info: InlineFragmentNodeInfo) { + if self.inline_context.is_none() { + self.inline_context = Some(InlineFragmentContext::new()); + } + self.inline_context.as_mut().unwrap().nodes.push(node_info); + } + + /// Determines which quantities (border/padding/margin/specified) should be included in the + /// intrinsic inline size of this fragment. + fn quantities_included_in_intrinsic_inline_size( + &self, + ) -> QuantitiesIncludedInIntrinsicInlineSizes { + match self.specific { + SpecificFragmentInfo::Canvas(_) | + SpecificFragmentInfo::Media(_) | + SpecificFragmentInfo::Generic | + SpecificFragmentInfo::GeneratedContent(_) | + SpecificFragmentInfo::Iframe(_) | + SpecificFragmentInfo::Image(_) | + SpecificFragmentInfo::InlineAbsolute(_) | + SpecificFragmentInfo::Multicol | + SpecificFragmentInfo::Svg(_) => { + QuantitiesIncludedInIntrinsicInlineSizes::all() + } + SpecificFragmentInfo::Table => { + QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_SPECIFIED | + QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_PADDING | + QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_BORDER + } + SpecificFragmentInfo::TableCell => { + let base_quantities = QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_PADDING | + QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_SPECIFIED; + if self.style.get_inherited_table().border_collapse == + BorderCollapse::Separate { + base_quantities | QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_BORDER + } else { + base_quantities + } + } + SpecificFragmentInfo::TableWrapper => { + let base_quantities = QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_MARGINS | + QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_SPECIFIED; + if self.style.get_inherited_table().border_collapse == + BorderCollapse::Separate { + base_quantities | QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_BORDER + } else { + base_quantities + } + } + SpecificFragmentInfo::TableRow => { + let base_quantities = + QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_SPECIFIED; + if self.style.get_inherited_table().border_collapse == + BorderCollapse::Separate { + base_quantities | QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_BORDER + } else { + base_quantities + } + } + SpecificFragmentInfo::TruncatedFragment(_) | + SpecificFragmentInfo::ScannedText(_) | + SpecificFragmentInfo::TableColumn(_) | + SpecificFragmentInfo::UnscannedText(_) | + SpecificFragmentInfo::InlineAbsoluteHypothetical(_) | + SpecificFragmentInfo::InlineBlock(_) | + SpecificFragmentInfo::MulticolColumn => { + QuantitiesIncludedInIntrinsicInlineSizes::empty() + } + } + } + + /// Returns the portion of the intrinsic inline-size that consists of borders/padding and + /// margins, respectively. + /// + /// FIXME(#2261, pcwalton): This won't work well for inlines: is this OK? + pub fn surrounding_intrinsic_inline_size(&self) -> (Au, Au) { + let flags = self.quantities_included_in_intrinsic_inline_size(); + let style = self.style(); + + // FIXME(pcwalton): Percentages should be relative to any definite size per CSS-SIZING. + // This will likely need to be done by pushing down definite sizes during selector + // cascading. + let margin = if flags.contains( + QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_MARGINS, + ) { + let margin = style.logical_margin(); + (MaybeAuto::from_style(margin.inline_start, Au(0)).specified_or_zero() + + MaybeAuto::from_style(margin.inline_end, Au(0)).specified_or_zero()) + } else { + Au(0) + }; + + // FIXME(pcwalton): Percentages should be relative to any definite size per CSS-SIZING. + // This will likely need to be done by pushing down definite sizes during selector + // cascading. + let padding = if flags.contains( + QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_PADDING, + ) { + let padding = style.logical_padding(); + (padding.inline_start.to_used_value(Au(0)) + padding.inline_end.to_used_value(Au(0))) + } else { + Au(0) + }; + + let border = if flags.contains( + QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_BORDER, + ) { + self.border_width().inline_start_end() + } else { + Au(0) + }; + + (border + padding, margin) + } + + /// Uses the style only to estimate the intrinsic inline-sizes. These may be modified for text + /// or replaced elements. + pub fn style_specified_intrinsic_inline_size(&self) -> IntrinsicISizesContribution { + let flags = self.quantities_included_in_intrinsic_inline_size(); + let style = self.style(); + + // FIXME(#2261, pcwalton): This won't work well for inlines: is this OK? + let (border_padding, margin) = self.surrounding_intrinsic_inline_size(); + + let mut specified = Au(0); + if flags.contains( + QuantitiesIncludedInIntrinsicInlineSizes::INTRINSIC_INLINE_SIZE_INCLUDES_SPECIFIED, + ) { + specified = style + .content_inline_size() + .to_used_value(Au(0)) + .unwrap_or(Au(0)); + specified = max( + style + .min_inline_size() + .to_used_value(Au(0)) + .unwrap_or(Au(0)), + specified, + ); + if let Some(max) = style.max_inline_size().to_used_value(Au(0)) { + specified = min(specified, max) + } + + if self.style.get_position().box_sizing == BoxSizing::BorderBox { + specified = max(Au(0), specified - border_padding); + } + } + + IntrinsicISizesContribution { + content_intrinsic_sizes: IntrinsicISizes { + minimum_inline_size: specified, + preferred_inline_size: specified, + }, + surrounding_size: border_padding + margin, + } + } + + /// intrinsic width of this replaced element. + #[inline] + pub fn intrinsic_width(&self) -> Au { + match self.specific { + SpecificFragmentInfo::Image(ref info) => { + if let Some(ref data) = info.metadata { + Au::from_px(data.width as i32) + } else { + Au(0) + } + }, + SpecificFragmentInfo::Media(ref info) => { + if let Some((_, width, _)) = info.current_frame { + Au::from_px(width as i32) + } else { + Au(0) + } + }, + SpecificFragmentInfo::Canvas(ref info) => info.dom_width, + SpecificFragmentInfo::Svg(ref info) => info.dom_width, + // Note: Currently for replaced element with no intrinsic size, + // this function simply returns the default object size. As long as + // these elements do not have intrinsic aspect ratio this should be + // sufficient, but we may need to investigate if this is enough for + // use cases like SVG. + SpecificFragmentInfo::Iframe(_) => Au::from_px(DEFAULT_REPLACED_WIDTH), + _ => panic!("Trying to get intrinsic width on non-replaced element!"), + } + } + + /// intrinsic width of this replaced element. + #[inline] + pub fn intrinsic_height(&self) -> Au { + match self.specific { + SpecificFragmentInfo::Image(ref info) => { + if let Some(ref data) = info.metadata { + Au::from_px(data.height as i32) + } else { + Au(0) + } + }, + SpecificFragmentInfo::Media(ref info) => { + if let Some((_, _, height)) = info.current_frame { + Au::from_px(height as i32) + } else { + Au(0) + } + }, + SpecificFragmentInfo::Canvas(ref info) => info.dom_height, + SpecificFragmentInfo::Svg(ref info) => info.dom_height, + SpecificFragmentInfo::Iframe(_) => Au::from_px(DEFAULT_REPLACED_HEIGHT), + _ => panic!("Trying to get intrinsic height on non-replaced element!"), + } + } + + /// Whether this replace element has intrinsic aspect ratio. + pub fn has_intrinsic_ratio(&self) -> bool { + match self.specific { + SpecificFragmentInfo::Image(_) | + SpecificFragmentInfo::Canvas(_) | + SpecificFragmentInfo::Media(_) | + // TODO(stshine): According to the SVG spec, whether a SVG element has intrinsic + // aspect ratio is determined by the `preserveAspectRatio` attribute. Since for + // now SVG is far from implemented, we simply choose the default behavior that + // the intrinsic aspect ratio is preserved. + // https://svgwg.org/svg2-draft/coords.html#PreserveAspectRatioAttribute + SpecificFragmentInfo::Svg(_) => + self.intrinsic_width() != Au(0) && self.intrinsic_height() != Au(0), + _ => false + } + } + + /// CSS 2.1 § 10.3.2 & 10.6.2 Calculate the used width and height of a replaced element. + /// When a parameter is `None` it means the specified size in certain direction + /// is unconstrained. The inline containing size can also be `None` since this + /// method is also used for calculating intrinsic inline size contribution. + pub fn calculate_replaced_sizes( + &self, + containing_inline_size: Option<Au>, + containing_block_size: Option<Au>, + ) -> (Au, Au) { + let (intrinsic_inline_size, intrinsic_block_size) = if self.style.writing_mode.is_vertical() + { + (self.intrinsic_height(), self.intrinsic_width()) + } else { + (self.intrinsic_width(), self.intrinsic_height()) + }; + + // Make sure the size we used here is for content box since they may be + // transferred by the intrinsic aspect ratio. + let inline_size = style_length(self.style.content_inline_size(), containing_inline_size) + .map(|x| x - self.box_sizing_boundary(Direction::Inline)); + let block_size = style_length(self.style.content_block_size(), containing_block_size) + .map(|x| x - self.box_sizing_boundary(Direction::Block)); + let inline_constraint = self.size_constraint(containing_inline_size, Direction::Inline); + let block_constraint = self.size_constraint(containing_block_size, Direction::Block); + + // https://drafts.csswg.org/css-images-3/#default-sizing + match (inline_size, block_size) { + // If the specified size is a definite width and height, the concrete + // object size is given that width and height. + (MaybeAuto::Specified(inline_size), MaybeAuto::Specified(block_size)) => ( + inline_constraint.clamp(inline_size), + block_constraint.clamp(block_size), + ), + + // If the specified size is only a width or height (but not both) + // then the concrete object size is given that specified width or + // height. The other dimension is calculated as follows: + // + // If the object has an intrinsic aspect ratio, the missing dimension + // of the concrete object size is calculated using the intrinsic + // aspect ratio and the present dimension. + // + // Otherwise, if the missing dimension is present in the object’s intrinsic + // dimensions, the missing dimension is taken from the object’s intrinsic + // dimensions. Otherwise it is taken from the default object size. + (MaybeAuto::Specified(inline_size), MaybeAuto::Auto) => { + let inline_size = inline_constraint.clamp(inline_size); + let block_size = if self.has_intrinsic_ratio() { + // Note: We can not precompute the ratio and store it as a float, because + // doing so may result one pixel difference in calculation for certain + // images, thus make some tests fail. + Au::new( + (inline_size.0 as i64 * intrinsic_block_size.0 as i64 / + intrinsic_inline_size.0 as i64) as i32, + ) + } else { + intrinsic_block_size + }; + (inline_size, block_constraint.clamp(block_size)) + }, + (MaybeAuto::Auto, MaybeAuto::Specified(block_size)) => { + let block_size = block_constraint.clamp(block_size); + let inline_size = if self.has_intrinsic_ratio() { + Au::new( + (block_size.0 as i64 * intrinsic_inline_size.0 as i64 / + intrinsic_block_size.0 as i64) as i32, + ) + } else { + intrinsic_inline_size + }; + (inline_constraint.clamp(inline_size), block_size) + }, + // https://drafts.csswg.org/css2/visudet.html#min-max-widths + (MaybeAuto::Auto, MaybeAuto::Auto) => { + if self.has_intrinsic_ratio() { + // This approch follows the spirit of cover and contain constraint. + // https://drafts.csswg.org/css-images-3/#cover-contain + + // First, create two rectangles that keep aspect ratio while may be clamped + // by the contraints; + let first_isize = inline_constraint.clamp(intrinsic_inline_size); + let first_bsize = Au::new( + (first_isize.0 as i64 * intrinsic_block_size.0 as i64 / + intrinsic_inline_size.0 as i64) as i32, + ); + let second_bsize = block_constraint.clamp(intrinsic_block_size); + let second_isize = Au::new( + (second_bsize.0 as i64 * intrinsic_inline_size.0 as i64 / + intrinsic_block_size.0 as i64) as i32, + ); + let (inline_size, block_size) = match ( + first_isize.cmp(&intrinsic_inline_size), + second_isize.cmp(&intrinsic_inline_size), + ) { + (Ordering::Equal, Ordering::Equal) => (first_isize, first_bsize), + // When only one rectangle is clamped, use it; + (Ordering::Equal, _) => (second_isize, second_bsize), + (_, Ordering::Equal) => (first_isize, first_bsize), + // When both rectangles grow (smaller than min sizes), + // Choose the larger one; + (Ordering::Greater, Ordering::Greater) => { + if first_isize > second_isize { + (first_isize, first_bsize) + } else { + (second_isize, second_bsize) + } + }, + // When both rectangles shrink (larger than max sizes), + // Choose the smaller one; + (Ordering::Less, Ordering::Less) => { + if first_isize > second_isize { + (second_isize, second_bsize) + } else { + (first_isize, first_bsize) + } + }, + // It does not matter which we choose here, because both sizes + // will be clamped to constraint; + (Ordering::Less, Ordering::Greater) | + (Ordering::Greater, Ordering::Less) => (first_isize, first_bsize), + }; + // Clamp the result and we are done :-) + ( + inline_constraint.clamp(inline_size), + block_constraint.clamp(block_size), + ) + } else { + ( + inline_constraint.clamp(intrinsic_inline_size), + block_constraint.clamp(intrinsic_block_size), + ) + } + }, + } + } + + /// Return a size constraint that can be used the clamp size in given direction. + /// To take `box-sizing: border-box` into account, the `border_padding` field + /// must be initialized first. + /// + /// TODO(stshine): Maybe there is a more convenient way. + pub fn size_constraint( + &self, + containing_size: Option<Au>, + direction: Direction, + ) -> SizeConstraint { + let (style_min_size, style_max_size) = match direction { + Direction::Inline => (self.style.min_inline_size(), self.style.max_inline_size()), + Direction::Block => (self.style.min_block_size(), self.style.max_block_size()), + }; + + let border = if self.style().get_position().box_sizing == BoxSizing::BorderBox { + Some(self.border_padding.start_end(direction)) + } else { + None + }; + + SizeConstraint::new(containing_size, style_min_size, style_max_size, border) + } + + /// Returns a guess as to the distances from the margin edge of this fragment to its content + /// in the inline direction. This will generally be correct unless percentages are involved. + /// + /// This is used for the float placement speculation logic. + pub fn guess_inline_content_edge_offsets(&self) -> SpeculatedInlineContentEdgeOffsets { + let logical_margin = self.style.logical_margin(); + let logical_padding = self.style.logical_padding(); + let border_width = self.border_width(); + SpeculatedInlineContentEdgeOffsets { + start: MaybeAuto::from_style(logical_margin.inline_start, Au(0)).specified_or_zero() + + logical_padding.inline_start.to_used_value(Au(0)) + + border_width.inline_start, + end: MaybeAuto::from_style(logical_margin.inline_end, Au(0)).specified_or_zero() + + logical_padding.inline_end.to_used_value(Au(0)) + + border_width.inline_end, + } + } + + /// Returns the sum of the inline-sizes of all the borders of this fragment. Note that this + /// can be expensive to compute, so if possible use the `border_padding` field instead. + #[inline] + pub fn border_width(&self) -> LogicalMargin<Au> { + let style_border_width = self.style().logical_border_width(); + + // NOTE: We can have nodes with different writing mode inside + // the inline fragment context, so we need to overwrite the + // writing mode to compute the child logical sizes. + let writing_mode = self.style.writing_mode; + let context_border = match self.inline_context { + None => LogicalMargin::zero(writing_mode), + Some(ref inline_fragment_context) => inline_fragment_context.nodes.iter().fold( + style_border_width, + |accumulator, node| { + let mut this_border_width = + node.style.border_width_for_writing_mode(writing_mode); + if !node + .flags + .contains(InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT) + { + this_border_width.inline_start = Au(0) + } + if !node + .flags + .contains(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT) + { + this_border_width.inline_end = Au(0) + } + accumulator + this_border_width + }, + ), + }; + style_border_width + context_border + } + + /// Returns the border width in given direction if this fragment has property + /// 'box-sizing: border-box'. The `border_padding` field must have been initialized. + pub fn box_sizing_boundary(&self, direction: Direction) -> Au { + match (self.style().get_position().box_sizing, direction) { + (BoxSizing::BorderBox, Direction::Inline) => self.border_padding.inline_start_end(), + (BoxSizing::BorderBox, Direction::Block) => self.border_padding.block_start_end(), + _ => Au(0), + } + } + + /// Computes the margins in the inline direction from the containing block inline-size and the + /// style. After this call, the inline direction of the `margin` field will be correct. + /// + /// Do not use this method if the inline direction margins are to be computed some other way + /// (for example, via constraint solving for blocks). + pub fn compute_inline_direction_margins(&mut self, containing_block_inline_size: Au) { + match self.specific { + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TableColumn(_) | + SpecificFragmentInfo::InlineAbsoluteHypothetical(_) => { + self.margin.inline_start = Au(0); + self.margin.inline_end = Au(0); + return; + }, + _ => { + let margin = self.style().logical_margin(); + self.margin.inline_start = + MaybeAuto::from_style(margin.inline_start, containing_block_inline_size) + .specified_or_zero(); + self.margin.inline_end = + MaybeAuto::from_style(margin.inline_end, containing_block_inline_size) + .specified_or_zero(); + }, + } + + if let Some(ref inline_context) = self.inline_context { + for node in &inline_context.nodes { + let margin = node.style.logical_margin(); + let this_inline_start_margin = if !node + .flags + .contains(InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT) + { + Au(0) + } else { + MaybeAuto::from_style(margin.inline_start, containing_block_inline_size) + .specified_or_zero() + }; + let this_inline_end_margin = if !node + .flags + .contains(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT) + { + Au(0) + } else { + MaybeAuto::from_style(margin.inline_end, containing_block_inline_size) + .specified_or_zero() + }; + + self.margin.inline_start += this_inline_start_margin; + self.margin.inline_end += this_inline_end_margin; + } + } + } + + /// Computes the margins in the block direction from the containing block inline-size and the + /// style. After this call, the block direction of the `margin` field will be correct. + /// + /// Do not use this method if the block direction margins are to be computed some other way + /// (for example, via constraint solving for absolutely-positioned flows). + pub fn compute_block_direction_margins(&mut self, containing_block_inline_size: Au) { + match self.specific { + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TableColumn(_) => { + self.margin.block_start = Au(0); + self.margin.block_end = Au(0) + }, + _ => { + // NB: Percentages are relative to containing block inline-size (not block-size) + // per CSS 2.1. + let margin = self.style().logical_margin(); + self.margin.block_start = + MaybeAuto::from_style(margin.block_start, containing_block_inline_size) + .specified_or_zero(); + self.margin.block_end = + MaybeAuto::from_style(margin.block_end, containing_block_inline_size) + .specified_or_zero(); + }, + } + } + + /// Computes the border and padding in both inline and block directions from the containing + /// block inline-size and the style. After this call, the `border_padding` field will be + /// correct. + pub fn compute_border_and_padding(&mut self, containing_block_inline_size: Au) { + // Compute border. + let border = match self.style.get_inherited_table().border_collapse { + BorderCollapse::Separate => self.border_width(), + BorderCollapse::Collapse => LogicalMargin::zero(self.style.writing_mode), + }; + + // Compute padding from the fragment's style. + let padding_from_style = match self.specific { + SpecificFragmentInfo::TableColumn(_) | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TableWrapper => LogicalMargin::zero(self.style.writing_mode), + _ => model::padding_from_style( + self.style(), + containing_block_inline_size, + self.style().writing_mode, + ), + }; + + // Compute padding from the inline fragment context. + let padding_from_inline_fragment_context = match (&self.specific, &self.inline_context) { + (_, &None) | + (&SpecificFragmentInfo::TableColumn(_), _) | + (&SpecificFragmentInfo::TableRow, _) | + (&SpecificFragmentInfo::TableWrapper, _) => { + LogicalMargin::zero(self.style.writing_mode) + }, + (_, &Some(ref inline_fragment_context)) => { + let writing_mode = self.style.writing_mode; + let zero_padding = LogicalMargin::zero(writing_mode); + inline_fragment_context + .nodes + .iter() + .fold(zero_padding, |accumulator, node| { + let mut padding = + model::padding_from_style(&*node.style, Au(0), writing_mode); + if !node + .flags + .contains(InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT) + { + padding.inline_start = Au(0) + } + if !node + .flags + .contains(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT) + { + padding.inline_end = Au(0) + } + accumulator + padding + }) + }, + }; + + self.border_padding = border + padding_from_style + padding_from_inline_fragment_context + } + + // Return offset from original position because of `position: relative`. + pub fn relative_position(&self, containing_block_size: &LogicalSize<Au>) -> LogicalSize<Au> { + fn from_style(style: &ComputedValues, container_size: &LogicalSize<Au>) -> LogicalSize<Au> { + let offsets = style.logical_position(); + let offset_i = if offsets.inline_start != LengthPercentageOrAuto::Auto { + MaybeAuto::from_style(offsets.inline_start, container_size.inline) + .specified_or_zero() + } else { + -MaybeAuto::from_style(offsets.inline_end, container_size.inline) + .specified_or_zero() + }; + let offset_b = if offsets.block_start != LengthPercentageOrAuto::Auto { + MaybeAuto::from_style(offsets.block_start, container_size.block).specified_or_zero() + } else { + -MaybeAuto::from_style(offsets.block_end, container_size.block).specified_or_zero() + }; + LogicalSize::new(style.writing_mode, offset_i, offset_b) + } + + // Go over the ancestor fragments and add all relative offsets (if any). + let mut rel_pos = if self.style().get_box().position == Position::Relative { + from_style(self.style(), containing_block_size) + } else { + LogicalSize::zero(self.style.writing_mode) + }; + + if let Some(ref inline_fragment_context) = self.inline_context { + for node in &inline_fragment_context.nodes { + if node.style.get_box().position == Position::Relative { + rel_pos = rel_pos + from_style(&*node.style, containing_block_size); + } + } + } + + rel_pos + } + + /// Always inline for SCCP. + /// + /// FIXME(pcwalton): Just replace with the clear type from the style module for speed? + #[inline(always)] + pub fn clear(&self) -> Option<ClearType> { + let style = self.style(); + match style.get_box().clear { + Clear::None => None, + Clear::Left => Some(ClearType::Left), + Clear::Right => Some(ClearType::Right), + Clear::Both => Some(ClearType::Both), + } + } + + #[inline(always)] + pub fn style(&self) -> &ComputedValues { + &*self.style + } + + #[inline(always)] + pub fn selected_style(&self) -> &ComputedValues { + &*self.selected_style + } + + pub fn white_space(&self) -> WhiteSpace { + self.style().get_inherited_text().white_space + } + + pub fn color(&self) -> Color { + self.style().get_inherited_text().color + } + + /// Returns the text decoration line of this fragment, according to the style of the nearest ancestor + /// element. + /// + /// NB: This may not be the actual text decoration line, because of the override rules specified in + /// CSS 2.1 § 16.3.1. Unfortunately, computing this properly doesn't really fit into Servo's + /// model. Therefore, this is a best lower bound approximation, but the end result may actually + /// have the various decoration flags turned on afterward. + pub fn text_decoration_line(&self) -> TextDecorationLine { + self.style().get_text().text_decoration_line + } + + /// Returns the inline-start offset from margin edge to content edge. + /// + /// FIXME(#2262, pcwalton): I think this method is pretty bogus, because it won't work for + /// inlines. + pub fn inline_start_offset(&self) -> Au { + match self.specific { + SpecificFragmentInfo::TableWrapper => self.margin.inline_start, + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableRow => self.border_padding.inline_start, + SpecificFragmentInfo::TableColumn(_) => Au(0), + _ => self.margin.inline_start + self.border_padding.inline_start, + } + } + + /// If this is a Column fragment, get the col span + /// + /// Panics for non-column fragments + pub fn column_span(&self) -> u32 { + match self.specific { + SpecificFragmentInfo::TableColumn(col_fragment) => max(col_fragment.span, 1), + _ => panic!("non-table-column fragment inside table column?!"), + } + } + + /// Returns true if this element can be split. This is true for text fragments, unless + /// `white-space: pre` or `white-space: nowrap` is set. + pub fn can_split(&self) -> bool { + self.is_scanned_text_fragment() && self.white_space().allow_wrap() + } + + /// Returns true if and only if this fragment is a generated content fragment. + pub fn is_unscanned_generated_content(&self) -> bool { + match self.specific { + SpecificFragmentInfo::GeneratedContent(ref content) => match **content { + GeneratedContentInfo::Empty => false, + _ => true, + }, + _ => false, + } + } + + /// Returns true if and only if this is a scanned text fragment. + pub fn is_scanned_text_fragment(&self) -> bool { + match self.specific { + SpecificFragmentInfo::ScannedText(..) => true, + _ => false, + } + } + + pub fn suppress_line_break_before(&self) -> bool { + match self.specific { + SpecificFragmentInfo::ScannedText(ref st) => st + .flags + .contains(ScannedTextFlags::SUPPRESS_LINE_BREAK_BEFORE), + _ => false, + } + } + + /// Computes the intrinsic inline-sizes of this fragment. + pub fn compute_intrinsic_inline_sizes(&mut self) -> IntrinsicISizesContribution { + let mut result = self.style_specified_intrinsic_inline_size(); + match self.specific { + SpecificFragmentInfo::Generic | + SpecificFragmentInfo::GeneratedContent(_) | + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableColumn(_) | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TableWrapper | + SpecificFragmentInfo::Multicol | + SpecificFragmentInfo::MulticolColumn | + SpecificFragmentInfo::InlineAbsoluteHypothetical(_) => {}, + SpecificFragmentInfo::InlineBlock(ref info) => { + let block_flow = info.flow_ref.as_block(); + result.union_block(&block_flow.base.intrinsic_inline_sizes) + }, + SpecificFragmentInfo::InlineAbsolute(ref info) => { + let block_flow = info.flow_ref.as_block(); + result.union_block(&block_flow.base.intrinsic_inline_sizes) + }, + SpecificFragmentInfo::Image(_) | + SpecificFragmentInfo::Media(_) | + SpecificFragmentInfo::Canvas(_) | + SpecificFragmentInfo::Iframe(_) | + SpecificFragmentInfo::Svg(_) => { + let inline_size = match self.style.content_inline_size() { + Size::Auto => None, + Size::LengthPercentage(ref lp) => lp.maybe_to_used_value(None), + }; + + let mut inline_size = inline_size.unwrap_or_else(|| { + // We have to initialize the `border_padding` field first to make + // the size constraints work properly. + // TODO(stshine): Find a cleaner way to do this. + let padding = self.style.logical_padding(); + self.border_padding.inline_start = padding.inline_start.to_used_value(Au(0)); + self.border_padding.inline_end = padding.inline_end.to_used_value(Au(0)); + self.border_padding.block_start = padding.block_start.to_used_value(Au(0)); + self.border_padding.block_end = padding.block_end.to_used_value(Au(0)); + let border = self.border_width(); + self.border_padding.inline_start += border.inline_start; + self.border_padding.inline_end += border.inline_end; + self.border_padding.block_start += border.block_start; + self.border_padding.block_end += border.block_end; + let (result_inline, _) = self.calculate_replaced_sizes(None, None); + result_inline + }); + + let size_constraint = self.size_constraint(None, Direction::Inline); + inline_size = size_constraint.clamp(inline_size); + + result.union_block(&IntrinsicISizes { + minimum_inline_size: inline_size, + preferred_inline_size: inline_size, + }); + }, + + SpecificFragmentInfo::TruncatedFragment(ref t) if t.text_info.is_some() => { + let text_fragment_info = t.text_info.as_ref().unwrap(); + handle_text(text_fragment_info, self, &mut result) + }, + SpecificFragmentInfo::ScannedText(ref text_fragment_info) => { + handle_text(text_fragment_info, self, &mut result) + }, + + SpecificFragmentInfo::TruncatedFragment(_) => { + return IntrinsicISizesContribution::new() + }, + + SpecificFragmentInfo::UnscannedText(..) => { + panic!("Unscanned text fragments should have been scanned by now!") + }, + }; + + fn handle_text( + text_fragment_info: &ScannedTextFragmentInfo, + self_: &Fragment, + result: &mut IntrinsicISizesContribution, + ) { + let range = &text_fragment_info.range; + + // See http://dev.w3.org/csswg/css-sizing/#max-content-inline-size. + // TODO: Account for soft wrap opportunities. + let max_line_inline_size = text_fragment_info + .run + .metrics_for_range(range) + .advance_width; + + let min_line_inline_size = if self_.white_space().allow_wrap() { + text_fragment_info.run.min_width_for_range(range) + } else { + max_line_inline_size + }; + + result.union_block(&IntrinsicISizes { + minimum_inline_size: min_line_inline_size, + preferred_inline_size: max_line_inline_size, + }) + } + + // Take borders and padding for parent inline fragments into account. + let writing_mode = self.style.writing_mode; + if let Some(ref context) = self.inline_context { + for node in &context.nodes { + let mut border_width = node.style.logical_border_width(); + let mut padding = model::padding_from_style(&*node.style, Au(0), writing_mode); + let mut margin = model::specified_margin_from_style(&*node.style, writing_mode); + if !node + .flags + .contains(InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT) + { + border_width.inline_start = Au(0); + padding.inline_start = Au(0); + margin.inline_start = Au(0); + } + if !node + .flags + .contains(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT) + { + border_width.inline_end = Au(0); + padding.inline_end = Au(0); + margin.inline_end = Au(0); + } + + result.surrounding_size = result.surrounding_size + + border_width.inline_start_end() + + padding.inline_start_end() + + margin.inline_start_end(); + } + } + + result + } + + /// Returns the narrowest inline-size that the first splittable part of this fragment could + /// possibly be split to. (In most cases, this returns the inline-size of the first word in + /// this fragment.) + pub fn minimum_splittable_inline_size(&self) -> Au { + match self.specific { + SpecificFragmentInfo::TruncatedFragment(ref t) if t.text_info.is_some() => { + let text = t.text_info.as_ref().unwrap(); + text.run.minimum_splittable_inline_size(&text.range) + }, + SpecificFragmentInfo::ScannedText(ref text) => { + text.run.minimum_splittable_inline_size(&text.range) + }, + _ => Au(0), + } + } + + /// Returns the dimensions of the content box. + /// + /// This is marked `#[inline]` because it is frequently called when only one or two of the + /// values are needed and that will save computation. + #[inline] + pub fn content_box(&self) -> LogicalRect<Au> { + self.border_box - self.border_padding + } + + /// Attempts to find the split positions of a text fragment so that its inline-size is no more + /// than `max_inline_size`. + /// + /// A return value of `None` indicates that the fragment could not be split. Otherwise the + /// information pertaining to the split is returned. The inline-start and inline-end split + /// information are both optional due to the possibility of them being whitespace. + pub fn calculate_split_position( + &self, + max_inline_size: Au, + starts_line: bool, + ) -> Option<SplitResult> { + let text_fragment_info = match self.specific { + SpecificFragmentInfo::ScannedText(ref text_fragment_info) => text_fragment_info, + _ => return None, + }; + + let mut flags = SplitOptions::empty(); + if starts_line { + flags.insert(SplitOptions::STARTS_LINE); + if self.style().get_inherited_text().overflow_wrap == OverflowWrap::BreakWord { + flags.insert(SplitOptions::RETRY_AT_CHARACTER_BOUNDARIES) + } + } + + match self.style().get_inherited_text().word_break { + WordBreak::Normal | WordBreak::KeepAll => { + // Break at normal word boundaries. keep-all forbids soft wrap opportunities. + let natural_word_breaking_strategy = text_fragment_info + .run + .natural_word_slices_in_range(&text_fragment_info.range); + self.calculate_split_position_using_breaking_strategy( + natural_word_breaking_strategy, + max_inline_size, + flags, + ) + }, + WordBreak::BreakAll => { + // Break at character boundaries. + let character_breaking_strategy = text_fragment_info + .run + .character_slices_in_range(&text_fragment_info.range); + flags.remove(SplitOptions::RETRY_AT_CHARACTER_BOUNDARIES); + self.calculate_split_position_using_breaking_strategy( + character_breaking_strategy, + max_inline_size, + flags, + ) + }, + } + } + + /// Does this fragment start on a glyph run boundary? + pub fn is_on_glyph_run_boundary(&self) -> bool { + let text_fragment_info = match self.specific { + SpecificFragmentInfo::ScannedText(ref text_fragment_info) => text_fragment_info, + _ => return true, + }; + text_fragment_info + .run + .on_glyph_run_boundary(text_fragment_info.range.begin()) + } + + /// Truncates this fragment to the given `max_inline_size`, using a character-based breaking + /// strategy. The resulting fragment will have `SpecificFragmentInfo::TruncatedFragment`, + /// preserving the original fragment for use in incremental reflow. + /// + /// This function will panic if self is already truncated. + pub fn truncate_to_inline_size(self, max_inline_size: Au) -> Fragment { + if let SpecificFragmentInfo::TruncatedFragment(_) = self.specific { + panic!("Cannot truncate an already truncated fragment"); + } + let info = self.calculate_truncate_to_inline_size(max_inline_size); + let (size, text_info) = match info { + Some(TruncationResult { + split: SplitInfo { inline_size, range }, + text_run, + }) => { + let size = LogicalSize::new( + self.style.writing_mode, + inline_size, + self.border_box.size.block, + ); + // Preserve the insertion point if it is in this fragment's range or it is at line end. + let (flags, insertion_point) = match self.specific { + SpecificFragmentInfo::ScannedText(ref info) => match info.insertion_point { + Some(index) if range.contains(index) => (info.flags, info.insertion_point), + Some(index) + if index == ByteIndex(text_run.text.chars().count() as isize - 1) && + index == range.end() => + { + (info.flags, info.insertion_point) + }, + _ => (info.flags, None), + }, + _ => (ScannedTextFlags::empty(), None), + }; + let text_info = + ScannedTextFragmentInfo::new(text_run, range, size, insertion_point, flags); + (size, Some(text_info)) + }, + None => (LogicalSize::zero(self.style.writing_mode), None), + }; + let mut result = self.transform(size, SpecificFragmentInfo::Generic); + result.specific = + SpecificFragmentInfo::TruncatedFragment(Box::new(TruncatedFragmentInfo { + text_info: text_info, + full: self, + })); + result + } + + /// Truncates this fragment to the given `max_inline_size`, using a character-based breaking + /// strategy. If no characters could fit, returns `None`. + fn calculate_truncate_to_inline_size(&self, max_inline_size: Au) -> Option<TruncationResult> { + let text_fragment_info = + if let SpecificFragmentInfo::ScannedText(ref text_fragment_info) = self.specific { + text_fragment_info + } else { + return None; + }; + + let character_breaking_strategy = text_fragment_info + .run + .character_slices_in_range(&text_fragment_info.range); + + let split_info = self.calculate_split_position_using_breaking_strategy( + character_breaking_strategy, + max_inline_size, + SplitOptions::empty(), + )?; + + let split = split_info.inline_start?; + Some(TruncationResult { + split: split, + text_run: split_info.text_run.clone(), + }) + } + + /// A helper method that uses the breaking strategy described by `slice_iterator` (at present, + /// either natural word breaking or character breaking) to split this fragment. + fn calculate_split_position_using_breaking_strategy<'a, I>( + &self, + slice_iterator: I, + max_inline_size: Au, + flags: SplitOptions, + ) -> Option<SplitResult> + where + I: Iterator<Item = TextRunSlice<'a>>, + { + let text_fragment_info = match self.specific { + SpecificFragmentInfo::ScannedText(ref text_fragment_info) => text_fragment_info, + _ => return None, + }; + + let mut remaining_inline_size = max_inline_size - self.border_padding.inline_start_end(); + let mut inline_start_range = Range::new(text_fragment_info.range.begin(), ByteIndex(0)); + let mut inline_end_range = None; + let mut overflowing = false; + + debug!( + "calculate_split_position_using_breaking_strategy: splitting text fragment \ + (strlen={}, range={:?}, max_inline_size={:?})", + text_fragment_info.run.text.len(), + text_fragment_info.range, + max_inline_size + ); + + for slice in slice_iterator { + debug!( + "calculate_split_position_using_breaking_strategy: considering slice \ + (offset={:?}, slice range={:?}, remaining_inline_size={:?})", + slice.offset, slice.range, remaining_inline_size + ); + + // Use the `remaining_inline_size` to find a split point if possible. If not, go around + // the loop again with the next slice. + let metrics = text_fragment_info + .run + .metrics_for_slice(slice.glyphs, &slice.range); + let advance = metrics.advance_width; + + // Have we found the split point? + if advance <= remaining_inline_size || slice.glyphs.is_whitespace() { + // Keep going; we haven't found the split point yet. + debug!("calculate_split_position_using_breaking_strategy: enlarging span"); + remaining_inline_size = remaining_inline_size - advance; + inline_start_range.extend_by(slice.range.length()); + continue; + } + + // The advance is more than the remaining inline-size, so split here. First, check to + // see if we're going to overflow the line. If so, perform a best-effort split. + let mut remaining_range = slice.text_run_range(); + let split_is_empty = inline_start_range.is_empty() && + !(self.requires_line_break_afterward_if_wrapping_on_newlines() && + !self.white_space().allow_wrap()); + if split_is_empty { + // We're going to overflow the line. + overflowing = true; + inline_start_range = slice.text_run_range(); + remaining_range = Range::new(slice.text_run_range().end(), ByteIndex(0)); + remaining_range.extend_to(text_fragment_info.range.end()); + } + + // Check to see if we need to create an inline-end chunk. + let slice_begin = remaining_range.begin(); + if slice_begin < text_fragment_info.range.end() { + // There still some things left over at the end of the line, so create the + // inline-end chunk. + let mut inline_end = remaining_range; + inline_end.extend_to(text_fragment_info.range.end()); + inline_end_range = Some(inline_end); + debug!( + "calculate_split_position: splitting remainder with inline-end range={:?}", + inline_end + ); + } + + // If we failed to find a suitable split point, we're on the verge of overflowing the + // line. + if split_is_empty || overflowing { + // If we've been instructed to retry at character boundaries (probably via + // `overflow-wrap: break-word`), do so. + if flags.contains(SplitOptions::RETRY_AT_CHARACTER_BOUNDARIES) { + let character_breaking_strategy = text_fragment_info + .run + .character_slices_in_range(&text_fragment_info.range); + let mut flags = flags; + flags.remove(SplitOptions::RETRY_AT_CHARACTER_BOUNDARIES); + return self.calculate_split_position_using_breaking_strategy( + character_breaking_strategy, + max_inline_size, + flags, + ); + } + + // We aren't at the start of the line, so don't overflow. Let inline layout wrap to + // the next line instead. + if !flags.contains(SplitOptions::STARTS_LINE) { + return None; + } + } + + break; + } + + let split_is_empty = inline_start_range.is_empty() && + !self.requires_line_break_afterward_if_wrapping_on_newlines(); + let inline_start = if !split_is_empty { + Some(SplitInfo::new(inline_start_range, &**text_fragment_info)) + } else { + None + }; + let inline_end = inline_end_range + .map(|inline_end_range| SplitInfo::new(inline_end_range, &**text_fragment_info)); + + Some(SplitResult { + inline_start: inline_start, + inline_end: inline_end, + text_run: text_fragment_info.run.clone(), + }) + } + + /// The opposite of `calculate_split_position_using_breaking_strategy`: merges this fragment + /// with the next one. + pub fn merge_with(&mut self, next_fragment: Fragment) { + match (&mut self.specific, &next_fragment.specific) { + ( + &mut SpecificFragmentInfo::ScannedText(ref mut this_info), + &SpecificFragmentInfo::ScannedText(ref other_info), + ) => { + debug_assert!(Arc::ptr_eq(&this_info.run, &other_info.run)); + this_info.range_end_including_stripped_whitespace = + other_info.range_end_including_stripped_whitespace; + if other_info.requires_line_break_afterward_if_wrapping_on_newlines() { + this_info.flags.insert( + ScannedTextFlags::REQUIRES_LINE_BREAK_AFTERWARD_IF_WRAPPING_ON_NEWLINES, + ); + } + if other_info.insertion_point.is_some() { + this_info.insertion_point = other_info.insertion_point; + } + self.border_padding.inline_end = next_fragment.border_padding.inline_end; + self.margin.inline_end = next_fragment.margin.inline_end; + }, + _ => panic!("Can only merge two scanned-text fragments!"), + } + self.reset_text_range_and_inline_size(); + self.meld_with_next_inline_fragment(&next_fragment); + } + + /// Restore any whitespace that was stripped from a text fragment, and recompute inline metrics + /// if necessary. + pub fn reset_text_range_and_inline_size(&mut self) { + if let SpecificFragmentInfo::ScannedText(ref mut info) = self.specific { + if info.run.extra_word_spacing != Au(0) { + Arc::make_mut(&mut info.run).extra_word_spacing = Au(0); + } + + // FIXME (mbrubeck): Do we need to restore leading too? + let range_end = info.range_end_including_stripped_whitespace; + if info.range.end() == range_end { + return; + } + info.range.extend_to(range_end); + info.content_size.inline = info.run.metrics_for_range(&info.range).advance_width; + self.border_box.size.inline = + info.content_size.inline + self.border_padding.inline_start_end(); + } + } + + /// Assigns replaced inline-size, padding, and margins for this fragment only if it is replaced + /// content per CSS 2.1 § 10.3.2. + pub fn assign_replaced_inline_size_if_necessary( + &mut self, + container_inline_size: Au, + container_block_size: Option<Au>, + ) { + match self.specific { + SpecificFragmentInfo::TruncatedFragment(ref t) if t.text_info.is_none() => return, + SpecificFragmentInfo::Generic | + SpecificFragmentInfo::GeneratedContent(_) | + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TableWrapper | + SpecificFragmentInfo::Multicol | + SpecificFragmentInfo::MulticolColumn => return, + SpecificFragmentInfo::TableColumn(_) => { + panic!("Table column fragments do not have inline size") + }, + SpecificFragmentInfo::UnscannedText(_) => { + panic!("Unscanned text fragments should have been scanned by now!") + }, + SpecificFragmentInfo::Canvas(_) | + SpecificFragmentInfo::Image(_) | + SpecificFragmentInfo::Media(_) | + SpecificFragmentInfo::Iframe(_) | + SpecificFragmentInfo::InlineBlock(_) | + SpecificFragmentInfo::InlineAbsoluteHypothetical(_) | + SpecificFragmentInfo::InlineAbsolute(_) | + SpecificFragmentInfo::ScannedText(_) | + SpecificFragmentInfo::TruncatedFragment(_) | + SpecificFragmentInfo::Svg(_) => {}, + }; + + match self.specific { + // Inline blocks + SpecificFragmentInfo::InlineAbsoluteHypothetical(ref mut info) => { + let block_flow = FlowRef::deref_mut(&mut info.flow_ref).as_mut_block(); + block_flow.base.position.size.inline = + block_flow.base.intrinsic_inline_sizes.preferred_inline_size; + + // This is a hypothetical box, so it takes up no space. + self.border_box.size.inline = Au(0); + }, + SpecificFragmentInfo::InlineBlock(ref mut info) => { + let block_flow = FlowRef::deref_mut(&mut info.flow_ref).as_mut_block(); + self.border_box.size.inline = max( + block_flow.base.intrinsic_inline_sizes.minimum_inline_size, + block_flow.base.intrinsic_inline_sizes.preferred_inline_size, + ); + block_flow.base.block_container_inline_size = self.border_box.size.inline; + block_flow.base.block_container_writing_mode = self.style.writing_mode; + }, + SpecificFragmentInfo::InlineAbsolute(ref mut info) => { + let block_flow = FlowRef::deref_mut(&mut info.flow_ref).as_mut_block(); + self.border_box.size.inline = max( + block_flow.base.intrinsic_inline_sizes.minimum_inline_size, + block_flow.base.intrinsic_inline_sizes.preferred_inline_size, + ); + block_flow.base.block_container_inline_size = self.border_box.size.inline; + block_flow.base.block_container_writing_mode = self.style.writing_mode; + }, + + // Text + SpecificFragmentInfo::TruncatedFragment(ref t) if t.text_info.is_some() => { + let info = t.text_info.as_ref().unwrap(); + // Scanned text fragments will have already had their content inline-sizes assigned + // by this point. + self.border_box.size.inline = + info.content_size.inline + self.border_padding.inline_start_end(); + }, + SpecificFragmentInfo::ScannedText(ref info) => { + // Scanned text fragments will have already had their content inline-sizes assigned + // by this point. + self.border_box.size.inline = + info.content_size.inline + self.border_padding.inline_start_end(); + }, + + // Replaced elements + _ if self.is_replaced() => { + let (inline_size, block_size) = self + .calculate_replaced_sizes(Some(container_inline_size), container_block_size); + self.border_box.size.inline = inline_size + self.border_padding.inline_start_end(); + self.border_box.size.block = block_size + self.border_padding.block_start_end(); + }, + + ref unhandled @ _ => { + panic!("this case should have been handled above: {:?}", unhandled) + }, + } + } + + /// Assign block-size for this fragment if it is replaced content. The inline-size must have + /// been assigned first. + /// + /// Ideally, this should follow CSS 2.1 § 10.6.2. + pub fn assign_replaced_block_size_if_necessary(&mut self) { + match self.specific { + SpecificFragmentInfo::TruncatedFragment(ref t) if t.text_info.is_none() => return, + SpecificFragmentInfo::Generic | + SpecificFragmentInfo::GeneratedContent(_) | + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TableWrapper | + SpecificFragmentInfo::Multicol | + SpecificFragmentInfo::MulticolColumn => return, + SpecificFragmentInfo::TableColumn(_) => { + panic!("Table column fragments do not have block size") + }, + SpecificFragmentInfo::UnscannedText(_) => { + panic!("Unscanned text fragments should have been scanned by now!") + }, + SpecificFragmentInfo::Canvas(_) | + SpecificFragmentInfo::Iframe(_) | + SpecificFragmentInfo::Image(_) | + SpecificFragmentInfo::Media(_) | + SpecificFragmentInfo::InlineBlock(_) | + SpecificFragmentInfo::InlineAbsoluteHypothetical(_) | + SpecificFragmentInfo::InlineAbsolute(_) | + SpecificFragmentInfo::ScannedText(_) | + SpecificFragmentInfo::TruncatedFragment(_) | + SpecificFragmentInfo::Svg(_) => {}, + } + + match self.specific { + // Text + SpecificFragmentInfo::TruncatedFragment(ref t) if t.text_info.is_some() => { + let info = t.text_info.as_ref().unwrap(); + // Scanned text fragments' content block-sizes are calculated by the text run + // scanner during flow construction. + self.border_box.size.block = + info.content_size.block + self.border_padding.block_start_end(); + }, + SpecificFragmentInfo::ScannedText(ref info) => { + // Scanned text fragments' content block-sizes are calculated by the text run + // scanner during flow construction. + self.border_box.size.block = + info.content_size.block + self.border_padding.block_start_end(); + }, + + // Inline blocks + SpecificFragmentInfo::InlineBlock(ref mut info) => { + // Not the primary fragment, so we do not take the noncontent size into account. + let block_flow = FlowRef::deref_mut(&mut info.flow_ref).as_block(); + self.border_box.size.block = block_flow.base.position.size.block + + block_flow.fragment.margin.block_start_end() + }, + SpecificFragmentInfo::InlineAbsoluteHypothetical(ref mut info) => { + // Not the primary fragment, so we do not take the noncontent size into account. + let block_flow = FlowRef::deref_mut(&mut info.flow_ref).as_block(); + self.border_box.size.block = block_flow.base.position.size.block; + }, + SpecificFragmentInfo::InlineAbsolute(ref mut info) => { + // Not the primary fragment, so we do not take the noncontent size into account. + let block_flow = FlowRef::deref_mut(&mut info.flow_ref).as_block(); + self.border_box.size.block = block_flow.base.position.size.block + + block_flow.fragment.margin.block_start_end() + }, + + // Replaced elements + _ if self.is_replaced() => {}, + + ref unhandled @ _ => panic!("should have been handled above: {:?}", unhandled), + } + } + + /// Returns true if this fragment is replaced content. + pub fn is_replaced(&self) -> bool { + match self.specific { + SpecificFragmentInfo::Iframe(_) | + SpecificFragmentInfo::Canvas(_) | + SpecificFragmentInfo::Image(_) | + SpecificFragmentInfo::Media(_) | + SpecificFragmentInfo::Svg(_) => true, + _ => false, + } + } + + /// Returns true if this fragment is replaced content or an inline-block or false otherwise. + pub fn is_replaced_or_inline_block(&self) -> bool { + match self.specific { + SpecificFragmentInfo::InlineAbsoluteHypothetical(_) | + SpecificFragmentInfo::InlineBlock(_) => true, + _ => self.is_replaced(), + } + } + + /// Calculates block-size above baseline, depth below baseline, and ascent for this fragment + /// when used in an inline formatting context. See CSS 2.1 § 10.8.1. + /// + /// This does not take `vertical-align` into account. For that, use `aligned_inline_metrics()`. + fn content_inline_metrics(&self, layout_context: &LayoutContext) -> InlineMetrics { + // CSS 2.1 § 10.8: "The height of each inline-level box in the line box is + // calculated. For replaced elements, inline-block elements, and inline-table + // elements, this is the height of their margin box." + // + // FIXME(pcwalton): We have to handle `Generic` and `GeneratedContent` here to avoid + // crashing in a couple of `css21_dev/html4/content-` WPTs, but I don't see how those two + // fragment types should end up inside inlines. (In the case of `GeneratedContent`, those + // fragment types should have been resolved by now…) + let inline_metrics = match self.specific { + SpecificFragmentInfo::Canvas(_) | + SpecificFragmentInfo::Iframe(_) | + SpecificFragmentInfo::Image(_) | + SpecificFragmentInfo::Media(_) | + SpecificFragmentInfo::Svg(_) | + SpecificFragmentInfo::Generic | + SpecificFragmentInfo::GeneratedContent(_) => { + let ascent = self.border_box.size.block + self.margin.block_end; + InlineMetrics { + space_above_baseline: ascent + self.margin.block_start, + space_below_baseline: Au(0), + ascent: ascent, + } + }, + SpecificFragmentInfo::TruncatedFragment(ref t) if t.text_info.is_some() => { + let info = t.text_info.as_ref().unwrap(); + inline_metrics_of_text(info, self, layout_context) + }, + SpecificFragmentInfo::ScannedText(ref info) => { + inline_metrics_of_text(info, self, layout_context) + }, + SpecificFragmentInfo::InlineBlock(ref info) => { + inline_metrics_of_block(&info.flow_ref, &*self.style) + }, + SpecificFragmentInfo::InlineAbsoluteHypothetical(ref info) => { + inline_metrics_of_block(&info.flow_ref, &*self.style) + }, + SpecificFragmentInfo::TruncatedFragment(..) | + SpecificFragmentInfo::InlineAbsolute(_) => InlineMetrics::new(Au(0), Au(0), Au(0)), + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableColumn(_) | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TableWrapper | + SpecificFragmentInfo::Multicol | + SpecificFragmentInfo::MulticolColumn | + SpecificFragmentInfo::UnscannedText(_) => { + unreachable!("Shouldn't see fragments of this type here!") + }, + }; + return inline_metrics; + + fn inline_metrics_of_text( + info: &ScannedTextFragmentInfo, + self_: &Fragment, + layout_context: &LayoutContext, + ) -> InlineMetrics { + // Fragments with no glyphs don't contribute any inline metrics. + // TODO: Filter out these fragments during flow construction? + if info.insertion_point.is_none() && info.content_size.inline == Au(0) { + return InlineMetrics::new(Au(0), Au(0), Au(0)); + } + // See CSS 2.1 § 10.8.1. + let font_metrics = with_thread_local_font_context(layout_context, |font_context| { + text::font_metrics_for_style(font_context, self_.style.clone_font()) + }); + let line_height = text::line_height_from_style(&*self_.style, &font_metrics); + InlineMetrics::from_font_metrics(&info.run.font_metrics, line_height) + } + + fn inline_metrics_of_block(flow: &FlowRef, style: &ComputedValues) -> InlineMetrics { + // CSS 2.1 § 10.8: "The height of each inline-level box in the line box is calculated. + // For replaced elements, inline-block elements, and inline-table elements, this is the + // height of their margin box." + // + // CSS 2.1 § 10.8.1: "The baseline of an 'inline-block' is the baseline of its last + // line box in the normal flow, unless it has either no in-flow line boxes or if its + // 'overflow' property has a computed value other than 'visible', in which case the + // baseline is the bottom margin edge." + // + // NB: We must use `block_flow.fragment.border_box.size.block` here instead of + // `block_flow.base.position.size.block` because sometimes the latter is late-computed + // and isn't up to date at this point. + let block_flow = flow.as_block(); + let start_margin = block_flow.fragment.margin.block_start; + let end_margin = block_flow.fragment.margin.block_end; + let border_box_block_size = block_flow.fragment.border_box.size.block; + + // -------- + // margin + // top -------- + + + // | | + // | | + // A ..pogo.. | + baseline_offset_of_last_line_box_in_flow() + // | + // -------- + border_box_block_size + // margin + // B -------- + // + // § 10.8.1 says that the baseline (and thus ascent, which is the + // distance from the baseline to the top) should be A if it has an + // in-flow line box and if overflow: visible, and B otherwise. + let ascent = match ( + flow.baseline_offset_of_last_line_box_in_flow(), + style.get_box().overflow_y, + ) { + // Case A + (Some(baseline_offset), StyleOverflow::Visible) => baseline_offset, + // Case B + _ => border_box_block_size + end_margin, + }; + + let space_below_baseline = border_box_block_size + end_margin - ascent; + let space_above_baseline = ascent + start_margin; + + InlineMetrics::new(space_above_baseline, space_below_baseline, ascent) + } + } + + /// Calculates the offset from the baseline that applies to this fragment due to + /// `vertical-align`. Positive values represent downward displacement. + /// + /// If `actual_line_metrics` is supplied, then these metrics are used to determine the + /// displacement of the fragment when `top` or `bottom` `vertical-align` values are + /// encountered. If this is not supplied, then `top` and `bottom` values are ignored. + fn vertical_alignment_offset( + &self, + layout_context: &LayoutContext, + content_inline_metrics: &InlineMetrics, + minimum_line_metrics: &LineMetrics, + actual_line_metrics: Option<&LineMetrics>, + ) -> Au { + let mut offset = Au(0); + for style in self.inline_styles() { + // If any of the inline styles say `top` or `bottom`, adjust the vertical align + // appropriately. + // + // FIXME(#5624, pcwalton): This passes our current reftests but isn't the right thing + // to do. + match style.get_box().vertical_align { + VerticalAlign::Keyword(kw) => match kw { + VerticalAlignKeyword::Baseline => {}, + VerticalAlignKeyword::Middle => { + let font_metrics = + with_thread_local_font_context(layout_context, |font_context| { + text::font_metrics_for_style(font_context, self.style.clone_font()) + }); + offset += (content_inline_metrics.ascent - + content_inline_metrics.space_below_baseline - + font_metrics.x_height) + .scale_by(0.5) + }, + VerticalAlignKeyword::Sub => { + offset += minimum_line_metrics + .space_needed() + .scale_by(FONT_SUBSCRIPT_OFFSET_RATIO) + }, + VerticalAlignKeyword::Super => { + offset -= minimum_line_metrics + .space_needed() + .scale_by(FONT_SUPERSCRIPT_OFFSET_RATIO) + }, + VerticalAlignKeyword::TextTop => { + offset = self.content_inline_metrics(layout_context).ascent - + minimum_line_metrics.space_above_baseline + }, + VerticalAlignKeyword::TextBottom => { + offset = minimum_line_metrics.space_below_baseline - + self.content_inline_metrics(layout_context) + .space_below_baseline + }, + VerticalAlignKeyword::Top => { + if let Some(actual_line_metrics) = actual_line_metrics { + offset = content_inline_metrics.ascent - + actual_line_metrics.space_above_baseline + } + }, + VerticalAlignKeyword::Bottom => { + if let Some(actual_line_metrics) = actual_line_metrics { + offset = actual_line_metrics.space_below_baseline - + content_inline_metrics.space_below_baseline + } + }, + }, + VerticalAlign::Length(ref lp) => { + offset -= lp.to_used_value(minimum_line_metrics.space_needed()); + }, + } + } + offset + } + + /// Calculates block-size above baseline, depth below baseline, and ascent for this fragment + /// when used in an inline formatting context, taking `vertical-align` (other than `top` or + /// `bottom`) into account. See CSS 2.1 § 10.8.1. + /// + /// If `actual_line_metrics` is supplied, then these metrics are used to determine the + /// displacement of the fragment when `top` or `bottom` `vertical-align` values are + /// encountered. If this is not supplied, then `top` and `bottom` values are ignored. + pub fn aligned_inline_metrics( + &self, + layout_context: &LayoutContext, + minimum_line_metrics: &LineMetrics, + actual_line_metrics: Option<&LineMetrics>, + ) -> InlineMetrics { + let content_inline_metrics = self.content_inline_metrics(layout_context); + let vertical_alignment_offset = self.vertical_alignment_offset( + layout_context, + &content_inline_metrics, + minimum_line_metrics, + actual_line_metrics, + ); + let mut space_above_baseline = match actual_line_metrics { + None => content_inline_metrics.space_above_baseline, + Some(actual_line_metrics) => actual_line_metrics.space_above_baseline, + }; + space_above_baseline = space_above_baseline - vertical_alignment_offset; + let space_below_baseline = + content_inline_metrics.space_below_baseline + vertical_alignment_offset; + let ascent = content_inline_metrics.ascent - vertical_alignment_offset; + InlineMetrics::new(space_above_baseline, space_below_baseline, ascent) + } + + /// Returns true if this fragment is a hypothetical box. See CSS 2.1 § 10.3.7. + pub fn is_hypothetical(&self) -> bool { + match self.specific { + SpecificFragmentInfo::InlineAbsoluteHypothetical(_) => true, + _ => false, + } + } + + /// Returns true if this fragment can merge with another immediately-following fragment or + /// false otherwise. + pub fn can_merge_with_fragment(&self, other: &Fragment) -> bool { + match (&self.specific, &other.specific) { + ( + &SpecificFragmentInfo::UnscannedText(ref first_unscanned_text), + &SpecificFragmentInfo::UnscannedText(_), + ) => { + // FIXME: Should probably use a whitelist of styles that can safely differ (#3165) + if self.style().get_font() != other.style().get_font() || + self.text_decoration_line() != other.text_decoration_line() || + self.white_space() != other.white_space() || + self.color() != other.color() + { + return false; + } + + if first_unscanned_text.text.ends_with('\n') { + return false; + } + + // If this node has any styles that have border/padding/margins on the following + // side, then we can't merge with the next fragment. + if let Some(ref inline_context) = self.inline_context { + for inline_context_node in inline_context.nodes.iter() { + if !inline_context_node + .flags + .contains(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT) + { + continue; + } + if inline_context_node.style.logical_margin().inline_end != + LengthPercentageOrAuto::zero() + { + return false; + } + if inline_context_node.style.logical_padding().inline_end != + LengthPercentage::zero() + { + return false; + } + if inline_context_node.style.logical_border_width().inline_end != Au(0) { + return false; + } + } + } + + // If the next fragment has any styles that have border/padding/margins on the + // preceding side, then it can't merge with us. + if let Some(ref inline_context) = other.inline_context { + for inline_context_node in inline_context.nodes.iter() { + if !inline_context_node + .flags + .contains(InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT) + { + continue; + } + if inline_context_node.style.logical_margin().inline_start != + LengthPercentageOrAuto::zero() + { + return false; + } + if inline_context_node.style.logical_padding().inline_start != + LengthPercentage::zero() + { + return false; + } + if inline_context_node + .style + .logical_border_width() + .inline_start != + Au(0) + { + return false; + } + } + } + + true + }, + _ => false, + } + } + + /// Returns true if and only if this is the *primary fragment* for the fragment's style object + /// (conceptually, though style sharing makes this not really true, of course). The primary + /// fragment is the one that draws backgrounds, borders, etc., and takes borders, padding and + /// margins into account. Every style object has at most one primary fragment. + /// + /// At present, all fragments are primary fragments except for inline-block and table wrapper + /// fragments. Inline-block fragments are not primary fragments because the corresponding block + /// flow is the primary fragment, while table wrapper fragments are not primary fragments + /// because the corresponding table flow is the primary fragment. + pub fn is_primary_fragment(&self) -> bool { + match self.specific { + SpecificFragmentInfo::InlineBlock(_) | + SpecificFragmentInfo::InlineAbsoluteHypothetical(_) | + SpecificFragmentInfo::InlineAbsolute(_) | + SpecificFragmentInfo::MulticolColumn | + SpecificFragmentInfo::TableWrapper => false, + SpecificFragmentInfo::Canvas(_) | + SpecificFragmentInfo::Generic | + SpecificFragmentInfo::GeneratedContent(_) | + SpecificFragmentInfo::Iframe(_) | + SpecificFragmentInfo::Image(_) | + SpecificFragmentInfo::Media(_) | + SpecificFragmentInfo::ScannedText(_) | + SpecificFragmentInfo::Svg(_) | + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableColumn(_) | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TruncatedFragment(_) | + SpecificFragmentInfo::Multicol | + SpecificFragmentInfo::UnscannedText(_) => true, + } + } + + /// Determines the inline sizes of inline-block fragments. These cannot be fully computed until + /// inline size assignment has run for the child flow: thus it is computed "late", during + /// block size assignment. + pub fn update_late_computed_replaced_inline_size_if_necessary(&mut self) { + if let SpecificFragmentInfo::InlineBlock(ref mut inline_block_info) = self.specific { + let block_flow = FlowRef::deref_mut(&mut inline_block_info.flow_ref).as_block(); + self.border_box.size.inline = block_flow.fragment.margin_box_inline_size(); + } + } + + pub fn update_late_computed_inline_position_if_necessary(&mut self) { + if let SpecificFragmentInfo::InlineAbsoluteHypothetical(ref mut info) = self.specific { + let position = self.border_box.start.i; + FlowRef::deref_mut(&mut info.flow_ref) + .update_late_computed_inline_position_if_necessary(position) + } + } + + pub fn update_late_computed_block_position_if_necessary(&mut self) { + if let SpecificFragmentInfo::InlineAbsoluteHypothetical(ref mut info) = self.specific { + let position = self.border_box.start.b; + FlowRef::deref_mut(&mut info.flow_ref) + .update_late_computed_block_position_if_necessary(position) + } + } + + pub fn repair_style(&mut self, new_style: &ServoArc<ComputedValues>) { + self.style = (*new_style).clone() + } + + /// Given the stacking-context-relative position of the containing flow, returns the border box + /// of this fragment relative to the parent stacking context. This takes `position: relative` + /// into account. + /// + /// If `coordinate_system` is `Parent`, this returns the border box in the parent stacking + /// context's coordinate system. Otherwise, if `coordinate_system` is `Own` and this fragment + /// establishes a stacking context itself, this returns a border box anchored at (0, 0). (If + /// this fragment does not establish a stacking context, then it always belongs to its parent + /// stacking context and thus `coordinate_system` is ignored.) + /// + /// This is the method you should use for display list construction as well as + /// `getBoundingClientRect()` and so forth. + pub fn stacking_relative_border_box( + &self, + stacking_relative_flow_origin: &Vector2D<Au>, + relative_containing_block_size: &LogicalSize<Au>, + relative_containing_block_mode: WritingMode, + coordinate_system: CoordinateSystem, + ) -> Rect<Au> { + let container_size = + relative_containing_block_size.to_physical(relative_containing_block_mode); + let border_box = self + .border_box + .to_physical(self.style.writing_mode, container_size); + if coordinate_system == CoordinateSystem::Own && self.establishes_stacking_context() { + return Rect::new(Point2D::zero(), border_box.size); + } + + // FIXME(pcwalton): This can double-count relative position sometimes for inlines (e.g. + // `<div style="position:relative">x</div>`, because the `position:relative` trickles down + // to the inline flow. Possibly we should extend the notion of "primary fragment" to fix + // this. + let relative_position = self.relative_position(relative_containing_block_size); + border_box + .translate_by_size(&relative_position.to_physical(self.style.writing_mode)) + .translate(&stacking_relative_flow_origin) + } + + /// Given the stacking-context-relative border box, returns the stacking-context-relative + /// content box. + pub fn stacking_relative_content_box( + &self, + stacking_relative_border_box: Rect<Au>, + ) -> Rect<Au> { + let border_padding = self.border_padding.to_physical(self.style.writing_mode); + Rect::new( + Point2D::new( + stacking_relative_border_box.origin.x + border_padding.left, + stacking_relative_border_box.origin.y + border_padding.top, + ), + Size2D::new( + stacking_relative_border_box.size.width - border_padding.horizontal(), + stacking_relative_border_box.size.height - border_padding.vertical(), + ), + ) + } + + /// Returns true if this fragment may establish a reference frame. + pub fn can_establish_reference_frame(&self) -> bool { + !self.style().get_box().transform.0.is_empty() || + self.style().get_box().perspective != Perspective::None + } + + /// Returns true if this fragment has a filter, transform, or perspective property set. + pub fn has_filter_transform_or_perspective(&self) -> bool { + !self.style().get_box().transform.0.is_empty() || + !self.style().get_effects().filter.0.is_empty() || + self.style().get_box().perspective != Perspective::None + } + + /// Returns true if this fragment establishes a new stacking context and false otherwise. + pub fn establishes_stacking_context(&self) -> bool { + // Text fragments shouldn't create stacking contexts. + match self.specific { + SpecificFragmentInfo::TruncatedFragment(_) | + SpecificFragmentInfo::ScannedText(_) | + SpecificFragmentInfo::UnscannedText(_) => return false, + _ => {}, + } + + if self.style().get_effects().opacity != 1.0 { + return true; + } + + if self.style().get_effects().mix_blend_mode != MixBlendMode::Normal { + return true; + } + + if self.has_filter_transform_or_perspective() { + return true; + } + + if self.style().get_box().transform_style == TransformStyle::Preserve3d || + self.style().overrides_transform_style() + { + return true; + } + + // Fixed position and sticky position always create stacking contexts. + if self.style().get_box().position == Position::Fixed || + self.style().get_box().position == Position::Sticky + { + return true; + } + + // Statically positioned fragments don't establish stacking contexts if the previous + // conditions are not fulfilled. Furthermore, z-index doesn't apply to statically + // positioned fragments. + if self.style().get_box().position == Position::Static { + return false; + } + + // For absolutely and relatively positioned fragments we only establish a stacking + // context if there is a z-index set. + // See https://www.w3.org/TR/CSS2/visuren.html#z-index + !self.style().get_position().z_index.is_auto() + } + + // Get the effective z-index of this fragment. Z-indices only apply to positioned element + // per CSS 2 9.9.1 (http://www.w3.org/TR/CSS2/visuren.html#z-index), so this value may differ + // from the value specified in the style. + pub fn effective_z_index(&self) -> i32 { + match self.style().get_box().position { + Position::Static => {}, + _ => return self.style().get_position().z_index.integer_or(0), + } + + if !self.style().get_box().transform.0.is_empty() { + return self.style().get_position().z_index.integer_or(0); + } + + match self.style().get_box().display { + Display::Flex => self.style().get_position().z_index.integer_or(0), + _ => 0, + } + } + + /// Computes the overflow rect of this fragment relative to the start of the flow. + pub fn compute_overflow( + &self, + flow_size: &Size2D<Au>, + relative_containing_block_size: &LogicalSize<Au>, + ) -> Overflow { + let mut border_box = self + .border_box + .to_physical(self.style.writing_mode, *flow_size); + + // Relative position can cause us to draw outside our border box. + // + // FIXME(pcwalton): I'm not a fan of the way this makes us crawl though so many styles all + // the time. Can't we handle relative positioning by just adjusting `border_box`? + let relative_position = self.relative_position(relative_containing_block_size); + border_box = + border_box.translate_by_size(&relative_position.to_physical(self.style.writing_mode)); + let mut overflow = Overflow::from_rect(&border_box); + + // Box shadows cause us to draw outside our border box. + for box_shadow in &*self.style().get_effects().box_shadow.0 { + let offset = Vector2D::new( + Au::from(box_shadow.base.horizontal), + Au::from(box_shadow.base.vertical), + ); + let inflation = Au::from(box_shadow.spread) + + Au::from(box_shadow.base.blur) * BLUR_INFLATION_FACTOR; + overflow.paint = overflow + .paint + .union(&border_box.translate(&offset).inflate(inflation, inflation)) + } + + // Outlines cause us to draw outside our border box. + let outline_width = Au::from(self.style.get_outline().outline_width); + if outline_width != Au(0) { + overflow.paint = overflow + .paint + .union(&border_box.inflate(outline_width, outline_width)) + } + + // Include the overflow of the block flow, if any. + match self.specific { + SpecificFragmentInfo::InlineBlock(ref info) => { + let block_flow = info.flow_ref.as_block(); + overflow.union(&block_flow.base().overflow); + }, + SpecificFragmentInfo::InlineAbsolute(ref info) => { + let block_flow = info.flow_ref.as_block(); + overflow.union(&block_flow.base().overflow); + }, + _ => (), + } + + // FIXME(pcwalton): Sometimes excessively fancy glyphs can make us draw outside our border + // box too. + overflow + } + + pub fn requires_line_break_afterward_if_wrapping_on_newlines(&self) -> bool { + match self.specific { + SpecificFragmentInfo::TruncatedFragment(ref t) if t.text_info.is_some() => { + let text = t.text_info.as_ref().unwrap(); + text.requires_line_break_afterward_if_wrapping_on_newlines() + }, + SpecificFragmentInfo::ScannedText(ref text) => { + text.requires_line_break_afterward_if_wrapping_on_newlines() + }, + _ => false, + } + } + + pub fn strip_leading_whitespace_if_necessary(&mut self) -> WhitespaceStrippingResult { + if self.white_space().preserve_spaces() { + return WhitespaceStrippingResult::RetainFragment; + } + + return match self.specific { + SpecificFragmentInfo::TruncatedFragment(ref mut t) if t.text_info.is_some() => { + let scanned_text_fragment_info = t.text_info.as_mut().unwrap(); + scanned_text(scanned_text_fragment_info, &mut self.border_box) + }, + SpecificFragmentInfo::ScannedText(ref mut scanned_text_fragment_info) => { + scanned_text(scanned_text_fragment_info, &mut self.border_box) + }, + SpecificFragmentInfo::UnscannedText(ref mut unscanned_text_fragment_info) => { + let mut new_text_string = String::new(); + let mut modified = false; + for (i, character) in unscanned_text_fragment_info.text.char_indices() { + if gfx::text::util::is_bidi_control(character) { + new_text_string.push(character); + continue; + } + if char_is_whitespace(character) { + modified = true; + continue; + } + // Finished processing leading control chars and whitespace. + if modified { + new_text_string.push_str(&unscanned_text_fragment_info.text[i..]); + } + break; + } + if modified { + unscanned_text_fragment_info.text = new_text_string.into_boxed_str(); + } + + WhitespaceStrippingResult::from_unscanned_text_fragment_info( + &unscanned_text_fragment_info, + ) + }, + _ => WhitespaceStrippingResult::RetainFragment, + }; + + fn scanned_text( + scanned_text_fragment_info: &mut ScannedTextFragmentInfo, + border_box: &mut LogicalRect<Au>, + ) -> WhitespaceStrippingResult { + let leading_whitespace_byte_count = scanned_text_fragment_info + .text() + .find(|c| !char_is_whitespace(c)) + .unwrap_or(scanned_text_fragment_info.text().len()); + + let whitespace_len = ByteIndex(leading_whitespace_byte_count as isize); + let whitespace_range = + Range::new(scanned_text_fragment_info.range.begin(), whitespace_len); + let text_bounds = scanned_text_fragment_info + .run + .metrics_for_range(&whitespace_range) + .bounding_box; + border_box.size.inline = border_box.size.inline - text_bounds.size.width; + scanned_text_fragment_info.content_size.inline = + scanned_text_fragment_info.content_size.inline - text_bounds.size.width; + + scanned_text_fragment_info + .range + .adjust_by(whitespace_len, -whitespace_len); + + WhitespaceStrippingResult::RetainFragment + } + } + + /// Returns true if the entire fragment was stripped. + pub fn strip_trailing_whitespace_if_necessary(&mut self) -> WhitespaceStrippingResult { + if self.white_space().preserve_spaces() { + return WhitespaceStrippingResult::RetainFragment; + } + + return match self.specific { + SpecificFragmentInfo::TruncatedFragment(ref mut t) if t.text_info.is_some() => { + let scanned_text_fragment_info = t.text_info.as_mut().unwrap(); + scanned_text(scanned_text_fragment_info, &mut self.border_box) + }, + SpecificFragmentInfo::ScannedText(ref mut scanned_text_fragment_info) => { + scanned_text(scanned_text_fragment_info, &mut self.border_box) + }, + SpecificFragmentInfo::UnscannedText(ref mut unscanned_text_fragment_info) => { + let mut trailing_bidi_control_characters_to_retain = Vec::new(); + let (mut modified, mut last_character_index) = (true, 0); + for (i, character) in unscanned_text_fragment_info.text.char_indices().rev() { + if gfx::text::util::is_bidi_control(character) { + trailing_bidi_control_characters_to_retain.push(character); + continue; + } + if char_is_whitespace(character) { + modified = true; + continue; + } + last_character_index = i + character.len_utf8(); + break; + } + if modified { + let mut text = unscanned_text_fragment_info.text.to_string(); + text.truncate(last_character_index); + for character in trailing_bidi_control_characters_to_retain.iter().rev() { + text.push(*character); + } + unscanned_text_fragment_info.text = text.into_boxed_str(); + } + + WhitespaceStrippingResult::from_unscanned_text_fragment_info( + &unscanned_text_fragment_info, + ) + }, + _ => WhitespaceStrippingResult::RetainFragment, + }; + + fn scanned_text( + scanned_text_fragment_info: &mut ScannedTextFragmentInfo, + border_box: &mut LogicalRect<Au>, + ) -> WhitespaceStrippingResult { + let mut trailing_whitespace_start_byte = 0; + for (i, c) in scanned_text_fragment_info.text().char_indices().rev() { + if !char_is_whitespace(c) { + trailing_whitespace_start_byte = i + c.len_utf8(); + break; + } + } + let whitespace_start = ByteIndex(trailing_whitespace_start_byte as isize); + let whitespace_len = scanned_text_fragment_info.range.length() - whitespace_start; + let mut whitespace_range = Range::new(whitespace_start, whitespace_len); + whitespace_range.shift_by(scanned_text_fragment_info.range.begin()); + + let text_bounds = scanned_text_fragment_info + .run + .metrics_for_range(&whitespace_range) + .bounding_box; + border_box.size.inline -= text_bounds.size.width; + scanned_text_fragment_info.content_size.inline -= text_bounds.size.width; + + scanned_text_fragment_info.range.extend_by(-whitespace_len); + WhitespaceStrippingResult::RetainFragment + } + } + + pub fn inline_styles(&self) -> InlineStyleIterator { + InlineStyleIterator::new(self) + } + + /// Returns the inline-size of this fragment's margin box. + pub fn margin_box_inline_size(&self) -> Au { + self.border_box.size.inline + self.margin.inline_start_end() + } + + /// Returns true if this node *or any of the nodes within its inline fragment context* have + /// non-`static` `position`. + pub fn is_positioned(&self) -> bool { + if self.style.get_box().position != Position::Static { + return true; + } + if let Some(ref inline_context) = self.inline_context { + for node in inline_context.nodes.iter() { + if node.style.get_box().position != Position::Static { + return true; + } + } + } + false + } + + /// Returns true if this node is absolutely positioned. + pub fn is_absolutely_positioned(&self) -> bool { + self.style.get_box().position == Position::Absolute + } + + pub fn is_inline_absolute(&self) -> bool { + match self.specific { + SpecificFragmentInfo::InlineAbsolute(..) => true, + _ => false, + } + } + + pub fn meld_with_next_inline_fragment(&mut self, next_fragment: &Fragment) { + if let Some(ref mut inline_context_of_this_fragment) = self.inline_context { + if let Some(ref inline_context_of_next_fragment) = next_fragment.inline_context { + for ( + inline_context_node_from_this_fragment, + inline_context_node_from_next_fragment, + ) in inline_context_of_this_fragment + .nodes + .iter_mut() + .rev() + .zip(inline_context_of_next_fragment.nodes.iter().rev()) + { + if !inline_context_node_from_next_fragment + .flags + .contains(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT) + { + continue; + } + if inline_context_node_from_next_fragment.address != + inline_context_node_from_this_fragment.address + { + continue; + } + inline_context_node_from_this_fragment + .flags + .insert(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT); + } + } + } + } + + pub fn meld_with_prev_inline_fragment(&mut self, prev_fragment: &Fragment) { + if let Some(ref mut inline_context_of_this_fragment) = self.inline_context { + if let Some(ref inline_context_of_prev_fragment) = prev_fragment.inline_context { + for ( + inline_context_node_from_prev_fragment, + inline_context_node_from_this_fragment, + ) in inline_context_of_prev_fragment + .nodes + .iter() + .rev() + .zip(inline_context_of_this_fragment.nodes.iter_mut().rev()) + { + if !inline_context_node_from_prev_fragment + .flags + .contains(InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT) + { + continue; + } + if inline_context_node_from_prev_fragment.address != + inline_context_node_from_this_fragment.address + { + continue; + } + inline_context_node_from_this_fragment + .flags + .insert(InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT); + } + } + } + } + + /// Returns true if any of the inline styles associated with this fragment have + /// `vertical-align` set to `top` or `bottom`. + pub fn is_vertically_aligned_to_top_or_bottom(&self) -> bool { + fn is_top_or_bottom(v: &VerticalAlign) -> bool { + match *v { + VerticalAlign::Keyword(VerticalAlignKeyword::Top) | + VerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => true, + _ => false, + } + } + + if is_top_or_bottom(&self.style.get_box().vertical_align) { + return true; + } + + if let Some(ref inline_context) = self.inline_context { + for node in &inline_context.nodes { + if is_top_or_bottom(&node.style.get_box().vertical_align) { + return true; + } + } + } + false + } + + pub fn is_text_or_replaced(&self) -> bool { + match self.specific { + SpecificFragmentInfo::Generic | + SpecificFragmentInfo::InlineAbsolute(_) | + SpecificFragmentInfo::InlineAbsoluteHypothetical(_) | + SpecificFragmentInfo::InlineBlock(_) | + SpecificFragmentInfo::Multicol | + SpecificFragmentInfo::MulticolColumn | + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableColumn(_) | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TableWrapper => false, + SpecificFragmentInfo::Canvas(_) | + SpecificFragmentInfo::GeneratedContent(_) | + SpecificFragmentInfo::Iframe(_) | + SpecificFragmentInfo::Image(_) | + SpecificFragmentInfo::Media(_) | + SpecificFragmentInfo::ScannedText(_) | + SpecificFragmentInfo::TruncatedFragment(_) | + SpecificFragmentInfo::Svg(_) | + SpecificFragmentInfo::UnscannedText(_) => true, + } + } + + /// Returns the 4D matrix representing this fragment's transform. + pub fn transform_matrix( + &self, + stacking_relative_border_box: &Rect<Au>, + ) -> Option<LayoutTransform> { + let list = &self.style.get_box().transform; + let transform = LayoutTransform::from_untyped( + &list + .to_transform_3d_matrix(Some(stacking_relative_border_box)) + .ok()? + .0, + ); + + let transform_origin = &self.style.get_box().transform_origin; + let transform_origin_x = transform_origin + .horizontal + .to_used_value(stacking_relative_border_box.size.width) + .to_f32_px(); + let transform_origin_y = transform_origin + .vertical + .to_used_value(stacking_relative_border_box.size.height) + .to_f32_px(); + let transform_origin_z = transform_origin.depth.px(); + + let pre_transform = LayoutTransform::create_translation( + transform_origin_x, + transform_origin_y, + transform_origin_z, + ); + let post_transform = LayoutTransform::create_translation( + -transform_origin_x, + -transform_origin_y, + -transform_origin_z, + ); + + Some(pre_transform.pre_mul(&transform).pre_mul(&post_transform)) + } + + /// Returns the 4D matrix representing this fragment's perspective. + pub fn perspective_matrix( + &self, + stacking_relative_border_box: &Rect<Au>, + ) -> Option<LayoutTransform> { + match self.style().get_box().perspective { + Perspective::Length(length) => { + let perspective_origin = self.style().get_box().perspective_origin; + let perspective_origin = Point2D::new( + perspective_origin + .horizontal + .to_used_value(stacking_relative_border_box.size.width), + perspective_origin + .vertical + .to_used_value(stacking_relative_border_box.size.height), + ) + .to_layout(); + + let pre_transform = LayoutTransform::create_translation( + perspective_origin.x, + perspective_origin.y, + 0.0, + ); + let post_transform = LayoutTransform::create_translation( + -perspective_origin.x, + -perspective_origin.y, + 0.0, + ); + + let perspective_matrix = LayoutTransform::from_untyped( + &transform::create_perspective_matrix(length.px()), + ); + + Some( + pre_transform + .pre_mul(&perspective_matrix) + .pre_mul(&post_transform), + ) + }, + Perspective::None => None, + } + } +} + +impl fmt::Debug for Fragment { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let border_padding_string = if !self.border_padding.is_zero() { + format!("\nborder_padding={:?}", self.border_padding) + } else { + "".to_owned() + }; + + let margin_string = if !self.margin.is_zero() { + format!("\nmargin={:?}", self.margin) + } else { + "".to_owned() + }; + + let damage_string = if self.restyle_damage != RestyleDamage::empty() { + format!("\ndamage={:?}", self.restyle_damage) + } else { + "".to_owned() + }; + + write!( + f, + "\n{}({}) [{:?}]\nborder_box={:?}{}{}{}", + self.specific.get_type(), + self.debug_id, + self.specific, + self.border_box, + border_padding_string, + margin_string, + damage_string + ) + } +} + +bitflags! { + struct QuantitiesIncludedInIntrinsicInlineSizes: u8 { + const INTRINSIC_INLINE_SIZE_INCLUDES_MARGINS = 0x01; + const INTRINSIC_INLINE_SIZE_INCLUDES_PADDING = 0x02; + const INTRINSIC_INLINE_SIZE_INCLUDES_BORDER = 0x04; + const INTRINSIC_INLINE_SIZE_INCLUDES_SPECIFIED = 0x08; + } +} + +bitflags! { + // Various flags we can use when splitting fragments. See + // `calculate_split_position_using_breaking_strategy()`. + struct SplitOptions: u8 { + #[doc = "True if this is the first fragment on the line."] + const STARTS_LINE = 0x01; + #[doc = "True if we should attempt to split at character boundaries if this split fails. \ + This is used to implement `overflow-wrap: break-word`."] + const RETRY_AT_CHARACTER_BOUNDARIES = 0x02; + } +} + +/// A top-down fragment border box iteration handler. +pub trait FragmentBorderBoxIterator { + /// The operation to perform. + fn process(&mut self, fragment: &Fragment, level: i32, overflow: &Rect<Au>); + + /// Returns true if this fragment must be processed in-order. If this returns false, + /// we skip the operation for this fragment, but continue processing siblings. + fn should_process(&mut self, fragment: &Fragment) -> bool; +} + +/// The coordinate system used in `stacking_relative_border_box()`. See the documentation of that +/// method for details. +#[derive(Clone, Debug, PartialEq)] +pub enum CoordinateSystem { + /// The border box returned is relative to the fragment's parent stacking context. + Parent, + /// The border box returned is relative to the fragment's own stacking context, if applicable. + Own, +} + +pub struct InlineStyleIterator<'a> { + fragment: &'a Fragment, + inline_style_index: usize, + primary_style_yielded: bool, +} + +impl<'a> Iterator for InlineStyleIterator<'a> { + type Item = &'a ComputedValues; + + fn next(&mut self) -> Option<&'a ComputedValues> { + if !self.primary_style_yielded { + self.primary_style_yielded = true; + return Some(&*self.fragment.style); + } + let inline_context = self.fragment.inline_context.as_ref()?; + let inline_style_index = self.inline_style_index; + if inline_style_index == inline_context.nodes.len() { + return None; + } + self.inline_style_index += 1; + Some(&*inline_context.nodes[inline_style_index].style) + } +} + +impl<'a> InlineStyleIterator<'a> { + fn new(fragment: &Fragment) -> InlineStyleIterator { + InlineStyleIterator { + fragment: fragment, + inline_style_index: 0, + primary_style_yielded: false, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum WhitespaceStrippingResult { + RetainFragment, + FragmentContainedOnlyBidiControlCharacters, + FragmentContainedOnlyWhitespace, +} + +impl WhitespaceStrippingResult { + fn from_unscanned_text_fragment_info( + info: &UnscannedTextFragmentInfo, + ) -> WhitespaceStrippingResult { + if info.text.is_empty() { + WhitespaceStrippingResult::FragmentContainedOnlyWhitespace + } else if info.text.chars().all(gfx::text::util::is_bidi_control) { + WhitespaceStrippingResult::FragmentContainedOnlyBidiControlCharacters + } else { + WhitespaceStrippingResult::RetainFragment + } + } +} + +/// The overflow area. We need two different notions of overflow: paint overflow and scrollable +/// overflow. +#[derive(Clone, Copy, Debug)] +pub struct Overflow { + pub scroll: Rect<Au>, + pub paint: Rect<Au>, +} + +impl Overflow { + pub fn new() -> Overflow { + Overflow { + scroll: Rect::zero(), + paint: Rect::zero(), + } + } + + pub fn from_rect(border_box: &Rect<Au>) -> Overflow { + Overflow { + scroll: *border_box, + paint: *border_box, + } + } + + pub fn union(&mut self, other: &Overflow) { + self.scroll = self.scroll.union(&other.scroll); + self.paint = self.paint.union(&other.paint); + } + + pub fn translate(&mut self, by: &Vector2D<Au>) { + self.scroll = self.scroll.translate(by); + self.paint = self.paint.translate(by); + } +} + +bitflags! { + pub struct FragmentFlags: u8 { + // TODO(stshine): find a better name since these flags can also be used for grid item. + /// Whether this fragment represents a child in a row flex container. + const IS_INLINE_FLEX_ITEM = 0b0000_0001; + /// Whether this fragment represents a child in a column flex container. + const IS_BLOCK_FLEX_ITEM = 0b0000_0010; + /// Whether this fragment represents the generated text from a text-overflow clip. + const IS_ELLIPSIS = 0b0000_0100; + } +} + +/// Specified distances from the margin edge of a block to its content in the inline direction. +/// These are returned by `guess_inline_content_edge_offsets()` and are used in the float placement +/// speculation logic. +#[derive(Clone, Copy, Debug)] +pub struct SpeculatedInlineContentEdgeOffsets { + pub start: Au, + pub end: Au, +} + +#[cfg(not(debug_assertions))] +#[derive(Clone)] +struct DebugId; + +#[cfg(debug_assertions)] +#[derive(Clone)] +struct DebugId(u16); + +#[cfg(not(debug_assertions))] +impl DebugId { + pub fn new() -> DebugId { + DebugId + } +} + +#[cfg(debug_assertions)] +impl DebugId { + pub fn new() -> DebugId { + DebugId(layout_debug::generate_unique_debug_id()) + } +} + +#[cfg(not(debug_assertions))] +impl fmt::Display for DebugId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:p}", &self) + } +} + +#[cfg(debug_assertions)] +impl fmt::Display for DebugId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[cfg(not(debug_assertions))] +impl Serialize for DebugId { + fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + serializer.serialize_str(&format!("{:p}", &self)) + } +} + +#[cfg(debug_assertions)] +impl Serialize for DebugId { + fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + serializer.serialize_u16(self.0) + } +} diff --git a/components/layout_2020/generated_content.rs b/components/layout_2020/generated_content.rs new file mode 100644 index 00000000000..e1ebc570a62 --- /dev/null +++ b/components/layout_2020/generated_content.rs @@ -0,0 +1,651 @@ +/* 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/. */ + +//! The generated content assignment phase. +//! +//! This phase handles CSS counters, quotes, and ordered lists per CSS § 12.3-12.5. It cannot be +//! done in parallel and is therefore a sequential pass that runs on as little of the flow tree +//! as possible. + +use crate::context::{with_thread_local_font_context, LayoutContext}; +use crate::display_list::items::OpaqueNode; +use crate::flow::{Flow, FlowFlags, GetBaseFlow, ImmutableFlowUtils}; +use crate::fragment::{ + Fragment, GeneratedContentInfo, SpecificFragmentInfo, UnscannedTextFragmentInfo, +}; +use crate::text::TextRunScanner; +use crate::traversal::InorderFlowTraversal; +use script_layout_interface::wrapper_traits::PseudoElementType; +use smallvec::SmallVec; +use std::collections::{HashMap, LinkedList}; +use style::computed_values::display::T as Display; +use style::computed_values::list_style_type::T as ListStyleType; +use style::properties::ComputedValues; +use style::selector_parser::RestyleDamage; +use style::servo::restyle_damage::ServoRestyleDamage; +use style::values::generics::counters::ContentItem; + +// Decimal styles per CSS-COUNTER-STYLES § 6.1: +static DECIMAL: [char; 10] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; +// TODO(pcwalton): `decimal-leading-zero` +static ARABIC_INDIC: [char; 10] = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩']; +// TODO(pcwalton): `armenian`, `upper-armenian`, `lower-armenian` +static BENGALI: [char; 10] = [ + '০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯', +]; +static CAMBODIAN: [char; 10] = [ + '០', '១', '២', '៣', '៤', '៥', '៦', '៧', '៨', '៩', +]; +// TODO(pcwalton): Suffix for CJK decimal. +static CJK_DECIMAL: [char; 10] = [ + '〇', '一', '二', '三', '四', '五', '六', '七', '八', '九', +]; +static DEVANAGARI: [char; 10] = [ + '०', '१', '२', '३', '४', '५', '६', '७', '८', '९', +]; +// TODO(pcwalton): `georgian` +static GUJARATI: [char; 10] = [ + '૦', '૧', '૨', '૩', '૪', '૫', '૬', '૭', '૮', '૯', +]; +static GURMUKHI: [char; 10] = [ + '੦', '੧', '੨', '੩', '੪', '੫', '੬', '੭', '੮', '੯', +]; +// TODO(pcwalton): `hebrew` +static KANNADA: [char; 10] = [ + '೦', '೧', '೨', '೩', '೪', '೫', '೬', '೭', '೮', '೯', +]; +static LAO: [char; 10] = [ + '໐', '໑', '໒', '໓', '໔', '໕', '໖', '໗', '໘', '໙', +]; +static MALAYALAM: [char; 10] = [ + '൦', '൧', '൨', '൩', '൪', '൫', '൬', '൭', '൮', '൯', +]; +static MONGOLIAN: [char; 10] = [ + '᠐', '᠑', '᠒', '᠓', '᠔', '᠕', '᠖', '᠗', '᠘', '᠙', +]; +static MYANMAR: [char; 10] = [ + '၀', '၁', '၂', '၃', '၄', '၅', '၆', '၇', '၈', '၉', +]; +static ORIYA: [char; 10] = [ + '୦', '୧', '୨', '୩', '୪', '୫', '୬', '୭', '୮', '୯', +]; +static PERSIAN: [char; 10] = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; +// TODO(pcwalton): `lower-roman`, `upper-roman` +static TELUGU: [char; 10] = [ + '౦', '౧', '౨', '౩', '౪', '౫', '౬', '౭', '౮', '౯', +]; +static THAI: [char; 10] = [ + '๐', '๑', '๒', '๓', '๔', '๕', '๖', '๗', '๘', '๙', +]; +static TIBETAN: [char; 10] = [ + '༠', '༡', '༢', '༣', '༤', '༥', '༦', '༧', '༨', '༩', +]; + +// Alphabetic styles per CSS-COUNTER-STYLES § 6.2: +static LOWER_ALPHA: [char; 26] = [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', + 't', 'u', 'v', 'w', 'x', 'y', 'z', +]; +static UPPER_ALPHA: [char; 26] = [ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', +]; +static CJK_EARTHLY_BRANCH: [char; 12] = [ + '子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', +]; +static CJK_HEAVENLY_STEM: [char; 10] = [ + '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸', +]; +static LOWER_GREEK: [char; 24] = [ + 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', + 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω', +]; +static HIRAGANA: [char; 48] = [ + 'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ', 'し', 'す', + 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に', 'ぬ', 'ね', 'の', 'は', + 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む', 'め', 'も', 'や', 'ゆ', 'よ', 'ら', + 'り', 'る', 'れ', 'ろ', 'わ', 'ゐ', 'ゑ', 'を', 'ん', +]; +static HIRAGANA_IROHA: [char; 47] = [ + 'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る', 'を', 'わ', + 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら', 'む', 'う', 'ゐ', 'の', + 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ', 'え', 'て', 'あ', 'さ', 'き', 'ゆ', + 'め', 'み', 'し', 'ゑ', 'ひ', 'も', 'せ', 'す', +]; +static KATAKANA: [char; 48] = [ + 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', + 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', + 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', + 'リ', 'ル', 'レ', 'ロ', 'ワ', 'ヰ', 'ヱ', 'ヲ', 'ン', +]; +static KATAKANA_IROHA: [char; 47] = [ + 'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル', 'ヲ', 'ワ', + 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ', 'ム', 'ウ', 'ヰ', 'ノ', + 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ', 'エ', 'テ', 'ア', 'サ', 'キ', 'ユ', + 'メ', 'ミ', 'シ', 'ヱ', 'ヒ', 'モ', 'セ', 'ス', +]; + +/// The generated content resolution traversal. +pub struct ResolveGeneratedContent<'a> { + /// The layout context. + layout_context: &'a LayoutContext<'a>, + /// The counter representing an ordered list item. + list_item: Counter, + /// Named CSS counters. + counters: HashMap<String, Counter>, + /// The level of quote nesting. + quote: u32, +} + +impl<'a> ResolveGeneratedContent<'a> { + /// Creates a new generated content resolution traversal. + pub fn new(layout_context: &'a LayoutContext) -> ResolveGeneratedContent<'a> { + ResolveGeneratedContent { + layout_context: layout_context, + list_item: Counter::new(), + counters: HashMap::new(), + quote: 0, + } + } +} + +impl<'a> InorderFlowTraversal for ResolveGeneratedContent<'a> { + #[inline] + fn process(&mut self, flow: &mut dyn Flow, level: u32) { + let mut mutator = ResolveGeneratedContentFragmentMutator { + traversal: self, + level: level, + is_block: flow.is_block_like(), + incremented: false, + }; + flow.mutate_fragments(&mut |fragment| mutator.mutate_fragment(fragment)) + } + + #[inline] + fn should_process_subtree(&mut self, flow: &mut dyn Flow) -> bool { + flow.base() + .restyle_damage + .intersects(ServoRestyleDamage::RESOLVE_GENERATED_CONTENT) || + flow.base().flags.intersects( + FlowFlags::AFFECTS_COUNTERS | FlowFlags::HAS_COUNTER_AFFECTING_CHILDREN, + ) + } +} + +/// The object that mutates the generated content fragments. +struct ResolveGeneratedContentFragmentMutator<'a, 'b: 'a> { + /// The traversal. + traversal: &'a mut ResolveGeneratedContent<'b>, + /// The level we're at in the flow tree. + level: u32, + /// Whether this flow is a block flow. + is_block: bool, + /// Whether we've incremented the counter yet. + incremented: bool, +} + +impl<'a, 'b> ResolveGeneratedContentFragmentMutator<'a, 'b> { + fn mutate_fragment(&mut self, fragment: &mut Fragment) { + // We only reset and/or increment counters once per flow. This avoids double-incrementing + // counters on list items (once for the main fragment and once for the marker). + if !self.incremented { + self.reset_and_increment_counters_as_necessary(fragment); + } + + let mut list_style_type = fragment.style().get_list().list_style_type; + if fragment.style().get_box().display != Display::ListItem { + list_style_type = ListStyleType::None + } + + let mut new_info = None; + { + let info = + if let SpecificFragmentInfo::GeneratedContent(ref mut info) = fragment.specific { + info + } else { + return; + }; + + match **info { + GeneratedContentInfo::ListItem => { + new_info = self.traversal.list_item.render( + self.traversal.layout_context, + fragment.node, + fragment.pseudo.clone(), + fragment.style.clone(), + list_style_type, + RenderingMode::Suffix(".\u{00a0}"), + ) + }, + GeneratedContentInfo::Empty | + GeneratedContentInfo::ContentItem(ContentItem::String(_)) => { + // Nothing to do here. + }, + GeneratedContentInfo::ContentItem(ContentItem::Counter( + ref counter_name, + counter_style, + )) => { + let temporary_counter = Counter::new(); + let counter = self + .traversal + .counters + .get(&*counter_name.0) + .unwrap_or(&temporary_counter); + new_info = counter.render( + self.traversal.layout_context, + fragment.node, + fragment.pseudo.clone(), + fragment.style.clone(), + counter_style, + RenderingMode::Plain, + ) + }, + GeneratedContentInfo::ContentItem(ContentItem::Counters( + ref counter_name, + ref separator, + counter_style, + )) => { + let temporary_counter = Counter::new(); + let counter = self + .traversal + .counters + .get(&*counter_name.0) + .unwrap_or(&temporary_counter); + new_info = counter.render( + self.traversal.layout_context, + fragment.node, + fragment.pseudo, + fragment.style.clone(), + counter_style, + RenderingMode::All(&separator), + ); + }, + GeneratedContentInfo::ContentItem(ContentItem::OpenQuote) => { + new_info = render_text( + self.traversal.layout_context, + fragment.node, + fragment.pseudo, + fragment.style.clone(), + self.quote(&*fragment.style, false), + ); + self.traversal.quote += 1 + }, + GeneratedContentInfo::ContentItem(ContentItem::CloseQuote) => { + if self.traversal.quote >= 1 { + self.traversal.quote -= 1 + } + + new_info = render_text( + self.traversal.layout_context, + fragment.node, + fragment.pseudo, + fragment.style.clone(), + self.quote(&*fragment.style, true), + ); + }, + GeneratedContentInfo::ContentItem(ContentItem::NoOpenQuote) => { + self.traversal.quote += 1 + }, + GeneratedContentInfo::ContentItem(ContentItem::NoCloseQuote) => { + if self.traversal.quote >= 1 { + self.traversal.quote -= 1 + } + }, + GeneratedContentInfo::ContentItem(ContentItem::Url(..)) => { + unreachable!("Servo doesn't parse content: url(..) yet") + }, + } + }; + + fragment.specific = match new_info { + Some(new_info) => new_info, + // If the fragment did not generate any content, replace it with a no-op placeholder + // so that it isn't processed again on the next layout. FIXME (mbrubeck): When + // processing an inline flow, this traversal should be allowed to insert or remove + // fragments. Then we can just remove these fragments rather than adding placeholders. + None => SpecificFragmentInfo::GeneratedContent(Box::new(GeneratedContentInfo::Empty)), + }; + } + + fn reset_and_increment_counters_as_necessary(&mut self, fragment: &mut Fragment) { + let mut list_style_type = fragment.style().get_list().list_style_type; + if !self.is_block || fragment.style().get_box().display != Display::ListItem { + list_style_type = ListStyleType::None + } + + match list_style_type { + ListStyleType::Disc | + ListStyleType::None | + ListStyleType::Circle | + ListStyleType::Square | + ListStyleType::DisclosureOpen | + ListStyleType::DisclosureClosed => {}, + _ => self.traversal.list_item.increment(self.level, 1), + } + + // Truncate down counters. + for (_, counter) in &mut self.traversal.counters { + counter.truncate_to_level(self.level); + } + self.traversal.list_item.truncate_to_level(self.level); + + for pair in &*fragment.style().get_counters().counter_reset { + let counter_name = &*pair.name.0; + if let Some(ref mut counter) = self.traversal.counters.get_mut(counter_name) { + counter.reset(self.level, pair.value); + continue; + } + + let mut counter = Counter::new(); + counter.reset(self.level, pair.value); + self.traversal + .counters + .insert(counter_name.to_owned(), counter); + } + + for pair in &*fragment.style().get_counters().counter_increment { + let counter_name = &*pair.name.0; + if let Some(ref mut counter) = self.traversal.counters.get_mut(counter_name) { + counter.increment(self.level, pair.value); + continue; + } + + let mut counter = Counter::new(); + counter.increment(self.level, pair.value); + self.traversal + .counters + .insert(counter_name.to_owned(), counter); + } + + self.incremented = true + } + + fn quote(&self, style: &ComputedValues, close: bool) -> String { + let quotes = &style.get_list().quotes; + if quotes.0.is_empty() { + return String::new(); + } + let pair = if self.traversal.quote as usize >= quotes.0.len() { + quotes.0.last().unwrap() + } else { + "es.0[self.traversal.quote as usize] + }; + if close { + pair.closing.to_string() + } else { + pair.opening.to_string() + } + } +} + +/// A counter per CSS 2.1 § 12.4. +struct Counter { + /// The values at each level. + values: Vec<CounterValue>, +} + +impl Counter { + fn new() -> Counter { + Counter { values: Vec::new() } + } + + fn reset(&mut self, level: u32, value: i32) { + // Do we have an instance of the counter at this level? If so, just mutate it. + if let Some(ref mut existing_value) = self.values.last_mut() { + if level == existing_value.level { + existing_value.value = value; + return; + } + } + + // Otherwise, push a new instance of the counter. + self.values.push(CounterValue { + level: level, + value: value, + }) + } + + fn truncate_to_level(&mut self, level: u32) { + if let Some(position) = self.values.iter().position(|value| value.level > level) { + self.values.truncate(position) + } + } + + fn increment(&mut self, level: u32, amount: i32) { + if let Some(ref mut value) = self.values.last_mut() { + value.value += amount; + return; + } + + self.values.push(CounterValue { + level: level, + value: amount, + }) + } + + fn render( + &self, + layout_context: &LayoutContext, + node: OpaqueNode, + pseudo: PseudoElementType, + style: crate::ServoArc<ComputedValues>, + list_style_type: ListStyleType, + mode: RenderingMode, + ) -> Option<SpecificFragmentInfo> { + let mut string = String::new(); + match mode { + RenderingMode::Plain => { + let value = match self.values.last() { + Some(ref value) => value.value, + None => 0, + }; + push_representation(value, list_style_type, &mut string) + }, + RenderingMode::Suffix(suffix) => { + let value = match self.values.last() { + Some(ref value) => value.value, + None => 0, + }; + push_representation(value, list_style_type, &mut string); + string.push_str(suffix) + }, + RenderingMode::All(separator) => { + let mut first = true; + for value in &self.values { + if !first { + string.push_str(separator) + } + first = false; + push_representation(value.value, list_style_type, &mut string) + } + }, + } + + if string.is_empty() { + None + } else { + render_text(layout_context, node, pseudo, style, string) + } + } +} + +/// How a counter value is to be rendered. +enum RenderingMode<'a> { + /// The innermost counter value is rendered with no extra decoration. + Plain, + /// The innermost counter value is rendered with the given string suffix. + Suffix(&'a str), + /// All values of the counter are rendered with the given separator string between them. + All(&'a str), +} + +/// The value of a counter at a given level. +struct CounterValue { + /// The level of the flow tree that this corresponds to. + level: u32, + /// The value of the counter at this level. + value: i32, +} + +/// Creates fragment info for a literal string. +fn render_text( + layout_context: &LayoutContext, + node: OpaqueNode, + pseudo: PseudoElementType, + style: crate::ServoArc<ComputedValues>, + string: String, +) -> Option<SpecificFragmentInfo> { + let mut fragments = LinkedList::new(); + let info = SpecificFragmentInfo::UnscannedText(Box::new(UnscannedTextFragmentInfo::new( + string.into_boxed_str(), + None, + ))); + fragments.push_back(Fragment::from_opaque_node_and_style( + node, + pseudo, + style.clone(), + style, + RestyleDamage::rebuild_and_reflow(), + info, + )); + // FIXME(pcwalton): This should properly handle multiple marker fragments. This could happen + // due to text run splitting. + let fragments = with_thread_local_font_context(layout_context, |font_context| { + TextRunScanner::new().scan_for_runs(font_context, fragments) + }); + if fragments.is_empty() { + None + } else { + Some(fragments.fragments.into_iter().next().unwrap().specific) + } +} + +/// Appends string that represents the value rendered using the system appropriate for the given +/// `list-style-type` onto the given string. +fn push_representation(value: i32, list_style_type: ListStyleType, accumulator: &mut String) { + match list_style_type { + ListStyleType::None => {}, + ListStyleType::Disc | + ListStyleType::Circle | + ListStyleType::Square | + ListStyleType::DisclosureOpen | + ListStyleType::DisclosureClosed => accumulator.push(static_representation(list_style_type)), + ListStyleType::Decimal => push_numeric_representation(value, &DECIMAL, accumulator), + ListStyleType::ArabicIndic => { + push_numeric_representation(value, &ARABIC_INDIC, accumulator) + }, + ListStyleType::Bengali => push_numeric_representation(value, &BENGALI, accumulator), + ListStyleType::Cambodian | ListStyleType::Khmer => { + push_numeric_representation(value, &CAMBODIAN, accumulator) + }, + ListStyleType::CjkDecimal => push_numeric_representation(value, &CJK_DECIMAL, accumulator), + ListStyleType::Devanagari => push_numeric_representation(value, &DEVANAGARI, accumulator), + ListStyleType::Gujarati => push_numeric_representation(value, &GUJARATI, accumulator), + ListStyleType::Gurmukhi => push_numeric_representation(value, &GURMUKHI, accumulator), + ListStyleType::Kannada => push_numeric_representation(value, &KANNADA, accumulator), + ListStyleType::Lao => push_numeric_representation(value, &LAO, accumulator), + ListStyleType::Malayalam => push_numeric_representation(value, &MALAYALAM, accumulator), + ListStyleType::Mongolian => push_numeric_representation(value, &MONGOLIAN, accumulator), + ListStyleType::Myanmar => push_numeric_representation(value, &MYANMAR, accumulator), + ListStyleType::Oriya => push_numeric_representation(value, &ORIYA, accumulator), + ListStyleType::Persian => push_numeric_representation(value, &PERSIAN, accumulator), + ListStyleType::Telugu => push_numeric_representation(value, &TELUGU, accumulator), + ListStyleType::Thai => push_numeric_representation(value, &THAI, accumulator), + ListStyleType::Tibetan => push_numeric_representation(value, &TIBETAN, accumulator), + ListStyleType::LowerAlpha => { + push_alphabetic_representation(value, &LOWER_ALPHA, accumulator) + }, + ListStyleType::UpperAlpha => { + push_alphabetic_representation(value, &UPPER_ALPHA, accumulator) + }, + ListStyleType::CjkEarthlyBranch => { + push_alphabetic_representation(value, &CJK_EARTHLY_BRANCH, accumulator) + }, + ListStyleType::CjkHeavenlyStem => { + push_alphabetic_representation(value, &CJK_HEAVENLY_STEM, accumulator) + }, + ListStyleType::LowerGreek => { + push_alphabetic_representation(value, &LOWER_GREEK, accumulator) + }, + ListStyleType::Hiragana => push_alphabetic_representation(value, &HIRAGANA, accumulator), + ListStyleType::HiraganaIroha => { + push_alphabetic_representation(value, &HIRAGANA_IROHA, accumulator) + }, + ListStyleType::Katakana => push_alphabetic_representation(value, &KATAKANA, accumulator), + ListStyleType::KatakanaIroha => { + push_alphabetic_representation(value, &KATAKANA_IROHA, accumulator) + }, + } +} + +/// Returns the static character that represents the value rendered using the given list-style, if +/// possible. +pub fn static_representation(list_style_type: ListStyleType) -> char { + match list_style_type { + ListStyleType::Disc => '•', + ListStyleType::Circle => '◦', + ListStyleType::Square => '▪', + ListStyleType::DisclosureOpen => '▾', + ListStyleType::DisclosureClosed => '‣', + _ => panic!("No static representation for this list-style-type!"), + } +} + +/// Pushes the string that represents the value rendered using the given *alphabetic system* onto +/// the accumulator per CSS-COUNTER-STYLES § 3.1.4. +fn push_alphabetic_representation(value: i32, system: &[char], accumulator: &mut String) { + let mut abs_value = handle_negative_value(value, accumulator); + + let mut string: SmallVec<[char; 8]> = SmallVec::new(); + while abs_value != 0 { + // Step 1. + abs_value = abs_value - 1; + // Step 2. + string.push(system[abs_value % system.len()]); + // Step 3. + abs_value = abs_value / system.len(); + } + + accumulator.extend(string.iter().cloned().rev()) +} + +/// Pushes the string that represents the value rendered using the given *numeric system* onto the +/// accumulator per CSS-COUNTER-STYLES § 3.1.5. +fn push_numeric_representation(value: i32, system: &[char], accumulator: &mut String) { + let mut abs_value = handle_negative_value(value, accumulator); + + // Step 1. + if abs_value == 0 { + accumulator.push(system[0]); + return; + } + + // Step 2. + let mut string: SmallVec<[char; 8]> = SmallVec::new(); + while abs_value != 0 { + // Step 2.1. + string.push(system[abs_value % system.len()]); + // Step 2.2. + abs_value = abs_value / system.len(); + } + + // Step 3. + accumulator.extend(string.iter().cloned().rev()) +} + +/// If the system uses a negative sign, handle negative values per CSS-COUNTER-STYLES § 2. +/// +/// Returns the absolute value of the counter. +fn handle_negative_value(value: i32, accumulator: &mut String) -> usize { + // 3. If the counter value is negative and the counter style uses a negative sign, instead + // generate an initial representation using the absolute value of the counter value. + if value < 0 { + // TODO: Support different negative signs using the 'negative' descriptor. + // https://drafts.csswg.org/date/2015-07-16/css-counter-styles/#counter-style-negative + accumulator.push('-'); + value.abs() as usize + } else { + value as usize + } +} diff --git a/components/layout_2020/incremental.rs b/components/layout_2020/incremental.rs new file mode 100644 index 00000000000..4e313f088bb --- /dev/null +++ b/components/layout_2020/incremental.rs @@ -0,0 +1,101 @@ +/* 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/. */ + +use crate::flow::{Flow, FlowFlags, GetBaseFlow}; +use style::computed_values::float::T as Float; +use style::selector_parser::RestyleDamage; +use style::servo::restyle_damage::ServoRestyleDamage; + +/// Used in a flow traversal to indicate whether this re-layout should be incremental or not. +#[derive(Clone, Copy, PartialEq)] +pub enum RelayoutMode { + Incremental, + Force, +} + +bitflags! { + pub struct SpecialRestyleDamage: u8 { + #[doc = "If this flag is set, we need to reflow the entire document. This is more or less a \ + temporary hack to deal with cases that we don't handle incrementally yet."] + const REFLOW_ENTIRE_DOCUMENT = 0x01; + } +} + +impl dyn Flow { + pub fn compute_layout_damage(&mut self) -> SpecialRestyleDamage { + let mut special_damage = SpecialRestyleDamage::empty(); + let is_absolutely_positioned = self + .base() + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED); + + // In addition to damage, we use this phase to compute whether nodes affect CSS counters. + let mut has_counter_affecting_children = false; + + { + let self_base = self.mut_base(); + // Take a snapshot of the parent damage before updating it with damage from children. + let parent_damage = self_base.restyle_damage; + + for kid in self_base.children.iter_mut() { + let child_is_absolutely_positioned = kid + .base() + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED); + kid.mut_base().restyle_damage.insert( + parent_damage + .damage_for_child(is_absolutely_positioned, child_is_absolutely_positioned), + ); + { + let kid: &mut dyn Flow = kid; + special_damage.insert(kid.compute_layout_damage()); + } + self_base.restyle_damage.insert( + kid.base() + .restyle_damage + .damage_for_parent(child_is_absolutely_positioned), + ); + + has_counter_affecting_children = has_counter_affecting_children || + kid.base().flags.intersects( + FlowFlags::AFFECTS_COUNTERS | FlowFlags::HAS_COUNTER_AFFECTING_CHILDREN, + ); + } + } + + let self_base = self.mut_base(); + if self_base.flags.float_kind() != Float::None && + self_base + .restyle_damage + .intersects(ServoRestyleDamage::REFLOW) + { + special_damage.insert(SpecialRestyleDamage::REFLOW_ENTIRE_DOCUMENT); + } + + if has_counter_affecting_children { + self_base + .flags + .insert(FlowFlags::HAS_COUNTER_AFFECTING_CHILDREN) + } else { + self_base + .flags + .remove(FlowFlags::HAS_COUNTER_AFFECTING_CHILDREN) + } + + special_damage + } + + pub fn reflow_entire_document(&mut self) { + let self_base = self.mut_base(); + self_base + .restyle_damage + .insert(RestyleDamage::rebuild_and_reflow()); + self_base + .restyle_damage + .remove(ServoRestyleDamage::RECONSTRUCT_FLOW); + for kid in self_base.children.iter_mut() { + kid.reflow_entire_document(); + } + } +} diff --git a/components/layout_2020/inline.rs b/components/layout_2020/inline.rs new file mode 100644 index 00000000000..f4dacefc5ef --- /dev/null +++ b/components/layout_2020/inline.rs @@ -0,0 +1,2220 @@ +/* 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/. */ + +use crate::block::AbsoluteAssignBSizesTraversal; +use crate::context::{LayoutContext, LayoutFontContext}; +use crate::display_list::items::{DisplayListSection, OpaqueNode}; +use crate::display_list::{ + BorderPaintingMode, DisplayListBuildState, StackingContextCollectionState, +}; +use crate::floats::{FloatKind, Floats, PlacementInfo}; +use crate::flow::{BaseFlow, Flow, FlowClass, ForceNonfloatedFlag}; +use crate::flow::{EarlyAbsolutePositionInfo, FlowFlags, GetBaseFlow, OpaqueFlow}; +use crate::flow_ref::FlowRef; +use crate::fragment::FragmentFlags; +use crate::fragment::SpecificFragmentInfo; +use crate::fragment::{CoordinateSystem, Fragment, FragmentBorderBoxIterator, Overflow}; +use crate::layout_debug; +use crate::model::IntrinsicISizesContribution; +use crate::text; +use crate::traversal::PreorderFlowTraversal; +use crate::ServoArc; +use app_units::{Au, MIN_AU}; +use euclid::{Point2D, Rect, Size2D}; +use gfx::font::FontMetrics; +use gfx_traits::print_tree::PrintTree; +use range::{Range, RangeIndex}; +use script_layout_interface::wrapper_traits::PseudoElementType; +use servo_geometry::MaxRect; +use std::cmp::max; +use std::collections::VecDeque; +use std::sync::Arc; +use std::{fmt, i32, isize, mem}; +use style::computed_values::display::T as Display; +use style::computed_values::overflow_x::T as StyleOverflow; +use style::computed_values::position::T as Position; +use style::computed_values::text_align::T as TextAlign; +use style::computed_values::text_justify::T as TextJustify; +use style::computed_values::white_space::T as WhiteSpace; +use style::logical_geometry::{LogicalRect, LogicalSize, WritingMode}; +use style::properties::ComputedValues; +use style::servo::restyle_damage::ServoRestyleDamage; +use style::values::computed::box_::VerticalAlign; +use style::values::generics::box_::VerticalAlignKeyword; +use style::values::specified::text::TextOverflowSide; +use unicode_bidi as bidi; + +/// `Line`s are represented as offsets into the child list, rather than +/// as an object that "owns" fragments. Choosing a different set of line +/// breaks requires a new list of offsets, and possibly some splitting and +/// merging of TextFragments. +/// +/// A similar list will keep track of the mapping between CSS fragments and +/// the corresponding fragments in the inline flow. +/// +/// After line breaks are determined, render fragments in the inline flow may +/// overlap visually. For example, in the case of nested inline CSS fragments, +/// outer inlines must be at least as large as the inner inlines, for +/// purposes of drawing noninherited things like backgrounds, borders, +/// outlines. +/// +/// N.B. roc has an alternative design where the list instead consists of +/// things like "start outer fragment, text, start inner fragment, text, end inner +/// fragment, text, end outer fragment, text". This seems a little complicated to +/// serve as the starting point, but the current design doesn't make it +/// hard to try out that alternative. +/// +/// Line fragments also contain some metadata used during line breaking. The +/// green zone is the area that the line can expand to before it collides +/// with a float or a horizontal wall of the containing block. The block-start +/// inline-start corner of the green zone is the same as that of the line, but +/// the green zone can be taller and wider than the line itself. +#[derive(Clone, Debug, Serialize)] +pub struct Line { + /// A range of line indices that describe line breaks. + /// + /// For example, consider the following HTML and rendered element with + /// linebreaks: + /// + /// ~~~html + /// <span>I <span>like truffles, <img></span> yes I do.</span> + /// ~~~ + /// + /// ~~~text + /// +------------+ + /// | I like | + /// | truffles, | + /// | +----+ | + /// | | | | + /// | +----+ yes | + /// | I do. | + /// +------------+ + /// ~~~ + /// + /// The ranges that describe these lines would be: + /// + /// | [0, 2) | [2, 3) | [3, 5) | [5, 6) | + /// |----------|-------------|-------------|----------| + /// | 'I like' | 'truffles,' | '<img> yes' | 'I do.' | + pub range: Range<FragmentIndex>, + + /// The bidirectional embedding level runs for this line, in visual order. + /// + /// Can be set to `None` if the line is 100% left-to-right. + pub visual_runs: Option<Vec<(Range<FragmentIndex>, bidi::Level)>>, + + /// The bounds are the exact position and extents of the line with respect + /// to the parent box. + /// + /// For example, for the HTML below... + /// + /// ~~~html + /// <div><span>I like truffles, <img></span></div> + /// ~~~ + /// + /// ...the bounds would be: + /// + /// ~~~text + /// +-----------------------------------------------------------+ + /// | ^ | + /// | | | + /// | origin.y | + /// | | | + /// | v | + /// |< - origin.x ->+ - - - - - - - - +---------+---- | + /// | | | | ^ | + /// | | | <img> | size.block | + /// | I like truffles, | | v | + /// | + - - - - - - - - +---------+---- | + /// | | | | + /// | |<------ size.inline ------>| | + /// | | + /// | | + /// +-----------------------------------------------------------+ + /// ~~~ + pub bounds: LogicalRect<Au>, + + /// The green zone is the greatest extent from which a line can extend to + /// before it collides with a float. + /// + /// ~~~text + /// +-----------------------+ + /// |::::::::::::::::: | + /// |:::::::::::::::::FFFFFF| + /// |============:::::FFFFFF| + /// |:::::::::::::::::FFFFFF| + /// |:::::::::::::::::FFFFFF| + /// |::::::::::::::::: | + /// | FFFFFFFFF | + /// | FFFFFFFFF | + /// | FFFFFFFFF | + /// | | + /// +-----------------------+ + /// + /// === line + /// ::: green zone + /// FFF float + /// ~~~ + pub green_zone: LogicalSize<Au>, + + /// The minimum metrics for this line, as specified by the style. + pub minimum_metrics: LineMetrics, + + /// The actual metrics for this line. + pub metrics: LineMetrics, +} + +impl Line { + fn new(writing_mode: WritingMode, minimum_metrics: &LineMetrics) -> Line { + Line { + range: Range::empty(), + visual_runs: None, + bounds: LogicalRect::zero(writing_mode), + green_zone: LogicalSize::zero(writing_mode), + minimum_metrics: *minimum_metrics, + metrics: *minimum_metrics, + } + } + + /// Returns the new metrics that this line would have if `new_fragment` were added to it. + /// + /// FIXME(pcwalton): this assumes that the tallest fragment in the line determines the line + /// block-size. This might not be the case with some weird text fonts. + fn new_metrics_for_fragment( + &self, + new_fragment: &Fragment, + layout_context: &LayoutContext, + ) -> LineMetrics { + if !new_fragment.is_vertically_aligned_to_top_or_bottom() { + let fragment_inline_metrics = + new_fragment.aligned_inline_metrics(layout_context, &self.minimum_metrics, None); + self.metrics + .new_metrics_for_fragment(&fragment_inline_metrics) + } else { + self.metrics + } + } + + /// Returns the new block size that this line would have if `new_fragment` were added to it. + /// `new_inline_metrics` represents the new inline metrics that this line would have; it can + /// be computed with `new_inline_metrics()`. + fn new_block_size_for_fragment( + &self, + new_fragment: &Fragment, + new_line_metrics: &LineMetrics, + layout_context: &LayoutContext, + ) -> Au { + let new_block_size = if new_fragment.is_vertically_aligned_to_top_or_bottom() { + max( + new_fragment + .aligned_inline_metrics(layout_context, &self.minimum_metrics, None) + .space_needed(), + self.minimum_metrics.space_needed(), + ) + } else { + new_line_metrics.space_needed() + }; + max(self.bounds.size.block, new_block_size) + } +} + +int_range_index! { + #[derive(Serialize)] + #[doc = "The index of a fragment in a flattened vector of DOM elements."] + struct FragmentIndex(isize) +} + +/// Arranges fragments into lines, splitting them up as necessary. +struct LineBreaker { + /// The floats we need to flow around. + floats: Floats, + /// The resulting fragment list for the flow, consisting of possibly-broken fragments. + new_fragments: Vec<Fragment>, + /// The next fragment or fragments that we need to work on. + work_list: VecDeque<Fragment>, + /// The line we're currently working on. + pending_line: Line, + /// The lines we've already committed. + lines: Vec<Line>, + /// The index of the last known good line breaking opportunity. The opportunity will either + /// be inside this fragment (if it is splittable) or immediately prior to it. + last_known_line_breaking_opportunity: Option<FragmentIndex>, + /// The current position in the block direction. + cur_b: Au, + /// The computed value of the indentation for the first line (`text-indent`, CSS 2.1 § 16.1). + first_line_indentation: Au, + /// The minimum metrics for each line, as specified by the line height and font style. + minimum_metrics: LineMetrics, +} + +impl LineBreaker { + /// Creates a new `LineBreaker` with a set of floats and the indentation of the first line. + fn new( + float_context: Floats, + first_line_indentation: Au, + minimum_line_metrics: &LineMetrics, + ) -> LineBreaker { + LineBreaker { + new_fragments: Vec::new(), + work_list: VecDeque::new(), + pending_line: Line::new(float_context.writing_mode, minimum_line_metrics), + floats: float_context, + lines: Vec::new(), + cur_b: Au(0), + last_known_line_breaking_opportunity: None, + first_line_indentation: first_line_indentation, + minimum_metrics: *minimum_line_metrics, + } + } + + /// Resets the `LineBreaker` to the initial state it had after a call to `new`. + fn reset_scanner(&mut self) { + self.lines = Vec::new(); + self.new_fragments = Vec::new(); + self.cur_b = Au(0); + self.reset_line(); + } + + /// Reinitializes the pending line to blank data. + fn reset_line(&mut self) { + self.last_known_line_breaking_opportunity = None; + // https://github.com/rust-lang/rust/issues/49282 + self.pending_line = Line::new(self.floats.writing_mode, &self.minimum_metrics); + } + + /// Reflows fragments for the given inline flow. + fn scan_for_lines(&mut self, flow: &mut InlineFlow, layout_context: &LayoutContext) { + self.reset_scanner(); + + // Create our fragment iterator. + debug!( + "LineBreaker: scanning for lines, {} fragments", + flow.fragments.len() + ); + let mut old_fragments = mem::replace(&mut flow.fragments, InlineFragments::new()); + let old_fragment_iter = old_fragments.fragments.into_iter(); + + // TODO(pcwalton): This would likely be better as a list of dirty line + // indices. That way we could resynchronize if we discover during reflow + // that all subsequent fragments must have the same position as they had + // in the previous reflow. I don't know how common this case really is + // in practice, but it's probably worth handling. + self.lines = Vec::new(); + + // Do the reflow. + self.reflow_fragments(old_fragment_iter, flow, layout_context); + + // Perform unicode bidirectional layout. + let para_level = flow.base.writing_mode.to_bidi_level(); + + // The text within a fragment is at a single bidi embedding level + // (because we split fragments on level run boundaries during flow + // construction), so we can build a level array with just one entry per + // fragment. + let levels: Vec<bidi::Level> = self + .new_fragments + .iter() + .map(|fragment| match fragment.specific { + SpecificFragmentInfo::ScannedText(ref info) => info.run.bidi_level, + _ => para_level, + }) + .collect(); + + let mut lines = mem::replace(&mut self.lines, Vec::new()); + + // If everything is LTR, don't bother with reordering. + if bidi::level::has_rtl(&levels) { + // Compute and store the visual ordering of the fragments within the + // line. + for line in &mut lines { + let range = line.range.begin().to_usize()..line.range.end().to_usize(); + // FIXME: Update to use BidiInfo::visual_runs, as this algorithm needs access to + // the original text and original BidiClass of its characters. + #[allow(deprecated)] + let runs = bidi::deprecated::visual_runs(range, &levels); + line.visual_runs = Some( + runs.iter() + .map(|run| { + let start = FragmentIndex(run.start as isize); + let len = FragmentIndex(run.len() as isize); + (Range::new(start, len), levels[run.start]) + }) + .collect(), + ); + } + } + + // Place the fragments back into the flow. + old_fragments.fragments = mem::replace(&mut self.new_fragments, vec![]); + flow.fragments = old_fragments; + flow.lines = lines; + } + + /// Reflows the given fragments, which have been plucked out of the inline flow. + fn reflow_fragments<'a, I>( + &mut self, + mut old_fragment_iter: I, + flow: &'a InlineFlow, + layout_context: &LayoutContext, + ) where + I: Iterator<Item = Fragment>, + { + loop { + // Acquire the next fragment to lay out from the work list or fragment list, as + // appropriate. + let fragment = match self.next_unbroken_fragment(&mut old_fragment_iter) { + None => break, + Some(fragment) => fragment, + }; + + // Do not reflow truncated fragments. Reflow the original fragment only. + let fragment = if fragment.flags.contains(FragmentFlags::IS_ELLIPSIS) { + continue; + } else if let SpecificFragmentInfo::TruncatedFragment(info) = fragment.specific { + info.full + } else { + fragment + }; + + // Try to append the fragment. + self.reflow_fragment(fragment, flow, layout_context); + } + + if !self.pending_line_is_empty() { + debug!( + "LineBreaker: partially full line {} at end of scanning; committing it", + self.lines.len() + ); + self.flush_current_line() + } + } + + /// Acquires a new fragment to lay out from the work list or fragment list as appropriate. + /// Note that you probably don't want to call this method directly in order to be incremental- + /// reflow-safe; try `next_unbroken_fragment` instead. + fn next_fragment<I>(&mut self, old_fragment_iter: &mut I) -> Option<Fragment> + where + I: Iterator<Item = Fragment>, + { + self.work_list + .pop_front() + .or_else(|| old_fragment_iter.next()) + } + + /// Acquires a new fragment to lay out from the work list or fragment list, + /// merging it with any subsequent fragments as appropriate. In effect, what + /// this method does is to return the next fragment to lay out, undoing line + /// break operations that any previous reflows may have performed. You + /// probably want to be using this method instead of `next_fragment`. + fn next_unbroken_fragment<I>(&mut self, old_fragment_iter: &mut I) -> Option<Fragment> + where + I: Iterator<Item = Fragment>, + { + let mut result = self.next_fragment(old_fragment_iter)?; + + loop { + let candidate = match self.next_fragment(old_fragment_iter) { + None => return Some(result), + Some(fragment) => fragment, + }; + + let need_to_merge = match (&mut result.specific, &candidate.specific) { + ( + &mut SpecificFragmentInfo::ScannedText(ref mut result_info), + &SpecificFragmentInfo::ScannedText(ref candidate_info), + ) => { + result.margin.inline_end == Au(0) && + candidate.margin.inline_start == Au(0) && + result.border_padding.inline_end == Au(0) && + candidate.border_padding.inline_start == Au(0) && + result_info.selected() == candidate_info.selected() && + Arc::ptr_eq(&result_info.run, &candidate_info.run) && + inline_contexts_are_equal( + &result.inline_context, + &candidate.inline_context, + ) + }, + _ => false, + }; + + if need_to_merge { + result.merge_with(candidate); + continue; + } + + self.work_list.push_front(candidate); + return Some(result); + } + } + + /// Commits a line to the list. + fn flush_current_line(&mut self) { + debug!( + "LineBreaker: flushing line {}: {:?}", + self.lines.len(), + self.pending_line + ); + self.strip_trailing_whitespace_from_pending_line_if_necessary(); + self.lines.push(self.pending_line.clone()); + self.cur_b = self.pending_line.bounds.start.b + self.pending_line.bounds.size.block; + self.reset_line(); + } + + /// Removes trailing whitespace from the pending line if necessary. This is done right before + /// flushing it. + fn strip_trailing_whitespace_from_pending_line_if_necessary(&mut self) { + if self.pending_line.range.is_empty() { + return; + } + let last_fragment_index = self.pending_line.range.end() - FragmentIndex(1); + let fragment = &mut self.new_fragments[last_fragment_index.get() as usize]; + + let old_fragment_inline_size = fragment.border_box.size.inline; + + fragment.strip_trailing_whitespace_if_necessary(); + + self.pending_line.bounds.size.inline += + fragment.border_box.size.inline - old_fragment_inline_size; + } + + /// Computes the position of a line that has only the provided fragment. Returns the bounding + /// rect of the line's green zone (whose origin coincides with the line's origin) and the + /// actual inline-size of the first fragment after splitting. + fn initial_line_placement( + &self, + flow: &InlineFlow, + first_fragment: &Fragment, + ceiling: Au, + ) -> (LogicalRect<Au>, Au) { + debug!( + "LineBreaker: trying to place first fragment of line {}; fragment size: {:?}, \ + splittable: {}", + self.lines.len(), + first_fragment.border_box.size, + first_fragment.can_split() + ); + + // Initially, pretend a splittable fragment has zero inline-size. We will move it later if + // it has nonzero inline-size and that causes problems. + let placement_inline_size = if first_fragment.can_split() { + first_fragment.minimum_splittable_inline_size() + } else { + first_fragment.margin_box_inline_size() + self.indentation_for_pending_fragment() + }; + + // Try to place the fragment between floats. + let line_bounds = self.floats.place_between_floats(&PlacementInfo { + size: LogicalSize::new( + self.floats.writing_mode, + placement_inline_size, + first_fragment.border_box.size.block, + ), + ceiling: ceiling, + max_inline_size: flow.base.position.size.inline, + kind: FloatKind::Left, + }); + + let fragment_margin_box_inline_size = first_fragment.margin_box_inline_size(); + + // Simple case: if the fragment fits, then we can stop here. + if line_bounds.size.inline > fragment_margin_box_inline_size { + debug!("LineBreaker: fragment fits on line {}", self.lines.len()); + return (line_bounds, fragment_margin_box_inline_size); + } + + // If not, but we can't split the fragment, then we'll place the line here and it will + // overflow. + if !first_fragment.can_split() { + debug!("LineBreaker: line doesn't fit, but is unsplittable"); + } + + (line_bounds, fragment_margin_box_inline_size) + } + + /// Performs float collision avoidance. This is called when adding a fragment is going to + /// increase the block-size, and because of that we will collide with some floats. + /// + /// We have two options here: + /// 1) Move the entire line so that it doesn't collide any more. + /// 2) Break the line and put the new fragment on the next line. + /// + /// The problem with option 1 is that we might move the line and then wind up breaking anyway, + /// which violates the standard. But option 2 is going to look weird sometimes. + /// + /// So we'll try to move the line whenever we can, but break if we have to. + /// + /// Returns false if and only if we should break the line. + fn avoid_floats( + &mut self, + flow: &InlineFlow, + in_fragment: Fragment, + new_block_size: Au, + ) -> bool { + debug!("LineBreaker: entering float collision avoider!"); + + // First predict where the next line is going to be. + let (next_line, first_fragment_inline_size) = + self.initial_line_placement(flow, &in_fragment, self.pending_line.bounds.start.b); + let next_green_zone = next_line.size; + + let new_inline_size = self.pending_line.bounds.size.inline + first_fragment_inline_size; + + // Now, see if everything can fit at the new location. + if next_green_zone.inline >= new_inline_size && next_green_zone.block >= new_block_size { + debug!( + "LineBreaker: case=adding fragment collides vertically with floats: moving \ + line" + ); + + self.pending_line.bounds.start = next_line.start; + self.pending_line.green_zone = next_green_zone; + + debug_assert!( + !self.pending_line_is_empty(), + "Non-terminating line breaking" + ); + self.work_list.push_front(in_fragment); + return true; + } + + debug!("LineBreaker: case=adding fragment collides vertically with floats: breaking line"); + self.work_list.push_front(in_fragment); + false + } + + /// Tries to append the given fragment to the line, splitting it if necessary. Commits the + /// current line if needed. + fn reflow_fragment( + &mut self, + mut fragment: Fragment, + flow: &InlineFlow, + layout_context: &LayoutContext, + ) { + // Undo any whitespace stripping from previous reflows. + fragment.reset_text_range_and_inline_size(); + // Determine initial placement for the fragment if we need to. + // + // Also, determine whether we can legally break the line before, or + // inside, this fragment. + let fragment_is_line_break_opportunity = if self.pending_line_is_empty() { + fragment.strip_leading_whitespace_if_necessary(); + let (line_bounds, _) = self.initial_line_placement(flow, &fragment, self.cur_b); + self.pending_line.bounds.start = line_bounds.start; + self.pending_line.green_zone = line_bounds.size; + false + } else { + // In case of Foo<span style="...">bar</span>, the line breaker will + // set the "suppress line break before" flag for the second fragment. + // + // In case of Foo<span>bar</span> the second fragment ("bar") will + // start _within_ a glyph run, so we also avoid breaking there + // + // is_on_glyph_run_boundary does a binary search, but this is ok + // because the result will be cached and reused in + // `calculate_split_position` later + if fragment.suppress_line_break_before() || !fragment.is_on_glyph_run_boundary() { + false + } else { + fragment.white_space().allow_wrap() + } + }; + + debug!( + "LineBreaker: trying to append to line {} \ + (fragment size: {:?}, green zone: {:?}): {:?}", + self.lines.len(), + fragment.border_box.size, + self.pending_line.green_zone, + fragment + ); + + // NB: At this point, if `green_zone.inline < + // self.pending_line.bounds.size.inline` or `green_zone.block < + // self.pending_line.bounds.size.block`, then we committed a line that + // overlaps with floats. + let green_zone = self.pending_line.green_zone; + let new_line_metrics = self + .pending_line + .new_metrics_for_fragment(&fragment, layout_context); + let new_block_size = self.pending_line.new_block_size_for_fragment( + &fragment, + &new_line_metrics, + layout_context, + ); + if new_block_size > green_zone.block { + // Uh-oh. Float collision imminent. Enter the float collision avoider! + if !self.avoid_floats(flow, fragment, new_block_size) { + self.flush_current_line(); + } + return; + } + + // Record the last known good line break opportunity if this is one. + if fragment_is_line_break_opportunity { + self.last_known_line_breaking_opportunity = Some(self.pending_line.range.end()) + } + + // If we must flush the line after finishing this fragment due to `white-space: pre`, + // detect that. + let line_flush_mode = if fragment.white_space().preserve_newlines() { + if fragment.requires_line_break_afterward_if_wrapping_on_newlines() { + LineFlushMode::Flush + } else { + LineFlushMode::No + } + } else { + LineFlushMode::No + }; + + // If we're not going to overflow the green zone vertically, we might still do so + // horizontally. We'll try to place the whole fragment on this line and break somewhere if + // it doesn't fit. + let indentation = self.indentation_for_pending_fragment(); + let new_inline_size = + self.pending_line.bounds.size.inline + fragment.margin_box_inline_size() + indentation; + if new_inline_size <= green_zone.inline { + debug!("LineBreaker: fragment fits without splitting"); + self.push_fragment_to_line(layout_context, fragment, line_flush_mode); + return; + } + + // If the wrapping mode prevents us from splitting, then back up and split at the last + // known good split point. + if !fragment.white_space().allow_wrap() { + debug!( + "LineBreaker: fragment can't split; falling back to last known good split point" + ); + self.split_line_at_last_known_good_position(layout_context, fragment, line_flush_mode); + return; + } + + // Split it up! + let available_inline_size = + green_zone.inline - self.pending_line.bounds.size.inline - indentation; + let inline_start_fragment; + let inline_end_fragment; + let split_result = match fragment + .calculate_split_position(available_inline_size, self.pending_line_is_empty()) + { + None => { + // We failed to split. Defer to the next line if we're allowed to; otherwise, + // rewind to the last line breaking opportunity. + if fragment_is_line_break_opportunity { + debug!("LineBreaker: fragment was unsplittable; deferring to next line"); + self.work_list.push_front(fragment); + self.flush_current_line(); + } else { + self.split_line_at_last_known_good_position( + layout_context, + fragment, + LineFlushMode::No, + ); + } + return; + }, + Some(split_result) => split_result, + }; + + inline_start_fragment = split_result + .inline_start + .as_ref() + .map(|x| fragment.transform_with_split_info(x, split_result.text_run.clone(), true)); + inline_end_fragment = split_result + .inline_end + .as_ref() + .map(|x| fragment.transform_with_split_info(x, split_result.text_run.clone(), false)); + + // Push the first fragment onto the line we're working on and start off the next line with + // the second fragment. If there's no second fragment, the next line will start off empty. + match (inline_start_fragment, inline_end_fragment) { + (Some(mut inline_start_fragment), Some(mut inline_end_fragment)) => { + inline_start_fragment.border_padding.inline_end = Au(0); + if let Some(ref mut inline_context) = inline_start_fragment.inline_context { + for node in &mut inline_context.nodes { + node.flags + .remove(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT); + } + } + inline_start_fragment.border_box.size.inline += + inline_start_fragment.border_padding.inline_start; + + inline_end_fragment.border_padding.inline_start = Au(0); + if let Some(ref mut inline_context) = inline_end_fragment.inline_context { + for node in &mut inline_context.nodes { + node.flags + .remove(InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT); + } + } + inline_end_fragment.border_box.size.inline += + inline_end_fragment.border_padding.inline_end; + + self.push_fragment_to_line( + layout_context, + inline_start_fragment, + LineFlushMode::Flush, + ); + self.work_list.push_front(inline_end_fragment) + }, + (Some(fragment), None) => { + self.push_fragment_to_line(layout_context, fragment, line_flush_mode); + }, + (None, Some(fragment)) => { + // Yes, this can happen! + self.flush_current_line(); + self.work_list.push_front(fragment) + }, + (None, None) => {}, + } + } + + /// Pushes a fragment to the current line unconditionally, possibly truncating it and placing + /// an ellipsis based on the value of `text-overflow`. If `flush_line` is `Flush`, then flushes + /// the line afterward; + fn push_fragment_to_line( + &mut self, + layout_context: &LayoutContext, + fragment: Fragment, + line_flush_mode: LineFlushMode, + ) { + let indentation = self.indentation_for_pending_fragment(); + if self.pending_line_is_empty() { + debug_assert!(self.new_fragments.len() <= (isize::MAX as usize)); + self.pending_line.range.reset( + FragmentIndex(self.new_fragments.len() as isize), + FragmentIndex(0), + ); + } + + // Determine if an ellipsis will be necessary to account for `text-overflow`. + let available_inline_size = self.pending_line.green_zone.inline - + self.pending_line.bounds.size.inline - + indentation; + + let ellipsis = match ( + &fragment.style().get_text().text_overflow.second, + fragment.style().get_box().overflow_x, + ) { + (&TextOverflowSide::Clip, _) | (_, StyleOverflow::Visible) => None, + (&TextOverflowSide::Ellipsis, _) => { + if fragment.margin_box_inline_size() > available_inline_size { + Some("…".to_string()) + } else { + None + } + }, + (&TextOverflowSide::String(ref string), _) => { + if fragment.margin_box_inline_size() > available_inline_size { + Some(string.to_string()) + } else { + None + } + }, + }; + + if let Some(string) = ellipsis { + let ellipsis = fragment.transform_into_ellipsis(layout_context, string); + let truncated = fragment + .truncate_to_inline_size(available_inline_size - ellipsis.margin_box_inline_size()); + self.push_fragment_to_line_ignoring_text_overflow(truncated, layout_context); + self.push_fragment_to_line_ignoring_text_overflow(ellipsis, layout_context); + } else { + self.push_fragment_to_line_ignoring_text_overflow(fragment, layout_context); + } + + if line_flush_mode == LineFlushMode::Flush { + self.flush_current_line() + } + } + + /// Pushes a fragment to the current line unconditionally, without placing an ellipsis in the + /// case of `text-overflow: ellipsis`. + fn push_fragment_to_line_ignoring_text_overflow( + &mut self, + fragment: Fragment, + layout_context: &LayoutContext, + ) { + let indentation = self.indentation_for_pending_fragment(); + self.pending_line.range.extend_by(FragmentIndex(1)); + + if !fragment.is_inline_absolute() && !fragment.is_hypothetical() { + self.pending_line.bounds.size.inline = self.pending_line.bounds.size.inline + + fragment.margin_box_inline_size() + + indentation; + self.pending_line.metrics = self + .pending_line + .new_metrics_for_fragment(&fragment, layout_context); + self.pending_line.bounds.size.block = self.pending_line.new_block_size_for_fragment( + &fragment, + &self.pending_line.metrics, + layout_context, + ); + } + + self.new_fragments.push(fragment); + } + + fn split_line_at_last_known_good_position( + &mut self, + layout_context: &LayoutContext, + cur_fragment: Fragment, + line_flush_mode: LineFlushMode, + ) { + let last_known_line_breaking_opportunity = match self.last_known_line_breaking_opportunity { + None => { + // No line breaking opportunity exists at all for this line. Overflow. + self.push_fragment_to_line(layout_context, cur_fragment, line_flush_mode); + return; + }, + Some(last_known_line_breaking_opportunity) => last_known_line_breaking_opportunity, + }; + + self.work_list.push_front(cur_fragment); + for fragment_index in + (last_known_line_breaking_opportunity.get()..self.pending_line.range.end().get()).rev() + { + debug_assert_eq!(fragment_index, (self.new_fragments.len() as isize) - 1); + self.work_list.push_front(self.new_fragments.pop().unwrap()); + } + + // FIXME(pcwalton): This should actually attempt to split the last fragment if + // possible to do so, to handle cases like: + // + // (available width) + // +-------------+ + // The alphabet + // (<em>abcdefghijklmnopqrstuvwxyz</em>) + // + // Here, the last known-good split point is inside the fragment containing + // "The alphabet (", which has already been committed by the time we get to this + // point. Unfortunately, the existing splitting API (`calculate_split_position`) + // has no concept of "split right before the last non-whitespace position". We'll + // need to add that feature to the API to handle this case correctly. + self.pending_line + .range + .extend_to(last_known_line_breaking_opportunity); + self.flush_current_line(); + } + + /// Returns the indentation that needs to be applied before the fragment we're reflowing. + fn indentation_for_pending_fragment(&self) -> Au { + if self.pending_line_is_empty() && self.lines.is_empty() { + self.first_line_indentation + } else { + Au(0) + } + } + + /// Returns true if the pending line is empty and false otherwise. + fn pending_line_is_empty(&self) -> bool { + self.pending_line.range.length() == FragmentIndex(0) + } +} + +/// Represents a list of inline fragments, including element ranges. +#[derive(Clone, Serialize)] +pub struct InlineFragments { + /// The fragments themselves. + pub fragments: Vec<Fragment>, +} + +impl InlineFragments { + /// Creates an empty set of inline fragments. + pub fn new() -> InlineFragments { + InlineFragments { fragments: vec![] } + } + + /// Returns the number of inline fragments. + pub fn len(&self) -> usize { + self.fragments.len() + } + + /// Returns true if this list contains no fragments and false if it contains at least one + /// fragment. + pub fn is_empty(&self) -> bool { + self.fragments.is_empty() + } + + /// A convenience function to return the fragment at a given index. + pub fn get(&self, index: usize) -> &Fragment { + &self.fragments[index] + } + + /// A convenience function to return a mutable reference to the fragment at a given index. + pub fn get_mut(&mut self, index: usize) -> &mut Fragment { + &mut self.fragments[index] + } +} + +#[allow(unsafe_code)] +unsafe impl crate::flow::HasBaseFlow for InlineFlow {} + +/// Flows for inline layout. +#[derive(Serialize)] +#[repr(C)] +pub struct InlineFlow { + /// Data common to all flows. + pub base: BaseFlow, + + /// A vector of all inline fragments. Several fragments may correspond to one node/element. + pub fragments: InlineFragments, + + /// A vector of ranges into fragments that represents line positions. These ranges are disjoint + /// and are the result of inline layout. This also includes some metadata used for positioning + /// lines. + pub lines: Vec<Line>, + + /// The minimum metrics for each line, as specified by the line height and font style. + pub minimum_line_metrics: LineMetrics, + + /// The amount of indentation to use on the first line. This is determined by our block parent + /// (because percentages are relative to the containing block, and we aren't in a position to + /// compute things relative to our parent's containing block). + pub first_line_indentation: Au, +} + +impl InlineFlow { + pub fn from_fragments(fragments: InlineFragments, writing_mode: WritingMode) -> InlineFlow { + let mut flow = InlineFlow { + base: BaseFlow::new(None, writing_mode, ForceNonfloatedFlag::ForceNonfloated), + fragments: fragments, + lines: Vec::new(), + minimum_line_metrics: LineMetrics::new(Au(0), Au(0)), + first_line_indentation: Au(0), + }; + + if flow + .fragments + .fragments + .iter() + .any(Fragment::is_unscanned_generated_content) + { + flow.base + .restyle_damage + .insert(ServoRestyleDamage::RESOLVE_GENERATED_CONTENT); + } + + flow + } + + /// Sets fragment positions in the inline direction based on alignment for one line. This + /// performs text justification if mandated by the style. + fn set_inline_fragment_positions( + fragments: &mut InlineFragments, + line: &Line, + line_align: TextAlign, + indentation: Au, + is_last_line: bool, + ) { + // Figure out how much inline-size we have. + let slack_inline_size = max(Au(0), line.green_zone.inline - line.bounds.size.inline); + + // Compute the value we're going to use for `text-justify`. + if fragments.fragments.is_empty() { + return; + } + let text_justify = fragments.fragments[0] + .style() + .get_inherited_text() + .text_justify; + + // Translate `left` and `right` to logical directions. + let is_ltr = fragments.fragments[0].style().writing_mode.is_bidi_ltr(); + let line_align = match (line_align, is_ltr) { + (TextAlign::Left, true) | + (TextAlign::ServoLeft, true) | + (TextAlign::Right, false) | + (TextAlign::ServoRight, false) => TextAlign::Start, + (TextAlign::Left, false) | + (TextAlign::ServoLeft, false) | + (TextAlign::Right, true) | + (TextAlign::ServoRight, true) => TextAlign::End, + _ => line_align, + }; + + // Set the fragment inline positions based on that alignment, and justify the text if + // necessary. + let mut inline_start_position_for_fragment = line.bounds.start.i + indentation; + match line_align { + TextAlign::Justify if !is_last_line && text_justify != TextJustify::None => { + InlineFlow::justify_inline_fragments(fragments, line, slack_inline_size) + }, + TextAlign::Justify | TextAlign::Start => {}, + TextAlign::Center | TextAlign::ServoCenter => { + inline_start_position_for_fragment = + inline_start_position_for_fragment + slack_inline_size.scale_by(0.5) + }, + TextAlign::End => { + inline_start_position_for_fragment = + inline_start_position_for_fragment + slack_inline_size + }, + TextAlign::Left | TextAlign::ServoLeft | TextAlign::Right | TextAlign::ServoRight => { + unreachable!() + }, + } + + // Lay out the fragments in visual order. + let run_count = match line.visual_runs { + Some(ref runs) => runs.len(), + None => 1, + }; + for run_idx in 0..run_count { + let (range, level) = match line.visual_runs { + Some(ref runs) if is_ltr => runs[run_idx], + Some(ref runs) => runs[run_count - run_idx - 1], // reverse order for RTL runs + None => (line.range, bidi::Level::ltr()), + }; + + struct MaybeReverse<I> { + iter: I, + reverse: bool, + } + + impl<I: DoubleEndedIterator> Iterator for MaybeReverse<I> { + type Item = I::Item; + + fn next(&mut self) -> Option<I::Item> { + if self.reverse { + self.iter.next_back() + } else { + self.iter.next() + } + } + + fn size_hint(&self) -> (usize, Option<usize>) { + self.iter.size_hint() + } + } + + // If the bidi embedding direction is opposite the layout direction, lay out this + // run in reverse order. + let fragment_indices = MaybeReverse { + iter: range.begin().get()..range.end().get(), + reverse: level.is_ltr() != is_ltr, + }; + + for fragment_index in fragment_indices { + let fragment = fragments.get_mut(fragment_index as usize); + inline_start_position_for_fragment = + inline_start_position_for_fragment + fragment.margin.inline_start; + + let border_start = if fragment.style.writing_mode.is_bidi_ltr() == is_ltr { + inline_start_position_for_fragment + } else { + line.green_zone.inline - + inline_start_position_for_fragment - + fragment.margin.inline_end - + fragment.border_box.size.inline + }; + fragment.border_box = LogicalRect::new( + fragment.style.writing_mode, + border_start, + fragment.border_box.start.b, + fragment.border_box.size.inline, + fragment.border_box.size.block, + ); + fragment.update_late_computed_inline_position_if_necessary(); + + if !fragment.is_inline_absolute() { + inline_start_position_for_fragment = inline_start_position_for_fragment + + fragment.border_box.size.inline + + fragment.margin.inline_end; + } + } + } + } + + /// Justifies the given set of inline fragments, distributing the `slack_inline_size` among all + /// of them according to the value of `text-justify`. + fn justify_inline_fragments( + fragments: &mut InlineFragments, + line: &Line, + slack_inline_size: Au, + ) { + // Fast path. + if slack_inline_size == Au(0) { + return; + } + + // First, calculate the number of expansion opportunities (spaces, normally). + let mut expansion_opportunities = 0; + for fragment_index in line.range.each_index() { + let fragment = fragments.get(fragment_index.to_usize()); + let scanned_text_fragment_info = match fragment.specific { + SpecificFragmentInfo::ScannedText(ref info) if !info.range.is_empty() => info, + _ => continue, + }; + let fragment_range = scanned_text_fragment_info.range; + + for slice in scanned_text_fragment_info + .run + .character_slices_in_range(&fragment_range) + { + expansion_opportunities += slice.glyphs.space_count_in_range(&slice.range) + } + } + + if expansion_opportunities == 0 { + return; + } + + // Then distribute all the space across the expansion opportunities. + let space_per_expansion_opportunity = slack_inline_size / expansion_opportunities as i32; + for fragment_index in line.range.each_index() { + let fragment = fragments.get_mut(fragment_index.to_usize()); + let scanned_text_fragment_info = match fragment.specific { + SpecificFragmentInfo::ScannedText(ref mut info) if !info.range.is_empty() => info, + _ => continue, + }; + let fragment_range = scanned_text_fragment_info.range; + let run = Arc::make_mut(&mut scanned_text_fragment_info.run); + run.extra_word_spacing = space_per_expansion_opportunity; + + // Recompute the fragment's border box size. + let new_inline_size = run.advance_for_range(&fragment_range); + let new_size = LogicalSize::new( + fragment.style.writing_mode, + new_inline_size, + fragment.border_box.size.block, + ); + fragment.border_box = LogicalRect::from_point_size( + fragment.style.writing_mode, + fragment.border_box.start, + new_size, + ); + } + } + + /// Sets final fragment positions in the block direction for one line. + fn set_block_fragment_positions( + fragments: &mut InlineFragments, + line: &Line, + minimum_line_metrics: &LineMetrics, + layout_context: &LayoutContext, + ) { + for fragment_index in line.range.each_index() { + let fragment = fragments.get_mut(fragment_index.to_usize()); + let line_metrics = LineMetrics::for_line_and_fragment(line, fragment, layout_context); + let inline_metrics = fragment.aligned_inline_metrics( + layout_context, + minimum_line_metrics, + Some(&line_metrics), + ); + + // Align the top of the fragment's border box with its ascent above the baseline. + fragment.border_box.start.b = + line.bounds.start.b + line_metrics.space_above_baseline - inline_metrics.ascent; + + // CSS 2.1 § 10.8: "The height of each inline-level box in the line box is + // calculated. For replaced elements, inline-block elements, and inline-table + // elements, this is the height of their margin box; for inline boxes, this is their + // 'line-height'." + // + // CSS 2.1 § 10.8.1: "Although margins, borders, and padding of non-replaced elements + // do not enter into the line box calculation, they are still rendered around inline + // boxes." + // + // Effectively, if the fragment is a non-replaced element (excluding inline-block), we + // need to align its ascent above the baseline with the top of the *content box*, not + // the border box. Since the code above has already aligned it to the border box, we + // simply need to adjust it in this case. + if !fragment.is_replaced_or_inline_block() { + fragment.border_box.start.b -= fragment.border_padding.block_start + } + + fragment.update_late_computed_block_position_if_necessary(); + } + } + + /// Computes the minimum metrics for each line. This is done during flow construction. + /// + /// `style` is the style of the block. + pub fn minimum_line_metrics( + &self, + font_context: &mut LayoutFontContext, + style: &ComputedValues, + ) -> LineMetrics { + InlineFlow::minimum_line_metrics_for_fragments( + &self.fragments.fragments, + font_context, + style, + ) + } + + /// Computes the minimum line metrics for the given fragments. This is typically done during + /// flow construction. + /// + /// `style` is the style of the block that these fragments belong to. + pub fn minimum_line_metrics_for_fragments( + fragments: &[Fragment], + font_context: &mut LayoutFontContext, + style: &ComputedValues, + ) -> LineMetrics { + // As a special case, if this flow contains only hypothetical fragments, then the entire + // flow is hypothetical and takes up no space. See CSS 2.1 § 10.3.7. + if fragments.iter().all(Fragment::is_hypothetical) { + return LineMetrics::new(Au(0), Au(0)); + } + + let font_style = style.clone_font(); + let font_metrics = text::font_metrics_for_style(font_context, font_style); + let line_height = text::line_height_from_style(style, &font_metrics); + let inline_metrics = if fragments.iter().any(Fragment::is_text_or_replaced) { + InlineMetrics::from_font_metrics(&font_metrics, line_height) + } else { + InlineMetrics::new(Au(0), Au(0), Au(0)) + }; + + let mut line_metrics = LineMetrics::new(Au(0), MIN_AU); + let mut largest_block_size_for_top_fragments = Au(0); + let mut largest_block_size_for_bottom_fragments = Au(0); + + // We use `VerticalAlign::baseline()` here because `vertical-align` must + // not apply to the inside of inline blocks. + update_line_metrics_for_fragment( + &mut line_metrics, + &inline_metrics, + style.get_box().display, + VerticalAlign::baseline(), + &mut largest_block_size_for_top_fragments, + &mut largest_block_size_for_bottom_fragments, + ); + + // According to CSS 2.1 § 10.8, `line-height` of any inline element specifies the minimal + // height of line boxes within the element. + for inline_context in fragments + .iter() + .filter_map(|fragment| fragment.inline_context.as_ref()) + { + for node in &inline_context.nodes { + let font_style = node.style.clone_font(); + let font_metrics = text::font_metrics_for_style(font_context, font_style); + let line_height = text::line_height_from_style(&*node.style, &font_metrics); + let inline_metrics = InlineMetrics::from_font_metrics(&font_metrics, line_height); + + update_line_metrics_for_fragment( + &mut line_metrics, + &inline_metrics, + node.style.get_box().display, + node.style.get_box().vertical_align, + &mut largest_block_size_for_top_fragments, + &mut largest_block_size_for_bottom_fragments, + ); + } + } + + line_metrics.space_above_baseline = max( + line_metrics.space_above_baseline, + largest_block_size_for_bottom_fragments - max(line_metrics.space_below_baseline, Au(0)), + ); + line_metrics.space_below_baseline = max( + line_metrics.space_below_baseline, + largest_block_size_for_top_fragments - line_metrics.space_above_baseline, + ); + + return line_metrics; + + fn update_line_metrics_for_fragment( + line_metrics: &mut LineMetrics, + inline_metrics: &InlineMetrics, + display_value: Display, + vertical_align_value: VerticalAlign, + largest_block_size_for_top_fragments: &mut Au, + largest_block_size_for_bottom_fragments: &mut Au, + ) { + // FIXME(emilio): This should probably be handled. + let vertical_align_value = match vertical_align_value { + VerticalAlign::Keyword(kw) => kw, + VerticalAlign::Length(..) => { + *line_metrics = line_metrics.new_metrics_for_fragment(inline_metrics); + return; + }, + }; + + match (display_value, vertical_align_value) { + (Display::Inline, VerticalAlignKeyword::Top) | + (Display::Block, VerticalAlignKeyword::Top) | + (Display::InlineFlex, VerticalAlignKeyword::Top) | + (Display::InlineBlock, VerticalAlignKeyword::Top) + if inline_metrics.space_above_baseline >= Au(0) => + { + *largest_block_size_for_top_fragments = max( + *largest_block_size_for_top_fragments, + inline_metrics.space_above_baseline + inline_metrics.space_below_baseline, + ) + }, + (Display::Inline, VerticalAlignKeyword::Bottom) | + (Display::Block, VerticalAlignKeyword::Bottom) | + (Display::InlineFlex, VerticalAlignKeyword::Bottom) | + (Display::InlineBlock, VerticalAlignKeyword::Bottom) + if inline_metrics.space_below_baseline >= Au(0) => + { + *largest_block_size_for_bottom_fragments = max( + *largest_block_size_for_bottom_fragments, + inline_metrics.space_above_baseline + inline_metrics.space_below_baseline, + ) + }, + _ => *line_metrics = line_metrics.new_metrics_for_fragment(inline_metrics), + } + } + } + + fn update_restyle_damage(&mut self) { + let mut damage = self.base.restyle_damage; + + for frag in &self.fragments.fragments { + damage.insert(frag.restyle_damage()); + } + + self.base.restyle_damage = damage; + } + + fn containing_block_range_for_flow_surrounding_fragment_at_index( + &self, + fragment_index: FragmentIndex, + ) -> Range<FragmentIndex> { + let mut start_index = fragment_index; + while start_index > FragmentIndex(0) && + self.fragments.fragments[(start_index - FragmentIndex(1)).get() as usize] + .is_positioned() + { + start_index = start_index - FragmentIndex(1) + } + + let mut end_index = fragment_index + FragmentIndex(1); + while end_index < FragmentIndex(self.fragments.fragments.len() as isize) && + self.fragments.fragments[end_index.get() as usize].is_positioned() + { + end_index = end_index + FragmentIndex(1) + } + + Range::new(start_index, end_index - start_index) + } + + fn containing_block_range_for_flow(&self, opaque_flow: OpaqueFlow) -> Range<FragmentIndex> { + match self + .fragments + .fragments + .iter() + .position(|fragment| match fragment.specific { + SpecificFragmentInfo::InlineAbsolute(ref inline_absolute) => { + OpaqueFlow::from_flow(&*inline_absolute.flow_ref) == opaque_flow + }, + SpecificFragmentInfo::InlineAbsoluteHypothetical( + ref inline_absolute_hypothetical, + ) => OpaqueFlow::from_flow(&*inline_absolute_hypothetical.flow_ref) == opaque_flow, + _ => false, + }) { + Some(index) => { + let index = FragmentIndex(index as isize); + self.containing_block_range_for_flow_surrounding_fragment_at_index(index) + }, + None => { + // FIXME(pcwalton): This is quite wrong. We should only return the range + // surrounding the inline fragments that constitute the containing block. But this + // suffices to get Google looking right. + Range::new( + FragmentIndex(0), + FragmentIndex(self.fragments.fragments.len() as isize), + ) + }, + } + } + + pub fn baseline_offset_of_last_line(&self) -> Option<Au> { + self.last_line_containing_real_fragments().map(|line| { + line.bounds.start.b + line.bounds.size.block - line.metrics.space_below_baseline + }) + } + + // Returns the last line that doesn't consist entirely of hypothetical boxes. + fn last_line_containing_real_fragments(&self) -> Option<&Line> { + for line in self.lines.iter().rev() { + if (line.range.begin().get()..line.range.end().get()) + .any(|index| !self.fragments.fragments[index as usize].is_hypothetical()) + { + return Some(line); + } + } + None + } + + fn build_display_list_for_inline_fragment_at_index( + &mut self, + state: &mut DisplayListBuildState, + index: usize, + ) { + let fragment = self.fragments.fragments.get_mut(index).unwrap(); + let stacking_relative_border_box = self + .base + .stacking_relative_border_box_for_display_list(fragment); + fragment.build_display_list( + state, + stacking_relative_border_box, + BorderPaintingMode::Separate, + DisplayListSection::Content, + self.base.clip, + None, + ); + } +} + +impl Flow for InlineFlow { + fn class(&self) -> FlowClass { + FlowClass::Inline + } + + fn as_inline(&self) -> &InlineFlow { + self + } + + fn as_mut_inline(&mut self) -> &mut InlineFlow { + self + } + + fn bubble_inline_sizes(&mut self) { + self.update_restyle_damage(); + + let _scope = layout_debug_scope!("inline::bubble_inline_sizes {:x}", self.base.debug_id()); + + let writing_mode = self.base.writing_mode; + for kid in self.base.child_iter_mut() { + kid.mut_base().floats = Floats::new(writing_mode); + } + + self.base + .flags + .remove(FlowFlags::CONTAINS_TEXT_OR_REPLACED_FRAGMENTS); + + let mut intrinsic_sizes_for_flow = IntrinsicISizesContribution::new(); + let mut intrinsic_sizes_for_inline_run = IntrinsicISizesContribution::new(); + let mut intrinsic_sizes_for_nonbroken_run = IntrinsicISizesContribution::new(); + for fragment in &mut self.fragments.fragments { + let intrinsic_sizes_for_fragment = fragment.compute_intrinsic_inline_sizes().finish(); + match fragment.style.get_inherited_text().white_space { + WhiteSpace::Nowrap => intrinsic_sizes_for_nonbroken_run + .union_nonbreaking_inline(&intrinsic_sizes_for_fragment), + WhiteSpace::Pre => { + intrinsic_sizes_for_nonbroken_run + .union_nonbreaking_inline(&intrinsic_sizes_for_fragment); + + // Flush the intrinsic sizes we've been gathering up in order to handle the + // line break, if necessary. + if fragment.requires_line_break_afterward_if_wrapping_on_newlines() { + intrinsic_sizes_for_inline_run + .union_inline(&intrinsic_sizes_for_nonbroken_run.finish()); + intrinsic_sizes_for_nonbroken_run = IntrinsicISizesContribution::new(); + intrinsic_sizes_for_flow + .union_block(&intrinsic_sizes_for_inline_run.finish()); + intrinsic_sizes_for_inline_run = IntrinsicISizesContribution::new(); + } + }, + WhiteSpace::PreWrap | WhiteSpace::PreLine => { + // Flush the intrinsic sizes we were gathering up for the nonbroken run, if + // necessary. + intrinsic_sizes_for_inline_run + .union_inline(&intrinsic_sizes_for_nonbroken_run.finish()); + intrinsic_sizes_for_nonbroken_run = IntrinsicISizesContribution::new(); + + intrinsic_sizes_for_nonbroken_run.union_inline(&intrinsic_sizes_for_fragment); + + // Flush the intrinsic sizes we've been gathering up in order to handle the + // line break, if necessary. + if fragment.requires_line_break_afterward_if_wrapping_on_newlines() { + intrinsic_sizes_for_inline_run + .union_inline(&intrinsic_sizes_for_nonbroken_run.finish()); + intrinsic_sizes_for_nonbroken_run = IntrinsicISizesContribution::new(); + intrinsic_sizes_for_flow + .union_block(&intrinsic_sizes_for_inline_run.finish()); + intrinsic_sizes_for_inline_run = IntrinsicISizesContribution::new(); + } + }, + WhiteSpace::Normal => { + // Flush the intrinsic sizes we were gathering up for the nonbroken run, if + // necessary. + intrinsic_sizes_for_inline_run + .union_inline(&intrinsic_sizes_for_nonbroken_run.finish()); + intrinsic_sizes_for_nonbroken_run = IntrinsicISizesContribution::new(); + + intrinsic_sizes_for_nonbroken_run.union_inline(&intrinsic_sizes_for_fragment); + }, + } + + fragment + .restyle_damage + .remove(ServoRestyleDamage::BUBBLE_ISIZES); + + if fragment.is_text_or_replaced() { + self.base + .flags + .insert(FlowFlags::CONTAINS_TEXT_OR_REPLACED_FRAGMENTS); + } + } + + // Flush any remaining nonbroken-run and inline-run intrinsic sizes. + intrinsic_sizes_for_inline_run.union_inline(&intrinsic_sizes_for_nonbroken_run.finish()); + intrinsic_sizes_for_flow.union_block(&intrinsic_sizes_for_inline_run.finish()); + + // Finish up the computation. + self.base.intrinsic_inline_sizes = intrinsic_sizes_for_flow.finish() + } + + /// Recursively (top-down) determines the actual inline-size of child contexts and fragments. + /// When called on this context, the context has had its inline-size set by the parent context. + fn assign_inline_sizes(&mut self, _: &LayoutContext) { + let _scope = layout_debug_scope!("inline::assign_inline_sizes {:x}", self.base.debug_id()); + + // Initialize content fragment inline-sizes if they haven't been initialized already. + // + // TODO: Combine this with `LineBreaker`'s walk in the fragment list, or put this into + // `Fragment`. + + debug!( + "InlineFlow::assign_inline_sizes: floats in: {:?}", + self.base.floats + ); + + let inline_size = self.base.block_container_inline_size; + let container_mode = self.base.block_container_writing_mode; + let container_block_size = self.base.block_container_explicit_block_size; + self.base.position.size.inline = inline_size; + + { + let this = &mut *self; + for fragment in this.fragments.fragments.iter_mut() { + fragment.compute_border_and_padding(inline_size); + fragment.compute_block_direction_margins(inline_size); + fragment.compute_inline_direction_margins(inline_size); + fragment + .assign_replaced_inline_size_if_necessary(inline_size, container_block_size); + } + } + + // If there are any inline-block kids, propagate explicit block and inline + // sizes down to them. + let block_container_explicit_block_size = self.base.block_container_explicit_block_size; + for kid in self.base.child_iter_mut() { + let kid_base = kid.mut_base(); + + kid_base.block_container_inline_size = inline_size; + kid_base.block_container_writing_mode = container_mode; + kid_base.block_container_explicit_block_size = block_container_explicit_block_size; + } + } + + /// Calculate and set the block-size of this flow. See CSS 2.1 § 10.6.1. + /// Note that we do not need to do in-order traversal because the children + /// are always block formatting context. + fn assign_block_size(&mut self, layout_context: &LayoutContext) { + let _scope = layout_debug_scope!("inline::assign_block_size {:x}", self.base.debug_id()); + + // Divide the fragments into lines. + // + // TODO(pcwalton, #226): Get the CSS `line-height` property from the + // style of the containing block to determine the minimum line block + // size. + // + // TODO(pcwalton, #226): Get the CSS `line-height` property from each + // non-replaced inline element to determine its block-size for computing + // the line's own block-size. + // + // TODO(pcwalton): Cache the line scanner? + debug!( + "assign_block_size_inline: floats in: {:?}", + self.base.floats + ); + + // Assign the block-size and late-computed inline-sizes for the inline fragments. + for fragment in &mut self.fragments.fragments { + fragment.update_late_computed_replaced_inline_size_if_necessary(); + fragment.assign_replaced_block_size_if_necessary(); + } + + // Reset our state, so that we handle incremental reflow correctly. + // + // TODO(pcwalton): Do something smarter, like Gecko and WebKit? + self.lines.clear(); + + // Determine how much indentation the first line wants. + let mut indentation = if self.fragments.is_empty() { + Au(0) + } else { + self.first_line_indentation + }; + + // Perform line breaking. + let mut scanner = LineBreaker::new( + self.base.floats.clone(), + indentation, + &self.minimum_line_metrics, + ); + scanner.scan_for_lines(self, layout_context); + + // Now, go through each line and lay out the fragments inside. + let line_count = self.lines.len(); + for (line_index, line) in self.lines.iter_mut().enumerate() { + // Lay out fragments in the inline direction, and justify them if + // necessary. + InlineFlow::set_inline_fragment_positions( + &mut self.fragments, + line, + self.base.flags.text_align(), + indentation, + line_index + 1 == line_count, + ); + + // Compute the final positions in the block direction of each fragment. + InlineFlow::set_block_fragment_positions( + &mut self.fragments, + line, + &self.minimum_line_metrics, + layout_context, + ); + + // This is used to set the block-start position of the next line in + // the next iteration of the loop. We're no longer on the first + // line, so set indentation to zero. + indentation = Au(0) + } + + if self.is_absolute_containing_block() { + // Assign block-sizes for all flows in this absolute flow tree. + // This is preorder because the block-size of an absolute flow may depend on + // the block-size of its containing block, which may also be an absolute flow. + let assign_abs_b_sizes = AbsoluteAssignBSizesTraversal(layout_context.shared_context()); + assign_abs_b_sizes.traverse_absolute_flows(&mut *self); + } + + self.base.position.size.block = match self.last_line_containing_real_fragments() { + Some(last_line) => last_line.bounds.start.b + last_line.bounds.size.block, + None => Au(0), + }; + + self.base.floats = scanner.floats; + let writing_mode = self.base.floats.writing_mode; + self.base.floats.translate(LogicalSize::new( + writing_mode, + Au(0), + -self.base.position.size.block, + )); + + let containing_block_size = + LogicalSize::new(writing_mode, Au(0), self.base.position.size.block); + self.mutate_fragments(&mut |f: &mut Fragment| match f.specific { + SpecificFragmentInfo::InlineBlock(ref mut info) => { + let block = FlowRef::deref_mut(&mut info.flow_ref); + block.mut_base().early_absolute_position_info = EarlyAbsolutePositionInfo { + relative_containing_block_size: containing_block_size, + relative_containing_block_mode: writing_mode, + }; + }, + SpecificFragmentInfo::InlineAbsolute(ref mut info) => { + let block = FlowRef::deref_mut(&mut info.flow_ref); + block.mut_base().early_absolute_position_info = EarlyAbsolutePositionInfo { + relative_containing_block_size: containing_block_size, + relative_containing_block_mode: writing_mode, + }; + }, + _ => (), + }); + + self.base + .restyle_damage + .remove(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + for fragment in &mut self.fragments.fragments { + fragment + .restyle_damage + .remove(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + } + } + + fn compute_stacking_relative_position(&mut self, _: &LayoutContext) { + // First, gather up the positions of all the containing blocks (if any). + // + // FIXME(pcwalton): This will get the absolute containing blocks inside `...` wrong in the + // case of something like: + // + // <span style="position: relative"> + // Foo + // <span style="display: inline-block">...</span> + // </span> + let mut containing_block_positions = Vec::new(); + let container_size = Size2D::new(self.base.block_container_inline_size, Au(0)); + for (fragment_index, fragment) in self.fragments.fragments.iter().enumerate() { + match fragment.specific { + SpecificFragmentInfo::InlineAbsolute(_) => { + let containing_block_range = self + .containing_block_range_for_flow_surrounding_fragment_at_index( + FragmentIndex(fragment_index as isize), + ); + let first_fragment_index = containing_block_range.begin().get() as usize; + debug_assert!(first_fragment_index < self.fragments.fragments.len()); + let first_fragment = &self.fragments.fragments[first_fragment_index]; + let padding_box_origin = (first_fragment.border_box - + first_fragment.style.logical_border_width()) + .start; + containing_block_positions.push( + padding_box_origin.to_physical(self.base.writing_mode, container_size), + ); + }, + SpecificFragmentInfo::InlineBlock(_) if fragment.is_positioned() => { + let containing_block_range = self + .containing_block_range_for_flow_surrounding_fragment_at_index( + FragmentIndex(fragment_index as isize), + ); + let first_fragment_index = containing_block_range.begin().get() as usize; + debug_assert!(first_fragment_index < self.fragments.fragments.len()); + let first_fragment = &self.fragments.fragments[first_fragment_index]; + let padding_box_origin = (first_fragment.border_box - + first_fragment.style.logical_border_width()) + .start; + containing_block_positions.push( + padding_box_origin.to_physical(self.base.writing_mode, container_size), + ); + }, + _ => {}, + } + } + + // Then compute the positions of all of our fragments. + let mut containing_block_positions = containing_block_positions.iter(); + for fragment in &mut self.fragments.fragments { + let stacking_relative_border_box = fragment.stacking_relative_border_box( + &self.base.stacking_relative_position, + &self + .base + .early_absolute_position_info + .relative_containing_block_size, + self.base + .early_absolute_position_info + .relative_containing_block_mode, + CoordinateSystem::Parent, + ); + let stacking_relative_content_box = + fragment.stacking_relative_content_box(stacking_relative_border_box); + + let is_positioned = fragment.is_positioned(); + match fragment.specific { + SpecificFragmentInfo::InlineBlock(ref mut info) => { + let flow = FlowRef::deref_mut(&mut info.flow_ref); + let block_flow = flow.as_mut_block(); + block_flow.base.late_absolute_position_info = + self.base.late_absolute_position_info; + + let stacking_relative_position = self.base.stacking_relative_position; + if is_positioned { + let padding_box_origin = containing_block_positions.next().unwrap(); + block_flow + .base + .late_absolute_position_info + .stacking_relative_position_of_absolute_containing_block = + *padding_box_origin + stacking_relative_position; + } + + block_flow.base.stacking_relative_position = + stacking_relative_content_box.origin.to_vector(); + + // Write the clip in our coordinate system into the child flow. (The kid will + // fix it up to be in its own coordinate system if necessary.) + block_flow.base.clip = self.base.clip.clone() + }, + SpecificFragmentInfo::InlineAbsoluteHypothetical(ref mut info) => { + let flow = FlowRef::deref_mut(&mut info.flow_ref); + let block_flow = flow.as_mut_block(); + block_flow.base.late_absolute_position_info = + self.base.late_absolute_position_info; + + block_flow.base.stacking_relative_position = + stacking_relative_border_box.origin.to_vector(); + + // As above, this is in our coordinate system for now. + block_flow.base.clip = self.base.clip.clone() + }, + SpecificFragmentInfo::InlineAbsolute(ref mut info) => { + let flow = FlowRef::deref_mut(&mut info.flow_ref); + let block_flow = flow.as_mut_block(); + block_flow.base.late_absolute_position_info = + self.base.late_absolute_position_info; + + let stacking_relative_position = self.base.stacking_relative_position; + let padding_box_origin = containing_block_positions.next().unwrap(); + block_flow + .base + .late_absolute_position_info + .stacking_relative_position_of_absolute_containing_block = + *padding_box_origin + stacking_relative_position; + + block_flow.base.stacking_relative_position = + stacking_relative_border_box.origin.to_vector(); + + // As above, this is in our coordinate system for now. + block_flow.base.clip = self.base.clip.clone() + }, + _ => {}, + } + } + } + + fn update_late_computed_inline_position_if_necessary(&mut self, _: Au) {} + + fn update_late_computed_block_position_if_necessary(&mut self, _: Au) {} + + fn collect_stacking_contexts(&mut self, state: &mut StackingContextCollectionState) { + self.base.stacking_context_id = state.current_stacking_context_id; + self.base.clipping_and_scrolling = Some(state.current_clipping_and_scrolling); + self.base.clip = state + .clip_stack + .last() + .cloned() + .unwrap_or_else(Rect::max_rect); + + let previous_cb_clipping_and_scrolling = state.containing_block_clipping_and_scrolling; + + for fragment in self.fragments.fragments.iter_mut() { + state.containing_block_clipping_and_scrolling = previous_cb_clipping_and_scrolling; + + let abspos_containing_block = fragment.style.get_box().position != Position::Static; + if abspos_containing_block { + state.containing_block_clipping_and_scrolling = + state.current_clipping_and_scrolling; + } + + // We clear this here, but it might be set again if we create a stacking context for + // this fragment. + fragment.established_reference_frame = None; + + if !fragment.collect_stacking_contexts_for_blocklike_fragment(state) { + if !fragment.establishes_stacking_context() { + fragment.stacking_context_id = state.current_stacking_context_id; + } else { + fragment.create_stacking_context_for_inline_block(&self.base, state); + } + } + + // Reset the containing block clipping and scrolling before each loop iteration, + // so we don't pollute subsequent fragments. + state.containing_block_clipping_and_scrolling = previous_cb_clipping_and_scrolling; + } + } + + fn build_display_list(&mut self, state: &mut DisplayListBuildState) { + debug!( + "Flow: building display list for {} inline fragments", + self.fragments.len() + ); + + // We iterate using an index here, because we want to avoid doing a doing + // a double-borrow of self (one mutable for the method call and one immutable + // for the self.fragments.fragment iterator itself). + for index in 0..self.fragments.fragments.len() { + let (establishes_stacking_context, stacking_context_id) = { + let fragment = self.fragments.fragments.get(index).unwrap(); + ( + self.base.stacking_context_id != fragment.stacking_context_id, + fragment.stacking_context_id, + ) + }; + + let parent_stacking_context_id = state.current_stacking_context_id; + if establishes_stacking_context { + state.current_stacking_context_id = stacking_context_id; + } + + self.build_display_list_for_inline_fragment_at_index(state, index); + + if establishes_stacking_context { + state.current_stacking_context_id = parent_stacking_context_id + } + } + + if !self.fragments.fragments.is_empty() { + self.base + .build_display_items_for_debugging_tint(state, self.fragments.fragments[0].node); + } + } + + fn repair_style(&mut self, _: &ServoArc<ComputedValues>) {} + + fn compute_overflow(&self) -> Overflow { + let mut overflow = Overflow::new(); + let flow_size = self.base.position.size.to_physical(self.base.writing_mode); + let relative_containing_block_size = &self + .base + .early_absolute_position_info + .relative_containing_block_size; + for fragment in &self.fragments.fragments { + overflow.union(&fragment.compute_overflow(&flow_size, &relative_containing_block_size)) + } + overflow + } + + fn iterate_through_fragment_border_boxes( + &self, + iterator: &mut dyn FragmentBorderBoxIterator, + level: i32, + stacking_context_position: &Point2D<Au>, + ) { + // FIXME(#2795): Get the real container size. + for fragment in &self.fragments.fragments { + if !iterator.should_process(fragment) { + continue; + } + + let stacking_relative_position = &self.base.stacking_relative_position; + let relative_containing_block_size = &self + .base + .early_absolute_position_info + .relative_containing_block_size; + let relative_containing_block_mode = self + .base + .early_absolute_position_info + .relative_containing_block_mode; + iterator.process( + fragment, + level, + &fragment + .stacking_relative_border_box( + stacking_relative_position, + relative_containing_block_size, + relative_containing_block_mode, + CoordinateSystem::Own, + ) + .translate(&stacking_context_position.to_vector()), + ) + } + } + + fn mutate_fragments(&mut self, mutator: &mut dyn FnMut(&mut Fragment)) { + for fragment in &mut self.fragments.fragments { + (*mutator)(fragment) + } + } + + fn contains_positioned_fragments(&self) -> bool { + self.fragments + .fragments + .iter() + .any(|fragment| fragment.is_positioned()) + } + + fn contains_relatively_positioned_fragments(&self) -> bool { + self.fragments + .fragments + .iter() + .any(|fragment| fragment.style.get_box().position == Position::Relative) + } + + fn generated_containing_block_size(&self, for_flow: OpaqueFlow) -> LogicalSize<Au> { + let mut containing_block_size = LogicalSize::new(self.base.writing_mode, Au(0), Au(0)); + for index in self.containing_block_range_for_flow(for_flow).each_index() { + let fragment = &self.fragments.fragments[index.get() as usize]; + if fragment.is_absolutely_positioned() { + continue; + } + containing_block_size.inline = + containing_block_size.inline + fragment.border_box.size.inline; + containing_block_size.block = + max(containing_block_size.block, fragment.border_box.size.block); + } + containing_block_size + } + + fn print_extra_flow_children(&self, print_tree: &mut PrintTree) { + for fragment in &self.fragments.fragments { + print_tree.add_item(format!("{:?}", fragment)); + } + } +} + +impl fmt::Debug for InlineFlow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{:?}({:x}) {:?}", + self.class(), + self.base.debug_id(), + self.base() + ) + } +} + +#[derive(Clone)] +pub struct InlineFragmentNodeInfo { + pub address: OpaqueNode, + pub style: ServoArc<ComputedValues>, + pub selected_style: ServoArc<ComputedValues>, + pub pseudo: PseudoElementType, + pub flags: InlineFragmentNodeFlags, +} + +bitflags! { + pub struct InlineFragmentNodeFlags: u8 { + const FIRST_FRAGMENT_OF_ELEMENT = 0x01; + const LAST_FRAGMENT_OF_ELEMENT = 0x02; + } +} + +impl fmt::Debug for InlineFragmentNodeInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self.flags.bits()) + } +} + +#[derive(Clone)] +pub struct InlineFragmentContext { + /// The list of nodes that this fragment will be inheriting styles from, + /// from the most deeply-nested node out. + pub nodes: Vec<InlineFragmentNodeInfo>, +} + +impl InlineFragmentContext { + pub fn new() -> InlineFragmentContext { + InlineFragmentContext { nodes: vec![] } + } + + #[inline] + pub fn contains_node(&self, node_address: OpaqueNode) -> bool { + self.nodes + .iter() + .position(|node| node.address == node_address) + .is_some() + } + + fn ptr_eq(&self, other: &InlineFragmentContext) -> bool { + if self.nodes.len() != other.nodes.len() { + return false; + } + for (this_node, other_node) in self.nodes.iter().zip(&other.nodes) { + if this_node.address != other_node.address { + return false; + } + } + true + } +} + +fn inline_contexts_are_equal( + inline_context_a: &Option<InlineFragmentContext>, + inline_context_b: &Option<InlineFragmentContext>, +) -> bool { + match (inline_context_a, inline_context_b) { + (&Some(ref inline_context_a), &Some(ref inline_context_b)) => { + inline_context_a.ptr_eq(inline_context_b) + }, + (&None, &None) => true, + (&Some(_), &None) | (&None, &Some(_)) => false, + } +} + +/// Ascent and space needed above and below the baseline for a fragment. See CSS 2.1 § 10.8.1. +/// +/// Descent is not included in this structure because it can be computed from the fragment's +/// border/content box and the ascent. +#[derive(Clone, Copy, Debug, Serialize)] +pub struct InlineMetrics { + /// The amount of space above the baseline needed for this fragment. + pub space_above_baseline: Au, + /// The amount of space below the baseline needed for this fragment. + pub space_below_baseline: Au, + /// The distance from the baseline to the top of this fragment. This can differ from + /// `block_size_above_baseline` if the fragment needs some empty space above it due to + /// line-height, etc. + pub ascent: Au, +} + +impl InlineMetrics { + /// Creates a new set of inline metrics. + pub fn new(space_above_baseline: Au, space_below_baseline: Au, ascent: Au) -> InlineMetrics { + InlineMetrics { + space_above_baseline: space_above_baseline, + space_below_baseline: space_below_baseline, + ascent: ascent, + } + } + + /// Calculates inline metrics from font metrics and line block-size per CSS 2.1 § 10.8.1. + #[inline] + pub fn from_font_metrics(font_metrics: &FontMetrics, line_height: Au) -> InlineMetrics { + let leading = line_height - (font_metrics.ascent + font_metrics.descent); + + // Calculating the half leading here and then using leading - half_leading + // below ensure that we don't introduce any rounding accuracy issues here. + // The invariant is that the resulting total line height must exactly + // equal the requested line_height. + let half_leading = leading.scale_by(0.5); + InlineMetrics { + space_above_baseline: font_metrics.ascent + half_leading, + space_below_baseline: font_metrics.descent + leading - half_leading, + ascent: font_metrics.ascent, + } + } + + /// Returns the sum of the space needed above and below the baseline. + fn space_needed(&self) -> Au { + self.space_above_baseline + self.space_below_baseline + } +} + +#[derive(Clone, Copy, PartialEq)] +enum LineFlushMode { + No, + Flush, +} + +#[derive(Clone, Copy, Debug, Serialize)] +pub struct LineMetrics { + pub space_above_baseline: Au, + pub space_below_baseline: Au, +} + +impl LineMetrics { + pub fn new(space_above_baseline: Au, space_below_baseline: Au) -> LineMetrics { + LineMetrics { + space_above_baseline: space_above_baseline, + space_below_baseline: space_below_baseline, + } + } + + /// Returns the line metrics that result from combining the line that these metrics represent + /// with a fragment with the given metrics. + fn new_metrics_for_fragment(&self, fragment_inline_metrics: &InlineMetrics) -> LineMetrics { + LineMetrics { + space_above_baseline: max( + self.space_above_baseline, + fragment_inline_metrics.space_above_baseline, + ), + space_below_baseline: max( + self.space_below_baseline, + fragment_inline_metrics.space_below_baseline, + ), + } + } + + fn for_line_and_fragment( + line: &Line, + fragment: &Fragment, + layout_context: &LayoutContext, + ) -> LineMetrics { + if !fragment.is_hypothetical() { + let space_above_baseline = line.metrics.space_above_baseline; + return LineMetrics { + space_above_baseline: space_above_baseline, + space_below_baseline: line.bounds.size.block - space_above_baseline, + }; + } + + let hypothetical_line_metrics = line.new_metrics_for_fragment(fragment, layout_context); + let hypothetical_block_size = + line.new_block_size_for_fragment(fragment, &hypothetical_line_metrics, layout_context); + let hypothetical_space_above_baseline = hypothetical_line_metrics.space_above_baseline; + LineMetrics { + space_above_baseline: hypothetical_space_above_baseline, + space_below_baseline: hypothetical_block_size - hypothetical_space_above_baseline, + } + } + + /// Returns the sum of the space needed above and below the baseline. + pub fn space_needed(&self) -> Au { + self.space_above_baseline + self.space_below_baseline + } +} diff --git a/components/layout_2020/layout_debug.rs b/components/layout_2020/layout_debug.rs new file mode 100644 index 00000000000..d3af6b4d8d5 --- /dev/null +++ b/components/layout_2020/layout_debug.rs @@ -0,0 +1,124 @@ +/* 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/. */ + +//! Supports writing a trace file created during each layout scope +//! that can be viewed by an external tool to make layout debugging easier. + +use crate::flow::GetBaseFlow; +use crate::flow_ref::FlowRef; +use serde_json::{to_string, to_value, Value}; +use std::borrow::ToOwned; +use std::cell::RefCell; +use std::fs::File; +use std::io::Write; +#[cfg(debug_assertions)] +use std::sync::atomic::{AtomicUsize, Ordering}; + +thread_local!(static STATE_KEY: RefCell<Option<State>> = RefCell::new(None)); + +#[cfg(debug_assertions)] +static DEBUG_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + +pub struct Scope; + +#[macro_export] +macro_rules! layout_debug_scope( + ($($arg:tt)*) => ( + if cfg!(debug_assertions) { + layout_debug::Scope::new(format!($($arg)*)) + } else { + layout_debug::Scope + } + ) +); + +#[derive(Serialize)] +struct ScopeData { + name: String, + pre: Value, + post: Value, + children: Vec<Box<ScopeData>>, +} + +impl ScopeData { + fn new(name: String, pre: Value) -> ScopeData { + ScopeData { + name: name, + pre: pre, + post: Value::Null, + children: vec![], + } + } +} + +struct State { + flow_root: FlowRef, + scope_stack: Vec<Box<ScopeData>>, +} + +/// A layout debugging scope. The entire state of the flow tree +/// will be output at the beginning and end of this scope. +impl Scope { + pub fn new(name: String) -> Scope { + STATE_KEY.with(|ref r| { + if let Some(ref mut state) = *r.borrow_mut() { + let flow_trace = to_value(&state.flow_root.base()).unwrap(); + let data = Box::new(ScopeData::new(name.clone(), flow_trace)); + state.scope_stack.push(data); + } + }); + Scope + } +} + +#[cfg(debug_assertions)] +impl Drop for Scope { + fn drop(&mut self) { + STATE_KEY.with(|ref r| { + if let Some(ref mut state) = *r.borrow_mut() { + let mut current_scope = state.scope_stack.pop().unwrap(); + current_scope.post = to_value(&state.flow_root.base()).unwrap(); + let previous_scope = state.scope_stack.last_mut().unwrap(); + previous_scope.children.push(current_scope); + } + }); + } +} + +/// Generate a unique ID. This is used for items such as Fragment +/// which are often reallocated but represent essentially the +/// same data. +#[cfg(debug_assertions)] +pub fn generate_unique_debug_id() -> u16 { + DEBUG_ID_COUNTER.fetch_add(1, Ordering::SeqCst) as u16 +} + +/// Begin a layout debug trace. If this has not been called, +/// creating debug scopes has no effect. +pub fn begin_trace(flow_root: FlowRef) { + assert!(STATE_KEY.with(|ref r| r.borrow().is_none())); + + STATE_KEY.with(|ref r| { + let flow_trace = to_value(&flow_root.base()).unwrap(); + let state = State { + scope_stack: vec![Box::new(ScopeData::new("root".to_owned(), flow_trace))], + flow_root: flow_root.clone(), + }; + *r.borrow_mut() = Some(state); + }); +} + +/// End the debug layout trace. This will write the layout +/// trace to disk in the current directory. The output +/// file can then be viewed with an external tool. +pub fn end_trace(generation: u32) { + let mut thread_state = STATE_KEY.with(|ref r| r.borrow_mut().take().unwrap()); + assert_eq!(thread_state.scope_stack.len(), 1); + let mut root_scope = thread_state.scope_stack.pop().unwrap(); + root_scope.post = to_value(&thread_state.flow_root.base()).unwrap(); + + let result = to_string(&root_scope).unwrap(); + let mut file = File::create(format!("layout_trace-{}.json", generation)).unwrap(); + file.write_all(result.as_bytes()).unwrap(); +} diff --git a/components/layout_2020/lib.rs b/components/layout_2020/lib.rs index daa3e8897c2..f8afdc79190 100644 --- a/components/layout_2020/lib.rs +++ b/components/layout_2020/lib.rs @@ -1,3 +1,63 @@ /* 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/. */ + +#![deny(unsafe_code)] + +#[macro_use] +extern crate bitflags; +#[macro_use] +extern crate html5ever; +#[macro_use] +extern crate log; +#[macro_use] +extern crate range; +#[macro_use] +extern crate serde; + +#[macro_use] +pub mod layout_debug; + +pub mod animation; +mod block; +pub mod construct; +pub mod context; +pub mod data; +pub mod display_list; +mod flex; +mod floats; +pub mod flow; +mod flow_list; +pub mod flow_ref; +mod fragment; +mod generated_content; +pub mod incremental; +mod inline; +mod linked_list; +mod list_item; +mod model; +mod multicol; +pub mod opaque_node; +pub mod parallel; +mod persistent_list; +pub mod query; +pub mod sequential; +mod table; +mod table_caption; +mod table_cell; +mod table_colgroup; +mod table_row; +mod table_rowgroup; +mod table_wrapper; +mod text; +pub mod traversal; +pub mod wrapper; + +// For unit tests: +pub use self::data::LayoutData; +pub use crate::fragment::Fragment; +pub use crate::fragment::SpecificFragmentInfo; + +// We can't use servo_arc for everything in layout, because the Flow stuff uses +// weak references. +use servo_arc::Arc as ServoArc; diff --git a/components/layout_2020/linked_list.rs b/components/layout_2020/linked_list.rs new file mode 100644 index 00000000000..09e605a3d31 --- /dev/null +++ b/components/layout_2020/linked_list.rs @@ -0,0 +1,21 @@ +/* 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/. */ + +//! Utility functions for doubly-linked lists. + +use std::collections::LinkedList; +use std::mem; + +/// Splits the head off a list in O(1) time, and returns the head. +pub fn split_off_head<T>(list: &mut LinkedList<T>) -> LinkedList<T> { + let tail = list.split_off(1); + mem::replace(list, tail) +} + +/// Prepends the items in the other list to this one, leaving the other list empty. +#[inline] +pub fn prepend_from<T>(this: &mut LinkedList<T>, other: &mut LinkedList<T>) { + other.append(this); + mem::swap(this, other); +} diff --git a/components/layout_2020/list_item.rs b/components/layout_2020/list_item.rs new file mode 100644 index 00000000000..3416ff444b3 --- /dev/null +++ b/components/layout_2020/list_item.rs @@ -0,0 +1,323 @@ +/* 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/. */ + +//! Layout for elements with a CSS `display` property of `list-item`. These elements consist of a +//! block and an extra inline fragment for the marker. + +use crate::block::BlockFlow; +use crate::context::{with_thread_local_font_context, LayoutContext}; +use crate::display_list::items::DisplayListSection; +use crate::display_list::{ + BorderPaintingMode, DisplayListBuildState, StackingContextCollectionState, +}; +use crate::floats::FloatKind; +use crate::flow::{Flow, FlowClass, OpaqueFlow}; +use crate::fragment::Overflow; +use crate::fragment::{ + CoordinateSystem, Fragment, FragmentBorderBoxIterator, GeneratedContentInfo, +}; +use crate::generated_content; +use crate::inline::InlineFlow; +use app_units::Au; +use euclid::Point2D; +use style::computed_values::list_style_type::T as ListStyleType; +use style::computed_values::position::T as Position; +use style::logical_geometry::LogicalSize; +use style::properties::ComputedValues; +use style::servo::restyle_damage::ServoRestyleDamage; + +#[allow(unsafe_code)] +unsafe impl crate::flow::HasBaseFlow for ListItemFlow {} + +/// A block with the CSS `display` property equal to `list-item`. +#[derive(Debug)] +#[repr(C)] +pub struct ListItemFlow { + /// Data common to all block flows. + pub block_flow: BlockFlow, + /// The marker, if outside. (Markers that are inside are instead just fragments on the interior + /// `InlineFlow`.) + pub marker_fragments: Vec<Fragment>, +} + +impl ListItemFlow { + pub fn from_fragments_and_flotation( + main_fragment: Fragment, + marker_fragments: Vec<Fragment>, + flotation: Option<FloatKind>, + ) -> ListItemFlow { + let mut this = ListItemFlow { + block_flow: BlockFlow::from_fragment_and_float_kind(main_fragment, flotation), + marker_fragments: marker_fragments, + }; + + if let Some(ref marker) = this.marker_fragments.first() { + match marker.style().get_list().list_style_type { + ListStyleType::Disc | + ListStyleType::None | + ListStyleType::Circle | + ListStyleType::Square | + ListStyleType::DisclosureOpen | + ListStyleType::DisclosureClosed => {}, + _ => this + .block_flow + .base + .restyle_damage + .insert(ServoRestyleDamage::RESOLVE_GENERATED_CONTENT), + } + } + + this + } + + /// Assign inline size and position for the marker. This is done during the `assign_block_size` + /// traversal because floats will impact the marker position. Therefore we need to have already + /// called `assign_block_size` on the list item's block flow, in order to know which floats + /// impact the position. + /// + /// Per CSS 2.1 § 12.5.1, the marker position is not precisely specified, but it must be on the + /// left side of the content (for ltr direction). However, flowing the marker around floats + /// matches the rendering of Gecko and Blink. + fn assign_marker_inline_sizes(&mut self, layout_context: &LayoutContext) { + let base = &self.block_flow.base; + let available_rect = base.floats.available_rect( + -base.position.size.block, + base.position.size.block, + base.block_container_inline_size, + ); + let mut marker_inline_start = available_rect + .unwrap_or(self.block_flow.fragment.border_box) + .start + .i; + + for marker in self.marker_fragments.iter_mut().rev() { + let container_block_size = self + .block_flow + .explicit_block_containing_size(layout_context.shared_context()); + marker.assign_replaced_inline_size_if_necessary( + base.block_container_inline_size, + container_block_size, + ); + + // Do this now. There's no need to do this in bubble-widths, since markers do not + // contribute to the inline size of this flow. + let intrinsic_inline_sizes = marker.compute_intrinsic_inline_sizes(); + + marker.border_box.size.inline = intrinsic_inline_sizes + .content_intrinsic_sizes + .preferred_inline_size; + marker_inline_start = marker_inline_start - marker.border_box.size.inline; + marker.border_box.start.i = marker_inline_start; + } + } + + fn assign_marker_block_sizes(&mut self, layout_context: &LayoutContext) { + // FIXME(pcwalton): Do this during flow construction, like `InlineFlow` does? + let marker_line_metrics = with_thread_local_font_context(layout_context, |font_context| { + InlineFlow::minimum_line_metrics_for_fragments( + &self.marker_fragments, + font_context, + &*self.block_flow.fragment.style, + ) + }); + + for marker in &mut self.marker_fragments { + marker.assign_replaced_block_size_if_necessary(); + let marker_inline_metrics = marker.aligned_inline_metrics( + layout_context, + &marker_line_metrics, + Some(&marker_line_metrics), + ); + marker.border_box.start.b = + marker_line_metrics.space_above_baseline - marker_inline_metrics.ascent; + } + } +} + +impl Flow for ListItemFlow { + fn class(&self) -> FlowClass { + FlowClass::ListItem + } + + fn as_mut_block(&mut self) -> &mut BlockFlow { + &mut self.block_flow + } + + fn as_block(&self) -> &BlockFlow { + &self.block_flow + } + + fn bubble_inline_sizes(&mut self) { + // The marker contributes no intrinsic inline-size, so… + self.block_flow.bubble_inline_sizes() + } + + fn assign_inline_sizes(&mut self, layout_context: &LayoutContext) { + self.block_flow.assign_inline_sizes(layout_context); + } + + fn assign_block_size(&mut self, layout_context: &LayoutContext) { + self.block_flow.assign_block_size(layout_context); + self.assign_marker_inline_sizes(layout_context); + self.assign_marker_block_sizes(layout_context); + } + + fn compute_stacking_relative_position(&mut self, layout_context: &LayoutContext) { + self.block_flow + .compute_stacking_relative_position(layout_context) + } + + fn place_float_if_applicable<'a>(&mut self) { + self.block_flow.place_float_if_applicable() + } + + 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 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, state: &mut DisplayListBuildState) { + // Draw the marker, if applicable. + for marker in &mut self.marker_fragments { + let stacking_relative_border_box = self + .block_flow + .base + .stacking_relative_border_box_for_display_list(marker); + marker.build_display_list( + state, + stacking_relative_border_box, + BorderPaintingMode::Separate, + DisplayListSection::Content, + self.block_flow.base.clip, + None, + ); + } + + // Draw the rest of the block. + self.block_flow + .build_display_list_for_block(state, BorderPaintingMode::Separate) + } + + fn collect_stacking_contexts(&mut self, state: &mut StackingContextCollectionState) { + self.block_flow.collect_stacking_contexts(state); + } + + fn repair_style(&mut self, new_style: &crate::ServoArc<ComputedValues>) { + self.block_flow.repair_style(new_style) + } + + fn compute_overflow(&self) -> Overflow { + let mut overflow = self.block_flow.compute_overflow(); + let flow_size = self + .block_flow + .base + .position + .size + .to_physical(self.block_flow.base.writing_mode); + let relative_containing_block_size = &self + .block_flow + .base + .early_absolute_position_info + .relative_containing_block_size; + + for fragment in &self.marker_fragments { + overflow.union(&fragment.compute_overflow(&flow_size, &relative_containing_block_size)) + } + overflow + } + + fn generated_containing_block_size(&self, flow: OpaqueFlow) -> LogicalSize<Au> { + self.block_flow.generated_containing_block_size(flow) + } + + /// The 'position' property of this flow. + fn positioning(&self) -> Position { + self.block_flow.positioning() + } + + fn iterate_through_fragment_border_boxes( + &self, + iterator: &mut dyn FragmentBorderBoxIterator, + level: i32, + stacking_context_position: &Point2D<Au>, + ) { + self.block_flow.iterate_through_fragment_border_boxes( + iterator, + level, + stacking_context_position, + ); + + for marker in &self.marker_fragments { + if iterator.should_process(marker) { + iterator.process( + marker, + level, + &marker + .stacking_relative_border_box( + &self.block_flow.base.stacking_relative_position, + &self + .block_flow + .base + .early_absolute_position_info + .relative_containing_block_size, + self.block_flow + .base + .early_absolute_position_info + .relative_containing_block_mode, + CoordinateSystem::Own, + ) + .translate(&stacking_context_position.to_vector()), + ); + } + } + } + + fn mutate_fragments(&mut self, mutator: &mut dyn FnMut(&mut Fragment)) { + self.block_flow.mutate_fragments(mutator); + + for marker in &mut self.marker_fragments { + (*mutator)(marker) + } + } +} + +/// The kind of content that `list-style-type` results in. +pub enum ListStyleTypeContent { + None, + StaticText(char), + GeneratedContent(Box<GeneratedContentInfo>), +} + +impl ListStyleTypeContent { + /// Returns the content to be used for the given value of the `list-style-type` property. + pub fn from_list_style_type(list_style_type: ListStyleType) -> ListStyleTypeContent { + // Just to keep things simple, use a nonbreaking space (Unicode 0xa0) to provide the marker + // separation. + match list_style_type { + ListStyleType::None => ListStyleTypeContent::None, + ListStyleType::Disc | + ListStyleType::Circle | + ListStyleType::Square | + ListStyleType::DisclosureOpen | + ListStyleType::DisclosureClosed => { + let text = generated_content::static_representation(list_style_type); + ListStyleTypeContent::StaticText(text) + }, + _ => ListStyleTypeContent::GeneratedContent(Box::new(GeneratedContentInfo::ListItem)), + } + } +} diff --git a/components/layout_2020/model.rs b/components/layout_2020/model.rs new file mode 100644 index 00000000000..17c3b625af3 --- /dev/null +++ b/components/layout_2020/model.rs @@ -0,0 +1,609 @@ +/* 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/. */ + +//! Borders, padding, and margins. + +use crate::fragment::Fragment; +use app_units::Au; +use euclid::SideOffsets2D; +use std::cmp::{max, min}; +use std::fmt; +use style::logical_geometry::{LogicalMargin, WritingMode}; +use style::properties::ComputedValues; +use style::values::computed::MaxSize; +use style::values::computed::{LengthPercentageOrAuto, Size}; + +/// A collapsible margin. See CSS 2.1 § 8.3.1. +#[derive(Clone, Copy, Debug)] +pub struct AdjoiningMargins { + /// The value of the greatest positive margin. + pub most_positive: Au, + + /// The actual value (not the absolute value) of the negative margin with the largest absolute + /// value. Since this is not the absolute value, this is always zero or negative. + pub most_negative: Au, +} + +impl AdjoiningMargins { + pub fn new() -> AdjoiningMargins { + AdjoiningMargins { + most_positive: Au(0), + most_negative: Au(0), + } + } + + pub fn from_margin(margin_value: Au) -> AdjoiningMargins { + if margin_value >= Au(0) { + AdjoiningMargins { + most_positive: margin_value, + most_negative: Au(0), + } + } else { + AdjoiningMargins { + most_positive: Au(0), + most_negative: margin_value, + } + } + } + + pub fn union(&mut self, other: AdjoiningMargins) { + self.most_positive = max(self.most_positive, other.most_positive); + self.most_negative = min(self.most_negative, other.most_negative) + } + + pub fn collapse(&self) -> Au { + self.most_positive + self.most_negative + } +} + +/// Represents the block-start and block-end margins of a flow with collapsible margins. See CSS 2.1 § 8.3.1. +#[derive(Clone, Copy, Debug)] +pub enum CollapsibleMargins { + /// Margins may not collapse with this flow. + None(Au, Au), + + /// Both the block-start and block-end margins (specified here in that order) may collapse, but the + /// margins do not collapse through this flow. + Collapse(AdjoiningMargins, AdjoiningMargins), + + /// Margins collapse *through* this flow. This means, essentially, that the flow doesn’t + /// have any border, padding, or out-of-flow (floating or positioned) content + CollapseThrough(AdjoiningMargins), +} + +impl CollapsibleMargins { + pub fn new() -> CollapsibleMargins { + CollapsibleMargins::None(Au(0), Au(0)) + } + + /// Returns the amount of margin that should be applied in a noncollapsible context. This is + /// currently used to apply block-start margin for hypothetical boxes, since we do not collapse + /// margins of hypothetical boxes. + pub fn block_start_margin_for_noncollapsible_context(&self) -> Au { + match *self { + CollapsibleMargins::None(block_start, _) => block_start, + CollapsibleMargins::Collapse(ref block_start, _) | + CollapsibleMargins::CollapseThrough(ref block_start) => block_start.collapse(), + } + } + + pub fn block_end_margin_for_noncollapsible_context(&self) -> Au { + match *self { + CollapsibleMargins::None(_, block_end) => block_end, + CollapsibleMargins::Collapse(_, ref block_end) | + CollapsibleMargins::CollapseThrough(ref block_end) => block_end.collapse(), + } + } +} + +enum FinalMarginState { + MarginsCollapseThrough, + BottomMarginCollapses, +} + +pub struct MarginCollapseInfo { + pub state: MarginCollapseState, + pub block_start_margin: AdjoiningMargins, + pub margin_in: AdjoiningMargins, +} + +impl MarginCollapseInfo { + pub fn initialize_block_start_margin( + fragment: &Fragment, + can_collapse_block_start_margin_with_kids: bool, + ) -> MarginCollapseInfo { + MarginCollapseInfo { + state: if can_collapse_block_start_margin_with_kids { + MarginCollapseState::AccumulatingCollapsibleTopMargin + } else { + MarginCollapseState::AccumulatingMarginIn + }, + block_start_margin: AdjoiningMargins::from_margin(fragment.margin.block_start), + margin_in: AdjoiningMargins::new(), + } + } + + pub fn finish_and_compute_collapsible_margins( + mut self, + fragment: &Fragment, + containing_block_size: Option<Au>, + can_collapse_block_end_margin_with_kids: bool, + mut may_collapse_through: bool, + ) -> (CollapsibleMargins, Au) { + let state = match self.state { + MarginCollapseState::AccumulatingCollapsibleTopMargin => { + may_collapse_through = may_collapse_through && + match fragment.style().content_block_size() { + Size::Auto => true, + Size::LengthPercentage(ref lp) => { + lp.is_definitely_zero() || + lp.maybe_to_used_value(containing_block_size).is_none() + }, + }; + + if may_collapse_through { + if fragment.style.min_block_size().is_auto() || + fragment.style().min_block_size().is_definitely_zero() + { + FinalMarginState::MarginsCollapseThrough + } else { + // If the fragment has non-zero min-block-size, margins may not + // collapse through it. + FinalMarginState::BottomMarginCollapses + } + } else { + // If the fragment has an explicitly specified block-size, margins may not + // collapse through it. + FinalMarginState::BottomMarginCollapses + } + }, + MarginCollapseState::AccumulatingMarginIn => FinalMarginState::BottomMarginCollapses, + }; + + // Different logic is needed here depending on whether this flow can collapse its block-end + // margin with its children. + let block_end_margin = fragment.margin.block_end; + if !can_collapse_block_end_margin_with_kids { + match state { + FinalMarginState::MarginsCollapseThrough => { + let advance = self.block_start_margin.collapse(); + self.margin_in + .union(AdjoiningMargins::from_margin(block_end_margin)); + ( + CollapsibleMargins::Collapse(self.block_start_margin, self.margin_in), + advance, + ) + }, + FinalMarginState::BottomMarginCollapses => { + let advance = self.margin_in.collapse(); + self.margin_in + .union(AdjoiningMargins::from_margin(block_end_margin)); + ( + CollapsibleMargins::Collapse(self.block_start_margin, self.margin_in), + advance, + ) + }, + } + } else { + match state { + FinalMarginState::MarginsCollapseThrough => { + self.block_start_margin + .union(AdjoiningMargins::from_margin(block_end_margin)); + ( + CollapsibleMargins::CollapseThrough(self.block_start_margin), + Au(0), + ) + }, + FinalMarginState::BottomMarginCollapses => { + self.margin_in + .union(AdjoiningMargins::from_margin(block_end_margin)); + ( + CollapsibleMargins::Collapse(self.block_start_margin, self.margin_in), + Au(0), + ) + }, + } + } + } + + pub fn current_float_ceiling(&mut self) -> Au { + match self.state { + MarginCollapseState::AccumulatingCollapsibleTopMargin => { + // We do not include the top margin in the float ceiling, because the float flow + // needs to be positioned relative to our *border box*, not our margin box. See + // `tests/ref/float_under_top_margin_a.html`. + Au(0) + }, + MarginCollapseState::AccumulatingMarginIn => self.margin_in.collapse(), + } + } + + /// Adds the child's potentially collapsible block-start margin to the current margin state and + /// advances the Y offset by the appropriate amount to handle that margin. Returns the amount + /// that should be added to the Y offset during block layout. + pub fn advance_block_start_margin( + &mut self, + child_collapsible_margins: &CollapsibleMargins, + can_collapse_block_start_margin: bool, + ) -> Au { + if !can_collapse_block_start_margin { + self.state = MarginCollapseState::AccumulatingMarginIn + } + + match (self.state, *child_collapsible_margins) { + ( + MarginCollapseState::AccumulatingCollapsibleTopMargin, + CollapsibleMargins::None(block_start, _), + ) => { + self.state = MarginCollapseState::AccumulatingMarginIn; + block_start + }, + ( + MarginCollapseState::AccumulatingCollapsibleTopMargin, + CollapsibleMargins::Collapse(block_start, _), + ) => { + self.block_start_margin.union(block_start); + self.state = MarginCollapseState::AccumulatingMarginIn; + Au(0) + }, + ( + MarginCollapseState::AccumulatingMarginIn, + CollapsibleMargins::None(block_start, _), + ) => { + let previous_margin_value = self.margin_in.collapse(); + self.margin_in = AdjoiningMargins::new(); + previous_margin_value + block_start + }, + ( + MarginCollapseState::AccumulatingMarginIn, + CollapsibleMargins::Collapse(block_start, _), + ) => { + self.margin_in.union(block_start); + let margin_value = self.margin_in.collapse(); + self.margin_in = AdjoiningMargins::new(); + margin_value + }, + (_, CollapsibleMargins::CollapseThrough(_)) => { + // For now, we ignore this; this will be handled by `advance_block_end_margin` + // below. + Au(0) + }, + } + } + + /// Adds the child's potentially collapsible block-end margin to the current margin state and + /// advances the Y offset by the appropriate amount to handle that margin. Returns the amount + /// that should be added to the Y offset during block layout. + pub fn advance_block_end_margin( + &mut self, + child_collapsible_margins: &CollapsibleMargins, + ) -> Au { + match (self.state, *child_collapsible_margins) { + ( + MarginCollapseState::AccumulatingCollapsibleTopMargin, + CollapsibleMargins::None(..), + ) | + ( + MarginCollapseState::AccumulatingCollapsibleTopMargin, + CollapsibleMargins::Collapse(..), + ) => { + // Can't happen because the state will have been replaced with + // `MarginCollapseState::AccumulatingMarginIn` above. + panic!("should not be accumulating collapsible block_start margins anymore!") + }, + ( + MarginCollapseState::AccumulatingCollapsibleTopMargin, + CollapsibleMargins::CollapseThrough(margin), + ) => { + self.block_start_margin.union(margin); + Au(0) + }, + (MarginCollapseState::AccumulatingMarginIn, CollapsibleMargins::None(_, block_end)) => { + assert_eq!(self.margin_in.most_positive, Au(0)); + assert_eq!(self.margin_in.most_negative, Au(0)); + block_end + }, + ( + MarginCollapseState::AccumulatingMarginIn, + CollapsibleMargins::Collapse(_, block_end), + ) | + ( + MarginCollapseState::AccumulatingMarginIn, + CollapsibleMargins::CollapseThrough(block_end), + ) => { + self.margin_in.union(block_end); + Au(0) + }, + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum MarginCollapseState { + /// We are accumulating margin on the logical top of this flow. + AccumulatingCollapsibleTopMargin, + /// We are accumulating margin between two blocks. + AccumulatingMarginIn, +} + +/// Intrinsic inline-sizes, which consist of minimum and preferred. +#[derive(Clone, Copy, Serialize)] +pub struct IntrinsicISizes { + /// The *minimum inline-size* of the content. + pub minimum_inline_size: Au, + /// The *preferred inline-size* of the content. + pub preferred_inline_size: Au, +} + +impl fmt::Debug for IntrinsicISizes { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "min={:?}, pref={:?}", + self.minimum_inline_size, self.preferred_inline_size + ) + } +} + +impl IntrinsicISizes { + pub fn new() -> IntrinsicISizes { + IntrinsicISizes { + minimum_inline_size: Au(0), + preferred_inline_size: Au(0), + } + } +} + +/// The temporary result of the computation of intrinsic inline-sizes. +#[derive(Debug)] +pub struct IntrinsicISizesContribution { + /// Intrinsic sizes for the content only (not counting borders, padding, or margins). + pub content_intrinsic_sizes: IntrinsicISizes, + /// The inline size of borders and padding, as well as margins if appropriate. + pub surrounding_size: Au, +} + +impl IntrinsicISizesContribution { + /// Creates and initializes an inline size computation with all sizes set to zero. + pub fn new() -> IntrinsicISizesContribution { + IntrinsicISizesContribution { + content_intrinsic_sizes: IntrinsicISizes::new(), + surrounding_size: Au(0), + } + } + + /// Adds the content intrinsic sizes and the surrounding size together to yield the final + /// intrinsic size computation. + pub fn finish(self) -> IntrinsicISizes { + IntrinsicISizes { + minimum_inline_size: self.content_intrinsic_sizes.minimum_inline_size + + self.surrounding_size, + preferred_inline_size: self.content_intrinsic_sizes.preferred_inline_size + + self.surrounding_size, + } + } + + /// Updates the computation so that the minimum is the maximum of the current minimum and the + /// given minimum and the preferred is the sum of the current preferred and the given + /// preferred. This is used when laying out fragments in the inline direction. + /// + /// FIXME(pcwalton): This is incorrect when the inline fragment contains forced line breaks + /// (e.g. `<br>` or `white-space: pre`). + pub fn union_inline(&mut self, sizes: &IntrinsicISizes) { + self.content_intrinsic_sizes.minimum_inline_size = max( + self.content_intrinsic_sizes.minimum_inline_size, + sizes.minimum_inline_size, + ); + self.content_intrinsic_sizes.preferred_inline_size = + self.content_intrinsic_sizes.preferred_inline_size + sizes.preferred_inline_size + } + + /// Updates the computation so that the minimum is the sum of the current minimum and the + /// given minimum and the preferred is the sum of the current preferred and the given + /// preferred. This is used when laying out fragments in the inline direction when + /// `white-space` is `pre` or `nowrap`. + pub fn union_nonbreaking_inline(&mut self, sizes: &IntrinsicISizes) { + self.content_intrinsic_sizes.minimum_inline_size = + self.content_intrinsic_sizes.minimum_inline_size + sizes.minimum_inline_size; + self.content_intrinsic_sizes.preferred_inline_size = + self.content_intrinsic_sizes.preferred_inline_size + sizes.preferred_inline_size + } + + /// Updates the computation so that the minimum is the maximum of the current minimum and the + /// given minimum and the preferred is the maximum of the current preferred and the given + /// preferred. This can be useful when laying out fragments in the block direction (but note + /// that it does not take floats into account, so `BlockFlow` does not use it). + /// + /// This is used when contributing the intrinsic sizes for individual fragments. + pub fn union_block(&mut self, sizes: &IntrinsicISizes) { + self.content_intrinsic_sizes.minimum_inline_size = max( + self.content_intrinsic_sizes.minimum_inline_size, + sizes.minimum_inline_size, + ); + self.content_intrinsic_sizes.preferred_inline_size = max( + self.content_intrinsic_sizes.preferred_inline_size, + sizes.preferred_inline_size, + ) + } +} + +/// Useful helper data type when computing values for blocks and positioned elements. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum MaybeAuto { + Auto, + Specified(Au), +} + +impl MaybeAuto { + #[inline] + pub fn from_style(length: LengthPercentageOrAuto, containing_length: Au) -> MaybeAuto { + match length { + LengthPercentageOrAuto::Auto => MaybeAuto::Auto, + LengthPercentageOrAuto::LengthPercentage(ref lp) => { + MaybeAuto::Specified(lp.to_used_value(containing_length)) + }, + } + } + + #[inline] + pub fn from_option(au: Option<Au>) -> MaybeAuto { + match au { + Some(l) => MaybeAuto::Specified(l), + _ => MaybeAuto::Auto, + } + } + + #[inline] + pub fn to_option(&self) -> Option<Au> { + match *self { + MaybeAuto::Specified(value) => Some(value), + MaybeAuto::Auto => None, + } + } + + #[inline] + pub fn specified_or_default(&self, default: Au) -> Au { + match *self { + MaybeAuto::Auto => default, + MaybeAuto::Specified(value) => value, + } + } + + #[inline] + pub fn specified_or_zero(&self) -> Au { + self.specified_or_default(Au::new(0)) + } + + #[inline] + pub fn is_auto(&self) -> bool { + match *self { + MaybeAuto::Auto => true, + MaybeAuto::Specified(..) => false, + } + } + + #[inline] + pub fn map<F>(&self, mapper: F) -> MaybeAuto + where + F: FnOnce(Au) -> Au, + { + match *self { + MaybeAuto::Auto => MaybeAuto::Auto, + MaybeAuto::Specified(value) => MaybeAuto::Specified(mapper(value)), + } + } +} + +/// Receive an optional container size and return used value for width or height. +/// +/// `style_length`: content size as given in the CSS. +pub fn style_length(style_length: Size, container_size: Option<Au>) -> MaybeAuto { + match style_length { + Size::Auto => MaybeAuto::Auto, + Size::LengthPercentage(ref lp) => { + MaybeAuto::from_option(lp.0.maybe_to_used_value(container_size)) + }, + } +} + +#[inline] +pub fn padding_from_style( + style: &ComputedValues, + containing_block_inline_size: Au, + writing_mode: WritingMode, +) -> LogicalMargin<Au> { + let padding_style = style.get_padding(); + LogicalMargin::from_physical( + writing_mode, + SideOffsets2D::new( + padding_style + .padding_top + .to_used_value(containing_block_inline_size), + padding_style + .padding_right + .to_used_value(containing_block_inline_size), + padding_style + .padding_bottom + .to_used_value(containing_block_inline_size), + padding_style + .padding_left + .to_used_value(containing_block_inline_size), + ), + ) +} + +/// Returns the explicitly-specified margin lengths from the given style. Percentage and auto +/// margins are returned as zero. +/// +/// This is used when calculating intrinsic inline sizes. +#[inline] +pub fn specified_margin_from_style( + style: &ComputedValues, + writing_mode: WritingMode, +) -> LogicalMargin<Au> { + let margin_style = style.get_margin(); + LogicalMargin::from_physical( + writing_mode, + SideOffsets2D::new( + MaybeAuto::from_style(margin_style.margin_top, Au(0)).specified_or_zero(), + MaybeAuto::from_style(margin_style.margin_right, Au(0)).specified_or_zero(), + MaybeAuto::from_style(margin_style.margin_bottom, Au(0)).specified_or_zero(), + MaybeAuto::from_style(margin_style.margin_left, Au(0)).specified_or_zero(), + ), + ) +} + +/// A min-size and max-size constraint. The constructor has a optional `border` +/// parameter, and when it is present the constraint will be subtracted. This is +/// used to adjust the constraint for `box-sizing: border-box`, and when you do so +/// make sure the size you want to clamp is intended to be used for content box. +#[derive(Clone, Copy, Debug, Serialize)] +pub struct SizeConstraint { + min_size: Au, + max_size: Option<Au>, +} + +impl SizeConstraint { + /// Create a `SizeConstraint` for an axis. + pub fn new( + container_size: Option<Au>, + min_size: Size, + max_size: MaxSize, + border: Option<Au>, + ) -> SizeConstraint { + let mut min_size = match min_size { + Size::Auto => Au(0), + Size::LengthPercentage(ref lp) => { + lp.maybe_to_used_value(container_size).unwrap_or(Au(0)) + }, + }; + + let mut max_size = match max_size { + MaxSize::None => None, + MaxSize::LengthPercentage(ref lp) => lp.maybe_to_used_value(container_size), + }; + + // Make sure max size is not smaller than min size. + max_size = max_size.map(|x| max(x, min_size)); + + if let Some(border) = border { + min_size = max(min_size - border, Au(0)); + max_size = max_size.map(|x| max(x - border, Au(0))); + } + + SizeConstraint { min_size, max_size } + } + + /// Clamp the given size by the given min size and max size constraint. + pub fn clamp(&self, other: Au) -> Au { + if other < self.min_size { + self.min_size + } else { + match self.max_size { + Some(max_size) if max_size < other => max_size, + _ => other, + } + } + } +} diff --git a/components/layout_2020/multicol.rs b/components/layout_2020/multicol.rs new file mode 100644 index 00000000000..d49e5f1d79f --- /dev/null +++ b/components/layout_2020/multicol.rs @@ -0,0 +1,384 @@ +/* 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/. */ + +//! CSS Multi-column layout http://dev.w3.org/csswg/css-multicol/ + +use crate::block::BlockFlow; +use crate::context::LayoutContext; +use crate::display_list::{DisplayListBuildState, StackingContextCollectionState}; +use crate::floats::FloatKind; +use crate::flow::{Flow, FlowClass, FragmentationContext, GetBaseFlow, OpaqueFlow}; +use crate::fragment::{Fragment, FragmentBorderBoxIterator, Overflow}; +use crate::ServoArc; +use app_units::Au; +use euclid::{Point2D, Vector2D}; +use gfx_traits::print_tree::PrintTree; +use std::cmp::{max, min}; +use std::fmt; +use std::sync::Arc; +use style::logical_geometry::LogicalSize; +use style::properties::ComputedValues; +use style::values::computed::length::{ + MaxSize, NonNegativeLengthOrAuto, NonNegativeLengthPercentageOrNormal, Size, +}; +use style::values::generics::column::ColumnCount; + +#[allow(unsafe_code)] +unsafe impl crate::flow::HasBaseFlow for MulticolFlow {} + +#[repr(C)] +pub struct MulticolFlow { + pub block_flow: BlockFlow, + + /// Length between the inline-start edge of a column and that of the next. + /// That is, the used column-width + used column-gap. + pub column_pitch: Au, +} + +#[allow(unsafe_code)] +unsafe impl crate::flow::HasBaseFlow for MulticolColumnFlow {} + +#[repr(C)] +pub struct MulticolColumnFlow { + pub block_flow: BlockFlow, +} + +impl MulticolFlow { + pub fn from_fragment(fragment: Fragment, float_kind: Option<FloatKind>) -> MulticolFlow { + MulticolFlow { + block_flow: BlockFlow::from_fragment_and_float_kind(fragment, float_kind), + column_pitch: Au(0), + } + } +} + +impl MulticolColumnFlow { + pub fn from_fragment(fragment: Fragment) -> MulticolColumnFlow { + MulticolColumnFlow { + block_flow: BlockFlow::from_fragment(fragment), + } + } +} + +impl Flow for MulticolFlow { + fn class(&self) -> FlowClass { + FlowClass::Multicol + } + + fn as_mut_block(&mut self) -> &mut BlockFlow { + &mut self.block_flow + } + + fn as_block(&self) -> &BlockFlow { + &self.block_flow + } + + fn bubble_inline_sizes(&mut self) { + // FIXME(SimonSapin) http://dev.w3.org/csswg/css-sizing/#multicol-intrinsic + self.block_flow.bubble_inline_sizes(); + } + + fn assign_inline_sizes(&mut self, layout_context: &LayoutContext) { + debug!( + "assign_inline_sizes({}): assigning inline_size for flow", + "multicol" + ); + let shared_context = layout_context.shared_context(); + self.block_flow.compute_inline_sizes(shared_context); + + // Move in from the inline-start border edge. + let inline_start_content_edge = self.block_flow.fragment.border_box.start.i + + self.block_flow.fragment.border_padding.inline_start; + + // Distance from the inline-end margin edge to the inline-end content edge. + let inline_end_content_edge = self.block_flow.fragment.margin.inline_end + + self.block_flow.fragment.border_padding.inline_end; + + self.block_flow.assign_inline_sizes(layout_context); + let padding_and_borders = self.block_flow.fragment.border_padding.inline_start_end(); + let content_inline_size = + self.block_flow.fragment.border_box.size.inline - padding_and_borders; + let column_width; + { + let style = &self.block_flow.fragment.style; + let column_gap = match style.get_position().column_gap { + NonNegativeLengthPercentageOrNormal::LengthPercentage(len) => { + len.0.to_pixel_length(content_inline_size).into() + }, + NonNegativeLengthPercentageOrNormal::Normal => { + self.block_flow.fragment.style.get_font().font_size.size() + }, + }; + + let column_style = style.get_column(); + let mut column_count; + if let NonNegativeLengthOrAuto::LengthPercentage(column_width) = + column_style.column_width + { + let column_width = Au::from(column_width); + column_count = max( + 1, + (content_inline_size + column_gap).0 / (column_width + column_gap).0, + ); + if let ColumnCount::Integer(specified_column_count) = column_style.column_count { + column_count = min(column_count, specified_column_count.0 as i32); + } + } else { + column_count = match column_style.column_count { + ColumnCount::Integer(n) => n.0, + _ => unreachable!(), + } + } + column_width = max( + Au(0), + (content_inline_size + column_gap) / column_count - column_gap, + ); + self.column_pitch = column_width + column_gap; + } + + self.block_flow.fragment.border_box.size.inline = content_inline_size + padding_and_borders; + + self.block_flow.propagate_assigned_inline_size_to_children( + shared_context, + inline_start_content_edge, + inline_end_content_edge, + column_width, + |_, _, _, _, _, _| {}, + ); + } + + fn assign_block_size(&mut self, ctx: &LayoutContext) { + debug!("assign_block_size: assigning block_size for multicol"); + + let fragmentation_context = Some(FragmentationContext { + this_fragment_is_empty: true, + available_block_size: { + let style = &self.block_flow.fragment.style; + let size = match style.content_block_size() { + Size::Auto => None, + Size::LengthPercentage(ref lp) => lp.maybe_to_used_value(None), + }; + let size = size.or_else(|| match style.max_block_size() { + MaxSize::None => None, + MaxSize::LengthPercentage(ref lp) => lp.maybe_to_used_value(None), + }); + + size.unwrap_or_else(|| { + // FIXME: do column balancing instead + // FIXME: (until column balancing) substract margins/borders/padding + LogicalSize::from_physical( + self.block_flow.base.writing_mode, + ctx.shared_context().viewport_size(), + ) + .block + }) + }, + }); + + // Before layout, everything is in a single "column" + assert_eq!(self.block_flow.base.children.len(), 1); + let mut column = self.block_flow.base.children.pop_front_arc().unwrap(); + + // Pretend there is no children for this: + self.block_flow.assign_block_size(ctx); + + loop { + let remaining = Arc::get_mut(&mut column) + .unwrap() + .fragment(ctx, fragmentation_context); + self.block_flow.base.children.push_back_arc(column); + column = match remaining { + Some(remaining) => remaining, + None => break, + }; + } + } + + fn compute_stacking_relative_position(&mut self, layout_context: &LayoutContext) { + self.block_flow + .compute_stacking_relative_position(layout_context); + let pitch = LogicalSize::new(self.block_flow.base.writing_mode, self.column_pitch, Au(0)); + let pitch = pitch.to_physical(self.block_flow.base.writing_mode); + for (i, child) in self.block_flow.base.children.iter_mut().enumerate() { + let point = &mut child.mut_base().stacking_relative_position; + *point = *point + Vector2D::new(pitch.width * i as i32, pitch.height * i as i32); + } + } + + 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, state: &mut DisplayListBuildState) { + debug!("build_display_list_multicol"); + self.block_flow.build_display_list(state); + } + + fn collect_stacking_contexts(&mut self, state: &mut StackingContextCollectionState) { + self.block_flow.collect_stacking_contexts(state); + } + + fn repair_style(&mut self, new_style: &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 dyn 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 dyn 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 Flow for MulticolColumnFlow { + fn class(&self) -> FlowClass { + FlowClass::MulticolColumn + } + + fn as_mut_block(&mut self) -> &mut BlockFlow { + &mut self.block_flow + } + + fn as_block(&self) -> &BlockFlow { + &self.block_flow + } + + fn bubble_inline_sizes(&mut self) { + self.block_flow.bubble_inline_sizes(); + } + + fn assign_inline_sizes(&mut self, layout_context: &LayoutContext) { + debug!( + "assign_inline_sizes({}): assigning inline_size for flow", + "multicol column" + ); + self.block_flow.assign_inline_sizes(layout_context); + } + + fn assign_block_size(&mut self, ctx: &LayoutContext) { + debug!("assign_block_size: assigning block_size for multicol column"); + self.block_flow.assign_block_size(ctx); + } + + fn fragment( + &mut self, + layout_context: &LayoutContext, + fragmentation_context: Option<FragmentationContext>, + ) -> Option<Arc<dyn Flow>> { + Flow::fragment(&mut self.block_flow, layout_context, fragmentation_context) + } + + 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, state: &mut DisplayListBuildState) { + debug!("build_display_list_multicol column"); + self.block_flow.build_display_list(state); + } + + fn collect_stacking_contexts(&mut self, state: &mut StackingContextCollectionState) { + self.block_flow.collect_stacking_contexts(state); + } + + fn repair_style(&mut self, new_style: &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 dyn 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 dyn 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 MulticolFlow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "MulticolFlow: {:?}", self.block_flow) + } +} + +impl fmt::Debug for MulticolColumnFlow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "MulticolColumnFlow: {:?}", self.block_flow) + } +} diff --git a/components/layout_2020/opaque_node.rs b/components/layout_2020/opaque_node.rs new file mode 100644 index 00000000000..b48820052e9 --- /dev/null +++ b/components/layout_2020/opaque_node.rs @@ -0,0 +1,19 @@ +/* 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/. */ + +use crate::display_list::items::OpaqueNode; +use libc::c_void; +use script_traits::UntrustedNodeAddress; + +pub trait OpaqueNodeMethods { + /// Converts this node to an `UntrustedNodeAddress`. An `UntrustedNodeAddress` is just the type + /// of node that script expects to receive in a hit test. + fn to_untrusted_node_address(&self) -> UntrustedNodeAddress; +} + +impl OpaqueNodeMethods for OpaqueNode { + fn to_untrusted_node_address(&self) -> UntrustedNodeAddress { + UntrustedNodeAddress(self.0 as *const c_void) + } +} diff --git a/components/layout_2020/parallel.rs b/components/layout_2020/parallel.rs new file mode 100644 index 00000000000..4ad10b3db11 --- /dev/null +++ b/components/layout_2020/parallel.rs @@ -0,0 +1,232 @@ +/* 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/. */ + +//! Implements parallel traversals over the DOM and flow trees. +//! +//! This code is highly unsafe. Keep this file small and easy to audit. + +#![allow(unsafe_code)] + +use crate::block::BlockFlow; +use crate::context::LayoutContext; +use crate::flow::{Flow, GetBaseFlow}; +use crate::flow_ref::FlowRef; +use crate::traversal::{AssignBSizes, AssignISizes, BubbleISizes}; +use crate::traversal::{PostorderFlowTraversal, PreorderFlowTraversal}; +use profile_traits::time::{self, profile, TimerMetadata}; +use servo_config::opts; +use smallvec::SmallVec; +use std::mem; +use std::ptr; +use std::sync::atomic::{AtomicIsize, Ordering}; + +/// Traversal chunk size. +const CHUNK_SIZE: usize = 16; + +pub type FlowList = SmallVec<[UnsafeFlow; CHUNK_SIZE]>; + +/// Vtable + pointer representation of a Flow trait object. +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct UnsafeFlow(*const dyn Flow); + +unsafe impl Sync for UnsafeFlow {} +unsafe impl Send for UnsafeFlow {} + +fn null_unsafe_flow() -> UnsafeFlow { + UnsafeFlow(ptr::null::<BlockFlow>()) +} + +pub fn mut_owned_flow_to_unsafe_flow(flow: *mut FlowRef) -> UnsafeFlow { + unsafe { UnsafeFlow(&**flow) } +} + +/// Information that we need stored in each flow. +pub struct FlowParallelInfo { + /// The number of children that still need work done. + pub children_count: AtomicIsize, + /// The address of the parent flow. + pub parent: UnsafeFlow, +} + +impl FlowParallelInfo { + pub fn new() -> FlowParallelInfo { + FlowParallelInfo { + children_count: AtomicIsize::new(0), + parent: null_unsafe_flow(), + } + } +} + +/// Process current flow and potentially traverse its ancestors. +/// +/// If we are the last child that finished processing, recursively process +/// our parent. Else, stop. Also, stop at the root. +/// +/// Thus, if we start with all the leaves of a tree, we end up traversing +/// the whole tree bottom-up because each parent will be processed exactly +/// once (by the last child that finishes processing). +/// +/// The only communication between siblings is that they both +/// fetch-and-subtract the parent's children count. +fn bottom_up_flow(mut unsafe_flow: UnsafeFlow, assign_bsize_traversal: &AssignBSizes) { + loop { + // Get a real flow. + let flow: &mut dyn Flow = unsafe { mem::transmute(unsafe_flow) }; + + // Perform the appropriate traversal. + if assign_bsize_traversal.should_process(flow) { + assign_bsize_traversal.process(flow); + } + + let base = flow.mut_base(); + + // Reset the count of children for the next layout traversal. + base.parallel + .children_count + .store(base.children.len() as isize, Ordering::Relaxed); + + // Possibly enqueue the parent. + let unsafe_parent = base.parallel.parent; + if unsafe_parent == null_unsafe_flow() { + // We're done! + break; + } + + // No, we're not at the root yet. Then are we the last child + // of our parent to finish processing? If so, we can continue + // on with our parent; otherwise, we've gotta wait. + let parent: &mut dyn Flow = unsafe { &mut *(unsafe_parent.0 as *mut dyn Flow) }; + let parent_base = parent.mut_base(); + if parent_base + .parallel + .children_count + .fetch_sub(1, Ordering::Relaxed) == + 1 + { + // We were the last child of our parent. Reflow our parent. + unsafe_flow = unsafe_parent + } else { + // Stop. + break; + } + } +} + +fn top_down_flow<'scope>( + unsafe_flows: &[UnsafeFlow], + pool: &'scope rayon::ThreadPool, + scope: &rayon::ScopeFifo<'scope>, + assign_isize_traversal: &'scope AssignISizes, + assign_bsize_traversal: &'scope AssignBSizes, +) { + let mut discovered_child_flows = FlowList::new(); + + for unsafe_flow in unsafe_flows { + let mut had_children = false; + unsafe { + // Get a real flow. + let flow: &mut dyn Flow = mem::transmute(*unsafe_flow); + flow.mut_base().thread_id = pool.current_thread_index().unwrap() as u8; + + if assign_isize_traversal.should_process(flow) { + // Perform the appropriate traversal. + assign_isize_traversal.process(flow); + } + + // Possibly enqueue the children. + for kid in flow.mut_base().child_iter_mut() { + had_children = true; + discovered_child_flows.push(UnsafeFlow(kid)); + } + } + + // If there were no more children, start assigning block-sizes. + if !had_children { + bottom_up_flow(*unsafe_flow, &assign_bsize_traversal) + } + } + + if discovered_child_flows.is_empty() { + return; + } + + if discovered_child_flows.len() <= CHUNK_SIZE { + // We can handle all the children in this work unit. + top_down_flow( + &discovered_child_flows, + pool, + scope, + &assign_isize_traversal, + &assign_bsize_traversal, + ); + } else { + // Spawn a new work unit for each chunk after the first. + let mut chunks = discovered_child_flows.chunks(CHUNK_SIZE); + let first_chunk = chunks.next(); + for chunk in chunks { + let nodes = chunk.iter().cloned().collect::<FlowList>(); + scope.spawn_fifo(move |scope| { + top_down_flow( + &nodes, + pool, + scope, + &assign_isize_traversal, + &assign_bsize_traversal, + ); + }); + } + if let Some(chunk) = first_chunk { + top_down_flow( + chunk, + pool, + scope, + &assign_isize_traversal, + &assign_bsize_traversal, + ); + } + } +} + +/// Run the main layout passes in parallel. +pub fn reflow( + root: &mut dyn Flow, + profiler_metadata: Option<TimerMetadata>, + time_profiler_chan: time::ProfilerChan, + context: &LayoutContext, + queue: &rayon::ThreadPool, +) { + if opts::get().bubble_inline_sizes_separately { + let bubble_inline_sizes = BubbleISizes { + layout_context: &context, + }; + bubble_inline_sizes.traverse(root); + } + + let assign_isize_traversal = &AssignISizes { + layout_context: &context, + }; + let assign_bsize_traversal = &AssignBSizes { + layout_context: &context, + }; + let nodes = [UnsafeFlow(root)]; + + queue.install(move || { + rayon::scope_fifo(move |scope| { + profile( + time::ProfilerCategory::LayoutParallelWarmup, + profiler_metadata, + time_profiler_chan, + move || { + top_down_flow( + &nodes, + queue, + scope, + assign_isize_traversal, + assign_bsize_traversal, + ); + }, + ); + }); + }); +} diff --git a/components/layout_2020/persistent_list.rs b/components/layout_2020/persistent_list.rs new file mode 100644 index 00000000000..16bbc319ea0 --- /dev/null +++ b/components/layout_2020/persistent_list.rs @@ -0,0 +1,101 @@ +/* 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/. */ + +//! A persistent, thread-safe singly-linked list. + +use std::sync::Arc; + +pub struct PersistentList<T> { + head: PersistentListLink<T>, + length: usize, +} + +struct PersistentListEntry<T> { + value: T, + next: PersistentListLink<T>, +} + +type PersistentListLink<T> = Option<Arc<PersistentListEntry<T>>>; + +impl<T> PersistentList<T> +where + T: Send + Sync, +{ + #[inline] + pub fn new() -> PersistentList<T> { + PersistentList { + head: None, + length: 0, + } + } + + #[inline] + pub fn len(&self) -> usize { + self.length + } + + #[inline] + pub fn front(&self) -> Option<&T> { + self.head.as_ref().map(|head| &head.value) + } + + #[inline] + pub fn prepend_elem(&self, value: T) -> PersistentList<T> { + PersistentList { + head: Some(Arc::new(PersistentListEntry { + value: value, + next: self.head.clone(), + })), + length: self.length + 1, + } + } + + #[inline] + pub fn iter(&self) -> PersistentListIterator<T> { + // This could clone (and would not need the lifetime if it did), but then it would incur + // atomic operations on every call to `.next()`. Bad. + PersistentListIterator { + entry: self.head.as_ref().map(|head| &**head), + } + } +} + +impl<T> Clone for PersistentList<T> +where + T: Send + Sync, +{ + fn clone(&self) -> PersistentList<T> { + // This establishes the persistent nature of this list: we can clone a list by just cloning + // its head. + PersistentList { + head: self.head.clone(), + length: self.length, + } + } +} + +pub struct PersistentListIterator<'a, T> +where + T: Send + Sync, +{ + entry: Option<&'a PersistentListEntry<T>>, +} + +impl<'a, T> Iterator for PersistentListIterator<'a, T> +where + T: Send + Sync + 'static, +{ + type Item = &'a T; + + #[inline] + fn next(&mut self) -> Option<&'a T> { + let entry = self.entry?; + let value = &entry.value; + self.entry = match entry.next { + None => None, + Some(ref entry) => Some(&**entry), + }; + Some(value) + } +} diff --git a/components/layout_2020/query.rs b/components/layout_2020/query.rs new file mode 100644 index 00000000000..789056753d5 --- /dev/null +++ b/components/layout_2020/query.rs @@ -0,0 +1,1111 @@ +/* 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/. */ + +//! Utilities for querying the layout, as needed by the layout thread. + +use crate::construct::ConstructionResult; +use crate::context::LayoutContext; +use crate::display_list::items::{DisplayList, OpaqueNode, ScrollOffsetMap}; +use crate::display_list::IndexableText; +use crate::flow::{Flow, GetBaseFlow}; +use crate::fragment::{Fragment, FragmentBorderBoxIterator, SpecificFragmentInfo}; +use crate::inline::InlineFragmentNodeFlags; +use crate::opaque_node::OpaqueNodeMethods; +use crate::sequential; +use crate::wrapper::LayoutNodeLayoutData; +use app_units::Au; +use euclid::{Point2D, Rect, Size2D, Vector2D}; +use ipc_channel::ipc::IpcSender; +use msg::constellation_msg::PipelineId; +use script_layout_interface::rpc::TextIndexResponse; +use script_layout_interface::rpc::{ContentBoxResponse, ContentBoxesResponse, LayoutRPC}; +use script_layout_interface::rpc::{NodeGeometryResponse, NodeScrollIdResponse}; +use script_layout_interface::rpc::{OffsetParentResponse, ResolvedStyleResponse, StyleResponse}; +use script_layout_interface::wrapper_traits::{ + LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode, +}; +use script_layout_interface::StyleData; +use script_layout_interface::{LayoutElementType, LayoutNodeType}; +use script_traits::LayoutMsg as ConstellationMsg; +use script_traits::UntrustedNodeAddress; +use std::cmp::{max, min}; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; +use style::computed_values::display::T as Display; +use style::computed_values::position::T as Position; +use style::computed_values::visibility::T as Visibility; +use style::context::{StyleContext, ThreadLocalStyleContext}; +use style::dom::TElement; +use style::logical_geometry::{BlockFlowDirection, InlineBaseDirection, WritingMode}; +use style::properties::{style_structs, LonghandId, PropertyDeclarationId, PropertyId}; +use style::selector_parser::PseudoElement; +use style_traits::ToCss; +use webrender_api::ExternalScrollId; + +/// Mutable data belonging to the LayoutThread. +/// +/// This needs to be protected by a mutex so we can do fast RPCs. +pub struct LayoutThreadData { + /// The channel on which messages can be sent to the constellation. + pub constellation_chan: IpcSender<ConstellationMsg>, + + /// The root stacking context. + pub display_list: Option<DisplayList>, + + pub indexable_text: IndexableText, + + /// A queued response for the union of the content boxes of a node. + pub content_box_response: Option<Rect<Au>>, + + /// A queued response for the content boxes of a node. + pub content_boxes_response: Vec<Rect<Au>>, + + /// A queued response for the client {top, left, width, height} of a node in pixels. + pub client_rect_response: Rect<i32>, + + /// A queued response for the scroll id for a given node. + pub scroll_id_response: Option<ExternalScrollId>, + + /// A queued response for the scroll {top, left, width, height} of a node in pixels. + pub scroll_area_response: Rect<i32>, + + /// A queued response for the resolved style property of an element. + pub resolved_style_response: String, + + /// A queued response for the offset parent/rect of a node. + pub offset_parent_response: OffsetParentResponse, + + /// A queued response for the style of a node. + pub style_response: StyleResponse, + + /// Scroll offsets of scrolling regions. + pub scroll_offsets: ScrollOffsetMap, + + /// Index in a text fragment. We need this do determine the insertion point. + pub text_index_response: TextIndexResponse, + + /// A queued response for the list of nodes at a given point. + pub nodes_from_point_response: Vec<UntrustedNodeAddress>, + + /// A queued response for the inner text of a given element. + pub element_inner_text_response: String, +} + +pub struct LayoutRPCImpl(pub Arc<Mutex<LayoutThreadData>>); + +// https://drafts.csswg.org/cssom-view/#overflow-directions +fn overflow_direction(writing_mode: &WritingMode) -> OverflowDirection { + match ( + writing_mode.block_flow_direction(), + writing_mode.inline_base_direction(), + ) { + (BlockFlowDirection::TopToBottom, InlineBaseDirection::LeftToRight) | + (BlockFlowDirection::LeftToRight, InlineBaseDirection::LeftToRight) => { + OverflowDirection::RightAndDown + }, + (BlockFlowDirection::TopToBottom, InlineBaseDirection::RightToLeft) | + (BlockFlowDirection::RightToLeft, InlineBaseDirection::LeftToRight) => { + OverflowDirection::LeftAndDown + }, + (BlockFlowDirection::RightToLeft, InlineBaseDirection::RightToLeft) => { + OverflowDirection::LeftAndUp + }, + (BlockFlowDirection::LeftToRight, InlineBaseDirection::RightToLeft) => { + OverflowDirection::RightAndUp + }, + } +} + +impl LayoutRPC for LayoutRPCImpl { + // The neat thing here is that in order to answer the following two queries we only + // need to compare nodes for equality. Thus we can safely work only with `OpaqueNode`. + fn content_box(&self) -> ContentBoxResponse { + let &LayoutRPCImpl(ref rw_data) = self; + let rw_data = rw_data.lock().unwrap(); + ContentBoxResponse(rw_data.content_box_response) + } + + /// Requests the dimensions of all the content boxes, as in the `getClientRects()` call. + fn content_boxes(&self) -> ContentBoxesResponse { + let &LayoutRPCImpl(ref rw_data) = self; + let rw_data = rw_data.lock().unwrap(); + ContentBoxesResponse(rw_data.content_boxes_response.clone()) + } + + fn nodes_from_point_response(&self) -> Vec<UntrustedNodeAddress> { + let &LayoutRPCImpl(ref rw_data) = self; + let rw_data = rw_data.lock().unwrap(); + rw_data.nodes_from_point_response.clone() + } + + fn node_geometry(&self) -> NodeGeometryResponse { + let &LayoutRPCImpl(ref rw_data) = self; + let rw_data = rw_data.lock().unwrap(); + NodeGeometryResponse { + client_rect: rw_data.client_rect_response, + } + } + + fn node_scroll_area(&self) -> NodeGeometryResponse { + NodeGeometryResponse { + client_rect: self.0.lock().unwrap().scroll_area_response, + } + } + + fn node_scroll_id(&self) -> NodeScrollIdResponse { + NodeScrollIdResponse( + self.0 + .lock() + .unwrap() + .scroll_id_response + .expect("scroll id is not correctly fetched"), + ) + } + + /// Retrieves the resolved value for a CSS style property. + fn resolved_style(&self) -> ResolvedStyleResponse { + let &LayoutRPCImpl(ref rw_data) = self; + let rw_data = rw_data.lock().unwrap(); + ResolvedStyleResponse(rw_data.resolved_style_response.clone()) + } + + fn offset_parent(&self) -> OffsetParentResponse { + let &LayoutRPCImpl(ref rw_data) = self; + let rw_data = rw_data.lock().unwrap(); + rw_data.offset_parent_response.clone() + } + + fn style(&self) -> StyleResponse { + let &LayoutRPCImpl(ref rw_data) = self; + let rw_data = rw_data.lock().unwrap(); + rw_data.style_response.clone() + } + + fn text_index(&self) -> TextIndexResponse { + let &LayoutRPCImpl(ref rw_data) = self; + let rw_data = rw_data.lock().unwrap(); + rw_data.text_index_response.clone() + } + + fn element_inner_text(&self) -> String { + let &LayoutRPCImpl(ref rw_data) = self; + let rw_data = rw_data.lock().unwrap(); + rw_data.element_inner_text_response.clone() + } +} + +struct UnioningFragmentBorderBoxIterator { + node_address: OpaqueNode, + rect: Option<Rect<Au>>, +} + +impl UnioningFragmentBorderBoxIterator { + fn new(node_address: OpaqueNode) -> UnioningFragmentBorderBoxIterator { + UnioningFragmentBorderBoxIterator { + node_address: node_address, + rect: None, + } + } +} + +impl FragmentBorderBoxIterator for UnioningFragmentBorderBoxIterator { + fn process(&mut self, _: &Fragment, _: i32, border_box: &Rect<Au>) { + self.rect = match self.rect { + Some(rect) => Some(rect.union(border_box)), + None => Some(*border_box), + }; + } + + fn should_process(&mut self, fragment: &Fragment) -> bool { + fragment.contains_node(self.node_address) + } +} + +struct CollectingFragmentBorderBoxIterator { + node_address: OpaqueNode, + rects: Vec<Rect<Au>>, +} + +impl CollectingFragmentBorderBoxIterator { + fn new(node_address: OpaqueNode) -> CollectingFragmentBorderBoxIterator { + CollectingFragmentBorderBoxIterator { + node_address: node_address, + rects: Vec::new(), + } + } +} + +impl FragmentBorderBoxIterator for CollectingFragmentBorderBoxIterator { + fn process(&mut self, _: &Fragment, _: i32, border_box: &Rect<Au>) { + self.rects.push(*border_box); + } + + fn should_process(&mut self, fragment: &Fragment) -> bool { + fragment.contains_node(self.node_address) + } +} + +enum Side { + Left, + Right, + Bottom, + Top, +} + +enum MarginPadding { + Margin, + Padding, +} + +enum PositionProperty { + Left, + Right, + Top, + Bottom, + Width, + Height, +} + +#[derive(Debug)] +enum OverflowDirection { + RightAndDown, + LeftAndDown, + LeftAndUp, + RightAndUp, +} + +struct PositionRetrievingFragmentBorderBoxIterator { + node_address: OpaqueNode, + result: Option<Au>, + position: Point2D<Au>, + property: PositionProperty, +} + +impl PositionRetrievingFragmentBorderBoxIterator { + fn new( + node_address: OpaqueNode, + property: PositionProperty, + position: Point2D<Au>, + ) -> PositionRetrievingFragmentBorderBoxIterator { + PositionRetrievingFragmentBorderBoxIterator { + node_address: node_address, + position: position, + property: property, + result: None, + } + } +} + +impl FragmentBorderBoxIterator for PositionRetrievingFragmentBorderBoxIterator { + fn process(&mut self, fragment: &Fragment, _: i32, border_box: &Rect<Au>) { + let border_padding = fragment + .border_padding + .to_physical(fragment.style.writing_mode); + self.result = Some(match self.property { + PositionProperty::Left => self.position.x, + PositionProperty::Top => self.position.y, + PositionProperty::Width => border_box.size.width - border_padding.horizontal(), + PositionProperty::Height => border_box.size.height - border_padding.vertical(), + // TODO: the following 2 calculations are completely wrong. + // They should return the difference between the parent's and this + // fragment's border boxes. + PositionProperty::Right => border_box.max_x() + self.position.x, + PositionProperty::Bottom => border_box.max_y() + self.position.y, + }); + } + + fn should_process(&mut self, fragment: &Fragment) -> bool { + fragment.contains_node(self.node_address) + } +} + +struct MarginRetrievingFragmentBorderBoxIterator { + node_address: OpaqueNode, + result: Option<Au>, + writing_mode: WritingMode, + margin_padding: MarginPadding, + side: Side, +} + +impl MarginRetrievingFragmentBorderBoxIterator { + fn new( + node_address: OpaqueNode, + side: Side, + margin_padding: MarginPadding, + writing_mode: WritingMode, + ) -> MarginRetrievingFragmentBorderBoxIterator { + MarginRetrievingFragmentBorderBoxIterator { + node_address: node_address, + side: side, + margin_padding: margin_padding, + result: None, + writing_mode: writing_mode, + } + } +} + +impl FragmentBorderBoxIterator for MarginRetrievingFragmentBorderBoxIterator { + fn process(&mut self, fragment: &Fragment, _: i32, _: &Rect<Au>) { + let rect = match self.margin_padding { + MarginPadding::Margin => &fragment.margin, + MarginPadding::Padding => &fragment.border_padding, + }; + self.result = Some(match self.side { + Side::Left => rect.left(self.writing_mode), + Side::Right => rect.right(self.writing_mode), + Side::Bottom => rect.bottom(self.writing_mode), + Side::Top => rect.top(self.writing_mode), + }); + } + + fn should_process(&mut self, fragment: &Fragment) -> bool { + fragment.contains_node(self.node_address) + } +} + +pub fn process_content_box_request( + requested_node: OpaqueNode, + layout_root: &mut dyn Flow, +) -> Option<Rect<Au>> { + // FIXME(pcwalton): This has not been updated to handle the stacking context relative + // stuff. So the position is wrong in most cases. + let mut iterator = UnioningFragmentBorderBoxIterator::new(requested_node); + sequential::iterate_through_flow_tree_fragment_border_boxes(layout_root, &mut iterator); + iterator.rect +} + +pub fn process_content_boxes_request( + requested_node: OpaqueNode, + layout_root: &mut dyn Flow, +) -> Vec<Rect<Au>> { + // FIXME(pcwalton): This has not been updated to handle the stacking context relative + // stuff. So the position is wrong in most cases. + let mut iterator = CollectingFragmentBorderBoxIterator::new(requested_node); + sequential::iterate_through_flow_tree_fragment_border_boxes(layout_root, &mut iterator); + iterator.rects +} + +struct FragmentLocatingFragmentIterator { + node_address: OpaqueNode, + client_rect: Rect<i32>, +} + +impl FragmentLocatingFragmentIterator { + fn new(node_address: OpaqueNode) -> FragmentLocatingFragmentIterator { + FragmentLocatingFragmentIterator { + node_address: node_address, + client_rect: Rect::zero(), + } + } +} + +struct UnioningFragmentScrollAreaIterator { + node_address: OpaqueNode, + union_rect: Rect<i32>, + origin_rect: Rect<i32>, + level: Option<i32>, + is_child: bool, + overflow_direction: OverflowDirection, +} + +impl UnioningFragmentScrollAreaIterator { + fn new(node_address: OpaqueNode) -> UnioningFragmentScrollAreaIterator { + UnioningFragmentScrollAreaIterator { + node_address: node_address, + union_rect: Rect::zero(), + origin_rect: Rect::zero(), + level: None, + is_child: false, + // FIXME(#20867) + overflow_direction: OverflowDirection::RightAndDown, + } + } +} + +struct NodeOffsetBoxInfo { + offset: Point2D<Au>, + rectangle: Rect<Au>, +} + +struct ParentBorderBoxInfo { + node_address: OpaqueNode, + origin: Point2D<Au>, +} + +struct ParentOffsetBorderBoxIterator { + node_address: OpaqueNode, + has_processed_node: bool, + node_offset_box: Option<NodeOffsetBoxInfo>, + parent_nodes: Vec<Option<ParentBorderBoxInfo>>, +} + +impl ParentOffsetBorderBoxIterator { + fn new(node_address: OpaqueNode) -> ParentOffsetBorderBoxIterator { + ParentOffsetBorderBoxIterator { + node_address: node_address, + has_processed_node: false, + node_offset_box: None, + parent_nodes: Vec::new(), + } + } +} + +impl FragmentBorderBoxIterator for FragmentLocatingFragmentIterator { + fn process(&mut self, fragment: &Fragment, _: i32, border_box: &Rect<Au>) { + let style_structs::Border { + border_top_width: top_width, + border_right_width: right_width, + border_bottom_width: bottom_width, + border_left_width: left_width, + .. + } = *fragment.style.get_border(); + let (left_width, right_width) = (left_width.px(), right_width.px()); + let (top_width, bottom_width) = (top_width.px(), bottom_width.px()); + self.client_rect.origin.y = top_width as i32; + self.client_rect.origin.x = left_width as i32; + self.client_rect.size.width = + (border_box.size.width.to_f32_px() - left_width - right_width) as i32; + self.client_rect.size.height = + (border_box.size.height.to_f32_px() - top_width - bottom_width) as i32; + } + + fn should_process(&mut self, fragment: &Fragment) -> bool { + fragment.node == self.node_address + } +} + +// https://drafts.csswg.org/cssom-view/#scrolling-area +impl FragmentBorderBoxIterator for UnioningFragmentScrollAreaIterator { + fn process(&mut self, fragment: &Fragment, level: i32, border_box: &Rect<Au>) { + // In cases in which smaller child elements contain less padding than the parent + // the a union of the two elements padding rectangles could result in an unwanted + // increase in size. To work around this, we store the original elements padding + // rectangle as `origin_rect` and the union of all child elements padding and + // margin rectangles as `union_rect`. + let style_structs::Border { + border_top_width: top_border, + border_right_width: right_border, + border_bottom_width: bottom_border, + border_left_width: left_border, + .. + } = *fragment.style.get_border(); + let (left_border, right_border) = (left_border.px(), right_border.px()); + let (top_border, bottom_border) = (top_border.px(), bottom_border.px()); + let right_padding = (border_box.size.width.to_f32_px() - right_border - left_border) as i32; + let bottom_padding = + (border_box.size.height.to_f32_px() - bottom_border - top_border) as i32; + let top_padding = top_border as i32; + let left_padding = left_border as i32; + + match self.level { + Some(start_level) if level <= start_level => { + self.is_child = false; + }, + Some(_) => { + let padding = Rect::new( + Point2D::new(left_padding, top_padding), + Size2D::new(right_padding, bottom_padding), + ); + let top_margin = fragment.margin.top(fragment.style.writing_mode).to_px(); + let left_margin = fragment.margin.left(fragment.style.writing_mode).to_px(); + let bottom_margin = fragment.margin.bottom(fragment.style.writing_mode).to_px(); + let right_margin = fragment.margin.right(fragment.style.writing_mode).to_px(); + let margin = Rect::new( + Point2D::new(left_margin, top_margin), + Size2D::new(right_margin, bottom_margin), + ); + self.union_rect = self.union_rect.union(&margin).union(&padding); + }, + None => { + self.level = Some(level); + self.is_child = true; + self.overflow_direction = overflow_direction(&fragment.style.writing_mode); + self.origin_rect = Rect::new( + Point2D::new(left_padding, top_padding), + Size2D::new(right_padding, bottom_padding), + ); + }, + }; + } + + fn should_process(&mut self, fragment: &Fragment) -> bool { + fragment.contains_node(self.node_address) || self.is_child + } +} + +// https://drafts.csswg.org/cssom-view/#extensions-to-the-htmlelement-interface +impl FragmentBorderBoxIterator for ParentOffsetBorderBoxIterator { + fn process(&mut self, fragment: &Fragment, level: i32, border_box: &Rect<Au>) { + if self.node_offset_box.is_none() { + // We haven't found the node yet, so we're still looking + // for its parent. Remove all nodes at this level or + // higher, as they can't be parents of this node. + self.parent_nodes.truncate(level as usize); + assert_eq!( + self.parent_nodes.len(), + level as usize, + "Skipped at least one level in the flow tree!" + ); + } + + if !fragment.is_primary_fragment() { + // This fragment doesn't correspond to anything worth + // taking measurements from. + + if self.node_offset_box.is_none() { + // If this is the only fragment in the flow, we need to + // do this to avoid failing the above assertion. + self.parent_nodes.push(None); + } + + return; + } + + if fragment.node == self.node_address { + // Found the fragment in the flow tree that matches the + // DOM node being looked for. + + assert!( + self.node_offset_box.is_none(), + "Node was being treated as inline, but it has an associated fragment!" + ); + + self.has_processed_node = true; + self.node_offset_box = Some(NodeOffsetBoxInfo { + offset: border_box.origin, + rectangle: *border_box, + }); + + // offsetParent returns null if the node is fixed. + if fragment.style.get_box().position == Position::Fixed { + self.parent_nodes.clear(); + } + } else if let Some(node) = fragment.inline_context.as_ref().and_then(|inline_context| { + inline_context + .nodes + .iter() + .find(|node| node.address == self.node_address) + }) { + // TODO: Handle cases where the `offsetParent` is an inline + // element. This will likely be impossible until + // https://github.com/servo/servo/issues/13982 is fixed. + + // Found a fragment in the flow tree whose inline context + // contains the DOM node we're looking for, i.e. the node + // is inline and contains this fragment. + match self.node_offset_box { + Some(NodeOffsetBoxInfo { + ref mut rectangle, .. + }) => { + *rectangle = rectangle.union(border_box); + }, + None => { + // https://github.com/servo/servo/issues/13982 will + // cause this assertion to fail sometimes, so it's + // commented out for now. + /*assert!(node.flags.contains(FIRST_FRAGMENT_OF_ELEMENT), + "First fragment of inline node found wasn't its first fragment!");*/ + + self.node_offset_box = Some(NodeOffsetBoxInfo { + offset: border_box.origin, + rectangle: *border_box, + }); + }, + } + + if node + .flags + .contains(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT) + { + self.has_processed_node = true; + } + } else if self.node_offset_box.is_none() { + // TODO(gw): Is there a less fragile way of checking whether this + // fragment is the body element, rather than just checking that + // it's at level 1 (below the root node)? + let is_body_element = level == 1; + + let is_valid_parent = match ( + is_body_element, + fragment.style.get_box().position, + &fragment.specific, + ) { + // Spec says it's valid if any of these are true: + // 1) Is the body element + // 2) Is static position *and* is a table or table cell + // 3) Is not static position + (true, _, _) | + (false, Position::Static, &SpecificFragmentInfo::Table) | + (false, Position::Static, &SpecificFragmentInfo::TableCell) | + (false, Position::Sticky, _) | + (false, Position::Absolute, _) | + (false, Position::Relative, _) | + (false, Position::Fixed, _) => true, + + // Otherwise, it's not a valid parent + (false, Position::Static, _) => false, + }; + + let parent_info = if is_valid_parent { + let border_width = fragment + .border_width() + .to_physical(fragment.style.writing_mode); + + Some(ParentBorderBoxInfo { + node_address: fragment.node, + origin: border_box.origin + Vector2D::new(border_width.left, border_width.top), + }) + } else { + None + }; + + self.parent_nodes.push(parent_info); + } + } + + fn should_process(&mut self, _: &Fragment) -> bool { + !self.has_processed_node + } +} + +pub fn process_node_geometry_request( + requested_node: OpaqueNode, + layout_root: &mut dyn Flow, +) -> Rect<i32> { + let mut iterator = FragmentLocatingFragmentIterator::new(requested_node); + sequential::iterate_through_flow_tree_fragment_border_boxes(layout_root, &mut iterator); + iterator.client_rect +} + +pub fn process_node_scroll_id_request<N: LayoutNode>( + id: PipelineId, + requested_node: N, +) -> ExternalScrollId { + let layout_node = requested_node.to_threadsafe(); + layout_node.generate_scroll_id(id) +} + +/// https://drafts.csswg.org/cssom-view/#scrolling-area +pub fn process_node_scroll_area_request( + requested_node: OpaqueNode, + layout_root: &mut dyn Flow, +) -> Rect<i32> { + let mut iterator = UnioningFragmentScrollAreaIterator::new(requested_node); + sequential::iterate_through_flow_tree_fragment_border_boxes(layout_root, &mut iterator); + match iterator.overflow_direction { + OverflowDirection::RightAndDown => { + let right = max( + iterator.union_rect.size.width, + iterator.origin_rect.size.width, + ); + let bottom = max( + iterator.union_rect.size.height, + iterator.origin_rect.size.height, + ); + Rect::new(iterator.origin_rect.origin, Size2D::new(right, bottom)) + }, + OverflowDirection::LeftAndDown => { + let bottom = max( + iterator.union_rect.size.height, + iterator.origin_rect.size.height, + ); + let left = min(iterator.union_rect.origin.x, iterator.origin_rect.origin.x); + Rect::new( + Point2D::new(left, iterator.origin_rect.origin.y), + Size2D::new(iterator.origin_rect.size.width, bottom), + ) + }, + OverflowDirection::LeftAndUp => { + let top = min(iterator.union_rect.origin.y, iterator.origin_rect.origin.y); + let left = min(iterator.union_rect.origin.x, iterator.origin_rect.origin.x); + Rect::new(Point2D::new(left, top), iterator.origin_rect.size) + }, + OverflowDirection::RightAndUp => { + let top = min(iterator.union_rect.origin.y, iterator.origin_rect.origin.y); + let right = max( + iterator.union_rect.size.width, + iterator.origin_rect.size.width, + ); + Rect::new( + Point2D::new(iterator.origin_rect.origin.x, top), + Size2D::new(right, iterator.origin_rect.size.height), + ) + }, + } +} + +/// Return the resolved value of property for a given (pseudo)element. +/// <https://drafts.csswg.org/cssom/#resolved-value> +pub fn process_resolved_style_request<'a, N>( + context: &LayoutContext, + node: N, + pseudo: &Option<PseudoElement>, + property: &PropertyId, + layout_root: &mut dyn Flow, +) -> String +where + N: LayoutNode, +{ + use style::stylist::RuleInclusion; + use style::traversal::resolve_style; + + let element = node.as_element().unwrap(); + + // We call process_resolved_style_request after performing a whole-document + // traversal, so in the common case, the element is styled. + if element.get_data().is_some() { + return process_resolved_style_request_internal(node, pseudo, property, layout_root); + } + + // In a display: none subtree. No pseudo-element exists. + if pseudo.is_some() { + return String::new(); + } + + let mut tlc = ThreadLocalStyleContext::new(&context.style_context); + let mut context = StyleContext { + shared: &context.style_context, + thread_local: &mut tlc, + }; + + let styles = resolve_style(&mut context, element, RuleInclusion::All, pseudo.as_ref()); + let style = styles.primary(); + let longhand_id = match *property { + PropertyId::LonghandAlias(id, _) | PropertyId::Longhand(id) => id, + // Firefox returns blank strings for the computed value of shorthands, + // so this should be web-compatible. + PropertyId::ShorthandAlias(..) | PropertyId::Shorthand(_) => return String::new(), + PropertyId::Custom(ref name) => { + return style.computed_value_to_string(PropertyDeclarationId::Custom(name)); + }, + }; + + // No need to care about used values here, since we're on a display: none + // subtree, use the resolved value. + style.computed_value_to_string(PropertyDeclarationId::Longhand(longhand_id)) +} + +/// The primary resolution logic, which assumes that the element is styled. +fn process_resolved_style_request_internal<'a, N>( + requested_node: N, + pseudo: &Option<PseudoElement>, + property: &PropertyId, + layout_root: &mut dyn Flow, +) -> String +where + N: LayoutNode, +{ + let layout_el = requested_node.to_threadsafe().as_element().unwrap(); + let layout_el = match *pseudo { + Some(PseudoElement::Before) => layout_el.get_before_pseudo(), + Some(PseudoElement::After) => layout_el.get_after_pseudo(), + Some(PseudoElement::DetailsSummary) | + Some(PseudoElement::DetailsContent) | + Some(PseudoElement::Selection) => None, + // FIXME(emilio): What about the other pseudos? Probably they shouldn't + // just return the element's style! + _ => Some(layout_el), + }; + + let layout_el = match layout_el { + None => { + // The pseudo doesn't exist, return nothing. Chrome seems to query + // the element itself in this case, Firefox uses the resolved value. + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=29006 + return String::new(); + }, + Some(layout_el) => layout_el, + }; + + let style = &*layout_el.resolved_style(); + let longhand_id = match *property { + PropertyId::LonghandAlias(id, _) | PropertyId::Longhand(id) => id, + // Firefox returns blank strings for the computed value of shorthands, + // so this should be web-compatible. + PropertyId::ShorthandAlias(..) | PropertyId::Shorthand(_) => return String::new(), + PropertyId::Custom(ref name) => { + return style.computed_value_to_string(PropertyDeclarationId::Custom(name)); + }, + }; + + let positioned = match style.get_box().position { + Position::Relative | Position::Sticky | Position::Fixed | Position::Absolute => true, + _ => false, + }; + + //TODO: determine whether requested property applies to the element. + // eg. width does not apply to non-replaced inline elements. + // Existing browsers disagree about when left/top/right/bottom apply + // (Chrome seems to think they never apply and always returns resolved values). + // There are probably other quirks. + let applies = true; + + fn used_value_for_position_property<N: LayoutNode>( + layout_el: <N::ConcreteThreadSafeLayoutNode as ThreadSafeLayoutNode>::ConcreteThreadSafeLayoutElement, + layout_root: &mut dyn Flow, + requested_node: N, + longhand_id: LonghandId, + ) -> String { + let maybe_data = layout_el.borrow_layout_data(); + let position = maybe_data.map_or(Point2D::zero(), |data| { + match (*data).flow_construction_result { + ConstructionResult::Flow(ref flow_ref, _) => flow_ref + .deref() + .base() + .stacking_relative_position + .to_point(), + // TODO(dzbarsky) search parents until we find node with a flow ref. + // https://github.com/servo/servo/issues/8307 + _ => Point2D::zero(), + } + }); + let property = match longhand_id { + LonghandId::Bottom => PositionProperty::Bottom, + LonghandId::Top => PositionProperty::Top, + LonghandId::Left => PositionProperty::Left, + LonghandId::Right => PositionProperty::Right, + LonghandId::Width => PositionProperty::Width, + LonghandId::Height => PositionProperty::Height, + _ => unreachable!(), + }; + let mut iterator = PositionRetrievingFragmentBorderBoxIterator::new( + requested_node.opaque(), + property, + position, + ); + sequential::iterate_through_flow_tree_fragment_border_boxes(layout_root, &mut iterator); + iterator + .result + .map(|r| r.to_css_string()) + .unwrap_or(String::new()) + } + + // TODO: we will return neither the computed nor used value for margin and padding. + match longhand_id { + LonghandId::MarginBottom | + LonghandId::MarginTop | + LonghandId::MarginLeft | + LonghandId::MarginRight | + LonghandId::PaddingBottom | + LonghandId::PaddingTop | + LonghandId::PaddingLeft | + LonghandId::PaddingRight + if applies && style.get_box().display != Display::None => + { + let (margin_padding, side) = match longhand_id { + LonghandId::MarginBottom => (MarginPadding::Margin, Side::Bottom), + LonghandId::MarginTop => (MarginPadding::Margin, Side::Top), + LonghandId::MarginLeft => (MarginPadding::Margin, Side::Left), + LonghandId::MarginRight => (MarginPadding::Margin, Side::Right), + LonghandId::PaddingBottom => (MarginPadding::Padding, Side::Bottom), + LonghandId::PaddingTop => (MarginPadding::Padding, Side::Top), + LonghandId::PaddingLeft => (MarginPadding::Padding, Side::Left), + LonghandId::PaddingRight => (MarginPadding::Padding, Side::Right), + _ => unreachable!(), + }; + let mut iterator = MarginRetrievingFragmentBorderBoxIterator::new( + requested_node.opaque(), + side, + margin_padding, + style.writing_mode, + ); + sequential::iterate_through_flow_tree_fragment_border_boxes(layout_root, &mut iterator); + iterator + .result + .map(|r| r.to_css_string()) + .unwrap_or(String::new()) + } + + LonghandId::Bottom | LonghandId::Top | LonghandId::Right | LonghandId::Left + if applies && positioned && style.get_box().display != Display::None => + { + used_value_for_position_property(layout_el, layout_root, requested_node, longhand_id) + }, + LonghandId::Width | LonghandId::Height + if applies && style.get_box().display != Display::None => + { + used_value_for_position_property(layout_el, layout_root, requested_node, longhand_id) + }, + // FIXME: implement used value computation for line-height + _ => style.computed_value_to_string(PropertyDeclarationId::Longhand(longhand_id)), + } +} + +pub fn process_offset_parent_query( + requested_node: OpaqueNode, + layout_root: &mut dyn Flow, +) -> OffsetParentResponse { + let mut iterator = ParentOffsetBorderBoxIterator::new(requested_node); + sequential::iterate_through_flow_tree_fragment_border_boxes(layout_root, &mut iterator); + + let node_offset_box = iterator.node_offset_box; + let parent_info = iterator + .parent_nodes + .into_iter() + .rev() + .filter_map(|info| info) + .next(); + match (node_offset_box, parent_info) { + (Some(node_offset_box), Some(parent_info)) => { + let origin = node_offset_box.offset - parent_info.origin.to_vector(); + let size = node_offset_box.rectangle.size; + OffsetParentResponse { + node_address: Some(parent_info.node_address.to_untrusted_node_address()), + rect: Rect::new(origin, size), + } + }, + _ => OffsetParentResponse::empty(), + } +} + +pub fn process_style_query<N: LayoutNode>(requested_node: N) -> StyleResponse { + let element = requested_node.as_element().unwrap(); + let data = element.borrow_data(); + + StyleResponse(data.map(|d| d.styles.primary().clone())) +} + +enum InnerTextItem { + Text(String), + RequiredLineBreakCount(u32), +} + +// https://html.spec.whatwg.org/multipage/#the-innertext-idl-attribute +pub fn process_element_inner_text_query<N: LayoutNode>( + node: N, + indexable_text: &IndexableText, +) -> String { + // Step 1. + let mut results = Vec::new(); + // Step 2. + inner_text_collection_steps(node, indexable_text, &mut results); + let mut max_req_line_break_count = 0; + let mut inner_text = Vec::new(); + for item in results { + match item { + InnerTextItem::Text(s) => { + if max_req_line_break_count > 0 { + // Step 5. + for _ in 0..max_req_line_break_count { + inner_text.push("\u{000A}".to_owned()); + } + max_req_line_break_count = 0; + } + // Step 3. + if !s.is_empty() { + inner_text.push(s.to_owned()); + } + }, + InnerTextItem::RequiredLineBreakCount(count) => { + // Step 4. + if inner_text.len() == 0 { + // Remove required line break count at the start. + continue; + } + // Store the count if it's the max of this run, + // but it may be ignored if no text item is found afterwards, + // which means that these are consecutive line breaks at the end. + if count > max_req_line_break_count { + max_req_line_break_count = count; + } + }, + } + } + inner_text.into_iter().collect() +} + +// https://html.spec.whatwg.org/multipage/#inner-text-collection-steps +#[allow(unsafe_code)] +fn inner_text_collection_steps<N: LayoutNode>( + node: N, + indexable_text: &IndexableText, + results: &mut Vec<InnerTextItem>, +) { + let mut items = Vec::new(); + for child in node.traverse_preorder() { + let node = match child.type_id() { + LayoutNodeType::Text => child.parent_node().unwrap(), + _ => child, + }; + + let element_data = unsafe { + node.get_style_and_layout_data() + .map(|d| &(*(d.ptr.as_ptr() as *mut StyleData)).element_data) + }; + + if element_data.is_none() { + continue; + } + + let style = match element_data.unwrap().borrow().styles.get_primary() { + None => continue, + Some(style) => style.clone(), + }; + + // Step 2. + if style.get_inherited_box().visibility != Visibility::Visible { + continue; + } + + // Step 3. + let display = style.get_box().display; + if !child.is_connected() || display == Display::None { + continue; + } + + match child.type_id() { + LayoutNodeType::Text => { + // Step 4. + if let Some(text_content) = indexable_text.get(child.opaque()) { + for content in text_content { + items.push(InnerTextItem::Text(content.text_run.text.to_string())); + } + } + }, + LayoutNodeType::Element(LayoutElementType::HTMLBRElement) => { + // Step 5. + items.push(InnerTextItem::Text(String::from( + "\u{000A}", /* line feed */ + ))); + }, + LayoutNodeType::Element(LayoutElementType::HTMLParagraphElement) => { + // Step 8. + items.insert(0, InnerTextItem::RequiredLineBreakCount(2)); + items.push(InnerTextItem::RequiredLineBreakCount(2)); + }, + _ => {}, + } + + match display { + Display::TableCell if !is_last_table_cell() => { + // Step 6. + items.push(InnerTextItem::Text(String::from("\u{0009}" /* tab */))); + }, + Display::TableRow if !is_last_table_row() => { + // Step 7. + items.push(InnerTextItem::Text(String::from( + "\u{000A}", /* line feed */ + ))); + }, + Display::Block | Display::Flex | Display::TableCaption | Display::Table => { + // Step 9. + items.insert(0, InnerTextItem::RequiredLineBreakCount(1)); + items.push(InnerTextItem::RequiredLineBreakCount(1)); + }, + _ => {}, + } + } + + results.append(&mut items); +} + +fn is_last_table_cell() -> bool { + // FIXME(ferjm) Implement this. + false +} + +fn is_last_table_row() -> bool { + // FIXME(ferjm) Implement this. + false +} diff --git a/components/layout_2020/sequential.rs b/components/layout_2020/sequential.rs new file mode 100644 index 00000000000..16dd3b8d4da --- /dev/null +++ b/components/layout_2020/sequential.rs @@ -0,0 +1,193 @@ +/* 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/. */ + +//! Implements sequential traversals over the DOM and flow trees. + +use crate::context::LayoutContext; +use crate::display_list::items::{self, CommonDisplayItem, DisplayItem, DisplayListSection}; +use crate::display_list::{DisplayListBuildState, StackingContextCollectionState}; +use crate::floats::SpeculatedFloatPlacement; +use crate::flow::{Flow, FlowFlags, GetBaseFlow, ImmutableFlowUtils}; +use crate::fragment::{CoordinateSystem, FragmentBorderBoxIterator}; +use crate::generated_content::ResolveGeneratedContent; +use crate::incremental::RelayoutMode; +use crate::traversal::{AssignBSizes, AssignISizes, BubbleISizes, BuildDisplayList}; +use crate::traversal::{InorderFlowTraversal, PostorderFlowTraversal, PreorderFlowTraversal}; +use app_units::Au; +use euclid::{Point2D, Rect, Size2D, Vector2D}; +use servo_config::opts; +use style::servo::restyle_damage::ServoRestyleDamage; +use webrender_api::units::LayoutPoint; + +pub fn resolve_generated_content(root: &mut dyn Flow, layout_context: &LayoutContext) { + ResolveGeneratedContent::new(&layout_context).traverse(root, 0); +} + +/// Run the main layout passes sequentially. +pub fn reflow(root: &mut dyn Flow, layout_context: &LayoutContext, relayout_mode: RelayoutMode) { + fn doit( + flow: &mut dyn Flow, + assign_inline_sizes: AssignISizes, + assign_block_sizes: AssignBSizes, + relayout_mode: RelayoutMode, + ) { + // Force reflow children during this traversal. This is needed when we failed + // the float speculation of a block formatting context and need to fix it. + if relayout_mode == RelayoutMode::Force { + flow.mut_base() + .restyle_damage + .insert(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + } + + if assign_inline_sizes.should_process(flow) { + assign_inline_sizes.process(flow); + } + + for kid in flow.mut_base().child_iter_mut() { + doit(kid, assign_inline_sizes, assign_block_sizes, relayout_mode); + } + + if assign_block_sizes.should_process(flow) { + assign_block_sizes.process(flow); + } + } + + if opts::get().bubble_inline_sizes_separately { + let bubble_inline_sizes = BubbleISizes { + layout_context: &layout_context, + }; + bubble_inline_sizes.traverse(root); + } + + let assign_inline_sizes = AssignISizes { + layout_context: &layout_context, + }; + let assign_block_sizes = AssignBSizes { + layout_context: &layout_context, + }; + + doit(root, assign_inline_sizes, assign_block_sizes, relayout_mode); +} + +pub fn build_display_list_for_subtree<'a>( + flow_root: &mut dyn Flow, + layout_context: &'a LayoutContext, + background_color: webrender_api::ColorF, + client_size: Size2D<Au>, +) -> DisplayListBuildState<'a> { + let mut state = StackingContextCollectionState::new(layout_context.id); + flow_root.collect_stacking_contexts(&mut state); + + let mut state = DisplayListBuildState::new(layout_context, state); + + // Create a base rectangle for the page background based on the root + // background color. + let base = state.create_base_display_item( + Rect::new(Point2D::new(Au::new(0), Au::new(0)), client_size), + flow_root.as_block().fragment.node, + None, + DisplayListSection::BackgroundAndBorders, + ); + state.add_display_item(DisplayItem::Rectangle(CommonDisplayItem::new( + base, + webrender_api::RectangleDisplayItem { + color: background_color, + common: items::empty_common_item_properties(), + }, + ))); + + let mut build_display_list = BuildDisplayList { state: state }; + build_display_list.traverse(flow_root); + build_display_list.state +} + +pub fn iterate_through_flow_tree_fragment_border_boxes( + root: &mut dyn Flow, + iterator: &mut dyn FragmentBorderBoxIterator, +) { + fn doit( + flow: &mut dyn Flow, + level: i32, + iterator: &mut dyn FragmentBorderBoxIterator, + stacking_context_position: &Point2D<Au>, + ) { + flow.iterate_through_fragment_border_boxes(iterator, level, stacking_context_position); + + for kid in flow.mut_base().child_iter_mut() { + let mut stacking_context_position = *stacking_context_position; + if kid.is_block_flow() && kid.as_block().fragment.establishes_stacking_context() { + stacking_context_position = + Point2D::new(kid.as_block().fragment.margin.inline_start, Au(0)) + + kid.base().stacking_relative_position + + stacking_context_position.to_vector(); + let relative_position = kid + .as_block() + .stacking_relative_border_box(CoordinateSystem::Own); + if let Some(matrix) = kid.as_block().fragment.transform_matrix(&relative_position) { + let transform_matrix = matrix.transform_point2d(&LayoutPoint::zero()).unwrap(); + stacking_context_position = stacking_context_position + + Vector2D::new( + Au::from_f32_px(transform_matrix.x), + Au::from_f32_px(transform_matrix.y), + ) + } + } + doit(kid, level + 1, iterator, &stacking_context_position); + } + } + + doit(root, 0, iterator, &Point2D::zero()); +} + +pub fn store_overflow(layout_context: &LayoutContext, flow: &mut dyn Flow) { + if !flow + .base() + .restyle_damage + .contains(ServoRestyleDamage::STORE_OVERFLOW) + { + return; + } + + for kid in flow.mut_base().child_iter_mut() { + store_overflow(layout_context, kid); + } + + flow.store_overflow(layout_context); + + flow.mut_base() + .restyle_damage + .remove(ServoRestyleDamage::STORE_OVERFLOW); +} + +/// Guesses how much inline size will be taken up by floats on the left and right sides of the +/// given flow. This is needed to speculatively calculate the inline sizes of block formatting +/// contexts. The speculation typically succeeds, but if it doesn't we have to lay it out again. +pub fn guess_float_placement(flow: &mut dyn Flow) { + if !flow + .base() + .restyle_damage + .intersects(ServoRestyleDamage::REFLOW) + { + return; + } + + let mut floats_in = SpeculatedFloatPlacement::compute_floats_in_for_first_child(flow); + for kid in flow.mut_base().child_iter_mut() { + if kid + .base() + .flags + .contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) + { + // Do not propagate floats in or out, but do propogate between kids. + guess_float_placement(kid); + } else { + floats_in.compute_floats_in(kid); + kid.mut_base().speculated_float_placement_in = floats_in; + guess_float_placement(kid); + floats_in = kid.base().speculated_float_placement_out; + } + } + floats_in.compute_floats_out(flow); + flow.mut_base().speculated_float_placement_out = floats_in +} diff --git a/components/layout_2020/table.rs b/components/layout_2020/table.rs new file mode 100644 index 00000000000..9f6219f0980 --- /dev/null +++ b/components/layout_2020/table.rs @@ -0,0 +1,1378 @@ +/* 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/. */ + +//! CSS table formatting contexts. + +use crate::block::{BlockFlow, CandidateBSizeIterator, ISizeAndMarginsComputer}; +use crate::block::{ISizeConstraintInput, ISizeConstraintSolution}; +use crate::context::LayoutContext; +use crate::display_list::{ + BorderPaintingMode, DisplayListBuildState, StackingContextCollectionFlags, + StackingContextCollectionState, +}; +use crate::flow::{ + BaseFlow, EarlyAbsolutePositionInfo, Flow, FlowClass, GetBaseFlow, ImmutableFlowUtils, + OpaqueFlow, +}; +use crate::flow_list::{FlowListIterator, MutFlowListIterator}; +use crate::fragment::{Fragment, FragmentBorderBoxIterator, Overflow}; +use crate::layout_debug; +use crate::model::{IntrinsicISizes, IntrinsicISizesContribution, MaybeAuto}; +use crate::table_cell::TableCellFlow; +use crate::table_row::{self, CellIntrinsicInlineSize, CollapsedBorder, CollapsedBorderProvenance}; +use crate::table_row::{TableRowFlow, TableRowSizeData}; +use crate::table_wrapper::TableLayout; +use app_units::Au; +use euclid::Point2D; +use gfx_traits::print_tree::PrintTree; +use std::{cmp, fmt}; +use style::computed_values::{border_collapse, border_spacing, table_layout}; +use style::context::SharedStyleContext; +use style::logical_geometry::LogicalSize; +use style::properties::style_structs::Background; +use style::properties::ComputedValues; +use style::servo::restyle_damage::ServoRestyleDamage; +use style::values::computed::Size; +use style::values::CSSFloat; + +#[allow(unsafe_code)] +unsafe impl crate::flow::HasBaseFlow for TableFlow {} + +/// A table flow corresponded to the table's internal table fragment under a table wrapper flow. +/// The properties `position`, `float`, and `margin-*` are used on the table wrapper fragment, +/// not table fragment per CSS 2.1 § 10.5. +#[derive(Serialize)] +#[repr(C)] +pub struct TableFlow { + pub block_flow: BlockFlow, + + /// Information about the intrinsic inline-sizes of each column, computed bottom-up during + /// intrinsic inline-size bubbling. + pub column_intrinsic_inline_sizes: Vec<ColumnIntrinsicInlineSize>, + + /// Information about the actual inline sizes of each column, computed top-down during actual + /// inline-size bubbling. + pub column_computed_inline_sizes: Vec<ColumnComputedInlineSize>, + + /// The final width of the borders in the inline direction for each cell, computed by the + /// entire table and pushed down into each row during inline size computation. + pub collapsed_inline_direction_border_widths_for_table: Vec<Au>, + + /// The final width of the borders in the block direction for each cell, computed by the + /// entire table and pushed down into each row during inline size computation. + pub collapsed_block_direction_border_widths_for_table: Vec<Au>, + + /// Table-layout property + pub table_layout: TableLayout, +} + +impl TableFlow { + pub fn from_fragment(fragment: Fragment) -> TableFlow { + let mut block_flow = BlockFlow::from_fragment(fragment); + let table_layout = + if block_flow.fragment().style().get_table().table_layout == table_layout::T::Fixed { + TableLayout::Fixed + } else { + TableLayout::Auto + }; + TableFlow { + block_flow: block_flow, + column_intrinsic_inline_sizes: Vec::new(), + column_computed_inline_sizes: Vec::new(), + collapsed_inline_direction_border_widths_for_table: Vec::new(), + collapsed_block_direction_border_widths_for_table: Vec::new(), + table_layout: table_layout, + } + } + + /// Update the corresponding value of `self_inline_sizes` if a value of `kid_inline_sizes` has + /// a larger value than one of `self_inline_sizes`. Returns the minimum and preferred inline + /// sizes. + fn update_automatic_column_inline_sizes( + parent_inline_sizes: &mut Vec<ColumnIntrinsicInlineSize>, + child_cell_inline_sizes: &[CellIntrinsicInlineSize], + surrounding_size: Au, + ) -> IntrinsicISizes { + let mut total_inline_sizes = IntrinsicISizes { + minimum_inline_size: surrounding_size, + preferred_inline_size: surrounding_size, + }; + let mut column_index = 0; + let mut incoming_rowspan = vec![]; + + for child_cell_inline_size in child_cell_inline_sizes { + // Skip any column occupied by a cell from a previous row. + while column_index < incoming_rowspan.len() && incoming_rowspan[column_index] != 1 { + if incoming_rowspan[column_index] > 1 { + incoming_rowspan[column_index] -= 1; + } + column_index += 1; + } + for _ in 0..child_cell_inline_size.column_span { + if column_index < parent_inline_sizes.len() { + // We already have some intrinsic size information for this column. Merge it in + // according to the rules specified in INTRINSIC § 4. + let parent_sizes = &mut parent_inline_sizes[column_index]; + if child_cell_inline_size.column_span > 1 { + // TODO(pcwalton): Perform the recursive algorithm specified in INTRINSIC § + // 4. For now we make this column contribute no width. + } else { + let column_size = &child_cell_inline_size.column_size; + *parent_sizes = ColumnIntrinsicInlineSize { + minimum_length: cmp::max( + parent_sizes.minimum_length, + column_size.minimum_length, + ), + percentage: parent_sizes.greatest_percentage(column_size), + preferred: cmp::max(parent_sizes.preferred, column_size.preferred), + constrained: parent_sizes.constrained || column_size.constrained, + } + } + } else { + // We discovered a new column. Initialize its data. + debug_assert_eq!(column_index, parent_inline_sizes.len()); + if child_cell_inline_size.column_span > 1 { + // TODO(pcwalton): Perform the recursive algorithm specified in INTRINSIC § + // 4. For now we make this column contribute no width. + parent_inline_sizes.push(ColumnIntrinsicInlineSize::new()) + } else { + parent_inline_sizes.push(child_cell_inline_size.column_size) + } + } + + total_inline_sizes.minimum_inline_size += + parent_inline_sizes[column_index].minimum_length; + total_inline_sizes.preferred_inline_size += + parent_inline_sizes[column_index].preferred; + + // If this cell spans later rows, record its rowspan. + if child_cell_inline_size.row_span > 1 { + if incoming_rowspan.len() < column_index + 1 { + incoming_rowspan.resize(column_index + 1, 0); + } + incoming_rowspan[column_index] = child_cell_inline_size.row_span; + } + + column_index += 1 + } + } + + total_inline_sizes + } + + /// Updates the minimum and preferred inline-size calculation for a single row. This is + /// factored out into a separate function because we process children of rowgroups too. + fn update_column_inline_sizes_for_row( + row: &TableRowFlow, + column_inline_sizes: &mut Vec<ColumnIntrinsicInlineSize>, + computation: &mut IntrinsicISizesContribution, + first_row: bool, + table_layout: TableLayout, + surrounding_inline_size: Au, + ) { + // Read column inline-sizes from the table-row, and assign inline-size=0 for the columns + // not defined in the column group. + // + // FIXME: Need to read inline-sizes from either table-header-group OR the first table-row. + match table_layout { + TableLayout::Fixed => { + // Fixed table layout only looks at the first row. + // + // FIXME(pcwalton): This is really inefficient. We should stop after the first row! + if first_row { + for cell_inline_size in &row.cell_intrinsic_inline_sizes { + column_inline_sizes.push(cell_inline_size.column_size); + } + } + }, + TableLayout::Auto => { + computation.union_block(&TableFlow::update_automatic_column_inline_sizes( + column_inline_sizes, + &row.cell_intrinsic_inline_sizes, + surrounding_inline_size, + )) + }, + } + } + + /// Returns the effective spacing per cell, taking the value of `border-collapse` into account. + pub fn spacing(&self) -> border_spacing::T { + let style = self.block_flow.fragment.style(); + match style.get_inherited_table().border_collapse { + border_collapse::T::Separate => style.get_inherited_table().border_spacing, + border_collapse::T::Collapse => border_spacing::T::zero(), + } + } + + pub fn total_horizontal_spacing(&self) -> Au { + let num_columns = self.column_intrinsic_inline_sizes.len(); + if num_columns == 0 { + return Au(0); + } + self.spacing().horizontal() * (num_columns as i32 + 1) + } + + fn column_styles(&self) -> Vec<ColumnStyle> { + let mut styles = vec![]; + for group in self + .block_flow + .base + .child_iter() + .filter(|kid| kid.is_table_colgroup()) + { + // XXXManishearth these as_foo methods should return options + // so that we can filter_map + let group = group.as_table_colgroup(); + let colgroup_style = group.fragment.as_ref().map(|f| f.style()); + + // The colgroup's span attribute is only relevant when + // it has no children + // https://html.spec.whatwg.org/multipage/#forming-a-table + if group.cols.is_empty() { + let span = group + .fragment + .as_ref() + .map(|f| f.column_span()) + .unwrap_or(1); + styles.push(ColumnStyle { + span, + colgroup_style, + col_style: None, + }); + } else { + for col in &group.cols { + // XXXManishearth Arc-cloning colgroup_style is suboptimal + styles.push(ColumnStyle { + span: col.column_span(), + colgroup_style: colgroup_style, + col_style: Some(col.style()), + }) + } + } + } + styles + } +} + +impl Flow for TableFlow { + fn class(&self) -> FlowClass { + FlowClass::Table + } + + fn as_mut_table(&mut self) -> &mut TableFlow { + self + } + + fn as_table(&self) -> &TableFlow { + self + } + + fn as_mut_block(&mut self) -> &mut BlockFlow { + &mut self.block_flow + } + + fn as_block(&self) -> &BlockFlow { + &self.block_flow + } + + fn mark_as_root(&mut self) { + self.block_flow.mark_as_root(); + } + + /// The specified column inline-sizes are set from column group and the first row for the fixed + /// table layout calculation. + /// The maximum min/pref inline-sizes of each column are set from the rows for the automatic + /// table layout calculation. + fn bubble_inline_sizes(&mut self) { + let _scope = layout_debug_scope!( + "table::bubble_inline_sizes {:x}", + self.block_flow.base.debug_id() + ); + + // Get column inline sizes from colgroups + for kid in self + .block_flow + .base + .child_iter_mut() + .filter(|kid| kid.is_table_colgroup()) + { + for specified_inline_size in &kid.as_mut_table_colgroup().inline_sizes { + self.column_intrinsic_inline_sizes + .push(ColumnIntrinsicInlineSize { + minimum_length: match *specified_inline_size { + Size::Auto => Au(0), + Size::LengthPercentage(ref lp) => { + lp.maybe_to_used_value(None).unwrap_or(Au(0)) + }, + }, + percentage: match *specified_inline_size { + Size::Auto => 0.0, + Size::LengthPercentage(ref lp) => { + lp.0.as_percentage().map_or(0.0, |p| p.0) + }, + }, + preferred: Au(0), + constrained: false, + }) + } + } + + self.collapsed_inline_direction_border_widths_for_table = Vec::new(); + self.collapsed_block_direction_border_widths_for_table = vec![Au(0)]; + + let collapsing_borders = self + .block_flow + .fragment + .style + .get_inherited_table() + .border_collapse == + border_collapse::T::Collapse; + let table_inline_collapsed_borders = if collapsing_borders { + Some(TableInlineCollapsedBorders { + start: CollapsedBorder::inline_start( + &*self.block_flow.fragment.style, + CollapsedBorderProvenance::FromTable, + ), + end: CollapsedBorder::inline_end( + &*self.block_flow.fragment.style, + CollapsedBorderProvenance::FromTable, + ), + }) + } else { + None + }; + + let mut computation = IntrinsicISizesContribution::new(); + let mut previous_collapsed_block_end_borders = + PreviousBlockCollapsedBorders::FromTable(CollapsedBorder::block_start( + &*self.block_flow.fragment.style, + CollapsedBorderProvenance::FromTable, + )); + let mut first_row = true; + let (border_padding, _) = self.block_flow.fragment.surrounding_intrinsic_inline_size(); + + { + let mut iterator = TableRowIterator::new(&mut self.block_flow.base).peekable(); + while let Some(row) = iterator.next() { + TableFlow::update_column_inline_sizes_for_row( + row, + &mut self.column_intrinsic_inline_sizes, + &mut computation, + first_row, + self.table_layout, + border_padding, + ); + if collapsing_borders { + let next_index_and_sibling = iterator.peek(); + let next_collapsed_borders_in_block_direction = match next_index_and_sibling { + Some(next_sibling) => NextBlockCollapsedBorders::FromNextRow( + &next_sibling + .as_table_row() + .preliminary_collapsed_borders + .block_start, + ), + None => NextBlockCollapsedBorders::FromTable(CollapsedBorder::block_end( + &*self.block_flow.fragment.style, + CollapsedBorderProvenance::FromTable, + )), + }; + perform_border_collapse_for_row( + row, + table_inline_collapsed_borders.as_ref().unwrap(), + previous_collapsed_block_end_borders, + next_collapsed_borders_in_block_direction, + &mut self.collapsed_inline_direction_border_widths_for_table, + &mut self.collapsed_block_direction_border_widths_for_table, + ); + previous_collapsed_block_end_borders = + PreviousBlockCollapsedBorders::FromPreviousRow( + row.final_collapsed_borders.block_end.clone(), + ); + } + first_row = false + } + } + + let total_horizontal_spacing = self.total_horizontal_spacing(); + let mut style_specified_intrinsic_inline_size = self + .block_flow + .fragment + .style_specified_intrinsic_inline_size() + .finish(); + style_specified_intrinsic_inline_size.minimum_inline_size -= total_horizontal_spacing; + style_specified_intrinsic_inline_size.preferred_inline_size -= total_horizontal_spacing; + computation.union_block(&style_specified_intrinsic_inline_size); + computation.surrounding_size += total_horizontal_spacing; + + self.block_flow.base.intrinsic_inline_sizes = computation.finish() + } + + /// Recursively (top-down) determines the actual inline-size of child contexts and fragments. + /// When called on this context, the context has had its inline-size set by the parent context. + fn assign_inline_sizes(&mut self, layout_context: &LayoutContext) { + let _scope = layout_debug_scope!( + "table::assign_inline_sizes {:x}", + self.block_flow.base.debug_id() + ); + debug!( + "assign_inline_sizes({}): assigning inline_size for flow", + "table" + ); + + let shared_context = layout_context.shared_context(); + // The position was set to the containing block by the flow's parent. + // FIXME: The code for distributing column widths should really be placed under table_wrapper.rs. + let containing_block_inline_size = self.block_flow.base.block_container_inline_size; + + let mut constrained_column_inline_sizes_indices = vec![]; + let mut unspecified_inline_sizes_indices = vec![]; + for (idx, column_inline_size) in self.column_intrinsic_inline_sizes.iter().enumerate() { + if column_inline_size.constrained { + constrained_column_inline_sizes_indices.push(idx); + } else if column_inline_size.percentage == 0.0 { + unspecified_inline_sizes_indices.push(idx); + } + } + + let inline_size_computer = InternalTable; + inline_size_computer.compute_used_inline_size( + &mut self.block_flow, + shared_context, + containing_block_inline_size, + ); + + let inline_start_content_edge = self.block_flow.fragment.border_padding.inline_start; + let inline_end_content_edge = self.block_flow.fragment.border_padding.inline_end; + let padding_and_borders = self.block_flow.fragment.border_padding.inline_start_end(); + let spacing_per_cell = self.spacing(); + let total_horizontal_spacing = self.total_horizontal_spacing(); + let content_inline_size = self.block_flow.fragment.border_box.size.inline - + padding_and_borders - + total_horizontal_spacing; + let mut remaining_inline_size = content_inline_size; + + match self.table_layout { + TableLayout::Fixed => { + self.column_computed_inline_sizes.clear(); + + // https://drafts.csswg.org/css2/tables.html#fixed-table-layout + for column_inline_size in &self.column_intrinsic_inline_sizes { + if column_inline_size.constrained { + self.column_computed_inline_sizes + .push(ColumnComputedInlineSize { + size: column_inline_size.minimum_length, + }); + remaining_inline_size -= column_inline_size.minimum_length; + } else if column_inline_size.percentage != 0.0 { + let size = remaining_inline_size.scale_by(column_inline_size.percentage); + self.column_computed_inline_sizes + .push(ColumnComputedInlineSize { size: size }); + remaining_inline_size -= size; + } else { + // Set the size to 0 now, distribute the remaining widths later + self.column_computed_inline_sizes + .push(ColumnComputedInlineSize { size: Au(0) }); + } + } + + // Distribute remaining content inline size + if unspecified_inline_sizes_indices.len() > 0 { + for &index in &unspecified_inline_sizes_indices { + self.column_computed_inline_sizes[index].size = remaining_inline_size + .scale_by(1.0 / unspecified_inline_sizes_indices.len() as f32); + } + } else { + let total_minimum_size = self + .column_intrinsic_inline_sizes + .iter() + .filter(|size| size.constrained) + .map(|size| size.minimum_length.0 as f32) + .sum::<f32>(); + + for &index in &constrained_column_inline_sizes_indices { + let inline_size = self.column_computed_inline_sizes[index].size.0; + self.column_computed_inline_sizes[index].size += + remaining_inline_size.scale_by(inline_size as f32 / total_minimum_size); + } + } + }, + _ => { + // The table wrapper already computed the inline-sizes and propagated them down + // to us. + }, + } + + let column_computed_inline_sizes = &self.column_computed_inline_sizes; + let collapsed_inline_direction_border_widths_for_table = + &self.collapsed_inline_direction_border_widths_for_table; + let mut collapsed_block_direction_border_widths_for_table = self + .collapsed_block_direction_border_widths_for_table + .iter() + .peekable(); + let mut incoming_rowspan = vec![]; + self.block_flow.propagate_assigned_inline_size_to_children( + shared_context, + inline_start_content_edge, + inline_end_content_edge, + content_inline_size, + |child_flow, + _child_index, + _content_inline_size, + writing_mode, + _inline_start_margin_edge, + _inline_end_margin_edge| { + table_row::propagate_column_inline_sizes_to_child( + child_flow, + writing_mode, + column_computed_inline_sizes, + &spacing_per_cell, + &mut incoming_rowspan, + ); + if child_flow.is_table_row() { + let child_table_row = child_flow.as_mut_table_row(); + child_table_row.populate_collapsed_border_spacing( + collapsed_inline_direction_border_widths_for_table, + &mut collapsed_block_direction_border_widths_for_table, + ); + } else if child_flow.is_table_rowgroup() { + let child_table_rowgroup = child_flow.as_mut_table_rowgroup(); + child_table_rowgroup.populate_collapsed_border_spacing( + collapsed_inline_direction_border_widths_for_table, + &mut collapsed_block_direction_border_widths_for_table, + ); + } + }, + ); + } + + fn assign_block_size(&mut self, lc: &LayoutContext) { + debug!("assign_block_size: assigning block_size for table"); + let vertical_spacing = self.spacing().vertical(); + self.block_flow + .assign_block_size_for_table_like_flow(vertical_spacing, lc) + } + + fn compute_stacking_relative_position(&mut self, layout_context: &LayoutContext) { + self.block_flow + .compute_stacking_relative_position(layout_context) + } + + fn generated_containing_block_size(&self, flow: OpaqueFlow) -> LogicalSize<Au> { + self.block_flow.generated_containing_block_size(flow) + } + + 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, state: &mut DisplayListBuildState) { + let border_painting_mode = match self + .block_flow + .fragment + .style + .get_inherited_table() + .border_collapse + { + border_collapse::T::Separate => BorderPaintingMode::Separate, + border_collapse::T::Collapse => BorderPaintingMode::Hidden, + }; + + self.block_flow + .build_display_list_for_block(state, border_painting_mode); + + let iter = TableCellStyleIterator::new(&self); + for style in iter { + style.build_display_list(state) + } + } + + fn collect_stacking_contexts(&mut self, state: &mut StackingContextCollectionState) { + // Stacking contexts are collected by the table wrapper. + self.block_flow.collect_stacking_contexts_for_block( + state, + StackingContextCollectionFlags::NEVER_CREATES_STACKING_CONTEXT, + ); + } + + 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 iterate_through_fragment_border_boxes( + &self, + iterator: &mut dyn 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 dyn 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); + } +} + +#[derive(Debug)] +struct ColumnStyle<'table> { + span: u32, + colgroup_style: Option<&'table ComputedValues>, + col_style: Option<&'table ComputedValues>, +} + +impl fmt::Debug for TableFlow { + /// Outputs a debugging string describing this table flow. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "TableFlow: {:?}", self.block_flow) + } +} + +/// Table, TableRowGroup, TableRow, TableCell types. +/// Their inline-sizes are calculated in the same way and do not have margins. +pub struct InternalTable; + +impl ISizeAndMarginsComputer for InternalTable { + /// Compute the used value of inline-size, taking care of min-inline-size and max-inline-size. + /// + /// CSS Section 10.4: Minimum and Maximum inline-sizes + fn compute_used_inline_size( + &self, + block: &mut BlockFlow, + shared_context: &SharedStyleContext, + parent_flow_inline_size: Au, + ) { + let mut input = self.compute_inline_size_constraint_inputs( + block, + parent_flow_inline_size, + shared_context, + ); + + // Tables are always at least as wide as their minimum inline size. + let minimum_inline_size = block.base.intrinsic_inline_sizes.minimum_inline_size - + block.fragment.border_padding.inline_start_end(); + input.available_inline_size = cmp::max(input.available_inline_size, minimum_inline_size); + + let solution = self.solve_inline_size_constraints(block, &input); + self.set_inline_size_constraint_solutions(block, solution); + } + + /// Solve the inline-size and margins constraints for this block flow. + fn solve_inline_size_constraints( + &self, + _: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + ISizeConstraintSolution::new(input.available_inline_size, Au(0), Au(0)) + } +} + +/// Information about the intrinsic inline sizes of columns within a table. +/// +/// During table inline-size bubbling, we might need to store both a percentage constraint and a +/// specific width constraint. For instance, one cell might say that it wants to be 100 pixels wide +/// in the inline direction and another cell might say that it wants to take up 20% of the inline- +/// size of the table. Now because we bubble up these constraints during the bubble-inline-sizes +/// phase of layout, we don't know yet how wide the table is ultimately going to be in the inline +/// direction. As we need to pick the maximum width of all cells for a column (in this case, the +/// maximum of 100 pixels and 20% of the table), the preceding constraint means that we must +/// potentially store both a specified width *and* a specified percentage, so that the inline-size +/// assignment phase of layout will know which one to pick. +#[derive(Clone, Copy, Debug, Serialize)] +pub struct ColumnIntrinsicInlineSize { + /// The preferred intrinsic inline size. + pub preferred: Au, + /// The largest specified size of this column as a length. + pub minimum_length: Au, + /// The largest specified size of this column as a percentage (`width` property). + pub percentage: CSSFloat, + /// Whether the column inline size is *constrained* per INTRINSIC § 4.1. + pub constrained: bool, +} + +impl ColumnIntrinsicInlineSize { + /// Returns a newly-initialized `ColumnIntrinsicInlineSize` with all fields blank. + pub fn new() -> ColumnIntrinsicInlineSize { + ColumnIntrinsicInlineSize { + preferred: Au(0), + minimum_length: Au(0), + percentage: 0.0, + constrained: false, + } + } + + /// Returns the higher of the two percentages specified in `self` and `other`. + pub fn greatest_percentage(&self, other: &ColumnIntrinsicInlineSize) -> CSSFloat { + if self.percentage > other.percentage { + self.percentage + } else { + other.percentage + } + } +} + +/// The actual inline size for each column. +/// +/// TODO(pcwalton): There will probably be some `border-collapse`-related info in here too +/// eventually. +#[derive(Clone, Copy, Debug, Serialize)] +pub struct ColumnComputedInlineSize { + /// The computed size of this inline column. + pub size: Au, +} + +pub trait VecExt<T> { + fn push_or_set(&mut self, index: usize, value: T) -> &mut T; + fn get_mut_or_push(&mut self, index: usize, zero: T) -> &mut T; +} + +impl<T> VecExt<T> for Vec<T> { + fn push_or_set(&mut self, index: usize, value: T) -> &mut T { + if index < self.len() { + self[index] = value + } else { + debug_assert_eq!(index, self.len()); + self.push(value) + } + &mut self[index] + } + + fn get_mut_or_push(&mut self, index: usize, zero: T) -> &mut T { + if index >= self.len() { + debug_assert_eq!(index, self.len()); + self.push(zero) + } + &mut self[index] + } +} + +/// Updates the border styles in the block direction for a single row. This function should +/// only be called if border collapsing is on. It is factored out into a separate function +/// because we process children of rowgroups too. +fn perform_border_collapse_for_row( + child_table_row: &mut TableRowFlow, + table_inline_borders: &TableInlineCollapsedBorders, + previous_block_borders: PreviousBlockCollapsedBorders, + next_block_borders: NextBlockCollapsedBorders, + inline_spacing: &mut Vec<Au>, + block_spacing: &mut Vec<Au>, +) { + // TODO mbrubeck: Take rowspan and colspan into account. + let number_of_borders_inline_direction = + child_table_row.preliminary_collapsed_borders.inline.len(); + // Compute interior inline borders. + for (i, this_inline_border) in child_table_row + .preliminary_collapsed_borders + .inline + .iter_mut() + .enumerate() + { + child_table_row + .final_collapsed_borders + .inline + .push_or_set(i, *this_inline_border); + if i == 0 { + child_table_row.final_collapsed_borders.inline[i].combine(&table_inline_borders.start); + } else if i + 1 == number_of_borders_inline_direction { + child_table_row.final_collapsed_borders.inline[i].combine(&table_inline_borders.end); + } + + let inline_spacing = inline_spacing.get_mut_or_push(i, Au(0)); + *inline_spacing = cmp::max( + *inline_spacing, + child_table_row.final_collapsed_borders.inline[i].width, + ) + } + + // Compute block-start borders. + let block_start_borders = &mut child_table_row.final_collapsed_borders.block_start; + *block_start_borders = child_table_row + .preliminary_collapsed_borders + .block_start + .clone(); + for (i, this_border) in block_start_borders.iter_mut().enumerate() { + match previous_block_borders { + PreviousBlockCollapsedBorders::FromPreviousRow(ref previous_block_borders) => { + if previous_block_borders.len() > i { + this_border.combine(&previous_block_borders[i]); + } + }, + PreviousBlockCollapsedBorders::FromTable(table_border) => { + this_border.combine(&table_border); + }, + } + } + + // Compute block-end borders. + let next_block = &mut child_table_row.final_collapsed_borders.block_end; + block_spacing.push(Au(0)); + let block_spacing = block_spacing.last_mut().unwrap(); + for (i, this_block_border) in child_table_row + .preliminary_collapsed_borders + .block_end + .iter() + .enumerate() + { + let next_block = next_block.push_or_set(i, *this_block_border); + match next_block_borders { + NextBlockCollapsedBorders::FromNextRow(next_block_borders) => { + if next_block_borders.len() > i { + next_block.combine(&next_block_borders[i]) + } + }, + NextBlockCollapsedBorders::FromTable(ref next_block_borders) => { + next_block.combine(next_block_borders); + }, + } + *block_spacing = cmp::max(*block_spacing, next_block.width) + } +} + +/// Encapsulates functionality shared among all table-like flows: for now, tables and table +/// rowgroups. +pub trait TableLikeFlow { + /// Lays out the rows of a table. + fn assign_block_size_for_table_like_flow( + &mut self, + block_direction_spacing: Au, + layout_context: &LayoutContext, + ); +} + +impl TableLikeFlow for BlockFlow { + fn assign_block_size_for_table_like_flow( + &mut self, + block_direction_spacing: Au, + layout_context: &LayoutContext, + ) { + debug_assert!( + self.fragment.style.get_inherited_table().border_collapse == + border_collapse::T::Separate || + block_direction_spacing == Au(0) + ); + + fn border_spacing_for_row( + fragment: &Fragment, + row: &TableRowFlow, + block_direction_spacing: Au, + ) -> Au { + match fragment.style.get_inherited_table().border_collapse { + border_collapse::T::Separate => block_direction_spacing, + border_collapse::T::Collapse => row.collapsed_border_spacing.block_start, + } + } + + if self + .base + .restyle_damage + .contains(ServoRestyleDamage::REFLOW) + { + let mut sizes = vec![Default::default()]; + // The amount of border spacing up to and including this row, + // but not including the spacing beneath it + let mut cumulative_border_spacing = Au(0); + let mut incoming_rowspan_data = vec![]; + let mut rowgroup_id = 0; + let mut first = true; + + // First pass: Compute block-direction border spacings + // XXXManishearth this can be done in tandem with the second pass, + // provided we never hit any rowspan cases + for kid in self.base.child_iter_mut() { + if kid.is_table_row() { + // skip the first row, it is accounted for + if first { + first = false; + continue; + } + cumulative_border_spacing += border_spacing_for_row( + &self.fragment, + kid.as_table_row(), + block_direction_spacing, + ); + sizes.push(TableRowSizeData { + // we haven't calculated sizes yet + size: Au(0), + cumulative_border_spacing, + rowgroup_id, + }); + } else if kid.is_table_rowgroup() && !first { + rowgroup_id += 1; + } + } + + // Second pass: Compute row block sizes + // [expensive: iterates over cells] + let mut i = 0; + for kid in self.base.child_iter_mut() { + if kid.is_table_row() { + let size = kid.as_mut_table_row().compute_block_size_table_row_base( + layout_context, + &mut incoming_rowspan_data, + &sizes, + i, + ); + sizes[i].size = size; + i += 1; + } + } + + // Our current border-box position. + let block_start_border_padding = self.fragment.border_padding.block_start; + let mut current_block_offset = block_start_border_padding; + let mut has_rows = false; + + // Third pass: Assign block sizes and positions to rows, cells, and other children + // [expensive: iterates over cells] + // At this point, `current_block_offset` is at the content edge of our box. Now iterate + // over children. + let mut i = 0; + for kid in self.base.child_iter_mut() { + if kid.is_table_row() { + has_rows = true; + let row = kid.as_mut_table_row(); + row.assign_block_size_to_self_and_children(&sizes, i); + row.mut_base().restyle_damage.remove( + ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW, + ); + current_block_offset = current_block_offset + + border_spacing_for_row(&self.fragment, row, block_direction_spacing); + i += 1; + } + + // At this point, `current_block_offset` is at the border edge of the child. + kid.mut_base().position.start.b = current_block_offset; + + // Move past the child's border box. Do not use the `translate_including_floats` + // function here because the child has already translated floats past its border + // box. + let kid_base = kid.mut_base(); + current_block_offset = current_block_offset + kid_base.position.size.block; + } + + // Compute any explicitly-specified block size. + // Can't use `for` because we assign to + // `candidate_block_size_iterator.candidate_value`. + let mut block_size = current_block_offset - block_start_border_padding; + let mut candidate_block_size_iterator = CandidateBSizeIterator::new( + &self.fragment, + self.base.block_container_explicit_block_size, + ); + while let Some(candidate_block_size) = candidate_block_size_iterator.next() { + candidate_block_size_iterator.candidate_value = match candidate_block_size { + MaybeAuto::Auto => block_size, + MaybeAuto::Specified(value) => value, + }; + } + + // Adjust `current_block_offset` as necessary to account for the explicitly-specified + // block-size. + block_size = candidate_block_size_iterator.candidate_value; + let delta = block_size - (current_block_offset - block_start_border_padding); + current_block_offset = current_block_offset + delta; + + // Take border, padding, and spacing into account. + let block_end_offset = self.fragment.border_padding.block_end + + if has_rows { + block_direction_spacing + } else { + Au(0) + }; + current_block_offset = current_block_offset + block_end_offset; + + // Now that `current_block_offset` is at the block-end of the border box, compute the + // final border box position. + self.fragment.border_box.size.block = current_block_offset; + self.fragment.border_box.start.b = Au(0); + self.base.position.size.block = current_block_offset; + + // Fourth pass: Assign absolute position info + // Write in the size of the relative containing block for children. (This information + // is also needed to handle RTL.) + for kid in self.base.child_iter_mut() { + kid.mut_base().early_absolute_position_info = EarlyAbsolutePositionInfo { + relative_containing_block_size: self.fragment.content_box().size, + relative_containing_block_mode: self.fragment.style().writing_mode, + }; + } + } + + self.base + .restyle_damage + .remove(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW); + } +} + +/// Inline collapsed borders for the table itself. +#[derive(Debug)] +struct TableInlineCollapsedBorders { + /// The table border at the start of the inline direction. + start: CollapsedBorder, + /// The table border at the end of the inline direction. + end: CollapsedBorder, +} + +enum PreviousBlockCollapsedBorders { + FromPreviousRow(Vec<CollapsedBorder>), + FromTable(CollapsedBorder), +} + +enum NextBlockCollapsedBorders<'a> { + FromNextRow(&'a [CollapsedBorder]), + FromTable(CollapsedBorder), +} + +/// Iterator over all the rows of a table, which also +/// provides the Fragment for rowgroups if any +struct TableRowAndGroupIterator<'a> { + kids: FlowListIterator<'a>, + group: Option<(&'a Fragment, FlowListIterator<'a>)>, +} + +impl<'a> TableRowAndGroupIterator<'a> { + fn new(base: &'a BaseFlow) -> Self { + TableRowAndGroupIterator { + kids: base.child_iter(), + group: None, + } + } +} + +impl<'a> Iterator for TableRowAndGroupIterator<'a> { + type Item = (Option<&'a Fragment>, &'a TableRowFlow); + #[inline] + fn next(&mut self) -> Option<Self::Item> { + // If we're inside a rowgroup, iterate through the rowgroup's children. + if let Some(ref mut group) = self.group { + if let Some(grandkid) = group.1.next() { + return Some((Some(group.0), grandkid.as_table_row())); + } + } + // Otherwise, iterate through the table's children. + self.group = None; + match self.kids.next() { + Some(kid) => { + if kid.is_table_rowgroup() { + let rowgroup = kid.as_table_rowgroup(); + let iter = rowgroup.block_flow.base.child_iter(); + self.group = Some((&rowgroup.block_flow.fragment, iter)); + self.next() + } else if kid.is_table_row() { + Some((None, kid.as_table_row())) + } else { + self.next() // Skip children that are not rows or rowgroups + } + }, + None => None, + } + } +} + +/// Iterator over all the rows of a table, which also +/// provides the Fragment for rowgroups if any +struct MutTableRowAndGroupIterator<'a> { + kids: MutFlowListIterator<'a>, + group: Option<(&'a Fragment, MutFlowListIterator<'a>)>, +} + +impl<'a> MutTableRowAndGroupIterator<'a> { + fn new(base: &'a mut BaseFlow) -> Self { + MutTableRowAndGroupIterator { + kids: base.child_iter_mut(), + group: None, + } + } +} + +impl<'a> Iterator for MutTableRowAndGroupIterator<'a> { + type Item = (Option<&'a Fragment>, &'a mut TableRowFlow); + #[inline] + fn next(&mut self) -> Option<Self::Item> { + // If we're inside a rowgroup, iterate through the rowgroup's children. + if let Some(ref mut group) = self.group { + if let Some(grandkid) = group.1.next() { + return Some((Some(group.0), grandkid.as_mut_table_row())); + } + } + // Otherwise, iterate through the table's children. + self.group = None; + match self.kids.next() { + Some(kid) => { + if kid.is_table_rowgroup() { + let rowgroup = kid.as_mut_table_rowgroup(); + let iter = rowgroup.block_flow.base.child_iter_mut(); + self.group = Some((&rowgroup.block_flow.fragment, iter)); + self.next() + } else if kid.is_table_row() { + Some((None, kid.as_mut_table_row())) + } else { + self.next() // Skip children that are not rows or rowgroups + } + }, + None => None, + } + } +} + +/// Iterator over all the rows of a table +struct TableRowIterator<'a>(MutTableRowAndGroupIterator<'a>); + +impl<'a> TableRowIterator<'a> { + fn new(base: &'a mut BaseFlow) -> Self { + TableRowIterator(MutTableRowAndGroupIterator::new(base)) + } +} + +impl<'a> Iterator for TableRowIterator<'a> { + type Item = &'a mut TableRowFlow; + #[inline] + fn next(&mut self) -> Option<Self::Item> { + self.0.next().map(|n| n.1) + } +} + +/// An iterator over table cells, yielding all relevant style objects +/// for each cell +/// +/// Used for correctly handling table layers from +/// https://drafts.csswg.org/css2/tables.html#table-layers +struct TableCellStyleIterator<'table> { + column_styles: Vec<ColumnStyle<'table>>, + row_iterator: TableRowAndGroupIterator<'table>, + row_info: Option<TableCellStyleIteratorRowInfo<'table>>, + column_index: TableCellColumnIndexData, +} + +struct TableCellStyleIteratorRowInfo<'table> { + row: &'table TableRowFlow, + rowgroup: Option<&'table Fragment>, + cell_iterator: FlowListIterator<'table>, +} + +impl<'table> TableCellStyleIterator<'table> { + fn new(table: &'table TableFlow) -> Self { + let column_styles = table.column_styles(); + let mut row_iterator = TableRowAndGroupIterator::new(&table.block_flow.base); + let row_info = if let Some((group, row)) = row_iterator.next() { + Some(TableCellStyleIteratorRowInfo { + row: &row, + rowgroup: group, + cell_iterator: row.block_flow.base.child_iter(), + }) + } else { + None + }; + TableCellStyleIterator { + column_styles, + row_iterator, + row_info, + column_index: Default::default(), + } + } +} + +struct TableCellStyleInfo<'table> { + cell: &'table TableCellFlow, + colgroup_style: Option<&'table ComputedValues>, + col_style: Option<&'table ComputedValues>, + rowgroup_style: Option<&'table ComputedValues>, + row_style: &'table ComputedValues, +} + +struct TableCellColumnIndexData { + /// Which column this is in the table + pub absolute: u32, + /// The index of the current column in column_styles + /// (i.e. which <col> element it is) + pub relative: u32, + /// In case of multispan <col>s, where we are in the + /// span of the current <col> element + pub relative_offset: u32, +} + +impl Default for TableCellColumnIndexData { + fn default() -> Self { + TableCellColumnIndexData { + absolute: 0, + relative: 0, + relative_offset: 0, + } + } +} + +impl TableCellColumnIndexData { + /// Moves forward by `amount` columns, updating the various indices used + /// + /// This totally ignores rowspan -- if colspan and rowspan clash, + /// they just overlap, so we ignore it. + fn advance(&mut self, amount: u32, column_styles: &[ColumnStyle]) { + self.absolute += amount; + self.relative_offset += amount; + if let Some(mut current_col) = column_styles.get(self.relative as usize) { + while self.relative_offset >= current_col.span { + // move to the next column + self.relative += 1; + self.relative_offset -= current_col.span; + if let Some(column_style) = column_styles.get(self.relative as usize) { + current_col = column_style; + } else { + // we ran out of column_styles, + // so we don't need to update the indices + break; + } + } + } + } +} + +impl<'table> Iterator for TableCellStyleIterator<'table> { + type Item = TableCellStyleInfo<'table>; + #[inline] + fn next(&mut self) -> Option<Self::Item> { + // FIXME We do this awkward .take() followed by shoving it back in + // because without NLL the row_info borrow lasts too long + if let Some(mut row_info) = self.row_info.take() { + if let Some(rowspan) = row_info + .row + .incoming_rowspan + .get(self.column_index.absolute as usize) + { + // we are not allowed to use this column as a starting point. Try the next one. + if *rowspan > 1 { + self.column_index.advance(1, &self.column_styles); + // put row_info back in + self.row_info = Some(row_info); + // try again + return self.next(); + } + } + if let Some(cell) = row_info.cell_iterator.next() { + let rowgroup_style = row_info.rowgroup.map(|r| r.style()); + let row_style = row_info.row.block_flow.fragment.style(); + let cell = cell.as_table_cell(); + let (col_style, colgroup_style) = if let Some(column_style) = + self.column_styles.get(self.column_index.relative as usize) + { + let styles = ( + column_style.col_style.clone(), + column_style.colgroup_style.clone(), + ); + self.column_index + .advance(cell.column_span, &self.column_styles); + + styles + } else { + (None, None) + }; + // put row_info back in + self.row_info = Some(row_info); + return Some(TableCellStyleInfo { + cell, + colgroup_style, + col_style, + rowgroup_style, + row_style, + }); + } else { + // next row + if let Some((group, row)) = self.row_iterator.next() { + self.row_info = Some(TableCellStyleIteratorRowInfo { + row: &row, + rowgroup: group, + cell_iterator: row.block_flow.base.child_iter(), + }); + self.column_index = Default::default(); + self.next() + } else { + // out of rows + // row_info stays None + None + } + } + } else { + // empty table + None + } + } +} + +impl<'table> TableCellStyleInfo<'table> { + fn build_display_list(&self, mut state: &mut DisplayListBuildState) { + use style::computed_values::visibility::T as Visibility; + + if !self.cell.visible || + self.cell + .block_flow + .fragment + .style() + .get_inherited_box() + .visibility != + Visibility::Visible + { + return; + } + let border_painting_mode = match self + .cell + .block_flow + .fragment + .style + .get_inherited_table() + .border_collapse + { + border_collapse::T::Separate => BorderPaintingMode::Separate, + border_collapse::T::Collapse => { + BorderPaintingMode::Collapse(&self.cell.collapsed_borders) + }, + }; + { + let cell_flow = &self.cell.block_flow; + let initial = ComputedValues::initial_values(); + + let build_dl = |sty: &ComputedValues, state: &mut &mut DisplayListBuildState| { + let background = sty.get_background(); + // Don't redraw backgrounds that we've already drawn + if background as *const Background == initial.get_background() as *const _ { + return; + } + let background_color = sty.resolve_color(background.background_color); + cell_flow.build_display_list_for_background_if_applicable_with_background( + state, + background, + background_color, + ); + }; + + if let Some(ref sty) = self.colgroup_style { + build_dl(&sty, &mut state); + } + if let Some(ref sty) = self.col_style { + build_dl(&sty, &mut state); + } + if let Some(ref sty) = self.rowgroup_style { + build_dl(sty, &mut state); + } + build_dl(self.row_style, &mut state); + } + // the restyle damage will be set in TableCellFlow::build_display_list() + self.cell + .block_flow + .build_display_list_for_block_no_damage(state, border_painting_mode) + } +} diff --git a/components/layout_2020/table_caption.rs b/components/layout_2020/table_caption.rs new file mode 100644 index 00000000000..3d44190f986 --- /dev/null +++ b/components/layout_2020/table_caption.rs @@ -0,0 +1,139 @@ +/* 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/. */ + +//! CSS table formatting contexts. + +use crate::block::BlockFlow; +use crate::context::LayoutContext; +use crate::display_list::{ + DisplayListBuildState, StackingContextCollectionFlags, StackingContextCollectionState, +}; +use crate::flow::{Flow, FlowClass, OpaqueFlow}; +use crate::fragment::{Fragment, FragmentBorderBoxIterator, Overflow}; +use app_units::Au; +use euclid::Point2D; +use gfx_traits::print_tree::PrintTree; +use std::fmt; +use style::logical_geometry::LogicalSize; +use style::properties::ComputedValues; + +#[allow(unsafe_code)] +unsafe impl crate::flow::HasBaseFlow for TableCaptionFlow {} + +/// A table formatting context. +#[repr(C)] +pub struct TableCaptionFlow { + pub block_flow: BlockFlow, +} + +impl TableCaptionFlow { + pub fn from_fragment(fragment: Fragment) -> TableCaptionFlow { + TableCaptionFlow { + block_flow: BlockFlow::from_fragment(fragment), + } + } +} + +impl Flow for TableCaptionFlow { + fn class(&self) -> FlowClass { + FlowClass::TableCaption + } + + fn as_mut_block(&mut self) -> &mut BlockFlow { + &mut self.block_flow + } + + fn as_block(&self) -> &BlockFlow { + &self.block_flow + } + + fn bubble_inline_sizes(&mut self) { + self.block_flow.bubble_inline_sizes(); + } + + fn assign_inline_sizes(&mut self, layout_context: &LayoutContext) { + debug!( + "assign_inline_sizes({}): assigning inline_size for flow", + "table_caption" + ); + self.block_flow.assign_inline_sizes(layout_context); + } + + fn assign_block_size(&mut self, layout_context: &LayoutContext) { + debug!("assign_block_size: assigning block_size for table_caption"); + self.block_flow.assign_block_size(layout_context); + } + + 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, state: &mut DisplayListBuildState) { + debug!("build_display_list_table_caption: same process as block flow"); + self.block_flow.build_display_list(state); + } + + 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 dyn 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 dyn 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 TableCaptionFlow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "TableCaptionFlow: {:?}", self.block_flow) + } +} diff --git a/components/layout_2020/table_cell.rs b/components/layout_2020/table_cell.rs new file mode 100644 index 00000000000..ec79f9f4540 --- /dev/null +++ b/components/layout_2020/table_cell.rs @@ -0,0 +1,506 @@ +/* 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/. */ + +//! CSS table formatting contexts. + +use crate::block::{BlockFlow, ISizeAndMarginsComputer, MarginsMayCollapseFlag}; +use crate::context::LayoutContext; +use crate::display_list::{ + DisplayListBuildState, StackingContextCollectionFlags, StackingContextCollectionState, +}; +use crate::flow::{Flow, FlowClass, FlowFlags, GetBaseFlow, OpaqueFlow}; +use crate::fragment::{Fragment, FragmentBorderBoxIterator, Overflow}; +use crate::layout_debug; +use crate::table::InternalTable; +use crate::table_row::{CollapsedBorder, CollapsedBorderProvenance}; +use app_units::Au; +use euclid::{Point2D, Rect, SideOffsets2D, Size2D}; +use gfx_traits::print_tree::PrintTree; +use script_layout_interface::wrapper_traits::ThreadSafeLayoutNode; +use std::fmt; +use style::logical_geometry::{LogicalMargin, LogicalRect, LogicalSize, WritingMode}; +use style::properties::ComputedValues; +use style::values::computed::length::Size; +use style::values::computed::Color; +use style::values::generics::box_::{VerticalAlign, VerticalAlignKeyword}; +use style::values::specified::BorderStyle; + +#[allow(unsafe_code)] +unsafe impl crate::flow::HasBaseFlow for TableCellFlow {} + +/// A table formatting context. +#[derive(Serialize)] +#[repr(C)] +pub struct TableCellFlow { + /// Data common to all block flows. + pub block_flow: BlockFlow, + + /// Border collapse information for the cell. + pub collapsed_borders: CollapsedBordersForCell, + + /// The column span of this cell. + pub column_span: u32, + + /// The rows spanned by this cell. + pub row_span: u32, + + /// Whether this cell is visible. If false, the value of `empty-cells` means that we must not + /// display this cell. + pub visible: bool, +} + +impl TableCellFlow { + pub fn from_fragment(fragment: Fragment) -> TableCellFlow { + TableCellFlow { + block_flow: BlockFlow::from_fragment(fragment), + collapsed_borders: CollapsedBordersForCell::new(), + column_span: 1, + row_span: 1, + visible: true, + } + } + + pub fn from_node_fragment_and_visibility_flag<N: ThreadSafeLayoutNode>( + node: &N, + fragment: Fragment, + visible: bool, + ) -> TableCellFlow { + TableCellFlow { + block_flow: BlockFlow::from_fragment(fragment), + collapsed_borders: CollapsedBordersForCell::new(), + column_span: node.get_colspan(), + row_span: node.get_rowspan(), + visible: visible, + } + } + + pub fn fragment(&mut self) -> &Fragment { + &self.block_flow.fragment + } + + pub fn mut_fragment(&mut self) -> &mut Fragment { + &mut self.block_flow.fragment + } + + /// Assign block-size for table-cell flow. + /// + /// inline(always) because this is only ever called by in-order or non-in-order top-level + /// methods. + #[inline(always)] + fn assign_block_size_table_cell_base(&mut self, layout_context: &LayoutContext) { + let remaining = self.block_flow.assign_block_size_block_base( + layout_context, + None, + MarginsMayCollapseFlag::MarginsMayNotCollapse, + ); + debug_assert!(remaining.is_none()); + } + + /// Position this cell's children according to vertical-align. + pub fn valign_children(&mut self) { + // Note to the reader: this code has been tested with negative margins. + // We end up with a "end" that's before the "start," but the math still works out. + let mut extents = None; + for kid in self.base().children.iter() { + let kid_base = kid.base(); + if kid_base.flags.contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) { + continue; + } + let start = kid_base.position.start.b - + kid_base + .collapsible_margins + .block_start_margin_for_noncollapsible_context(); + let end = kid_base.position.start.b + + kid_base.position.size.block + + kid_base + .collapsible_margins + .block_end_margin_for_noncollapsible_context(); + match extents { + Some((ref mut first_start, ref mut last_end)) => { + if start < *first_start { + *first_start = start + } + if end > *last_end { + *last_end = end + } + }, + None => extents = Some((start, end)), + } + } + let (first_start, last_end) = match extents { + Some(extents) => extents, + None => return, + }; + + let kids_size = last_end - first_start; + let self_size = self.base().position.size.block - + self.block_flow.fragment.border_padding.block_start_end(); + let kids_self_gap = self_size - kids_size; + + // This offset should also account for VerticalAlign::baseline. + // Need max cell ascent from the first row of this cell. + let offset = match self.block_flow.fragment.style().get_box().vertical_align { + VerticalAlign::Keyword(VerticalAlignKeyword::Middle) => kids_self_gap / 2, + VerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => kids_self_gap, + _ => Au(0), + }; + if offset == Au(0) { + return; + } + + for kid in self.mut_base().children.iter_mut() { + let kid_base = kid.mut_base(); + if !kid_base.flags.contains(FlowFlags::IS_ABSOLUTELY_POSITIONED) { + kid_base.position.start.b += offset + } + } + } + + // Total block size of child + // + // Call after block size calculation + pub fn total_block_size(&mut self) -> Au { + // TODO: Percentage block-size + let specified = self + .fragment() + .style() + .content_block_size() + .to_used_value(Au(0)) + .unwrap_or(Au(0)); + specified + self.fragment().border_padding.block_start_end() + } +} + +impl Flow for TableCellFlow { + fn class(&self) -> FlowClass { + FlowClass::TableCell + } + + fn as_mut_table_cell(&mut self) -> &mut TableCellFlow { + self + } + + fn as_table_cell(&self) -> &TableCellFlow { + self + } + + fn as_mut_block(&mut self) -> &mut BlockFlow { + &mut self.block_flow + } + + fn as_block(&self) -> &BlockFlow { + &self.block_flow + } + + /// Minimum/preferred inline-sizes set by this function are used in automatic table layout + /// calculation. + fn bubble_inline_sizes(&mut self) { + let _scope = layout_debug_scope!( + "table_cell::bubble_inline_sizes {:x}", + self.block_flow.base.debug_id() + ); + + self.block_flow.bubble_inline_sizes_for_block(true); + let specified_inline_size = match self.block_flow.fragment.style().content_inline_size() { + Size::Auto => Au(0), + Size::LengthPercentage(ref lp) => lp.to_used_value(Au(0)), + }; + + if self + .block_flow + .base + .intrinsic_inline_sizes + .minimum_inline_size < + specified_inline_size + { + self.block_flow + .base + .intrinsic_inline_sizes + .minimum_inline_size = specified_inline_size + } + if self + .block_flow + .base + .intrinsic_inline_sizes + .preferred_inline_size < + self.block_flow + .base + .intrinsic_inline_sizes + .minimum_inline_size + { + self.block_flow + .base + .intrinsic_inline_sizes + .preferred_inline_size = self + .block_flow + .base + .intrinsic_inline_sizes + .minimum_inline_size; + } + } + + /// Recursively (top-down) determines the actual inline-size of child contexts and fragments. + /// When called on this context, the context has had its inline-size set by the parent table + /// row. + fn assign_inline_sizes(&mut self, layout_context: &LayoutContext) { + let _scope = layout_debug_scope!( + "table_cell::assign_inline_sizes {:x}", + self.block_flow.base.debug_id() + ); + debug!( + "assign_inline_sizes({}): assigning inline_size for flow", + "table_cell" + ); + + let shared_context = layout_context.shared_context(); + // The position was set to the column inline-size by the parent flow, table row flow. + let containing_block_inline_size = self.block_flow.base.block_container_inline_size; + + let inline_size_computer = InternalTable; + inline_size_computer.compute_used_inline_size( + &mut self.block_flow, + shared_context, + containing_block_inline_size, + ); + + let inline_start_content_edge = self.block_flow.fragment.border_box.start.i + + self.block_flow.fragment.border_padding.inline_start; + let inline_end_content_edge = self.block_flow.base.block_container_inline_size - + self.block_flow.fragment.border_padding.inline_start_end() - + self.block_flow.fragment.border_box.size.inline; + let padding_and_borders = self.block_flow.fragment.border_padding.inline_start_end(); + let content_inline_size = + self.block_flow.fragment.border_box.size.inline - padding_and_borders; + + self.block_flow.propagate_assigned_inline_size_to_children( + shared_context, + inline_start_content_edge, + inline_end_content_edge, + content_inline_size, + |_, _, _, _, _, _| {}, + ); + } + + fn assign_block_size(&mut self, layout_context: &LayoutContext) { + debug!("assign_block_size: assigning block_size for table_cell"); + self.assign_block_size_table_cell_base(layout_context); + } + + 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; + // This is handled by TableCellStyleInfo::build_display_list() + // when the containing table builds its 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 dyn 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 dyn 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 TableCellFlow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "TableCellFlow: {:?}", self.block_flow) + } +} + +#[derive(Clone, Copy, Debug, Serialize)] +pub struct CollapsedBordersForCell { + pub inline_start_border: CollapsedBorder, + pub inline_end_border: CollapsedBorder, + pub block_start_border: CollapsedBorder, + pub block_end_border: CollapsedBorder, + pub inline_start_width: Au, + pub inline_end_width: Au, + pub block_start_width: Au, + pub block_end_width: Au, +} + +impl CollapsedBordersForCell { + fn new() -> CollapsedBordersForCell { + CollapsedBordersForCell { + inline_start_border: CollapsedBorder::new(), + inline_end_border: CollapsedBorder::new(), + block_start_border: CollapsedBorder::new(), + block_end_border: CollapsedBorder::new(), + inline_start_width: Au(0), + inline_end_width: Au(0), + block_start_width: Au(0), + block_end_width: Au(0), + } + } + + fn should_paint_inline_start_border(&self) -> bool { + self.inline_start_border.provenance != CollapsedBorderProvenance::FromPreviousTableCell + } + + fn should_paint_inline_end_border(&self) -> bool { + self.inline_end_border.provenance != CollapsedBorderProvenance::FromNextTableCell + } + + fn should_paint_block_start_border(&self) -> bool { + self.block_start_border.provenance != CollapsedBorderProvenance::FromPreviousTableCell + } + + fn should_paint_block_end_border(&self) -> bool { + self.block_end_border.provenance != CollapsedBorderProvenance::FromNextTableCell + } + + pub fn adjust_border_widths_for_painting(&self, border_widths: &mut LogicalMargin<Au>) { + border_widths.inline_start = if !self.should_paint_inline_start_border() { + Au(0) + } else { + self.inline_start_border.width + }; + border_widths.inline_end = if !self.should_paint_inline_end_border() { + Au(0) + } else { + self.inline_end_border.width + }; + border_widths.block_start = if !self.should_paint_block_start_border() { + Au(0) + } else { + self.block_start_border.width + }; + border_widths.block_end = if !self.should_paint_block_end_border() { + Au(0) + } else { + self.block_end_border.width + } + } + + pub fn adjust_border_bounds_for_painting( + &self, + border_bounds: &mut Rect<Au>, + writing_mode: WritingMode, + ) { + let inline_start_divisor = if self.should_paint_inline_start_border() { + 2 + } else { + -2 + }; + let inline_start_offset = + self.inline_start_width / 2 + self.inline_start_border.width / inline_start_divisor; + let inline_end_divisor = if self.should_paint_inline_end_border() { + 2 + } else { + -2 + }; + let inline_end_offset = + self.inline_end_width / 2 + self.inline_end_border.width / inline_end_divisor; + let block_start_divisor = if self.should_paint_block_start_border() { + 2 + } else { + -2 + }; + let block_start_offset = + self.block_start_width / 2 + self.block_start_border.width / block_start_divisor; + let block_end_divisor = if self.should_paint_block_end_border() { + 2 + } else { + -2 + }; + let block_end_offset = + self.block_end_width / 2 + self.block_end_border.width / block_end_divisor; + + // FIXME(pcwalton): Get the real container size. + let mut logical_bounds = + LogicalRect::from_physical(writing_mode, *border_bounds, Size2D::new(Au(0), Au(0))); + logical_bounds.start.i = logical_bounds.start.i - inline_start_offset; + logical_bounds.start.b = logical_bounds.start.b - block_start_offset; + logical_bounds.size.inline = + logical_bounds.size.inline + inline_start_offset + inline_end_offset; + logical_bounds.size.block = + logical_bounds.size.block + block_start_offset + block_end_offset; + *border_bounds = logical_bounds.to_physical(writing_mode, Size2D::new(Au(0), Au(0))) + } + + pub fn adjust_border_colors_and_styles_for_painting( + &self, + border_colors: &mut SideOffsets2D<Color>, + border_styles: &mut SideOffsets2D<BorderStyle>, + writing_mode: WritingMode, + ) { + let logical_border_colors = LogicalMargin::new( + writing_mode, + self.block_start_border.color, + self.inline_end_border.color, + self.block_end_border.color, + self.inline_start_border.color, + ); + *border_colors = logical_border_colors.to_physical(writing_mode); + + let logical_border_styles = LogicalMargin::new( + writing_mode, + self.block_start_border.style, + self.inline_end_border.style, + self.block_end_border.style, + self.inline_start_border.style, + ); + *border_styles = logical_border_styles.to_physical(writing_mode); + } +} diff --git a/components/layout_2020/table_colgroup.rs b/components/layout_2020/table_colgroup.rs new file mode 100644 index 00000000000..c07ad56f95a --- /dev/null +++ b/components/layout_2020/table_colgroup.rs @@ -0,0 +1,131 @@ +/* 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/. */ + +//! CSS table formatting contexts. + +use crate::context::LayoutContext; +use crate::display_list::{DisplayListBuildState, StackingContextCollectionState}; +use crate::flow::{BaseFlow, Flow, FlowClass, ForceNonfloatedFlag, OpaqueFlow}; +use crate::fragment::{Fragment, FragmentBorderBoxIterator, Overflow}; +use crate::layout_debug; +use app_units::Au; +use euclid::Point2D; +use std::fmt; +use style::logical_geometry::LogicalSize; +use style::properties::ComputedValues; +use style::values::computed::Size; + +#[allow(unsafe_code)] +unsafe impl crate::flow::HasBaseFlow for TableColGroupFlow {} + +/// A table formatting context. +#[repr(C)] +pub struct TableColGroupFlow { + /// Data common to all flows. + pub base: BaseFlow, + + /// The associated fragment. + pub fragment: Option<Fragment>, + + /// The table column fragments + pub cols: Vec<Fragment>, + + /// The specified inline-sizes of table columns. (We use `LengthPercentageOrAuto` here in + /// lieu of `ColumnInlineSize` because column groups do not establish minimum or preferred + /// inline sizes.) + pub inline_sizes: Vec<Size>, +} + +impl TableColGroupFlow { + pub fn from_fragments(fragment: Fragment, fragments: Vec<Fragment>) -> TableColGroupFlow { + let writing_mode = fragment.style().writing_mode; + TableColGroupFlow { + base: BaseFlow::new( + Some(fragment.style()), + writing_mode, + ForceNonfloatedFlag::ForceNonfloated, + ), + fragment: Some(fragment), + cols: fragments, + inline_sizes: vec![], + } + } +} + +impl Flow for TableColGroupFlow { + fn class(&self) -> FlowClass { + FlowClass::TableColGroup + } + + fn as_mut_table_colgroup(&mut self) -> &mut TableColGroupFlow { + self + } + + fn as_table_colgroup(&self) -> &TableColGroupFlow { + self + } + + fn bubble_inline_sizes(&mut self) { + let _scope = layout_debug_scope!( + "table_colgroup::bubble_inline_sizes {:x}", + self.base.debug_id() + ); + + for fragment in &self.cols { + // Retrieve the specified value from the appropriate CSS property. + let inline_size = fragment.style().content_inline_size(); + for _ in 0..fragment.column_span() { + self.inline_sizes.push(inline_size) + } + } + } + + /// Table column inline-sizes are assigned in the table flow and propagated to table row flows + /// and/or rowgroup flows. Therefore, table colgroup flows do not need to assign inline-sizes. + fn assign_inline_sizes(&mut self, _: &LayoutContext) {} + + /// Table columns do not have block-size. + fn assign_block_size(&mut self, _: &LayoutContext) {} + + fn update_late_computed_inline_position_if_necessary(&mut self, _: Au) {} + + fn update_late_computed_block_position_if_necessary(&mut self, _: Au) {} + + // Table columns are invisible. + fn build_display_list(&mut self, _: &mut DisplayListBuildState) {} + + fn collect_stacking_contexts(&mut self, state: &mut StackingContextCollectionState) { + self.base.stacking_context_id = state.current_stacking_context_id; + self.base.clipping_and_scrolling = Some(state.current_clipping_and_scrolling); + } + + fn repair_style(&mut self, _: &crate::ServoArc<ComputedValues>) {} + + fn compute_overflow(&self) -> Overflow { + Overflow::new() + } + + fn generated_containing_block_size(&self, _: OpaqueFlow) -> LogicalSize<Au> { + panic!("Table column groups can't be containing blocks!") + } + + fn iterate_through_fragment_border_boxes( + &self, + _: &mut dyn FragmentBorderBoxIterator, + _: i32, + _: &Point2D<Au>, + ) { + } + + fn mutate_fragments(&mut self, _: &mut dyn FnMut(&mut Fragment)) {} +} + +impl fmt::Debug for TableColGroupFlow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.fragment { + Some(ref rb) => write!(f, "TableColGroupFlow: {:?}", rb), + None => write!(f, "TableColGroupFlow"), + } + } +} diff --git a/components/layout_2020/table_row.rs b/components/layout_2020/table_row.rs new file mode 100644 index 00000000000..824da31edad --- /dev/null +++ b/components/layout_2020/table_row.rs @@ -0,0 +1,1158 @@ +/* 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/. */ + +//! CSS table formatting contexts. + +use crate::block::{BlockFlow, ISizeAndMarginsComputer}; +use crate::context::LayoutContext; +use crate::display_list::{ + DisplayListBuildState, 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::table::{ColumnComputedInlineSize, ColumnIntrinsicInlineSize, InternalTable, VecExt}; +use crate::table_cell::{CollapsedBordersForCell, TableCellFlow}; +use app_units::Au; +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, Size}; + +#[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, + ); + + // TODO: Percentage block-size + let block_size = self + .block_flow + .fragment + .style() + .content_block_size() + .to_used_value(Au(0)) + .unwrap_or(max_block_size); + max(block_size, max_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 { + Size::Auto => None, + Size::LengthPercentage(ref lp) => lp.0.maybe_to_used_value(None), + } + .unwrap_or(child_base.intrinsic_inline_sizes.minimum_inline_size), + percentage: match child_specified_inline_size { + Size::Auto => 0.0, + Size::LengthPercentage(ref lp) => lp.0.as_percentage().map_or(0.0, |p| p.0), + }, + preferred: child_base.intrinsic_inline_sizes.preferred_inline_size, + constrained: match child_specified_inline_size { + Size::Auto => false, + Size::LengthPercentage(ref lp) => lp.0.maybe_to_used_value(None).is_some(), + }, + }; + 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 dyn 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 dyn 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 dyn 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 dyn 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); +} diff --git a/components/layout_2020/table_rowgroup.rs b/components/layout_2020/table_rowgroup.rs new file mode 100644 index 00000000000..814cbe0d2d2 --- /dev/null +++ b/components/layout_2020/table_rowgroup.rs @@ -0,0 +1,274 @@ +/* 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/. */ + +//! CSS table formatting contexts. + +use crate::block::{BlockFlow, ISizeAndMarginsComputer}; +use crate::context::LayoutContext; +use crate::display_list::{ + DisplayListBuildState, StackingContextCollectionFlags, StackingContextCollectionState, +}; +use crate::flow::{Flow, FlowClass, OpaqueFlow}; +use crate::fragment::{Fragment, FragmentBorderBoxIterator, Overflow}; +use crate::layout_debug; +use crate::table::{ColumnIntrinsicInlineSize, InternalTable, TableLikeFlow}; +use app_units::Au; +use euclid::Point2D; +use gfx_traits::print_tree::PrintTree; +use serde::{Serialize, Serializer}; +use std::fmt; +use std::iter::{IntoIterator, Iterator, Peekable}; +use style::computed_values::{border_collapse, border_spacing}; +use style::logical_geometry::LogicalSize; +use style::properties::ComputedValues; + +#[allow(unsafe_code)] +unsafe impl crate::flow::HasBaseFlow for TableRowGroupFlow {} + +/// A table formatting context. +#[repr(C)] +pub struct TableRowGroupFlow { + /// Fields common to all block flows. + pub block_flow: BlockFlow, + + /// Information about the intrinsic inline-sizes of each column. + pub column_intrinsic_inline_sizes: Vec<ColumnIntrinsicInlineSize>, + + /// The spacing for this rowgroup. + pub spacing: border_spacing::T, + + /// The final width of the borders in the inline direction for each cell, computed by the + /// entire table and pushed down into each row during inline size computation. + pub collapsed_inline_direction_border_widths_for_table: Vec<Au>, + + /// The final width of the borders in the block direction for each cell, computed by the + /// entire table and pushed down into each row during inline size computation. + pub collapsed_block_direction_border_widths_for_table: Vec<Au>, +} + +impl Serialize for TableRowGroupFlow { + fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { + self.block_flow.serialize(serializer) + } +} + +impl TableRowGroupFlow { + pub fn from_fragment(fragment: Fragment) -> TableRowGroupFlow { + TableRowGroupFlow { + block_flow: BlockFlow::from_fragment(fragment), + column_intrinsic_inline_sizes: Vec::new(), + spacing: border_spacing::T::zero(), + collapsed_inline_direction_border_widths_for_table: Vec::new(), + collapsed_block_direction_border_widths_for_table: Vec::new(), + } + } + + 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_inline_direction_border_widths_for_table + .clear(); + self.collapsed_inline_direction_border_widths_for_table + .extend( + collapsed_inline_direction_border_widths_for_table + .into_iter() + .map(|x| *x), + ); + + for _ in 0..self.block_flow.base.children.len() { + if let Some(collapsed_block_direction_border_width_for_table) = + collapsed_block_direction_border_widths_for_table.next() + { + self.collapsed_block_direction_border_widths_for_table + .push(*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_block_direction_border_widths_for_table + .push(**collapsed_block_direction_border_width_for_table) + } + } +} + +impl Flow for TableRowGroupFlow { + fn class(&self) -> FlowClass { + FlowClass::TableRowGroup + } + + fn as_mut_table_rowgroup(&mut self) -> &mut TableRowGroupFlow { + self + } + + fn as_table_rowgroup(&self) -> &TableRowGroupFlow { + self + } + + fn as_mut_block(&mut self) -> &mut BlockFlow { + &mut self.block_flow + } + + fn as_block(&self) -> &BlockFlow { + &self.block_flow + } + + fn bubble_inline_sizes(&mut self) { + let _scope = layout_debug_scope!( + "table_rowgroup::bubble_inline_sizes {:x}", + self.block_flow.base.debug_id() + ); + // Proper calculation of intrinsic sizes in table layout requires access to the entire + // table, which we don't have yet. Defer to our parent. + } + + /// Recursively (top-down) determines the actual inline-size of child contexts and fragments. + /// When called on this context, the context has had its inline-size set by the parent context. + fn assign_inline_sizes(&mut self, layout_context: &LayoutContext) { + let _scope = layout_debug_scope!( + "table_rowgroup::assign_inline_sizes {:x}", + self.block_flow.base.debug_id() + ); + debug!( + "assign_inline_sizes({}): assigning inline_size for flow", + "table_rowgroup" + ); + + 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; + let (inline_start_content_edge, inline_end_content_edge) = (Au(0), Au(0)); + let content_inline_size = containing_block_inline_size; + + let border_collapse = self + .block_flow + .fragment + .style + .get_inherited_table() + .border_collapse; + let inline_size_computer = InternalTable; + inline_size_computer.compute_used_inline_size( + &mut self.block_flow, + shared_context, + containing_block_inline_size, + ); + + let collapsed_inline_direction_border_widths_for_table = + &self.collapsed_inline_direction_border_widths_for_table; + let mut collapsed_block_direction_border_widths_for_table = self + .collapsed_block_direction_border_widths_for_table + .iter() + .peekable(); + self.block_flow.propagate_assigned_inline_size_to_children( + shared_context, + inline_start_content_edge, + inline_end_content_edge, + content_inline_size, + |child_flow, + _child_index, + _content_inline_size, + _writing_mode, + _inline_start_margin_edge, + _inline_end_margin_edge| { + if border_collapse == border_collapse::T::Collapse { + let child_table_row = child_flow.as_mut_table_row(); + child_table_row.populate_collapsed_border_spacing( + collapsed_inline_direction_border_widths_for_table, + &mut collapsed_block_direction_border_widths_for_table, + ); + } + }, + ); + } + + fn assign_block_size(&mut self, lc: &LayoutContext) { + debug!("assign_block_size: assigning block_size for table_rowgroup"); + self.block_flow + .assign_block_size_for_table_like_flow(self.spacing.vertical(), lc); + } + + 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; + + // 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::POSITION_NEVER_CREATES_CONTAINING_BLOCK, + ); + } + + 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 dyn 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 dyn 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 TableRowGroupFlow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "TableRowGroupFlow: {:?}", self.block_flow) + } +} diff --git a/components/layout_2020/table_wrapper.rs b/components/layout_2020/table_wrapper.rs new file mode 100644 index 00000000000..65ecc07d3ab --- /dev/null +++ b/components/layout_2020/table_wrapper.rs @@ -0,0 +1,996 @@ +/* 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/. */ + +//! 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. + +use crate::block::{ + AbsoluteNonReplaced, BlockFlow, FloatNonReplaced, ISizeAndMarginsComputer, ISizeConstraintInput, +}; +use crate::block::{ISizeConstraintSolution, MarginsMayCollapseFlag}; +use crate::context::LayoutContext; +use crate::display_list::StackingContextCollectionState; +use crate::display_list::{DisplayListBuildState, StackingContextCollectionFlags}; +use crate::floats::FloatKind; +use crate::flow::{Flow, FlowClass, FlowFlags, ImmutableFlowUtils, OpaqueFlow}; +use crate::fragment::{Fragment, FragmentBorderBoxIterator, Overflow}; +use crate::model::MaybeAuto; +use crate::table::{ColumnComputedInlineSize, ColumnIntrinsicInlineSize}; +use app_units::Au; +use euclid::Point2D; +use gfx_traits::print_tree::PrintTree; +use std::cmp::{max, min}; +use std::fmt; +use std::ops::Add; +use style::computed_values::{position, table_layout}; +use style::context::SharedStyleContext; +use style::logical_geometry::{LogicalRect, LogicalSize}; +use style::properties::ComputedValues; +use style::values::computed::Size; +use style::values::CSSFloat; + +#[derive(Clone, Copy, Debug, Serialize)] +pub enum TableLayout { + Fixed, + Auto, +} + +#[allow(unsafe_code)] +unsafe impl crate::flow::HasBaseFlow for TableWrapperFlow {} + +/// A table wrapper flow based on a block formatting context. +#[derive(Serialize)] +#[repr(C)] +pub struct TableWrapperFlow { + pub block_flow: BlockFlow, + + /// Intrinsic column inline sizes according to INTRINSIC § 4.1 + pub column_intrinsic_inline_sizes: Vec<ColumnIntrinsicInlineSize>, + + /// Table-layout property + pub table_layout: TableLayout, +} + +impl TableWrapperFlow { + pub fn from_fragment(fragment: Fragment) -> TableWrapperFlow { + TableWrapperFlow::from_fragment_and_float_kind(fragment, None) + } + + pub fn from_fragment_and_float_kind( + fragment: Fragment, + float_kind: Option<FloatKind>, + ) -> TableWrapperFlow { + let mut block_flow = BlockFlow::from_fragment_and_float_kind(fragment, float_kind); + let table_layout = + if block_flow.fragment().style().get_table().table_layout == table_layout::T::Fixed { + TableLayout::Fixed + } else { + TableLayout::Auto + }; + TableWrapperFlow { + block_flow: block_flow, + column_intrinsic_inline_sizes: vec![], + table_layout: table_layout, + } + } + + fn border_padding_and_spacing(&mut self) -> (Au, Au) { + let (mut table_border_padding, mut spacing) = (Au(0), Au(0)); + for kid in self.block_flow.base.child_iter_mut() { + if kid.is_table() { + let kid_table = kid.as_table(); + spacing = kid_table.total_horizontal_spacing(); + table_border_padding = kid_table + .block_flow + .fragment + .border_padding + .inline_start_end(); + break; + } + } + (table_border_padding, spacing) + } + + // Instructs our first child, which is the table itself, to compute its border and padding. + // + // 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. + fn compute_border_and_padding_of_table(&mut self) { + let available_inline_size = self.block_flow.base.block_container_inline_size; + for kid in self.block_flow.base.child_iter_mut() { + if !kid.is_table() { + continue; + } + + let kid_table = kid.as_mut_table(); + let kid_block_flow = &mut kid_table.block_flow; + kid_block_flow + .fragment + .compute_border_and_padding(available_inline_size); + kid_block_flow + .fragment + .compute_block_direction_margins(available_inline_size); + kid_block_flow + .fragment + .compute_inline_direction_margins(available_inline_size); + return; + } + } + + /// Calculates table column sizes for automatic layout per INTRINSIC § 4.3. + fn calculate_table_column_sizes_for_automatic_layout( + &mut self, + intermediate_column_inline_sizes: &mut [IntermediateColumnInlineSize], + ) { + let available_inline_size = self.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_intrinsic_inline_sizes + .iter() + .map(|column_intrinsic_inline_size| { + let guess = AutoLayoutCandidateGuess::from_column_intrinsic_inline_size( + column_intrinsic_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 (intermediate_column_inline_size, guess) in intermediate_column_inline_sizes + .iter_mut() + .zip(guesses.iter()) + { + intermediate_column_inline_size.size = guess.calculate(selection); + intermediate_column_inline_size.percentage = 0.0; + total_used_inline_size = total_used_inline_size + intermediate_column_inline_size.size + } + + // 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 == + SelectedAutoLayoutCandidateGuess::UsePreferredGuessAndDistributeExcessInlineSize + { + let mut info = ExcessInlineSizeDistributionInfo::new(); + for column_intrinsic_inline_size in &self.column_intrinsic_inline_sizes { + info.update(column_intrinsic_inline_size) + } + + let mut total_distributed_excess_size = Au(0); + for (intermediate_column_inline_size, column_intrinsic_inline_size) in + intermediate_column_inline_sizes + .iter_mut() + .zip(self.column_intrinsic_inline_sizes.iter()) + { + info.distribute_excess_inline_size_to_column( + intermediate_column_inline_size, + column_intrinsic_inline_size, + excess_inline_size, + &mut total_distributed_excess_size, + ) + } + total_used_inline_size = available_inline_size + } + + self.set_inline_size(total_used_inline_size) + } + + fn available_inline_size(&mut self) -> Au { + let available_inline_size = self.block_flow.fragment.border_box.size.inline; + let (table_border_padding, spacing) = self.border_padding_and_spacing(); + + // 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 = match self.block_flow.fragment.style().content_inline_size() { + Size::Auto => { + self.block_flow + .get_shrink_to_fit_inline_size(available_inline_size) - + table_border_padding + }, + // FIXME(mttr): This fixes #4421 without breaking our current reftests, but I'm not + // completely sure this is "correct". + // + // That said, `available_inline_size` is, as far as I can tell, equal to the table's + // computed width property (W) and is used from this point forward in a way that seems + // to correspond with CSS 2.1 § 17.5.2.2 under "Column and caption widths influence the + // final table width as follows: …" + _ => available_inline_size, + }; + available_inline_size - spacing + } + + fn set_inline_size(&mut self, total_used_inline_size: Au) { + let (table_border_padding, spacing) = self.border_padding_and_spacing(); + self.block_flow.fragment.border_box.size.inline = + total_used_inline_size + table_border_padding + spacing; + self.block_flow.base.position.size.inline = total_used_inline_size + + table_border_padding + + spacing + + self.block_flow.fragment.margin.inline_start_end(); + + let writing_mode = self.block_flow.base.writing_mode; + let container_mode = self.block_flow.base.block_container_writing_mode; + + if writing_mode.is_bidi_ltr() != container_mode.is_bidi_ltr() { + // If our "start" direction is different from our parent flow, then `border_box.start.i` + // depends on `border_box.size.inline`. + self.block_flow.fragment.border_box.start.i = + self.block_flow.base.block_container_inline_size - + self.block_flow.fragment.margin.inline_end - + self.block_flow.fragment.border_box.size.inline; + } + } + + fn compute_used_inline_size( + &mut self, + shared_context: &SharedStyleContext, + parent_flow_inline_size: Au, + intermediate_column_inline_sizes: &[IntermediateColumnInlineSize], + ) { + let (border_padding, spacing) = self.border_padding_and_spacing(); + let minimum_width_of_all_columns = intermediate_column_inline_sizes.iter().fold( + border_padding + spacing, + |accumulator, intermediate_column_inline_sizes| { + accumulator + intermediate_column_inline_sizes.size + }, + ); + let preferred_width_of_all_columns = self.column_intrinsic_inline_sizes.iter().fold( + border_padding + spacing, + |accumulator, column_intrinsic_inline_sizes| { + accumulator + column_intrinsic_inline_sizes.preferred + }, + ); + + // Delegate to the appropriate inline size computer to find the constraint inputs and write + // the constraint solutions in. + if self.block_flow.base.flags.is_float() { + let inline_size_computer = FloatedTable { + minimum_width_of_all_columns: minimum_width_of_all_columns, + preferred_width_of_all_columns: preferred_width_of_all_columns, + table_border_padding: border_padding, + }; + let input = inline_size_computer.compute_inline_size_constraint_inputs( + &mut self.block_flow, + parent_flow_inline_size, + shared_context, + ); + + let solution = + inline_size_computer.solve_inline_size_constraints(&mut self.block_flow, &input); + inline_size_computer + .set_inline_size_constraint_solutions(&mut self.block_flow, solution); + inline_size_computer + .set_inline_position_of_flow_if_necessary(&mut self.block_flow, solution); + return; + } + + if !self + .block_flow + .base + .flags + .contains(FlowFlags::INLINE_POSITION_IS_STATIC) + { + let inline_size_computer = AbsoluteTable { + minimum_width_of_all_columns: minimum_width_of_all_columns, + preferred_width_of_all_columns: preferred_width_of_all_columns, + table_border_padding: border_padding, + }; + let input = inline_size_computer.compute_inline_size_constraint_inputs( + &mut self.block_flow, + parent_flow_inline_size, + shared_context, + ); + + let solution = + inline_size_computer.solve_inline_size_constraints(&mut self.block_flow, &input); + inline_size_computer + .set_inline_size_constraint_solutions(&mut self.block_flow, solution); + inline_size_computer + .set_inline_position_of_flow_if_necessary(&mut self.block_flow, solution); + return; + } + + let inline_size_computer = Table { + minimum_width_of_all_columns: minimum_width_of_all_columns, + preferred_width_of_all_columns: preferred_width_of_all_columns, + table_border_padding: border_padding, + }; + let input = inline_size_computer.compute_inline_size_constraint_inputs( + &mut self.block_flow, + parent_flow_inline_size, + shared_context, + ); + + let solution = + inline_size_computer.solve_inline_size_constraints(&mut self.block_flow, &input); + inline_size_computer.set_inline_size_constraint_solutions(&mut self.block_flow, solution); + inline_size_computer + .set_inline_position_of_flow_if_necessary(&mut self.block_flow, solution); + } +} + +impl Flow for TableWrapperFlow { + fn class(&self) -> FlowClass { + FlowClass::TableWrapper + } + + fn as_table_wrapper(&self) -> &TableWrapperFlow { + self + } + + fn as_mut_block(&mut self) -> &mut BlockFlow { + &mut self.block_flow + } + + fn as_block(&self) -> &BlockFlow { + &self.block_flow + } + + fn mark_as_root(&mut self) { + self.block_flow.mark_as_root(); + } + + fn bubble_inline_sizes(&mut self) { + // Get the intrinsic column inline-sizes info from the table flow. + for kid in self.block_flow.base.child_iter_mut() { + debug_assert!(kid.is_table_caption() || kid.is_table()); + if kid.is_table() { + let table = kid.as_table(); + self.column_intrinsic_inline_sizes = table.column_intrinsic_inline_sizes.clone(); + } + } + + self.block_flow.bubble_inline_sizes(); + } + + fn assign_inline_sizes(&mut self, layout_context: &LayoutContext) { + debug!( + "assign_inline_sizes({}): assigning inline_size for flow", + if self.block_flow.base.flags.is_float() { + "floated table_wrapper" + } else { + "table_wrapper" + } + ); + + let shared_context = layout_context.shared_context(); + self.block_flow + .initialize_container_size_for_root(shared_context); + + let mut intermediate_column_inline_sizes = self + .column_intrinsic_inline_sizes + .iter() + .map( + |column_intrinsic_inline_size| IntermediateColumnInlineSize { + size: column_intrinsic_inline_size.minimum_length, + percentage: column_intrinsic_inline_size.percentage, + }, + ) + .collect::<Vec<_>>(); + + // Our inline-size was set to the inline-size of the containing block by the flow's parent. + // Now compute the real value. + let containing_block_inline_size = self.block_flow.base.block_container_inline_size; + if self.block_flow.base.flags.is_float() { + self.block_flow + .float + .as_mut() + .unwrap() + .containing_inline_size = containing_block_inline_size; + } + + // This has to be done before computing our inline size because `compute_used_inline_size` + // internally consults the border and padding of the table. + self.compute_border_and_padding_of_table(); + + self.compute_used_inline_size( + shared_context, + containing_block_inline_size, + &intermediate_column_inline_sizes, + ); + + match self.table_layout { + TableLayout::Auto => self.calculate_table_column_sizes_for_automatic_layout( + &mut intermediate_column_inline_sizes, + ), + TableLayout::Fixed => {}, + } + + 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; + let inline_end_content_edge = self.block_flow.fragment.border_padding.inline_end + + self.block_flow.fragment.margin.inline_end; + + // In case of fixed layout, column inline-sizes are calculated in table flow. + let assigned_column_inline_sizes = match self.table_layout { + TableLayout::Fixed => None, + TableLayout::Auto => Some( + intermediate_column_inline_sizes + .iter() + .map(|sizes| ColumnComputedInlineSize { size: sizes.size }) + .collect::<Vec<_>>(), + ), + }; + + match assigned_column_inline_sizes { + None => self.block_flow.propagate_assigned_inline_size_to_children( + shared_context, + inline_start_content_edge, + inline_end_content_edge, + content_inline_size, + |_, _, _, _, _, _| {}, + ), + Some(ref assigned_column_inline_sizes) => { + self.block_flow.propagate_assigned_inline_size_to_children( + shared_context, + inline_start_content_edge, + inline_end_content_edge, + content_inline_size, + |child_flow, _, _, _, _, _| { + if child_flow.class() == FlowClass::Table { + child_flow.as_mut_table().column_computed_inline_sizes = + assigned_column_inline_sizes.to_vec(); + } + }, + ) + }, + } + } + + fn assign_block_size(&mut self, layout_context: &LayoutContext) { + debug!("assign_block_size: assigning block_size for table_wrapper"); + let remaining = self.block_flow.assign_block_size_block_base( + layout_context, + None, + MarginsMayCollapseFlag::MarginsMayNotCollapse, + ); + debug_assert!(remaining.is_none()); + } + + fn compute_stacking_relative_position(&mut self, layout_context: &LayoutContext) { + self.block_flow + .compute_stacking_relative_position(layout_context) + } + + fn place_float_if_applicable<'a>(&mut self) { + self.block_flow.place_float_if_applicable() + } + + fn assign_block_size_for_inorder_child_if_necessary( + &mut self, + layout_context: &LayoutContext, + parent_thread_id: u8, + content_box: LogicalRect<Au>, + ) -> bool { + self.block_flow + .assign_block_size_for_inorder_child_if_necessary( + layout_context, + parent_thread_id, + content_box, + ) + } + + 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 generated_containing_block_size(&self, flow: OpaqueFlow) -> LogicalSize<Au> { + self.block_flow.generated_containing_block_size(flow) + } + + fn build_display_list(&mut self, state: &mut DisplayListBuildState) { + self.block_flow.build_display_list(state); + } + + fn collect_stacking_contexts(&mut self, state: &mut StackingContextCollectionState) { + self.block_flow.collect_stacking_contexts_for_block( + state, + StackingContextCollectionFlags::POSITION_NEVER_CREATES_CONTAINING_BLOCK | + StackingContextCollectionFlags::NEVER_CREATES_CLIP_SCROLL_NODE, + ); + } + + 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 iterate_through_fragment_border_boxes( + &self, + iterator: &mut dyn 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 dyn 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); + } + + fn positioning(&self) -> position::T { + self.block_flow.positioning() + } +} + +impl fmt::Debug for TableWrapperFlow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.block_flow.base.flags.is_float() { + write!(f, "TableWrapperFlow(Float): {:?}", self.block_flow) + } else { + write!(f, "TableWrapperFlow: {:?}", self.block_flow) + } + } +} + +/// 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_intrinsic_inline_size( + column_intrinsic_inline_size: &ColumnIntrinsicInlineSize, + assignable_inline_size: Au, + ) -> AutoLayoutCandidateGuess { + let minimum_percentage_guess = max( + assignable_inline_size.scale_by(column_intrinsic_inline_size.percentage), + column_intrinsic_inline_size.minimum_length, + ); + AutoLayoutCandidateGuess { + minimum_guess: column_intrinsic_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_intrinsic_inline_size.percentage > 0.0 { + minimum_percentage_guess + } else if column_intrinsic_inline_size.constrained { + column_intrinsic_inline_size.preferred + } else { + column_intrinsic_inline_size.minimum_length + }, + preferred_guess: if column_intrinsic_inline_size.percentage > 0.0 { + minimum_percentage_guess + } else { + column_intrinsic_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 { + SelectedAutoLayoutCandidateGuess::UseMinimumGuess => self.minimum_guess, + SelectedAutoLayoutCandidateGuess:: + InterpolateBetweenMinimumGuessAndMinimumPercentageGuess(weight) => { + interp(self.minimum_guess, self.minimum_percentage_guess, weight) + } + SelectedAutoLayoutCandidateGuess:: + InterpolateBetweenMinimumPercentageGuessAndMinimumSpecifiedGuess(weight) => { + interp(self.minimum_percentage_guess, self.minimum_specified_guess, weight) + } + SelectedAutoLayoutCandidateGuess:: + InterpolateBetweenMinimumSpecifiedGuessAndPreferredGuess(weight) => { + interp(self.minimum_specified_guess, self.preferred_guess, weight) + } + SelectedAutoLayoutCandidateGuess::UsePreferredGuessAndDistributeExcessInlineSize => { + self.preferred_guess + } + } + } +} + +impl<'a> Add for &'a AutoLayoutCandidateGuess { + type Output = 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. +#[derive(Clone, Copy, Debug, PartialEq)] +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 { + SelectedAutoLayoutCandidateGuess::UseMinimumGuess + } else if assignable_inline_size < guess.minimum_percentage_guess { + let weight = weight( + guess.minimum_guess, + assignable_inline_size, + guess.minimum_percentage_guess, + ); + SelectedAutoLayoutCandidateGuess::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, + ); + SelectedAutoLayoutCandidateGuess::InterpolateBetweenMinimumPercentageGuessAndMinimumSpecifiedGuess(weight) + } else if assignable_inline_size < guess.preferred_guess { + let weight = weight( + guess.minimum_specified_guess, + assignable_inline_size, + guess.preferred_guess, + ); + SelectedAutoLayoutCandidateGuess::InterpolateBetweenMinimumSpecifiedGuessAndPreferredGuess(weight) + } else { + SelectedAutoLayoutCandidateGuess::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_f32_px() / (high - low).to_f32_px() +} + +/// 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_intrinsic_inline_size: &ColumnIntrinsicInlineSize) { + if !column_intrinsic_inline_size.constrained && + column_intrinsic_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_intrinsic_inline_size.preferred; + self.count_of_nonconstrained_columns_with_no_percentage += 1 + } + if column_intrinsic_inline_size.constrained && + column_intrinsic_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_intrinsic_inline_size.preferred + } + self.total_percentage += column_intrinsic_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, + intermediate_column_inline_size: &mut IntermediateColumnInlineSize, + column_intrinsic_inline_size: &ColumnIntrinsicInlineSize, + 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) { + // FIXME(spec, pcwalton): Gecko and WebKit do *something* here when there are + // nonconstrained columns with no percentage *and* no preferred width. What do they + // do? + if !column_intrinsic_inline_size.constrained && + column_intrinsic_inline_size.percentage == 0.0 + { + column_intrinsic_inline_size.preferred.to_f32_px() / + self.preferred_inline_size_of_nonconstrained_columns_with_no_percentage + .to_f32_px() + } else { + 0.0 + } + } 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_intrinsic_inline_size.preferred.to_f32_px() / + self.preferred_inline_size_of_constrained_columns_with_no_percentage + .to_f32_px() + } else if self.total_percentage > 0.0 { + column_intrinsic_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; + intermediate_column_inline_size.size = + intermediate_column_inline_size.size + amount_to_distribute + } +} + +/// An intermediate column size assignment. +struct IntermediateColumnInlineSize { + size: Au, + percentage: f32, +} + +/// Returns the computed inline size of the table wrapper represented by `block`. +/// +/// `table_border_padding` is the sum of the sizes of all border and padding in the inline +/// direction of the table contained within this table wrapper. +fn initial_computed_inline_size( + block: &mut BlockFlow, + containing_block_inline_size: Au, + minimum_width_of_all_columns: Au, + preferred_width_of_all_columns: Au, + table_border_padding: Au, +) -> MaybeAuto { + match block.fragment.style.content_inline_size() { + Size::Auto => { + if preferred_width_of_all_columns + table_border_padding <= containing_block_inline_size + { + MaybeAuto::Specified(preferred_width_of_all_columns + table_border_padding) + } else if minimum_width_of_all_columns > containing_block_inline_size { + MaybeAuto::Specified(minimum_width_of_all_columns) + } else { + MaybeAuto::Auto + } + }, + Size::LengthPercentage(ref lp) => { + let used = lp.to_used_value(containing_block_inline_size); + MaybeAuto::Specified(max( + used - table_border_padding, + minimum_width_of_all_columns, + )) + }, + } +} + +struct Table { + minimum_width_of_all_columns: Au, + preferred_width_of_all_columns: Au, + table_border_padding: Au, +} + +impl ISizeAndMarginsComputer for Table { + fn compute_border_and_padding(&self, block: &mut BlockFlow, containing_block_inline_size: Au) { + block + .fragment + .compute_border_and_padding(containing_block_inline_size) + } + + fn initial_computed_inline_size( + &self, + block: &mut BlockFlow, + parent_flow_inline_size: Au, + shared_context: &SharedStyleContext, + ) -> MaybeAuto { + let containing_block_inline_size = + self.containing_block_inline_size(block, parent_flow_inline_size, shared_context); + initial_computed_inline_size( + block, + containing_block_inline_size, + self.minimum_width_of_all_columns, + self.preferred_width_of_all_columns, + self.table_border_padding, + ) + } + + fn solve_inline_size_constraints( + &self, + block: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + self.solve_block_inline_size_constraints(block, input) + } +} + +struct FloatedTable { + minimum_width_of_all_columns: Au, + preferred_width_of_all_columns: Au, + table_border_padding: Au, +} + +impl ISizeAndMarginsComputer for FloatedTable { + fn compute_border_and_padding(&self, block: &mut BlockFlow, containing_block_inline_size: Au) { + block + .fragment + .compute_border_and_padding(containing_block_inline_size) + } + + fn initial_computed_inline_size( + &self, + block: &mut BlockFlow, + parent_flow_inline_size: Au, + shared_context: &SharedStyleContext, + ) -> MaybeAuto { + let containing_block_inline_size = + self.containing_block_inline_size(block, parent_flow_inline_size, shared_context); + initial_computed_inline_size( + block, + containing_block_inline_size, + self.minimum_width_of_all_columns, + self.preferred_width_of_all_columns, + self.table_border_padding, + ) + } + + fn solve_inline_size_constraints( + &self, + block: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + FloatNonReplaced.solve_inline_size_constraints(block, input) + } +} + +struct AbsoluteTable { + minimum_width_of_all_columns: Au, + preferred_width_of_all_columns: Au, + table_border_padding: Au, +} + +impl ISizeAndMarginsComputer for AbsoluteTable { + fn compute_border_and_padding(&self, block: &mut BlockFlow, containing_block_inline_size: Au) { + block + .fragment + .compute_border_and_padding(containing_block_inline_size) + } + + fn initial_computed_inline_size( + &self, + block: &mut BlockFlow, + parent_flow_inline_size: Au, + shared_context: &SharedStyleContext, + ) -> MaybeAuto { + let containing_block_inline_size = + self.containing_block_inline_size(block, parent_flow_inline_size, shared_context); + initial_computed_inline_size( + block, + containing_block_inline_size, + self.minimum_width_of_all_columns, + self.preferred_width_of_all_columns, + self.table_border_padding, + ) + } + + fn containing_block_inline_size( + &self, + block: &mut BlockFlow, + parent_flow_inline_size: Au, + shared_context: &SharedStyleContext, + ) -> Au { + AbsoluteNonReplaced.containing_block_inline_size( + block, + parent_flow_inline_size, + shared_context, + ) + } + + fn solve_inline_size_constraints( + &self, + block: &mut BlockFlow, + input: &ISizeConstraintInput, + ) -> ISizeConstraintSolution { + AbsoluteNonReplaced.solve_inline_size_constraints(block, input) + } + + fn set_inline_position_of_flow_if_necessary( + &self, + block: &mut BlockFlow, + solution: ISizeConstraintSolution, + ) { + AbsoluteNonReplaced.set_inline_position_of_flow_if_necessary(block, solution); + } +} diff --git a/components/layout_2020/tests/size_of.rs b/components/layout_2020/tests/size_of.rs new file mode 100644 index 00000000000..e2cec625075 --- /dev/null +++ b/components/layout_2020/tests/size_of.rs @@ -0,0 +1,18 @@ +/* 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/. */ + +#![cfg(target_pointer_width = "64")] + +#[macro_use] +extern crate size_of_test; + +use layout::Fragment; +use layout::SpecificFragmentInfo; + +size_of_test!(test_size_of_fragment, Fragment, 176); +size_of_test!( + test_size_of_specific_fragment_info, + SpecificFragmentInfo, + 24 +); diff --git a/components/layout_2020/text.rs b/components/layout_2020/text.rs new file mode 100644 index 00000000000..199bd7e07f2 --- /dev/null +++ b/components/layout_2020/text.rs @@ -0,0 +1,798 @@ +/* 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/. */ + +//! Text layout. + +use crate::context::LayoutFontContext; +use crate::fragment::{Fragment, ScannedTextFlags}; +use crate::fragment::{ScannedTextFragmentInfo, SpecificFragmentInfo, UnscannedTextFragmentInfo}; +use crate::inline::{InlineFragmentNodeFlags, InlineFragments}; +use crate::linked_list::split_off_head; +use app_units::Au; +use gfx::font::{FontMetrics, FontRef, RunMetrics, ShapingFlags, ShapingOptions}; +use gfx::text::glyph::ByteIndex; +use gfx::text::text_run::TextRun; +use gfx::text::util::{self, CompressionMode}; +use range::Range; +use servo_atoms::Atom; +use std::borrow::ToOwned; +use std::collections::LinkedList; +use std::mem; +use std::sync::Arc; +use style::computed_values::text_rendering::T as TextRendering; +use style::computed_values::white_space::T as WhiteSpace; +use style::computed_values::word_break::T as WordBreak; +use style::logical_geometry::{LogicalSize, WritingMode}; +use style::properties::style_structs::Font as FontStyleStruct; +use style::properties::ComputedValues; +use style::values::generics::text::LineHeight; +use style::values::specified::text::{TextTransform, TextTransformCase}; +use unicode_bidi as bidi; +use unicode_script::{get_script, Script}; +use xi_unicode::LineBreakLeafIter; + +/// Returns the concatenated text of a list of unscanned text fragments. +fn text(fragments: &LinkedList<Fragment>) -> String { + // FIXME: Some of this work is later duplicated in split_first_fragment_at_newline_if_necessary + // and transform_text. This code should be refactored so that the all the scanning for + // newlines is done in a single pass. + + let mut text = String::new(); + + for fragment in fragments { + if let SpecificFragmentInfo::UnscannedText(ref info) = fragment.specific { + if fragment.white_space().preserve_newlines() { + text.push_str(&info.text); + } else { + text.push_str(&info.text.replace("\n", " ")); + } + } + } + text +} + +/// A stack-allocated object for scanning an inline flow into `TextRun`-containing `TextFragment`s. +pub struct TextRunScanner { + pub clump: LinkedList<Fragment>, +} + +impl TextRunScanner { + pub fn new() -> TextRunScanner { + TextRunScanner { + clump: LinkedList::new(), + } + } + + pub fn scan_for_runs( + &mut self, + font_context: &mut LayoutFontContext, + mut fragments: LinkedList<Fragment>, + ) -> InlineFragments { + debug!( + "TextRunScanner: scanning {} fragments for text runs...", + fragments.len() + ); + debug_assert!(!fragments.is_empty()); + + // Calculate bidi embedding levels, so we can split bidirectional fragments for reordering. + let text = text(&fragments); + let para_level = fragments + .front() + .unwrap() + .style + .writing_mode + .to_bidi_level(); + let bidi_info = bidi::BidiInfo::new(&text, Some(para_level)); + + // Optimization: If all the text is LTR, don't bother splitting on bidi levels. + let bidi_levels = if bidi_info.has_rtl() { + Some(&bidi_info.levels[..]) + } else { + None + }; + + // FIXME(pcwalton): We want to be sure not to allocate multiple times, since this is a + // performance-critical spot, but this may overestimate and allocate too much memory. + let mut new_fragments = Vec::with_capacity(fragments.len()); + let mut last_whitespace = false; + let mut paragraph_bytes_processed = 0; + + // The first time we process a text run we will set this + // linebreaker. There is no way for the linebreaker to start + // with an empty state; you must give it its first input immediately. + // + // This linebreaker is shared across text runs, so we can know if + // there is a break at the beginning of a text run or clump, e.g. + // in the case of FooBar<span>Baz</span> + let mut linebreaker = None; + + while !fragments.is_empty() { + // Create a clump. + split_first_fragment_at_newline_if_necessary(&mut fragments); + self.clump.append(&mut split_off_head(&mut fragments)); + while !fragments.is_empty() && + self.clump + .back() + .unwrap() + .can_merge_with_fragment(fragments.front().unwrap()) + { + split_first_fragment_at_newline_if_necessary(&mut fragments); + self.clump.append(&mut split_off_head(&mut fragments)); + } + + // Flush that clump to the list of fragments we're building up. + last_whitespace = self.flush_clump_to_list( + font_context, + &mut new_fragments, + &mut paragraph_bytes_processed, + bidi_levels, + last_whitespace, + &mut linebreaker, + ); + } + + debug!("TextRunScanner: complete."); + InlineFragments { + fragments: new_fragments, + } + } + + /// A "clump" is a range of inline flow leaves that can be merged together into a single + /// fragment. Adjacent text with the same style can be merged, and nothing else can. + /// + /// The flow keeps track of the fragments contained by all non-leaf DOM nodes. This is necessary + /// for correct painting order. Since we compress several leaf fragments here, the mapping must + /// be adjusted. + fn flush_clump_to_list( + &mut self, + mut font_context: &mut LayoutFontContext, + out_fragments: &mut Vec<Fragment>, + paragraph_bytes_processed: &mut usize, + bidi_levels: Option<&[bidi::Level]>, + mut last_whitespace: bool, + linebreaker: &mut Option<LineBreakLeafIter>, + ) -> bool { + debug!( + "TextRunScanner: flushing {} fragments in range", + self.clump.len() + ); + + debug_assert!(!self.clump.is_empty()); + match self.clump.front().unwrap().specific { + SpecificFragmentInfo::UnscannedText(_) => {}, + _ => { + debug_assert!( + self.clump.len() == 1, + "WAT: can't coalesce non-text nodes in flush_clump_to_list()!" + ); + out_fragments.push(self.clump.pop_front().unwrap()); + return false; + }, + } + + // Concatenate all of the transformed strings together, saving the new character indices. + let mut mappings: Vec<RunMapping> = Vec::new(); + let runs = { + let font_group; + let compression; + let text_transform; + let letter_spacing; + let word_spacing; + let text_rendering; + let word_break; + { + let in_fragment = self.clump.front().unwrap(); + let font_style = in_fragment.style().clone_font(); + let inherited_text_style = in_fragment.style().get_inherited_text(); + font_group = font_context.font_group(font_style); + compression = match in_fragment.white_space() { + WhiteSpace::Normal | WhiteSpace::Nowrap => { + CompressionMode::CompressWhitespaceNewline + }, + WhiteSpace::Pre | WhiteSpace::PreWrap => CompressionMode::CompressNone, + WhiteSpace::PreLine => CompressionMode::CompressWhitespace, + }; + text_transform = inherited_text_style.text_transform; + letter_spacing = inherited_text_style.letter_spacing; + word_spacing = inherited_text_style.word_spacing.to_hash_key(); + text_rendering = inherited_text_style.text_rendering; + word_break = inherited_text_style.word_break; + } + + // First, transform/compress text of all the nodes. + let (mut run_info_list, mut run_info) = (Vec::new(), RunInfo::new()); + let mut insertion_point = None; + + for (fragment_index, in_fragment) in self.clump.iter().enumerate() { + debug!(" flushing {:?}", in_fragment); + let mut mapping = RunMapping::new(&run_info_list[..], fragment_index); + let text; + let selection; + match in_fragment.specific { + SpecificFragmentInfo::UnscannedText(ref text_fragment_info) => { + text = &text_fragment_info.text; + selection = text_fragment_info.selection; + }, + _ => panic!("Expected an unscanned text fragment!"), + }; + insertion_point = match selection { + Some(range) if range.is_empty() => { + // `range` is the range within the current fragment. To get the range + // within the text run, offset it by the length of the preceding fragments. + Some(range.begin() + ByteIndex(run_info.text.len() as isize)) + }, + _ => None, + }; + + let (mut start_position, mut end_position) = (0, 0); + for (byte_index, character) in text.char_indices() { + if !character.is_control() { + let font = font_group + .borrow_mut() + .find_by_codepoint(&mut font_context, character); + + let bidi_level = match bidi_levels { + Some(levels) => levels[*paragraph_bytes_processed], + None => bidi::Level::ltr(), + }; + + // Break the run if the new character has a different explicit script than the + // previous characters. + // + // TODO: Special handling of paired punctuation characters. + // http://www.unicode.org/reports/tr24/#Common + let script = get_script(character); + let compatible_script = is_compatible(script, run_info.script); + if compatible_script && !is_specific(run_info.script) && is_specific(script) + { + run_info.script = script; + } + + let selected = match selection { + Some(range) => range.contains(ByteIndex(byte_index as isize)), + None => false, + }; + + // Now, if necessary, flush the mapping we were building up. + let flush_run = !run_info.has_font(&font) || + run_info.bidi_level != bidi_level || + !compatible_script; + let new_mapping_needed = flush_run || mapping.selected != selected; + + if new_mapping_needed { + // We ignore empty mappings at the very start of a fragment. + // The run info values are uninitialized at this point so + // flushing an empty mapping is pointless. + if end_position > 0 { + mapping.flush( + &mut mappings, + &mut run_info, + &**text, + compression, + text_transform, + &mut last_whitespace, + &mut start_position, + end_position, + ); + } + if run_info.text.len() > 0 { + if flush_run { + run_info.flush(&mut run_info_list, &mut insertion_point); + run_info = RunInfo::new(); + } + mapping = RunMapping::new(&run_info_list[..], fragment_index); + } + run_info.font = font; + run_info.bidi_level = bidi_level; + run_info.script = script; + mapping.selected = selected; + } + } + + // Consume this character. + end_position += character.len_utf8(); + *paragraph_bytes_processed += character.len_utf8(); + } + + // Flush the last mapping we created for this fragment to the list. + mapping.flush( + &mut mappings, + &mut run_info, + &**text, + compression, + text_transform, + &mut last_whitespace, + &mut start_position, + end_position, + ); + } + + // Push the final run info. + run_info.flush(&mut run_info_list, &mut insertion_point); + + // Per CSS 2.1 § 16.4, "when the resultant space between two characters is not the same + // as the default space, user agents should not use ligatures." This ensures that, for + // example, `finally` with a wide `letter-spacing` renders as `f i n a l l y` and not + // `fi n a l l y`. + let mut flags = ShapingFlags::empty(); + if letter_spacing.0.px() != 0. { + flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG); + } + if text_rendering == TextRendering::Optimizespeed { + flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG); + flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG) + } + if word_break == WordBreak::KeepAll { + flags.insert(ShapingFlags::KEEP_ALL_FLAG); + } + let options = ShapingOptions { + letter_spacing: if letter_spacing.0.px() == 0. { + None + } else { + Some(Au::from(letter_spacing.0)) + }, + word_spacing, + script: Script::Common, + flags: flags, + }; + + let mut result = Vec::with_capacity(run_info_list.len()); + for run_info in run_info_list { + let mut options = options; + options.script = run_info.script; + if run_info.bidi_level.is_rtl() { + options.flags.insert(ShapingFlags::RTL_FLAG); + } + + // If no font is found (including fallbacks), there's no way we can render. + let font = run_info + .font + .or_else(|| font_group.borrow_mut().first(&mut font_context)) + .expect("No font found for text run!"); + + let (run, break_at_zero) = TextRun::new( + &mut *font.borrow_mut(), + run_info.text, + &options, + run_info.bidi_level, + linebreaker, + ); + result.push(( + ScannedTextRun { + run: Arc::new(run), + insertion_point: run_info.insertion_point, + }, + break_at_zero, + )) + } + result + }; + + // Make new fragments with the runs and adjusted text indices. + debug!("TextRunScanner: pushing {} fragment(s)", self.clump.len()); + let mut mappings = mappings.into_iter().peekable(); + let mut prev_fragments_to_meld = Vec::new(); + + for (logical_offset, old_fragment) in mem::replace(&mut self.clump, LinkedList::new()) + .into_iter() + .enumerate() + { + let mut is_first_mapping_of_this_old_fragment = true; + loop { + match mappings.peek() { + Some(mapping) if mapping.old_fragment_index == logical_offset => {}, + Some(_) | None => { + if is_first_mapping_of_this_old_fragment { + // There were no mappings for this unscanned fragment. Transfer its + // flags to the previous/next sibling elements instead. + if let Some(ref mut last_fragment) = out_fragments.last_mut() { + last_fragment.meld_with_next_inline_fragment(&old_fragment); + } + prev_fragments_to_meld.push(old_fragment); + } + break; + }, + }; + let mapping = mappings.next().unwrap(); + let (scanned_run, break_at_zero) = runs[mapping.text_run_index].clone(); + + let mut byte_range = Range::new( + ByteIndex(mapping.byte_range.begin() as isize), + ByteIndex(mapping.byte_range.length() as isize), + ); + + let mut flags = ScannedTextFlags::empty(); + if !break_at_zero && mapping.byte_range.begin() == 0 { + // If this is the first segment of the text run, + // and the text run doesn't break at zero, suppress line breaks + flags.insert(ScannedTextFlags::SUPPRESS_LINE_BREAK_BEFORE) + } + let text_size = old_fragment.border_box.size; + + let requires_line_break_afterward_if_wrapping_on_newlines = scanned_run.run.text + [mapping.byte_range.begin()..mapping.byte_range.end()] + .ends_with('\n'); + + if requires_line_break_afterward_if_wrapping_on_newlines { + byte_range.extend_by(ByteIndex(-1)); // Trim the '\n' + flags.insert( + ScannedTextFlags::REQUIRES_LINE_BREAK_AFTERWARD_IF_WRAPPING_ON_NEWLINES, + ); + } + + if mapping.selected { + flags.insert(ScannedTextFlags::SELECTED); + } + + let insertion_point = + if mapping.contains_insertion_point(scanned_run.insertion_point) { + scanned_run.insertion_point + } else { + None + }; + + let mut new_text_fragment_info = Box::new(ScannedTextFragmentInfo::new( + scanned_run.run, + byte_range, + text_size, + insertion_point, + flags, + )); + + let new_metrics = new_text_fragment_info.run.metrics_for_range(&byte_range); + let writing_mode = old_fragment.style.writing_mode; + let bounding_box_size = bounding_box_for_run_metrics(&new_metrics, writing_mode); + new_text_fragment_info.content_size = bounding_box_size; + + let mut new_fragment = old_fragment.transform( + bounding_box_size, + SpecificFragmentInfo::ScannedText(new_text_fragment_info), + ); + + let is_last_mapping_of_this_old_fragment = match mappings.peek() { + Some(mapping) if mapping.old_fragment_index == logical_offset => false, + _ => true, + }; + + if let Some(ref mut context) = new_fragment.inline_context { + for node in &mut context.nodes { + if !is_last_mapping_of_this_old_fragment { + node.flags + .remove(InlineFragmentNodeFlags::LAST_FRAGMENT_OF_ELEMENT); + } + if !is_first_mapping_of_this_old_fragment { + node.flags + .remove(InlineFragmentNodeFlags::FIRST_FRAGMENT_OF_ELEMENT); + } + } + } + + for prev_fragment in prev_fragments_to_meld.drain(..) { + new_fragment.meld_with_prev_inline_fragment(&prev_fragment); + } + + is_first_mapping_of_this_old_fragment = false; + out_fragments.push(new_fragment) + } + } + + last_whitespace + } +} + +#[inline] +fn bounding_box_for_run_metrics( + metrics: &RunMetrics, + writing_mode: WritingMode, +) -> LogicalSize<Au> { + // TODO: When the text-orientation property is supported, the block and inline directions may + // be swapped for horizontal glyphs in vertical lines. + LogicalSize::new( + writing_mode, + metrics.bounding_box.size.width, + metrics.bounding_box.size.height, + ) +} + +/// Returns the metrics of the font represented by the given `FontStyleStruct`. +/// +/// `#[inline]` because often the caller only needs a few fields from the font metrics. +/// +/// # Panics +/// +/// Panics if no font can be found for the given font style. +#[inline] +pub fn font_metrics_for_style( + mut font_context: &mut LayoutFontContext, + style: crate::ServoArc<FontStyleStruct>, +) -> FontMetrics { + let font_group = font_context.font_group(style); + let font = font_group.borrow_mut().first(&mut font_context); + let font = font.as_ref().unwrap().borrow(); + + font.metrics.clone() +} + +/// Returns the line block-size needed by the given computed style and font size. +pub fn line_height_from_style(style: &ComputedValues, metrics: &FontMetrics) -> Au { + let font_size = style.get_font().font_size.size(); + match style.get_inherited_text().line_height { + LineHeight::Normal => Au::from(metrics.line_gap), + LineHeight::Number(l) => font_size.scale_by(l.0), + LineHeight::Length(l) => Au::from(l), + } +} + +fn split_first_fragment_at_newline_if_necessary(fragments: &mut LinkedList<Fragment>) { + if fragments.is_empty() { + return; + } + + let new_fragment = { + let first_fragment = fragments.front_mut().unwrap(); + let string_before; + let selection_before; + { + if !first_fragment.white_space().preserve_newlines() { + return; + } + + let unscanned_text_fragment_info = match first_fragment.specific { + SpecificFragmentInfo::UnscannedText(ref mut unscanned_text_fragment_info) => { + unscanned_text_fragment_info + }, + _ => return, + }; + + let position = match unscanned_text_fragment_info.text.find('\n') { + Some(position) if position < unscanned_text_fragment_info.text.len() - 1 => { + position + }, + Some(_) | None => return, + }; + + string_before = unscanned_text_fragment_info.text[..(position + 1)].to_owned(); + unscanned_text_fragment_info.text = unscanned_text_fragment_info.text[(position + 1)..] + .to_owned() + .into_boxed_str(); + let offset = ByteIndex(string_before.len() as isize); + match unscanned_text_fragment_info.selection { + Some(ref mut selection) if selection.begin() >= offset => { + // Selection is entirely in the second fragment. + selection_before = None; + selection.shift_by(-offset); + }, + Some(ref mut selection) if selection.end() > offset => { + // Selection is split across two fragments. + selection_before = Some(Range::new(selection.begin(), offset)); + *selection = Range::new(ByteIndex(0), selection.end() - offset); + }, + _ => { + // Selection is entirely in the first fragment. + selection_before = unscanned_text_fragment_info.selection; + unscanned_text_fragment_info.selection = None; + }, + }; + } + first_fragment.transform( + first_fragment.border_box.size, + SpecificFragmentInfo::UnscannedText(Box::new(UnscannedTextFragmentInfo::new( + string_before.into_boxed_str(), + selection_before, + ))), + ) + }; + + fragments.push_front(new_fragment); +} + +/// Information about a text run that we're about to create. This is used in `scan_for_runs`. +struct RunInfo { + /// The text that will go in this text run. + text: String, + /// The insertion point in this text run, if applicable. + insertion_point: Option<ByteIndex>, + /// The font that the text should be rendered with. + font: Option<FontRef>, + /// The bidirection embedding level of this text run. + bidi_level: bidi::Level, + /// The Unicode script property of this text run. + script: Script, +} + +impl RunInfo { + fn new() -> RunInfo { + RunInfo { + text: String::new(), + insertion_point: None, + font: None, + bidi_level: bidi::Level::ltr(), + script: Script::Common, + } + } + + /// Finish processing this RunInfo and add it to the "done" list. + /// + /// * `insertion_point`: The position of the insertion point, in characters relative to the start + /// of this text run. + fn flush(mut self, list: &mut Vec<RunInfo>, insertion_point: &mut Option<ByteIndex>) { + if let Some(idx) = *insertion_point { + let char_len = ByteIndex(self.text.len() as isize); + if idx <= char_len { + // The insertion point is in this text run. + self.insertion_point = insertion_point.take() + } else { + // Continue looking for the insertion point in the next text run. + *insertion_point = Some(idx - char_len) + } + } + list.push(self); + } + + fn has_font(&self, font: &Option<FontRef>) -> bool { + fn identifier(font: &Option<FontRef>) -> Option<Atom> { + font.as_ref().map(|f| f.borrow().identifier()) + } + + identifier(&self.font) == identifier(font) + } +} + +/// A mapping from a portion of an unscanned text fragment to the text run we're going to create +/// for it. +#[derive(Clone, Copy, Debug)] +struct RunMapping { + /// The range of byte indices within the text fragment. + byte_range: Range<usize>, + /// The index of the unscanned text fragment that this mapping corresponds to. + old_fragment_index: usize, + /// The index of the text run we're going to create. + text_run_index: usize, + /// Is the text in this fragment selected? + selected: bool, +} + +impl RunMapping { + /// Given the current set of text runs, creates a run mapping for the next fragment. + /// `run_info_list` describes the set of runs we've seen already. + fn new(run_info_list: &[RunInfo], fragment_index: usize) -> RunMapping { + RunMapping { + byte_range: Range::new(0, 0), + old_fragment_index: fragment_index, + text_run_index: run_info_list.len(), + selected: false, + } + } + + /// Flushes this run mapping to the list. `run_info` describes the text run that we're + /// currently working on. `text` refers to the text of this fragment. + fn flush( + mut self, + mappings: &mut Vec<RunMapping>, + run_info: &mut RunInfo, + text: &str, + compression: CompressionMode, + text_transform: TextTransform, + last_whitespace: &mut bool, + start_position: &mut usize, + end_position: usize, + ) { + let was_empty = *start_position == end_position; + let old_byte_length = run_info.text.len(); + *last_whitespace = util::transform_text( + &text[(*start_position)..end_position], + compression, + *last_whitespace, + &mut run_info.text, + ); + + // Account for `text-transform`. (Confusingly, this is not handled in "text + // transformation" above, but we follow Gecko in the naming.) + let is_first_run = *start_position == 0; + apply_style_transform_if_necessary( + &mut run_info.text, + old_byte_length, + text_transform, + *last_whitespace, + is_first_run, + ); + *start_position = end_position; + + let new_byte_length = run_info.text.len(); + let is_empty = new_byte_length == old_byte_length; + + // Don't save mappings that contain only discarded characters. + // (But keep ones that contained no characters to begin with, since they might have been + // generated by an empty flow to draw its borders/padding/insertion point.) + if is_empty && !was_empty { + return; + } + + self.byte_range = Range::new(old_byte_length, new_byte_length - old_byte_length); + mappings.push(self) + } + + /// Is the insertion point for this text run within this mapping? + /// + /// NOTE: We treat the range as inclusive at both ends, since the insertion point can lie + /// before the first character *or* after the last character, and should be drawn even if the + /// text is empty. + fn contains_insertion_point(&self, insertion_point: Option<ByteIndex>) -> bool { + match insertion_point.map(ByteIndex::to_usize) { + None => false, + Some(idx) => self.byte_range.begin() <= idx && idx <= self.byte_range.end(), + } + } +} + +/// Accounts for `text-transform`. +/// +/// FIXME(#4311, pcwalton): Title-case mapping can change length of the string; +/// case mapping should be language-specific; `full-width`; +/// use graphemes instead of characters. +fn apply_style_transform_if_necessary( + string: &mut String, + first_character_position: usize, + text_transform: TextTransform, + last_whitespace: bool, + is_first_run: bool, +) { + match text_transform.case_ { + TextTransformCase::None => {}, + TextTransformCase::Uppercase => { + let original = string[first_character_position..].to_owned(); + string.truncate(first_character_position); + for ch in original.chars().flat_map(|ch| ch.to_uppercase()) { + string.push(ch); + } + }, + TextTransformCase::Lowercase => { + let original = string[first_character_position..].to_owned(); + string.truncate(first_character_position); + for ch in original.chars().flat_map(|ch| ch.to_lowercase()) { + string.push(ch); + } + }, + TextTransformCase::Capitalize => { + let original = string[first_character_position..].to_owned(); + string.truncate(first_character_position); + + let mut capitalize_next_letter = is_first_run || last_whitespace; + for character in original.chars() { + // FIXME(#4311, pcwalton): Should be the CSS/Unicode notion of a *typographic + // letter unit*, not an *alphabetic* character: + // + // http://dev.w3.org/csswg/css-text/#typographic-letter-unit + if capitalize_next_letter && character.is_alphabetic() { + string.push(character.to_uppercase().next().unwrap()); + capitalize_next_letter = false; + continue; + } + + string.push(character); + + // FIXME(#4311, pcwalton): Try UAX29 instead of just whitespace. + if character.is_whitespace() { + capitalize_next_letter = true + } + } + }, + } +} + +#[derive(Clone)] +struct ScannedTextRun { + run: Arc<TextRun>, + insertion_point: Option<ByteIndex>, +} + +/// Can a character with script `b` continue a text run with script `a`? +fn is_compatible(a: Script, b: Script) -> bool { + a == b || !is_specific(a) || !is_specific(b) +} + +/// Returns true if the script is not invalid or inherited. +fn is_specific(script: Script) -> bool { + script != Script::Common && script != Script::Inherited +} diff --git a/components/layout_2020/traversal.rs b/components/layout_2020/traversal.rs new file mode 100644 index 00000000000..e968c184341 --- /dev/null +++ b/components/layout_2020/traversal.rs @@ -0,0 +1,350 @@ +/* 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/. */ + +//! Traversals over the DOM and flow trees, running the layout computations. + +use crate::construct::FlowConstructor; +use crate::context::LayoutContext; +use crate::display_list::DisplayListBuildState; +use crate::flow::{Flow, FlowFlags, GetBaseFlow, ImmutableFlowUtils}; +use crate::wrapper::ThreadSafeLayoutNodeHelpers; +use crate::wrapper::{GetRawData, LayoutNodeLayoutData}; +use script_layout_interface::wrapper_traits::{LayoutNode, ThreadSafeLayoutNode}; +use servo_config::opts; +use style::context::{SharedStyleContext, StyleContext}; +use style::data::ElementData; +use style::dom::{NodeInfo, TElement, TNode}; +use style::selector_parser::RestyleDamage; +use style::servo::restyle_damage::ServoRestyleDamage; +use style::traversal::PerLevelTraversalData; +use style::traversal::{recalc_style_at, DomTraversal}; + +pub struct RecalcStyleAndConstructFlows<'a> { + context: LayoutContext<'a>, +} + +impl<'a> RecalcStyleAndConstructFlows<'a> { + /// Creates a traversal context, taking ownership of the shared layout context. + pub fn new(context: LayoutContext<'a>) -> Self { + RecalcStyleAndConstructFlows { context: context } + } + + /// Consumes this traversal context, returning ownership of the shared layout + /// context to the caller. + pub fn destroy(self) -> LayoutContext<'a> { + self.context + } +} + +#[allow(unsafe_code)] +impl<'a, E> DomTraversal<E> for RecalcStyleAndConstructFlows<'a> +where + E: TElement, + E::ConcreteNode: LayoutNode, + E::FontMetricsProvider: Send, +{ + fn process_preorder<F>( + &self, + traversal_data: &PerLevelTraversalData, + context: &mut StyleContext<E>, + node: E::ConcreteNode, + note_child: F, + ) where + F: FnMut(E::ConcreteNode), + { + // FIXME(pcwalton): Stop allocating here. Ideally this should just be + // done by the HTML parser. + unsafe { node.initialize_data() }; + + if !node.is_text_node() { + let el = node.as_element().unwrap(); + let mut data = el.mutate_data().unwrap(); + recalc_style_at(self, traversal_data, context, el, &mut data, note_child); + } + } + + fn process_postorder(&self, _style_context: &mut StyleContext<E>, node: E::ConcreteNode) { + construct_flows_at(&self.context, node); + } + + fn text_node_needs_traversal(node: E::ConcreteNode, parent_data: &ElementData) -> bool { + // Text nodes never need styling. However, there are two cases they may need + // flow construction: + // (1) They child doesn't yet have layout data (preorder traversal initializes it). + // (2) The parent element has restyle damage (so the text flow also needs fixup). + node.get_raw_data().is_none() || !parent_data.damage.is_empty() + } + + fn shared_context(&self) -> &SharedStyleContext { + &self.context.style_context + } +} + +/// A top-down traversal. +pub trait PreorderFlowTraversal { + /// The operation to perform. Return true to continue or false to stop. + fn process(&self, flow: &mut dyn Flow); + + /// Returns true if this node should be processed and false if neither this node nor its + /// descendants should be processed. + fn should_process_subtree(&self, _flow: &mut dyn Flow) -> bool { + true + } + + /// Returns true if this node must be processed in-order. If this returns false, + /// we skip the operation for this node, but continue processing the descendants. + /// This is called *after* parent nodes are visited. + fn should_process(&self, _flow: &mut dyn Flow) -> bool { + true + } + + /// Traverses the tree in preorder. + fn traverse(&self, flow: &mut dyn Flow) { + if !self.should_process_subtree(flow) { + return; + } + if self.should_process(flow) { + self.process(flow); + } + for kid in flow.mut_base().child_iter_mut() { + self.traverse(kid); + } + } + + /// Traverse the Absolute flow tree in preorder. + /// + /// Traverse all your direct absolute descendants, who will then traverse + /// their direct absolute descendants. + /// + /// Return true if the traversal is to continue or false to stop. + fn traverse_absolute_flows(&self, flow: &mut dyn Flow) { + if self.should_process(flow) { + self.process(flow); + } + for descendant_link in flow.mut_base().abs_descendants.iter() { + self.traverse_absolute_flows(descendant_link) + } + } +} + +/// A bottom-up traversal, with a optional in-order pass. +pub trait PostorderFlowTraversal { + /// The operation to perform. Return true to continue or false to stop. + fn process(&self, flow: &mut dyn Flow); + + /// Returns false if this node must be processed in-order. If this returns false, we skip the + /// operation for this node, but continue processing the ancestors. This is called *after* + /// child nodes are visited. + fn should_process(&self, _flow: &mut dyn Flow) -> bool { + true + } + + /// Traverses the tree in postorder. + fn traverse(&self, flow: &mut dyn Flow) { + for kid in flow.mut_base().child_iter_mut() { + self.traverse(kid); + } + if self.should_process(flow) { + self.process(flow); + } + } +} + +/// An in-order (sequential only) traversal. +pub trait InorderFlowTraversal { + /// The operation to perform. Returns the level of the tree we're at. + fn process(&mut self, flow: &mut dyn Flow, level: u32); + + /// Returns true if this node should be processed and false if neither this node nor its + /// descendants should be processed. + fn should_process_subtree(&mut self, _flow: &mut dyn Flow) -> bool { + true + } + + /// Traverses the tree in-order. + fn traverse(&mut self, flow: &mut dyn Flow, level: u32) { + if !self.should_process_subtree(flow) { + return; + } + self.process(flow, level); + for kid in flow.mut_base().child_iter_mut() { + self.traverse(kid, level + 1); + } + } +} + +/// A bottom-up, parallelizable traversal. +pub trait PostorderNodeMutTraversal<ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode> { + /// The operation to perform. Return true to continue or false to stop. + fn process(&mut self, node: &ConcreteThreadSafeLayoutNode); +} + +/// The flow construction traversal, which builds flows for styled nodes. +#[inline] +#[allow(unsafe_code)] +fn construct_flows_at<N>(context: &LayoutContext, node: N) +where + N: LayoutNode, +{ + debug!("construct_flows_at: {:?}", node); + + // Construct flows for this node. + { + let tnode = node.to_threadsafe(); + + // Always reconstruct if incremental layout is turned off. + let nonincremental_layout = opts::get().nonincremental_layout; + if nonincremental_layout || + tnode.restyle_damage() != RestyleDamage::empty() || + node.as_element() + .map_or(false, |el| el.has_dirty_descendants()) + { + let mut flow_constructor = FlowConstructor::new(context); + if nonincremental_layout || !flow_constructor.repair_if_possible(&tnode) { + flow_constructor.process(&tnode); + debug!( + "Constructed flow for {:?}: {:x}", + tnode, + tnode.flow_debug_id() + ); + } + } + + tnode + .mutate_layout_data() + .unwrap() + .flags + .insert(crate::data::LayoutDataFlags::HAS_BEEN_TRAVERSED); + } + + if let Some(el) = node.as_element() { + unsafe { + el.unset_dirty_descendants(); + } + } +} + +/// The bubble-inline-sizes traversal, the first part of layout computation. This computes +/// preferred and intrinsic inline-sizes and bubbles them up the tree. +pub struct BubbleISizes<'a> { + pub layout_context: &'a LayoutContext<'a>, +} + +impl<'a> PostorderFlowTraversal for BubbleISizes<'a> { + #[inline] + fn process(&self, flow: &mut dyn Flow) { + flow.bubble_inline_sizes(); + flow.mut_base() + .restyle_damage + .remove(ServoRestyleDamage::BUBBLE_ISIZES); + } + + #[inline] + fn should_process(&self, flow: &mut dyn Flow) -> bool { + flow.base() + .restyle_damage + .contains(ServoRestyleDamage::BUBBLE_ISIZES) + } +} + +/// The assign-inline-sizes traversal. In Gecko this corresponds to `Reflow`. +#[derive(Clone, Copy)] +pub struct AssignISizes<'a> { + pub layout_context: &'a LayoutContext<'a>, +} + +impl<'a> PreorderFlowTraversal for AssignISizes<'a> { + #[inline] + fn process(&self, flow: &mut dyn Flow) { + flow.assign_inline_sizes(self.layout_context); + } + + #[inline] + fn should_process(&self, flow: &mut dyn Flow) -> bool { + flow.base() + .restyle_damage + .intersects(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW) + } +} + +/// The assign-block-sizes-and-store-overflow traversal, the last (and most expensive) part of +/// layout computation. Determines the final block-sizes for all layout objects and computes +/// positions. In Gecko this corresponds to `Reflow`. +#[derive(Clone, Copy)] +pub struct AssignBSizes<'a> { + pub layout_context: &'a LayoutContext<'a>, +} + +impl<'a> PostorderFlowTraversal for AssignBSizes<'a> { + #[inline] + fn process(&self, flow: &mut dyn Flow) { + // Can't do anything with anything that floats might flow through until we reach their + // inorder parent. + // + // NB: We must return without resetting the restyle bits for these, as we haven't actually + // reflowed anything! + if flow.floats_might_flow_through() { + return; + } + + flow.assign_block_size(self.layout_context); + } + + #[inline] + fn should_process(&self, flow: &mut dyn Flow) -> bool { + let base = flow.base(); + base.restyle_damage.intersects(ServoRestyleDamage::REFLOW_OUT_OF_FLOW | ServoRestyleDamage::REFLOW) && + // The fragmentation countainer is responsible for calling + // Flow::fragment recursively + !base.flags.contains(FlowFlags::CAN_BE_FRAGMENTED) + } +} + +pub struct ComputeStackingRelativePositions<'a> { + pub layout_context: &'a LayoutContext<'a>, +} + +impl<'a> PreorderFlowTraversal for ComputeStackingRelativePositions<'a> { + #[inline] + fn should_process_subtree(&self, flow: &mut dyn Flow) -> bool { + flow.base() + .restyle_damage + .contains(ServoRestyleDamage::REPOSITION) + } + + #[inline] + fn process(&self, flow: &mut dyn Flow) { + flow.compute_stacking_relative_position(self.layout_context); + flow.mut_base() + .restyle_damage + .remove(ServoRestyleDamage::REPOSITION) + } +} + +pub struct BuildDisplayList<'a> { + pub state: DisplayListBuildState<'a>, +} + +impl<'a> BuildDisplayList<'a> { + #[inline] + pub fn traverse(&mut self, flow: &mut dyn Flow) { + let parent_stacking_context_id = self.state.current_stacking_context_id; + self.state.current_stacking_context_id = flow.base().stacking_context_id; + + let parent_clipping_and_scrolling = self.state.current_clipping_and_scrolling; + self.state.current_clipping_and_scrolling = flow.clipping_and_scrolling(); + + flow.build_display_list(&mut self.state); + flow.mut_base() + .restyle_damage + .remove(ServoRestyleDamage::REPAINT); + + for kid in flow.mut_base().child_iter_mut() { + self.traverse(kid); + } + + self.state.current_stacking_context_id = parent_stacking_context_id; + self.state.current_clipping_and_scrolling = parent_clipping_and_scrolling; + } +} diff --git a/components/layout_2020/wrapper.rs b/components/layout_2020/wrapper.rs new file mode 100644 index 00000000000..063d7f3d25d --- /dev/null +++ b/components/layout_2020/wrapper.rs @@ -0,0 +1,174 @@ +/* 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/. */ + +//! A safe wrapper for DOM nodes that prevents layout from mutating the DOM, from letting DOM nodes +//! escape, and from generally doing anything that it isn't supposed to. This is accomplished via +//! a simple whitelist of allowed operations, along with some lifetime magic to prevent nodes from +//! escaping. +//! +//! As a security wrapper is only as good as its whitelist, be careful when adding operations to +//! this list. The cardinal rules are: +//! +//! 1. Layout is not allowed to mutate the DOM. +//! +//! 2. Layout is not allowed to see anything with `LayoutDom` in the name, because it could hang +//! onto these objects and cause use-after-free. +//! +//! When implementing wrapper functions, be careful that you do not touch the borrow flags, or you +//! will race and cause spurious thread failure. (Note that I do not believe these races are +//! exploitable, but they'll result in brokenness nonetheless.) +//! +//! Rules of the road for this file: +//! +//! * Do not call any methods on DOM nodes without checking to see whether they use borrow flags. +//! +//! o Instead of `get_attr()`, use `.get_attr_val_for_layout()`. +//! +//! o Instead of `html_element_in_html_document()`, use +//! `html_element_in_html_document_for_layout()`. + +#![allow(unsafe_code)] + +use crate::data::{LayoutData, LayoutDataFlags, StyleAndLayoutData}; +use atomic_refcell::{AtomicRef, AtomicRefMut}; +use script_layout_interface::wrapper_traits::GetLayoutData; +use script_layout_interface::wrapper_traits::{ThreadSafeLayoutElement, ThreadSafeLayoutNode}; +use style::dom::{NodeInfo, TNode}; +use style::selector_parser::RestyleDamage; +use style::values::computed::counters::ContentItem; +use style::values::generics::counters::Content; + +pub trait LayoutNodeLayoutData { + /// Similar to borrow_data*, but returns the full PersistentLayoutData rather + /// than only the style::data::ElementData. + fn borrow_layout_data(&self) -> Option<AtomicRef<LayoutData>>; + fn mutate_layout_data(&self) -> Option<AtomicRefMut<LayoutData>>; + fn flow_debug_id(self) -> usize; +} + +impl<T: GetLayoutData> LayoutNodeLayoutData for T { + fn borrow_layout_data(&self) -> Option<AtomicRef<LayoutData>> { + self.get_raw_data().map(|d| d.layout_data.borrow()) + } + + fn mutate_layout_data(&self) -> Option<AtomicRefMut<LayoutData>> { + self.get_raw_data().map(|d| d.layout_data.borrow_mut()) + } + + fn flow_debug_id(self) -> usize { + self.borrow_layout_data() + .map_or(0, |d| d.flow_construction_result.debug_id()) + } +} + +pub trait GetRawData { + fn get_raw_data(&self) -> Option<&StyleAndLayoutData>; +} + +impl<T: GetLayoutData> GetRawData for T { + fn get_raw_data(&self) -> Option<&StyleAndLayoutData> { + self.get_style_and_layout_data().map(|opaque| { + let container = opaque.ptr.as_ptr() as *mut StyleAndLayoutData; + unsafe { &*container } + }) + } +} + +pub trait ThreadSafeLayoutNodeHelpers { + /// Returns the layout data flags for this node. + fn flags(self) -> LayoutDataFlags; + + /// Adds the given flags to this node. + fn insert_flags(self, new_flags: LayoutDataFlags); + + /// Removes the given flags from this node. + fn remove_flags(self, flags: LayoutDataFlags); + + /// If this is a text node, generated content, or a form element, copies out + /// its content. Otherwise, panics. + /// + /// FIXME(pcwalton): This might have too much copying and/or allocation. Profile this. + fn text_content(&self) -> TextContent; + + /// The RestyleDamage from any restyling, or RestyleDamage::rebuild_and_reflow() if this + /// is the first time layout is visiting this node. We implement this here, rather than + /// with the rest of the wrapper layer, because we need layout code to determine whether + /// layout has visited the node. + fn restyle_damage(self) -> RestyleDamage; +} + +impl<T: ThreadSafeLayoutNode> ThreadSafeLayoutNodeHelpers for T { + fn flags(self) -> LayoutDataFlags { + self.borrow_layout_data().as_ref().unwrap().flags + } + + fn insert_flags(self, new_flags: LayoutDataFlags) { + self.mutate_layout_data().unwrap().flags.insert(new_flags); + } + + fn remove_flags(self, flags: LayoutDataFlags) { + self.mutate_layout_data().unwrap().flags.remove(flags); + } + + fn text_content(&self) -> TextContent { + if self.get_pseudo_element_type().is_replaced_content() { + let style = self.as_element().unwrap().resolved_style(); + + return TextContent::GeneratedContent(match style.as_ref().get_counters().content { + Content::Items(ref value) => value.to_vec(), + _ => vec![], + }); + } + + TextContent::Text(self.node_text_content().into_boxed_str()) + } + + fn restyle_damage(self) -> RestyleDamage { + // We need the underlying node to potentially access the parent in the + // case of text nodes. This is safe as long as we don't let the parent + // escape and never access its descendants. + let mut node = unsafe { self.unsafe_get() }; + + // If this is a text node, use the parent element, since that's what + // controls our style. + if node.is_text_node() { + node = node.parent_node().unwrap(); + debug_assert!(node.is_element()); + } + + let damage = { + let data = node.get_raw_data().unwrap(); + + if !data + .layout_data + .borrow() + .flags + .contains(crate::data::LayoutDataFlags::HAS_BEEN_TRAVERSED) + { + // We're reflowing a node that was styled for the first time and + // has never been visited by layout. Return rebuild_and_reflow, + // because that's what the code expects. + RestyleDamage::rebuild_and_reflow() + } else { + data.style_data.element_data.borrow().damage + } + }; + + damage + } +} + +pub enum TextContent { + Text(Box<str>), + GeneratedContent(Vec<ContentItem>), +} + +impl TextContent { + pub fn is_empty(&self) -> bool { + match *self { + TextContent::Text(_) => false, + TextContent::GeneratedContent(ref content) => content.is_empty(), + } + } +} diff --git a/components/layout_thread_2020/Cargo.toml b/components/layout_thread_2020/Cargo.toml index 8711cec2638..3cc6f57136e 100644 --- a/components/layout_thread_2020/Cargo.toml +++ b/components/layout_thread_2020/Cargo.toml @@ -11,18 +11,43 @@ name = "layout_thread" path = "lib.rs" [dependencies] +app_units = "0.7" +atomic_refcell = "0.1" crossbeam-channel = "0.3" +embedder_traits = {path = "../embedder_traits"} euclid = "0.20" +fnv = "1.0" +fxhash = "0.2" gfx = {path = "../gfx"} +gfx_traits = {path = "../gfx_traits"} +histogram = "0.6.8" +html5ever = "0.23" ipc-channel = "0.11" layout = {path = "../layout_2020", package = "layout_2020"} layout_traits = {path = "../layout_traits"} +lazy_static = "1" +libc = "0.2" +log = "0.4" +time = "0.1.17" +malloc_size_of = { path = "../malloc_size_of" } metrics = {path = "../metrics"} msg = {path = "../msg"} net_traits = {path = "../net_traits"} +parking_lot = {version = "0.8", features = ["nightly"]} profile_traits = {path = "../profile_traits"} +range = {path = "../range"} +rayon = "1" +script = {path = "../script"} script_layout_interface = {path = "../script_layout_interface"} script_traits = {path = "../script_traits"} -servo_url = {path = "../url"} +selectors = { path = "../selectors" } +serde_json = "1.0" +servo_allocator = {path = "../allocator"} +servo_arc = {path = "../servo_arc"} +servo_atoms = {path = "../atoms"} +servo_config = {path = "../config"} servo_geometry = {path = "../geometry"} +servo_url = {path = "../url"} +style = {path = "../style"} +style_traits = {path = "../style_traits"} webrender_api = {git = "https://github.com/servo/webrender", features = ["ipc"]} diff --git a/components/layout_thread_2020/dom_wrapper.rs b/components/layout_thread_2020/dom_wrapper.rs new file mode 100644 index 00000000000..5d815517532 --- /dev/null +++ b/components/layout_thread_2020/dom_wrapper.rs @@ -0,0 +1,1550 @@ +/* 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/. */ + +//! A safe wrapper for DOM nodes that prevents layout from mutating the DOM, from letting DOM nodes +//! escape, and from generally doing anything that it isn't supposed to. This is accomplished via +//! a simple whitelist of allowed operations, along with some lifetime magic to prevent nodes from +//! escaping. +//! +//! As a security wrapper is only as good as its whitelist, be careful when adding operations to +//! this list. The cardinal rules are: +//! +//! 1. Layout is not allowed to mutate the DOM. +//! +//! 2. Layout is not allowed to see anything with `LayoutDom` in the name, because it could hang +//! onto these objects and cause use-after-free. +//! +//! When implementing wrapper functions, be careful that you do not touch the borrow flags, or you +//! will race and cause spurious thread failure. (Note that I do not believe these races are +//! exploitable, but they'll result in brokenness nonetheless.) +//! +//! Rules of the road for this file: +//! +//! * Do not call any methods on DOM nodes without checking to see whether they use borrow flags. +//! +//! o Instead of `get_attr()`, use `.get_attr_val_for_layout()`. +//! +//! o Instead of `html_element_in_html_document()`, use +//! `html_element_in_html_document_for_layout()`. + +#![allow(unsafe_code)] + +use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; +use gfx_traits::ByteIndex; +use html5ever::{LocalName, Namespace}; +use layout::data::StyleAndLayoutData; +use layout::wrapper::GetRawData; +use msg::constellation_msg::{BrowsingContextId, PipelineId}; +use net_traits::image::base::{Image, ImageMetadata}; +use range::Range; +use script::layout_exports::NodeFlags; +use script::layout_exports::PendingRestyle; +use script::layout_exports::ShadowRoot; +use script::layout_exports::{ + CharacterDataTypeId, DocumentFragmentTypeId, ElementTypeId, HTMLElementTypeId, NodeTypeId, + TextTypeId, +}; +use script::layout_exports::{Document, Element, Node, Text}; +use script::layout_exports::{LayoutCharacterDataHelpers, LayoutDocumentHelpers}; +use script::layout_exports::{ + LayoutDom, LayoutElementHelpers, LayoutNodeHelpers, LayoutShadowRootHelpers, + RawLayoutElementHelpers, +}; +use script_layout_interface::wrapper_traits::{ + DangerousThreadSafeLayoutNode, GetLayoutData, LayoutNode, +}; +use script_layout_interface::wrapper_traits::{ + PseudoElementType, ThreadSafeLayoutElement, ThreadSafeLayoutNode, +}; +use script_layout_interface::{ + HTMLCanvasData, HTMLMediaData, LayoutNodeType, OpaqueStyleAndLayoutData, +}; +use script_layout_interface::{SVGSVGData, StyleData, TrustedNodeAddress}; +use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; +use selectors::matching::VisitedHandlingMode; +use selectors::matching::{ElementSelectorFlags, MatchingContext, QuirksMode}; +use selectors::sink::Push; +use servo_arc::{Arc, ArcBorrow}; +use servo_atoms::Atom; +use servo_url::ServoUrl; +use std::fmt; +use std::fmt::Debug; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; +use std::ptr::NonNull; +use std::sync::atomic::Ordering; +use std::sync::Arc as StdArc; +use style::applicable_declarations::ApplicableDeclarationBlock; +use style::attr::AttrValue; +use style::context::SharedStyleContext; +use style::data::ElementData; +use style::dom::{DomChildren, LayoutIterator, NodeInfo, OpaqueNode}; +use style::dom::{TDocument, TElement, TNode, TShadowRoot}; +use style::element_state::*; +use style::font_metrics::ServoMetricsProvider; +use style::media_queries::Device; +use style::properties::{ComputedValues, PropertyDeclarationBlock}; +use style::selector_parser::{extended_filtering, PseudoElement, SelectorImpl}; +use style::selector_parser::{AttrValue as SelectorAttrValue, Lang, NonTSPseudoClass}; +use style::shared_lock::{ + Locked as StyleLocked, SharedRwLock as StyleSharedRwLock, SharedRwLockReadGuard, +}; +use style::str::is_whitespace; +use style::stylist::CascadeData; +use style::CaseSensitivityExt; + +pub unsafe fn drop_style_and_layout_data(data: OpaqueStyleAndLayoutData) { + let ptr = data.ptr.as_ptr() as *mut StyleData; + let non_opaque: *mut StyleAndLayoutData = ptr as *mut _; + let _ = Box::from_raw(non_opaque); +} + +#[derive(Clone, Copy)] +pub struct ServoLayoutNode<'a> { + /// The wrapped node. + node: LayoutDom<Node>, + + /// Being chained to a PhantomData prevents `LayoutNode`s from escaping. + chain: PhantomData<&'a ()>, +} + +impl<'ln> Debug for ServoLayoutNode<'ln> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(el) = self.as_element() { + el.fmt(f) + } else { + if self.is_text_node() { + write!(f, "<text node> ({:#x})", self.opaque().0) + } else { + write!(f, "<non-text node> ({:#x})", self.opaque().0) + } + } + } +} + +impl<'a> PartialEq for ServoLayoutNode<'a> { + #[inline] + fn eq(&self, other: &ServoLayoutNode) -> bool { + self.node == other.node + } +} + +impl<'ln> ServoLayoutNode<'ln> { + fn from_layout_js(n: LayoutDom<Node>) -> ServoLayoutNode<'ln> { + ServoLayoutNode { + node: n, + chain: PhantomData, + } + } + + pub unsafe fn new(address: &TrustedNodeAddress) -> ServoLayoutNode { + ServoLayoutNode::from_layout_js(LayoutDom::from_trusted_node_address(*address)) + } + + /// Creates a new layout node with the same lifetime as this layout node. + pub unsafe fn new_with_this_lifetime(&self, node: &LayoutDom<Node>) -> ServoLayoutNode<'ln> { + ServoLayoutNode { + node: *node, + chain: self.chain, + } + } + + fn script_type_id(&self) -> NodeTypeId { + unsafe { self.node.type_id_for_layout() } + } +} + +impl<'ln> NodeInfo for ServoLayoutNode<'ln> { + fn is_element(&self) -> bool { + unsafe { self.node.is_element_for_layout() } + } + + fn is_text_node(&self) -> bool { + self.script_type_id() == + NodeTypeId::CharacterData(CharacterDataTypeId::Text(TextTypeId::Text)) + } +} + +#[derive(Clone, Copy, PartialEq)] +pub struct ServoShadowRoot<'a> { + /// The wrapped shadow root. + shadow_root: LayoutDom<ShadowRoot>, + + /// Being chained to a PhantomData prevents `ShadowRoot`s from escaping. + chain: PhantomData<&'a ()>, +} + +impl<'lr> Debug for ServoShadowRoot<'lr> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.as_node().fmt(f) + } +} + +impl<'lr> TShadowRoot for ServoShadowRoot<'lr> { + type ConcreteNode = ServoLayoutNode<'lr>; + + fn as_node(&self) -> Self::ConcreteNode { + ServoLayoutNode::from_layout_js(self.shadow_root.upcast()) + } + + fn host(&self) -> ServoLayoutElement<'lr> { + ServoLayoutElement::from_layout_js(unsafe { self.shadow_root.get_host_for_layout() }) + } + + fn style_data<'a>(&self) -> Option<&'a CascadeData> + where + Self: 'a, + { + Some(unsafe { + &self + .shadow_root + .get_style_data_for_layout::<ServoLayoutElement>() + .data + }) + } +} + +impl<'lr> ServoShadowRoot<'lr> { + fn from_layout_js(shadow_root: LayoutDom<ShadowRoot>) -> ServoShadowRoot<'lr> { + ServoShadowRoot { + shadow_root, + chain: PhantomData, + } + } + + pub unsafe fn flush_stylesheets( + &self, + device: &Device, + quirks_mode: QuirksMode, + guard: &SharedRwLockReadGuard, + ) { + self.shadow_root + .flush_stylesheets::<ServoLayoutElement>(device, quirks_mode, guard) + } +} + +impl<'ln> TNode for ServoLayoutNode<'ln> { + type ConcreteDocument = ServoLayoutDocument<'ln>; + type ConcreteElement = ServoLayoutElement<'ln>; + type ConcreteShadowRoot = ServoShadowRoot<'ln>; + + fn parent_node(&self) -> Option<Self> { + unsafe { + self.node + .composed_parent_node_ref() + .map(|node| self.new_with_this_lifetime(&node)) + } + } + + fn first_child(&self) -> Option<Self> { + unsafe { + self.node + .first_child_ref() + .map(|node| self.new_with_this_lifetime(&node)) + } + } + + fn last_child(&self) -> Option<Self> { + unsafe { + self.node + .last_child_ref() + .map(|node| self.new_with_this_lifetime(&node)) + } + } + + fn prev_sibling(&self) -> Option<Self> { + unsafe { + self.node + .prev_sibling_ref() + .map(|node| self.new_with_this_lifetime(&node)) + } + } + + fn next_sibling(&self) -> Option<Self> { + unsafe { + self.node + .next_sibling_ref() + .map(|node| self.new_with_this_lifetime(&node)) + } + } + + fn owner_doc(&self) -> Self::ConcreteDocument { + ServoLayoutDocument::from_layout_js(unsafe { self.node.owner_doc_for_layout() }) + } + + fn traversal_parent(&self) -> Option<ServoLayoutElement<'ln>> { + let parent = self.parent_node()?; + if let Some(shadow) = parent.as_shadow_root() { + return Some(shadow.host()); + }; + parent.as_element() + } + + fn opaque(&self) -> OpaqueNode { + unsafe { self.get_jsmanaged().opaque() } + } + + fn debug_id(self) -> usize { + self.opaque().0 + } + + fn as_element(&self) -> Option<ServoLayoutElement<'ln>> { + as_element(self.node) + } + + fn as_document(&self) -> Option<ServoLayoutDocument<'ln>> { + self.node + .downcast() + .map(ServoLayoutDocument::from_layout_js) + } + + fn as_shadow_root(&self) -> Option<ServoShadowRoot<'ln>> { + self.node.downcast().map(ServoShadowRoot::from_layout_js) + } + + fn is_in_document(&self) -> bool { + unsafe { self.node.get_flag(NodeFlags::IS_IN_DOC) } + } +} + +impl<'ln> LayoutNode for ServoLayoutNode<'ln> { + type ConcreteThreadSafeLayoutNode = ServoThreadSafeLayoutNode<'ln>; + + fn to_threadsafe(&self) -> Self::ConcreteThreadSafeLayoutNode { + ServoThreadSafeLayoutNode::new(self) + } + + fn type_id(&self) -> LayoutNodeType { + self.script_type_id().into() + } + + unsafe fn initialize_data(&self) { + if self.get_raw_data().is_none() { + let ptr: *mut StyleAndLayoutData = Box::into_raw(Box::new(StyleAndLayoutData::new())); + let opaque = OpaqueStyleAndLayoutData { + ptr: NonNull::new_unchecked(ptr as *mut StyleData), + }; + self.init_style_and_layout_data(opaque); + }; + } + + unsafe fn init_style_and_layout_data(&self, data: OpaqueStyleAndLayoutData) { + self.get_jsmanaged().init_style_and_layout_data(data); + } + + unsafe fn take_style_and_layout_data(&self) -> OpaqueStyleAndLayoutData { + self.get_jsmanaged().take_style_and_layout_data() + } + + fn is_connected(&self) -> bool { + unsafe { self.node.get_flag(NodeFlags::IS_CONNECTED) } + } +} + +impl<'ln> GetLayoutData for ServoLayoutNode<'ln> { + fn get_style_and_layout_data(&self) -> Option<OpaqueStyleAndLayoutData> { + unsafe { self.get_jsmanaged().get_style_and_layout_data() } + } +} + +impl<'le> GetLayoutData for ServoLayoutElement<'le> { + fn get_style_and_layout_data(&self) -> Option<OpaqueStyleAndLayoutData> { + self.as_node().get_style_and_layout_data() + } +} + +impl<'ln> GetLayoutData for ServoThreadSafeLayoutNode<'ln> { + fn get_style_and_layout_data(&self) -> Option<OpaqueStyleAndLayoutData> { + self.node.get_style_and_layout_data() + } +} + +impl<'le> GetLayoutData for ServoThreadSafeLayoutElement<'le> { + fn get_style_and_layout_data(&self) -> Option<OpaqueStyleAndLayoutData> { + self.element.as_node().get_style_and_layout_data() + } +} + +impl<'ln> ServoLayoutNode<'ln> { + /// Returns the interior of this node as a `LayoutDom`. This is highly unsafe for layout to + /// call and as such is marked `unsafe`. + pub unsafe fn get_jsmanaged(&self) -> &LayoutDom<Node> { + &self.node + } +} + +// A wrapper around documents that ensures ayout can only ever access safe properties. +#[derive(Clone, Copy)] +pub struct ServoLayoutDocument<'ld> { + document: LayoutDom<Document>, + chain: PhantomData<&'ld ()>, +} + +impl<'ld> TDocument for ServoLayoutDocument<'ld> { + type ConcreteNode = ServoLayoutNode<'ld>; + + fn as_node(&self) -> Self::ConcreteNode { + ServoLayoutNode::from_layout_js(self.document.upcast()) + } + + fn quirks_mode(&self) -> QuirksMode { + unsafe { self.document.quirks_mode() } + } + + fn is_html_document(&self) -> bool { + unsafe { self.document.is_html_document_for_layout() } + } +} + +impl<'ld> ServoLayoutDocument<'ld> { + pub fn root_element(&self) -> Option<ServoLayoutElement<'ld>> { + self.as_node() + .dom_children() + .flat_map(|n| n.as_element()) + .next() + } + + pub fn drain_pending_restyles(&self) -> Vec<(ServoLayoutElement<'ld>, PendingRestyle)> { + let elements = unsafe { self.document.drain_pending_restyles() }; + elements + .into_iter() + .map(|(el, snapshot)| (ServoLayoutElement::from_layout_js(el), snapshot)) + .collect() + } + + pub fn needs_paint_from_layout(&self) { + unsafe { self.document.needs_paint_from_layout() } + } + + pub fn will_paint(&self) { + unsafe { self.document.will_paint() } + } + + pub fn style_shared_lock(&self) -> &StyleSharedRwLock { + unsafe { self.document.style_shared_lock() } + } + + pub fn shadow_roots(&self) -> Vec<ServoShadowRoot> { + unsafe { + self.document + .shadow_roots() + .iter() + .map(|sr| { + debug_assert!(sr.upcast::<Node>().get_flag(NodeFlags::IS_CONNECTED)); + ServoShadowRoot::from_layout_js(*sr) + }) + .collect() + } + } + + pub fn flush_shadow_roots_stylesheets( + &self, + device: &Device, + quirks_mode: QuirksMode, + guard: &SharedRwLockReadGuard, + ) { + unsafe { + if !self.document.shadow_roots_styles_changed() { + return; + } + self.document.flush_shadow_roots_stylesheets(); + for shadow_root in self.shadow_roots() { + shadow_root.flush_stylesheets(device, quirks_mode, guard); + } + } + } + + pub fn from_layout_js(doc: LayoutDom<Document>) -> ServoLayoutDocument<'ld> { + ServoLayoutDocument { + document: doc, + chain: PhantomData, + } + } +} + +/// A wrapper around elements that ensures layout can only ever access safe properties. +#[derive(Clone, Copy)] +pub struct ServoLayoutElement<'le> { + element: LayoutDom<Element>, + chain: PhantomData<&'le ()>, +} + +impl<'le> fmt::Debug for ServoLayoutElement<'le> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "<{}", self.element.local_name())?; + if let Some(id) = self.id() { + write!(f, " id={}", id)?; + } + write!(f, "> ({:#x})", self.as_node().opaque().0) + } +} + +impl<'le> TElement for ServoLayoutElement<'le> { + type ConcreteNode = ServoLayoutNode<'le>; + type TraversalChildrenIterator = DomChildren<Self::ConcreteNode>; + + type FontMetricsProvider = ServoMetricsProvider; + + fn as_node(&self) -> ServoLayoutNode<'le> { + ServoLayoutNode::from_layout_js(self.element.upcast()) + } + + fn traversal_children(&self) -> LayoutIterator<Self::TraversalChildrenIterator> { + LayoutIterator(if let Some(shadow) = self.shadow_root() { + shadow.as_node().dom_children() + } else { + self.as_node().dom_children() + }) + } + + fn is_html_element(&self) -> bool { + unsafe { self.element.is_html_element() } + } + + fn is_mathml_element(&self) -> bool { + *self.element.namespace() == ns!(mathml) + } + + fn is_svg_element(&self) -> bool { + *self.element.namespace() == ns!(svg) + } + + fn has_part_attr(&self) -> bool { + false + } + + fn style_attribute(&self) -> Option<ArcBorrow<StyleLocked<PropertyDeclarationBlock>>> { + unsafe { + (*self.element.style_attribute()) + .as_ref() + .map(|x| x.borrow_arc()) + } + } + + fn state(&self) -> ElementState { + self.element.get_state_for_layout() + } + + #[inline] + fn has_attr(&self, namespace: &Namespace, attr: &LocalName) -> bool { + self.get_attr(namespace, attr).is_some() + } + + #[inline] + fn id(&self) -> Option<&Atom> { + unsafe { (*self.element.id_attribute()).as_ref() } + } + + #[inline(always)] + fn each_class<F>(&self, mut callback: F) + where + F: FnMut(&Atom), + { + unsafe { + if let Some(ref classes) = self.element.get_classes_for_layout() { + for class in *classes { + callback(class) + } + } + } + } + + fn has_dirty_descendants(&self) -> bool { + unsafe { + self.as_node() + .node + .get_flag(NodeFlags::HAS_DIRTY_DESCENDANTS) + } + } + + fn has_snapshot(&self) -> bool { + unsafe { self.as_node().node.get_flag(NodeFlags::HAS_SNAPSHOT) } + } + + fn handled_snapshot(&self) -> bool { + unsafe { self.as_node().node.get_flag(NodeFlags::HANDLED_SNAPSHOT) } + } + + unsafe fn set_handled_snapshot(&self) { + self.as_node() + .node + .set_flag(NodeFlags::HANDLED_SNAPSHOT, true); + } + + unsafe fn set_dirty_descendants(&self) { + debug_assert!(self.as_node().is_connected()); + self.as_node() + .node + .set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true) + } + + unsafe fn unset_dirty_descendants(&self) { + self.as_node() + .node + .set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, false) + } + + fn store_children_to_process(&self, n: isize) { + let data = self.get_style_data().unwrap(); + data.parallel + .children_to_process + .store(n, Ordering::Relaxed); + } + + fn did_process_child(&self) -> isize { + let data = self.get_style_data().unwrap(); + let old_value = data + .parallel + .children_to_process + .fetch_sub(1, Ordering::Relaxed); + debug_assert!(old_value >= 1); + old_value - 1 + } + + unsafe fn clear_data(&self) { + if self.get_raw_data().is_some() { + drop_style_and_layout_data(self.as_node().take_style_and_layout_data()); + } + } + + unsafe fn ensure_data(&self) -> AtomicRefMut<ElementData> { + self.as_node().initialize_data(); + self.mutate_data().unwrap() + } + + fn get_data(&self) -> Option<&AtomicRefCell<ElementData>> { + unsafe { + self.get_style_and_layout_data() + .map(|d| &(*(d.ptr.as_ptr() as *mut StyleData)).element_data) + } + } + + fn skip_item_display_fixup(&self) -> bool { + false + } + + unsafe fn set_selector_flags(&self, flags: ElementSelectorFlags) { + self.element.insert_selector_flags(flags); + } + + fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool { + self.element.has_selector_flags(flags) + } + + fn has_animations(&self) -> bool { + // We use this function not only for Gecko but also for Servo to know if this element has + // animations, so we maybe try to get the important rules of this element. This is used for + // off-main thread animations, but we don't support it on Servo, so return false directly. + false + } + + fn has_css_animations(&self) -> bool { + unreachable!("this should be only called on gecko"); + } + + fn has_css_transitions(&self) -> bool { + unreachable!("this should be only called on gecko"); + } + + #[inline] + fn lang_attr(&self) -> Option<SelectorAttrValue> { + self.get_attr(&ns!(xml), &local_name!("lang")) + .or_else(|| self.get_attr(&ns!(), &local_name!("lang"))) + .map(|v| String::from(v as &str)) + } + + fn match_element_lang( + &self, + override_lang: Option<Option<SelectorAttrValue>>, + value: &Lang, + ) -> bool { + // Servo supports :lang() from CSS Selectors 4, which can take a comma- + // separated list of language tags in the pseudo-class, and which + // performs RFC 4647 extended filtering matching on them. + // + // FIXME(heycam): This is wrong, since extended_filtering accepts + // a string containing commas (separating each language tag in + // a list) but the pseudo-class instead should be parsing and + // storing separate <ident> or <string>s for each language tag. + // + // FIXME(heycam): Look at `element`'s document's Content-Language + // HTTP header for language tags to match `value` against. To + // do this, we should make `get_lang_for_layout` return an Option, + // so we can decide when to fall back to the Content-Language check. + let element_lang = match override_lang { + Some(Some(lang)) => lang, + Some(None) => String::new(), + None => self.element.get_lang_for_layout(), + }; + extended_filtering(&element_lang, &*value) + } + + fn is_html_document_body_element(&self) -> bool { + // This is only used for the "tables inherit from body" quirk, which we + // don't implement. + // + // FIXME(emilio): We should be able to give the right answer though! + false + } + + fn synthesize_presentational_hints_for_legacy_attributes<V>( + &self, + _visited_handling: VisitedHandlingMode, + hints: &mut V, + ) where + V: Push<ApplicableDeclarationBlock>, + { + unsafe { + self.element + .synthesize_presentational_hints_for_legacy_attributes(hints); + } + } + + /// The shadow root this element is a host of. + fn shadow_root(&self) -> Option<ServoShadowRoot<'le>> { + unsafe { + self.element + .get_shadow_root_for_layout() + .map(ServoShadowRoot::from_layout_js) + } + } + + /// The shadow root which roots the subtree this element is contained in. + fn containing_shadow(&self) -> Option<ServoShadowRoot<'le>> { + unsafe { + self.element + .upcast() + .containing_shadow_root_for_layout() + .map(ServoShadowRoot::from_layout_js) + } + } + + fn local_name(&self) -> &LocalName { + self.element.local_name() + } + + fn namespace(&self) -> &Namespace { + self.element.namespace() + } +} + +impl<'le> PartialEq for ServoLayoutElement<'le> { + fn eq(&self, other: &Self) -> bool { + self.as_node() == other.as_node() + } +} + +impl<'le> Hash for ServoLayoutElement<'le> { + fn hash<H: Hasher>(&self, state: &mut H) { + self.element.hash(state); + } +} + +impl<'le> Eq for ServoLayoutElement<'le> {} + +impl<'le> ServoLayoutElement<'le> { + fn from_layout_js(el: LayoutDom<Element>) -> ServoLayoutElement<'le> { + ServoLayoutElement { + element: el, + chain: PhantomData, + } + } + + #[inline] + fn get_attr_enum(&self, namespace: &Namespace, name: &LocalName) -> Option<&AttrValue> { + unsafe { (*self.element.unsafe_get()).get_attr_for_layout(namespace, name) } + } + + #[inline] + fn get_attr(&self, namespace: &Namespace, name: &LocalName) -> Option<&str> { + unsafe { (*self.element.unsafe_get()).get_attr_val_for_layout(namespace, name) } + } + + fn get_style_data(&self) -> Option<&StyleData> { + unsafe { + self.get_style_and_layout_data() + .map(|d| &*(d.ptr.as_ptr() as *mut StyleData)) + } + } + + pub unsafe fn unset_snapshot_flags(&self) { + self.as_node() + .node + .set_flag(NodeFlags::HAS_SNAPSHOT | NodeFlags::HANDLED_SNAPSHOT, false); + } + + pub unsafe fn set_has_snapshot(&self) { + self.as_node().node.set_flag(NodeFlags::HAS_SNAPSHOT, true); + } + + pub unsafe fn note_dirty_descendant(&self) { + use selectors::Element; + + let mut current = Some(*self); + while let Some(el) = current { + // FIXME(bholley): Ideally we'd have the invariant that any element + // with has_dirty_descendants also has the bit set on all its + // ancestors. However, there are currently some corner-cases where + // we get that wrong. I have in-flight patches to fix all this + // stuff up, so we just always propagate this bit for now. + el.set_dirty_descendants(); + current = el.parent_element(); + } + } +} + +fn as_element<'le>(node: LayoutDom<Node>) -> Option<ServoLayoutElement<'le>> { + node.downcast().map(ServoLayoutElement::from_layout_js) +} + +impl<'le> ::selectors::Element for ServoLayoutElement<'le> { + type Impl = SelectorImpl; + + fn opaque(&self) -> ::selectors::OpaqueElement { + ::selectors::OpaqueElement::new(unsafe { &*(self.as_node().opaque().0 as *const ()) }) + } + + fn parent_element(&self) -> Option<ServoLayoutElement<'le>> { + unsafe { + self.element + .upcast() + .composed_parent_node_ref() + .and_then(as_element) + } + } + + fn parent_node_is_shadow_root(&self) -> bool { + match self.as_node().parent_node() { + None => false, + Some(node) => { + node.script_type_id() == + NodeTypeId::DocumentFragment(DocumentFragmentTypeId::ShadowRoot) + }, + } + } + + fn containing_shadow_host(&self) -> Option<Self> { + self.containing_shadow().map(|s| s.host()) + } + + fn prev_sibling_element(&self) -> Option<ServoLayoutElement<'le>> { + let mut node = self.as_node(); + while let Some(sibling) = node.prev_sibling() { + if let Some(element) = sibling.as_element() { + return Some(element); + } + node = sibling; + } + None + } + + fn next_sibling_element(&self) -> Option<ServoLayoutElement<'le>> { + let mut node = self.as_node(); + while let Some(sibling) = node.next_sibling() { + if let Some(element) = sibling.as_element() { + return Some(element); + } + node = sibling; + } + None + } + + fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&String>, + ) -> bool { + match *ns { + NamespaceConstraint::Specific(ref ns) => self + .get_attr_enum(ns, local_name) + .map_or(false, |value| value.eval_selector(operation)), + NamespaceConstraint::Any => { + let values = + unsafe { (*self.element.unsafe_get()).get_attr_vals_for_layout(local_name) }; + values.iter().any(|value| value.eval_selector(operation)) + }, + } + } + + fn is_root(&self) -> bool { + match self.as_node().parent_node() { + None => false, + Some(node) => match node.script_type_id() { + NodeTypeId::Document(_) => true, + _ => false, + }, + } + } + + fn is_empty(&self) -> bool { + self.as_node() + .dom_children() + .all(|node| match node.script_type_id() { + NodeTypeId::Element(..) => false, + NodeTypeId::CharacterData(CharacterDataTypeId::Text(TextTypeId::Text)) => unsafe { + node.node.downcast().unwrap().data_for_layout().is_empty() + }, + _ => true, + }) + } + + #[inline] + fn has_local_name(&self, name: &LocalName) -> bool { + self.element.local_name() == name + } + + #[inline] + fn has_namespace(&self, ns: &Namespace) -> bool { + self.element.namespace() == ns + } + + #[inline] + fn is_same_type(&self, other: &Self) -> bool { + self.element.local_name() == other.element.local_name() && + self.element.namespace() == other.element.namespace() + } + + fn is_pseudo_element(&self) -> bool { + false + } + + fn match_pseudo_element( + &self, + _pseudo: &PseudoElement, + _context: &mut MatchingContext<Self::Impl>, + ) -> bool { + false + } + + fn match_non_ts_pseudo_class<F>( + &self, + pseudo_class: &NonTSPseudoClass, + _: &mut MatchingContext<Self::Impl>, + _: &mut F, + ) -> bool + where + F: FnMut(&Self, ElementSelectorFlags), + { + match *pseudo_class { + // https://github.com/servo/servo/issues/8718 + NonTSPseudoClass::Link | NonTSPseudoClass::AnyLink => self.is_link(), + NonTSPseudoClass::Visited => false, + + NonTSPseudoClass::Lang(ref lang) => self.match_element_lang(None, &*lang), + + NonTSPseudoClass::ServoNonZeroBorder => unsafe { + match (*self.element.unsafe_get()) + .get_attr_for_layout(&ns!(), &local_name!("border")) + { + None | Some(&AttrValue::UInt(_, 0)) => false, + _ => true, + } + }, + NonTSPseudoClass::ReadOnly => !self + .element + .get_state_for_layout() + .contains(pseudo_class.state_flag()), + + NonTSPseudoClass::Active | + NonTSPseudoClass::Focus | + NonTSPseudoClass::Fullscreen | + NonTSPseudoClass::Hover | + NonTSPseudoClass::Enabled | + NonTSPseudoClass::Disabled | + NonTSPseudoClass::Checked | + NonTSPseudoClass::Indeterminate | + NonTSPseudoClass::ReadWrite | + NonTSPseudoClass::PlaceholderShown | + NonTSPseudoClass::Target => self + .element + .get_state_for_layout() + .contains(pseudo_class.state_flag()), + } + } + + #[inline] + fn is_link(&self) -> bool { + unsafe { + match self.as_node().script_type_id() { + // https://html.spec.whatwg.org/multipage/#selector-link + NodeTypeId::Element(ElementTypeId::HTMLElement( + HTMLElementTypeId::HTMLAnchorElement, + )) | + NodeTypeId::Element(ElementTypeId::HTMLElement( + HTMLElementTypeId::HTMLAreaElement, + )) | + NodeTypeId::Element(ElementTypeId::HTMLElement( + HTMLElementTypeId::HTMLLinkElement, + )) => (*self.element.unsafe_get()) + .get_attr_val_for_layout(&ns!(), &local_name!("href")) + .is_some(), + _ => false, + } + } + } + + #[inline] + fn has_id(&self, id: &Atom, case_sensitivity: CaseSensitivity) -> bool { + unsafe { + (*self.element.id_attribute()) + .as_ref() + .map_or(false, |atom| case_sensitivity.eq_atom(atom, id)) + } + } + + #[inline] + fn is_part(&self, _name: &Atom) -> bool { + false + } + + #[inline] + fn has_class(&self, name: &Atom, case_sensitivity: CaseSensitivity) -> bool { + unsafe { self.element.has_class_for_layout(name, case_sensitivity) } + } + + fn is_html_slot_element(&self) -> bool { + unsafe { self.element.is_html_element() && self.local_name() == &local_name!("slot") } + } + + fn is_html_element_in_html_document(&self) -> bool { + unsafe { + if !self.element.is_html_element() { + return false; + } + } + + self.as_node().owner_doc().is_html_document() + } +} + +#[derive(Clone, Copy, Debug)] +pub struct ServoThreadSafeLayoutNode<'ln> { + /// The wrapped node. + node: ServoLayoutNode<'ln>, + + /// The pseudo-element type, with (optionally) + /// a specified display value to override the stylesheet. + pseudo: PseudoElementType, +} + +impl<'a> PartialEq for ServoThreadSafeLayoutNode<'a> { + #[inline] + fn eq(&self, other: &ServoThreadSafeLayoutNode<'a>) -> bool { + self.node == other.node + } +} + +impl<'ln> DangerousThreadSafeLayoutNode for ServoThreadSafeLayoutNode<'ln> { + unsafe fn dangerous_first_child(&self) -> Option<Self> { + self.get_jsmanaged() + .first_child_ref() + .map(|node| self.new_with_this_lifetime(&node)) + } + unsafe fn dangerous_next_sibling(&self) -> Option<Self> { + self.get_jsmanaged() + .next_sibling_ref() + .map(|node| self.new_with_this_lifetime(&node)) + } +} + +impl<'ln> ServoThreadSafeLayoutNode<'ln> { + /// Creates a new layout node with the same lifetime as this layout node. + pub unsafe fn new_with_this_lifetime( + &self, + node: &LayoutDom<Node>, + ) -> ServoThreadSafeLayoutNode<'ln> { + ServoThreadSafeLayoutNode { + node: self.node.new_with_this_lifetime(node), + pseudo: PseudoElementType::Normal, + } + } + + /// Creates a new `ServoThreadSafeLayoutNode` from the given `ServoLayoutNode`. + pub fn new<'a>(node: &ServoLayoutNode<'a>) -> ServoThreadSafeLayoutNode<'a> { + ServoThreadSafeLayoutNode { + node: node.clone(), + pseudo: PseudoElementType::Normal, + } + } + + /// Returns the interior of this node as a `LayoutDom`. This is highly unsafe for layout to + /// call and as such is marked `unsafe`. + unsafe fn get_jsmanaged(&self) -> &LayoutDom<Node> { + self.node.get_jsmanaged() + } +} + +impl<'ln> NodeInfo for ServoThreadSafeLayoutNode<'ln> { + fn is_element(&self) -> bool { + self.node.is_element() + } + + fn is_text_node(&self) -> bool { + self.node.is_text_node() + } +} + +impl<'ln> ThreadSafeLayoutNode for ServoThreadSafeLayoutNode<'ln> { + type ConcreteNode = ServoLayoutNode<'ln>; + type ConcreteThreadSafeLayoutElement = ServoThreadSafeLayoutElement<'ln>; + type ConcreteElement = ServoLayoutElement<'ln>; + type ChildrenIterator = ThreadSafeLayoutNodeChildrenIterator<Self>; + + fn opaque(&self) -> OpaqueNode { + unsafe { self.get_jsmanaged().opaque() } + } + + fn type_id(&self) -> Option<LayoutNodeType> { + if self.pseudo == PseudoElementType::Normal { + Some(self.node.type_id()) + } else { + None + } + } + + fn parent_style(&self) -> Arc<ComputedValues> { + let parent = self.node.parent_node().unwrap().as_element().unwrap(); + let parent_data = parent.get_data().unwrap().borrow(); + parent_data.styles.primary().clone() + } + + fn debug_id(self) -> usize { + self.node.debug_id() + } + + fn children(&self) -> LayoutIterator<Self::ChildrenIterator> { + if let Some(shadow) = self.node.as_element().and_then(|e| e.shadow_root()) { + return LayoutIterator(ThreadSafeLayoutNodeChildrenIterator::new( + shadow.as_node().to_threadsafe(), + )); + } + LayoutIterator(ThreadSafeLayoutNodeChildrenIterator::new(*self)) + } + + fn as_element(&self) -> Option<ServoThreadSafeLayoutElement<'ln>> { + self.node + .as_element() + .map(|el| ServoThreadSafeLayoutElement { + element: el, + pseudo: self.pseudo, + }) + } + + fn get_style_and_layout_data(&self) -> Option<OpaqueStyleAndLayoutData> { + self.node.get_style_and_layout_data() + } + + fn is_ignorable_whitespace(&self, context: &SharedStyleContext) -> bool { + unsafe { + let text: LayoutDom<Text> = match self.get_jsmanaged().downcast() { + Some(text) => text, + None => return false, + }; + + if !is_whitespace(text.upcast().data_for_layout()) { + return false; + } + + // NB: See the rules for `white-space` here: + // + // http://www.w3.org/TR/CSS21/text.html#propdef-white-space + // + // If you implement other values for this property, you will almost certainly + // want to update this check. + !self + .style(context) + .get_inherited_text() + .white_space + .preserve_newlines() + } + } + + unsafe fn unsafe_get(self) -> Self::ConcreteNode { + self.node + } + + fn node_text_content(&self) -> String { + let this = unsafe { self.get_jsmanaged() }; + return this.text_content(); + } + + fn selection(&self) -> Option<Range<ByteIndex>> { + let this = unsafe { self.get_jsmanaged() }; + + this.selection().map(|range| { + Range::new( + ByteIndex(range.start as isize), + ByteIndex(range.len() as isize), + ) + }) + } + + fn image_url(&self) -> Option<ServoUrl> { + let this = unsafe { self.get_jsmanaged() }; + this.image_url() + } + + fn image_density(&self) -> Option<f64> { + let this = unsafe { self.get_jsmanaged() }; + this.image_density() + } + + fn image_data(&self) -> Option<(Option<StdArc<Image>>, Option<ImageMetadata>)> { + let this = unsafe { self.get_jsmanaged() }; + this.image_data() + } + + fn canvas_data(&self) -> Option<HTMLCanvasData> { + let this = unsafe { self.get_jsmanaged() }; + this.canvas_data() + } + + fn media_data(&self) -> Option<HTMLMediaData> { + let this = unsafe { self.get_jsmanaged() }; + this.media_data() + } + + fn svg_data(&self) -> Option<SVGSVGData> { + let this = unsafe { self.get_jsmanaged() }; + this.svg_data() + } + + // Can return None if the iframe has no nested browsing context + fn iframe_browsing_context_id(&self) -> Option<BrowsingContextId> { + let this = unsafe { self.get_jsmanaged() }; + this.iframe_browsing_context_id() + } + + // Can return None if the iframe has no nested browsing context + fn iframe_pipeline_id(&self) -> Option<PipelineId> { + let this = unsafe { self.get_jsmanaged() }; + this.iframe_pipeline_id() + } + + fn get_colspan(&self) -> u32 { + unsafe { + self.get_jsmanaged() + .downcast::<Element>() + .unwrap() + .get_colspan() + } + } + + fn get_rowspan(&self) -> u32 { + unsafe { + self.get_jsmanaged() + .downcast::<Element>() + .unwrap() + .get_rowspan() + } + } +} + +pub struct ThreadSafeLayoutNodeChildrenIterator<ConcreteNode: ThreadSafeLayoutNode> { + current_node: Option<ConcreteNode>, + parent_node: ConcreteNode, +} + +impl<ConcreteNode> ThreadSafeLayoutNodeChildrenIterator<ConcreteNode> +where + ConcreteNode: DangerousThreadSafeLayoutNode, +{ + pub fn new(parent: ConcreteNode) -> Self { + let first_child: Option<ConcreteNode> = match parent.get_pseudo_element_type() { + PseudoElementType::Normal => parent + .get_before_pseudo() + .or_else(|| parent.get_details_summary_pseudo()) + .or_else(|| unsafe { parent.dangerous_first_child() }), + PseudoElementType::DetailsContent | PseudoElementType::DetailsSummary => unsafe { + parent.dangerous_first_child() + }, + _ => None, + }; + ThreadSafeLayoutNodeChildrenIterator { + current_node: first_child, + parent_node: parent, + } + } +} + +impl<ConcreteNode> Iterator for ThreadSafeLayoutNodeChildrenIterator<ConcreteNode> +where + ConcreteNode: DangerousThreadSafeLayoutNode, +{ + type Item = ConcreteNode; + fn next(&mut self) -> Option<ConcreteNode> { + use selectors::Element; + match self.parent_node.get_pseudo_element_type() { + PseudoElementType::Before | PseudoElementType::After => None, + + PseudoElementType::DetailsSummary => { + let mut current_node = self.current_node.clone(); + loop { + let next_node = if let Some(ref node) = current_node { + if let Some(element) = node.as_element() { + if element.has_local_name(&local_name!("summary")) && + element.has_namespace(&ns!(html)) + { + self.current_node = None; + return Some(node.clone()); + } + } + unsafe { node.dangerous_next_sibling() } + } else { + self.current_node = None; + return None; + }; + current_node = next_node; + } + }, + + PseudoElementType::DetailsContent => { + let node = self.current_node.clone(); + let node = node.and_then(|node| { + if node.is_element() && + node.as_element() + .unwrap() + .has_local_name(&local_name!("summary")) && + node.as_element().unwrap().has_namespace(&ns!(html)) + { + unsafe { node.dangerous_next_sibling() } + } else { + Some(node) + } + }); + self.current_node = node.and_then(|node| unsafe { node.dangerous_next_sibling() }); + node + }, + + PseudoElementType::Normal => { + let node = self.current_node.clone(); + if let Some(ref node) = node { + self.current_node = match node.get_pseudo_element_type() { + PseudoElementType::Before => self + .parent_node + .get_details_summary_pseudo() + .or_else(|| unsafe { self.parent_node.dangerous_first_child() }) + .or_else(|| self.parent_node.get_after_pseudo()), + PseudoElementType::Normal => unsafe { node.dangerous_next_sibling() } + .or_else(|| self.parent_node.get_after_pseudo()), + PseudoElementType::DetailsSummary => { + self.parent_node.get_details_content_pseudo() + }, + PseudoElementType::DetailsContent => self.parent_node.get_after_pseudo(), + PseudoElementType::After => None, + }; + } + node + }, + } + } +} + +/// A wrapper around elements that ensures layout can only +/// ever access safe properties and cannot race on elements. +#[derive(Clone, Copy, Debug)] +pub struct ServoThreadSafeLayoutElement<'le> { + element: ServoLayoutElement<'le>, + + /// The pseudo-element type, with (optionally) + /// a specified display value to override the stylesheet. + pseudo: PseudoElementType, +} + +impl<'le> ThreadSafeLayoutElement for ServoThreadSafeLayoutElement<'le> { + type ConcreteThreadSafeLayoutNode = ServoThreadSafeLayoutNode<'le>; + type ConcreteElement = ServoLayoutElement<'le>; + + fn as_node(&self) -> ServoThreadSafeLayoutNode<'le> { + ServoThreadSafeLayoutNode { + node: self.element.as_node(), + pseudo: self.pseudo.clone(), + } + } + + fn get_pseudo_element_type(&self) -> PseudoElementType { + self.pseudo + } + + fn with_pseudo(&self, pseudo: PseudoElementType) -> Self { + ServoThreadSafeLayoutElement { + element: self.element.clone(), + pseudo, + } + } + + fn type_id(&self) -> Option<LayoutNodeType> { + self.as_node().type_id() + } + + unsafe fn unsafe_get(self) -> ServoLayoutElement<'le> { + self.element + } + + fn get_attr_enum(&self, namespace: &Namespace, name: &LocalName) -> Option<&AttrValue> { + self.element.get_attr_enum(namespace, name) + } + + fn get_attr<'a>(&'a self, namespace: &Namespace, name: &LocalName) -> Option<&'a str> { + self.element.get_attr(namespace, name) + } + + fn style_data(&self) -> AtomicRef<ElementData> { + self.element + .get_data() + .expect("Unstyled layout node?") + .borrow() + } + + fn is_shadow_host(&self) -> bool { + self.element.shadow_root().is_some() + } +} + +/// This implementation of `::selectors::Element` is used for implementing lazy +/// pseudo-elements. +/// +/// Lazy pseudo-elements in Servo only allows selectors using safe properties, +/// i.e., local_name, attributes, so they can only be used for **private** +/// pseudo-elements (like `::-servo-details-content`). +/// +/// Probably a few more of this functions can be implemented (like `has_class`, etc.), +/// but they have no use right now. +/// +/// Note that the element implementation is needed only for selector matching, +/// not for inheritance (styles are inherited appropiately). +impl<'le> ::selectors::Element for ServoThreadSafeLayoutElement<'le> { + type Impl = SelectorImpl; + + fn opaque(&self) -> ::selectors::OpaqueElement { + ::selectors::OpaqueElement::new(unsafe { &*(self.as_node().opaque().0 as *const ()) }) + } + + fn is_pseudo_element(&self) -> bool { + false + } + + fn parent_element(&self) -> Option<Self> { + warn!("ServoThreadSafeLayoutElement::parent_element called"); + None + } + + fn parent_node_is_shadow_root(&self) -> bool { + false + } + + fn containing_shadow_host(&self) -> Option<Self> { + None + } + + // Skips non-element nodes + fn prev_sibling_element(&self) -> Option<Self> { + warn!("ServoThreadSafeLayoutElement::prev_sibling_element called"); + None + } + + // Skips non-element nodes + fn next_sibling_element(&self) -> Option<Self> { + warn!("ServoThreadSafeLayoutElement::next_sibling_element called"); + None + } + + fn is_html_slot_element(&self) -> bool { + self.element.is_html_slot_element() + } + + fn is_html_element_in_html_document(&self) -> bool { + debug!("ServoThreadSafeLayoutElement::is_html_element_in_html_document called"); + true + } + + #[inline] + fn has_local_name(&self, name: &LocalName) -> bool { + self.element.local_name() == name + } + + #[inline] + fn has_namespace(&self, ns: &Namespace) -> bool { + self.element.namespace() == ns + } + + #[inline] + fn is_same_type(&self, other: &Self) -> bool { + self.element.local_name() == other.element.local_name() && + self.element.namespace() == other.element.namespace() + } + + fn match_pseudo_element( + &self, + _pseudo: &PseudoElement, + _context: &mut MatchingContext<Self::Impl>, + ) -> bool { + false + } + + fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&String>, + ) -> bool { + match *ns { + NamespaceConstraint::Specific(ref ns) => self + .get_attr_enum(ns, local_name) + .map_or(false, |value| value.eval_selector(operation)), + NamespaceConstraint::Any => { + let values = unsafe { + (*self.element.element.unsafe_get()).get_attr_vals_for_layout(local_name) + }; + values.iter().any(|v| v.eval_selector(operation)) + }, + } + } + + fn match_non_ts_pseudo_class<F>( + &self, + _: &NonTSPseudoClass, + _: &mut MatchingContext<Self::Impl>, + _: &mut F, + ) -> bool + where + F: FnMut(&Self, ElementSelectorFlags), + { + // NB: This could maybe be implemented + warn!("ServoThreadSafeLayoutElement::match_non_ts_pseudo_class called"); + false + } + + fn is_link(&self) -> bool { + warn!("ServoThreadSafeLayoutElement::is_link called"); + false + } + + fn has_id(&self, _id: &Atom, _case_sensitivity: CaseSensitivity) -> bool { + debug!("ServoThreadSafeLayoutElement::has_id called"); + false + } + + #[inline] + fn is_part(&self, _name: &Atom) -> bool { + debug!("ServoThreadSafeLayoutElement::is_part called"); + false + } + + fn has_class(&self, _name: &Atom, _case_sensitivity: CaseSensitivity) -> bool { + debug!("ServoThreadSafeLayoutElement::has_class called"); + false + } + + fn is_empty(&self) -> bool { + warn!("ServoThreadSafeLayoutElement::is_empty called"); + false + } + + fn is_root(&self) -> bool { + warn!("ServoThreadSafeLayoutElement::is_root called"); + false + } +} diff --git a/components/layout_thread_2020/lib.rs b/components/layout_thread_2020/lib.rs index b5bbe316dcc..4f5018e0a98 100644 --- a/components/layout_thread_2020/lib.rs +++ b/components/layout_thread_2020/lib.rs @@ -2,42 +2,310 @@ * 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/. */ -use crossbeam_channel::{Receiver, Sender}; -use euclid::Size2D; +// Work around https://github.com/rust-lang/rust/issues/62132 +#![recursion_limit = "128"] + +//! The layout thread. Performs layout on the DOM, builds display lists and sends them to be +//! painted. + +#[macro_use] +extern crate crossbeam_channel; +#[macro_use] +extern crate html5ever; +#[macro_use] +extern crate layout; +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate log; +#[macro_use] +extern crate profile_traits; + +mod dom_wrapper; + +use crate::dom_wrapper::drop_style_and_layout_data; +use crate::dom_wrapper::{ServoLayoutDocument, ServoLayoutElement, ServoLayoutNode}; +use app_units::Au; +use crossbeam_channel::{unbounded, Receiver, Sender}; +use embedder_traits::resources::{self, Resource}; +use euclid::{Point2D, Rect, Size2D, TypedScale, TypedSize2D}; +use fnv::FnvHashMap; +use fxhash::{FxHashMap, FxHashSet}; +use gfx::font; use gfx::font_cache_thread::FontCacheThread; -use ipc_channel::ipc::{IpcReceiver, IpcSender}; -use metrics::PaintTimeMetrics; -use msg::constellation_msg::TopLevelBrowsingContextId; -use msg::constellation_msg::{BackgroundHangMonitorRegister, PipelineId}; -use net_traits::image_cache::ImageCache; -use profile_traits::{mem, time}; -use script_traits::LayoutMsg as ConstellationMsg; -use script_traits::{ConstellationControlMsg, LayoutControlMsg}; -use servo_geometry::DeviceIndependentPixel; +use gfx::font_context; +use gfx_traits::{node_id_from_scroll_id, Epoch}; +use histogram::Histogram; +use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; +use ipc_channel::router::ROUTER; +use layout::animation; +use layout::construct::ConstructionResult; +use layout::context::malloc_size_of_persistent_local_context; +use layout::context::LayoutContext; +use layout::context::RegisteredPainter; +use layout::context::RegisteredPainters; +use layout::display_list::items::{OpaqueNode, WebRenderImageInfo}; +use layout::display_list::{IndexableText, ToLayout, WebRenderDisplayListConverter}; +use layout::flow::{Flow, GetBaseFlow, ImmutableFlowUtils, MutableOwnedFlowUtils}; +use layout::flow_ref::FlowRef; +use layout::incremental::{RelayoutMode, SpecialRestyleDamage}; +use layout::layout_debug; +use layout::parallel; +use layout::query::{ + process_content_box_request, process_content_boxes_request, LayoutRPCImpl, LayoutThreadData, +}; +use layout::query::{process_element_inner_text_query, process_node_geometry_request}; +use layout::query::{process_node_scroll_area_request, process_node_scroll_id_request}; +use layout::query::{ + process_offset_parent_query, process_resolved_style_request, process_style_query, +}; +use layout::sequential; +use layout::traversal::{ + ComputeStackingRelativePositions, PreorderFlowTraversal, RecalcStyleAndConstructFlows, +}; +use layout::wrapper::LayoutNodeLayoutData; +use layout_traits::LayoutThreadFactory; +use libc::c_void; +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use metrics::{PaintTimeMetrics, ProfilerMetadataFactory, ProgressiveWebMetric}; +use msg::constellation_msg::{ + BackgroundHangMonitor, BackgroundHangMonitorRegister, HangAnnotation, +}; +use msg::constellation_msg::{BrowsingContextId, MonitoredComponentId, TopLevelBrowsingContextId}; +use msg::constellation_msg::{LayoutHangAnnotation, MonitoredComponentType, PipelineId}; +use net_traits::image_cache::{ImageCache, UsePlaceholder}; +use parking_lot::RwLock; +use profile_traits::mem::{self as profile_mem, Report, ReportKind, ReportsChan}; +use profile_traits::time::{self as profile_time, profile, TimerMetadata}; +use profile_traits::time::{TimerMetadataFrameType, TimerMetadataReflowType}; +use script_layout_interface::message::{LayoutThreadInit, Msg, NodesFromPointQueryType, Reflow}; +use script_layout_interface::message::{QueryMsg, ReflowComplete, ReflowGoal, ScriptReflow}; +use script_layout_interface::rpc::TextIndexResponse; +use script_layout_interface::rpc::{LayoutRPC, OffsetParentResponse, StyleResponse}; +use script_layout_interface::wrapper_traits::LayoutNode; +use script_traits::Painter; +use script_traits::{ConstellationControlMsg, LayoutControlMsg, LayoutMsg as ConstellationMsg}; +use script_traits::{DrawAPaintImageResult, IFrameSizeMsg, PaintWorkletError, WindowSizeType}; +use script_traits::{ScrollState, UntrustedNodeAddress}; +use selectors::Element; +use servo_arc::Arc as ServoArc; +use servo_atoms::Atom; +use servo_config::opts; +use servo_config::pref; +use servo_geometry::{DeviceIndependentPixel, MaxRect}; use servo_url::ServoUrl; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; +use std::borrow::ToOwned; +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; +use std::process; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::thread; +use std::time::Duration; +use style::animation::Animation; +use style::context::{QuirksMode, RegisteredSpeculativePainter, RegisteredSpeculativePainters}; +use style::context::{SharedStyleContext, ThreadLocalStyleContextCreationInfo}; +use style::dom::{ShowSubtree, ShowSubtreeDataAndPrimaryValues, TDocument, TElement, TNode}; +use style::driver; +use style::error_reporting::RustLogReporter; +use style::global_style_data::{GLOBAL_STYLE_DATA, STYLE_THREAD_POOL}; +use style::invalidation::element::restyle_hints::RestyleHint; +use style::logical_geometry::LogicalPoint; +use style::media_queries::{Device, MediaList, MediaType}; +use style::properties::PropertyId; +use style::selector_parser::SnapshotMap; +use style::servo::restyle_damage::ServoRestyleDamage; +use style::shared_lock::{SharedRwLock, SharedRwLockReadGuard, StylesheetGuards}; +use style::stylesheets::{ + DocumentStyleSheet, Origin, Stylesheet, StylesheetInDocument, UserAgentStylesheets, +}; +use style::stylist::Stylist; +use style::thread_state::{self, ThreadState}; +use style::timer::Timer; +use style::traversal::DomTraversal; +use style::traversal_flags::TraversalFlags; +use style_traits::CSSPixel; +use style_traits::DevicePixel; +use style_traits::SpeculativePainter; + +/// Information needed by the layout thread. +pub struct LayoutThread { + /// The ID of the pipeline that we belong to. + id: PipelineId, + + /// The ID of the top-level browsing context that we belong to. + top_level_browsing_context_id: TopLevelBrowsingContextId, + + /// The URL of the pipeline that we belong to. + url: ServoUrl, + + /// Performs CSS selector matching and style resolution. + stylist: Stylist, + + /// Is the current reflow of an iframe, as opposed to a root window? + is_iframe: bool, + + /// The port on which we receive messages from the script thread. + port: Receiver<Msg>, + + /// The port on which we receive messages from the constellation. + pipeline_port: Receiver<LayoutControlMsg>, + + /// The port on which we receive messages from the font cache thread. + font_cache_receiver: Receiver<()>, + + /// The channel on which the font cache can send messages to us. + font_cache_sender: IpcSender<()>, + + /// A means of communication with the background hang monitor. + background_hang_monitor: Box<dyn BackgroundHangMonitor>, + + /// The channel on which messages can be sent to the constellation. + constellation_chan: IpcSender<ConstellationMsg>, + + /// The channel on which messages can be sent to the script thread. + script_chan: IpcSender<ConstellationControlMsg>, + + /// The channel on which messages can be sent to the time profiler. + time_profiler_chan: profile_time::ProfilerChan, + + /// The channel on which messages can be sent to the memory profiler. + mem_profiler_chan: profile_mem::ProfilerChan, + + /// Reference to the script thread image cache. + image_cache: Arc<dyn ImageCache>, + + /// Public interface to the font cache thread. + font_cache_thread: FontCacheThread, + + /// Is this the first reflow in this LayoutThread? + first_reflow: Cell<bool>, + + /// Flag to indicate whether to use parallel operations + parallel_flag: bool, + + /// Starts at zero, and increased by one every time a layout completes. + /// This can be used to easily check for invalid stale data. + generation: Cell<u32>, + + /// A channel on which new animations that have been triggered by style recalculation can be + /// sent. + new_animations_sender: Sender<Animation>, + + /// Receives newly-discovered animations. + new_animations_receiver: Receiver<Animation>, + + /// The number of Web fonts that have been requested but not yet loaded. + outstanding_web_fonts: Arc<AtomicUsize>, + + /// The root of the flow tree. + root_flow: RefCell<Option<FlowRef>>, + + /// The document-specific shared lock used for author-origin stylesheets + document_shared_lock: Option<SharedRwLock>, + + /// The list of currently-running animations. + running_animations: ServoArc<RwLock<FxHashMap<OpaqueNode, Vec<Animation>>>>, + + /// The list of animations that have expired since the last style recalculation. + expired_animations: ServoArc<RwLock<FxHashMap<OpaqueNode, Vec<Animation>>>>, + + /// A counter for epoch messages + epoch: Cell<Epoch>, + + /// The size of the viewport. This may be different from the size of the screen due to viewport + /// constraints. + viewport_size: Size2D<Au>, + + /// A mutex to allow for fast, read-only RPC of layout's internal data + /// structures, while still letting the LayoutThread modify them. + /// + /// All the other elements of this struct are read-only. + rw_data: Arc<Mutex<LayoutThreadData>>, + + webrender_image_cache: Arc<RwLock<FnvHashMap<(ServoUrl, UsePlaceholder), WebRenderImageInfo>>>, + + /// The executors for paint worklets. + registered_painters: RegisteredPaintersImpl, + + /// Webrender interface. + webrender_api: webrender_api::RenderApi, + + /// Webrender document. + webrender_document: webrender_api::DocumentId, + + /// The timer object to control the timing of the animations. This should + /// only be a test-mode timer during testing for animations. + timer: Timer, + + /// Paint time metrics. + paint_time_metrics: PaintTimeMetrics, + + /// The time a layout query has waited before serviced by layout thread. + layout_query_waiting_time: Histogram, -pub struct LayoutThread; + /// The sizes of all iframes encountered during the last layout operation. + last_iframe_sizes: RefCell<HashMap<BrowsingContextId, TypedSize2D<f32, CSSPixel>>>, -impl layout_traits::LayoutThreadFactory for LayoutThread { - type Message = script_layout_interface::message::Msg; + /// Flag that indicates if LayoutThread is busy handling a request. + busy: Arc<AtomicBool>, - #[allow(unused)] + /// Load web fonts synchronously to avoid non-deterministic network-driven reflows. + load_webfonts_synchronously: bool, + + /// The initial request size of the window + initial_window_size: TypedSize2D<u32, DeviceIndependentPixel>, + + /// The ratio of device pixels per px at the default scale. + /// If unspecified, will use the platform default setting. + device_pixels_per_px: Option<f32>, + + /// Dumps the display list form after a layout. + dump_display_list: bool, + + /// Dumps the display list in JSON form after a layout. + dump_display_list_json: bool, + + /// Dumps the DOM after restyle. + dump_style_tree: bool, + + /// Dumps the flow tree after a layout. + dump_rule_tree: bool, + + /// Emits notifications when there is a relayout. + relayout_event: bool, + + /// True to turn off incremental layout. + nonincremental_layout: bool, + + /// True if each step of layout is traced to an external JSON file + /// for debugging purposes. Setting this implies sequential layout + /// and paint. + trace_layout: bool, + + /// Dumps the flow tree after a layout. + dump_flow_tree: bool, +} + +impl LayoutThreadFactory for LayoutThread { + type Message = Msg; + + /// Spawns a new layout thread. fn create( id: PipelineId, top_level_browsing_context_id: TopLevelBrowsingContextId, url: ServoUrl, is_iframe: bool, - chan: (Sender<Self::Message>, Receiver<Self::Message>), + chan: (Sender<Msg>, Receiver<Msg>), pipeline_port: IpcReceiver<LayoutControlMsg>, - background_hang_monitor: Box<dyn BackgroundHangMonitorRegister>, + background_hang_monitor_register: Box<dyn BackgroundHangMonitorRegister>, constellation_chan: IpcSender<ConstellationMsg>, script_chan: IpcSender<ConstellationControlMsg>, image_cache: Arc<dyn ImageCache>, font_cache_thread: FontCacheThread, - time_profiler_chan: time::ProfilerChan, - mem_profiler_chan: mem::ProfilerChan, + time_profiler_chan: profile_time::ProfilerChan, + mem_profiler_chan: profile_mem::ProfilerChan, content_process_shutdown_chan: Option<IpcSender<()>>, webrender_api_sender: webrender_api::RenderApiSender, webrender_document: webrender_api::DocumentId, @@ -55,5 +323,1843 @@ impl layout_traits::LayoutThreadFactory for LayoutThread { trace_layout: bool, dump_flow_tree: bool, ) { + thread::Builder::new() + .name(format!("LayoutThread {:?}", id)) + .spawn(move || { + thread_state::initialize(ThreadState::LAYOUT); + + // In order to get accurate crash reports, we install the top-level bc id. + TopLevelBrowsingContextId::install(top_level_browsing_context_id); + + { + // Ensures layout thread is destroyed before we send shutdown message + let sender = chan.0; + + let background_hang_monitor = background_hang_monitor_register + .register_component( + MonitoredComponentId(id, MonitoredComponentType::Layout), + Duration::from_millis(1000), + Duration::from_millis(5000), + ); + + let layout = LayoutThread::new( + id, + top_level_browsing_context_id, + url, + is_iframe, + chan.1, + pipeline_port, + background_hang_monitor, + constellation_chan, + script_chan, + image_cache.clone(), + font_cache_thread, + time_profiler_chan, + mem_profiler_chan.clone(), + webrender_api_sender, + webrender_document, + paint_time_metrics, + busy, + load_webfonts_synchronously, + initial_window_size, + device_pixels_per_px, + dump_display_list, + dump_display_list_json, + dump_style_tree, + dump_rule_tree, + relayout_event, + nonincremental_layout, + trace_layout, + dump_flow_tree, + ); + + let reporter_name = format!("layout-reporter-{}", id); + mem_profiler_chan.run_with_memory_reporting( + || { + layout.start(); + }, + reporter_name, + sender, + Msg::CollectReports, + ); + } + if let Some(content_process_shutdown_chan) = content_process_shutdown_chan { + let _ = content_process_shutdown_chan.send(()); + } + }) + .expect("Thread spawning failed"); + } +} + +struct ScriptReflowResult { + script_reflow: ScriptReflow, + result: RefCell<Option<ReflowComplete>>, +} + +impl Deref for ScriptReflowResult { + type Target = ScriptReflow; + fn deref(&self) -> &ScriptReflow { + &self.script_reflow + } +} + +impl DerefMut for ScriptReflowResult { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.script_reflow + } +} + +impl ScriptReflowResult { + fn new(script_reflow: ScriptReflow) -> ScriptReflowResult { + ScriptReflowResult { + script_reflow: script_reflow, + result: RefCell::new(Some(Default::default())), + } + } +} + +impl Drop for ScriptReflowResult { + fn drop(&mut self) { + self.script_reflow + .script_join_chan + .send(self.result.borrow_mut().take().unwrap()) + .unwrap(); + } +} + +/// The `LayoutThread` `rw_data` lock must remain locked until the first reflow, +/// as RPC calls don't make sense until then. Use this in combination with +/// `LayoutThread::lock_rw_data` and `LayoutThread::return_rw_data`. +pub enum RWGuard<'a> { + /// If the lock was previously held, from when the thread started. + Held(MutexGuard<'a, LayoutThreadData>), + /// If the lock was just used, and has been returned since there has been + /// a reflow already. + Used(MutexGuard<'a, LayoutThreadData>), +} + +impl<'a> Deref for RWGuard<'a> { + type Target = LayoutThreadData; + fn deref(&self) -> &LayoutThreadData { + match *self { + RWGuard::Held(ref x) => &**x, + RWGuard::Used(ref x) => &**x, + } + } +} + +impl<'a> DerefMut for RWGuard<'a> { + fn deref_mut(&mut self) -> &mut LayoutThreadData { + match *self { + RWGuard::Held(ref mut x) => &mut **x, + RWGuard::Used(ref mut x) => &mut **x, + } + } +} + +struct RwData<'a, 'b: 'a> { + rw_data: &'b Arc<Mutex<LayoutThreadData>>, + possibly_locked_rw_data: &'a mut Option<MutexGuard<'b, LayoutThreadData>>, +} + +impl<'a, 'b: 'a> RwData<'a, 'b> { + /// If no reflow has happened yet, this will just return the lock in + /// `possibly_locked_rw_data`. Otherwise, it will acquire the `rw_data` lock. + /// + /// If you do not wish RPCs to remain blocked, just drop the `RWGuard` + /// returned from this function. If you _do_ wish for them to remain blocked, + /// use `block`. + fn lock(&mut self) -> RWGuard<'b> { + match self.possibly_locked_rw_data.take() { + None => RWGuard::Used(self.rw_data.lock().unwrap()), + Some(x) => RWGuard::Held(x), + } + } +} + +fn add_font_face_rules( + stylesheet: &Stylesheet, + guard: &SharedRwLockReadGuard, + device: &Device, + font_cache_thread: &FontCacheThread, + font_cache_sender: &IpcSender<()>, + outstanding_web_fonts_counter: &Arc<AtomicUsize>, + load_webfonts_synchronously: bool, +) { + if load_webfonts_synchronously { + let (sender, receiver) = ipc::channel().unwrap(); + stylesheet.effective_font_face_rules(&device, guard, |rule| { + if let Some(font_face) = rule.font_face() { + let effective_sources = font_face.effective_sources(); + font_cache_thread.add_web_font( + font_face.family().clone(), + effective_sources, + sender.clone(), + ); + receiver.recv().unwrap(); + } + }) + } else { + stylesheet.effective_font_face_rules(&device, guard, |rule| { + if let Some(font_face) = rule.font_face() { + let effective_sources = font_face.effective_sources(); + outstanding_web_fonts_counter.fetch_add(1, Ordering::SeqCst); + font_cache_thread.add_web_font( + font_face.family().clone(), + effective_sources, + (*font_cache_sender).clone(), + ); + } + }) + } +} + +impl LayoutThread { + /// Creates a new `LayoutThread` structure. + fn new( + id: PipelineId, + top_level_browsing_context_id: TopLevelBrowsingContextId, + url: ServoUrl, + is_iframe: bool, + port: Receiver<Msg>, + pipeline_port: IpcReceiver<LayoutControlMsg>, + background_hang_monitor: Box<dyn BackgroundHangMonitor>, + constellation_chan: IpcSender<ConstellationMsg>, + script_chan: IpcSender<ConstellationControlMsg>, + image_cache: Arc<dyn ImageCache>, + font_cache_thread: FontCacheThread, + time_profiler_chan: profile_time::ProfilerChan, + mem_profiler_chan: profile_mem::ProfilerChan, + webrender_api_sender: webrender_api::RenderApiSender, + webrender_document: webrender_api::DocumentId, + paint_time_metrics: PaintTimeMetrics, + busy: Arc<AtomicBool>, + load_webfonts_synchronously: bool, + initial_window_size: TypedSize2D<u32, DeviceIndependentPixel>, + device_pixels_per_px: Option<f32>, + dump_display_list: bool, + dump_display_list_json: bool, + dump_style_tree: bool, + dump_rule_tree: bool, + relayout_event: bool, + nonincremental_layout: bool, + trace_layout: bool, + dump_flow_tree: bool, + ) -> LayoutThread { + // The device pixel ratio is incorrect (it does not have the hidpi value), + // but it will be set correctly when the initial reflow takes place. + let device = Device::new( + MediaType::screen(), + initial_window_size.to_f32() * TypedScale::new(1.0), + TypedScale::new(device_pixels_per_px.unwrap_or(1.0)), + ); + + // Create the channel on which new animations can be sent. + let (new_animations_sender, new_animations_receiver) = unbounded(); + + // Proxy IPC messages from the pipeline to the layout thread. + let pipeline_receiver = ROUTER.route_ipc_receiver_to_new_crossbeam_receiver(pipeline_port); + + // Ask the router to proxy IPC messages from the font cache thread to the layout thread. + let (ipc_font_cache_sender, ipc_font_cache_receiver) = ipc::channel().unwrap(); + let font_cache_receiver = + ROUTER.route_ipc_receiver_to_new_crossbeam_receiver(ipc_font_cache_receiver); + + LayoutThread { + id: id, + top_level_browsing_context_id: top_level_browsing_context_id, + url: url, + is_iframe: is_iframe, + port: port, + pipeline_port: pipeline_receiver, + script_chan: script_chan.clone(), + background_hang_monitor, + constellation_chan: constellation_chan.clone(), + time_profiler_chan: time_profiler_chan, + mem_profiler_chan: mem_profiler_chan, + registered_painters: RegisteredPaintersImpl(Default::default()), + image_cache: image_cache.clone(), + font_cache_thread: font_cache_thread, + first_reflow: Cell::new(true), + font_cache_receiver: font_cache_receiver, + font_cache_sender: ipc_font_cache_sender, + parallel_flag: true, + generation: Cell::new(0), + new_animations_sender: new_animations_sender, + new_animations_receiver: new_animations_receiver, + outstanding_web_fonts: Arc::new(AtomicUsize::new(0)), + root_flow: RefCell::new(None), + document_shared_lock: None, + running_animations: ServoArc::new(RwLock::new(Default::default())), + expired_animations: ServoArc::new(RwLock::new(Default::default())), + epoch: Cell::new(Epoch(0)), + viewport_size: Size2D::new(Au(0), Au(0)), + webrender_api: webrender_api_sender.create_api(), + webrender_document, + stylist: Stylist::new(device, QuirksMode::NoQuirks), + rw_data: Arc::new(Mutex::new(LayoutThreadData { + constellation_chan: constellation_chan, + display_list: None, + indexable_text: IndexableText::default(), + content_box_response: None, + content_boxes_response: Vec::new(), + client_rect_response: Rect::zero(), + scroll_id_response: None, + scroll_area_response: Rect::zero(), + resolved_style_response: String::new(), + offset_parent_response: OffsetParentResponse::empty(), + style_response: StyleResponse(None), + scroll_offsets: HashMap::new(), + text_index_response: TextIndexResponse(None), + nodes_from_point_response: vec![], + element_inner_text_response: String::new(), + })), + webrender_image_cache: Arc::new(RwLock::new(FnvHashMap::default())), + timer: if pref!(layout.animations.test.enabled) { + Timer::test_mode() + } else { + Timer::new() + }, + paint_time_metrics: paint_time_metrics, + layout_query_waiting_time: Histogram::new(), + last_iframe_sizes: Default::default(), + busy, + load_webfonts_synchronously, + initial_window_size, + device_pixels_per_px, + dump_display_list, + dump_display_list_json, + dump_style_tree, + dump_rule_tree, + relayout_event, + nonincremental_layout, + trace_layout, + dump_flow_tree, + } + } + + /// Starts listening on the port. + fn start(mut self) { + let rw_data = self.rw_data.clone(); + let mut possibly_locked_rw_data = Some(rw_data.lock().unwrap()); + let mut rw_data = RwData { + rw_data: &rw_data, + possibly_locked_rw_data: &mut possibly_locked_rw_data, + }; + while self.handle_request(&mut rw_data) { + // Loop indefinitely. + } + } + + // Create a layout context for use in building display lists, hit testing, &c. + fn build_layout_context<'a>( + &'a self, + guards: StylesheetGuards<'a>, + script_initiated_layout: bool, + snapshot_map: &'a SnapshotMap, + ) -> LayoutContext<'a> { + let thread_local_style_context_creation_data = + ThreadLocalStyleContextCreationInfo::new(self.new_animations_sender.clone()); + + LayoutContext { + id: self.id, + style_context: SharedStyleContext { + stylist: &self.stylist, + options: GLOBAL_STYLE_DATA.options.clone(), + guards, + visited_styles_enabled: false, + running_animations: self.running_animations.clone(), + expired_animations: self.expired_animations.clone(), + registered_speculative_painters: &self.registered_painters, + local_context_creation_data: Mutex::new(thread_local_style_context_creation_data), + timer: self.timer.clone(), + traversal_flags: TraversalFlags::empty(), + snapshot_map: snapshot_map, + }, + image_cache: self.image_cache.clone(), + font_cache_thread: Mutex::new(self.font_cache_thread.clone()), + webrender_image_cache: self.webrender_image_cache.clone(), + pending_images: if script_initiated_layout { + Some(Mutex::new(vec![])) + } else { + None + }, + newly_transitioning_nodes: if script_initiated_layout { + Some(Mutex::new(vec![])) + } else { + None + }, + registered_painters: &self.registered_painters, + } + } + + fn notify_activity_to_hang_monitor(&self, request: &Msg) { + let hang_annotation = match request { + Msg::AddStylesheet(..) => LayoutHangAnnotation::AddStylesheet, + Msg::RemoveStylesheet(..) => LayoutHangAnnotation::RemoveStylesheet, + Msg::SetQuirksMode(..) => LayoutHangAnnotation::SetQuirksMode, + Msg::Reflow(..) => LayoutHangAnnotation::Reflow, + Msg::GetRPC(..) => LayoutHangAnnotation::GetRPC, + Msg::TickAnimations => LayoutHangAnnotation::TickAnimations, + Msg::AdvanceClockMs(..) => LayoutHangAnnotation::AdvanceClockMs, + Msg::ReapStyleAndLayoutData(..) => LayoutHangAnnotation::ReapStyleAndLayoutData, + Msg::CollectReports(..) => LayoutHangAnnotation::CollectReports, + Msg::PrepareToExit(..) => LayoutHangAnnotation::PrepareToExit, + Msg::ExitNow => LayoutHangAnnotation::ExitNow, + Msg::GetCurrentEpoch(..) => LayoutHangAnnotation::GetCurrentEpoch, + Msg::GetWebFontLoadState(..) => LayoutHangAnnotation::GetWebFontLoadState, + Msg::CreateLayoutThread(..) => LayoutHangAnnotation::CreateLayoutThread, + Msg::SetFinalUrl(..) => LayoutHangAnnotation::SetFinalUrl, + Msg::SetScrollStates(..) => LayoutHangAnnotation::SetScrollStates, + Msg::UpdateScrollStateFromScript(..) => { + LayoutHangAnnotation::UpdateScrollStateFromScript + }, + Msg::RegisterPaint(..) => LayoutHangAnnotation::RegisterPaint, + Msg::SetNavigationStart(..) => LayoutHangAnnotation::SetNavigationStart, + Msg::GetRunningAnimations(..) => LayoutHangAnnotation::GetRunningAnimations, + }; + self.background_hang_monitor + .notify_activity(HangAnnotation::Layout(hang_annotation)); + } + + /// Receives and dispatches messages from the script and constellation threads + fn handle_request<'a, 'b>(&mut self, possibly_locked_rw_data: &mut RwData<'a, 'b>) -> bool { + enum Request { + FromPipeline(LayoutControlMsg), + FromScript(Msg), + FromFontCache, + } + + // Notify the background-hang-monitor we are waiting for an event. + self.background_hang_monitor.notify_wait(); + + let request = select! { + recv(self.pipeline_port) -> msg => Request::FromPipeline(msg.unwrap()), + recv(self.port) -> msg => Request::FromScript(msg.unwrap()), + recv(self.font_cache_receiver) -> msg => { msg.unwrap(); Request::FromFontCache } + }; + + self.busy.store(true, Ordering::Relaxed); + let result = match request { + Request::FromPipeline(LayoutControlMsg::SetScrollStates(new_scroll_states)) => self + .handle_request_helper( + Msg::SetScrollStates(new_scroll_states), + possibly_locked_rw_data, + ), + Request::FromPipeline(LayoutControlMsg::TickAnimations) => { + self.handle_request_helper(Msg::TickAnimations, possibly_locked_rw_data) + }, + Request::FromPipeline(LayoutControlMsg::GetCurrentEpoch(sender)) => { + self.handle_request_helper(Msg::GetCurrentEpoch(sender), possibly_locked_rw_data) + }, + Request::FromPipeline(LayoutControlMsg::GetWebFontLoadState(sender)) => self + .handle_request_helper(Msg::GetWebFontLoadState(sender), possibly_locked_rw_data), + Request::FromPipeline(LayoutControlMsg::ExitNow) => { + self.handle_request_helper(Msg::ExitNow, possibly_locked_rw_data) + }, + Request::FromPipeline(LayoutControlMsg::PaintMetric(epoch, paint_time)) => { + self.paint_time_metrics.maybe_set_metric(epoch, paint_time); + true + }, + Request::FromScript(msg) => self.handle_request_helper(msg, possibly_locked_rw_data), + Request::FromFontCache => { + let _rw_data = possibly_locked_rw_data.lock(); + self.outstanding_web_fonts.fetch_sub(1, Ordering::SeqCst); + font_context::invalidate_font_caches(); + self.script_chan + .send(ConstellationControlMsg::WebFontLoaded(self.id)) + .unwrap(); + true + }, + }; + self.busy.store(false, Ordering::Relaxed); + result + } + + /// Receives and dispatches messages from other threads. + fn handle_request_helper<'a, 'b>( + &mut self, + request: Msg, + possibly_locked_rw_data: &mut RwData<'a, 'b>, + ) -> bool { + self.notify_activity_to_hang_monitor(&request); + + match request { + Msg::AddStylesheet(stylesheet, before_stylesheet) => { + let guard = stylesheet.shared_lock.read(); + self.handle_add_stylesheet(&stylesheet, &guard); + + match before_stylesheet { + Some(insertion_point) => self.stylist.insert_stylesheet_before( + DocumentStyleSheet(stylesheet.clone()), + DocumentStyleSheet(insertion_point), + &guard, + ), + None => self + .stylist + .append_stylesheet(DocumentStyleSheet(stylesheet.clone()), &guard), + } + }, + Msg::RemoveStylesheet(stylesheet) => { + let guard = stylesheet.shared_lock.read(); + self.stylist + .remove_stylesheet(DocumentStyleSheet(stylesheet.clone()), &guard); + }, + Msg::SetQuirksMode(mode) => self.handle_set_quirks_mode(mode), + Msg::GetRPC(response_chan) => { + response_chan + .send(Box::new(LayoutRPCImpl(self.rw_data.clone())) as Box<dyn LayoutRPC + Send>) + .unwrap(); + }, + Msg::Reflow(data) => { + let mut data = ScriptReflowResult::new(data); + profile( + profile_time::ProfilerCategory::LayoutPerform, + self.profiler_metadata(), + self.time_profiler_chan.clone(), + || self.handle_reflow(&mut data, possibly_locked_rw_data), + ); + }, + Msg::TickAnimations => self.tick_all_animations(possibly_locked_rw_data), + Msg::SetScrollStates(new_scroll_states) => { + self.set_scroll_states(new_scroll_states, possibly_locked_rw_data); + }, + Msg::UpdateScrollStateFromScript(state) => { + let mut rw_data = possibly_locked_rw_data.lock(); + rw_data + .scroll_offsets + .insert(state.scroll_id, state.scroll_offset); + + let point = Point2D::new(-state.scroll_offset.x, -state.scroll_offset.y); + let mut txn = webrender_api::Transaction::new(); + txn.scroll_node_with_id( + webrender_api::units::LayoutPoint::from_untyped(&point), + state.scroll_id, + webrender_api::ScrollClamping::ToContentBounds, + ); + self.webrender_api + .send_transaction(self.webrender_document, txn); + }, + Msg::ReapStyleAndLayoutData(dead_data) => unsafe { + drop_style_and_layout_data(dead_data) + }, + Msg::CollectReports(reports_chan) => { + self.collect_reports(reports_chan, possibly_locked_rw_data); + }, + Msg::GetCurrentEpoch(sender) => { + let _rw_data = possibly_locked_rw_data.lock(); + sender.send(self.epoch.get()).unwrap(); + }, + Msg::AdvanceClockMs(how_many, do_tick) => { + self.handle_advance_clock_ms(how_many, possibly_locked_rw_data, do_tick); + }, + Msg::GetWebFontLoadState(sender) => { + let _rw_data = possibly_locked_rw_data.lock(); + let outstanding_web_fonts = self.outstanding_web_fonts.load(Ordering::SeqCst); + sender.send(outstanding_web_fonts != 0).unwrap(); + }, + Msg::CreateLayoutThread(info) => self.create_layout_thread(info), + Msg::SetFinalUrl(final_url) => { + self.url = final_url; + }, + Msg::RegisterPaint(name, mut properties, painter) => { + debug!("Registering the painter"); + let properties = properties + .drain(..) + .filter_map(|name| { + let id = PropertyId::parse_enabled_for_all_content(&*name).ok()?; + Some((name.clone(), id)) + }) + .filter(|&(_, ref id)| !id.is_shorthand()) + .collect(); + let registered_painter = RegisteredPainterImpl { + name: name.clone(), + properties, + painter, + }; + self.registered_painters.0.insert(name, registered_painter); + }, + Msg::PrepareToExit(response_chan) => { + self.prepare_to_exit(response_chan); + return false; + }, + // Receiving the Exit message at this stage only happens when layout is undergoing a "force exit". + Msg::ExitNow => { + debug!("layout: ExitNow received"); + self.exit_now(); + return false; + }, + Msg::SetNavigationStart(time) => { + self.paint_time_metrics.set_navigation_start(time); + }, + Msg::GetRunningAnimations(sender) => { + let _ = sender.send(self.running_animations.read().len()); + }, + } + + true + } + + fn collect_reports<'a, 'b>( + &self, + reports_chan: ReportsChan, + possibly_locked_rw_data: &mut RwData<'a, 'b>, + ) { + let mut reports = vec![]; + // Servo uses vanilla jemalloc, which doesn't have a + // malloc_enclosing_size_of function. + let mut ops = MallocSizeOfOps::new(servo_allocator::usable_size, None, None); + + // FIXME(njn): Just measuring the display tree for now. + let rw_data = possibly_locked_rw_data.lock(); + let display_list = rw_data.display_list.as_ref(); + let formatted_url = &format!("url({})", self.url); + reports.push(Report { + path: path![formatted_url, "layout-thread", "display-list"], + kind: ReportKind::ExplicitJemallocHeapSize, + size: display_list.map_or(0, |sc| sc.size_of(&mut ops)), + }); + + reports.push(Report { + path: path![formatted_url, "layout-thread", "stylist"], + kind: ReportKind::ExplicitJemallocHeapSize, + size: self.stylist.size_of(&mut ops), + }); + + // The LayoutThread has data in Persistent TLS... + reports.push(Report { + path: path![formatted_url, "layout-thread", "local-context"], + kind: ReportKind::ExplicitJemallocHeapSize, + size: malloc_size_of_persistent_local_context(&mut ops), + }); + + reports_chan.send(reports); + } + + fn create_layout_thread(&self, info: LayoutThreadInit) { + LayoutThread::create( + info.id, + self.top_level_browsing_context_id, + info.url.clone(), + info.is_parent, + info.layout_pair, + info.pipeline_port, + info.background_hang_monitor_register, + info.constellation_chan, + info.script_chan.clone(), + info.image_cache.clone(), + self.font_cache_thread.clone(), + self.time_profiler_chan.clone(), + self.mem_profiler_chan.clone(), + info.content_process_shutdown_chan, + self.webrender_api.clone_sender(), + self.webrender_document, + info.paint_time_metrics, + info.layout_is_busy, + self.load_webfonts_synchronously, + self.initial_window_size, + self.device_pixels_per_px, + self.dump_display_list, + self.dump_display_list_json, + self.dump_style_tree, + self.dump_rule_tree, + self.relayout_event, + self.nonincremental_layout, + self.trace_layout, + self.dump_flow_tree, + ); + } + + /// Enters a quiescent state in which no new messages will be processed until an `ExitNow` is + /// received. A pong is immediately sent on the given response channel. + fn prepare_to_exit(&mut self, response_chan: Sender<()>) { + response_chan.send(()).unwrap(); + loop { + match self.port.recv().unwrap() { + Msg::ReapStyleAndLayoutData(dead_data) => unsafe { + drop_style_and_layout_data(dead_data) + }, + Msg::ExitNow => { + debug!("layout thread is exiting..."); + self.exit_now(); + break; + }, + Msg::CollectReports(_) => { + // Just ignore these messages at this point. + }, + _ => panic!("layout: unexpected message received after `PrepareToExitMsg`"), + } + } + } + + /// Shuts down the layout thread now. If there are any DOM nodes left, layout will now (safely) + /// crash. + fn exit_now(&mut self) { + // Drop the root flow explicitly to avoid holding style data, such as + // rule nodes. The `Stylist` checks when it is dropped that all rule + // nodes have been GCed, so we want drop anyone who holds them first. + let waiting_time_min = self.layout_query_waiting_time.minimum().unwrap_or(0); + let waiting_time_max = self.layout_query_waiting_time.maximum().unwrap_or(0); + let waiting_time_mean = self.layout_query_waiting_time.mean().unwrap_or(0); + let waiting_time_stddev = self.layout_query_waiting_time.stddev().unwrap_or(0); + debug!( + "layout: query waiting time: min: {}, max: {}, mean: {}, standard_deviation: {}", + waiting_time_min, waiting_time_max, waiting_time_mean, waiting_time_stddev + ); + + self.root_flow.borrow_mut().take(); + self.background_hang_monitor.unregister(); + } + + fn handle_add_stylesheet(&self, stylesheet: &Stylesheet, guard: &SharedRwLockReadGuard) { + // Find all font-face rules and notify the font cache of them. + // GWTODO: Need to handle unloading web fonts. + if stylesheet.is_effective_for_device(self.stylist.device(), &guard) { + add_font_face_rules( + &*stylesheet, + &guard, + self.stylist.device(), + &self.font_cache_thread, + &self.font_cache_sender, + &self.outstanding_web_fonts, + self.load_webfonts_synchronously, + ); + } + } + + /// Advances the animation clock of the document. + fn handle_advance_clock_ms<'a, 'b>( + &mut self, + how_many_ms: i32, + possibly_locked_rw_data: &mut RwData<'a, 'b>, + tick_animations: bool, + ) { + self.timer.increment(how_many_ms as f64 / 1000.0); + if tick_animations { + self.tick_all_animations(possibly_locked_rw_data); + } + } + + /// Sets quirks mode for the document, causing the quirks mode stylesheet to be used. + fn handle_set_quirks_mode<'a, 'b>(&mut self, quirks_mode: QuirksMode) { + self.stylist.set_quirks_mode(quirks_mode); + } + + fn try_get_layout_root<N: LayoutNode>(&self, node: N) -> Option<FlowRef> { + let result = node.mutate_layout_data()?.flow_construction_result.get(); + + let mut flow = match result { + ConstructionResult::Flow(mut flow, abs_descendants) => { + // Note: Assuming that the root has display 'static' (as per + // CSS Section 9.3.1). Otherwise, if it were absolutely + // positioned, it would return a reference to itself in + // `abs_descendants` and would lead to a circular reference. + // Set Root as CB for any remaining absolute descendants. + flow.set_absolute_descendants(abs_descendants); + flow + }, + _ => return None, + }; + + FlowRef::deref_mut(&mut flow).mark_as_root(); + + Some(flow) + } + + /// Performs layout constraint solving. + /// + /// This corresponds to `Reflow()` in Gecko and `layout()` in WebKit/Blink and should be + /// benchmarked against those two. It is marked `#[inline(never)]` to aid profiling. + #[inline(never)] + fn solve_constraints(layout_root: &mut dyn Flow, layout_context: &LayoutContext) { + let _scope = layout_debug_scope!("solve_constraints"); + sequential::reflow(layout_root, layout_context, RelayoutMode::Incremental); + } + + /// Performs layout constraint solving in parallel. + /// + /// This corresponds to `Reflow()` in Gecko and `layout()` in WebKit/Blink and should be + /// benchmarked against those two. It is marked `#[inline(never)]` to aid profiling. + #[inline(never)] + fn solve_constraints_parallel( + traversal: &rayon::ThreadPool, + layout_root: &mut dyn Flow, + profiler_metadata: Option<TimerMetadata>, + time_profiler_chan: profile_time::ProfilerChan, + layout_context: &LayoutContext, + ) { + let _scope = layout_debug_scope!("solve_constraints_parallel"); + + // NOTE: this currently computes borders, so any pruning should separate that + // operation out. + parallel::reflow( + layout_root, + profiler_metadata, + time_profiler_chan, + layout_context, + traversal, + ); + } + + /// Computes the stacking-relative positions of all flows and, if the painting is dirty and the + /// reflow type need it, builds the display list. + fn compute_abs_pos_and_build_display_list( + &self, + data: &Reflow, + reflow_goal: &ReflowGoal, + document: Option<&ServoLayoutDocument>, + layout_root: &mut dyn Flow, + layout_context: &mut LayoutContext, + rw_data: &mut LayoutThreadData, + ) { + let writing_mode = layout_root.base().writing_mode; + let (metadata, sender) = (self.profiler_metadata(), self.time_profiler_chan.clone()); + profile( + profile_time::ProfilerCategory::LayoutDispListBuild, + metadata.clone(), + sender.clone(), + || { + layout_root.mut_base().stacking_relative_position = + LogicalPoint::zero(writing_mode) + .to_physical(writing_mode, self.viewport_size) + .to_vector(); + + layout_root.mut_base().clip = data.page_clip_rect; + + let traversal = ComputeStackingRelativePositions { + layout_context: layout_context, + }; + traversal.traverse(layout_root); + + if layout_root + .base() + .restyle_damage + .contains(ServoRestyleDamage::REPAINT) || + rw_data.display_list.is_none() + { + if reflow_goal.needs_display_list() { + let background_color = get_root_flow_background_color(layout_root); + let mut build_state = sequential::build_display_list_for_subtree( + layout_root, + layout_context, + background_color, + data.page_clip_rect.size, + ); + + debug!("Done building display list."); + + let root_size = { + let root_flow = layout_root.base(); + if self.stylist.viewport_constraints().is_some() { + root_flow.position.size.to_physical(root_flow.writing_mode) + } else { + root_flow.overflow.scroll.size + } + }; + + let origin = Rect::new(Point2D::new(Au(0), Au(0)), root_size).to_layout(); + build_state.root_stacking_context.bounds = origin; + build_state.root_stacking_context.overflow = origin; + + if !build_state.iframe_sizes.is_empty() { + // build_state.iframe_sizes is only used here, so its okay to replace + // it with an empty vector + let iframe_sizes = + std::mem::replace(&mut build_state.iframe_sizes, vec![]); + // Collect the last frame's iframe sizes to compute any differences. + // Every frame starts with a fresh collection so that any removed + // iframes do not linger. + let last_iframe_sizes = std::mem::replace( + &mut *self.last_iframe_sizes.borrow_mut(), + HashMap::default(), + ); + let mut size_messages = vec![]; + for new_size in iframe_sizes { + // Only notify the constellation about existing iframes + // that have a new size, or iframes that did not previously + // exist. + if let Some(old_size) = last_iframe_sizes.get(&new_size.id) { + if *old_size != new_size.size { + size_messages.push(IFrameSizeMsg { + data: new_size, + type_: WindowSizeType::Resize, + }); + } + } else { + size_messages.push(IFrameSizeMsg { + data: new_size, + type_: WindowSizeType::Initial, + }); + } + self.last_iframe_sizes + .borrow_mut() + .insert(new_size.id, new_size.size); + } + + if !size_messages.is_empty() { + let msg = ConstellationMsg::IFrameSizes(size_messages); + if let Err(e) = self.constellation_chan.send(msg) { + warn!("Layout resize to constellation failed ({}).", e); + } + } + } + + rw_data.indexable_text = std::mem::replace( + &mut build_state.indexable_text, + IndexableText::default(), + ); + rw_data.display_list = Some(build_state.to_display_list()); + } + } + + if !reflow_goal.needs_display() { + // Defer the paint step until the next ForDisplay. + // + // We need to tell the document about this so it doesn't + // incorrectly suppress reflows. See #13131. + document + .expect("No document in a non-display reflow?") + .needs_paint_from_layout(); + return; + } + if let Some(document) = document { + document.will_paint(); + } + + let display_list = rw_data.display_list.as_mut().unwrap(); + + if self.dump_display_list { + display_list.print(); + } + if self.dump_display_list_json { + println!("{}", serde_json::to_string_pretty(&display_list).unwrap()); + } + + debug!("Layout done!"); + + // TODO: Avoid the temporary conversion and build webrender sc/dl directly! + let builder = display_list.convert_to_webrender(self.id); + + let viewport_size = Size2D::new( + self.viewport_size.width.to_f32_px(), + self.viewport_size.height.to_f32_px(), + ); + + let mut epoch = self.epoch.get(); + epoch.next(); + self.epoch.set(epoch); + + let viewport_size = webrender_api::units::LayoutSize::from_untyped(&viewport_size); + + // Observe notifications about rendered frames if needed right before + // sending the display list to WebRender in order to set time related + // Progressive Web Metrics. + self.paint_time_metrics + .maybe_observe_paint_time(self, epoch, &*display_list); + + let mut txn = webrender_api::Transaction::new(); + txn.set_display_list( + webrender_api::Epoch(epoch.0), + None, + viewport_size, + builder.finalize(), + true, + ); + txn.generate_frame(); + self.webrender_api + .send_transaction(self.webrender_document, txn); + }, + ); + } + + /// The high-level routine that performs layout threads. + fn handle_reflow<'a, 'b>( + &mut self, + data: &mut ScriptReflowResult, + possibly_locked_rw_data: &mut RwData<'a, 'b>, + ) { + let document = unsafe { ServoLayoutNode::new(&data.document) }; + let document = document.as_document().unwrap(); + + // Parallelize if there's more than 750 objects based on rzambre's suggestion + // https://github.com/servo/servo/issues/10110 + self.parallel_flag = data.dom_count > 750; + debug!("layout: received layout request for: {}", self.url); + debug!("Number of objects in DOM: {}", data.dom_count); + debug!("layout: parallel? {}", self.parallel_flag); + + let mut rw_data = possibly_locked_rw_data.lock(); + + // Record the time that layout query has been waited. + let now = time::precise_time_ns(); + if let ReflowGoal::LayoutQuery(_, timestamp) = data.reflow_goal { + self.layout_query_waiting_time + .increment(now - timestamp) + .expect("layout: wrong layout query timestamp"); + }; + + let element = match document.root_element() { + None => { + // Since we cannot compute anything, give spec-required placeholders. + debug!("layout: No root node: bailing"); + match data.reflow_goal { + ReflowGoal::LayoutQuery(ref query_msg, _) => match query_msg { + &QueryMsg::ContentBoxQuery(_) => { + rw_data.content_box_response = None; + }, + &QueryMsg::ContentBoxesQuery(_) => { + rw_data.content_boxes_response = Vec::new(); + }, + &QueryMsg::NodesFromPointQuery(..) => { + rw_data.nodes_from_point_response = Vec::new(); + }, + &QueryMsg::NodeGeometryQuery(_) => { + rw_data.client_rect_response = Rect::zero(); + }, + &QueryMsg::NodeScrollGeometryQuery(_) => { + rw_data.scroll_area_response = Rect::zero(); + }, + &QueryMsg::NodeScrollIdQuery(_) => { + rw_data.scroll_id_response = None; + }, + &QueryMsg::ResolvedStyleQuery(_, _, _) => { + rw_data.resolved_style_response = String::new(); + }, + &QueryMsg::OffsetParentQuery(_) => { + rw_data.offset_parent_response = OffsetParentResponse::empty(); + }, + &QueryMsg::StyleQuery(_) => { + rw_data.style_response = StyleResponse(None); + }, + &QueryMsg::TextIndexQuery(..) => { + rw_data.text_index_response = TextIndexResponse(None); + }, + &QueryMsg::ElementInnerTextQuery(_) => { + rw_data.element_inner_text_response = String::new(); + }, + }, + ReflowGoal::Full | ReflowGoal::TickAnimations => {}, + } + return; + }, + Some(x) => x, + }; + + debug!( + "layout: processing reflow request for: {:?} ({}) (query={:?})", + element, self.url, data.reflow_goal + ); + trace!("{:?}", ShowSubtree(element.as_node())); + + let initial_viewport = data.window_size.initial_viewport; + let device_pixel_ratio = data.window_size.device_pixel_ratio; + let old_viewport_size = self.viewport_size; + let current_screen_size = Size2D::new( + Au::from_f32_px(initial_viewport.width), + Au::from_f32_px(initial_viewport.height), + ); + + // Calculate the actual viewport as per DEVICE-ADAPT § 6 + // If the entire flow tree is invalid, then it will be reflowed anyhow. + let document_shared_lock = document.style_shared_lock(); + self.document_shared_lock = Some(document_shared_lock.clone()); + let author_guard = document_shared_lock.read(); + + let ua_stylesheets = &*UA_STYLESHEETS; + let ua_or_user_guard = ua_stylesheets.shared_lock.read(); + let guards = StylesheetGuards { + author: &author_guard, + ua_or_user: &ua_or_user_guard, + }; + + let had_used_viewport_units = self.stylist.device().used_viewport_units(); + let device = Device::new(MediaType::screen(), initial_viewport, device_pixel_ratio); + let sheet_origins_affected_by_device_change = self.stylist.set_device(device, &guards); + + self.stylist + .force_stylesheet_origins_dirty(sheet_origins_affected_by_device_change); + self.viewport_size = + self.stylist + .viewport_constraints() + .map_or(current_screen_size, |constraints| { + debug!("Viewport constraints: {:?}", constraints); + + // other rules are evaluated against the actual viewport + Size2D::new( + Au::from_f32_px(constraints.size.width), + Au::from_f32_px(constraints.size.height), + ) + }); + + let viewport_size_changed = self.viewport_size != old_viewport_size; + if viewport_size_changed { + if let Some(constraints) = self.stylist.viewport_constraints() { + // let the constellation know about the viewport constraints + rw_data + .constellation_chan + .send(ConstellationMsg::ViewportConstrained( + self.id, + constraints.clone(), + )) + .unwrap(); + } + if had_used_viewport_units { + if let Some(mut data) = element.mutate_data() { + data.hint.insert(RestyleHint::recascade_subtree()); + } + } + } + + { + if self.first_reflow.get() { + debug!("First reflow, rebuilding user and UA rules"); + for stylesheet in &ua_stylesheets.user_or_user_agent_stylesheets { + self.stylist + .append_stylesheet(stylesheet.clone(), &ua_or_user_guard); + self.handle_add_stylesheet(&stylesheet.0, &ua_or_user_guard); + } + + if self.stylist.quirks_mode() != QuirksMode::NoQuirks { + self.stylist.append_stylesheet( + ua_stylesheets.quirks_mode_stylesheet.clone(), + &ua_or_user_guard, + ); + self.handle_add_stylesheet( + &ua_stylesheets.quirks_mode_stylesheet.0, + &ua_or_user_guard, + ); + } + } + + if data.stylesheets_changed { + debug!("Doc sheets changed, flushing author sheets too"); + self.stylist + .force_stylesheet_origins_dirty(Origin::Author.into()); + } + } + + if viewport_size_changed { + if let Some(mut flow) = self.try_get_layout_root(element.as_node()) { + LayoutThread::reflow_all_nodes(FlowRef::deref_mut(&mut flow)); + } + } + + debug!( + "Shadow roots in document {:?}", + document.shadow_roots().len() + ); + + // Flush shadow roots stylesheets if dirty. + document.flush_shadow_roots_stylesheets( + &self.stylist.device(), + document.quirks_mode(), + guards.author.clone(), + ); + + let restyles = document.drain_pending_restyles(); + debug!("Draining restyles: {}", restyles.len()); + + let mut map = SnapshotMap::new(); + let elements_with_snapshot: Vec<_> = restyles + .iter() + .filter(|r| r.1.snapshot.is_some()) + .map(|r| r.0) + .collect(); + + for (el, restyle) in restyles { + // Propagate the descendant bit up the ancestors. Do this before + // the restyle calculation so that we can also do it for new + // unstyled nodes, which the descendants bit helps us find. + if let Some(parent) = el.parent_element() { + unsafe { parent.note_dirty_descendant() }; + } + + // If we haven't styled this node yet, we don't need to track a + // restyle. + let style_data = match el.get_data() { + Some(d) => d, + None => { + unsafe { el.unset_snapshot_flags() }; + continue; + }, + }; + + if let Some(s) = restyle.snapshot { + unsafe { el.set_has_snapshot() }; + map.insert(el.as_node().opaque(), s); + } + + let mut style_data = style_data.borrow_mut(); + + // Stash the data on the element for processing by the style system. + style_data.hint.insert(restyle.hint.into()); + style_data.damage = restyle.damage; + debug!("Noting restyle for {:?}: {:?}", el, style_data); + } + + self.stylist.flush(&guards, Some(element), Some(&map)); + + // Create a layout context for use throughout the following passes. + let mut layout_context = self.build_layout_context(guards.clone(), true, &map); + + let (thread_pool, num_threads) = if self.parallel_flag { + ( + STYLE_THREAD_POOL.style_thread_pool.as_ref(), + STYLE_THREAD_POOL.num_threads, + ) + } else { + (None, 1) + }; + + let traversal = RecalcStyleAndConstructFlows::new(layout_context); + let token = { + let shared = + <RecalcStyleAndConstructFlows as DomTraversal<ServoLayoutElement>>::shared_context( + &traversal, + ); + RecalcStyleAndConstructFlows::pre_traverse(element, shared) + }; + + if token.should_traverse() { + // Recalculate CSS styles and rebuild flows and fragments. + profile( + profile_time::ProfilerCategory::LayoutStyleRecalc, + self.profiler_metadata(), + self.time_profiler_chan.clone(), + || { + // Perform CSS selector matching and flow construction. + driver::traverse_dom::<ServoLayoutElement, RecalcStyleAndConstructFlows>( + &traversal, + token, + thread_pool, + ); + }, + ); + // TODO(pcwalton): Measure energy usage of text shaping, perhaps? + let text_shaping_time = + font::get_and_reset_text_shaping_performance_counter() / num_threads; + profile_time::send_profile_data( + profile_time::ProfilerCategory::LayoutTextShaping, + self.profiler_metadata(), + &self.time_profiler_chan, + 0, + text_shaping_time as u64, + 0, + 0, + ); + + // Retrieve the (possibly rebuilt) root flow. + *self.root_flow.borrow_mut() = self.try_get_layout_root(element.as_node()); + } + + for element in elements_with_snapshot { + unsafe { element.unset_snapshot_flags() } + } + + layout_context = traversal.destroy(); + + if self.dump_style_tree { + println!("{:?}", ShowSubtreeDataAndPrimaryValues(element.as_node())); + } + + if self.dump_rule_tree { + layout_context + .style_context + .stylist + .rule_tree() + .dump_stdout(&guards); + } + + // GC the rule tree if some heuristics are met. + unsafe { + layout_context.style_context.stylist.rule_tree().maybe_gc(); + } + + // Perform post-style recalculation layout passes. + if let Some(mut root_flow) = self.root_flow.borrow().clone() { + self.perform_post_style_recalc_layout_passes( + &mut root_flow, + &data.reflow_info, + &data.reflow_goal, + Some(&document), + &mut rw_data, + &mut layout_context, + FxHashSet::default(), + ); + } + + self.first_reflow.set(false); + self.respond_to_query_if_necessary( + &data.reflow_goal, + &mut *rw_data, + &mut layout_context, + data.result.borrow_mut().as_mut().unwrap(), + ); + } + + fn respond_to_query_if_necessary( + &self, + reflow_goal: &ReflowGoal, + rw_data: &mut LayoutThreadData, + context: &mut LayoutContext, + reflow_result: &mut ReflowComplete, + ) { + let pending_images = match context.pending_images { + Some(ref pending) => std::mem::replace(&mut *pending.lock().unwrap(), vec![]), + None => vec![], + }; + reflow_result.pending_images = pending_images; + + let newly_transitioning_nodes = match context.newly_transitioning_nodes { + Some(ref nodes) => std::mem::replace(&mut *nodes.lock().unwrap(), vec![]), + None => vec![], + }; + reflow_result.newly_transitioning_nodes = newly_transitioning_nodes; + + let mut root_flow = match self.root_flow.borrow().clone() { + Some(root_flow) => root_flow, + None => return, + }; + let root_flow = FlowRef::deref_mut(&mut root_flow); + match *reflow_goal { + ReflowGoal::LayoutQuery(ref querymsg, _) => match querymsg { + &QueryMsg::ContentBoxQuery(node) => { + rw_data.content_box_response = process_content_box_request(node, root_flow); + }, + &QueryMsg::ContentBoxesQuery(node) => { + rw_data.content_boxes_response = process_content_boxes_request(node, root_flow); + }, + &QueryMsg::TextIndexQuery(node, point_in_node) => { + let point_in_node = Point2D::new( + Au::from_f32_px(point_in_node.x), + Au::from_f32_px(point_in_node.y), + ); + rw_data.text_index_response = + TextIndexResponse(rw_data.indexable_text.text_index(node, point_in_node)); + }, + &QueryMsg::NodeGeometryQuery(node) => { + rw_data.client_rect_response = process_node_geometry_request(node, root_flow); + }, + &QueryMsg::NodeScrollGeometryQuery(node) => { + rw_data.scroll_area_response = + process_node_scroll_area_request(node, root_flow); + }, + &QueryMsg::NodeScrollIdQuery(node) => { + let node = unsafe { ServoLayoutNode::new(&node) }; + rw_data.scroll_id_response = + Some(process_node_scroll_id_request(self.id, node)); + }, + &QueryMsg::ResolvedStyleQuery(node, ref pseudo, ref property) => { + let node = unsafe { ServoLayoutNode::new(&node) }; + rw_data.resolved_style_response = + process_resolved_style_request(context, node, pseudo, property, root_flow); + }, + &QueryMsg::OffsetParentQuery(node) => { + rw_data.offset_parent_response = process_offset_parent_query(node, root_flow); + }, + &QueryMsg::StyleQuery(node) => { + let node = unsafe { ServoLayoutNode::new(&node) }; + rw_data.style_response = process_style_query(node); + }, + &QueryMsg::NodesFromPointQuery(client_point, ref reflow_goal) => { + let mut flags = match reflow_goal { + &NodesFromPointQueryType::Topmost => webrender_api::HitTestFlags::empty(), + &NodesFromPointQueryType::All => webrender_api::HitTestFlags::FIND_ALL, + }; + + // The point we get is not relative to the entire WebRender scene, but to this + // particular pipeline, so we need to tell WebRender about that. + flags.insert(webrender_api::HitTestFlags::POINT_RELATIVE_TO_PIPELINE_VIEWPORT); + + let client_point = + webrender_api::units::WorldPoint::from_untyped(&client_point); + let results = self.webrender_api.hit_test( + self.webrender_document, + Some(self.id.to_webrender()), + client_point, + flags, + ); + + rw_data.nodes_from_point_response = results + .items + .iter() + .map(|item| UntrustedNodeAddress(item.tag.0 as *const c_void)) + .collect() + }, + &QueryMsg::ElementInnerTextQuery(node) => { + let node = unsafe { ServoLayoutNode::new(&node) }; + rw_data.element_inner_text_response = + process_element_inner_text_query(node, &rw_data.indexable_text); + }, + }, + ReflowGoal::Full | ReflowGoal::TickAnimations => {}, + } + } + + fn set_scroll_states<'a, 'b>( + &mut self, + new_scroll_states: Vec<ScrollState>, + possibly_locked_rw_data: &mut RwData<'a, 'b>, + ) { + let mut rw_data = possibly_locked_rw_data.lock(); + let mut script_scroll_states = vec![]; + let mut layout_scroll_states = HashMap::new(); + for new_state in &new_scroll_states { + let offset = new_state.scroll_offset; + layout_scroll_states.insert(new_state.scroll_id, offset); + + if new_state.scroll_id.is_root() { + script_scroll_states.push((UntrustedNodeAddress::from_id(0), offset)) + } else if let Some(node_id) = node_id_from_scroll_id(new_state.scroll_id.0 as usize) { + script_scroll_states.push((UntrustedNodeAddress::from_id(node_id), offset)) + } + } + let _ = self + .script_chan + .send(ConstellationControlMsg::SetScrollState( + self.id, + script_scroll_states, + )); + rw_data.scroll_offsets = layout_scroll_states + } + + fn tick_all_animations<'a, 'b>(&mut self, possibly_locked_rw_data: &mut RwData<'a, 'b>) { + let mut rw_data = possibly_locked_rw_data.lock(); + self.tick_animations(&mut rw_data); + } + + fn tick_animations(&mut self, rw_data: &mut LayoutThreadData) { + if self.relayout_event { + println!( + "**** pipeline={}\tForDisplay\tSpecial\tAnimationTick", + self.id + ); + } + + if let Some(mut root_flow) = self.root_flow.borrow().clone() { + let reflow_info = Reflow { + page_clip_rect: Rect::max_rect(), + }; + + // Unwrap here should not panic since self.root_flow is only ever set to Some(_) + // in handle_reflow() where self.document_shared_lock is as well. + let author_shared_lock = self.document_shared_lock.clone().unwrap(); + let author_guard = author_shared_lock.read(); + let ua_or_user_guard = UA_STYLESHEETS.shared_lock.read(); + let guards = StylesheetGuards { + author: &author_guard, + ua_or_user: &ua_or_user_guard, + }; + let snapshots = SnapshotMap::new(); + let mut layout_context = self.build_layout_context(guards, false, &snapshots); + + let invalid_nodes = { + // Perform an abbreviated style recalc that operates without access to the DOM. + let animations = self.running_animations.read(); + profile( + profile_time::ProfilerCategory::LayoutStyleRecalc, + self.profiler_metadata(), + self.time_profiler_chan.clone(), + || { + animation::recalc_style_for_animations::<ServoLayoutElement>( + &layout_context, + FlowRef::deref_mut(&mut root_flow), + &animations, + ) + }, + ) + }; + self.perform_post_style_recalc_layout_passes( + &mut root_flow, + &reflow_info, + &ReflowGoal::TickAnimations, + None, + &mut *rw_data, + &mut layout_context, + invalid_nodes, + ); + assert!(layout_context.pending_images.is_none()); + assert!(layout_context.newly_transitioning_nodes.is_none()); + } + } + + fn perform_post_style_recalc_layout_passes( + &self, + root_flow: &mut FlowRef, + data: &Reflow, + reflow_goal: &ReflowGoal, + document: Option<&ServoLayoutDocument>, + rw_data: &mut LayoutThreadData, + context: &mut LayoutContext, + invalid_nodes: FxHashSet<OpaqueNode>, + ) { + { + let mut newly_transitioning_nodes = context + .newly_transitioning_nodes + .as_ref() + .map(|nodes| nodes.lock().unwrap()); + let newly_transitioning_nodes = + newly_transitioning_nodes.as_mut().map(|nodes| &mut **nodes); + // Kick off animations if any were triggered, expire completed ones. + animation::update_animation_state::<ServoLayoutElement>( + &self.constellation_chan, + &self.script_chan, + &mut *self.running_animations.write(), + &mut *self.expired_animations.write(), + invalid_nodes, + newly_transitioning_nodes, + &self.new_animations_receiver, + self.id, + &self.timer, + ); + } + + profile( + profile_time::ProfilerCategory::LayoutRestyleDamagePropagation, + self.profiler_metadata(), + self.time_profiler_chan.clone(), + || { + // Call `compute_layout_damage` even in non-incremental mode, because it sets flags + // that are needed in both incremental and non-incremental traversals. + let damage = FlowRef::deref_mut(root_flow).compute_layout_damage(); + + if self.nonincremental_layout || + damage.contains(SpecialRestyleDamage::REFLOW_ENTIRE_DOCUMENT) + { + FlowRef::deref_mut(root_flow).reflow_entire_document() + } + }, + ); + + if self.trace_layout { + layout_debug::begin_trace(root_flow.clone()); + } + + // Resolve generated content. + profile( + profile_time::ProfilerCategory::LayoutGeneratedContent, + self.profiler_metadata(), + self.time_profiler_chan.clone(), + || sequential::resolve_generated_content(FlowRef::deref_mut(root_flow), &context), + ); + + // Guess float placement. + profile( + profile_time::ProfilerCategory::LayoutFloatPlacementSpeculation, + self.profiler_metadata(), + self.time_profiler_chan.clone(), + || sequential::guess_float_placement(FlowRef::deref_mut(root_flow)), + ); + + // Perform the primary layout passes over the flow tree to compute the locations of all + // the boxes. + if root_flow + .base() + .restyle_damage + .intersects(ServoRestyleDamage::REFLOW | ServoRestyleDamage::REFLOW_OUT_OF_FLOW) + { + profile( + profile_time::ProfilerCategory::LayoutMain, + self.profiler_metadata(), + self.time_profiler_chan.clone(), + || { + let profiler_metadata = self.profiler_metadata(); + + let thread_pool = if self.parallel_flag { + STYLE_THREAD_POOL.style_thread_pool.as_ref() + } else { + None + }; + + if let Some(pool) = thread_pool { + // Parallel mode. + LayoutThread::solve_constraints_parallel( + pool, + FlowRef::deref_mut(root_flow), + profiler_metadata, + self.time_profiler_chan.clone(), + &*context, + ); + } else { + //Sequential mode + LayoutThread::solve_constraints(FlowRef::deref_mut(root_flow), &context) + } + }, + ); + } + + profile( + profile_time::ProfilerCategory::LayoutStoreOverflow, + self.profiler_metadata(), + self.time_profiler_chan.clone(), + || { + sequential::store_overflow(context, FlowRef::deref_mut(root_flow) as &mut dyn Flow); + }, + ); + + self.perform_post_main_layout_passes( + data, + root_flow, + reflow_goal, + document, + rw_data, + context, + ); + } + + fn perform_post_main_layout_passes( + &self, + data: &Reflow, + mut root_flow: &mut FlowRef, + reflow_goal: &ReflowGoal, + document: Option<&ServoLayoutDocument>, + rw_data: &mut LayoutThreadData, + layout_context: &mut LayoutContext, + ) { + // Build the display list if necessary, and send it to the painter. + self.compute_abs_pos_and_build_display_list( + data, + reflow_goal, + document, + FlowRef::deref_mut(&mut root_flow), + &mut *layout_context, + rw_data, + ); + + if self.trace_layout { + layout_debug::end_trace(self.generation.get()); + } + + if self.dump_flow_tree { + root_flow.print("Post layout flow tree".to_owned()); + } + + self.generation.set(self.generation.get() + 1); + } + + fn reflow_all_nodes(flow: &mut dyn Flow) { + debug!("reflowing all nodes!"); + flow.mut_base().restyle_damage.insert( + ServoRestyleDamage::REPAINT | + ServoRestyleDamage::STORE_OVERFLOW | + ServoRestyleDamage::REFLOW | + ServoRestyleDamage::REPOSITION, + ); + + for child in flow.mut_base().child_iter_mut() { + LayoutThread::reflow_all_nodes(child); + } + } + + /// Returns profiling information which is passed to the time profiler. + fn profiler_metadata(&self) -> Option<TimerMetadata> { + Some(TimerMetadata { + url: self.url.to_string(), + iframe: if self.is_iframe { + TimerMetadataFrameType::IFrame + } else { + TimerMetadataFrameType::RootWindow + }, + incremental: if self.first_reflow.get() { + TimerMetadataReflowType::FirstReflow + } else { + TimerMetadataReflowType::Incremental + }, + }) + } +} + +impl ProfilerMetadataFactory for LayoutThread { + fn new_metadata(&self) -> Option<TimerMetadata> { + self.profiler_metadata() + } +} + +// The default computed value for background-color is transparent (see +// http://dev.w3.org/csswg/css-backgrounds/#background-color). However, we +// need to propagate the background color from the root HTML/Body +// element (http://dev.w3.org/csswg/css-backgrounds/#special-backgrounds) if +// it is non-transparent. The phrase in the spec "If the canvas background +// is not opaque, what shows through is UA-dependent." is handled by rust-layers +// clearing the frame buffer to white. This ensures that setting a background +// color on an iframe element, while the iframe content itself has a default +// transparent background color is handled correctly. +fn get_root_flow_background_color(flow: &mut dyn Flow) -> webrender_api::ColorF { + let transparent = webrender_api::ColorF { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }; + if !flow.is_block_like() { + return transparent; + } + + let block_flow = flow.as_mut_block(); + let kid = match block_flow.base.children.iter_mut().next() { + None => return transparent, + Some(kid) => kid, + }; + if !kid.is_block_like() { + return transparent; + } + + let kid_block_flow = kid.as_block(); + let color = kid_block_flow.fragment.style.resolve_color( + kid_block_flow + .fragment + .style + .get_background() + .background_color, + ); + webrender_api::ColorF::new( + color.red_f32(), + color.green_f32(), + color.blue_f32(), + color.alpha_f32(), + ) +} + +fn get_ua_stylesheets() -> Result<UserAgentStylesheets, &'static str> { + fn parse_ua_stylesheet( + shared_lock: &SharedRwLock, + filename: &str, + content: &[u8], + ) -> Result<DocumentStyleSheet, &'static str> { + Ok(DocumentStyleSheet(ServoArc::new(Stylesheet::from_bytes( + content, + ServoUrl::parse(&format!("chrome://resources/{:?}", filename)).unwrap(), + None, + None, + Origin::UserAgent, + MediaList::empty(), + shared_lock.clone(), + None, + None, + QuirksMode::NoQuirks, + )))) + } + + let shared_lock = &GLOBAL_STYLE_DATA.shared_lock; + + // FIXME: presentational-hints.css should be at author origin with zero specificity. + // (Does it make a difference?) + let mut user_or_user_agent_stylesheets = vec![ + parse_ua_stylesheet( + &shared_lock, + "user-agent.css", + &resources::read_bytes(Resource::UserAgentCSS), + )?, + parse_ua_stylesheet( + &shared_lock, + "servo.css", + &resources::read_bytes(Resource::ServoCSS), + )?, + parse_ua_stylesheet( + &shared_lock, + "presentational-hints.css", + &resources::read_bytes(Resource::PresentationalHintsCSS), + )?, + ]; + + for &(ref contents, ref url) in &opts::get().user_stylesheets { + user_or_user_agent_stylesheets.push(DocumentStyleSheet(ServoArc::new( + Stylesheet::from_bytes( + &contents, + url.clone(), + None, + None, + Origin::User, + MediaList::empty(), + shared_lock.clone(), + None, + Some(&RustLogReporter), + QuirksMode::NoQuirks, + ), + ))); + } + + let quirks_mode_stylesheet = parse_ua_stylesheet( + &shared_lock, + "quirks-mode.css", + &resources::read_bytes(Resource::QuirksModeCSS), + )?; + + Ok(UserAgentStylesheets { + shared_lock: shared_lock.clone(), + user_or_user_agent_stylesheets: user_or_user_agent_stylesheets, + quirks_mode_stylesheet: quirks_mode_stylesheet, + }) +} + +lazy_static! { + static ref UA_STYLESHEETS: UserAgentStylesheets = { + match get_ua_stylesheets() { + Ok(stylesheets) => stylesheets, + Err(filename) => { + error!("Failed to load UA stylesheet {}!", filename); + process::exit(1); + }, + } + }; +} + +struct RegisteredPainterImpl { + painter: Box<dyn Painter>, + name: Atom, + // FIXME: Should be a PrecomputedHashMap. + properties: FxHashMap<Atom, PropertyId>, +} + +impl SpeculativePainter for RegisteredPainterImpl { + fn speculatively_draw_a_paint_image( + &self, + properties: Vec<(Atom, String)>, + arguments: Vec<String>, + ) { + self.painter + .speculatively_draw_a_paint_image(properties, arguments); + } +} + +impl RegisteredSpeculativePainter for RegisteredPainterImpl { + fn properties(&self) -> &FxHashMap<Atom, PropertyId> { + &self.properties + } + fn name(&self) -> Atom { + self.name.clone() + } +} + +impl Painter for RegisteredPainterImpl { + fn draw_a_paint_image( + &self, + size: TypedSize2D<f32, CSSPixel>, + device_pixel_ratio: TypedScale<f32, CSSPixel, DevicePixel>, + properties: Vec<(Atom, String)>, + arguments: Vec<String>, + ) -> Result<DrawAPaintImageResult, PaintWorkletError> { + self.painter + .draw_a_paint_image(size, device_pixel_ratio, properties, arguments) + } +} + +impl RegisteredPainter for RegisteredPainterImpl {} + +struct RegisteredPaintersImpl(FnvHashMap<Atom, RegisteredPainterImpl>); + +impl RegisteredSpeculativePainters for RegisteredPaintersImpl { + fn get(&self, name: &Atom) -> Option<&dyn RegisteredSpeculativePainter> { + self.0 + .get(&name) + .map(|painter| painter as &dyn RegisteredSpeculativePainter) + } +} + +impl RegisteredPainters for RegisteredPaintersImpl { + fn get(&self, name: &Atom) -> Option<&dyn RegisteredPainter> { + self.0 + .get(&name) + .map(|painter| painter as &dyn RegisteredPainter) } } |