diff options
Diffstat (limited to 'components/layout/flow')
-rw-r--r-- | components/layout/flow/construct.rs | 762 | ||||
-rw-r--r-- | components/layout/flow/float.rs | 1183 | ||||
-rw-r--r-- | components/layout/flow/inline/construct.rs | 636 | ||||
-rw-r--r-- | components/layout/flow/inline/inline_box.rs | 257 | ||||
-rw-r--r-- | components/layout/flow/inline/line.rs | 911 | ||||
-rw-r--r-- | components/layout/flow/inline/line_breaker.rs | 120 | ||||
-rw-r--r-- | components/layout/flow/inline/mod.rs | 2525 | ||||
-rw-r--r-- | components/layout/flow/inline/text_run.rs | 640 | ||||
-rw-r--r-- | components/layout/flow/mod.rs | 2370 | ||||
-rw-r--r-- | components/layout/flow/root.rs | 500 |
10 files changed, 9904 insertions, 0 deletions
diff --git a/components/layout/flow/construct.rs b/components/layout/flow/construct.rs new file mode 100644 index 00000000000..a6471756db8 --- /dev/null +++ b/components/layout/flow/construct.rs @@ -0,0 +1,762 @@ +/* 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 std::borrow::Cow; +use std::convert::TryFrom; + +use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use servo_arc::Arc; +use style::properties::ComputedValues; +use style::properties::longhands::list_style_position::computed_value::T as ListStylePosition; +use style::selector_parser::PseudoElement; +use style::str::char_is_whitespace; + +use super::OutsideMarker; +use super::inline::InlineFormattingContext; +use super::inline::construct::InlineFormattingContextBuilder; +use super::inline::inline_box::InlineBox; +use crate::PropagatedBoxTreeData; +use crate::cell::ArcRefCell; +use crate::context::LayoutContext; +use crate::dom::{BoxSlot, LayoutBox, NodeExt}; +use crate::dom_traversal::{ + Contents, NodeAndStyleInfo, NonReplacedContents, PseudoElementContentItem, TraversalHandler, +}; +use crate::flow::float::FloatBox; +use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox}; +use crate::formatting_contexts::IndependentFormattingContext; +use crate::fragment_tree::FragmentFlags; +use crate::layout_box_base::LayoutBoxBase; +use crate::positioned::AbsolutelyPositionedBox; +use crate::style_ext::{ComputedValuesExt, DisplayGeneratingBox, DisplayInside, DisplayOutside}; +use crate::table::{AnonymousTableContent, Table}; + +impl BlockFormattingContext { + pub(crate) fn construct<'dom, Node>( + context: &LayoutContext, + info: &NodeAndStyleInfo<Node>, + contents: NonReplacedContents, + propagated_data: PropagatedBoxTreeData, + is_list_item: bool, + ) -> Self + where + Node: NodeExt<'dom>, + { + Self::from_block_container(BlockContainer::construct( + context, + info, + contents, + propagated_data, + is_list_item, + )) + } + + pub(crate) fn from_block_container(contents: BlockContainer) -> Self { + let contains_floats = contents.contains_floats(); + Self { + contents, + contains_floats, + } + } +} + +struct BlockLevelJob<'dom, Node> { + info: NodeAndStyleInfo<Node>, + box_slot: BoxSlot<'dom>, + propagated_data: PropagatedBoxTreeData, + kind: BlockLevelCreator, +} + +enum BlockLevelCreator { + SameFormattingContextBlock(IntermediateBlockContainer), + Independent { + display_inside: DisplayInside, + contents: Contents, + }, + OutOfFlowAbsolutelyPositionedBox { + display_inside: DisplayInside, + contents: Contents, + }, + OutOfFlowFloatBox { + display_inside: DisplayInside, + contents: Contents, + }, + OutsideMarker { + list_item_style: Arc<ComputedValues>, + contents: Vec<PseudoElementContentItem>, + }, + AnonymousTable { + table_block: ArcRefCell<BlockLevelBox>, + }, +} + +/// A block container that may still have to be constructed. +/// +/// Represents either the inline formatting context of an anonymous block +/// box or the yet-to-be-computed block container generated from the children +/// of a given element. +/// +/// Deferring allows using rayon’s `into_par_iter`. +enum IntermediateBlockContainer { + InlineFormattingContext(BlockContainer), + Deferred { + contents: NonReplacedContents, + propagated_data: PropagatedBoxTreeData, + is_list_item: bool, + }, +} + +/// A builder for a block container. +/// +/// This builder starts from the first child of a given DOM node +/// and does a preorder traversal of all of its inclusive siblings. +pub(crate) struct BlockContainerBuilder<'dom, 'style, Node> { + context: &'style LayoutContext<'style>, + + /// This NodeAndStyleInfo contains the root node, the corresponding pseudo + /// content designator, and the block container style. + info: &'style NodeAndStyleInfo<Node>, + + /// The list of block-level boxes to be built for the final block container. + /// + /// Contains all the block-level jobs we found traversing the tree + /// so far, if this is empty at the end of the traversal and the ongoing + /// inline formatting context is not empty, the block container establishes + /// an inline formatting context (see end of `build`). + /// + /// DOM nodes which represent block-level boxes are immediately pushed + /// to this list with their style without ever being traversed at this + /// point, instead we just move to their next sibling. If the DOM node + /// doesn't have a next sibling, we either reached the end of the container + /// root or there are ongoing inline-level boxes + /// (see `handle_block_level_element`). + block_level_boxes: Vec<BlockLevelJob<'dom, Node>>, + + /// Whether or not this builder has yet produced a block which would be + /// be considered the first line for the purposes of `text-indent`. + have_already_seen_first_line_for_text_indent: bool, + + /// The propagated data to use for BoxTree construction. + propagated_data: PropagatedBoxTreeData, + + inline_formatting_context_builder: InlineFormattingContextBuilder, + + /// The [`NodeAndStyleInfo`] to use for anonymous block boxes pushed to the list of + /// block-level boxes, lazily initialized (see `end_ongoing_inline_formatting_context`). + anonymous_box_info: Option<NodeAndStyleInfo<Node>>, + + /// A collection of content that is being added to an anonymous table. This is + /// composed of any sequence of internal table elements or table captions that + /// are found outside of a table. + anonymous_table_content: Vec<AnonymousTableContent<'dom, Node>>, +} + +impl BlockContainer { + pub fn construct<'dom, Node>( + context: &LayoutContext, + info: &NodeAndStyleInfo<Node>, + contents: NonReplacedContents, + propagated_data: PropagatedBoxTreeData, + is_list_item: bool, + ) -> BlockContainer + where + Node: NodeExt<'dom>, + { + let mut builder = BlockContainerBuilder::new(context, info, propagated_data); + + if is_list_item { + if let Some((marker_info, marker_contents)) = crate::lists::make_marker(context, info) { + match marker_info.style.clone_list_style_position() { + ListStylePosition::Inside => { + builder.handle_list_item_marker_inside(&marker_info, info, marker_contents) + }, + ListStylePosition::Outside => builder.handle_list_item_marker_outside( + &marker_info, + info, + marker_contents, + info.style.clone(), + ), + } + } + } + + contents.traverse(context, info, &mut builder); + builder.finish() + } +} + +impl<'dom, 'style, Node> BlockContainerBuilder<'dom, 'style, Node> +where + Node: NodeExt<'dom>, +{ + pub(crate) fn new( + context: &'style LayoutContext, + info: &'style NodeAndStyleInfo<Node>, + propagated_data: PropagatedBoxTreeData, + ) -> Self { + BlockContainerBuilder { + context, + info, + block_level_boxes: Vec::new(), + propagated_data: propagated_data.union(&info.style), + have_already_seen_first_line_for_text_indent: false, + anonymous_box_info: None, + anonymous_table_content: Vec::new(), + inline_formatting_context_builder: InlineFormattingContextBuilder::new(), + } + } + + pub(crate) fn finish(mut self) -> BlockContainer { + debug_assert!( + !self + .inline_formatting_context_builder + .currently_processing_inline_box() + ); + + self.finish_anonymous_table_if_needed(); + + if let Some(inline_formatting_context) = self.inline_formatting_context_builder.finish( + self.context, + self.propagated_data, + !self.have_already_seen_first_line_for_text_indent, + self.info.is_single_line_text_input(), + self.info.style.writing_mode.to_bidi_level(), + ) { + // There are two options here. This block was composed of both one or more inline formatting contexts + // and child blocks OR this block was a single inline formatting context. In the latter case, we + // just return the inline formatting context as the block itself. + if self.block_level_boxes.is_empty() { + return BlockContainer::InlineFormattingContext(inline_formatting_context); + } + self.push_block_level_job_for_inline_formatting_context(inline_formatting_context); + } + + let context = self.context; + let block_level_boxes = if self.context.use_rayon { + self.block_level_boxes + .into_par_iter() + .map(|block_level_job| block_level_job.finish(context)) + .collect() + } else { + self.block_level_boxes + .into_iter() + .map(|block_level_job| block_level_job.finish(context)) + .collect() + }; + + BlockContainer::BlockLevelBoxes(block_level_boxes) + } + + fn finish_anonymous_table_if_needed(&mut self) { + if self.anonymous_table_content.is_empty() { + return; + } + + // From https://drafts.csswg.org/css-tables/#fixup-algorithm: + // > If the box’s parent is an inline, run-in, or ruby box (or any box that would perform + // > inlinification of its children), then an inline-table box must be generated; otherwise + // > it must be a table box. + // + // Note that text content in the inline formatting context isn't enough to force the + // creation of an inline table. It requires the parent to be an inline box. + let inline_table = self + .inline_formatting_context_builder + .currently_processing_inline_box(); + + // Text decorations are not propagated to atomic inline-level descendants. + // From https://drafts.csswg.org/css2/#lining-striking-props: + // > Note that text decorations are not propagated to floating and absolutely + // > positioned descendants, nor to the contents of atomic inline-level descendants + // > such as inline blocks and inline tables. + let propagated_data = match inline_table { + true => self.propagated_data.without_text_decorations(), + false => self.propagated_data, + }; + + let contents: Vec<AnonymousTableContent<'dom, Node>> = + self.anonymous_table_content.drain(..).collect(); + let last_text = match contents.last() { + Some(AnonymousTableContent::Text(info, text)) => Some((info.clone(), text.clone())), + _ => None, + }; + + let (table_info, ifc) = + Table::construct_anonymous(self.context, self.info, contents, propagated_data); + + if inline_table { + self.inline_formatting_context_builder.push_atomic(ifc); + } else { + let table_block = ArcRefCell::new(BlockLevelBox::Independent(ifc)); + self.end_ongoing_inline_formatting_context(); + self.block_level_boxes.push(BlockLevelJob { + info: table_info, + box_slot: BoxSlot::dummy(), + kind: BlockLevelCreator::AnonymousTable { table_block }, + propagated_data, + }); + } + + // If the last element in the anonymous table content is whitespace, that + // whitespace doesn't actually belong to the table. It should be processed outside + // ie become a space between the anonymous table and the rest of the block + // content. Anonymous tables are really only constructed around internal table + // elements and the whitespace between them, so this trailing whitespace should + // not be included. + // + // See https://drafts.csswg.org/css-tables/#fixup-algorithm sections "Remove + // irrelevant boxes" and "Generate missing parents." + if let Some((info, text)) = last_text { + self.handle_text(&info, text); + } + } +} + +impl<'dom, Node> TraversalHandler<'dom, Node> for BlockContainerBuilder<'dom, '_, Node> +where + Node: NodeExt<'dom>, +{ + fn handle_element( + &mut self, + info: &NodeAndStyleInfo<Node>, + display: DisplayGeneratingBox, + contents: Contents, + box_slot: BoxSlot<'dom>, + ) { + match display { + DisplayGeneratingBox::OutsideInside { outside, inside } => { + self.finish_anonymous_table_if_needed(); + + match outside { + DisplayOutside::Inline => { + self.handle_inline_level_element(info, inside, contents, box_slot) + }, + DisplayOutside::Block => { + let box_style = info.style.get_box(); + // Floats and abspos cause blockification, so they only happen in this case. + // https://drafts.csswg.org/css2/visuren.html#dis-pos-flo + if box_style.position.is_absolutely_positioned() { + self.handle_absolutely_positioned_element( + info, inside, contents, box_slot, + ) + } else if box_style.float.is_floating() { + self.handle_float_element(info, inside, contents, box_slot) + } else { + self.handle_block_level_element(info, inside, contents, box_slot) + } + }, + }; + }, + DisplayGeneratingBox::LayoutInternal(_) => { + self.anonymous_table_content + .push(AnonymousTableContent::Element { + info: info.clone(), + display, + contents, + box_slot, + }); + }, + } + } + + fn handle_text(&mut self, info: &NodeAndStyleInfo<Node>, text: Cow<'dom, str>) { + if text.is_empty() { + return; + } + + // If we are building an anonymous table ie this text directly followed internal + // table elements that did not have a `<table>` ancestor, then we forward all + // whitespace to the table builder. + if !self.anonymous_table_content.is_empty() && text.chars().all(char_is_whitespace) { + self.anonymous_table_content + .push(AnonymousTableContent::Text(info.clone(), text)); + return; + } else { + self.finish_anonymous_table_if_needed(); + } + + self.inline_formatting_context_builder.push_text(text, info); + } +} + +impl<'dom, Node> BlockContainerBuilder<'dom, '_, Node> +where + Node: NodeExt<'dom>, +{ + fn handle_list_item_marker_inside( + &mut self, + marker_info: &NodeAndStyleInfo<Node>, + container_info: &NodeAndStyleInfo<Node>, + contents: Vec<crate::dom_traversal::PseudoElementContentItem>, + ) { + // TODO: We do not currently support saving box slots for ::marker pseudo-elements + // that are part nested in ::before and ::after pseudo elements. For now, just + // forget about them once they are built. + let box_slot = match container_info.pseudo_element_type { + Some(_) => BoxSlot::dummy(), + None => marker_info + .node + .pseudo_element_box_slot(PseudoElement::Marker), + }; + + self.handle_inline_level_element( + marker_info, + DisplayInside::Flow { + is_list_item: false, + }, + NonReplacedContents::OfPseudoElement(contents).into(), + box_slot, + ); + } + + fn handle_list_item_marker_outside( + &mut self, + marker_info: &NodeAndStyleInfo<Node>, + container_info: &NodeAndStyleInfo<Node>, + contents: Vec<crate::dom_traversal::PseudoElementContentItem>, + list_item_style: Arc<ComputedValues>, + ) { + // TODO: We do not currently support saving box slots for ::marker pseudo-elements + // that are part nested in ::before and ::after pseudo elements. For now, just + // forget about them once they are built. + let box_slot = match container_info.pseudo_element_type { + Some(_) => BoxSlot::dummy(), + None => marker_info + .node + .pseudo_element_box_slot(PseudoElement::Marker), + }; + + self.block_level_boxes.push(BlockLevelJob { + info: marker_info.clone(), + box_slot, + kind: BlockLevelCreator::OutsideMarker { + contents, + list_item_style, + }, + propagated_data: self.propagated_data.without_text_decorations(), + }); + } + + fn handle_inline_level_element( + &mut self, + info: &NodeAndStyleInfo<Node>, + display_inside: DisplayInside, + contents: Contents, + box_slot: BoxSlot<'dom>, + ) { + let (DisplayInside::Flow { is_list_item }, false) = + (display_inside, contents.is_replaced()) + else { + // If this inline element is an atomic, handle it and return. + let atomic = self.inline_formatting_context_builder.push_atomic( + IndependentFormattingContext::construct( + self.context, + info, + display_inside, + contents, + // Text decorations are not propagated to atomic inline-level descendants. + self.propagated_data.without_text_decorations(), + ), + ); + box_slot.set(LayoutBox::InlineLevel(atomic)); + return; + }; + + // Otherwise, this is just a normal inline box. Whatever happened before, all we need to do + // before recurring is to remember this ongoing inline level box. + let inline_item = self + .inline_formatting_context_builder + .start_inline_box(InlineBox::new(info)); + + if is_list_item { + if let Some((marker_info, marker_contents)) = + crate::lists::make_marker(self.context, info) + { + // Ignore `list-style-position` here: + // “If the list item is an inline box: this value is equivalent to `inside`.” + // https://drafts.csswg.org/css-lists/#list-style-position-outside + self.handle_list_item_marker_inside(&marker_info, info, marker_contents) + } + } + + // `unwrap` doesn’t panic here because `is_replaced` returned `false`. + NonReplacedContents::try_from(contents) + .unwrap() + .traverse(self.context, info, self); + + self.finish_anonymous_table_if_needed(); + + self.inline_formatting_context_builder.end_inline_box(); + box_slot.set(LayoutBox::InlineLevel(inline_item)); + } + + fn handle_block_level_element( + &mut self, + info: &NodeAndStyleInfo<Node>, + display_inside: DisplayInside, + contents: Contents, + box_slot: BoxSlot<'dom>, + ) { + // We just found a block level element, all ongoing inline level boxes + // need to be split around it. + // + // After calling `split_around_block_and_finish`, + // `self.inline_formatting_context_builder` is set up with the state + // that we want to have after we push the block below. + if let Some(inline_formatting_context) = self + .inline_formatting_context_builder + .split_around_block_and_finish( + self.context, + self.propagated_data, + !self.have_already_seen_first_line_for_text_indent, + self.info.style.writing_mode.to_bidi_level(), + ) + { + self.push_block_level_job_for_inline_formatting_context(inline_formatting_context); + } + + let propagated_data = self.propagated_data; + let kind = match contents { + Contents::NonReplaced(contents) => match display_inside { + DisplayInside::Flow { is_list_item } + // Fragment flags are just used to indicate that the element is not replaced, so empty + // flags are okay here. + if !info.style.establishes_block_formatting_context( + FragmentFlags::empty() + ) => + { + BlockLevelCreator::SameFormattingContextBlock( + IntermediateBlockContainer::Deferred { + contents, + propagated_data, + is_list_item, + }, + ) + }, + _ => BlockLevelCreator::Independent { + display_inside, + contents: contents.into(), + }, + }, + Contents::Replaced(contents) => { + let contents = Contents::Replaced(contents); + BlockLevelCreator::Independent { + display_inside, + contents, + } + }, + }; + self.block_level_boxes.push(BlockLevelJob { + info: info.clone(), + box_slot, + kind, + propagated_data, + }); + + // Any block also counts as the first line for the purposes of text indent. Even if + // they don't actually indent. + self.have_already_seen_first_line_for_text_indent = true; + } + + fn handle_absolutely_positioned_element( + &mut self, + info: &NodeAndStyleInfo<Node>, + display_inside: DisplayInside, + contents: Contents, + box_slot: BoxSlot<'dom>, + ) { + if !self.inline_formatting_context_builder.is_empty() { + let inline_level_box = self + .inline_formatting_context_builder + .push_absolutely_positioned_box(AbsolutelyPositionedBox::construct( + self.context, + info, + display_inside, + contents, + )); + box_slot.set(LayoutBox::InlineLevel(inline_level_box)); + return; + } + + let kind = BlockLevelCreator::OutOfFlowAbsolutelyPositionedBox { + contents, + display_inside, + }; + self.block_level_boxes.push(BlockLevelJob { + info: info.clone(), + box_slot, + kind, + propagated_data: self.propagated_data.without_text_decorations(), + }); + } + + fn handle_float_element( + &mut self, + info: &NodeAndStyleInfo<Node>, + display_inside: DisplayInside, + contents: Contents, + box_slot: BoxSlot<'dom>, + ) { + if !self.inline_formatting_context_builder.is_empty() { + let inline_level_box = + self.inline_formatting_context_builder + .push_float_box(FloatBox::construct( + self.context, + info, + display_inside, + contents, + self.propagated_data, + )); + box_slot.set(LayoutBox::InlineLevel(inline_level_box)); + return; + } + + let kind = BlockLevelCreator::OutOfFlowFloatBox { + contents, + display_inside, + }; + self.block_level_boxes.push(BlockLevelJob { + info: info.clone(), + box_slot, + kind, + propagated_data: self.propagated_data.without_text_decorations(), + }); + } + + fn end_ongoing_inline_formatting_context(&mut self) { + if let Some(inline_formatting_context) = self.inline_formatting_context_builder.finish( + self.context, + self.propagated_data, + !self.have_already_seen_first_line_for_text_indent, + self.info.is_single_line_text_input(), + self.info.style.writing_mode.to_bidi_level(), + ) { + self.push_block_level_job_for_inline_formatting_context(inline_formatting_context); + } + } + + fn push_block_level_job_for_inline_formatting_context( + &mut self, + inline_formatting_context: InlineFormattingContext, + ) { + let layout_context = self.context; + let info = self + .anonymous_box_info + .get_or_insert_with(|| { + self.info + .pseudo(layout_context, PseudoElement::ServoAnonymousBox) + .expect("Should never fail to create anonymous box") + }) + .clone(); + + self.block_level_boxes.push(BlockLevelJob { + info, + // FIXME(nox): We should be storing this somewhere. + box_slot: BoxSlot::dummy(), + kind: BlockLevelCreator::SameFormattingContextBlock( + IntermediateBlockContainer::InlineFormattingContext( + BlockContainer::InlineFormattingContext(inline_formatting_context), + ), + ), + propagated_data: self.propagated_data, + }); + + self.have_already_seen_first_line_for_text_indent = true; + } +} + +impl<'dom, Node> BlockLevelJob<'dom, Node> +where + Node: NodeExt<'dom>, +{ + fn finish(self, context: &LayoutContext) -> ArcRefCell<BlockLevelBox> { + let info = &self.info; + let block_level_box = match self.kind { + BlockLevelCreator::SameFormattingContextBlock(intermediate_block_container) => { + let contents = intermediate_block_container.finish(context, info); + let contains_floats = contents.contains_floats(); + ArcRefCell::new(BlockLevelBox::SameFormattingContextBlock { + base: LayoutBoxBase::new(info.into(), info.style.clone()), + contents, + contains_floats, + }) + }, + BlockLevelCreator::Independent { + display_inside, + contents, + } => { + let context = IndependentFormattingContext::construct( + context, + info, + display_inside, + contents, + self.propagated_data, + ); + ArcRefCell::new(BlockLevelBox::Independent(context)) + }, + BlockLevelCreator::OutOfFlowAbsolutelyPositionedBox { + display_inside, + contents, + } => ArcRefCell::new(BlockLevelBox::OutOfFlowAbsolutelyPositionedBox( + ArcRefCell::new(AbsolutelyPositionedBox::construct( + context, + info, + display_inside, + contents, + )), + )), + BlockLevelCreator::OutOfFlowFloatBox { + display_inside, + contents, + } => ArcRefCell::new(BlockLevelBox::OutOfFlowFloatBox(FloatBox::construct( + context, + info, + display_inside, + contents, + self.propagated_data, + ))), + BlockLevelCreator::OutsideMarker { + contents, + list_item_style, + } => { + let contents = NonReplacedContents::OfPseudoElement(contents); + let block_container = BlockContainer::construct( + context, + info, + contents, + self.propagated_data.without_text_decorations(), + false, /* is_list_item */ + ); + ArcRefCell::new(BlockLevelBox::OutsideMarker(OutsideMarker { + base: LayoutBoxBase::new(info.into(), info.style.clone()), + block_container, + list_item_style, + })) + }, + BlockLevelCreator::AnonymousTable { table_block } => table_block, + }; + self.box_slot + .set(LayoutBox::BlockLevel(block_level_box.clone())); + block_level_box + } +} + +impl IntermediateBlockContainer { + fn finish<'dom, Node>( + self, + context: &LayoutContext, + info: &NodeAndStyleInfo<Node>, + ) -> BlockContainer + where + Node: NodeExt<'dom>, + { + match self { + IntermediateBlockContainer::Deferred { + contents, + propagated_data, + is_list_item, + } => BlockContainer::construct(context, info, contents, propagated_data, is_list_item), + IntermediateBlockContainer::InlineFormattingContext(block_container) => block_container, + } + } +} diff --git a/components/layout/flow/float.rs b/components/layout/flow/float.rs new file mode 100644 index 00000000000..0570ce0d0f4 --- /dev/null +++ b/components/layout/flow/float.rs @@ -0,0 +1,1183 @@ +/* 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/. */ + +//! Float layout. +//! +//! See CSS 2.1 § 9.5.1: <https://www.w3.org/TR/CSS2/visuren.html#float-position> + +use std::collections::VecDeque; +use std::fmt::Debug; +use std::mem; +use std::ops::Range; + +use app_units::{Au, MAX_AU, MIN_AU}; +use euclid::num::Zero; +use malloc_size_of_derive::MallocSizeOf; +use servo_arc::Arc; +use style::computed_values::float::T as FloatProperty; +use style::computed_values::position::T as Position; +use style::logical_geometry::WritingMode; +use style::properties::ComputedValues; +use style::values::computed::Clear as StyleClear; + +use crate::context::LayoutContext; +use crate::dom::NodeExt; +use crate::dom_traversal::{Contents, NodeAndStyleInfo}; +use crate::formatting_contexts::IndependentFormattingContext; +use crate::fragment_tree::{BoxFragment, CollapsedMargin}; +use crate::geom::{LogicalRect, LogicalVec2, ToLogical}; +use crate::positioned::{PositioningContext, relative_adjustement}; +use crate::style_ext::{DisplayInside, PaddingBorderMargin}; +use crate::{ContainingBlock, PropagatedBoxTreeData}; + +/// A floating box. +#[derive(Debug, MallocSizeOf)] +pub(crate) struct FloatBox { + /// The formatting context that makes up the content of this box. + pub contents: IndependentFormattingContext, +} + +/// `FloatContext` positions floats relative to the independent block formatting +/// context which contains the floating elements. The Fragment tree positions +/// elements relative to their containing blocks. This data structure is used to +/// help map between these two coordinate systems. +#[derive(Clone, Copy, Debug)] +pub struct ContainingBlockPositionInfo { + /// The distance from the block start of the independent block formatting + /// context that contains the floats and the block start of the current + /// containing block, excluding uncollapsed block start margins. Note that + /// this does not include uncollapsed block start margins because we don't + /// know the value of collapsed margins until we lay out children. + pub(crate) block_start: Au, + /// Any uncollapsed block start margins that we have collected between the + /// block start of the float containing independent block formatting context + /// and this containing block, including for this containing block. + pub(crate) block_start_margins_not_collapsed: CollapsedMargin, + /// The distance from the inline start position of the float containing + /// independent formatting context and the inline start of this containing + /// block. + pub inline_start: Au, + /// The offset from the inline start position of the float containing + /// independent formatting context to the inline end of this containing + /// block. + pub inline_end: Au, +} + +impl ContainingBlockPositionInfo { + pub fn new_with_inline_offsets(inline_start: Au, inline_end: Au) -> Self { + Self { + block_start: Au::zero(), + block_start_margins_not_collapsed: CollapsedMargin::zero(), + inline_start, + inline_end, + } + } +} + +/// This data strucure is used to try to place non-floating content among float content. +/// This is used primarily to place replaced content and independent formatting contexts +/// next to floats, as the specifcation dictates. +pub(crate) struct PlacementAmongFloats<'a> { + /// The [FloatContext] to use for this placement. + float_context: &'a FloatContext, + /// The current bands we are considering for this placement. + current_bands: VecDeque<FloatBand>, + /// The next band, needed to know the height of the last band in current_bands. + next_band: FloatBand, + /// The size of the object to place. + object_size: LogicalVec2<Au>, + /// The minimum position in the block direction for the placement. Objects should not + /// be placed before this point. + ceiling: Au, + /// The inline position where the object would be if there were no floats. The object + /// can be placed after it due to floats, but not before it. + min_inline_start: Au, + /// The maximum inline position that the object can attain when avoiding floats. + max_inline_end: Au, +} + +impl<'a> PlacementAmongFloats<'a> { + pub(crate) fn new( + float_context: &'a FloatContext, + ceiling: Au, + object_size: LogicalVec2<Au>, + pbm: &PaddingBorderMargin, + ) -> Self { + let mut ceiling_band = float_context.bands.find(ceiling).unwrap(); + let (current_bands, next_band) = if ceiling == MAX_AU { + (VecDeque::new(), ceiling_band) + } else { + ceiling_band.top = ceiling; + let current_bands = VecDeque::from([ceiling_band]); + let next_band = float_context.bands.find_next(ceiling).unwrap(); + (current_bands, next_band) + }; + let min_inline_start = float_context.containing_block_info.inline_start + + pbm.margin.inline_start.auto_is(Au::zero); + let max_inline_end = (float_context.containing_block_info.inline_end - + pbm.margin.inline_end.auto_is(Au::zero)) + .max(min_inline_start + object_size.inline); + PlacementAmongFloats { + float_context, + current_bands, + next_band, + object_size, + ceiling, + min_inline_start, + max_inline_end, + } + } + + /// The top of the bands under consideration. This is initially the ceiling provided + /// during creation of this [`PlacementAmongFloats`], but may be larger if the top + /// band is discarded. + fn top_of_bands(&self) -> Option<Au> { + self.current_bands.front().map(|band| band.top) + } + + /// The height of the bands under consideration. + fn current_bands_height(&self) -> Au { + if self.next_band.top == MAX_AU { + // Treat MAX_AU as infinity. + MAX_AU + } else { + let top = self + .top_of_bands() + .expect("Should have bands before reaching the end"); + self.next_band.top - top + } + } + + /// Add a single band to the bands under consideration and calculate the new + /// [`PlacementAmongFloats::next_band`]. + fn add_one_band(&mut self) { + assert!(self.next_band.top != MAX_AU); + self.current_bands.push_back(self.next_band); + self.next_band = self + .float_context + .bands + .find_next(self.next_band.top) + .unwrap(); + } + + /// Adds bands to the set of bands under consideration until their block size is at + /// least large enough to contain the block size of the object being placed. + fn accumulate_enough_bands_for_block_size(&mut self) { + while self.current_bands_height() < self.object_size.block { + self.add_one_band(); + } + } + + /// Find the start and end of the inline space provided by the current set of bands + /// under consideration. + fn calculate_inline_start_and_end(&self) -> (Au, Au) { + let mut max_inline_start = self.min_inline_start; + let mut min_inline_end = self.max_inline_end; + for band in self.current_bands.iter() { + if let Some(inline_start) = band.inline_start { + max_inline_start.max_assign(inline_start); + } + if let Some(inline_end) = band.inline_end { + min_inline_end.min_assign(inline_end); + } + } + (max_inline_start, min_inline_end) + } + + /// Find the total inline size provided by the current set of bands under consideration. + fn calculate_viable_inline_size(&self) -> Au { + let (inline_start, inline_end) = self.calculate_inline_start_and_end(); + inline_end - inline_start + } + + fn try_place_once(&mut self) -> Option<LogicalRect<Au>> { + assert!(!self.current_bands.is_empty()); + self.accumulate_enough_bands_for_block_size(); + let (inline_start, inline_end) = self.calculate_inline_start_and_end(); + let available_inline_size = inline_end - inline_start; + if available_inline_size < self.object_size.inline { + return None; + } + Some(LogicalRect { + start_corner: LogicalVec2 { + inline: inline_start, + block: self.top_of_bands().unwrap(), + }, + size: LogicalVec2 { + inline: available_inline_size, + block: self.current_bands_height(), + }, + }) + } + + /// Checks if we either have bands or we have gone past all of them. + /// This is an invariant that should hold, otherwise we are in a broken state. + fn has_bands_or_at_end(&self) -> bool { + !self.current_bands.is_empty() || self.next_band.top == MAX_AU + } + + fn pop_front_band_ensuring_has_bands_or_at_end(&mut self) { + self.current_bands.pop_front(); + if !self.has_bands_or_at_end() { + self.add_one_band(); + } + } + + /// Run the placement algorithm for this [PlacementAmongFloats]. + pub(crate) fn place(&mut self) -> LogicalRect<Au> { + debug_assert!(self.has_bands_or_at_end()); + while !self.current_bands.is_empty() { + if let Some(result) = self.try_place_once() { + return result; + } + self.pop_front_band_ensuring_has_bands_or_at_end(); + } + debug_assert!(self.has_bands_or_at_end()); + + // We could not fit the object in among the floats, so we place it as if it + // cleared all floats. + LogicalRect { + start_corner: LogicalVec2 { + inline: self.min_inline_start, + block: self + .ceiling + .max(self.float_context.clear_inline_start_position) + .max(self.float_context.clear_inline_end_position), + }, + size: LogicalVec2 { + inline: self.max_inline_end - self.min_inline_start, + block: MAX_AU, + }, + } + } + + /// After placing an object with `height: auto` (and using the minimum inline and + /// block size as the object size) and then laying it out, try to fit the object into + /// the current set of bands, given block size after layout and the available inline + /// space from the original placement. This will return true if the object fits at the + /// original placement location or false if the placement and layout must be run again + /// (with this [PlacementAmongFloats]). + pub(crate) fn try_to_expand_for_auto_block_size( + &mut self, + block_size_after_layout: Au, + size_from_placement: &LogicalVec2<Au>, + ) -> bool { + debug_assert!(self.has_bands_or_at_end()); + debug_assert_eq!(size_from_placement.block, self.current_bands_height()); + debug_assert_eq!( + size_from_placement.inline, + self.calculate_viable_inline_size() + ); + + // If the object after layout fits into the originally calculated placement, then + // it fits without any more work. + if block_size_after_layout <= size_from_placement.block { + return true; + } + + // Keep searching until we have found an area with enough height + // to contain the block after layout. + let old_num_bands = self.current_bands.len(); + assert!(old_num_bands > 0); + while self.current_bands_height() < block_size_after_layout { + self.add_one_band(); + + // If the new inline size is narrower, we must stop and run layout again. + // Normally, a narrower block size means a bigger height, but in some + // circumstances, such as when aspect ratio is used a narrower inline size + // can counter-interuitively lead to a smaller block size after layout! + let available_inline_size = self.calculate_viable_inline_size(); + if available_inline_size < size_from_placement.inline { + // If the inline size becomes smaller than the minimum inline size, then + // the current set of bands will never work and we must try removing the + // first and searching starting from the second. + if available_inline_size < self.object_size.inline { + self.next_band = self.current_bands[old_num_bands]; + self.current_bands.truncate(old_num_bands); + self.pop_front_band_ensuring_has_bands_or_at_end(); + } + return false; + } + } + true + } +} + +/// Data kept during layout about the floats in a given block formatting context. +/// +/// This is a persistent data structure. Each float has its own private copy of the float context, +/// although such copies may share portions of the `bands` tree. +#[derive(Clone, Debug)] +pub struct FloatContext { + /// A persistent AA tree of float bands. + /// + /// This tree is immutable; modification operations return the new tree, which may share nodes + /// with previous versions of the tree. + pub bands: FloatBandTree, + /// The block-direction "ceiling" defined by the placement of other floated content of + /// this FloatContext. No new floats can be placed at a lower block start than this value. + pub ceiling_from_floats: Au, + /// The block-direction "ceiling" defined by the placement of non-floated content that + /// precedes floated content in the document. Note that this may actually decrease as + /// content is laid out in the case that content overflows its container. + pub ceiling_from_non_floats: Au, + /// Details about the position of the containing block relative to the + /// independent block formatting context that contains all of the floats + /// this `FloatContext` positions. + pub containing_block_info: ContainingBlockPositionInfo, + /// The (logically) lowest margin edge of the last inline-start float. + pub clear_inline_start_position: Au, + /// The (logically) lowest margin edge of the last inline-end float. + pub clear_inline_end_position: Au, +} + +impl FloatContext { + /// Returns a new float context representing a containing block with the given content + /// inline-size. + pub fn new(max_inline_size: Au) -> Self { + let mut bands = FloatBandTree::new(); + bands = bands.insert(FloatBand { + top: MIN_AU, + inline_start: None, + inline_end: None, + }); + bands = bands.insert(FloatBand { + top: MAX_AU, + inline_start: None, + inline_end: None, + }); + FloatContext { + bands, + ceiling_from_floats: Au::zero(), + ceiling_from_non_floats: Au::zero(), + containing_block_info: ContainingBlockPositionInfo::new_with_inline_offsets( + Au::zero(), + max_inline_size, + ), + clear_inline_start_position: Au::zero(), + clear_inline_end_position: Au::zero(), + } + } + + /// (Logically) lowers the ceiling to at least `new_ceiling` units. + /// + /// If the ceiling is already logically lower (i.e. larger) than this, does nothing. + pub fn set_ceiling_from_non_floats(&mut self, new_ceiling: Au) { + self.ceiling_from_non_floats = new_ceiling; + } + + /// The "ceiling" used for float placement. This is the minimum block position value + /// that should be used for placing any new float. + fn ceiling(&mut self) -> Au { + self.ceiling_from_floats.max(self.ceiling_from_non_floats) + } + + /// Determines where a float with the given placement would go, but leaves the float context + /// unmodified. Returns the start corner of its margin box. + /// + /// This should be used for placing inline elements and block formatting contexts so that they + /// don't collide with floats. + pub(crate) fn place_object(&self, object: &PlacementInfo, ceiling: Au) -> LogicalVec2<Au> { + let ceiling = match object.clear { + Clear::None => ceiling, + Clear::InlineStart => ceiling.max(self.clear_inline_start_position), + Clear::InlineEnd => ceiling.max(self.clear_inline_end_position), + Clear::Both => ceiling + .max(self.clear_inline_start_position) + .max(self.clear_inline_end_position), + }; + + // Find the first band this float fits in. + let mut first_band = self.bands.find(ceiling).unwrap(); + while !first_band.object_fits(object, &self.containing_block_info) { + let next_band = self.bands.find_next(first_band.top).unwrap(); + if next_band.top == MAX_AU { + break; + } + first_band = next_band; + } + + // The object fits perfectly here. Place it. + match object.side { + FloatSide::InlineStart => { + let inline_start_object_edge = match first_band.inline_start { + Some(inline_start) => inline_start.max(self.containing_block_info.inline_start), + None => self.containing_block_info.inline_start, + }; + LogicalVec2 { + inline: inline_start_object_edge, + block: first_band.top.max(ceiling), + } + }, + FloatSide::InlineEnd => { + let inline_end_object_edge = match first_band.inline_end { + Some(inline_end) => inline_end.min(self.containing_block_info.inline_end), + None => self.containing_block_info.inline_end, + }; + LogicalVec2 { + inline: inline_end_object_edge - object.size.inline, + block: first_band.top.max(ceiling), + } + }, + } + } + + /// Places a new float and adds it to the list. Returns the start corner of its margin box. + pub fn add_float(&mut self, new_float: &PlacementInfo) -> LogicalVec2<Au> { + // Place the float. + let ceiling = self.ceiling(); + let new_float_origin = self.place_object(new_float, ceiling); + let new_float_extent = match new_float.side { + FloatSide::InlineStart => new_float_origin.inline + new_float.size.inline, + FloatSide::InlineEnd => new_float_origin.inline, + }; + + let new_float_rect = LogicalRect { + start_corner: new_float_origin, + // If this float has a negative margin, we should only consider its non-negative + // block size contribution when determing where to place it. When the margin is + // so negative that it's placed completely above the current float ceiling, then + // we should position it as if it had zero block size. + size: LogicalVec2 { + inline: new_float.size.inline.max(Au::zero()), + block: new_float.size.block.max(Au::zero()), + }, + }; + + // Update clear. + match new_float.side { + FloatSide::InlineStart => { + self.clear_inline_start_position + .max_assign(new_float_rect.max_block_position()); + }, + FloatSide::InlineEnd => { + self.clear_inline_end_position + .max_assign(new_float_rect.max_block_position()); + }, + } + + // Split the first band if necessary. + let mut first_band = self.bands.find(new_float_rect.start_corner.block).unwrap(); + first_band.top = new_float_rect.start_corner.block; + self.bands = self.bands.insert(first_band); + + // Split the last band if necessary. + let mut last_band = self + .bands + .find(new_float_rect.max_block_position()) + .unwrap(); + last_band.top = new_float_rect.max_block_position(); + self.bands = self.bands.insert(last_band); + + // Update all bands that contain this float to reflect the new available size. + let block_range = new_float_rect.start_corner.block..new_float_rect.max_block_position(); + self.bands = self + .bands + .set_range(&block_range, new_float.side, new_float_extent); + + // CSS 2.1 § 9.5.1 rule 6: The outer top of a floating box may not be higher than the outer + // top of any block or floated box generated by an element earlier in the source document. + self.ceiling_from_floats + .max_assign(new_float_rect.start_corner.block); + + new_float_rect.start_corner + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Clear { + None, + InlineStart, + InlineEnd, + Both, +} + +impl Clear { + pub(crate) fn from_style_and_container_writing_mode( + style: &ComputedValues, + container_writing_mode: WritingMode, + ) -> Self { + match style.get_box().clear { + StyleClear::None => Self::None, + StyleClear::Both => Self::Both, + StyleClear::InlineStart => Self::InlineStart, + StyleClear::InlineEnd => Self::InlineEnd, + StyleClear::Left if container_writing_mode.is_bidi_ltr() => Self::InlineStart, + StyleClear::Left => Self::InlineEnd, + StyleClear::Right if container_writing_mode.is_bidi_ltr() => Self::InlineEnd, + StyleClear::Right => Self::InlineStart, + } + } +} + +/// Information needed to place a float so that it doesn't collide with existing floats. +#[derive(Clone, Debug)] +pub struct PlacementInfo { + /// The *margin* box size of the float. + pub size: LogicalVec2<Au>, + /// Which side of the containing block the float is aligned to. + pub side: FloatSide, + /// Which side or sides to clear existing floats on. + pub clear: Clear, +} + +/// Whether the float is aligned to the inline-start or inline-end side of its containing block. +/// +/// See CSS 2.1 § 9.5.1: <https://www.w3.org/TR/CSS2/visuren.html#float-position> +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum FloatSide { + InlineStart, + InlineEnd, +} + +/// Internal data structure that describes a nonoverlapping vertical region in which floats may be placed. +/// Floats must go between "inline-start edge + `inline_start`" and "inline-end edge - `inline_end`". +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FloatBand { + /// The logical vertical position of the top of this band. + pub top: Au, + /// The distance from the inline-start edge of the block formatting context to the first legal + /// (logically) horizontal position where floats may be placed. If `None`, there are no floats + /// to the inline-start; distinguishing between the cases of "a zero-width float is present" and + /// "no floats at all are present" is necessary to, for example, clear past zero-width floats. + pub inline_start: Option<Au>, + /// The distance from the *inline-start* edge of the block formatting context to the last legal + /// (logically) horizontal position where floats may be placed. If `None`, there are no floats + /// to the inline-end; distinguishing between the cases of "a zero-width float is present" and + /// "no floats at all are present" is necessary to, for example, clear past zero-width floats. + pub inline_end: Option<Au>, +} + +impl FloatSide { + pub(crate) fn from_style_and_container_writing_mode( + style: &ComputedValues, + container_writing_mode: WritingMode, + ) -> Option<FloatSide> { + Some(match style.get_box().float { + FloatProperty::None => return None, + FloatProperty::InlineStart => Self::InlineStart, + FloatProperty::InlineEnd => Self::InlineEnd, + FloatProperty::Left if container_writing_mode.is_bidi_ltr() => Self::InlineStart, + FloatProperty::Left => Self::InlineEnd, + FloatProperty::Right if container_writing_mode.is_bidi_ltr() => Self::InlineEnd, + FloatProperty::Right => Self::InlineStart, + }) + } +} + +impl FloatBand { + /// Determines whether an object fits in a band. Returns true if the object fits. + fn object_fits(&self, object: &PlacementInfo, walls: &ContainingBlockPositionInfo) -> bool { + match object.side { + FloatSide::InlineStart => { + // Compute a candidate inline-start position for the object. + let candidate_inline_start = match self.inline_start { + None => walls.inline_start, + Some(inline_start) => inline_start.max(walls.inline_start), + }; + + // If this band has an existing inline-start float in it, then make sure that the object + // doesn't stick out past the inline-end edge (rule 7). + if self.inline_start.is_some() && + candidate_inline_start + object.size.inline > walls.inline_end + { + return false; + } + + // If this band has an existing inline-end float in it, make sure we don't collide with + // it (rule 3). + match self.inline_end { + None => true, + Some(inline_end) => object.size.inline <= inline_end - candidate_inline_start, + } + }, + + FloatSide::InlineEnd => { + // Compute a candidate inline-end position for the object. + let candidate_inline_end = match self.inline_end { + None => walls.inline_end, + Some(inline_end) => inline_end.min(walls.inline_end), + }; + + // If this band has an existing inline-end float in it, then make sure that the new + // object doesn't stick out past the inline-start edge (rule 7). + if self.inline_end.is_some() && + candidate_inline_end - object.size.inline < walls.inline_start + { + return false; + } + + // If this band has an existing inline-start float in it, make sure we don't collide with + // it (rule 3). + match self.inline_start { + None => true, + Some(inline_start) => object.size.inline <= candidate_inline_end - inline_start, + } + }, + } + } +} + +// Float band storage + +/// A persistent AA tree for float band storage. +/// +/// Bands here are nonoverlapping, and there is guaranteed to be a band at block-position 0 and +/// another band at block-position infinity. +/// +/// AA trees were chosen for simplicity. +/// +/// See: <https://en.wikipedia.org/wiki/AA_tree> +/// <https://arxiv.org/pdf/1412.4882.pdf> +#[derive(Clone, Debug)] +pub struct FloatBandTree { + pub root: FloatBandLink, +} + +/// A single edge (or lack thereof) in the float band tree. +#[derive(Clone, Debug)] +pub struct FloatBandLink(pub Option<Arc<FloatBandNode>>); + +/// A single node in the float band tree. +#[derive(Clone, Debug)] +pub struct FloatBandNode { + /// The actual band. + pub band: FloatBand, + /// The left child. + pub left: FloatBandLink, + /// The right child. + pub right: FloatBandLink, + /// The level, which increases as you go up the tree. + /// + /// This value is needed for tree balancing. + pub level: i32, +} + +impl FloatBandTree { + /// Creates a new float band tree. + pub fn new() -> FloatBandTree { + FloatBandTree { + root: FloatBandLink(None), + } + } + + /// Returns the first band whose top is less than or equal to the given `block_position`. + pub fn find(&self, block_position: Au) -> Option<FloatBand> { + self.root.find(block_position) + } + + /// Returns the first band whose top is strictly greater than to the given `block_position`. + pub fn find_next(&self, block_position: Au) -> Option<FloatBand> { + self.root.find_next(block_position) + } + + /// Sets the side values of all bands within the given half-open range to be at least + /// `new_value`. + #[must_use] + pub fn set_range(&self, range: &Range<Au>, side: FloatSide, new_value: Au) -> FloatBandTree { + FloatBandTree { + root: FloatBandLink( + self.root + .0 + .as_ref() + .map(|root| root.set_range(range, side, new_value)), + ), + } + } + + /// Inserts a new band into the tree. If the band has the same level as a pre-existing one, + /// replaces the existing band with the new one. + #[must_use] + pub fn insert(&self, band: FloatBand) -> FloatBandTree { + FloatBandTree { + root: self.root.insert(band), + } + } +} + +impl Default for FloatBandTree { + fn default() -> Self { + Self::new() + } +} + +impl FloatBandNode { + fn new(band: FloatBand) -> FloatBandNode { + FloatBandNode { + band, + left: FloatBandLink(None), + right: FloatBandLink(None), + level: 1, + } + } + + /// Sets the side values of all bands within the given half-open range to be at least + /// `new_value`. + fn set_range(&self, range: &Range<Au>, side: FloatSide, new_value: Au) -> Arc<FloatBandNode> { + let mut new_band = self.band; + if self.band.top >= range.start && self.band.top < range.end { + match side { + FloatSide::InlineStart => { + new_band.inline_start = match new_band.inline_start { + Some(old_value) => Some(std::cmp::max(old_value, new_value)), + None => Some(new_value), + }; + }, + FloatSide::InlineEnd => { + new_band.inline_end = match new_band.inline_end { + Some(old_value) => Some(std::cmp::min(old_value, new_value)), + None => Some(new_value), + }; + }, + } + } + + let new_left = match self.left.0 { + None => FloatBandLink(None), + Some(ref old_left) if range.start < new_band.top => { + FloatBandLink(Some(old_left.set_range(range, side, new_value))) + }, + Some(ref old_left) => FloatBandLink(Some((*old_left).clone())), + }; + + let new_right = match self.right.0 { + None => FloatBandLink(None), + Some(ref old_right) if range.end > new_band.top => { + FloatBandLink(Some(old_right.set_range(range, side, new_value))) + }, + Some(ref old_right) => FloatBandLink(Some((*old_right).clone())), + }; + + Arc::new(FloatBandNode { + band: new_band, + left: new_left, + right: new_right, + level: self.level, + }) + } +} + +impl FloatBandLink { + /// Returns the first band whose top is less than or equal to the given `block_position`. + fn find(&self, block_position: Au) -> Option<FloatBand> { + let this = match self.0 { + None => return None, + Some(ref node) => node, + }; + + if block_position < this.band.top { + return this.left.find(block_position); + } + + // It's somewhere in this subtree, but we aren't sure whether it's here or in the right + // subtree. + if let Some(band) = this.right.find(block_position) { + return Some(band); + } + + Some(this.band) + } + + /// Returns the first band whose top is strictly greater than the given `block_position`. + fn find_next(&self, block_position: Au) -> Option<FloatBand> { + let this = match self.0 { + None => return None, + Some(ref node) => node, + }; + + if block_position >= this.band.top { + return this.right.find_next(block_position); + } + + // It's somewhere in this subtree, but we aren't sure whether it's here or in the left + // subtree. + if let Some(band) = this.left.find_next(block_position) { + return Some(band); + } + + Some(this.band) + } + + /// Inserts a new band into the tree. If the band has the same level as a pre-existing one, + /// replaces the existing band with the new one. + fn insert(&self, band: FloatBand) -> FloatBandLink { + let mut this = match self.0 { + None => return FloatBandLink(Some(Arc::new(FloatBandNode::new(band)))), + Some(ref this) => (**this).clone(), + }; + + if band.top < this.band.top { + this.left = this.left.insert(band); + return FloatBandLink(Some(Arc::new(this))).skew().split(); + } + if band.top > this.band.top { + this.right = this.right.insert(band); + return FloatBandLink(Some(Arc::new(this))).skew().split(); + } + + this.band = band; + FloatBandLink(Some(Arc::new(this))) + } + + /// Corrects tree balance: + ///```text + /// T L + /// / \ / \ + /// L R → A T if level(T) = level(L) + /// / \ / \ + /// A B B R + /// ``` + fn skew(&self) -> FloatBandLink { + if let Some(ref this) = self.0 { + if let Some(ref left) = this.left.0 { + if this.level == left.level { + return FloatBandLink(Some(Arc::new(FloatBandNode { + level: this.level, + left: left.left.clone(), + band: left.band, + right: FloatBandLink(Some(Arc::new(FloatBandNode { + level: this.level, + left: left.right.clone(), + band: this.band, + right: this.right.clone(), + }))), + }))); + } + } + } + + (*self).clone() + } + + /// Corrects tree balance: + ///```text + /// T R + /// / \ / \ + /// A R → T X if level(T) = level(X) + /// / \ / \ + /// B X A B + /// ``` + fn split(&self) -> FloatBandLink { + if let Some(ref this) = self.0 { + if let Some(ref right) = this.right.0 { + if let Some(ref right_right) = right.right.0 { + if this.level == right_right.level { + return FloatBandLink(Some(Arc::new(FloatBandNode { + level: this.level + 1, + left: FloatBandLink(Some(Arc::new(FloatBandNode { + level: this.level, + left: this.left.clone(), + band: this.band, + right: right.left.clone(), + }))), + band: right.band, + right: right.right.clone(), + }))); + } + } + } + } + + (*self).clone() + } +} + +impl FloatBox { + /// Creates a new float box. + pub fn construct<'dom>( + context: &LayoutContext, + info: &NodeAndStyleInfo<impl NodeExt<'dom>>, + display_inside: DisplayInside, + contents: Contents, + propagated_data: PropagatedBoxTreeData, + ) -> Self { + Self { + contents: IndependentFormattingContext::construct( + context, + info, + display_inside, + contents, + // Text decorations are not propagated to any out-of-flow descendants + propagated_data.without_text_decorations(), + ), + } + } + + /// Lay out this float box and its children. Note that the position will be relative to + /// the float containing block formatting context. A later step adjusts the position + /// to be relative to the containing block. + pub fn layout( + &self, + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + containing_block: &ContainingBlock, + ) -> BoxFragment { + let style = self.contents.style().clone(); + positioning_context.layout_maybe_position_relative_fragment( + layout_context, + containing_block, + &style, + |positioning_context| { + self.contents + .layout_float_or_atomic_inline( + layout_context, + positioning_context, + containing_block, + ) + .fragment + }, + ) + } +} + +/// Layout state that we maintain when doing sequential traversals of the box tree in document +/// order. +/// +/// This data is only needed for float placement and float interaction, and as such is only present +/// if the current block formatting context contains floats. +/// +/// All coordinates here are relative to the start of the nearest ancestor block formatting context. +/// +/// This structure is expected to be cheap to clone, in order to allow for "snapshots" that enable +/// restarting layout at any point in the tree. +#[derive(Clone)] +pub(crate) struct SequentialLayoutState { + /// Holds all floats in this block formatting context. + pub(crate) floats: FloatContext, + /// The (logically) bottom border edge or top padding edge of the last in-flow block. Floats + /// cannot be placed above this line. + /// + /// This is often, but not always, the same as the float ceiling. The float ceiling can be lower + /// than this value because this value is calculated based on in-flow boxes only, while + /// out-of-flow floats can affect the ceiling as well (see CSS 2.1 § 9.5.1 rule 6). + pub(crate) bfc_relative_block_position: Au, + /// Any collapsible margins that we've encountered after `bfc_relative_block_position`. + pub(crate) current_margin: CollapsedMargin, +} + +impl SequentialLayoutState { + /// Creates a new empty `SequentialLayoutState`. + pub(crate) fn new(max_inline_size: Au) -> SequentialLayoutState { + SequentialLayoutState { + floats: FloatContext::new(max_inline_size), + current_margin: CollapsedMargin::zero(), + bfc_relative_block_position: Au::zero(), + } + } + + /// Moves the current block position (logically) down by `block_distance`. This may be + /// a negative advancement in the case that that block content overflows its + /// container, when the container is adjusting the block position of the + /// [`SequentialLayoutState`] after processing its overflowing content. + /// + /// Floats may not be placed higher than the current block position. + pub(crate) fn advance_block_position(&mut self, block_distance: Au) { + self.bfc_relative_block_position += block_distance; + self.floats + .set_ceiling_from_non_floats(self.bfc_relative_block_position); + } + + /// Replace the entire [ContainingBlockPositionInfo] data structure stored + /// by this [SequentialLayoutState]. Return the old data structure. + pub(crate) fn replace_containing_block_position_info( + &mut self, + mut position_info: ContainingBlockPositionInfo, + ) -> ContainingBlockPositionInfo { + mem::swap(&mut position_info, &mut self.floats.containing_block_info); + position_info + } + + /// Return the current block position in the float containing block formatting + /// context and any uncollapsed block margins. + pub(crate) fn current_block_position_including_margins(&self) -> Au { + self.bfc_relative_block_position + self.current_margin.solve() + } + + /// Collapses margins, moving the block position down by the collapsed value of `current_margin` + /// and resetting `current_margin` to zero. + /// + /// Call this method before laying out children when it is known that the start margin of the + /// current fragment can't collapse with the margins of any of its children. + pub(crate) fn collapse_margins(&mut self) { + self.advance_block_position(self.current_margin.solve()); + self.current_margin = CollapsedMargin::zero(); + } + + /// Computes the position of the block-start border edge of an element + /// with the provided `block_start_margin`, assuming no clearance. + pub(crate) fn position_without_clearance(&self, block_start_margin: &CollapsedMargin) -> Au { + // Adjoin `current_margin` and `block_start_margin` since there is no clearance. + self.bfc_relative_block_position + self.current_margin.adjoin(block_start_margin).solve() + } + + /// Computes the position of the block-start border edge of an element + /// with the provided `block_start_margin`, assuming a clearance of 0px. + pub(crate) fn position_with_zero_clearance(&self, block_start_margin: &CollapsedMargin) -> Au { + // Clearance prevents `current_margin` and `block_start_margin` from being + // adjoining, so we need to solve them separately and then sum. + self.bfc_relative_block_position + self.current_margin.solve() + block_start_margin.solve() + } + + /// Returns the block-end outer edge of the lowest float that is to be cleared (if any) + /// by an element with the provided `clear` and `block_start_margin`. + pub(crate) fn calculate_clear_position( + &self, + clear: Clear, + block_start_margin: &CollapsedMargin, + ) -> Option<Au> { + if clear == Clear::None { + return None; + } + + // Calculate the hypothetical position where the element's top border edge + // would have been if the element's `clear` property had been `none`. + let hypothetical_block_position = self.position_without_clearance(block_start_margin); + + // Check if the hypothetical position is past the relevant floats, + // in that case we don't need to add clearance. + let clear_position = match clear { + Clear::None => unreachable!(), + Clear::InlineStart => self.floats.clear_inline_start_position, + Clear::InlineEnd => self.floats.clear_inline_end_position, + Clear::Both => self + .floats + .clear_inline_start_position + .max(self.floats.clear_inline_end_position), + }; + if hypothetical_block_position >= clear_position { + None + } else { + Some(clear_position) + } + } + + /// Returns the amount of clearance (if any) that a block with the given `clear` value + /// needs to have at `current_block_position_including_margins()`. + /// `block_start_margin` is the top margin of the block, after collapsing (if possible) + /// with the margin of its contents. This must not be included in `current_margin`, + /// since adding clearance will prevent `current_margin` and `block_start_margin` + /// from collapsing together. + /// + /// <https://www.w3.org/TR/2011/REC-CSS2-20110607/visuren.html#flow-control> + pub(crate) fn calculate_clearance( + &self, + clear: Clear, + block_start_margin: &CollapsedMargin, + ) -> Option<Au> { + self.calculate_clear_position(clear, block_start_margin) + .map(|offset| offset - self.position_with_zero_clearance(block_start_margin)) + } + + /// A block that is replaced or establishes an independent formatting context can't overlap floats, + /// it has to be placed next to them, and may get some clearance if there isn't enough space. + /// Given such a block with the provided 'clear', 'block_start_margin', 'pbm' and 'object_size', + /// this method finds an area that is big enough and doesn't overlap floats. + /// It returns a tuple with: + /// - The clearance amount (if any), which includes both the effect of 'clear' + /// and the extra space to avoid floats. + /// - The LogicalRect in which the block can be placed without overlapping floats. + pub(crate) fn calculate_clearance_and_inline_adjustment( + &self, + clear: Clear, + block_start_margin: &CollapsedMargin, + pbm: &PaddingBorderMargin, + object_size: LogicalVec2<Au>, + ) -> (Option<Au>, LogicalRect<Au>) { + // First compute the clear position required by the 'clear' property. + // The code below may then add extra clearance when the element can't fit + // next to floats not covered by 'clear'. + let clear_position = self.calculate_clear_position(clear, block_start_margin); + let ceiling = + clear_position.unwrap_or_else(|| self.position_without_clearance(block_start_margin)); + let mut placement = PlacementAmongFloats::new(&self.floats, ceiling, object_size, pbm); + let placement_rect = placement.place(); + let position = &placement_rect.start_corner; + let has_clearance = clear_position.is_some() || position.block > ceiling; + let clearance = has_clearance + .then(|| position.block - self.position_with_zero_clearance(block_start_margin)); + (clearance, placement_rect) + } + + /// Adds a new adjoining margin. + pub(crate) fn adjoin_assign(&mut self, margin: &CollapsedMargin) { + self.current_margin.adjoin_assign(margin) + } + + /// Get the offset of the current containing block and any uncollapsed margins. + pub(crate) fn current_containing_block_offset(&self) -> Au { + self.floats.containing_block_info.block_start + + self.floats + .containing_block_info + .block_start_margins_not_collapsed + .solve() + } + + /// This function places a Fragment that has been created for a FloatBox. + pub(crate) fn place_float_fragment( + &mut self, + box_fragment: &mut BoxFragment, + containing_block: &ContainingBlock, + margins_collapsing_with_parent_containing_block: CollapsedMargin, + block_offset_from_containing_block_top: Au, + ) { + let block_start_of_containing_block_in_bfc = self.floats.containing_block_info.block_start + + self.floats + .containing_block_info + .block_start_margins_not_collapsed + .adjoin(&margins_collapsing_with_parent_containing_block) + .solve(); + + self.floats.set_ceiling_from_non_floats( + block_start_of_containing_block_in_bfc + block_offset_from_containing_block_top, + ); + + let container_writing_mode = containing_block.style.writing_mode; + let logical_float_size = box_fragment + .content_rect + .size + .to_logical(container_writing_mode); + let pbm_sums = box_fragment + .padding_border_margin() + .to_logical(container_writing_mode); + let margin_box_start_corner = self.floats.add_float(&PlacementInfo { + size: logical_float_size + pbm_sums.sum(), + side: FloatSide::from_style_and_container_writing_mode( + &box_fragment.style, + container_writing_mode, + ) + .expect("Float box wasn't floated!"), + clear: Clear::from_style_and_container_writing_mode( + &box_fragment.style, + container_writing_mode, + ), + }); + + // Re-calculate relative adjustment so that it is not lost when the BoxFragment's + // `content_rect` is overwritten below. + let relative_offset = match box_fragment.style.clone_position() { + Position::Relative => relative_adjustement(&box_fragment.style, containing_block), + _ => LogicalVec2::zero(), + }; + + // This is the position of the float in the float-containing block formatting context. We add the + // existing start corner here because we may have already gotten some relative positioning offset. + let new_position_in_bfc = + margin_box_start_corner + pbm_sums.start_offset() + relative_offset; + + // This is the position of the float relative to the containing block start. + let new_position_in_containing_block = LogicalVec2 { + inline: new_position_in_bfc.inline - self.floats.containing_block_info.inline_start, + block: new_position_in_bfc.block - block_start_of_containing_block_in_bfc, + }; + + box_fragment.content_rect = LogicalRect { + start_corner: new_position_in_containing_block, + size: box_fragment + .content_rect + .size + .to_logical(container_writing_mode), + } + .as_physical(Some(containing_block)); + } +} diff --git a/components/layout/flow/inline/construct.rs b/components/layout/flow/inline/construct.rs new file mode 100644 index 00000000000..7c668751ef6 --- /dev/null +++ b/components/layout/flow/inline/construct.rs @@ -0,0 +1,636 @@ +/* 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 std::borrow::Cow; +use std::char::{ToLowercase, ToUppercase}; + +use icu_segmenter::WordSegmenter; +use servo_arc::Arc; +use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; +use style::values::specified::text::TextTransformCase; +use unicode_bidi::Level; + +use super::text_run::TextRun; +use super::{InlineBox, InlineBoxIdentifier, InlineBoxes, InlineFormattingContext, InlineItem}; +use crate::PropagatedBoxTreeData; +use crate::cell::ArcRefCell; +use crate::context::LayoutContext; +use crate::dom::NodeExt; +use crate::dom_traversal::NodeAndStyleInfo; +use crate::flow::float::FloatBox; +use crate::formatting_contexts::IndependentFormattingContext; +use crate::positioned::AbsolutelyPositionedBox; +use crate::style_ext::ComputedValuesExt; + +#[derive(Default)] +pub(crate) struct InlineFormattingContextBuilder { + /// The collection of text strings that make up this [`InlineFormattingContext`] under + /// construction. + pub text_segments: Vec<String>, + + /// The current offset in the final text string of this [`InlineFormattingContext`], + /// used to properly set the text range of new [`InlineItem::TextRun`]s. + current_text_offset: usize, + + /// Whether the last processed node ended with whitespace. This is used to + /// implement rule 4 of <https://www.w3.org/TR/css-text-3/#collapse>: + /// + /// > Any collapsible space immediately following another collapsible space—even one + /// > outside the boundary of the inline containing that space, provided both spaces are + /// > within the same inline formatting context—is collapsed to have zero advance width. + /// > (It is invisible, but retains its soft wrap opportunity, if any.) + last_inline_box_ended_with_collapsible_white_space: bool, + + /// Whether or not the current state of the inline formatting context is on a word boundary + /// for the purposes of `text-transform: capitalize`. + on_word_boundary: bool, + + /// Whether or not this inline formatting context will contain floats. + pub contains_floats: bool, + + /// The current list of [`InlineItem`]s in this [`InlineFormattingContext`] under + /// construction. This is stored in a flat list to make it easy to access the last + /// item. + pub inline_items: Vec<ArcRefCell<InlineItem>>, + + /// The current [`InlineBox`] tree of this [`InlineFormattingContext`] under construction. + pub inline_boxes: InlineBoxes, + + /// The ongoing stack of inline boxes stack of the builder. + /// + /// Contains all the currently ongoing inline boxes we entered so far. + /// The traversal is at all times as deep in the tree as this stack is, + /// which is why the code doesn't need to keep track of the actual + /// container root (see `handle_inline_level_element`). + /// + /// When an inline box ends, it's removed from this stack. + inline_box_stack: Vec<InlineBoxIdentifier>, + + /// Whether or not the inline formatting context under construction has any + /// uncollapsible text content. + pub has_uncollapsible_text_content: bool, +} + +impl InlineFormattingContextBuilder { + pub(crate) fn new() -> Self { + // For the purposes of `text-transform: capitalize` the start of the IFC is a word boundary. + Self { + on_word_boundary: true, + ..Default::default() + } + } + + pub(crate) fn currently_processing_inline_box(&self) -> bool { + !self.inline_box_stack.is_empty() + } + + fn push_control_character_string(&mut self, string_to_push: &str) { + self.text_segments.push(string_to_push.to_owned()); + self.current_text_offset += string_to_push.len(); + } + + /// Return true if this [`InlineFormattingContextBuilder`] is empty for the purposes of ignoring + /// during box tree construction. An IFC is empty if it only contains TextRuns with + /// completely collapsible whitespace. When that happens it can be ignored completely. + pub(crate) fn is_empty(&self) -> bool { + if self.has_uncollapsible_text_content { + return false; + } + + if !self.inline_box_stack.is_empty() { + return false; + } + + fn inline_level_box_is_empty(inline_level_box: &InlineItem) -> bool { + match inline_level_box { + InlineItem::StartInlineBox(_) => false, + InlineItem::EndInlineBox => false, + // Text content is handled by `self.has_uncollapsible_text` content above in order + // to avoid having to iterate through the character once again. + InlineItem::TextRun(_) => true, + InlineItem::OutOfFlowAbsolutelyPositionedBox(..) => false, + InlineItem::OutOfFlowFloatBox(_) => false, + InlineItem::Atomic(..) => false, + } + } + + self.inline_items + .iter() + .all(|inline_level_box| inline_level_box_is_empty(&inline_level_box.borrow())) + } + + pub(crate) fn push_atomic( + &mut self, + independent_formatting_context: IndependentFormattingContext, + ) -> ArcRefCell<InlineItem> { + let inline_level_box = ArcRefCell::new(InlineItem::Atomic( + Arc::new(independent_formatting_context), + self.current_text_offset, + Level::ltr(), /* This will be assigned later if necessary. */ + )); + self.inline_items.push(inline_level_box.clone()); + + // Push an object replacement character for this atomic, which will ensure that the line breaker + // inserts a line breaking opportunity here. + self.push_control_character_string("\u{fffc}"); + + self.last_inline_box_ended_with_collapsible_white_space = false; + self.on_word_boundary = true; + + inline_level_box + } + + pub(crate) fn push_absolutely_positioned_box( + &mut self, + absolutely_positioned_box: AbsolutelyPositionedBox, + ) -> ArcRefCell<InlineItem> { + let absolutely_positioned_box = ArcRefCell::new(absolutely_positioned_box); + let inline_level_box = ArcRefCell::new(InlineItem::OutOfFlowAbsolutelyPositionedBox( + absolutely_positioned_box, + self.current_text_offset, + )); + + self.inline_items.push(inline_level_box.clone()); + inline_level_box + } + + pub(crate) fn push_float_box(&mut self, float_box: FloatBox) -> ArcRefCell<InlineItem> { + let inline_level_box = ArcRefCell::new(InlineItem::OutOfFlowFloatBox(Arc::new(float_box))); + self.inline_items.push(inline_level_box.clone()); + self.contains_floats = true; + inline_level_box + } + + pub(crate) fn start_inline_box(&mut self, inline_box: InlineBox) -> ArcRefCell<InlineItem> { + self.push_control_character_string(inline_box.base.style.bidi_control_chars().0); + + let (identifier, inline_box) = self.inline_boxes.start_inline_box(inline_box); + let inline_level_box = ArcRefCell::new(InlineItem::StartInlineBox(inline_box)); + self.inline_items.push(inline_level_box.clone()); + self.inline_box_stack.push(identifier); + inline_level_box + } + + pub(crate) fn end_inline_box(&mut self) -> ArcRefCell<InlineBox> { + let identifier = self.end_inline_box_internal(); + let inline_level_box = self.inline_boxes.get(&identifier); + inline_level_box.borrow_mut().is_last_fragment = true; + + self.push_control_character_string( + inline_level_box.borrow().base.style.bidi_control_chars().1, + ); + + inline_level_box + } + + fn end_inline_box_internal(&mut self) -> InlineBoxIdentifier { + let identifier = self + .inline_box_stack + .pop() + .expect("Ended non-existent inline box"); + self.inline_items + .push(ArcRefCell::new(InlineItem::EndInlineBox)); + + self.inline_boxes.end_inline_box(identifier); + identifier + } + + pub(crate) fn push_text<'dom, Node: NodeExt<'dom>>( + &mut self, + text: Cow<'dom, str>, + info: &NodeAndStyleInfo<Node>, + ) { + let white_space_collapse = info.style.clone_white_space_collapse(); + let collapsed = WhitespaceCollapse::new( + text.chars(), + white_space_collapse, + self.last_inline_box_ended_with_collapsible_white_space, + ); + + // TODO: Not all text transforms are about case, this logic should stop ignoring + // TextTransform::FULL_WIDTH and TextTransform::FULL_SIZE_KANA. + let text_transform = info.style.clone_text_transform().case(); + let capitalized_text: String; + let char_iterator: Box<dyn Iterator<Item = char>> = match text_transform { + TextTransformCase::None => Box::new(collapsed), + TextTransformCase::Capitalize => { + // `TextTransformation` doesn't support capitalization, so we must capitalize the whole + // string at once and make a copy. Here `on_word_boundary` indicates whether or not the + // inline formatting context as a whole is on a word boundary. This is different from + // `last_inline_box_ended_with_collapsible_white_space` because the word boundaries are + // between atomic inlines and at the start of the IFC, and because preserved spaces + // are a word boundary. + let collapsed_string: String = collapsed.collect(); + capitalized_text = capitalize_string(&collapsed_string, self.on_word_boundary); + Box::new(capitalized_text.chars()) + }, + _ => { + // If `text-transform` is active, wrap the `WhitespaceCollapse` iterator in + // a `TextTransformation` iterator. + Box::new(TextTransformation::new(collapsed, text_transform)) + }, + }; + + let white_space_collapse = info.style.clone_white_space_collapse(); + let new_text: String = char_iterator + .inspect(|&character| { + self.has_uncollapsible_text_content |= matches!( + white_space_collapse, + WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces + ) || !character.is_ascii_whitespace() || + (character == '\n' && white_space_collapse != WhiteSpaceCollapse::Collapse); + }) + .collect(); + + if new_text.is_empty() { + return; + } + + let selection_range = info.get_selection_range(); + let selected_style = info.get_selected_style(); + + if let Some(last_character) = new_text.chars().next_back() { + self.on_word_boundary = last_character.is_whitespace(); + self.last_inline_box_ended_with_collapsible_white_space = + self.on_word_boundary && white_space_collapse != WhiteSpaceCollapse::Preserve; + } + + let new_range = self.current_text_offset..self.current_text_offset + new_text.len(); + self.current_text_offset = new_range.end; + self.text_segments.push(new_text); + + if let Some(inline_item) = self.inline_items.last() { + if let InlineItem::TextRun(text_run) = &mut *inline_item.borrow_mut() { + text_run.borrow_mut().text_range.end = new_range.end; + return; + } + } + + self.inline_items + .push(ArcRefCell::new(InlineItem::TextRun(ArcRefCell::new( + TextRun::new( + info.into(), + info.style.clone(), + new_range, + selection_range, + selected_style, + ), + )))); + } + + pub(crate) fn split_around_block_and_finish( + &mut self, + layout_context: &LayoutContext, + propagated_data: PropagatedBoxTreeData, + has_first_formatted_line: bool, + default_bidi_level: Level, + ) -> Option<InlineFormattingContext> { + if self.is_empty() { + return None; + } + + // Create a new inline builder which will be active after the block splits this inline formatting + // context. It has the same inline box structure as this builder, except the boxes are + // marked as not being the first fragment. No inline content is carried over to this new + // builder. + let mut new_builder = InlineFormattingContextBuilder::new(); + for identifier in self.inline_box_stack.iter() { + new_builder.start_inline_box( + self.inline_boxes + .get(identifier) + .borrow() + .split_around_block(), + ); + } + let mut inline_builder_from_before_split = std::mem::replace(self, new_builder); + + // End all ongoing inline boxes in the first builder, but ensure that they are not + // marked as the final fragments, so that they do not get inline end margin, borders, + // and padding. + while !inline_builder_from_before_split.inline_box_stack.is_empty() { + inline_builder_from_before_split.end_inline_box_internal(); + } + + inline_builder_from_before_split.finish( + layout_context, + propagated_data, + has_first_formatted_line, + /* is_single_line_text_input = */ false, + default_bidi_level, + ) + } + + /// Finish the current inline formatting context, returning [`None`] if the context was empty. + pub(crate) fn finish( + &mut self, + layout_context: &LayoutContext, + propagated_data: PropagatedBoxTreeData, + has_first_formatted_line: bool, + is_single_line_text_input: bool, + default_bidi_level: Level, + ) -> Option<InlineFormattingContext> { + if self.is_empty() { + return None; + } + + let old_builder = std::mem::replace(self, InlineFormattingContextBuilder::new()); + assert!(old_builder.inline_box_stack.is_empty()); + + Some(InlineFormattingContext::new_with_builder( + old_builder, + layout_context, + propagated_data, + has_first_formatted_line, + is_single_line_text_input, + default_bidi_level, + )) + } +} + +fn preserve_segment_break() -> bool { + true +} + +pub struct WhitespaceCollapse<InputIterator> { + char_iterator: InputIterator, + white_space_collapse: WhiteSpaceCollapse, + + /// Whether or not we should collapse white space completely at the start of the string. + /// This is true when the last character handled in our owning [`super::InlineFormattingContext`] + /// was collapsible white space. + remove_collapsible_white_space_at_start: bool, + + /// Whether or not the last character produced was newline. There is special behavior + /// we do after each newline. + following_newline: bool, + + /// Whether or not we have seen any non-white space characters, indicating that we are not + /// in a collapsible white space section at the beginning of the string. + have_seen_non_white_space_characters: bool, + + /// Whether the last character that we processed was a non-newline white space character. When + /// collapsing white space we need to wait until the next non-white space character or the end + /// of the string to push a single white space. + inside_white_space: bool, + + /// When we enter a collapsible white space region, we may need to wait to produce a single + /// white space character as soon as we encounter a non-white space character. When that + /// happens we queue up the non-white space character for the next iterator call. + character_pending_to_return: Option<char>, +} + +impl<InputIterator> WhitespaceCollapse<InputIterator> { + pub fn new( + char_iterator: InputIterator, + white_space_collapse: WhiteSpaceCollapse, + trim_beginning_white_space: bool, + ) -> Self { + Self { + char_iterator, + white_space_collapse, + remove_collapsible_white_space_at_start: trim_beginning_white_space, + inside_white_space: false, + following_newline: false, + have_seen_non_white_space_characters: false, + character_pending_to_return: None, + } + } + + fn is_leading_trimmed_white_space(&self) -> bool { + !self.have_seen_non_white_space_characters && self.remove_collapsible_white_space_at_start + } + + /// Whether or not we need to produce a space character if the next character is not a newline + /// and not white space. This happens when we are exiting a section of white space and we + /// waited to produce a single space character for the entire section of white space (but + /// not following or preceding a newline). + fn need_to_produce_space_character_after_white_space(&self) -> bool { + self.inside_white_space && !self.following_newline && !self.is_leading_trimmed_white_space() + } +} + +impl<InputIterator> Iterator for WhitespaceCollapse<InputIterator> +where + InputIterator: Iterator<Item = char>, +{ + type Item = char; + + fn next(&mut self) -> Option<Self::Item> { + // Point 4.1.1 first bullet: + // > If white-space is set to normal, nowrap, or pre-line, whitespace + // > characters are considered collapsible + // If whitespace is not considered collapsible, it is preserved entirely, which + // means that we can simply return the input string exactly. + if self.white_space_collapse == WhiteSpaceCollapse::Preserve || + self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces + { + // From <https://drafts.csswg.org/css-text-3/#white-space-processing>: + // > Carriage returns (U+000D) are treated identically to spaces (U+0020) in all respects. + // + // In the non-preserved case these are converted to space below. + return match self.char_iterator.next() { + Some('\r') => Some(' '), + next => next, + }; + } + + if let Some(character) = self.character_pending_to_return.take() { + self.inside_white_space = false; + self.have_seen_non_white_space_characters = true; + self.following_newline = false; + return Some(character); + } + + while let Some(character) = self.char_iterator.next() { + // Don't push non-newline whitespace immediately. Instead wait to push it until we + // know that it isn't followed by a newline. See `push_pending_whitespace_if_needed` + // above. + if character.is_ascii_whitespace() && character != '\n' { + self.inside_white_space = true; + continue; + } + + // Point 4.1.1: + // > 2. Collapsible segment breaks are transformed for rendering according to the + // > segment break transformation rules. + if character == '\n' { + // From <https://drafts.csswg.org/css-text-3/#line-break-transform> + // (4.1.3 -- the segment break transformation rules): + // + // > When white-space is pre, pre-wrap, or pre-line, segment breaks are not + // > collapsible and are instead transformed into a preserved line feed" + if self.white_space_collapse != WhiteSpaceCollapse::Collapse { + self.inside_white_space = false; + self.following_newline = true; + return Some(character); + + // Point 4.1.3: + // > 1. First, any collapsible segment break immediately following another + // > collapsible segment break is removed. + // > 2. Then any remaining segment break is either transformed into a space (U+0020) + // > or removed depending on the context before and after the break. + } else if !self.following_newline && + preserve_segment_break() && + !self.is_leading_trimmed_white_space() + { + self.inside_white_space = false; + self.following_newline = true; + return Some(' '); + } else { + self.following_newline = true; + continue; + } + } + + // Point 4.1.1: + // > 2. Any sequence of collapsible spaces and tabs immediately preceding or + // > following a segment break is removed. + // > 3. Every collapsible tab is converted to a collapsible space (U+0020). + // > 4. Any collapsible space immediately following another collapsible space—even + // > one outside the boundary of the inline containing that space, provided both + // > spaces are within the same inline formatting context—is collapsed to have zero + // > advance width. + if self.need_to_produce_space_character_after_white_space() { + self.inside_white_space = false; + self.character_pending_to_return = Some(character); + return Some(' '); + } + + self.inside_white_space = false; + self.have_seen_non_white_space_characters = true; + self.following_newline = false; + return Some(character); + } + + if self.need_to_produce_space_character_after_white_space() { + self.inside_white_space = false; + return Some(' '); + } + + None + } + + fn size_hint(&self) -> (usize, Option<usize>) { + self.char_iterator.size_hint() + } + + fn count(self) -> usize + where + Self: Sized, + { + self.char_iterator.count() + } +} + +enum PendingCaseConversionResult { + Uppercase(ToUppercase), + Lowercase(ToLowercase), +} + +impl PendingCaseConversionResult { + fn next(&mut self) -> Option<char> { + match self { + PendingCaseConversionResult::Uppercase(to_uppercase) => to_uppercase.next(), + PendingCaseConversionResult::Lowercase(to_lowercase) => to_lowercase.next(), + } + } +} + +/// This is an interator that consumes a char iterator and produces character transformed +/// by the given CSS `text-transform` value. It currently does not support +/// `text-transform: capitalize` because Unicode segmentation libraries do not support +/// streaming input one character at a time. +pub struct TextTransformation<InputIterator> { + /// The input character iterator. + char_iterator: InputIterator, + /// The `text-transform` value to use. + text_transform: TextTransformCase, + /// If an uppercasing or lowercasing produces more than one character, this + /// caches them so that they can be returned in subsequent iterator calls. + pending_case_conversion_result: Option<PendingCaseConversionResult>, +} + +impl<InputIterator> TextTransformation<InputIterator> { + pub fn new(char_iterator: InputIterator, text_transform: TextTransformCase) -> Self { + Self { + char_iterator, + text_transform, + pending_case_conversion_result: None, + } + } +} + +impl<InputIterator> Iterator for TextTransformation<InputIterator> +where + InputIterator: Iterator<Item = char>, +{ + type Item = char; + + fn next(&mut self) -> Option<Self::Item> { + if let Some(character) = self + .pending_case_conversion_result + .as_mut() + .and_then(|result| result.next()) + { + return Some(character); + } + self.pending_case_conversion_result = None; + + for character in self.char_iterator.by_ref() { + match self.text_transform { + TextTransformCase::None => return Some(character), + TextTransformCase::Uppercase => { + let mut pending_result = + PendingCaseConversionResult::Uppercase(character.to_uppercase()); + if let Some(character) = pending_result.next() { + self.pending_case_conversion_result = Some(pending_result); + return Some(character); + } + }, + TextTransformCase::Lowercase => { + let mut pending_result = + PendingCaseConversionResult::Lowercase(character.to_lowercase()); + if let Some(character) = pending_result.next() { + self.pending_case_conversion_result = Some(pending_result); + return Some(character); + } + }, + // `text-transform: capitalize` currently cannot work on a per-character basis, + // so must be handled outside of this iterator. + TextTransformCase::Capitalize => return Some(character), + } + } + None + } +} + +/// Given a string and whether the start of the string represents a word boundary, create a copy of +/// the string with letters after word boundaries capitalized. +fn capitalize_string(string: &str, allow_word_at_start: bool) -> String { + let mut output_string = String::new(); + output_string.reserve(string.len()); + + let word_segmenter = WordSegmenter::new_auto(); + let mut bounds = word_segmenter.segment_str(string).peekable(); + let mut byte_index = 0; + for character in string.chars() { + let current_byte_index = byte_index; + byte_index += character.len_utf8(); + + if let Some(next_index) = bounds.peek() { + if *next_index == current_byte_index { + bounds.next(); + + if current_byte_index != 0 || allow_word_at_start { + output_string.extend(character.to_uppercase()); + continue; + } + } + } + + output_string.push(character); + } + + output_string +} diff --git a/components/layout/flow/inline/inline_box.rs b/components/layout/flow/inline/inline_box.rs new file mode 100644 index 00000000000..97398d6e708 --- /dev/null +++ b/components/layout/flow/inline/inline_box.rs @@ -0,0 +1,257 @@ +/* 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 std::vec::IntoIter; + +use app_units::Au; +use fonts::FontMetrics; +use malloc_size_of_derive::MallocSizeOf; + +use super::{InlineContainerState, InlineContainerStateFlags, inline_container_needs_strut}; +use crate::ContainingBlock; +use crate::cell::ArcRefCell; +use crate::context::LayoutContext; +use crate::dom::NodeExt; +use crate::dom_traversal::NodeAndStyleInfo; +use crate::fragment_tree::BaseFragmentInfo; +use crate::layout_box_base::LayoutBoxBase; +use crate::style_ext::{LayoutStyle, PaddingBorderMargin}; + +#[derive(Debug, MallocSizeOf)] +pub(crate) struct InlineBox { + pub base: LayoutBoxBase, + /// The identifier of this inline box in the containing [`super::InlineFormattingContext`]. + pub(super) identifier: InlineBoxIdentifier, + pub is_first_fragment: bool, + pub is_last_fragment: bool, + /// The index of the default font in the [`super::InlineFormattingContext`]'s font metrics store. + /// This is initialized during IFC shaping. + pub default_font_index: Option<usize>, +} + +impl InlineBox { + pub(crate) fn new<'dom, Node: NodeExt<'dom>>(info: &NodeAndStyleInfo<Node>) -> Self { + Self { + base: LayoutBoxBase::new(info.into(), info.style.clone()), + // This will be assigned later, when the box is actually added to the IFC. + identifier: InlineBoxIdentifier::default(), + is_first_fragment: true, + is_last_fragment: false, + default_font_index: None, + } + } + + pub(crate) fn split_around_block(&self) -> Self { + Self { + base: LayoutBoxBase::new(self.base.base_fragment_info, self.base.style.clone()), + is_first_fragment: false, + is_last_fragment: false, + ..*self + } + } + + #[inline] + pub(crate) fn layout_style(&self) -> LayoutStyle { + LayoutStyle::Default(&self.base.style) + } +} + +#[derive(Debug, Default, MallocSizeOf)] +pub(crate) struct InlineBoxes { + /// A collection of all inline boxes in a particular [`super::InlineFormattingContext`]. + inline_boxes: Vec<ArcRefCell<InlineBox>>, + + /// A list of tokens that represent the actual tree of inline boxes, while allowing + /// easy traversal forward and backwards through the tree. This structure is also + /// stored in the [`super::InlineFormattingContext::inline_items`], but this version is + /// faster to iterate. + inline_box_tree: Vec<InlineBoxTreePathToken>, +} + +impl InlineBoxes { + pub(super) fn len(&self) -> usize { + self.inline_boxes.len() + } + + pub(super) fn iter(&self) -> impl Iterator<Item = &ArcRefCell<InlineBox>> { + self.inline_boxes.iter() + } + + pub(super) fn get(&self, identifier: &InlineBoxIdentifier) -> ArcRefCell<InlineBox> { + self.inline_boxes[identifier.index_in_inline_boxes as usize].clone() + } + + pub(super) fn end_inline_box(&mut self, identifier: InlineBoxIdentifier) { + self.inline_box_tree + .push(InlineBoxTreePathToken::End(identifier)); + } + + pub(super) fn start_inline_box( + &mut self, + mut inline_box: InlineBox, + ) -> (InlineBoxIdentifier, ArcRefCell<InlineBox>) { + assert!(self.inline_boxes.len() <= u32::MAX as usize); + assert!(self.inline_box_tree.len() <= u32::MAX as usize); + + let index_in_inline_boxes = self.inline_boxes.len() as u32; + let index_of_start_in_tree = self.inline_box_tree.len() as u32; + + let identifier = InlineBoxIdentifier { + index_of_start_in_tree, + index_in_inline_boxes, + }; + inline_box.identifier = identifier; + let inline_box = ArcRefCell::new(inline_box); + + self.inline_boxes.push(inline_box.clone()); + self.inline_box_tree + .push(InlineBoxTreePathToken::Start(identifier)); + + (identifier, inline_box) + } + + pub(super) fn get_path( + &self, + from: Option<InlineBoxIdentifier>, + to: InlineBoxIdentifier, + ) -> IntoIter<InlineBoxTreePathToken> { + if from == Some(to) { + return Vec::new().into_iter(); + } + + let mut from_index = match from { + Some(InlineBoxIdentifier { + index_of_start_in_tree, + .. + }) => index_of_start_in_tree as usize, + None => 0, + }; + let mut to_index = to.index_of_start_in_tree as usize; + let is_reversed = to_index < from_index; + + // Do not include the first or final token, depending on direction. These can be equal + // if we are starting or going to the the root of the inline formatting context, in which + // case we don't want to adjust. + if to_index > from_index && from.is_some() { + from_index += 1; + } else if to_index < from_index { + to_index += 1; + } + + let mut path = Vec::with_capacity(from_index.abs_diff(to_index)); + let min = from_index.min(to_index); + let max = from_index.max(to_index); + + for token in &self.inline_box_tree[min..=max] { + // Skip useless recursion into inline boxes; we are looking for a direct path. + if Some(&token.reverse()) == path.last() { + path.pop(); + } else { + path.push(*token); + } + } + + if is_reversed { + path.reverse(); + for token in path.iter_mut() { + *token = token.reverse(); + } + } + + path.into_iter() + } +} + +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)] +pub(super) enum InlineBoxTreePathToken { + Start(InlineBoxIdentifier), + End(InlineBoxIdentifier), +} + +impl InlineBoxTreePathToken { + fn reverse(&self) -> Self { + match self { + Self::Start(index) => Self::End(*index), + Self::End(index) => Self::Start(*index), + } + } +} + +/// An identifier for a particular [`InlineBox`] to be used to fetch it from an [`InlineBoxes`] +/// store of inline boxes. +/// +/// [`u32`] is used for the index, in order to save space. The value refers to the token +/// in the start tree data structure which can be fetched to find the actual index of +/// of the [`InlineBox`] in [`InlineBoxes::inline_boxes`]. +#[derive(Clone, Copy, Debug, Default, Eq, Hash, MallocSizeOf, PartialEq)] +pub(crate) struct InlineBoxIdentifier { + pub index_of_start_in_tree: u32, + pub index_in_inline_boxes: u32, +} + +pub(super) struct InlineBoxContainerState { + /// The container state common to both [`InlineBox`] and the root of the + /// [`super::InlineFormattingContext`]. + pub base: InlineContainerState, + + /// The [`InlineBoxIdentifier`] of this inline container state. If this is the root + /// the identifier is [`None`]. + pub identifier: InlineBoxIdentifier, + + /// The [`BaseFragmentInfo`] of the [`InlineBox`] that this state tracks. + pub base_fragment_info: BaseFragmentInfo, + + /// The [`PaddingBorderMargin`] of the [`InlineBox`] that this state tracks. + pub pbm: PaddingBorderMargin, + + /// Whether this is the last fragment of this InlineBox. This may not be the case if + /// the InlineBox is split due to an block-in-inline-split and this is not the last of + /// that split. + pub is_last_fragment: bool, +} + +impl InlineBoxContainerState { + pub(super) fn new( + inline_box: &InlineBox, + containing_block: &ContainingBlock, + layout_context: &LayoutContext, + parent_container: &InlineContainerState, + is_last_fragment: bool, + font_metrics: Option<&FontMetrics>, + ) -> Self { + let style = inline_box.base.style.clone(); + let pbm = inline_box + .layout_style() + .padding_border_margin(containing_block); + + let mut flags = InlineContainerStateFlags::empty(); + if inline_container_needs_strut(&style, layout_context, Some(&pbm)) { + flags.insert(InlineContainerStateFlags::CREATE_STRUT); + } + + Self { + base: InlineContainerState::new( + style, + flags, + Some(parent_container), + parent_container.text_decoration_line, + font_metrics, + ), + identifier: inline_box.identifier, + base_fragment_info: inline_box.base.base_fragment_info, + pbm, + is_last_fragment, + } + } + + pub(super) fn calculate_space_above_baseline(&self) -> Au { + let (ascent, descent, line_gap) = ( + self.base.font_metrics.ascent, + self.base.font_metrics.descent, + self.base.font_metrics.line_gap, + ); + let leading = line_gap - (ascent + descent); + leading.scale_by(0.5) + ascent + } +} diff --git a/components/layout/flow/inline/line.rs b/components/layout/flow/inline/line.rs new file mode 100644 index 00000000000..c42f32c9242 --- /dev/null +++ b/components/layout/flow/inline/line.rs @@ -0,0 +1,911 @@ +/* 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 bitflags::bitflags; +use fonts::{ByteIndex, FontMetrics, GlyphStore}; +use itertools::Either; +use range::Range; +use servo_arc::Arc; +use style::Zero; +use style::computed_values::position::T as Position; +use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; +use style::properties::ComputedValues; +use style::values::generics::box_::{GenericVerticalAlign, VerticalAlignKeyword}; +use style::values::generics::font::LineHeight; +use style::values::specified::align::AlignFlags; +use style::values::specified::box_::DisplayOutside; +use style::values::specified::text::TextDecorationLine; +use unicode_bidi::{BidiInfo, Level}; +use webrender_api::FontInstanceKey; + +use super::inline_box::{InlineBoxContainerState, InlineBoxIdentifier, InlineBoxTreePathToken}; +use super::{InlineFormattingContextLayout, LineBlockSizes}; +use crate::cell::ArcRefCell; +use crate::fragment_tree::{BaseFragmentInfo, BoxFragment, Fragment, TextFragment}; +use crate::geom::{LogicalRect, LogicalVec2, PhysicalRect, ToLogical}; +use crate::positioned::{ + AbsolutelyPositionedBox, PositioningContext, PositioningContextLength, relative_adjustement, +}; +use crate::{ContainingBlock, ContainingBlockSize}; + +pub(super) struct LineMetrics { + /// The block offset of the line start in the containing + /// [`crate::flow::InlineFormattingContext`]. + pub block_offset: Au, + + /// The block size of this line. + pub block_size: Au, + + /// The block offset of this line's baseline from [`Self::block_offset`]. + pub baseline_block_offset: Au, +} + +bitflags! { + struct LineLayoutInlineContainerFlags: u8 { + /// Whether or not any line items were processed for this inline box, this includes + /// any child inline boxes. + const HAD_ANY_LINE_ITEMS = 1 << 0; + /// Whether or not the starting inline border, padding, or margin of the inline box + /// was encountered. + const HAD_INLINE_START_PBM = 1 << 2; + /// Whether or not the ending inline border, padding, or margin of the inline box + /// was encountered. + const HAD_INLINE_END_PBM = 1 << 3; + /// Whether or not any floats were encountered while laying out this inline box. + const HAD_ANY_FLOATS = 1 << 4; + } +} + +/// The state used when laying out a collection of [`LineItem`]s into a line. This state is stored +/// per-inline container. For instance, when laying out the conents of a `<span>` a fresh +/// [`LineItemLayoutInlineContainerState`] is pushed onto [`LineItemLayout`]'s stack of states. +pub(super) struct LineItemLayoutInlineContainerState { + /// If this inline container is not the root inline container, the identifier of the [`super::InlineBox`] + /// that is currently being laid out. + pub identifier: Option<InlineBoxIdentifier>, + + /// The fragments and their logical rectangle relative within the current inline box (or + /// line). These logical rectangles will be converted into physical ones and the Fragment's + /// `content_rect` will be updated once the inline box's final size is known in + /// [`LineItemLayout::end_inline_box`]. + pub fragments: Vec<(Fragment, LogicalRect<Au>)>, + + /// The current inline advance of the layout in the coordinates of this inline box. + pub inline_advance: Au, + + /// Flags which track various features during layout. + flags: LineLayoutInlineContainerFlags, + + /// The offset of the parent, relative to the start position of the line, not including + /// any inline start and end borders which are only processed when the inline box is + /// finished. + pub parent_offset: LogicalVec2<Au>, + + /// The block offset of the parent's baseline relative to the block start of the line. This + /// is often the same as [`Self::parent_offset`], but can be different for the root + /// element. + pub baseline_offset: Au, + + /// If this inline box establishes a containing block for positioned elements, this + /// is a fresh positioning context to contain them. Otherwise, this holds the starting + /// offset in the *parent* positioning context so that static positions can be updated + /// at the end of layout. + pub positioning_context_or_start_offset_in_parent: + Either<PositioningContext, PositioningContextLength>, +} + +impl LineItemLayoutInlineContainerState { + fn new( + identifier: Option<InlineBoxIdentifier>, + parent_offset: LogicalVec2<Au>, + baseline_offset: Au, + positioning_context_or_start_offset_in_parent: Either< + PositioningContext, + PositioningContextLength, + >, + ) -> Self { + Self { + identifier, + fragments: Vec::new(), + inline_advance: Au::zero(), + flags: LineLayoutInlineContainerFlags::empty(), + parent_offset, + baseline_offset, + positioning_context_or_start_offset_in_parent, + } + } + + fn root(starting_inline_advance: Au, baseline_offset: Au) -> Self { + let mut state = Self::new( + None, + LogicalVec2::zero(), + baseline_offset, + Either::Right(PositioningContextLength::zero()), + ); + state.inline_advance = starting_inline_advance; + state + } +} + +/// The second phase of [`super::InlineFormattingContext`] layout: once items are gathered +/// for a line, we must lay them out and create fragments for them, properly positioning them +/// according to their baselines and also handling absolutely positioned children. +pub(super) struct LineItemLayout<'layout_data, 'layout> { + /// The state of the overall [`super::InlineFormattingContext`] layout. + layout: &'layout mut InlineFormattingContextLayout<'layout_data>, + + /// The set of [`LineItemLayoutInlineContainerState`] created while laying out items + /// on this line. This does not include the current level of recursion. + pub state_stack: Vec<LineItemLayoutInlineContainerState>, + + /// The current [`LineItemLayoutInlineContainerState`]. + pub current_state: LineItemLayoutInlineContainerState, + + /// The metrics of this line, which should remain constant throughout the + /// layout process. + pub line_metrics: LineMetrics, + + /// The amount of space to add to each justification opportunity in order to implement + /// `text-align: justify`. + pub justification_adjustment: Au, +} + +impl LineItemLayout<'_, '_> { + pub(super) fn layout_line_items( + layout: &mut InlineFormattingContextLayout, + line_items: Vec<LineItem>, + start_position: LogicalVec2<Au>, + effective_block_advance: &LineBlockSizes, + justification_adjustment: Au, + ) -> Vec<Fragment> { + let baseline_offset = effective_block_advance.find_baseline_offset(); + LineItemLayout { + layout, + state_stack: Vec::new(), + current_state: LineItemLayoutInlineContainerState::root( + start_position.inline, + baseline_offset, + ), + line_metrics: LineMetrics { + block_offset: start_position.block, + block_size: effective_block_advance.resolve(), + baseline_block_offset: baseline_offset, + }, + justification_adjustment, + } + .layout(line_items) + } + + /// Start and end inline boxes in tree order, so that it reflects the given inline box. + fn prepare_layout_for_inline_box(&mut self, new_inline_box: Option<InlineBoxIdentifier>) { + // Optimize the case where we are moving to the root of the inline box stack. + let Some(new_inline_box) = new_inline_box else { + while !self.state_stack.is_empty() { + self.end_inline_box(); + } + return; + }; + + // Otherwise, follow the path given to us by our collection of inline boxes, so we know which + // inline boxes to start and end. + let path = self + .layout + .ifc + .inline_boxes + .get_path(self.current_state.identifier, new_inline_box); + for token in path { + match token { + InlineBoxTreePathToken::Start(ref identifier) => self.start_inline_box(identifier), + InlineBoxTreePathToken::End(_) => self.end_inline_box(), + } + } + } + + pub(super) fn layout(&mut self, mut line_items: Vec<LineItem>) -> Vec<Fragment> { + let mut last_level = Level::ltr(); + let levels: Vec<_> = line_items + .iter() + .map(|item| { + let level = match item { + LineItem::TextRun(_, text_run) => text_run.bidi_level, + // TODO: This level needs either to be last_level, or if there were + // unicode characters inserted for the inline box, we need to get the + // level from them. + LineItem::InlineStartBoxPaddingBorderMargin(_) => last_level, + LineItem::InlineEndBoxPaddingBorderMargin(_) => last_level, + LineItem::Atomic(_, atomic) => atomic.bidi_level, + LineItem::AbsolutelyPositioned(..) => last_level, + LineItem::Float(..) => { + // At this point the float is already positioned, so it doesn't really matter what + // position it's fragment has in the order of line items. + last_level + }, + }; + last_level = level; + level + }) + .collect(); + + if self.layout.ifc.has_right_to_left_content { + sort_by_indices_in_place(&mut line_items, BidiInfo::reorder_visual(&levels)); + } + + // `BidiInfo::reorder_visual` will reorder the contents of the line so that they + // are in the correct order as if one was looking at the line from left-to-right. + // During this layout we do not lay out from left to right. Instead we lay out + // from inline-start to inline-end. If the overall line contents have been flipped + // for BiDi, flip them again so that they are in line start-to-end order rather + // than left-to-right order. + let line_item_iterator = if self + .layout + .containing_block + .style + .writing_mode + .is_bidi_ltr() + { + Either::Left(line_items.into_iter()) + } else { + Either::Right(line_items.into_iter().rev()) + }; + + for item in line_item_iterator.into_iter().by_ref() { + // When preparing to lay out a new line item, start and end inline boxes, so that the current + // inline box state reflects the item's parent. Items in the line are not necessarily in tree + // order due to BiDi and other reordering so the inline box of the item could potentially be + // any in the inline formatting context. + self.prepare_layout_for_inline_box(item.inline_box_identifier()); + + self.current_state + .flags + .insert(LineLayoutInlineContainerFlags::HAD_ANY_LINE_ITEMS); + match item { + LineItem::InlineStartBoxPaddingBorderMargin(_) => { + self.current_state + .flags + .insert(LineLayoutInlineContainerFlags::HAD_INLINE_START_PBM); + }, + LineItem::InlineEndBoxPaddingBorderMargin(_) => { + self.current_state + .flags + .insert(LineLayoutInlineContainerFlags::HAD_INLINE_END_PBM); + }, + LineItem::TextRun(_, text_run) => self.layout_text_run(text_run), + LineItem::Atomic(_, atomic) => self.layout_atomic(atomic), + LineItem::AbsolutelyPositioned(_, absolute) => self.layout_absolute(absolute), + LineItem::Float(_, float) => self.layout_float(float), + } + } + + // Move back to the root of the inline box tree, so that all boxes are ended. + self.prepare_layout_for_inline_box(None); + + let fragments_and_rectangles = std::mem::take(&mut self.current_state.fragments); + fragments_and_rectangles + .into_iter() + .map(|(mut fragment, logical_rect)| { + if matches!(fragment, Fragment::Float(_)) { + return fragment; + } + + // We do not know the actual physical position of a logically laid out inline element, until + // we know the width of the containing inline block. This step converts the logical rectangle + // into a physical one based on the inline formatting context width. + fragment.mutate_content_rect(|content_rect| { + *content_rect = logical_rect.as_physical(Some(self.layout.containing_block)) + }); + + fragment + }) + .collect() + } + + fn current_positioning_context_mut(&mut self) -> &mut PositioningContext { + if let Either::Left(ref mut positioning_context) = self + .current_state + .positioning_context_or_start_offset_in_parent + { + return positioning_context; + } + self.state_stack + .iter_mut() + .rev() + .find_map( + |state| match state.positioning_context_or_start_offset_in_parent { + Either::Left(ref mut positioning_context) => Some(positioning_context), + Either::Right(_) => None, + }, + ) + .unwrap_or(self.layout.positioning_context) + } + + fn start_inline_box(&mut self, identifier: &InlineBoxIdentifier) { + let inline_box_state = + &*self.layout.inline_box_states[identifier.index_in_inline_boxes as usize]; + let inline_box = self.layout.ifc.inline_boxes.get(identifier); + let inline_box = &*(inline_box.borrow()); + + let style = &inline_box.base.style; + let space_above_baseline = inline_box_state.calculate_space_above_baseline(); + let block_start_offset = + self.calculate_inline_box_block_start(inline_box_state, space_above_baseline); + + let positioning_context_or_start_offset_in_parent = + match PositioningContext::new_for_style(style) { + Some(positioning_context) => Either::Left(positioning_context), + None => Either::Right(self.current_positioning_context_mut().len()), + }; + + let parent_offset = LogicalVec2 { + inline: self.current_state.inline_advance + self.current_state.parent_offset.inline, + block: block_start_offset, + }; + + let outer_state = std::mem::replace( + &mut self.current_state, + LineItemLayoutInlineContainerState::new( + Some(*identifier), + parent_offset, + block_start_offset + space_above_baseline, + positioning_context_or_start_offset_in_parent, + ), + ); + + self.state_stack.push(outer_state); + } + + fn end_inline_box(&mut self) { + let outer_state = self.state_stack.pop().expect("Ended unknown inline box"); + let inner_state = std::mem::replace(&mut self.current_state, outer_state); + + let identifier = inner_state.identifier.expect("Ended unknown inline box"); + let inline_box_state = + &*self.layout.inline_box_states[identifier.index_in_inline_boxes as usize]; + let inline_box = self.layout.ifc.inline_boxes.get(&identifier); + let inline_box = &*(inline_box.borrow()); + + let mut padding = inline_box_state.pbm.padding; + let mut border = inline_box_state.pbm.border; + let mut margin = inline_box_state.pbm.margin.auto_is(Au::zero); + + let mut had_start = inner_state + .flags + .contains(LineLayoutInlineContainerFlags::HAD_INLINE_START_PBM); + let mut had_end = inner_state + .flags + .contains(LineLayoutInlineContainerFlags::HAD_INLINE_END_PBM); + + let containing_block_writing_mode = self.layout.containing_block.style.writing_mode; + if containing_block_writing_mode.is_bidi_ltr() != + inline_box.base.style.writing_mode.is_bidi_ltr() + { + std::mem::swap(&mut had_start, &mut had_end) + } + + if !had_start { + padding.inline_start = Au::zero(); + border.inline_start = Au::zero(); + margin.inline_start = Au::zero(); + } + if !had_end { + padding.inline_end = Au::zero(); + border.inline_end = Au::zero(); + margin.inline_end = Au::zero(); + } + // If the inline box didn't have any content at all and it isn't the first fragment for + // an element (needed for layout queries currently) and it didn't have any padding, border, + // or margin do not make a fragment for it. + // + // Note: This is an optimization, but also has side effects. Any fragments on a line will + // force the baseline to advance in the parent IFC. + let pbm_sums = padding + border + margin; + if inner_state.fragments.is_empty() && !had_start && pbm_sums.inline_sum().is_zero() { + return; + } + + // Make `content_rect` relative to the parent Fragment. + let mut content_rect = LogicalRect { + start_corner: LogicalVec2 { + inline: self.current_state.inline_advance + pbm_sums.inline_start, + block: inner_state.parent_offset.block - self.current_state.parent_offset.block, + }, + size: LogicalVec2 { + inline: inner_state.inline_advance, + block: inline_box_state.base.font_metrics.line_gap, + }, + }; + + // Relative adjustment should not affect the rest of line layout, so we can + // do it right before creating the Fragment. + let style = &inline_box.base.style; + if style.get_box().position == Position::Relative { + content_rect.start_corner += relative_adjustement(style, self.layout.containing_block); + } + + let ifc_writing_mode = self.layout.containing_block.style.writing_mode; + let inline_box_containing_block = ContainingBlock { + size: ContainingBlockSize { + inline: content_rect.size.inline, + block: Default::default(), + }, + style: self.layout.containing_block.style, + }; + let fragments = inner_state + .fragments + .into_iter() + .map(|(mut fragment, logical_rect)| { + let is_float = matches!(fragment, Fragment::Float(_)); + fragment.mutate_content_rect(|content_rect| { + if is_float { + content_rect.origin -= + pbm_sums.start_offset().to_physical_size(ifc_writing_mode); + } else { + // We do not know the actual physical position of a logically laid out inline element, until + // we know the width of the containing inline block. This step converts the logical rectangle + // into a physical one now that we've computed inline size of the containing inline block above. + *content_rect = logical_rect.as_physical(Some(&inline_box_containing_block)) + } + }); + fragment + }) + .collect(); + + // Previously all the fragment's children were positioned relative to the linebox, + // but they need to be made relative to this fragment. + let physical_content_rect = content_rect.as_physical(Some(self.layout.containing_block)); + let mut fragment = BoxFragment::new( + inline_box.base.base_fragment_info, + style.clone(), + fragments, + physical_content_rect, + padding.to_physical(ifc_writing_mode), + border.to_physical(ifc_writing_mode), + margin.to_physical(ifc_writing_mode), + None, /* clearance */ + ); + + let offset_from_parent_ifc = LogicalVec2 { + inline: pbm_sums.inline_start + self.current_state.inline_advance, + block: content_rect.start_corner.block, + } + .to_physical_vector(self.layout.containing_block.style.writing_mode); + + match inner_state.positioning_context_or_start_offset_in_parent { + Either::Left(mut positioning_context) => { + positioning_context + .layout_collected_children(self.layout.layout_context, &mut fragment); + positioning_context.adjust_static_position_of_hoisted_fragments_with_offset( + &offset_from_parent_ifc, + PositioningContextLength::zero(), + ); + self.current_positioning_context_mut() + .append(positioning_context); + }, + Either::Right(start_offset) => { + self.current_positioning_context_mut() + .adjust_static_position_of_hoisted_fragments_with_offset( + &offset_from_parent_ifc, + start_offset, + ); + }, + } + + self.current_state.inline_advance += inner_state.inline_advance + pbm_sums.inline_sum(); + + let fragment = Fragment::Box(ArcRefCell::new(fragment)); + inline_box.base.add_fragment(fragment.clone()); + + self.current_state.fragments.push((fragment, content_rect)); + } + + fn calculate_inline_box_block_start( + &self, + inline_box_state: &InlineBoxContainerState, + space_above_baseline: Au, + ) -> Au { + let font_metrics = &inline_box_state.base.font_metrics; + let style = &inline_box_state.base.style; + let line_gap = font_metrics.line_gap; + + // The baseline offset that we have in `Self::baseline_offset` is relative to the line + // baseline, so we need to make it relative to the line block start. + match inline_box_state.base.style.clone_vertical_align() { + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => { + let line_height: Au = line_height(style, font_metrics); + (line_height - line_gap).scale_by(0.5) + }, + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => { + let line_height: Au = line_height(style, font_metrics); + let half_leading = (line_height - line_gap).scale_by(0.5); + self.line_metrics.block_size - line_height + half_leading + }, + _ => { + self.line_metrics.baseline_block_offset + inline_box_state.base.baseline_offset - + space_above_baseline + }, + } + } + + fn layout_text_run(&mut self, text_item: TextRunLineItem) { + if text_item.text.is_empty() { + return; + } + + let mut number_of_justification_opportunities = 0; + let mut inline_advance = text_item + .text + .iter() + .map(|glyph_store| { + number_of_justification_opportunities += glyph_store.total_word_separators(); + glyph_store.total_advance() + }) + .sum(); + + if !self.justification_adjustment.is_zero() { + inline_advance += self + .justification_adjustment + .scale_by(number_of_justification_opportunities as f32); + } + + // The block start of the TextRun is often zero (meaning it has the same font metrics as the + // inline box's strut), but for children of the inline formatting context root or for + // fallback fonts that use baseline relative alignment, it might be different. + let start_corner = LogicalVec2 { + inline: self.current_state.inline_advance, + block: self.current_state.baseline_offset - + text_item.font_metrics.ascent - + self.current_state.parent_offset.block, + }; + let content_rect = LogicalRect { + start_corner, + size: LogicalVec2 { + block: text_item.font_metrics.line_gap, + inline: inline_advance, + }, + }; + + self.current_state.inline_advance += inline_advance; + self.current_state.fragments.push(( + Fragment::Text(ArcRefCell::new(TextFragment { + base: text_item.base_fragment_info.into(), + parent_style: text_item.parent_style, + rect: PhysicalRect::zero(), + font_metrics: text_item.font_metrics, + font_key: text_item.font_key, + glyphs: text_item.text, + text_decoration_line: text_item.text_decoration_line, + justification_adjustment: self.justification_adjustment, + selection_range: text_item.selection_range, + selected_style: text_item.selected_style, + })), + content_rect, + )); + } + + fn layout_atomic(&mut self, atomic: AtomicLineItem) { + // The initial `start_corner` of the Fragment is only the PaddingBorderMargin sum start + // offset, which is the sum of the start component of the padding, border, and margin. + // This needs to be added to the calculated block and inline positions. + // Make the final result relative to the parent box. + let ifc_writing_mode = self.layout.containing_block.style.writing_mode; + let content_rect = { + let block_start = atomic.calculate_block_start(&self.line_metrics); + let atomic_fragment = atomic.fragment.borrow_mut(); + let padding_border_margin_sides = atomic_fragment + .padding_border_margin() + .to_logical(ifc_writing_mode); + + let mut atomic_offset = LogicalVec2 { + inline: self.current_state.inline_advance + + padding_border_margin_sides.inline_start, + block: block_start - self.current_state.parent_offset.block + + padding_border_margin_sides.block_start, + }; + + if atomic_fragment.style.get_box().position == Position::Relative { + atomic_offset += + relative_adjustement(&atomic_fragment.style, self.layout.containing_block); + } + + // Reconstruct a logical rectangle relative to the inline box container that will be used + // after the inline box is procesed to find a final physical rectangle. + LogicalRect { + start_corner: atomic_offset, + size: atomic_fragment + .content_rect + .size + .to_logical(ifc_writing_mode), + } + }; + + if let Some(mut positioning_context) = atomic.positioning_context { + let physical_rect_as_if_in_root = + content_rect.as_physical(Some(self.layout.containing_block)); + positioning_context.adjust_static_position_of_hoisted_fragments_with_offset( + &physical_rect_as_if_in_root.origin.to_vector(), + PositioningContextLength::zero(), + ); + + self.current_positioning_context_mut() + .append(positioning_context); + } + + self.current_state.inline_advance += atomic.size.inline; + + self.current_state + .fragments + .push((Fragment::Box(atomic.fragment), content_rect)); + } + + fn layout_absolute(&mut self, absolute: AbsolutelyPositionedLineItem) { + let absolutely_positioned_box = (*absolute.absolutely_positioned_box).borrow(); + let style = absolutely_positioned_box.context.style(); + + // From https://drafts.csswg.org/css2/#abs-non-replaced-width + // > The static-position containing block is the containing block of a + // > hypothetical box that would have been the first box of the element if its + // > specified position value had been static and its specified float had been + // > none. (Note that due to the rules in section 9.7 this hypothetical + // > calculation might require also assuming a different computed value for + // > display.) + // + // This box is different based on the original `display` value of the + // absolutely positioned element. If it's `inline` it would be placed inline + // at the top of the line, but if it's block it would be placed in a new + // block position after the linebox established by this line. + let initial_start_corner = + if style.get_box().original_display.outside() == DisplayOutside::Inline { + // Top of the line at the current inline position. + LogicalVec2 { + inline: self.current_state.inline_advance, + block: -self.current_state.parent_offset.block, + } + } else { + // After the bottom of the line at the start of the inline formatting context. + LogicalVec2 { + inline: -self.current_state.parent_offset.inline, + block: self.line_metrics.block_size - self.current_state.parent_offset.block, + } + }; + + // Since alignment of absolutes in inlines is currently always `start`, the size of + // of the static position rectangle does not matter. + let static_position_rect = LogicalRect { + start_corner: initial_start_corner, + size: LogicalVec2::zero(), + } + .as_physical(Some(self.layout.containing_block)); + + let hoisted_box = AbsolutelyPositionedBox::to_hoisted( + absolute.absolutely_positioned_box.clone(), + static_position_rect, + LogicalVec2 { + inline: AlignFlags::START, + block: AlignFlags::START, + }, + self.layout.containing_block.style.writing_mode, + ); + + let hoisted_fragment = hoisted_box.fragment.clone(); + self.current_positioning_context_mut().push(hoisted_box); + self.current_state.fragments.push(( + Fragment::AbsoluteOrFixedPositioned(hoisted_fragment), + LogicalRect::zero(), + )); + } + + fn layout_float(&mut self, float: FloatLineItem) { + self.current_state + .flags + .insert(LineLayoutInlineContainerFlags::HAD_ANY_FLOATS); + + // The `BoxFragment` for this float is positioned relative to the IFC, so we need + // to move it to be positioned relative to our parent InlineBox line item. Float + // fragments are children of these InlineBoxes and not children of the inline + // formatting context, so that they are parented properly for StackingContext + // properties such as opacity & filters. + let distance_from_parent_to_ifc = LogicalVec2 { + inline: self.current_state.parent_offset.inline, + block: self.line_metrics.block_offset + self.current_state.parent_offset.block, + }; + float.fragment.borrow_mut().content_rect.origin -= distance_from_parent_to_ifc + .to_physical_size(self.layout.containing_block.style.writing_mode); + + self.current_state + .fragments + .push((Fragment::Float(float.fragment), LogicalRect::zero())); + } +} + +pub(super) enum LineItem { + InlineStartBoxPaddingBorderMargin(InlineBoxIdentifier), + InlineEndBoxPaddingBorderMargin(InlineBoxIdentifier), + TextRun(Option<InlineBoxIdentifier>, TextRunLineItem), + Atomic(Option<InlineBoxIdentifier>, AtomicLineItem), + AbsolutelyPositioned(Option<InlineBoxIdentifier>, AbsolutelyPositionedLineItem), + Float(Option<InlineBoxIdentifier>, FloatLineItem), +} + +impl LineItem { + fn inline_box_identifier(&self) -> Option<InlineBoxIdentifier> { + match self { + LineItem::InlineStartBoxPaddingBorderMargin(identifier) => Some(*identifier), + LineItem::InlineEndBoxPaddingBorderMargin(identifier) => Some(*identifier), + LineItem::TextRun(identifier, _) => *identifier, + LineItem::Atomic(identifier, _) => *identifier, + LineItem::AbsolutelyPositioned(identifier, _) => *identifier, + LineItem::Float(identifier, _) => *identifier, + } + } + + pub(super) fn trim_whitespace_at_end(&mut self, whitespace_trimmed: &mut Au) -> bool { + match self { + LineItem::InlineStartBoxPaddingBorderMargin(_) => true, + LineItem::InlineEndBoxPaddingBorderMargin(_) => true, + LineItem::TextRun(_, item) => item.trim_whitespace_at_end(whitespace_trimmed), + LineItem::Atomic(..) => false, + LineItem::AbsolutelyPositioned(..) => true, + LineItem::Float(..) => true, + } + } + + pub(super) fn trim_whitespace_at_start(&mut self, whitespace_trimmed: &mut Au) -> bool { + match self { + LineItem::InlineStartBoxPaddingBorderMargin(_) => true, + LineItem::InlineEndBoxPaddingBorderMargin(_) => true, + LineItem::TextRun(_, item) => item.trim_whitespace_at_start(whitespace_trimmed), + LineItem::Atomic(..) => false, + LineItem::AbsolutelyPositioned(..) => true, + LineItem::Float(..) => true, + } + } +} + +pub(super) struct TextRunLineItem { + pub base_fragment_info: BaseFragmentInfo, + pub parent_style: Arc<ComputedValues>, + pub text: Vec<std::sync::Arc<GlyphStore>>, + pub font_metrics: FontMetrics, + pub font_key: FontInstanceKey, + pub text_decoration_line: TextDecorationLine, + /// The BiDi level of this [`TextRunLineItem`] to enable reordering. + pub bidi_level: Level, + pub selection_range: Option<Range<ByteIndex>>, + pub selected_style: Arc<ComputedValues>, +} + +impl TextRunLineItem { + fn trim_whitespace_at_end(&mut self, whitespace_trimmed: &mut Au) -> bool { + if matches!( + self.parent_style.get_inherited_text().white_space_collapse, + WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces + ) { + return false; + } + + let index_of_last_non_whitespace = self + .text + .iter() + .rev() + .position(|glyph| !glyph.is_whitespace()) + .map(|offset_from_end| self.text.len() - offset_from_end); + + let first_whitespace_index = index_of_last_non_whitespace.unwrap_or(0); + *whitespace_trimmed += self + .text + .drain(first_whitespace_index..) + .map(|glyph| glyph.total_advance()) + .sum(); + + // Only keep going if we only encountered whitespace. + index_of_last_non_whitespace.is_none() + } + + fn trim_whitespace_at_start(&mut self, whitespace_trimmed: &mut Au) -> bool { + if matches!( + self.parent_style.get_inherited_text().white_space_collapse, + WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces + ) { + return false; + } + + let index_of_first_non_whitespace = self + .text + .iter() + .position(|glyph| !glyph.is_whitespace()) + .unwrap_or(self.text.len()); + + *whitespace_trimmed += self + .text + .drain(0..index_of_first_non_whitespace) + .map(|glyph| glyph.total_advance()) + .sum(); + + // Only keep going if we only encountered whitespace. + self.text.is_empty() + } + + pub(crate) fn can_merge(&self, font_key: FontInstanceKey, bidi_level: Level) -> bool { + self.font_key == font_key && self.bidi_level == bidi_level + } +} + +pub(super) struct AtomicLineItem { + pub fragment: ArcRefCell<BoxFragment>, + pub size: LogicalVec2<Au>, + pub positioning_context: Option<PositioningContext>, + + /// The block offset of this items' baseline relative to the baseline of the line. + /// This will be zero for boxes with `vertical-align: top` and `vertical-align: + /// bottom` since their baselines are calculated late in layout. + pub baseline_offset_in_parent: Au, + + /// The offset of the baseline inside this item. + pub baseline_offset_in_item: Au, + + /// The BiDi level of this [`AtomicLineItem`] to enable reordering. + pub bidi_level: Level, +} + +impl AtomicLineItem { + /// Given the metrics for a line, our vertical alignment, and our block size, find a block start + /// position relative to the top of the line. + fn calculate_block_start(&self, line_metrics: &LineMetrics) -> Au { + match self.fragment.borrow().style.clone_vertical_align() { + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => Au::zero(), + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => { + line_metrics.block_size - self.size.block + }, + + // This covers all baseline-relative vertical alignment. + _ => { + let baseline = line_metrics.baseline_block_offset + self.baseline_offset_in_parent; + baseline - self.baseline_offset_in_item + }, + } + } +} + +pub(super) struct AbsolutelyPositionedLineItem { + pub absolutely_positioned_box: ArcRefCell<AbsolutelyPositionedBox>, +} + +pub(super) struct FloatLineItem { + pub fragment: ArcRefCell<BoxFragment>, + /// Whether or not this float Fragment has been placed yet. Fragments that + /// do not fit on a line need to be placed after the hypothetical block start + /// of the next line. + pub needs_placement: bool, +} + +fn line_height(parent_style: &ComputedValues, font_metrics: &FontMetrics) -> Au { + let font = parent_style.get_font(); + let font_size = font.font_size.computed_size(); + match font.line_height { + LineHeight::Normal => font_metrics.line_gap, + LineHeight::Number(number) => (font_size * number.0).into(), + LineHeight::Length(length) => length.0.into(), + } +} + +/// Sort a mutable slice by the the given indices array in place, reording the slice so that final +/// value of `slice[x]` is `slice[indices[x]]`. +fn sort_by_indices_in_place<T>(data: &mut [T], mut indices: Vec<usize>) { + for idx in 0..data.len() { + if indices[idx] == idx { + continue; + } + + let mut current_idx = idx; + loop { + let target_idx = indices[current_idx]; + indices[current_idx] = current_idx; + if indices[target_idx] == target_idx { + break; + } + data.swap(current_idx, target_idx); + current_idx = target_idx; + } + } +} diff --git a/components/layout/flow/inline/line_breaker.rs b/components/layout/flow/inline/line_breaker.rs new file mode 100644 index 00000000000..28301fdadf8 --- /dev/null +++ b/components/layout/flow/inline/line_breaker.rs @@ -0,0 +1,120 @@ +/* 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 std::ops::Range; + +use icu_segmenter::LineSegmenter; + +pub(crate) struct LineBreaker { + linebreaks: Vec<usize>, + current_offset: usize, +} + +impl LineBreaker { + pub(crate) fn new(string: &str) -> Self { + let line_segmenter = LineSegmenter::new_auto(); + Self { + // From https://docs.rs/icu_segmenter/1.5.0/icu_segmenter/struct.LineSegmenter.html + // > For consistency with the grapheme, word, and sentence segmenters, there is always a + // > breakpoint returned at index 0, but this breakpoint is not a meaningful line break + // > opportunity. + // + // Skip this first line break opportunity, as it isn't interesting to us. + linebreaks: line_segmenter.segment_str(string).skip(1).collect(), + current_offset: 0, + } + } + + pub(crate) fn advance_to_linebreaks_in_range(&mut self, text_range: Range<usize>) -> &[usize] { + let linebreaks_in_range = self.linebreaks_in_range_after_current_offset(text_range); + self.current_offset = linebreaks_in_range.end; + &self.linebreaks[linebreaks_in_range] + } + + fn linebreaks_in_range_after_current_offset(&self, text_range: Range<usize>) -> Range<usize> { + assert!(text_range.start <= text_range.end); + + let mut linebreaks_range = self.current_offset..self.linebreaks.len(); + + while self.linebreaks[linebreaks_range.start] < text_range.start && + linebreaks_range.len() > 1 + { + linebreaks_range.start += 1; + } + + let mut ending_linebreak_index = linebreaks_range.start; + while self.linebreaks[ending_linebreak_index] < text_range.end && + ending_linebreak_index < self.linebreaks.len() - 1 + { + ending_linebreak_index += 1; + } + linebreaks_range.end = ending_linebreak_index; + linebreaks_range + } +} + +#[test] +fn test_linebreaker_ranges() { + let linebreaker = LineBreaker::new("abc def"); + assert_eq!(linebreaker.linebreaks, [4, 7]); + assert_eq!( + linebreaker.linebreaks_in_range_after_current_offset(0..5), + 0..1 + ); + // The last linebreak should not be included for the text range we are interested in. + assert_eq!( + linebreaker.linebreaks_in_range_after_current_offset(0..7), + 0..1 + ); + + let linebreaker = LineBreaker::new("abc d def"); + assert_eq!(linebreaker.linebreaks, [4, 6, 9]); + assert_eq!( + linebreaker.linebreaks_in_range_after_current_offset(0..5), + 0..1 + ); + assert_eq!( + linebreaker.linebreaks_in_range_after_current_offset(0..7), + 0..2 + ); + assert_eq!( + linebreaker.linebreaks_in_range_after_current_offset(0..9), + 0..2 + ); + + assert_eq!( + linebreaker.linebreaks_in_range_after_current_offset(4..9), + 0..2 + ); + + std::panic::catch_unwind(|| { + let linebreaker = LineBreaker::new("abc def"); + linebreaker.linebreaks_in_range_after_current_offset(5..2); + }) + .expect_err("Reversed range should cause an assertion failure."); +} + +#[test] +fn test_linebreaker_stateful_advance() { + let mut linebreaker = LineBreaker::new("abc d def"); + assert_eq!(linebreaker.linebreaks, [4, 6, 9]); + assert!(linebreaker.advance_to_linebreaks_in_range(0..7) == &[4, 6]); + assert!(linebreaker.advance_to_linebreaks_in_range(8..9).is_empty()); + + // We've already advanced, so a range from the beginning shouldn't affect things. + assert!(linebreaker.advance_to_linebreaks_in_range(0..9).is_empty()); + + linebreaker.current_offset = 0; + + // Sending a value out of range shoudn't break things. + assert!(linebreaker.advance_to_linebreaks_in_range(0..999) == &[4, 6]); + + linebreaker.current_offset = 0; + + std::panic::catch_unwind(|| { + let mut linebreaker = LineBreaker::new("abc d def"); + linebreaker.advance_to_linebreaks_in_range(2..0); + }) + .expect_err("Reversed range should cause an assertion failure."); +} diff --git a/components/layout/flow/inline/mod.rs b/components/layout/flow/inline/mod.rs new file mode 100644 index 00000000000..490917d95a3 --- /dev/null +++ b/components/layout/flow/inline/mod.rs @@ -0,0 +1,2525 @@ +/* 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/. */ + +//! # Inline Formatting Context Layout +//! +//! Inline layout is divided into three phases: +//! +//! 1. Box Tree Construction +//! 2. Box to Line Layout +//! 3. Line to Fragment Layout +//! +//! The first phase happens during normal box tree constrution, while the second two phases happen +//! during fragment tree construction (sometimes called just "layout"). +//! +//! ## Box Tree Construction +//! +//! During box tree construction, DOM elements are transformed into a box tree. This phase collects +//! all of the inline boxes, text, atomic inline elements (boxes with `display: inline-block` or +//! `display: inline-table` as well as things like images and canvas), absolutely positioned blocks, +//! and floated blocks. +//! +//! During the last part of this phase, whitespace is collapsed and text is segmented into +//! [`TextRun`]s based on script, chosen font, and line breaking opportunities. In addition, default +//! fonts are selected for every inline box. Each segment of text is shaped using HarfBuzz and +//! turned into a series of glyphs, which all have a size and a position relative to the origin of +//! the [`TextRun`] (calculated in later phases). +//! +//! The code for this phase is mainly in `construct.rs`, but text handling can also be found in +//! `text_runs.rs.` +//! +//! ## Box to Line Layout +//! +//! During the first phase of fragment tree construction, box tree items are laid out into +//! [`LineItem`]s and fragmented based on line boundaries. This is where line breaking happens. This +//! part of layout fragments boxes and their contents across multiple lines while positioning floats +//! and making sure non-floated contents flow around them. In addition, all atomic elements are laid +//! out, which may descend into their respective trees and create fragments. Finally, absolutely +//! positioned content is collected in order to later hoist it to the containing block for +//! absolutes. +//! +//! Note that during this phase, layout does not know the final block position of content. Only +//! during line to fragment layout, are the final block positions calculated based on the line's +//! final content and its vertical alignment. Instead, positions and line heights are calculated +//! relative to the line's final baseline which will be determined in the final phase. +//! +//! [`LineItem`]s represent a particular set of content on a line. Currently this is represented by +//! a linear series of items that describe the line's hierarchy of inline boxes and content. The +//! item types are: +//! +//! - [`LineItem::InlineStartBoxPaddingBorderMargin`] +//! - [`LineItem::InlineEndBoxPaddingBorderMargin`] +//! - [`LineItem::TextRun`] +//! - [`LineItem::Atomic`] +//! - [`LineItem::AbsolutelyPositioned`] +//! - [`LineItem::Float`] +//! +//! The code for this can be found by looking for methods of the form `layout_into_line_item()`. +//! +//! ## Line to Fragment Layout +//! +//! During the second phase of fragment tree construction, the final block position of [`LineItem`]s +//! is calculated and they are converted into [`Fragment`]s. After layout, the [`LineItem`]s are +//! discarded and the new fragments are incorporated into the fragment tree. The final static +//! position of absolutely positioned content is calculated and it is hoisted to its containing +//! block via [`PositioningContext`]. +//! +//! The code for this phase, can mainly be found in `line.rs`. +//! + +pub mod construct; +pub mod inline_box; +pub mod line; +mod line_breaker; +pub mod text_run; + +use std::cell::{OnceCell, RefCell}; +use std::mem; +use std::rc::Rc; + +use app_units::{Au, MAX_AU}; +use bitflags::bitflags; +use construct::InlineFormattingContextBuilder; +use fonts::{ByteIndex, FontMetrics, GlyphStore}; +use inline_box::{InlineBox, InlineBoxContainerState, InlineBoxIdentifier, InlineBoxes}; +use line::{ + AbsolutelyPositionedLineItem, AtomicLineItem, FloatLineItem, LineItem, LineItemLayout, + TextRunLineItem, +}; +use line_breaker::LineBreaker; +use malloc_size_of_derive::MallocSizeOf; +use range::Range; +use servo_arc::Arc; +use style::Zero; +use style::computed_values::text_wrap_mode::T as TextWrapMode; +use style::computed_values::vertical_align::T as VerticalAlign; +use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; +use style::context::QuirksMode; +use style::properties::ComputedValues; +use style::properties::style_structs::InheritedText; +use style::values::generics::box_::VerticalAlignKeyword; +use style::values::generics::font::LineHeight; +use style::values::specified::box_::BaselineSource; +use style::values::specified::text::{TextAlignKeyword, TextDecorationLine}; +use style::values::specified::{TextAlignLast, TextJustify}; +use text_run::{ + TextRun, XI_LINE_BREAKING_CLASS_GL, XI_LINE_BREAKING_CLASS_WJ, XI_LINE_BREAKING_CLASS_ZWJ, + add_or_get_font, get_font_for_first_font_for_style, +}; +use unicode_bidi::{BidiInfo, Level}; +use webrender_api::FontInstanceKey; +use xi_unicode::linebreak_property; + +use super::float::{Clear, PlacementAmongFloats}; +use super::{ + CacheableLayoutResult, IndependentFloatOrAtomicLayoutResult, + IndependentFormattingContextContents, +}; +use crate::cell::ArcRefCell; +use crate::context::LayoutContext; +use crate::flow::CollapsibleWithParentStartMargin; +use crate::flow::float::{FloatBox, SequentialLayoutState}; +use crate::formatting_contexts::{ + Baselines, IndependentFormattingContext, IndependentNonReplacedContents, +}; +use crate::fragment_tree::{ + BoxFragment, CollapsedBlockMargins, CollapsedMargin, Fragment, FragmentFlags, + PositioningFragment, +}; +use crate::geom::{LogicalRect, LogicalVec2, ToLogical}; +use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; +use crate::sizing::{ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult}; +use crate::style_ext::{ComputedValuesExt, PaddingBorderMargin}; +use crate::{ConstraintSpace, ContainingBlock, PropagatedBoxTreeData}; + +// From gfxFontConstants.h in Firefox. +static FONT_SUBSCRIPT_OFFSET_RATIO: f32 = 0.20; +static FONT_SUPERSCRIPT_OFFSET_RATIO: f32 = 0.34; + +#[derive(Debug, MallocSizeOf)] +pub(crate) struct InlineFormattingContext { + /// All [`InlineItem`]s in this [`InlineFormattingContext`] stored in a flat array. + /// [`InlineItem::StartInlineBox`] and [`InlineItem::EndInlineBox`] allow representing + /// the tree of inline boxes within the formatting context, but a flat array allows + /// easy iteration through all inline items. + pub(super) inline_items: Vec<ArcRefCell<InlineItem>>, + + /// The tree of inline boxes in this [`InlineFormattingContext`]. These are stored in + /// a flat array with each being given a [`InlineBoxIdentifier`]. + pub(super) inline_boxes: InlineBoxes, + + /// The text content of this inline formatting context. + pub(super) text_content: String, + + /// A store of font information for all the shaped segments in this formatting + /// context in order to avoid duplicating this information. + pub font_metrics: Vec<FontKeyAndMetrics>, + + pub(super) text_decoration_line: TextDecorationLine, + + /// Whether this IFC contains the 1st formatted line of an element: + /// <https://www.w3.org/TR/css-pseudo-4/#first-formatted-line>. + pub(super) has_first_formatted_line: bool, + + /// Whether or not this [`InlineFormattingContext`] contains floats. + pub(super) contains_floats: bool, + + /// Whether or not this is an [`InlineFormattingContext`] for a single line text input. + pub(super) is_single_line_text_input: bool, + + /// Whether or not this is an [`InlineFormattingContext`] has right-to-left content, which + /// will require reordering during layout. + pub(super) has_right_to_left_content: bool, +} + +/// A collection of data used to cache [`FontMetrics`] in the [`InlineFormattingContext`] +#[derive(Debug, MallocSizeOf)] +pub(crate) struct FontKeyAndMetrics { + pub key: FontInstanceKey, + pub pt_size: Au, + pub metrics: FontMetrics, +} + +#[derive(Debug, MallocSizeOf)] +pub(crate) enum InlineItem { + StartInlineBox(ArcRefCell<InlineBox>), + EndInlineBox, + TextRun(ArcRefCell<TextRun>), + OutOfFlowAbsolutelyPositionedBox( + ArcRefCell<AbsolutelyPositionedBox>, + usize, /* offset_in_text */ + ), + OutOfFlowFloatBox(#[conditional_malloc_size_of] Arc<FloatBox>), + Atomic( + #[conditional_malloc_size_of] Arc<IndependentFormattingContext>, + usize, /* offset_in_text */ + Level, /* bidi_level */ + ), +} + +impl InlineItem { + pub(crate) fn invalidate_cached_fragment(&self) { + match self { + InlineItem::StartInlineBox(inline_box) => { + inline_box.borrow().base.invalidate_cached_fragment() + }, + InlineItem::EndInlineBox | InlineItem::TextRun(..) => {}, + InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => { + positioned_box + .borrow() + .context + .base + .invalidate_cached_fragment(); + }, + InlineItem::OutOfFlowFloatBox(float_box) => { + float_box.contents.base.invalidate_cached_fragment() + }, + InlineItem::Atomic(independent_formatting_context, ..) => { + independent_formatting_context + .base + .invalidate_cached_fragment(); + }, + } + } + + pub(crate) fn fragments(&self) -> Vec<Fragment> { + match self { + InlineItem::StartInlineBox(inline_box) => inline_box.borrow().base.fragments(), + InlineItem::EndInlineBox | InlineItem::TextRun(..) => { + unreachable!("Should never have these kind of fragments attached to a DOM node") + }, + InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => { + positioned_box.borrow().context.base.fragments() + }, + InlineItem::OutOfFlowFloatBox(float_box) => float_box.contents.base.fragments(), + InlineItem::Atomic(independent_formatting_context, ..) => { + independent_formatting_context.base.fragments() + }, + } + } +} + +/// Information about the current line under construction for a particular +/// [`InlineFormattingContextLayout`]. This tracks position and size information while +/// [`LineItem`]s are collected and is used as input when those [`LineItem`]s are +/// converted into [`Fragment`]s during the final phase of line layout. Note that this +/// does not store the [`LineItem`]s themselves, as they are stored as part of the +/// nesting state in the [`InlineFormattingContextLayout`]. +struct LineUnderConstruction { + /// The position where this line will start once it is laid out. This includes any + /// offset from `text-indent`. + start_position: LogicalVec2<Au>, + + /// The current inline position in the line being laid out into [`LineItem`]s in this + /// [`InlineFormattingContext`] independent of the depth in the nesting level. + inline_position: Au, + + /// The maximum block size of all boxes that ended and are in progress in this line. + /// This uses [`LineBlockSizes`] instead of a simple value, because the final block size + /// depends on vertical alignment. + max_block_size: LineBlockSizes, + + /// Whether any active linebox has added a glyph or atomic element to this line, which + /// indicates that the next run that exceeds the line length can cause a line break. + has_content: bool, + + /// Whether or not there are floats that did not fit on the current line. Before + /// the [`LineItem`]s of this line are laid out, these floats will need to be + /// placed directly below this line, but still as children of this line's Fragments. + has_floats_waiting_to_be_placed: bool, + + /// A rectangular area (relative to the containing block / inline formatting + /// context boundaries) where we can fit the line box without overlapping floats. + /// Note that when this is not empty, its start corner takes precedence over + /// [`LineUnderConstruction::start_position`]. + placement_among_floats: OnceCell<LogicalRect<Au>>, + + /// The LineItems for the current line under construction that have already + /// been committed to this line. + line_items: Vec<LineItem>, +} + +impl LineUnderConstruction { + fn new(start_position: LogicalVec2<Au>) -> Self { + Self { + inline_position: start_position.inline, + start_position, + max_block_size: LineBlockSizes::zero(), + has_content: false, + has_floats_waiting_to_be_placed: false, + placement_among_floats: OnceCell::new(), + line_items: Vec::new(), + } + } + + fn line_block_start_considering_placement_among_floats(&self) -> Au { + match self.placement_among_floats.get() { + Some(placement_among_floats) => placement_among_floats.start_corner.block, + None => self.start_position.block, + } + } + + fn replace_placement_among_floats(&mut self, new_placement: LogicalRect<Au>) { + self.placement_among_floats.take(); + let _ = self.placement_among_floats.set(new_placement); + } + + /// Trim the trailing whitespace in this line and return the width of the whitespace trimmed. + fn trim_trailing_whitespace(&mut self) -> Au { + // From <https://www.w3.org/TR/css-text-3/#white-space-phase-2>: + // > 3. A sequence of collapsible spaces at the end of a line is removed, + // > as well as any trailing U+1680 OGHAM SPACE MARK whose white-space + // > property is normal, nowrap, or pre-line. + let mut whitespace_trimmed = Au::zero(); + for item in self.line_items.iter_mut().rev() { + if !item.trim_whitespace_at_end(&mut whitespace_trimmed) { + break; + } + } + + whitespace_trimmed + } + + /// Count the number of justification opportunities in this line. + fn count_justification_opportunities(&self) -> usize { + self.line_items + .iter() + .filter_map(|item| match item { + LineItem::TextRun(_, text_run) => Some( + text_run + .text + .iter() + .map(|glyph_store| glyph_store.total_word_separators()) + .sum::<usize>(), + ), + _ => None, + }) + .sum() + } +} + +/// A block size relative to a line's final baseline. This is to track the size +/// contribution of a particular element of a line above and below the baseline. +/// These sizes can be combined with other baseline relative sizes before the +/// final baseline position is known. The values here are relative to the +/// overall line's baseline and *not* the nested baseline of an inline box. +#[derive(Clone, Debug)] +struct BaselineRelativeSize { + /// The ascent above the baseline, where a positive value means a larger + /// ascent. Thus, the top of this size contribution is `baseline_offset - + /// ascent`. + ascent: Au, + + /// The descent below the baseline, where a positive value means a larger + /// descent. Thus, the bottom of this size contribution is `baseline_offset + + /// descent`. + descent: Au, +} + +impl BaselineRelativeSize { + fn zero() -> Self { + Self { + ascent: Au::zero(), + descent: Au::zero(), + } + } + + fn max(&self, other: &Self) -> Self { + BaselineRelativeSize { + ascent: self.ascent.max(other.ascent), + descent: self.descent.max(other.descent), + } + } + + /// Given an offset from the line's root baseline, adjust this [`BaselineRelativeSize`] + /// by that offset. This is used to adjust a [`BaselineRelativeSize`] for different kinds + /// of baseline-relative `vertical-align`. This will "move" measured size of a particular + /// inline box's block size. For example, in the following HTML: + /// + /// ```html + /// <div> + /// <span style="vertical-align: 5px">child content</span> + /// </div> + /// ```` + /// + /// If this [`BaselineRelativeSize`] is for the `<span>` then the adjustment + /// passed here would be equivalent to -5px. + fn adjust_for_nested_baseline_offset(&mut self, baseline_offset: Au) { + self.ascent -= baseline_offset; + self.descent += baseline_offset; + } +} + +#[derive(Clone, Debug)] +struct LineBlockSizes { + line_height: Au, + baseline_relative_size_for_line_height: Option<BaselineRelativeSize>, + size_for_baseline_positioning: BaselineRelativeSize, +} + +impl LineBlockSizes { + fn zero() -> Self { + LineBlockSizes { + line_height: Au::zero(), + baseline_relative_size_for_line_height: None, + size_for_baseline_positioning: BaselineRelativeSize::zero(), + } + } + + fn resolve(&self) -> Au { + let height_from_ascent_and_descent = self + .baseline_relative_size_for_line_height + .as_ref() + .map(|size| (size.ascent + size.descent).abs()) + .unwrap_or_else(Au::zero); + self.line_height.max(height_from_ascent_and_descent) + } + + fn max(&self, other: &LineBlockSizes) -> LineBlockSizes { + let baseline_relative_size = match ( + self.baseline_relative_size_for_line_height.as_ref(), + other.baseline_relative_size_for_line_height.as_ref(), + ) { + (Some(our_size), Some(other_size)) => Some(our_size.max(other_size)), + (our_size, other_size) => our_size.or(other_size).cloned(), + }; + Self { + line_height: self.line_height.max(other.line_height), + baseline_relative_size_for_line_height: baseline_relative_size, + size_for_baseline_positioning: self + .size_for_baseline_positioning + .max(&other.size_for_baseline_positioning), + } + } + + fn max_assign(&mut self, other: &LineBlockSizes) { + *self = self.max(other); + } + + fn adjust_for_baseline_offset(&mut self, baseline_offset: Au) { + if let Some(size) = self.baseline_relative_size_for_line_height.as_mut() { + size.adjust_for_nested_baseline_offset(baseline_offset) + } + self.size_for_baseline_positioning + .adjust_for_nested_baseline_offset(baseline_offset); + } + + /// From <https://drafts.csswg.org/css2/visudet.html#line-height>: + /// > The inline-level boxes are aligned vertically according to their 'vertical-align' + /// > property. In case they are aligned 'top' or 'bottom', they must be aligned so as + /// > to minimize the line box height. If such boxes are tall enough, there are multiple + /// > solutions and CSS 2 does not define the position of the line box's baseline (i.e., + /// > the position of the strut, see below). + fn find_baseline_offset(&self) -> Au { + match self.baseline_relative_size_for_line_height.as_ref() { + Some(size) => size.ascent, + None => { + // This is the case mentinoned above where there are multiple solutions. + // This code is putting the baseline roughly in the middle of the line. + let leading = self.resolve() - + (self.size_for_baseline_positioning.ascent + + self.size_for_baseline_positioning.descent); + leading.scale_by(0.5) + self.size_for_baseline_positioning.ascent + }, + } + } +} + +/// The current unbreakable segment under construction for an inline formatting context. +/// Items accumulate here until we reach a soft line break opportunity during processing +/// of inline content or we reach the end of the formatting context. +struct UnbreakableSegmentUnderConstruction { + /// The size of this unbreakable segment in both dimension. + inline_size: Au, + + /// The maximum block size that this segment has. This uses [`LineBlockSizes`] instead of a + /// simple value, because the final block size depends on vertical alignment. + max_block_size: LineBlockSizes, + + /// The LineItems for the segment under construction + line_items: Vec<LineItem>, + + /// The depth in the inline box hierarchy at the start of this segment. This is used + /// to prefix this segment when it is pushed to a new line. + inline_box_hierarchy_depth: Option<usize>, + + /// Whether any active linebox has added a glyph or atomic element to this line + /// segment, which indicates that the next run that exceeds the line length can cause + /// a line break. + has_content: bool, + + /// The inline size of any trailing whitespace in this segment. + trailing_whitespace_size: Au, +} + +impl UnbreakableSegmentUnderConstruction { + fn new() -> Self { + Self { + inline_size: Au::zero(), + max_block_size: LineBlockSizes { + line_height: Au::zero(), + baseline_relative_size_for_line_height: None, + size_for_baseline_positioning: BaselineRelativeSize::zero(), + }, + line_items: Vec::new(), + inline_box_hierarchy_depth: None, + has_content: false, + trailing_whitespace_size: Au::zero(), + } + } + + /// Reset this segment after its contents have been committed to a line. + fn reset(&mut self) { + assert!(self.line_items.is_empty()); // Preserve allocated memory. + self.inline_size = Au::zero(); + self.max_block_size = LineBlockSizes::zero(); + self.inline_box_hierarchy_depth = None; + self.has_content = false; + self.trailing_whitespace_size = Au::zero(); + } + + /// Push a single line item to this segment. In addition, record the inline box + /// hierarchy depth if this is the first segment. The hierarchy depth is used to + /// duplicate the necessary `StartInlineBox` tokens if this segment is ultimately + /// placed on a new empty line. + fn push_line_item(&mut self, line_item: LineItem, inline_box_hierarchy_depth: usize) { + if self.line_items.is_empty() { + self.inline_box_hierarchy_depth = Some(inline_box_hierarchy_depth); + } + self.line_items.push(line_item); + } + + /// Trim whitespace from the beginning of this UnbreakbleSegmentUnderConstruction. + /// + /// From <https://www.w3.org/TR/css-text-3/#white-space-phase-2>: + /// + /// > Then, the entire block is rendered. Inlines are laid out, taking bidi + /// > reordering into account, and wrapping as specified by the text-wrap + /// > property. As each line is laid out, + /// > 1. A sequence of collapsible spaces at the beginning of a line is removed. + /// + /// This prevents whitespace from being added to the beginning of a line. + fn trim_leading_whitespace(&mut self) { + let mut whitespace_trimmed = Au::zero(); + for item in self.line_items.iter_mut() { + if !item.trim_whitespace_at_start(&mut whitespace_trimmed) { + break; + } + } + self.inline_size -= whitespace_trimmed; + } +} + +bitflags! { + pub struct InlineContainerStateFlags: u8 { + const CREATE_STRUT = 0b0001; + const IS_SINGLE_LINE_TEXT_INPUT = 0b0010; + } +} + +pub(super) struct InlineContainerState { + /// The style of this inline container. + style: Arc<ComputedValues>, + + /// Flags which describe details of this [`InlineContainerState`]. + flags: InlineContainerStateFlags, + + /// Whether or not we have processed any content (an atomic element or text) for + /// this inline box on the current line OR any previous line. + has_content: RefCell<bool>, + + /// Indicates whether this nesting level have text decorations in effect. + /// From <https://drafts.csswg.org/css-text-decor/#line-decoration> + // "When specified on or propagated to a block container that establishes + // an IFC..." + text_decoration_line: TextDecorationLine, + + /// The block size contribution of this container's default font ie the size of the + /// "strut." Whether this is integrated into the [`Self::nested_strut_block_sizes`] + /// depends on the line-height quirk described in + /// <https://quirks.spec.whatwg.org/#the-line-height-calculation-quirk>. + strut_block_sizes: LineBlockSizes, + + /// The strut block size of this inline container maxed with the strut block + /// sizes of all inline container ancestors. In quirks mode, this will be + /// zero, until we know that an element has inline content. + nested_strut_block_sizes: LineBlockSizes, + + /// The baseline offset of this container from the baseline of the line. The is the + /// cumulative offset of this container and all of its parents. In contrast to the + /// `vertical-align` property a positive value indicates an offset "below" the + /// baseline while a negative value indicates one "above" it (when the block direction + /// is vertical). + pub baseline_offset: Au, + + /// The font metrics of the non-fallback font for this container. + font_metrics: FontMetrics, +} + +pub(super) struct InlineFormattingContextLayout<'layout_data> { + positioning_context: &'layout_data mut PositioningContext, + containing_block: &'layout_data ContainingBlock<'layout_data>, + sequential_layout_state: Option<&'layout_data mut SequentialLayoutState>, + layout_context: &'layout_data LayoutContext<'layout_data>, + + /// The [`InlineFormattingContext`] that we are laying out. + ifc: &'layout_data InlineFormattingContext, + + /// The [`InlineContainerState`] for the container formed by the root of the + /// [`InlineFormattingContext`]. This is effectively the "root inline box" described + /// by <https://drafts.csswg.org/css-inline/#model>: + /// + /// > The block container also generates a root inline box, which is an anonymous + /// > inline box that holds all of its inline-level contents. (Thus, all text in an + /// > inline formatting context is directly contained by an inline box, whether the root + /// > inline box or one of its descendants.) The root inline box inherits from its + /// > parent block container, but is otherwise unstyleable. + root_nesting_level: InlineContainerState, + + /// A stack of [`InlineBoxContainerState`] that is used to produce [`LineItem`]s either when we + /// reach the end of an inline box or when we reach the end of a line. Only at the end + /// of the inline box is the state popped from the stack. + inline_box_state_stack: Vec<Rc<InlineBoxContainerState>>, + + /// A collection of [`InlineBoxContainerState`] of all the inlines that are present + /// in this inline formatting context. We keep this as well as the stack, so that we + /// can access them during line layout, which may happen after relevant [`InlineBoxContainerState`]s + /// have been popped of the the stack. + inline_box_states: Vec<Rc<InlineBoxContainerState>>, + + /// A vector of fragment that are laid out. This includes one [`Fragment::Positioning`] + /// per line that is currently laid out plus fragments for all floats, which + /// are currently laid out at the top-level of each [`InlineFormattingContext`]. + fragments: Vec<Fragment>, + + /// Information about the line currently being laid out into [`LineItem`]s. + current_line: LineUnderConstruction, + + /// Information about the unbreakable line segment currently being laid out into [`LineItem`]s. + current_line_segment: UnbreakableSegmentUnderConstruction, + + /// After a forced line break (for instance from a `<br>` element) we wait to actually + /// break the line until seeing more content. This allows ongoing inline boxes to finish, + /// since in the case where they have no more content they should not be on the next + /// line. + /// + /// For instance: + /// + /// ``` html + /// <span style="border-right: 30px solid blue;"> + /// first line<br> + /// </span> + /// second line + /// ``` + /// + /// In this case, the `<span>` should not extend to the second line. If we linebreak + /// as soon as we encounter the `<br>` the `<span>`'s ending inline borders would be + /// placed on the second line, because we add those borders in + /// [`InlineFormattingContextLayout::finish_inline_box()`]. + linebreak_before_new_content: bool, + + /// When a `<br>` element has `clear`, this needs to be applied after the linebreak, + /// which will be processed *after* the `<br>` element is processed. This member + /// stores any deferred `clear` to apply after a linebreak. + deferred_br_clear: Clear, + + /// Whether or not a soft wrap opportunity is queued. Soft wrap opportunities are + /// queued after replaced content and they are processed when the next text content + /// is encountered. + pub have_deferred_soft_wrap_opportunity: bool, + + /// Whether or not this InlineFormattingContext has processed any in flow content at all. + had_inflow_content: bool, + + /// Whether or not the layout of this InlineFormattingContext depends on the block size + /// of its container for the purposes of flexbox layout. + depends_on_block_constraints: bool, + + /// The currently white-space-collapse setting of this line. This is stored on the + /// [`InlineFormattingContextLayout`] because when a soft wrap opportunity is defined + /// by the boundary between two characters, the white-space-collapse property of their + /// nearest common ancestor is used. + white_space_collapse: WhiteSpaceCollapse, + + /// The currently text-wrap-mode setting of this line. This is stored on the + /// [`InlineFormattingContextLayout`] because when a soft wrap opportunity is defined + /// by the boundary between two characters, the text-wrap-mode property of their nearest + /// common ancestor is used. + text_wrap_mode: TextWrapMode, + + /// The offset of the first and last baselines in the inline formatting context that we + /// are laying out. This is used to propagate baselines to the ancestors of + /// `display: inline-block` elements and table content. + baselines: Baselines, +} + +impl InlineFormattingContextLayout<'_> { + fn current_inline_container_state(&self) -> &InlineContainerState { + match self.inline_box_state_stack.last() { + Some(inline_box_state) => &inline_box_state.base, + None => &self.root_nesting_level, + } + } + + fn current_inline_box_identifier(&self) -> Option<InlineBoxIdentifier> { + self.inline_box_state_stack + .last() + .map(|state| state.identifier) + } + + fn current_line_max_block_size_including_nested_containers(&self) -> LineBlockSizes { + self.current_inline_container_state() + .nested_strut_block_sizes + .max(&self.current_line.max_block_size) + } + + fn propagate_current_nesting_level_white_space_style(&mut self) { + let style = match self.inline_box_state_stack.last() { + Some(inline_box_state) => &inline_box_state.base.style, + None => self.containing_block.style, + }; + let style_text = style.get_inherited_text(); + self.white_space_collapse = style_text.white_space_collapse; + self.text_wrap_mode = style_text.text_wrap_mode; + } + + fn processing_br_element(&self) -> bool { + self.inline_box_state_stack + .last() + .map(|state| { + state + .base_fragment_info + .flags + .contains(FragmentFlags::IS_BR_ELEMENT) + }) + .unwrap_or(false) + } + + /// Start laying out a particular [`InlineBox`] into line items. This will push + /// a new [`InlineBoxContainerState`] onto [`Self::inline_box_state_stack`]. + fn start_inline_box(&mut self, inline_box: &InlineBox) { + let inline_box_state = InlineBoxContainerState::new( + inline_box, + self.containing_block, + self.layout_context, + self.current_inline_container_state(), + inline_box.is_last_fragment, + inline_box + .default_font_index + .map(|index| &self.ifc.font_metrics[index].metrics), + ); + + self.depends_on_block_constraints |= inline_box + .base + .style + .depends_on_block_constraints_due_to_relative_positioning( + self.containing_block.style.writing_mode, + ); + + // If we are starting a `<br>` element prepare to clear after its deferred linebreak has been + // processed. Note that a `<br>` is composed of the element itself and the inner pseudo-element + // with the actual linebreak. Both will have this `FragmentFlag`; that's why this code only + // sets `deferred_br_clear` if it isn't set yet. + if inline_box_state + .base_fragment_info + .flags + .contains(FragmentFlags::IS_BR_ELEMENT) && + self.deferred_br_clear == Clear::None + { + self.deferred_br_clear = Clear::from_style_and_container_writing_mode( + &inline_box_state.base.style, + self.containing_block.style.writing_mode, + ); + } + + if inline_box.is_first_fragment { + self.current_line_segment.inline_size += inline_box_state.pbm.padding.inline_start + + inline_box_state.pbm.border.inline_start + + inline_box_state.pbm.margin.inline_start.auto_is(Au::zero); + self.current_line_segment + .line_items + .push(LineItem::InlineStartBoxPaddingBorderMargin( + inline_box.identifier, + )); + } + + let inline_box_state = Rc::new(inline_box_state); + + // Push the state onto the IFC-wide collection of states. Inline boxes are numbered in + // the order that they are encountered, so this should correspond to the order they + // are pushed onto `self.inline_box_states`. + assert_eq!( + self.inline_box_states.len(), + inline_box.identifier.index_in_inline_boxes as usize + ); + self.inline_box_states.push(inline_box_state.clone()); + self.inline_box_state_stack.push(inline_box_state); + } + + /// Finish laying out a particular [`InlineBox`] into line items. This will + /// pop its state off of [`Self::inline_box_state_stack`]. + fn finish_inline_box(&mut self) { + let inline_box_state = match self.inline_box_state_stack.pop() { + Some(inline_box_state) => inline_box_state, + None => return, // We are at the root. + }; + + self.current_line_segment + .max_block_size + .max_assign(&inline_box_state.base.nested_strut_block_sizes); + + // If the inline box that we just finished had any content at all, we want to propagate + // the `white-space` property of its parent to future inline children. This is because + // when a soft wrap opportunity is defined by the boundary between two elements, the + // `white-space` used is that of their nearest common ancestor. + if *inline_box_state.base.has_content.borrow() { + self.propagate_current_nesting_level_white_space_style(); + } + + if inline_box_state.is_last_fragment { + let pbm_end = inline_box_state.pbm.padding.inline_end + + inline_box_state.pbm.border.inline_end + + inline_box_state.pbm.margin.inline_end.auto_is(Au::zero); + self.current_line_segment.inline_size += pbm_end; + self.current_line_segment + .line_items + .push(LineItem::InlineEndBoxPaddingBorderMargin( + inline_box_state.identifier, + )) + } + } + + fn finish_last_line(&mut self) { + // We are at the end of the IFC, and we need to do a few things to make sure that + // the current segment is committed and that the final line is finished. + // + // A soft wrap opportunity makes it so the current segment is placed on a new line + // if it doesn't fit on the current line under construction. + self.process_soft_wrap_opportunity(); + + // `process_soft_line_wrap_opportunity` does not commit the segment to a line if + // there is no line wrapping, so this forces the segment into the current line. + self.commit_current_segment_to_line(); + + // Finally we finish the line itself and convert all of the LineItems into + // fragments. + self.finish_current_line_and_reset(true /* last_line_or_forced_line_break */); + } + + /// Finish layout of all inline boxes for the current line. This will gather all + /// [`LineItem`]s and turn them into [`Fragment`]s, then reset the + /// [`InlineFormattingContextLayout`] preparing it for laying out a new line. + fn finish_current_line_and_reset(&mut self, last_line_or_forced_line_break: bool) { + let whitespace_trimmed = self.current_line.trim_trailing_whitespace(); + let (inline_start_position, justification_adjustment) = self + .calculate_current_line_inline_start_and_justification_adjustment( + whitespace_trimmed, + last_line_or_forced_line_break, + ); + + let block_start_position = self + .current_line + .line_block_start_considering_placement_among_floats(); + let had_inline_advance = + self.current_line.inline_position != self.current_line.start_position.inline; + + let effective_block_advance = if self.current_line.has_content || + had_inline_advance || + self.linebreak_before_new_content + { + self.current_line_max_block_size_including_nested_containers() + } else { + LineBlockSizes::zero() + }; + + let resolved_block_advance = effective_block_advance.resolve(); + let mut block_end_position = block_start_position + resolved_block_advance; + if let Some(sequential_layout_state) = self.sequential_layout_state.as_mut() { + // This amount includes both the block size of the line and any extra space + // added to move the line down in order to avoid overlapping floats. + let increment = block_end_position - self.current_line.start_position.block; + sequential_layout_state.advance_block_position(increment); + + // This newline may have been triggered by a `<br>` with clearance, in which case we + // want to make sure that we make space not only for the current line, but any clearance + // from floats. + if let Some(clearance) = sequential_layout_state + .calculate_clearance(self.deferred_br_clear, &CollapsedMargin::zero()) + { + sequential_layout_state.advance_block_position(clearance); + block_end_position += clearance; + }; + self.deferred_br_clear = Clear::None; + } + + // Set up the new line now that we no longer need the old one. + let mut line_to_layout = std::mem::replace( + &mut self.current_line, + LineUnderConstruction::new(LogicalVec2 { + inline: Au::zero(), + block: block_end_position, + }), + ); + + if line_to_layout.has_floats_waiting_to_be_placed { + place_pending_floats(self, &mut line_to_layout.line_items); + } + + let start_position = LogicalVec2 { + block: block_start_position, + inline: inline_start_position, + }; + + let baseline_offset = effective_block_advance.find_baseline_offset(); + let start_positioning_context_length = self.positioning_context.len(); + let fragments = LineItemLayout::layout_line_items( + self, + line_to_layout.line_items, + start_position, + &effective_block_advance, + justification_adjustment, + ); + + // If the line doesn't have any fragments, we don't need to add a containing fragment for it. + if fragments.is_empty() && + self.positioning_context.len() == start_positioning_context_length + { + return; + } + + let baseline = baseline_offset + block_start_position; + self.baselines.first.get_or_insert(baseline); + self.baselines.last = Some(baseline); + + // The inline part of this start offset was taken into account when determining + // the inline start of the line in `calculate_inline_start_for_current_line` so + // we do not need to include it in the `start_corner` of the line's main Fragment. + let start_corner = LogicalVec2 { + inline: Au::zero(), + block: block_start_position, + }; + + let logical_origin_in_physical_coordinates = + start_corner.to_physical_vector(self.containing_block.style.writing_mode); + self.positioning_context + .adjust_static_position_of_hoisted_fragments_with_offset( + &logical_origin_in_physical_coordinates, + start_positioning_context_length, + ); + + let physical_line_rect = LogicalRect { + start_corner, + size: LogicalVec2 { + inline: self.containing_block.size.inline, + block: effective_block_advance.resolve(), + }, + } + .as_physical(Some(self.containing_block)); + self.fragments + .push(Fragment::Positioning(PositioningFragment::new_anonymous( + physical_line_rect, + fragments, + ))); + } + + /// Given the amount of whitespace trimmed from the line and taking into consideration + /// the `text-align` property, calculate where the line under construction starts in + /// the inline axis as well as the adjustment needed for every justification opportunity + /// to account for `text-align: justify`. + fn calculate_current_line_inline_start_and_justification_adjustment( + &self, + whitespace_trimmed: Au, + last_line_or_forced_line_break: bool, + ) -> (Au, Au) { + enum TextAlign { + Start, + Center, + End, + } + let style = self.containing_block.style; + let mut text_align_keyword = style.clone_text_align(); + + if last_line_or_forced_line_break { + text_align_keyword = match style.clone_text_align_last() { + TextAlignLast::Auto if text_align_keyword == TextAlignKeyword::Justify => { + TextAlignKeyword::Start + }, + TextAlignLast::Auto => text_align_keyword, + TextAlignLast::Start => TextAlignKeyword::Start, + TextAlignLast::End => TextAlignKeyword::End, + TextAlignLast::Left => TextAlignKeyword::Left, + TextAlignLast::Right => TextAlignKeyword::Right, + TextAlignLast::Center => TextAlignKeyword::Center, + TextAlignLast::Justify => TextAlignKeyword::Justify, + }; + } + + let text_align = match text_align_keyword { + TextAlignKeyword::Start => TextAlign::Start, + TextAlignKeyword::Center | TextAlignKeyword::MozCenter => TextAlign::Center, + TextAlignKeyword::End => TextAlign::End, + TextAlignKeyword::Left | TextAlignKeyword::MozLeft => { + if style.writing_mode.line_left_is_inline_start() { + TextAlign::Start + } else { + TextAlign::End + } + }, + TextAlignKeyword::Right | TextAlignKeyword::MozRight => { + if style.writing_mode.line_left_is_inline_start() { + TextAlign::End + } else { + TextAlign::Start + } + }, + TextAlignKeyword::Justify => TextAlign::Start, + }; + + let (line_start, available_space) = match self.current_line.placement_among_floats.get() { + Some(placement_among_floats) => ( + placement_among_floats.start_corner.inline, + placement_among_floats.size.inline, + ), + None => (Au::zero(), self.containing_block.size.inline), + }; + + // Properly handling text-indent requires that we do not align the text + // into the text-indent. + // See <https://drafts.csswg.org/css-text/#text-indent-property> + // "This property specifies the indentation applied to lines of inline content in + // a block. The indent is treated as a margin applied to the start edge of the + // line box." + let text_indent = self.current_line.start_position.inline; + let line_length = self.current_line.inline_position - whitespace_trimmed - text_indent; + let adjusted_line_start = line_start + + match text_align { + TextAlign::Start => text_indent, + TextAlign::End => (available_space - line_length).max(text_indent), + TextAlign::Center => (available_space - line_length + text_indent) + .scale_by(0.5) + .max(text_indent), + }; + + // Calculate the justification adjustment. This is simply the remaining space on the line, + // dividided by the number of justficiation opportunities that we recorded when building + // the line. + let text_justify = self.containing_block.style.clone_text_justify(); + let justification_adjustment = match (text_align_keyword, text_justify) { + // `text-justify: none` should disable text justification. + // TODO: Handle more `text-justify` values. + (TextAlignKeyword::Justify, TextJustify::None) => Au::zero(), + (TextAlignKeyword::Justify, _) => { + match self.current_line.count_justification_opportunities() { + 0 => Au::zero(), + num_justification_opportunities => { + (available_space - text_indent - line_length) + .scale_by(1. / num_justification_opportunities as f32) + }, + } + }, + _ => Au::zero(), + }; + + // If the content overflows the line, then justification adjustment will become negative. In + // that case, do not make any adjustment for justification. + let justification_adjustment = justification_adjustment.max(Au::zero()); + + (adjusted_line_start, justification_adjustment) + } + + fn place_float_fragment(&mut self, fragment: &mut BoxFragment) { + let state = self + .sequential_layout_state + .as_mut() + .expect("Tried to lay out a float with no sequential placement state!"); + + let block_offset_from_containining_block_top = state + .current_block_position_including_margins() - + state.current_containing_block_offset(); + state.place_float_fragment( + fragment, + self.containing_block, + CollapsedMargin::zero(), + block_offset_from_containining_block_top, + ); + } + + /// Place a FloatLineItem. This is done when an unbreakable segment is committed to + /// the current line. Placement of FloatLineItems might need to be deferred until the + /// line is complete in the case that floats stop fitting on the current line. + /// + /// When placing floats we do not want to take into account any trailing whitespace on + /// the line, because that whitespace will be trimmed in the case that the line is + /// broken. Thus this function takes as an argument the new size (without whitespace) of + /// the line that these floats are joining. + fn place_float_line_item_for_commit_to_line( + &mut self, + float_item: &mut FloatLineItem, + line_inline_size_without_trailing_whitespace: Au, + ) { + let mut float_fragment = float_item.fragment.borrow_mut(); + let logical_margin_rect_size = float_fragment + .margin_rect() + .size + .to_logical(self.containing_block.style.writing_mode); + let inline_size = logical_margin_rect_size.inline.max(Au::zero()); + + let available_inline_size = match self.current_line.placement_among_floats.get() { + Some(placement_among_floats) => placement_among_floats.size.inline, + None => self.containing_block.size.inline, + } - line_inline_size_without_trailing_whitespace; + + // If this float doesn't fit on the current line or a previous float didn't fit on + // the current line, we need to place it starting at the next line BUT still as + // children of this line's hierarchy of inline boxes (for the purposes of properly + // parenting in their stacking contexts). Once all the line content is gathered we + // will place them later. + let has_content = self.current_line.has_content || self.current_line_segment.has_content; + let fits_on_line = !has_content || inline_size <= available_inline_size; + let needs_placement_later = + self.current_line.has_floats_waiting_to_be_placed || !fits_on_line; + + if needs_placement_later { + self.current_line.has_floats_waiting_to_be_placed = true; + } else { + self.place_float_fragment(&mut float_fragment); + float_item.needs_placement = false; + } + + // We've added a new float to the IFC, but this may have actually changed the + // position of the current line. In order to determine that we regenerate the + // placement among floats for the current line, which may adjust its inline + // start position. + let new_placement = self.place_line_among_floats(&LogicalVec2 { + inline: line_inline_size_without_trailing_whitespace, + block: self.current_line.max_block_size.resolve(), + }); + self.current_line + .replace_placement_among_floats(new_placement); + } + + /// Given a new potential line size for the current line, create a "placement" for that line. + /// This tells us whether or not the new potential line will fit in the current block position + /// or need to be moved. In addition, the placement rect determines the inline start and end + /// of the line if it's used as the final placement among floats. + fn place_line_among_floats(&self, potential_line_size: &LogicalVec2<Au>) -> LogicalRect<Au> { + let sequential_layout_state = self + .sequential_layout_state + .as_ref() + .expect("Should not have called this function without having floats."); + + let ifc_offset_in_float_container = LogicalVec2 { + inline: sequential_layout_state + .floats + .containing_block_info + .inline_start, + block: sequential_layout_state.current_containing_block_offset(), + }; + + let ceiling = self + .current_line + .line_block_start_considering_placement_among_floats(); + let mut placement = PlacementAmongFloats::new( + &sequential_layout_state.floats, + ceiling + ifc_offset_in_float_container.block, + LogicalVec2 { + inline: potential_line_size.inline, + block: potential_line_size.block, + }, + &PaddingBorderMargin::zero(), + ); + + let mut placement_rect = placement.place(); + placement_rect.start_corner -= ifc_offset_in_float_container; + placement_rect + } + + /// Returns true if a new potential line size for the current line would require a line + /// break. This takes into account floats and will also update the "placement among + /// floats" for this line if the potential line size would not cause a line break. + /// Thus, calling this method has side effects and should only be done while in the + /// process of laying out line content that is always going to be committed to this + /// line or the next. + fn new_potential_line_size_causes_line_break( + &mut self, + potential_line_size: &LogicalVec2<Au>, + ) -> bool { + let available_line_space = if self.sequential_layout_state.is_some() { + self.current_line + .placement_among_floats + .get_or_init(|| self.place_line_among_floats(potential_line_size)) + .size + } else { + LogicalVec2 { + inline: self.containing_block.size.inline, + block: MAX_AU, + } + }; + + let inline_would_overflow = potential_line_size.inline > available_line_space.inline; + let block_would_overflow = potential_line_size.block > available_line_space.block; + + // The first content that is added to a line cannot trigger a line break and + // the `white-space` propertly can also prevent all line breaking. + let can_break = self.current_line.has_content; + + // If this is the first content on the line and we already have a float placement, + // that means that the placement was initialized by a leading float in the IFC. + // This placement needs to be updated, because the first line content might push + // the block start of the line downward. If there is no float placement, we want + // to make one to properly set the block position of the line. + if !can_break { + // Even if we cannot break, adding content to this line might change its position. + // In that case we need to redo our placement among floats. + if self.sequential_layout_state.is_some() && + (inline_would_overflow || block_would_overflow) + { + let new_placement = self.place_line_among_floats(potential_line_size); + self.current_line + .replace_placement_among_floats(new_placement); + } + + return false; + } + + // If the potential line is larger than the containing block we do not even need to consider + // floats. We definitely have to do a linebreak. + if potential_line_size.inline > self.containing_block.size.inline { + return true; + } + + // Not fitting in the block space means that our block size has changed and we had a + // placement among floats that is no longer valid. This same placement might just + // need to be expanded or perhaps we need to line break. + if block_would_overflow { + // If we have a limited block size then we are wedging this line between floats. + assert!(self.sequential_layout_state.is_some()); + let new_placement = self.place_line_among_floats(potential_line_size); + if new_placement.start_corner.block != + self.current_line + .line_block_start_considering_placement_among_floats() + { + return true; + } else { + self.current_line + .replace_placement_among_floats(new_placement); + return false; + } + } + + // Otherwise the new potential line size will require a newline if it fits in the + // inline space available for this line. This space may be smaller than the + // containing block if floats shrink the available inline space. + inline_would_overflow + } + + pub(super) fn defer_forced_line_break(&mut self) { + // If the current portion of the unbreakable segment does not fit on the current line + // we need to put it on a new line *before* actually triggering the hard line break. + if !self.unbreakable_segment_fits_on_line() { + self.process_line_break(false /* forced_line_break */); + } + + // Defer the actual line break until we've cleared all ending inline boxes. + self.linebreak_before_new_content = true; + + // In quirks mode, the line-height isn't automatically added to the line. If we consider a + // forced line break a kind of preserved white space, quirks mode requires that we add the + // line-height of the current element to the line box height. + // + // The exception here is `<br>` elements. They are implemented with `pre-line` in Servo, but + // this is an implementation detail. The "magic" behavior of `<br>` elements is that they + // add line-height to the line conditionally: only when they are on an otherwise empty line. + let line_is_empty = + !self.current_line_segment.has_content && !self.current_line.has_content; + if !self.processing_br_element() || line_is_empty { + let strut_size = self + .current_inline_container_state() + .strut_block_sizes + .clone(); + self.update_unbreakable_segment_for_new_content( + &strut_size, + Au::zero(), + SegmentContentFlags::empty(), + ); + } + + self.had_inflow_content = true; + } + + pub(super) fn possibly_flush_deferred_forced_line_break(&mut self) { + if !self.linebreak_before_new_content { + return; + } + + self.commit_current_segment_to_line(); + self.process_line_break(true /* forced_line_break */); + self.linebreak_before_new_content = false; + } + + fn push_line_item_to_unbreakable_segment(&mut self, line_item: LineItem) { + self.current_line_segment + .push_line_item(line_item, self.inline_box_state_stack.len()); + } + + pub(super) fn push_glyph_store_to_unbreakable_segment( + &mut self, + glyph_store: std::sync::Arc<GlyphStore>, + text_run: &TextRun, + font_index: usize, + bidi_level: Level, + range: range::Range<ByteIndex>, + ) { + let inline_advance = glyph_store.total_advance(); + let flags = if glyph_store.is_whitespace() { + SegmentContentFlags::from(text_run.parent_style.get_inherited_text()) + } else { + SegmentContentFlags::empty() + }; + + // If the metrics of this font don't match the default font, we are likely using a fallback + // font and need to adjust the line size to account for a potentially different font. + // If somehow the metrics match, the line size won't change. + let ifc_font_info = &self.ifc.font_metrics[font_index]; + let font_metrics = ifc_font_info.metrics.clone(); + let using_fallback_font = + self.current_inline_container_state().font_metrics != font_metrics; + + let quirks_mode = self.layout_context.style_context.quirks_mode() != QuirksMode::NoQuirks; + let strut_size = if using_fallback_font { + // TODO(mrobinson): This value should probably be cached somewhere. + let container_state = self.current_inline_container_state(); + let vertical_align = effective_vertical_align( + &container_state.style, + self.inline_box_state_stack.last().map(|c| &c.base), + ); + let mut block_size = container_state.get_block_size_contribution( + vertical_align, + &font_metrics, + &container_state.font_metrics, + ); + block_size.adjust_for_baseline_offset(container_state.baseline_offset); + block_size + } else if quirks_mode && !flags.is_collapsible_whitespace() { + // Normally, the strut is incorporated into the nested block size. In quirks mode though + // if we find any text that isn't collapsed whitespace, we need to incorporate the strut. + // TODO(mrobinson): This isn't quite right for situations where collapsible white space + // ultimately does not collapse because it is between two other pieces of content. + self.current_inline_container_state() + .strut_block_sizes + .clone() + } else { + LineBlockSizes::zero() + }; + self.update_unbreakable_segment_for_new_content(&strut_size, inline_advance, flags); + + let current_inline_box_identifier = self.current_inline_box_identifier(); + match self.current_line_segment.line_items.last_mut() { + Some(LineItem::TextRun(inline_box_identifier, line_item)) + if *inline_box_identifier == current_inline_box_identifier && + line_item.can_merge(ifc_font_info.key, bidi_level) => + { + line_item.text.push(glyph_store); + return; + }, + _ => {}, + } + + let selection_range = if let Some(selection) = &text_run.selection_range { + let intersection = selection.intersect(&range); + if intersection.is_empty() { + let insertion_point_index = selection.begin(); + // We only allow the caret to be shown in the start of the fragment if it is the first fragment. + // Otherwise this will cause duplicate caret, especially apparent when encountered line break. + if insertion_point_index >= range.begin() && + insertion_point_index <= range.end() && + (range.begin() != insertion_point_index || range.begin().0 == 0) + { + Some(Range::new( + insertion_point_index - range.begin(), + ByteIndex(0), + )) + } else { + None + } + } else { + Some(Range::new( + intersection.begin() - range.begin(), + intersection.length(), + )) + } + } else { + None + }; + + self.push_line_item_to_unbreakable_segment(LineItem::TextRun( + current_inline_box_identifier, + TextRunLineItem { + text: vec![glyph_store], + base_fragment_info: text_run.base_fragment_info, + parent_style: text_run.parent_style.clone(), + font_metrics, + font_key: ifc_font_info.key, + text_decoration_line: self.current_inline_container_state().text_decoration_line, + bidi_level, + selection_range, + selected_style: text_run.selected_style.clone(), + }, + )); + } + + fn update_unbreakable_segment_for_new_content( + &mut self, + block_sizes_of_content: &LineBlockSizes, + inline_size: Au, + flags: SegmentContentFlags, + ) { + if flags.is_collapsible_whitespace() || flags.is_wrappable_and_hangable() { + self.current_line_segment.trailing_whitespace_size = inline_size; + } else { + self.current_line_segment.trailing_whitespace_size = Au::zero(); + } + if !flags.is_collapsible_whitespace() { + self.current_line_segment.has_content = true; + self.had_inflow_content = true; + } + + // This may or may not include the size of the strut depending on the quirks mode setting. + let container_max_block_size = &self + .current_inline_container_state() + .nested_strut_block_sizes + .clone(); + self.current_line_segment + .max_block_size + .max_assign(container_max_block_size); + self.current_line_segment + .max_block_size + .max_assign(block_sizes_of_content); + + self.current_line_segment.inline_size += inline_size; + + // Propagate the whitespace setting to the current nesting level. + *self + .current_inline_container_state() + .has_content + .borrow_mut() = true; + self.propagate_current_nesting_level_white_space_style(); + } + + fn process_line_break(&mut self, forced_line_break: bool) { + self.current_line_segment.trim_leading_whitespace(); + self.finish_current_line_and_reset(forced_line_break); + } + + pub(super) fn unbreakable_segment_fits_on_line(&mut self) -> bool { + let potential_line_size = LogicalVec2 { + inline: self.current_line.inline_position + self.current_line_segment.inline_size - + self.current_line_segment.trailing_whitespace_size, + block: self + .current_line_max_block_size_including_nested_containers() + .max(&self.current_line_segment.max_block_size) + .resolve(), + }; + + !self.new_potential_line_size_causes_line_break(&potential_line_size) + } + + /// Process a soft wrap opportunity. This will either commit the current unbreakble + /// segment to the current line, if it fits within the containing block and float + /// placement boundaries, or do a line break and then commit the segment. + pub(super) fn process_soft_wrap_opportunity(&mut self) { + if self.current_line_segment.line_items.is_empty() { + return; + } + if self.text_wrap_mode == TextWrapMode::Nowrap { + return; + } + + let potential_line_size = LogicalVec2 { + inline: self.current_line.inline_position + self.current_line_segment.inline_size - + self.current_line_segment.trailing_whitespace_size, + block: self + .current_line_max_block_size_including_nested_containers() + .max(&self.current_line_segment.max_block_size) + .resolve(), + }; + + if self.new_potential_line_size_causes_line_break(&potential_line_size) { + self.process_line_break(false /* forced_line_break */); + } + self.commit_current_segment_to_line(); + } + + /// Commit the current unbrekable segment to the current line. In addition, this will + /// place all floats in the unbreakable segment and expand the line dimensions. + fn commit_current_segment_to_line(&mut self) { + // The line segments might have no items and have content after processing a forced + // linebreak on an empty line. + if self.current_line_segment.line_items.is_empty() && !self.current_line_segment.has_content + { + return; + } + + if !self.current_line.has_content { + self.current_line_segment.trim_leading_whitespace(); + } + + self.current_line.inline_position += self.current_line_segment.inline_size; + self.current_line.max_block_size = self + .current_line_max_block_size_including_nested_containers() + .max(&self.current_line_segment.max_block_size); + let line_inline_size_without_trailing_whitespace = + self.current_line.inline_position - self.current_line_segment.trailing_whitespace_size; + + // Place all floats in this unbreakable segment. + let mut segment_items = mem::take(&mut self.current_line_segment.line_items); + for item in segment_items.iter_mut() { + if let LineItem::Float(_, float_item) = item { + self.place_float_line_item_for_commit_to_line( + float_item, + line_inline_size_without_trailing_whitespace, + ); + } + } + + // If the current line was never placed among floats, we need to do that now based on the + // new size. Calling `new_potential_line_size_causes_line_break()` here triggers the + // new line to be positioned among floats. This should never ask for a line + // break because it is the first content on the line. + if self.current_line.line_items.is_empty() { + let will_break = self.new_potential_line_size_causes_line_break(&LogicalVec2 { + inline: line_inline_size_without_trailing_whitespace, + block: self.current_line_segment.max_block_size.resolve(), + }); + assert!(!will_break); + } + + self.current_line.line_items.extend(segment_items); + self.current_line.has_content |= self.current_line_segment.has_content; + + self.current_line_segment.reset(); + } +} + +bitflags! { + pub struct SegmentContentFlags: u8 { + const COLLAPSIBLE_WHITESPACE = 0b00000001; + const WRAPPABLE_AND_HANGABLE_WHITESPACE = 0b00000010; + } +} + +impl SegmentContentFlags { + fn is_collapsible_whitespace(&self) -> bool { + self.contains(Self::COLLAPSIBLE_WHITESPACE) + } + + fn is_wrappable_and_hangable(&self) -> bool { + self.contains(Self::WRAPPABLE_AND_HANGABLE_WHITESPACE) + } +} + +impl From<&InheritedText> for SegmentContentFlags { + fn from(style_text: &InheritedText) -> Self { + let mut flags = Self::empty(); + + // White-space with `white-space-collapse: break-spaces` or `white-space-collapse: preserve` + // never collapses. + if !matches!( + style_text.white_space_collapse, + WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces + ) { + flags.insert(Self::COLLAPSIBLE_WHITESPACE); + } + + // White-space with `white-space-collapse: break-spaces` never hangs and always takes up + // space. + if style_text.text_wrap_mode == TextWrapMode::Wrap && + style_text.white_space_collapse != WhiteSpaceCollapse::BreakSpaces + { + flags.insert(Self::WRAPPABLE_AND_HANGABLE_WHITESPACE); + } + flags + } +} + +impl InlineFormattingContext { + #[cfg_attr( + feature = "tracing", + tracing::instrument( + name = "InlineFormattingContext::new_with_builder", + skip_all, + fields(servo_profiling = true), + level = "trace", + ) + )] + pub(super) fn new_with_builder( + builder: InlineFormattingContextBuilder, + layout_context: &LayoutContext, + propagated_data: PropagatedBoxTreeData, + has_first_formatted_line: bool, + is_single_line_text_input: bool, + starting_bidi_level: Level, + ) -> Self { + // This is to prevent a double borrow. + let text_content: String = builder.text_segments.into_iter().collect(); + let mut font_metrics = Vec::new(); + + let bidi_info = BidiInfo::new(&text_content, Some(starting_bidi_level)); + let has_right_to_left_content = bidi_info.has_rtl(); + + let mut new_linebreaker = LineBreaker::new(text_content.as_str()); + for item in builder.inline_items.iter() { + match &mut *item.borrow_mut() { + InlineItem::TextRun(text_run) => { + text_run.borrow_mut().segment_and_shape( + &text_content, + &layout_context.font_context, + &mut new_linebreaker, + &mut font_metrics, + &bidi_info, + ); + }, + InlineItem::StartInlineBox(inline_box) => { + let inline_box = &mut *inline_box.borrow_mut(); + if let Some(font) = get_font_for_first_font_for_style( + &inline_box.base.style, + &layout_context.font_context, + ) { + inline_box.default_font_index = Some(add_or_get_font( + &font, + &mut font_metrics, + &layout_context.font_context, + )); + } + }, + InlineItem::Atomic(_, index_in_text, bidi_level) => { + *bidi_level = bidi_info.levels[*index_in_text]; + }, + InlineItem::OutOfFlowAbsolutelyPositionedBox(..) | + InlineItem::OutOfFlowFloatBox(_) | + InlineItem::EndInlineBox => {}, + } + } + + InlineFormattingContext { + text_content, + inline_items: builder.inline_items, + inline_boxes: builder.inline_boxes, + font_metrics, + text_decoration_line: propagated_data.text_decoration, + has_first_formatted_line, + contains_floats: builder.contains_floats, + is_single_line_text_input, + has_right_to_left_content, + } + } + + pub(super) fn layout( + &self, + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + containing_block: &ContainingBlock, + sequential_layout_state: Option<&mut SequentialLayoutState>, + collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin, + ) -> CacheableLayoutResult { + let first_line_inline_start = if self.has_first_formatted_line { + containing_block + .style + .get_inherited_text() + .text_indent + .length + .to_used_value(containing_block.size.inline) + } else { + Au::zero() + }; + + // Clear any cached inline fragments from previous layouts. + for inline_box in self.inline_boxes.iter() { + inline_box.borrow().base.clear_fragments(); + } + + let style = containing_block.style; + + // It's unfortunate that it isn't possible to get this during IFC text processing, but in + // that situation the style of the containing block is unknown. + let default_font_metrics = + get_font_for_first_font_for_style(style, &layout_context.font_context) + .map(|font| font.metrics.clone()); + + let style_text = containing_block.style.get_inherited_text(); + let mut inline_container_state_flags = InlineContainerStateFlags::empty(); + if inline_container_needs_strut(style, layout_context, None) { + inline_container_state_flags.insert(InlineContainerStateFlags::CREATE_STRUT); + } + if self.is_single_line_text_input { + inline_container_state_flags + .insert(InlineContainerStateFlags::IS_SINGLE_LINE_TEXT_INPUT); + } + + let mut layout = InlineFormattingContextLayout { + positioning_context, + containing_block, + sequential_layout_state, + layout_context, + ifc: self, + fragments: Vec::new(), + current_line: LineUnderConstruction::new(LogicalVec2 { + inline: first_line_inline_start, + block: Au::zero(), + }), + root_nesting_level: InlineContainerState::new( + style.to_arc(), + inline_container_state_flags, + None, /* parent_container */ + self.text_decoration_line, + default_font_metrics.as_ref(), + ), + inline_box_state_stack: Vec::new(), + inline_box_states: Vec::with_capacity(self.inline_boxes.len()), + current_line_segment: UnbreakableSegmentUnderConstruction::new(), + linebreak_before_new_content: false, + deferred_br_clear: Clear::None, + have_deferred_soft_wrap_opportunity: false, + had_inflow_content: false, + depends_on_block_constraints: false, + white_space_collapse: style_text.white_space_collapse, + text_wrap_mode: style_text.text_wrap_mode, + baselines: Baselines::default(), + }; + + // FIXME(pcwalton): This assumes that margins never collapse through inline formatting + // contexts (i.e. that inline formatting contexts are never empty). Is that right? + // FIXME(mrobinson): This should not happen if the IFC collapses through. + if let Some(ref mut sequential_layout_state) = layout.sequential_layout_state { + sequential_layout_state.collapse_margins(); + // FIXME(mrobinson): Collapse margins in the containing block offsets as well?? + } + + for item in self.inline_items.iter() { + let item = &*item.borrow(); + + // Any new box should flush a pending hard line break. + if !matches!(item, InlineItem::EndInlineBox) { + layout.possibly_flush_deferred_forced_line_break(); + } + + match item { + InlineItem::StartInlineBox(inline_box) => { + layout.start_inline_box(&inline_box.borrow()); + }, + InlineItem::EndInlineBox => layout.finish_inline_box(), + InlineItem::TextRun(run) => run.borrow().layout_into_line_items(&mut layout), + InlineItem::Atomic(atomic_formatting_context, offset_in_text, bidi_level) => { + atomic_formatting_context.layout_into_line_items( + &mut layout, + *offset_in_text, + *bidi_level, + ); + }, + InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, _) => { + layout.push_line_item_to_unbreakable_segment(LineItem::AbsolutelyPositioned( + layout.current_inline_box_identifier(), + AbsolutelyPositionedLineItem { + absolutely_positioned_box: positioned_box.clone(), + }, + )); + }, + InlineItem::OutOfFlowFloatBox(float_box) => { + float_box.layout_into_line_items(&mut layout); + }, + } + } + + layout.finish_last_line(); + + let mut collapsible_margins_in_children = CollapsedBlockMargins::zero(); + let content_block_size = layout.current_line.start_position.block; + collapsible_margins_in_children.collapsed_through = !layout.had_inflow_content && + content_block_size.is_zero() && + collapsible_with_parent_start_margin.0; + + CacheableLayoutResult { + fragments: layout.fragments, + content_block_size, + collapsible_margins_in_children, + baselines: layout.baselines, + depends_on_block_constraints: layout.depends_on_block_constraints, + content_inline_size_for_table: None, + specific_layout_info: None, + } + } + + fn next_character_prevents_soft_wrap_opportunity(&self, index: usize) -> bool { + let Some(character) = self.text_content[index..].chars().nth(1) else { + return false; + }; + char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character) + } + + fn previous_character_prevents_soft_wrap_opportunity(&self, index: usize) -> bool { + let Some(character) = self.text_content[0..index].chars().next_back() else { + return false; + }; + char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character) + } +} + +impl InlineContainerState { + fn new( + style: Arc<ComputedValues>, + flags: InlineContainerStateFlags, + parent_container: Option<&InlineContainerState>, + parent_text_decoration_line: TextDecorationLine, + font_metrics: Option<&FontMetrics>, + ) -> Self { + let text_decoration_line = parent_text_decoration_line | style.clone_text_decoration_line(); + let font_metrics = font_metrics.cloned().unwrap_or_else(FontMetrics::empty); + let line_height = line_height( + &style, + &font_metrics, + flags.contains(InlineContainerStateFlags::IS_SINGLE_LINE_TEXT_INPUT), + ); + + let mut baseline_offset = Au::zero(); + let mut strut_block_sizes = Self::get_block_sizes_with_style( + effective_vertical_align(&style, parent_container), + &style, + &font_metrics, + &font_metrics, + line_height, + ); + if let Some(parent_container) = parent_container { + // The baseline offset from `vertical-align` might adjust where our block size contribution is + // within the line. + baseline_offset = parent_container.get_cumulative_baseline_offset_for_child( + style.clone_vertical_align(), + &strut_block_sizes, + ); + strut_block_sizes.adjust_for_baseline_offset(baseline_offset); + } + + let mut nested_block_sizes = parent_container + .map(|container| container.nested_strut_block_sizes.clone()) + .unwrap_or_else(LineBlockSizes::zero); + if flags.contains(InlineContainerStateFlags::CREATE_STRUT) { + nested_block_sizes.max_assign(&strut_block_sizes); + } + + Self { + style, + flags, + has_content: RefCell::new(false), + text_decoration_line, + nested_strut_block_sizes: nested_block_sizes, + strut_block_sizes, + baseline_offset, + font_metrics, + } + } + + fn get_block_sizes_with_style( + vertical_align: VerticalAlign, + style: &ComputedValues, + font_metrics: &FontMetrics, + font_metrics_of_first_font: &FontMetrics, + line_height: Au, + ) -> LineBlockSizes { + if !is_baseline_relative(vertical_align) { + return LineBlockSizes { + line_height, + baseline_relative_size_for_line_height: None, + size_for_baseline_positioning: BaselineRelativeSize::zero(), + }; + } + + // From https://drafts.csswg.org/css-inline/#inline-height + // > If line-height computes to `normal` and either `text-box-edge` is `leading` or this + // > is the root inline box, the font’s line gap metric may also be incorporated + // > into A and D by adding half to each side as half-leading. + // + // `text-box-edge` isn't implemented (and this is a draft specification), so it's + // always effectively `leading`, which means we always take into account the line gap + // when `line-height` is normal. + let mut ascent = font_metrics.ascent; + let mut descent = font_metrics.descent; + if style.get_font().line_height == LineHeight::Normal { + let half_leading_from_line_gap = + (font_metrics.line_gap - descent - ascent).scale_by(0.5); + ascent += half_leading_from_line_gap; + descent += half_leading_from_line_gap; + } + + // The ascent and descent we use for computing the line's final line height isn't + // the same the ascent and descent we use for finding the baseline. For finding + // the baseline we want the content rect. + let size_for_baseline_positioning = BaselineRelativeSize { ascent, descent }; + + // From https://drafts.csswg.org/css-inline/#inline-height + // > When its computed line-height is not normal, its layout bounds are derived solely + // > from metrics of its first available font (ignoring glyphs from other fonts), and + // > leading is used to adjust the effective A and D to add up to the used line-height. + // > Calculate the leading L as L = line-height - (A + D). Half the leading (its + // > half-leading) is added above A of the first available font, and the other half + // > below D of the first available font, giving an effective ascent above the baseline + // > of A′ = A + L/2, and an effective descent of D′ = D + L/2. + // + // Note that leading might be negative here and the line-height might be zero. In + // the case where the height is zero, ascent and descent will move to the same + // point in the block axis. Even though the contribution to the line height is + // zero in this case, the line may get some height when taking them into + // considering with other zero line height boxes that converge on other block axis + // locations when using the above formula. + if style.get_font().line_height != LineHeight::Normal { + ascent = font_metrics_of_first_font.ascent; + descent = font_metrics_of_first_font.descent; + let half_leading = (line_height - (ascent + descent)).scale_by(0.5); + // We want the sum of `ascent` and `descent` to equal `line_height`. + // If we just add `half_leading` to both, then we may not get `line_height` + // due to precision limitations of `Au`. Instead, we set `descent` to + // the value that will guarantee the correct sum. + ascent += half_leading; + descent = line_height - ascent; + } + + LineBlockSizes { + line_height, + baseline_relative_size_for_line_height: Some(BaselineRelativeSize { ascent, descent }), + size_for_baseline_positioning, + } + } + + fn get_block_size_contribution( + &self, + vertical_align: VerticalAlign, + font_metrics: &FontMetrics, + font_metrics_of_first_font: &FontMetrics, + ) -> LineBlockSizes { + Self::get_block_sizes_with_style( + vertical_align, + &self.style, + font_metrics, + font_metrics_of_first_font, + line_height( + &self.style, + font_metrics, + self.flags + .contains(InlineContainerStateFlags::IS_SINGLE_LINE_TEXT_INPUT), + ), + ) + } + + fn get_cumulative_baseline_offset_for_child( + &self, + child_vertical_align: VerticalAlign, + child_block_size: &LineBlockSizes, + ) -> Au { + let block_size = self.get_block_size_contribution( + child_vertical_align.clone(), + &self.font_metrics, + &self.font_metrics, + ); + self.baseline_offset + + match child_vertical_align { + // `top` and `bottom are not actually relative to the baseline, but this value is unused + // in those cases. + // TODO: We should distinguish these from `baseline` in order to implement "aligned subtrees" properly. + // See https://drafts.csswg.org/css2/#aligned-subtree. + VerticalAlign::Keyword(VerticalAlignKeyword::Baseline) | + VerticalAlign::Keyword(VerticalAlignKeyword::Top) | + VerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => Au::zero(), + VerticalAlign::Keyword(VerticalAlignKeyword::Sub) => { + block_size.resolve().scale_by(FONT_SUBSCRIPT_OFFSET_RATIO) + }, + VerticalAlign::Keyword(VerticalAlignKeyword::Super) => { + -block_size.resolve().scale_by(FONT_SUPERSCRIPT_OFFSET_RATIO) + }, + VerticalAlign::Keyword(VerticalAlignKeyword::TextTop) => { + child_block_size.size_for_baseline_positioning.ascent - self.font_metrics.ascent + }, + VerticalAlign::Keyword(VerticalAlignKeyword::Middle) => { + // "Align the vertical midpoint of the box with the baseline of the parent + // box plus half the x-height of the parent." + (child_block_size.size_for_baseline_positioning.ascent - + child_block_size.size_for_baseline_positioning.descent - + self.font_metrics.x_height) + .scale_by(0.5) + }, + VerticalAlign::Keyword(VerticalAlignKeyword::TextBottom) => { + self.font_metrics.descent - + child_block_size.size_for_baseline_positioning.descent + }, + VerticalAlign::Length(length_percentage) => { + -length_percentage.to_used_value(child_block_size.line_height) + }, + } + } +} + +impl IndependentFormattingContext { + fn layout_into_line_items( + &self, + layout: &mut InlineFormattingContextLayout, + offset_in_text: usize, + bidi_level: Level, + ) { + // We need to know the inline size of the atomic before deciding whether to do the line break. + let mut child_positioning_context = PositioningContext::new_for_style(self.style()) + .unwrap_or_else(|| PositioningContext::new_for_subtree(true)); + let IndependentFloatOrAtomicLayoutResult { + mut fragment, + baselines, + pbm_sums, + } = self.layout_float_or_atomic_inline( + layout.layout_context, + &mut child_positioning_context, + layout.containing_block, + ); + + // If this Fragment's layout depends on the block size of the containing block, + // then the entire layout of the inline formatting context does as well. + layout.depends_on_block_constraints |= fragment.base.flags.contains( + FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM, + ); + + // Offset the content rectangle by the physical offset of the padding, border, and margin. + let container_writing_mode = layout.containing_block.style.writing_mode; + let pbm_physical_offset = pbm_sums + .start_offset() + .to_physical_size(container_writing_mode); + fragment.content_rect = fragment + .content_rect + .translate(pbm_physical_offset.to_vector()); + + // Apply baselines if necessary. + let mut fragment = match baselines { + Some(baselines) => fragment.with_baselines(baselines), + None => fragment, + }; + + // Lay out absolutely positioned children if this new atomic establishes a containing block + // for absolutes. + let positioning_context = if self.is_replaced() { + None + } else { + if fragment + .style + .establishes_containing_block_for_absolute_descendants(fragment.base.flags) + { + child_positioning_context + .layout_collected_children(layout.layout_context, &mut fragment); + } + Some(child_positioning_context) + }; + + if layout.text_wrap_mode == TextWrapMode::Wrap && + !layout + .ifc + .previous_character_prevents_soft_wrap_opportunity(offset_in_text) + { + layout.process_soft_wrap_opportunity(); + } + + let size = pbm_sums.sum() + + fragment + .content_rect + .size + .to_logical(container_writing_mode); + let baseline_offset = self + .pick_baseline(&fragment.baselines(container_writing_mode)) + .map(|baseline| pbm_sums.block_start + baseline) + .unwrap_or(size.block); + + let (block_sizes, baseline_offset_in_parent) = + self.get_block_sizes_and_baseline_offset(layout, size.block, baseline_offset); + layout.update_unbreakable_segment_for_new_content( + &block_sizes, + size.inline, + SegmentContentFlags::empty(), + ); + + let fragment = ArcRefCell::new(fragment); + self.base.set_fragment(Fragment::Box(fragment.clone())); + + layout.push_line_item_to_unbreakable_segment(LineItem::Atomic( + layout.current_inline_box_identifier(), + AtomicLineItem { + fragment, + size, + positioning_context, + baseline_offset_in_parent, + baseline_offset_in_item: baseline_offset, + bidi_level, + }, + )); + + // If there's a soft wrap opportunity following this atomic, defer a soft wrap opportunity + // for when we next process text content. + if !layout + .ifc + .next_character_prevents_soft_wrap_opportunity(offset_in_text) + { + layout.have_deferred_soft_wrap_opportunity = true; + } + } + + /// Picks either the first or the last baseline, depending on `baseline-source`. + /// TODO: clarify that this is not to be used for box alignment in flex/grid + /// <https://drafts.csswg.org/css-inline/#baseline-source> + fn pick_baseline(&self, baselines: &Baselines) -> Option<Au> { + match self.style().clone_baseline_source() { + BaselineSource::First => baselines.first, + BaselineSource::Last => baselines.last, + BaselineSource::Auto => match &self.contents { + IndependentFormattingContextContents::NonReplaced( + IndependentNonReplacedContents::Flow(_), + ) => baselines.last, + _ => baselines.first, + }, + } + } + + fn get_block_sizes_and_baseline_offset( + &self, + ifc: &InlineFormattingContextLayout, + block_size: Au, + baseline_offset_in_content_area: Au, + ) -> (LineBlockSizes, Au) { + let mut contribution = if !is_baseline_relative(self.style().clone_vertical_align()) { + LineBlockSizes { + line_height: block_size, + baseline_relative_size_for_line_height: None, + size_for_baseline_positioning: BaselineRelativeSize::zero(), + } + } else { + let baseline_relative_size = BaselineRelativeSize { + ascent: baseline_offset_in_content_area, + descent: block_size - baseline_offset_in_content_area, + }; + LineBlockSizes { + line_height: block_size, + baseline_relative_size_for_line_height: Some(baseline_relative_size.clone()), + size_for_baseline_positioning: baseline_relative_size, + } + }; + + let baseline_offset = ifc + .current_inline_container_state() + .get_cumulative_baseline_offset_for_child( + self.style().clone_vertical_align(), + &contribution, + ); + contribution.adjust_for_baseline_offset(baseline_offset); + + (contribution, baseline_offset) + } +} + +impl FloatBox { + fn layout_into_line_items(&self, layout: &mut InlineFormattingContextLayout) { + let fragment = ArcRefCell::new(self.layout( + layout.layout_context, + layout.positioning_context, + layout.containing_block, + )); + + self.contents + .base + .set_fragment(Fragment::Box(fragment.clone())); + layout.push_line_item_to_unbreakable_segment(LineItem::Float( + layout.current_inline_box_identifier(), + FloatLineItem { + fragment, + needs_placement: true, + }, + )); + } +} + +fn place_pending_floats(ifc: &mut InlineFormattingContextLayout, line_items: &mut [LineItem]) { + for item in line_items.iter_mut() { + if let LineItem::Float(_, float_line_item) = item { + if float_line_item.needs_placement { + ifc.place_float_fragment(&mut float_line_item.fragment.borrow_mut()); + } + } + } +} + +fn line_height( + parent_style: &ComputedValues, + font_metrics: &FontMetrics, + is_single_line_text_input: bool, +) -> Au { + let font = parent_style.get_font(); + let font_size = font.font_size.computed_size(); + let mut line_height = match font.line_height { + LineHeight::Normal => font_metrics.line_gap, + LineHeight::Number(number) => (font_size * number.0).into(), + LineHeight::Length(length) => length.0.into(), + }; + + // Single line text inputs line height is clamped to the size of `normal`. See + // <https://github.com/whatwg/html/pull/5462>. + if is_single_line_text_input { + line_height.max_assign(font_metrics.line_gap); + } + + line_height +} + +fn effective_vertical_align( + style: &ComputedValues, + container: Option<&InlineContainerState>, +) -> VerticalAlign { + if container.is_none() { + // If we are at the root of the inline formatting context, we shouldn't use the + // computed `vertical-align`, since it has no effect on the contents of this IFC + // (it can just affect how the block container is aligned within the parent IFC). + VerticalAlign::Keyword(VerticalAlignKeyword::Baseline) + } else { + style.clone_vertical_align() + } +} + +fn is_baseline_relative(vertical_align: VerticalAlign) -> bool { + !matches!( + vertical_align, + VerticalAlign::Keyword(VerticalAlignKeyword::Top) | + VerticalAlign::Keyword(VerticalAlignKeyword::Bottom) + ) +} + +/// Whether or not a strut should be created for an inline container. Normally +/// all inline containers get struts. In quirks mode this isn't always the case +/// though. +/// +/// From <https://quirks.spec.whatwg.org/#the-line-height-calculation-quirk> +/// +/// > ### § 3.3. The line height calculation quirk +/// > In quirks mode and limited-quirks mode, an inline box that matches the following +/// > conditions, must, for the purpose of line height calculation, act as if the box had a +/// > line-height of zero. +/// > +/// > - The border-top-width, border-bottom-width, padding-top and padding-bottom +/// > properties have a used value of zero and the box has a vertical writing mode, or the +/// > border-right-width, border-left-width, padding-right and padding-left properties have +/// > a used value of zero and the box has a horizontal writing mode. +/// > - It either contains no text or it contains only collapsed whitespace. +/// > +/// > ### § 3.4. The blocks ignore line-height quirk +/// > In quirks mode and limited-quirks mode, for a block container element whose content is +/// > composed of inline-level elements, the element’s line-height must be ignored for the +/// > purpose of calculating the minimal height of line boxes within the element. +/// +/// Since we incorporate the size of the strut into the line-height calculation when +/// adding text, we can simply not incorporate the strut at the start of inline box +/// processing. This also works the same for the root of the IFC. +fn inline_container_needs_strut( + style: &ComputedValues, + layout_context: &LayoutContext, + pbm: Option<&PaddingBorderMargin>, +) -> bool { + if layout_context.style_context.quirks_mode() == QuirksMode::NoQuirks { + return true; + } + + // This is not in a standard yet, but all browsers disable this quirk for list items. + // See https://github.com/whatwg/quirks/issues/38. + if style.get_box().display.is_list_item() { + return true; + } + + pbm.map(|pbm| !pbm.padding_border_sums.inline.is_zero()) + .unwrap_or(false) +} + +impl ComputeInlineContentSizes for InlineFormattingContext { + // This works on an already-constructed `InlineFormattingContext`, + // Which would have to change if/when + // `BlockContainer::construct` parallelize their construction. + fn compute_inline_content_sizes( + &self, + layout_context: &LayoutContext, + constraint_space: &ConstraintSpace, + ) -> InlineContentSizesResult { + ContentSizesComputation::compute(self, layout_context, constraint_space) + } +} + +/// A struct which takes care of computing [`ContentSizes`] for an [`InlineFormattingContext`]. +struct ContentSizesComputation<'layout_data> { + layout_context: &'layout_data LayoutContext<'layout_data>, + constraint_space: &'layout_data ConstraintSpace, + paragraph: ContentSizes, + current_line: ContentSizes, + /// Size for whitespace pending to be added to this line. + pending_whitespace: ContentSizes, + /// Whether or not the current line has seen any content (excluding collapsed whitespace), + /// when sizing under a min-content constraint. + had_content_yet_for_min_content: bool, + /// Whether or not the current line has seen any content (excluding collapsed whitespace), + /// when sizing under a max-content constraint. + had_content_yet_for_max_content: bool, + /// Stack of ending padding, margin, and border to add to the length + /// when an inline box finishes. + ending_inline_pbm_stack: Vec<Au>, + depends_on_block_constraints: bool, +} + +impl<'layout_data> ContentSizesComputation<'layout_data> { + fn traverse( + mut self, + inline_formatting_context: &InlineFormattingContext, + ) -> InlineContentSizesResult { + for inline_item in inline_formatting_context.inline_items.iter() { + self.process_item(&inline_item.borrow(), inline_formatting_context); + } + self.forced_line_break(); + + InlineContentSizesResult { + sizes: self.paragraph, + depends_on_block_constraints: self.depends_on_block_constraints, + } + } + + fn process_item( + &mut self, + inline_item: &InlineItem, + inline_formatting_context: &InlineFormattingContext, + ) { + match inline_item { + InlineItem::StartInlineBox(inline_box) => { + // For margins and paddings, a cyclic percentage is resolved against zero + // for determining intrinsic size contributions. + // https://drafts.csswg.org/css-sizing-3/#min-percentage-contribution + let inline_box = inline_box.borrow(); + let zero = Au::zero(); + let writing_mode = self.constraint_space.writing_mode; + let layout_style = inline_box.layout_style(); + let padding = layout_style + .padding(writing_mode) + .percentages_relative_to(zero); + let border = layout_style.border_width(writing_mode); + let margin = inline_box + .base + .style + .margin(writing_mode) + .percentages_relative_to(zero) + .auto_is(Au::zero); + + let pbm = margin + padding + border; + if inline_box.is_first_fragment { + self.add_inline_size(pbm.inline_start); + } + if inline_box.is_last_fragment { + self.ending_inline_pbm_stack.push(pbm.inline_end); + } else { + self.ending_inline_pbm_stack.push(Au::zero()); + } + }, + InlineItem::EndInlineBox => { + let length = self.ending_inline_pbm_stack.pop().unwrap_or_else(Au::zero); + self.add_inline_size(length); + }, + InlineItem::TextRun(text_run) => { + let text_run = &*text_run.borrow(); + for segment in text_run.shaped_text.iter() { + let style_text = text_run.parent_style.get_inherited_text(); + let can_wrap = style_text.text_wrap_mode == TextWrapMode::Wrap; + + // TODO: This should take account whether or not the first and last character prevent + // linebreaks after atomics as in layout. + if can_wrap && segment.break_at_start { + self.line_break_opportunity() + } + + for run in segment.runs.iter() { + let advance = run.glyph_store.total_advance(); + if run.glyph_store.is_whitespace() { + // If this run is a forced line break, we *must* break the line + // and start measuring from the inline origin once more. + if run.is_single_preserved_newline() { + self.forced_line_break(); + continue; + } + if !matches!( + style_text.white_space_collapse, + WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces + ) { + if can_wrap { + self.line_break_opportunity(); + } else if self.had_content_yet_for_min_content { + self.pending_whitespace.min_content += advance; + } + if self.had_content_yet_for_max_content { + self.pending_whitespace.max_content += advance; + } + continue; + } + if can_wrap { + self.pending_whitespace.max_content += advance; + self.commit_pending_whitespace(); + self.line_break_opportunity(); + continue; + } + } + + self.commit_pending_whitespace(); + self.add_inline_size(advance); + + // Typically whitespace glyphs are placed in a separate store, + // but for `white-space: break-spaces` we place the first whitespace + // with the preceding text. That prevents a line break before that + // first space, but we still need to allow a line break after it. + if can_wrap && run.glyph_store.ends_with_whitespace() { + self.line_break_opportunity(); + } + } + } + }, + InlineItem::Atomic(atomic, offset_in_text, _level) => { + // TODO: need to handle TextWrapMode::Nowrap. + if !inline_formatting_context + .previous_character_prevents_soft_wrap_opportunity(*offset_in_text) + { + self.line_break_opportunity(); + } + + let InlineContentSizesResult { + sizes: outer, + depends_on_block_constraints, + } = atomic.outer_inline_content_sizes( + self.layout_context, + &self.constraint_space.into(), + &LogicalVec2::zero(), + false, /* auto_block_size_stretches_to_containing_block */ + ); + self.depends_on_block_constraints |= depends_on_block_constraints; + + if !inline_formatting_context + .next_character_prevents_soft_wrap_opportunity(*offset_in_text) + { + self.line_break_opportunity(); + } + + self.commit_pending_whitespace(); + self.current_line += outer; + }, + _ => {}, + } + } + + fn add_inline_size(&mut self, l: Au) { + self.current_line.min_content += l; + self.current_line.max_content += l; + } + + fn line_break_opportunity(&mut self) { + // Clear the pending whitespace, assuming that at the end of the line + // it needs to either hang or be removed. If that isn't the case, + // `commit_pending_whitespace()` should be called first. + self.pending_whitespace.min_content = Au::zero(); + let current_min_content = mem::take(&mut self.current_line.min_content); + self.paragraph.min_content.max_assign(current_min_content); + self.had_content_yet_for_min_content = false; + } + + fn forced_line_break(&mut self) { + // Handle the line break for min-content sizes. + self.line_break_opportunity(); + + // Repeat the same logic, but now for max-content sizes. + self.pending_whitespace.max_content = Au::zero(); + let current_max_content = mem::take(&mut self.current_line.max_content); + self.paragraph.max_content.max_assign(current_max_content); + self.had_content_yet_for_max_content = false; + } + + fn commit_pending_whitespace(&mut self) { + self.current_line += mem::take(&mut self.pending_whitespace); + self.had_content_yet_for_min_content = true; + self.had_content_yet_for_max_content = true; + } + + /// Compute the [`ContentSizes`] of the given [`InlineFormattingContext`]. + fn compute( + inline_formatting_context: &InlineFormattingContext, + layout_context: &'layout_data LayoutContext, + constraint_space: &'layout_data ConstraintSpace, + ) -> InlineContentSizesResult { + Self { + layout_context, + constraint_space, + paragraph: ContentSizes::zero(), + current_line: ContentSizes::zero(), + pending_whitespace: ContentSizes::zero(), + had_content_yet_for_min_content: false, + had_content_yet_for_max_content: false, + ending_inline_pbm_stack: Vec::new(), + depends_on_block_constraints: false, + } + .traverse(inline_formatting_context) + } +} + +/// Whether or not this character will rpevent a soft wrap opportunity when it +/// comes before or after an atomic inline element. +/// +/// From <https://www.w3.org/TR/css-text-3/#line-break-details>: +/// +/// > For Web-compatibility there is a soft wrap opportunity before and after each +/// > replaced element or other atomic inline, even when adjacent to a character that +/// > would normally suppress them, including U+00A0 NO-BREAK SPACE. However, with +/// > the exception of U+00A0 NO-BREAK SPACE, there must be no soft wrap opportunity +/// > between atomic inlines and adjacent characters belonging to the Unicode GL, WJ, +/// > or ZWJ line breaking classes. +fn char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character: char) -> bool { + if character == '\u{00A0}' { + return false; + } + let class = linebreak_property(character); + class == XI_LINE_BREAKING_CLASS_GL || + class == XI_LINE_BREAKING_CLASS_WJ || + class == XI_LINE_BREAKING_CLASS_ZWJ +} diff --git a/components/layout/flow/inline/text_run.rs b/components/layout/flow/inline/text_run.rs new file mode 100644 index 00000000000..0d0c6398017 --- /dev/null +++ b/components/layout/flow/inline/text_run.rs @@ -0,0 +1,640 @@ +/* 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 std::mem; +use std::ops::Range; + +use app_units::Au; +use base::text::is_bidi_control; +use fonts::{ + FontContext, FontRef, GlyphRun, LAST_RESORT_GLYPH_ADVANCE, ShapingFlags, ShapingOptions, +}; +use fonts_traits::ByteIndex; +use log::warn; +use malloc_size_of_derive::MallocSizeOf; +use range::Range as ServoRange; +use servo_arc::Arc; +use style::computed_values::text_rendering::T as TextRendering; +use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; +use style::computed_values::word_break::T as WordBreak; +use style::properties::ComputedValues; +use style::str::char_is_whitespace; +use style::values::computed::OverflowWrap; +use unicode_bidi::{BidiInfo, Level}; +use unicode_script::Script; +use xi_unicode::linebreak_property; + +use super::line_breaker::LineBreaker; +use super::{FontKeyAndMetrics, InlineFormattingContextLayout}; +use crate::fragment_tree::BaseFragmentInfo; + +// These constants are the xi-unicode line breaking classes that are defined in +// `table.rs`. Unfortunately, they are only identified by number. +pub(crate) const XI_LINE_BREAKING_CLASS_CM: u8 = 9; +pub(crate) const XI_LINE_BREAKING_CLASS_GL: u8 = 12; +pub(crate) const XI_LINE_BREAKING_CLASS_ZW: u8 = 28; +pub(crate) const XI_LINE_BREAKING_CLASS_WJ: u8 = 30; +pub(crate) const XI_LINE_BREAKING_CLASS_ZWJ: u8 = 42; + +/// <https://www.w3.org/TR/css-display-3/#css-text-run> +#[derive(Debug, MallocSizeOf)] +pub(crate) struct TextRun { + pub base_fragment_info: BaseFragmentInfo, + #[conditional_malloc_size_of] + pub parent_style: Arc<ComputedValues>, + pub text_range: Range<usize>, + + /// The text of this [`TextRun`] with a font selected, broken into unbreakable + /// segments, and shaped. + pub shaped_text: Vec<TextRunSegment>, + pub selection_range: Option<ServoRange<ByteIndex>>, + #[conditional_malloc_size_of] + pub selected_style: Arc<ComputedValues>, +} + +// There are two reasons why we might want to break at the start: +// +// 1. The line breaker told us that a break was necessary between two separate +// instances of sending text to it. +// 2. We are following replaced content ie `have_deferred_soft_wrap_opportunity`. +// +// In both cases, we don't want to do this if the first character prevents a +// soft wrap opportunity. +#[derive(PartialEq)] +enum SegmentStartSoftWrapPolicy { + Force, + FollowLinebreaker, +} + +#[derive(Debug, MallocSizeOf)] +pub(crate) struct TextRunSegment { + /// The index of this font in the parent [`super::InlineFormattingContext`]'s collection of font + /// information. + pub font_index: usize, + + /// The [`Script`] of this segment. + pub script: Script, + + /// The bidi Level of this segment. + pub bidi_level: Level, + + /// The range of bytes in the parent [`super::InlineFormattingContext`]'s text content. + pub range: Range<usize>, + + /// Whether or not the linebreaker said that we should allow a line break at the start of this + /// segment. + pub break_at_start: bool, + + /// The shaped runs within this segment. + pub runs: Vec<GlyphRun>, +} + +impl TextRunSegment { + fn new(font_index: usize, script: Script, bidi_level: Level, start_offset: usize) -> Self { + Self { + font_index, + script, + bidi_level, + range: start_offset..start_offset, + runs: Vec::new(), + break_at_start: false, + } + } + + /// Update this segment if the Font and Script are compatible. The update will only + /// ever make the Script specific. Returns true if the new Font and Script are + /// compatible with this segment or false otherwise. + fn update_if_compatible( + &mut self, + new_font: &FontRef, + script: Script, + bidi_level: Level, + fonts: &[FontKeyAndMetrics], + font_context: &FontContext, + ) -> bool { + fn is_specific(script: Script) -> bool { + script != Script::Common && script != Script::Inherited + } + + if bidi_level != self.bidi_level { + return false; + } + + let current_font_key_and_metrics = &fonts[self.font_index]; + if new_font.key(font_context) != current_font_key_and_metrics.key || + new_font.descriptor.pt_size != current_font_key_and_metrics.pt_size + { + return false; + } + + if !is_specific(self.script) && is_specific(script) { + self.script = script; + } + script == self.script || !is_specific(script) + } + + fn layout_into_line_items( + &self, + text_run: &TextRun, + mut soft_wrap_policy: SegmentStartSoftWrapPolicy, + ifc: &mut InlineFormattingContextLayout, + ) { + if self.break_at_start && soft_wrap_policy == SegmentStartSoftWrapPolicy::FollowLinebreaker + { + soft_wrap_policy = SegmentStartSoftWrapPolicy::Force; + } + + let mut byte_processed = ByteIndex(0); + for (run_index, run) in self.runs.iter().enumerate() { + ifc.possibly_flush_deferred_forced_line_break(); + + // If this whitespace forces a line break, queue up a hard line break the next time we + // see any content. We don't line break immediately, because we'd like to finish processing + // any ongoing inline boxes before ending the line. + if run.is_single_preserved_newline() { + byte_processed = byte_processed + run.range.length(); + ifc.defer_forced_line_break(); + continue; + } + // Break before each unbreakable run in this TextRun, except the first unless the + // linebreaker was set to break before the first run. + if run_index != 0 || soft_wrap_policy == SegmentStartSoftWrapPolicy::Force { + ifc.process_soft_wrap_opportunity(); + } + ifc.push_glyph_store_to_unbreakable_segment( + run.glyph_store.clone(), + text_run, + self.font_index, + self.bidi_level, + ServoRange::<ByteIndex>::new( + byte_processed + ByteIndex(self.range.start as isize), + ByteIndex(self.range.len() as isize) - byte_processed, + ), + ); + byte_processed = byte_processed + run.range.length(); + } + } + + fn shape_and_push_range( + &mut self, + range: &Range<usize>, + formatting_context_text: &str, + segment_font: &FontRef, + options: &ShapingOptions, + ) { + self.runs.push(GlyphRun { + glyph_store: segment_font.shape_text(&formatting_context_text[range.clone()], options), + range: ServoRange::new( + ByteIndex(range.start as isize), + ByteIndex(range.len() as isize), + ), + }); + } + + /// Shape the text of this [`TextRunSegment`], first finding "words" for the shaper by processing + /// the linebreaks found in the owning [`super::InlineFormattingContext`]. Linebreaks are filtered, + /// based on the style of the parent inline box. + fn shape_text( + &mut self, + parent_style: &ComputedValues, + formatting_context_text: &str, + linebreaker: &mut LineBreaker, + shaping_options: &ShapingOptions, + font: FontRef, + ) { + // Gather the linebreaks that apply to this segment from the inline formatting context's collection + // of line breaks. Also add a simulated break at the end of the segment in order to ensure the final + // piece of text is processed. + let range = self.range.clone(); + let linebreaks = linebreaker.advance_to_linebreaks_in_range(self.range.clone()); + let linebreak_iter = linebreaks.iter().chain(std::iter::once(&range.end)); + + self.runs.clear(); + self.runs.reserve(linebreaks.len()); + self.break_at_start = false; + + let text_style = parent_style.get_inherited_text().clone(); + let can_break_anywhere = text_style.word_break == WordBreak::BreakAll || + text_style.overflow_wrap == OverflowWrap::Anywhere || + text_style.overflow_wrap == OverflowWrap::BreakWord; + + let mut last_slice = self.range.start..self.range.start; + for break_index in linebreak_iter { + if *break_index == self.range.start { + self.break_at_start = true; + continue; + } + + let mut options = *shaping_options; + + // Extend the slice to the next UAX#14 line break opportunity. + let mut slice = last_slice.end..*break_index; + let word = &formatting_context_text[slice.clone()]; + + // Split off any trailing whitespace into a separate glyph run. + let mut whitespace = slice.end..slice.end; + let mut rev_char_indices = word.char_indices().rev().peekable(); + + let mut ends_with_whitespace = false; + let ends_with_newline = rev_char_indices + .peek() + .is_some_and(|&(_, character)| character == '\n'); + if let Some((first_white_space_index, first_white_space_character)) = rev_char_indices + .take_while(|&(_, character)| char_is_whitespace(character)) + .last() + { + ends_with_whitespace = true; + whitespace.start = slice.start + first_white_space_index; + + // If line breaking for a piece of text that has `white-space-collapse: break-spaces` there + // is a line break opportunity *after* every preserved space, but not before. This means + // that we should not split off the first whitespace, unless that white-space is a preserved + // newline. + // + // An exception to this is if the style tells us that we can break in the middle of words. + if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces && + first_white_space_character != '\n' && + !can_break_anywhere + { + whitespace.start += first_white_space_character.len_utf8(); + options + .flags + .insert(ShapingFlags::ENDS_WITH_WHITESPACE_SHAPING_FLAG); + } + + slice.end = whitespace.start; + } + + // If there's no whitespace and `word-break` is set to `keep-all`, try increasing the slice. + // TODO: This should only happen for CJK text. + if !ends_with_whitespace && + *break_index != self.range.end && + text_style.word_break == WordBreak::KeepAll && + !can_break_anywhere + { + continue; + } + + // Only advance the last slice if we are not going to try to expand the slice. + last_slice = slice.start..*break_index; + + // Push the non-whitespace part of the range. + if !slice.is_empty() { + self.shape_and_push_range(&slice, formatting_context_text, &font, &options); + } + + if whitespace.is_empty() { + continue; + } + + options.flags.insert( + ShapingFlags::IS_WHITESPACE_SHAPING_FLAG | + ShapingFlags::ENDS_WITH_WHITESPACE_SHAPING_FLAG, + ); + + // If `white-space-collapse: break-spaces` is active, insert a line breaking opportunity + // between each white space character in the white space that we trimmed off. + if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces { + let start_index = whitespace.start; + for (index, character) in formatting_context_text[whitespace].char_indices() { + let index = start_index + index; + self.shape_and_push_range( + &(index..index + character.len_utf8()), + formatting_context_text, + &font, + &options, + ); + } + continue; + } + + // The breaker breaks after every newline, so either there is none, + // or there is exactly one at the very end. In the latter case, + // split it into a different run. That's because shaping considers + // a newline to have the same advance as a space, but during layout + // we want to treat the newline as having no advance. + if ends_with_newline && whitespace.len() > 1 { + self.shape_and_push_range( + &(whitespace.start..whitespace.end - 1), + formatting_context_text, + &font, + &options, + ); + self.shape_and_push_range( + &(whitespace.end - 1..whitespace.end), + formatting_context_text, + &font, + &options, + ); + } else { + self.shape_and_push_range(&whitespace, formatting_context_text, &font, &options); + } + } + } +} + +impl TextRun { + pub(crate) fn new( + base_fragment_info: BaseFragmentInfo, + parent_style: Arc<ComputedValues>, + text_range: Range<usize>, + selection_range: Option<ServoRange<ByteIndex>>, + selected_style: Arc<ComputedValues>, + ) -> Self { + Self { + base_fragment_info, + parent_style, + text_range, + shaped_text: Vec::new(), + selection_range, + selected_style, + } + } + + pub(super) fn segment_and_shape( + &mut self, + formatting_context_text: &str, + font_context: &FontContext, + linebreaker: &mut LineBreaker, + font_cache: &mut Vec<FontKeyAndMetrics>, + bidi_info: &BidiInfo, + ) { + let inherited_text_style = self.parent_style.get_inherited_text().clone(); + let letter_spacing = inherited_text_style + .letter_spacing + .0 + .resolve(self.parent_style.clone_font().font_size.computed_size()); + let letter_spacing = if letter_spacing.px() != 0. { + Some(app_units::Au::from(letter_spacing)) + } else { + None + }; + + let mut flags = ShapingFlags::empty(); + if letter_spacing.is_some() { + flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG); + } + if inherited_text_style.text_rendering == TextRendering::Optimizespeed { + flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG); + flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG) + } + + let specified_word_spacing = &inherited_text_style.word_spacing; + let style_word_spacing: Option<Au> = specified_word_spacing.to_length().map(|l| l.into()); + + let segments = self + .segment_text_by_font(formatting_context_text, font_context, font_cache, bidi_info) + .into_iter() + .map(|(mut segment, font)| { + let word_spacing = style_word_spacing.unwrap_or_else(|| { + let space_width = font + .glyph_index(' ') + .map(|glyph_id| font.glyph_h_advance(glyph_id)) + .unwrap_or(LAST_RESORT_GLYPH_ADVANCE); + specified_word_spacing.to_used_value(Au::from_f64_px(space_width)) + }); + + let mut flags = flags; + if segment.bidi_level.is_rtl() { + flags.insert(ShapingFlags::RTL_FLAG); + } + let shaping_options = ShapingOptions { + letter_spacing, + word_spacing, + script: segment.script, + flags, + }; + + segment.shape_text( + &self.parent_style, + formatting_context_text, + linebreaker, + &shaping_options, + font, + ); + segment + }) + .collect(); + + let _ = std::mem::replace(&mut self.shaped_text, segments); + } + + /// Take the [`TextRun`]'s text and turn it into [`TextRunSegment`]s. Each segment has a matched + /// font and script. Fonts may differ when glyphs are found in fallback fonts. Fonts are stored + /// in the `font_cache` which is a cache of all font keys and metrics used in this + /// [`super::InlineFormattingContext`]. + fn segment_text_by_font( + &mut self, + formatting_context_text: &str, + font_context: &FontContext, + font_cache: &mut Vec<FontKeyAndMetrics>, + bidi_info: &BidiInfo, + ) -> Vec<(TextRunSegment, FontRef)> { + let font_group = font_context.font_group(self.parent_style.clone_font()); + let mut current: Option<(TextRunSegment, FontRef)> = None; + let mut results = Vec::new(); + + let text_run_text = &formatting_context_text[self.text_range.clone()]; + let char_iterator = TwoCharsAtATimeIterator::new(text_run_text.chars()); + let mut next_byte_index = self.text_range.start; + for (character, next_character) in char_iterator { + let current_byte_index = next_byte_index; + next_byte_index += character.len_utf8(); + + if char_does_not_change_font(character) { + continue; + } + + // If the script and BiDi level do not change, use the current font as the first fallback. This + // can potentially speed up fallback on long font lists or with uncommon scripts which might be + // at the bottom of the list. + let script = Script::from(character); + let bidi_level = bidi_info.levels[current_byte_index]; + let current_font = current.as_ref().and_then(|(text_run_segment, font)| { + if text_run_segment.bidi_level == bidi_level && text_run_segment.script == script { + Some(font.clone()) + } else { + None + } + }); + + let Some(font) = font_group.write().find_by_codepoint( + font_context, + character, + next_character, + current_font, + ) else { + continue; + }; + + // If the existing segment is compatible with the character, keep going. + if let Some(current) = current.as_mut() { + if current.0.update_if_compatible( + &font, + script, + bidi_level, + font_cache, + font_context, + ) { + continue; + } + } + + let font_index = add_or_get_font(&font, font_cache, font_context); + + // Add the new segment and finish the existing one, if we had one. If the first + // characters in the run were control characters we may be creating the first + // segment in the middle of the run (ie the start should be the start of this + // text run's text). + let start_byte_index = match current { + Some(_) => current_byte_index, + None => self.text_range.start, + }; + let new = ( + TextRunSegment::new(font_index, script, bidi_level, start_byte_index), + font, + ); + if let Some(mut finished) = current.replace(new) { + // The end of the previous segment is the start of the next one. + finished.0.range.end = current_byte_index; + results.push(finished); + } + } + + // Either we have a current segment or we only had control character and whitespace. In both + // of those cases, just use the first font. + if current.is_none() { + current = font_group.write().first(font_context).map(|font| { + let font_index = add_or_get_font(&font, font_cache, font_context); + ( + TextRunSegment::new( + font_index, + Script::Common, + Level::ltr(), + self.text_range.start, + ), + font, + ) + }) + } + + // Extend the last segment to the end of the string and add it to the results. + if let Some(mut last_segment) = current.take() { + last_segment.0.range.end = self.text_range.end; + results.push(last_segment); + } + + results + } + + pub(super) fn layout_into_line_items(&self, ifc: &mut InlineFormattingContextLayout) { + if self.text_range.is_empty() { + return; + } + + // If we are following replaced content, we should have a soft wrap opportunity, unless the + // first character of this `TextRun` prevents that soft wrap opportunity. If we see such a + // character it should also override the LineBreaker's indication to break at the start. + let have_deferred_soft_wrap_opportunity = + mem::replace(&mut ifc.have_deferred_soft_wrap_opportunity, false); + let mut soft_wrap_policy = match have_deferred_soft_wrap_opportunity { + true => SegmentStartSoftWrapPolicy::Force, + false => SegmentStartSoftWrapPolicy::FollowLinebreaker, + }; + + for segment in self.shaped_text.iter() { + segment.layout_into_line_items(self, soft_wrap_policy, ifc); + soft_wrap_policy = SegmentStartSoftWrapPolicy::FollowLinebreaker; + } + } +} + +/// Whether or not this character should be able to change the font during segmentation. Certain +/// character are not rendered at all, so it doesn't matter what font we use to render them. They +/// should just be added to the current segment. +fn char_does_not_change_font(character: char) -> bool { + if character.is_control() { + return true; + } + if character == '\u{00A0}' { + return true; + } + if is_bidi_control(character) { + return false; + } + + let class = linebreak_property(character); + class == XI_LINE_BREAKING_CLASS_CM || + class == XI_LINE_BREAKING_CLASS_GL || + class == XI_LINE_BREAKING_CLASS_ZW || + class == XI_LINE_BREAKING_CLASS_WJ || + class == XI_LINE_BREAKING_CLASS_ZWJ +} + +pub(super) fn add_or_get_font( + font: &FontRef, + ifc_fonts: &mut Vec<FontKeyAndMetrics>, + font_context: &FontContext, +) -> usize { + let font_instance_key = font.key(font_context); + for (index, ifc_font_info) in ifc_fonts.iter().enumerate() { + if ifc_font_info.key == font_instance_key && + ifc_font_info.pt_size == font.descriptor.pt_size + { + return index; + } + } + ifc_fonts.push(FontKeyAndMetrics { + metrics: font.metrics.clone(), + key: font_instance_key, + pt_size: font.descriptor.pt_size, + }); + ifc_fonts.len() - 1 +} + +pub(super) fn get_font_for_first_font_for_style( + style: &ComputedValues, + font_context: &FontContext, +) -> Option<FontRef> { + let font = font_context + .font_group(style.clone_font()) + .write() + .first(font_context); + if font.is_none() { + warn!("Could not find font for style: {:?}", style.clone_font()); + } + font +} +pub(crate) struct TwoCharsAtATimeIterator<InputIterator> { + /// The input character iterator. + iterator: InputIterator, + /// The first character to produce in the next run of the iterator. + next_character: Option<char>, +} + +impl<InputIterator> TwoCharsAtATimeIterator<InputIterator> { + fn new(iterator: InputIterator) -> Self { + Self { + iterator, + next_character: None, + } + } +} + +impl<InputIterator> Iterator for TwoCharsAtATimeIterator<InputIterator> +where + InputIterator: Iterator<Item = char>, +{ + type Item = (char, Option<char>); + + fn next(&mut self) -> Option<Self::Item> { + // If the iterator isn't initialized do that now. + if self.next_character.is_none() { + self.next_character = self.iterator.next(); + } + let character = self.next_character?; + self.next_character = self.iterator.next(); + Some((character, self.next_character)) + } +} diff --git a/components/layout/flow/mod.rs b/components/layout/flow/mod.rs new file mode 100644 index 00000000000..f92650ef340 --- /dev/null +++ b/components/layout/flow/mod.rs @@ -0,0 +1,2370 @@ +/* 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/. */ +#![allow(rustdoc::private_intra_doc_links)] + +//! Flow layout, also known as block-and-inline layout. + +use app_units::{Au, MAX_AU}; +use inline::InlineFormattingContext; +use malloc_size_of_derive::MallocSizeOf; +use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; +use servo_arc::Arc; +use style::Zero; +use style::computed_values::clear::T as StyleClear; +use style::logical_geometry::Direction; +use style::properties::ComputedValues; +use style::servo::selector_parser::PseudoElement; +use style::values::computed::Size as StyleSize; +use style::values::specified::align::AlignFlags; +use style::values::specified::{Display, TextAlignKeyword}; + +use crate::cell::ArcRefCell; +use crate::context::LayoutContext; +use crate::flow::float::{ + Clear, ContainingBlockPositionInfo, FloatBox, FloatSide, PlacementAmongFloats, + SequentialLayoutState, +}; +use crate::formatting_contexts::{ + Baselines, IndependentFormattingContext, IndependentFormattingContextContents, + IndependentNonReplacedContents, +}; +use crate::fragment_tree::{ + BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, CollapsedMargin, Fragment, FragmentFlags, +}; +use crate::geom::{ + AuOrAuto, LogicalRect, LogicalSides, LogicalSides1D, LogicalVec2, PhysicalPoint, PhysicalRect, + PhysicalSides, Size, Sizes, ToLogical, ToLogicalWithContainingBlock, +}; +use crate::layout_box_base::{CacheableLayoutResult, LayoutBoxBase}; +use crate::positioned::{AbsolutelyPositionedBox, PositioningContext, PositioningContextLength}; +use crate::replaced::ReplacedContents; +use crate::sizing::{self, ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult}; +use crate::style_ext::{ContentBoxSizesAndPBM, LayoutStyle, PaddingBorderMargin}; +use crate::{ + ConstraintSpace, ContainingBlock, ContainingBlockSize, IndefiniteContainingBlock, + SizeConstraint, +}; + +mod construct; +pub mod float; +pub mod inline; +mod root; + +pub(crate) use construct::BlockContainerBuilder; +pub use root::{BoxTree, CanvasBackground}; + +#[derive(Debug, MallocSizeOf)] +pub(crate) struct BlockFormattingContext { + pub contents: BlockContainer, + pub contains_floats: bool, +} + +#[derive(Debug, MallocSizeOf)] +pub(crate) enum BlockContainer { + BlockLevelBoxes(Vec<ArcRefCell<BlockLevelBox>>), + InlineFormattingContext(InlineFormattingContext), +} + +impl BlockContainer { + fn contains_floats(&self) -> bool { + match self { + BlockContainer::BlockLevelBoxes(boxes) => boxes + .iter() + .any(|block_level_box| block_level_box.borrow().contains_floats()), + BlockContainer::InlineFormattingContext(context) => context.contains_floats, + } + } +} + +#[derive(Debug, MallocSizeOf)] +pub(crate) enum BlockLevelBox { + Independent(IndependentFormattingContext), + OutOfFlowAbsolutelyPositionedBox(ArcRefCell<AbsolutelyPositionedBox>), + OutOfFlowFloatBox(FloatBox), + OutsideMarker(OutsideMarker), + SameFormattingContextBlock { + base: LayoutBoxBase, + contents: BlockContainer, + contains_floats: bool, + }, +} + +impl BlockLevelBox { + pub(crate) fn invalidate_cached_fragment(&self) { + self.with_base(LayoutBoxBase::invalidate_cached_fragment); + } + + pub(crate) fn fragments(&self) -> Vec<Fragment> { + self.with_base(LayoutBoxBase::fragments) + } + + pub(crate) fn with_base<T>(&self, callback: impl Fn(&LayoutBoxBase) -> T) -> T { + match self { + BlockLevelBox::Independent(independent_formatting_context) => { + callback(&independent_formatting_context.base) + }, + BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => { + callback(&positioned_box.borrow().context.base) + }, + BlockLevelBox::OutOfFlowFloatBox(float_box) => callback(&float_box.contents.base), + BlockLevelBox::OutsideMarker(outside_marker) => callback(&outside_marker.base), + BlockLevelBox::SameFormattingContextBlock { base, .. } => callback(base), + } + } + + fn contains_floats(&self) -> bool { + match self { + BlockLevelBox::SameFormattingContextBlock { + contains_floats, .. + } => *contains_floats, + BlockLevelBox::OutOfFlowFloatBox { .. } => true, + _ => false, + } + } + + fn find_block_margin_collapsing_with_parent( + &self, + layout_context: &LayoutContext, + collected_margin: &mut CollapsedMargin, + containing_block: &ContainingBlock, + ) -> bool { + let layout_style = match self { + BlockLevelBox::SameFormattingContextBlock { base, contents, .. } => { + contents.layout_style(base) + }, + BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_) | + BlockLevelBox::OutOfFlowFloatBox(_) => return true, + BlockLevelBox::OutsideMarker(_) => return false, + BlockLevelBox::Independent(context) => { + // FIXME: If the element doesn't fit next to floats, it will get clearance. + // In that case this should be returning false. + context.layout_style() + }, + }; + + // FIXME: This should only return false when 'clear' causes clearance. + let style = layout_style.style(); + if style.get_box().clear != StyleClear::None { + return false; + } + + let ContentBoxSizesAndPBM { + content_box_sizes, + pbm, + .. + } = layout_style.content_box_sizes_and_padding_border_margin(&containing_block.into()); + let margin = pbm.margin.auto_is(Au::zero); + collected_margin.adjoin_assign(&CollapsedMargin::new(margin.block_start)); + + let child_boxes = match self { + BlockLevelBox::SameFormattingContextBlock { contents, .. } => match contents { + BlockContainer::BlockLevelBoxes(boxes) => boxes, + BlockContainer::InlineFormattingContext(_) => return false, + }, + _ => return false, + }; + + if !pbm.padding.block_start.is_zero() || !pbm.border.block_start.is_zero() { + return false; + } + + let available_inline_size = + containing_block.size.inline - pbm.padding_border_sums.inline - margin.inline_sum(); + let available_block_size = containing_block.size.block.to_definite().map(|block_size| { + Au::zero().max(block_size - pbm.padding_border_sums.block - margin.block_sum()) + }); + + let tentative_block_size = content_box_sizes.block.resolve_extrinsic( + Size::FitContent, + Au::zero(), + available_block_size, + ); + + let get_inline_content_sizes = || { + let constraint_space = ConstraintSpace::new( + tentative_block_size, + style.writing_mode, + None, /* TODO: support preferred aspect ratios on non-replaced boxes */ + ); + self.inline_content_sizes(layout_context, &constraint_space) + .sizes + }; + let inline_size = content_box_sizes.inline.resolve( + Direction::Inline, + Size::Stretch, + Au::zero, + Some(available_inline_size), + get_inline_content_sizes, + false, /* is_table */ + ); + + let containing_block_for_children = ContainingBlock { + size: ContainingBlockSize { + inline: inline_size, + block: tentative_block_size, + }, + style, + }; + + if !Self::find_block_margin_collapsing_with_parent_from_slice( + layout_context, + child_boxes, + collected_margin, + &containing_block_for_children, + ) { + return false; + } + + if !block_size_is_zero_or_intrinsic(style.content_block_size(), containing_block) || + !block_size_is_zero_or_intrinsic(style.min_block_size(), containing_block) || + !pbm.padding_border_sums.block.is_zero() + { + return false; + } + + collected_margin.adjoin_assign(&CollapsedMargin::new(margin.block_end)); + + true + } + + fn find_block_margin_collapsing_with_parent_from_slice( + layout_context: &LayoutContext, + boxes: &[ArcRefCell<BlockLevelBox>], + margin: &mut CollapsedMargin, + containing_block: &ContainingBlock, + ) -> bool { + boxes.iter().all(|block_level_box| { + block_level_box + .borrow() + .find_block_margin_collapsing_with_parent(layout_context, margin, containing_block) + }) + } +} + +#[derive(Clone, Copy)] +pub(crate) struct CollapsibleWithParentStartMargin(bool); + +/// The contentes of a BlockContainer created to render a list marker +/// for a list that has `list-style-position: outside`. +#[derive(Debug, MallocSizeOf)] +pub(crate) struct OutsideMarker { + #[conditional_malloc_size_of] + pub list_item_style: Arc<ComputedValues>, + pub base: LayoutBoxBase, + pub block_container: BlockContainer, +} + +impl OutsideMarker { + fn inline_content_sizes( + &self, + layout_context: &LayoutContext, + constraint_space: &ConstraintSpace, + ) -> InlineContentSizesResult { + self.base + .inline_content_sizes(layout_context, constraint_space, &self.block_container) + } + + fn layout( + &self, + layout_context: &LayoutContext<'_>, + containing_block: &ContainingBlock<'_>, + positioning_context: &mut PositioningContext, + sequential_layout_state: Option<&mut SequentialLayoutState>, + collapsible_with_parent_start_margin: Option<CollapsibleWithParentStartMargin>, + ) -> Fragment { + let constraint_space = ConstraintSpace::new_for_style_and_ratio( + &self.base.style, + None, /* TODO: support preferred aspect ratios on non-replaced boxes */ + ); + let content_sizes = self.inline_content_sizes(layout_context, &constraint_space); + let containing_block_for_children = ContainingBlock { + size: ContainingBlockSize { + inline: content_sizes.sizes.max_content, + block: SizeConstraint::default(), + }, + style: &self.base.style, + }; + + // A ::marker can't have a stretch size (must be auto), so this doesn't matter. + // https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing + let ignore_block_margins_for_stretch = LogicalSides1D::new(false, false); + + let flow_layout = self.block_container.layout( + layout_context, + positioning_context, + &containing_block_for_children, + sequential_layout_state, + collapsible_with_parent_start_margin.unwrap_or(CollapsibleWithParentStartMargin(false)), + ignore_block_margins_for_stretch, + ); + + let max_inline_size = + flow_layout + .fragments + .iter() + .fold(Au::zero(), |current_max, fragment| { + current_max.max( + match fragment { + Fragment::Text(text) => text.borrow().rect, + Fragment::Image(image) => image.borrow().rect, + Fragment::Positioning(positioning) => positioning.borrow().rect, + Fragment::Box(_) | + Fragment::Float(_) | + Fragment::AbsoluteOrFixedPositioned(_) | + Fragment::IFrame(_) => { + unreachable!( + "Found unexpected fragment type in outside list marker!" + ); + }, + } + .to_logical(&containing_block_for_children) + .max_inline_position(), + ) + }); + + // Position the marker beyond the inline start of the border box list item. This needs to + // take into account the border and padding of the item. + // + // TODO: This is the wrong containing block, as it should be the containing block of the + // parent of this list item. What this means in practice is that the writing mode could be + // wrong and padding defined as a percentage will be resolved incorrectly. + // + // TODO: This should use the LayoutStyle of the list item, not the default one. Currently + // they are the same, but this could change in the future. + let pbm_of_list_item = + LayoutStyle::Default(&self.list_item_style).padding_border_margin(containing_block); + let content_rect = LogicalRect { + start_corner: LogicalVec2 { + inline: -max_inline_size - + (pbm_of_list_item.border.inline_start + + pbm_of_list_item.padding.inline_start), + block: Zero::zero(), + }, + size: LogicalVec2 { + inline: max_inline_size, + block: flow_layout.content_block_size, + }, + }; + + let mut base_fragment_info = BaseFragmentInfo::anonymous(); + base_fragment_info.flags |= FragmentFlags::IS_OUTSIDE_LIST_ITEM_MARKER; + + Fragment::Box(ArcRefCell::new(BoxFragment::new( + base_fragment_info, + self.base.style.clone(), + flow_layout.fragments, + content_rect.as_physical(Some(containing_block)), + PhysicalSides::zero(), + PhysicalSides::zero(), + PhysicalSides::zero(), + None, + ))) + } +} + +impl BlockFormattingContext { + pub(super) fn layout( + &self, + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + containing_block: &ContainingBlock, + depends_on_block_constraints: bool, + ) -> CacheableLayoutResult { + let mut sequential_layout_state = if self.contains_floats || !layout_context.use_rayon { + Some(SequentialLayoutState::new(containing_block.size.inline)) + } else { + None + }; + + // Since this is an independent formatting context, we don't ignore block margins when + // resolving a stretch block size of the children. + // https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing + let ignore_block_margins_for_stretch = LogicalSides1D::new(false, false); + + let flow_layout = self.contents.layout( + layout_context, + positioning_context, + containing_block, + sequential_layout_state.as_mut(), + CollapsibleWithParentStartMargin(false), + ignore_block_margins_for_stretch, + ); + debug_assert!( + !flow_layout + .collapsible_margins_in_children + .collapsed_through + ); + + // The content height of a BFC root should include any float participating in that BFC + // (https://drafts.csswg.org/css2/#root-height), we implement this by imagining there is + // an element with `clear: both` after the actual contents. + let clearance = sequential_layout_state.and_then(|sequential_layout_state| { + sequential_layout_state.calculate_clearance(Clear::Both, &CollapsedMargin::zero()) + }); + + CacheableLayoutResult { + fragments: flow_layout.fragments, + content_block_size: flow_layout.content_block_size + + flow_layout.collapsible_margins_in_children.end.solve() + + clearance.unwrap_or_default(), + content_inline_size_for_table: None, + baselines: flow_layout.baselines, + depends_on_block_constraints: depends_on_block_constraints || + flow_layout.depends_on_block_constraints, + specific_layout_info: None, + collapsible_margins_in_children: CollapsedBlockMargins::zero(), + } + } + + #[inline] + pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> { + LayoutStyle::Default(&base.style) + } +} + +/// Finds the min/max-content inline size of the block-level children of a block container. +/// The in-flow boxes will stack vertically, so we only need to consider the maximum size. +/// But floats can flow horizontally depending on 'clear', so we may need to sum their sizes. +/// CSS 2 does not define the exact algorithm, this logic is based on the behavior observed +/// on Gecko and Blink. +fn compute_inline_content_sizes_for_block_level_boxes( + boxes: &[ArcRefCell<BlockLevelBox>], + layout_context: &LayoutContext, + containing_block: &IndefiniteContainingBlock, +) -> InlineContentSizesResult { + let get_box_info = |box_: &ArcRefCell<BlockLevelBox>| { + match &*box_.borrow() { + BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_) | + BlockLevelBox::OutsideMarker { .. } => None, + BlockLevelBox::OutOfFlowFloatBox(float_box) => { + let inline_content_sizes_result = float_box.contents.outer_inline_content_sizes( + layout_context, + containing_block, + &LogicalVec2::zero(), + false, /* auto_block_size_stretches_to_containing_block */ + ); + let style = &float_box.contents.style(); + Some(( + inline_content_sizes_result, + FloatSide::from_style_and_container_writing_mode( + style, + containing_block.writing_mode, + ), + Clear::from_style_and_container_writing_mode( + style, + containing_block.writing_mode, + ), + )) + }, + BlockLevelBox::SameFormattingContextBlock { base, contents, .. } => { + let inline_content_sizes_result = sizing::outer_inline( + &contents.layout_style(base), + containing_block, + &LogicalVec2::zero(), + false, /* auto_block_size_stretches_to_containing_block */ + false, /* is_replaced */ + !matches!(base.style.pseudo(), Some(PseudoElement::ServoAnonymousBox)), + |_| None, /* TODO: support preferred aspect ratios on non-replaced boxes */ + |constraint_space| { + base.inline_content_sizes(layout_context, constraint_space, contents) + }, + ); + // A block in the same BFC can overlap floats, it's not moved next to them, + // so we shouldn't add its size to the size of the floats. + // Instead, we treat it like an independent block with 'clear: both'. + Some((inline_content_sizes_result, None, Clear::Both)) + }, + BlockLevelBox::Independent(independent) => { + let inline_content_sizes_result = independent.outer_inline_content_sizes( + layout_context, + containing_block, + &LogicalVec2::zero(), + false, /* auto_block_size_stretches_to_containing_block */ + ); + Some(( + inline_content_sizes_result, + None, + Clear::from_style_and_container_writing_mode( + independent.style(), + containing_block.writing_mode, + ), + )) + }, + } + }; + + /// When iterating the block-level boxes to compute the inline content sizes, + /// this struct contains the data accumulated up to the current box. + #[derive(Default)] + struct AccumulatedData { + /// Whether the inline size depends on the block one. + depends_on_block_constraints: bool, + /// The maximum size seen so far, not including trailing uncleared floats. + max_size: ContentSizes, + /// The size of the trailing uncleared floats on the inline-start side + /// of the containing block. + start_floats: ContentSizes, + /// The size of the trailing uncleared floats on the inline-end side + /// of the containing block. + end_floats: ContentSizes, + } + + impl AccumulatedData { + fn max_size_including_uncleared_floats(&self) -> ContentSizes { + self.max_size.max(self.start_floats.union(&self.end_floats)) + } + fn clear_floats(&mut self, clear: Clear) { + match clear { + Clear::InlineStart => { + self.max_size = self.max_size_including_uncleared_floats(); + self.start_floats = ContentSizes::zero(); + }, + Clear::InlineEnd => { + self.max_size = self.max_size_including_uncleared_floats(); + self.end_floats = ContentSizes::zero(); + }, + Clear::Both => { + self.max_size = self.max_size_including_uncleared_floats(); + self.start_floats = ContentSizes::zero(); + self.end_floats = ContentSizes::zero(); + }, + Clear::None => {}, + }; + } + } + + let accumulate = + |mut data: AccumulatedData, + (inline_content_sizes_result, float, clear): (InlineContentSizesResult, _, _)| { + let size = inline_content_sizes_result.sizes.max(ContentSizes::zero()); + let depends_on_block_constraints = + inline_content_sizes_result.depends_on_block_constraints; + data.depends_on_block_constraints |= depends_on_block_constraints; + data.clear_floats(clear); + match float { + Some(FloatSide::InlineStart) => data.start_floats = data.start_floats.union(&size), + Some(FloatSide::InlineEnd) => data.end_floats = data.end_floats.union(&size), + None => { + data.max_size = data + .max_size + .max(data.start_floats.union(&data.end_floats).union(&size)); + data.start_floats = ContentSizes::zero(); + data.end_floats = ContentSizes::zero(); + }, + } + data + }; + let data = if layout_context.use_rayon { + boxes + .par_iter() + .filter_map(get_box_info) + .collect::<Vec<_>>() + .into_iter() + .fold(AccumulatedData::default(), accumulate) + } else { + boxes + .iter() + .filter_map(get_box_info) + .fold(AccumulatedData::default(), accumulate) + }; + InlineContentSizesResult { + depends_on_block_constraints: data.depends_on_block_constraints, + sizes: data.max_size_including_uncleared_floats(), + } +} + +impl BlockContainer { + fn layout( + &self, + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + containing_block: &ContainingBlock, + sequential_layout_state: Option<&mut SequentialLayoutState>, + collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin, + ignore_block_margins_for_stretch: LogicalSides1D<bool>, + ) -> CacheableLayoutResult { + match self { + BlockContainer::BlockLevelBoxes(child_boxes) => layout_block_level_children( + layout_context, + positioning_context, + child_boxes, + containing_block, + sequential_layout_state, + collapsible_with_parent_start_margin, + ignore_block_margins_for_stretch, + ), + BlockContainer::InlineFormattingContext(ifc) => ifc.layout( + layout_context, + positioning_context, + containing_block, + sequential_layout_state, + collapsible_with_parent_start_margin, + ), + } + } + + #[inline] + pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> { + LayoutStyle::Default(&base.style) + } +} + +impl ComputeInlineContentSizes for BlockContainer { + fn compute_inline_content_sizes( + &self, + layout_context: &LayoutContext, + constraint_space: &ConstraintSpace, + ) -> InlineContentSizesResult { + match &self { + Self::BlockLevelBoxes(boxes) => compute_inline_content_sizes_for_block_level_boxes( + boxes, + layout_context, + &constraint_space.into(), + ), + Self::InlineFormattingContext(context) => { + context.compute_inline_content_sizes(layout_context, constraint_space) + }, + } + } +} + +fn layout_block_level_children( + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + child_boxes: &[ArcRefCell<BlockLevelBox>], + containing_block: &ContainingBlock, + mut sequential_layout_state: Option<&mut SequentialLayoutState>, + collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin, + ignore_block_margins_for_stretch: LogicalSides1D<bool>, +) -> CacheableLayoutResult { + let mut placement_state = + PlacementState::new(collapsible_with_parent_start_margin, containing_block); + + let fragments = match sequential_layout_state { + Some(ref mut sequential_layout_state) => layout_block_level_children_sequentially( + layout_context, + positioning_context, + child_boxes, + containing_block, + sequential_layout_state, + &mut placement_state, + ignore_block_margins_for_stretch, + ), + None => layout_block_level_children_in_parallel( + layout_context, + positioning_context, + child_boxes, + containing_block, + &mut placement_state, + ignore_block_margins_for_stretch, + ), + }; + + let depends_on_block_constraints = fragments.iter().any(|fragment| { + fragment.base().is_some_and(|base| { + base.flags.contains( + FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM, + ) + }) + }); + + let (content_block_size, collapsible_margins_in_children, baselines) = placement_state.finish(); + CacheableLayoutResult { + fragments, + content_block_size, + collapsible_margins_in_children, + baselines, + depends_on_block_constraints, + content_inline_size_for_table: None, + specific_layout_info: None, + } +} + +fn layout_block_level_children_in_parallel( + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + child_boxes: &[ArcRefCell<BlockLevelBox>], + containing_block: &ContainingBlock, + placement_state: &mut PlacementState, + ignore_block_margins_for_stretch: LogicalSides1D<bool>, +) -> Vec<Fragment> { + let collects_for_nearest_positioned_ancestor = + positioning_context.collects_for_nearest_positioned_ancestor(); + let mut layout_results: Vec<(Fragment, PositioningContext)> = + Vec::with_capacity(child_boxes.len()); + + child_boxes + .par_iter() + .map(|child_box| { + let mut child_positioning_context = + PositioningContext::new_for_subtree(collects_for_nearest_positioned_ancestor); + let fragment = child_box.borrow().layout( + layout_context, + &mut child_positioning_context, + containing_block, + /* sequential_layout_state = */ None, + /* collapsible_with_parent_start_margin = */ None, + ignore_block_margins_for_stretch, + ); + (fragment, child_positioning_context) + }) + .collect_into_vec(&mut layout_results); + + layout_results + .into_iter() + .map(|(mut fragment, mut child_positioning_context)| { + placement_state.place_fragment_and_update_baseline(&mut fragment, None); + child_positioning_context.adjust_static_position_of_hoisted_fragments( + &fragment, + PositioningContextLength::zero(), + ); + positioning_context.append(child_positioning_context); + fragment + }) + .collect() +} + +fn layout_block_level_children_sequentially( + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + child_boxes: &[ArcRefCell<BlockLevelBox>], + containing_block: &ContainingBlock, + sequential_layout_state: &mut SequentialLayoutState, + placement_state: &mut PlacementState, + ignore_block_margins_for_stretch: LogicalSides1D<bool>, +) -> Vec<Fragment> { + // Because floats are involved, we do layout for this block formatting context in tree + // order without parallelism. This enables mutable access to a `SequentialLayoutState` that + // tracks every float encountered so far (again in tree order). + child_boxes + .iter() + .map(|child_box| { + let positioning_context_length_before_layout = positioning_context.len(); + let mut fragment = child_box.borrow().layout( + layout_context, + positioning_context, + containing_block, + Some(&mut *sequential_layout_state), + Some(CollapsibleWithParentStartMargin( + placement_state.next_in_flow_margin_collapses_with_parent_start_margin, + )), + ignore_block_margins_for_stretch, + ); + + placement_state + .place_fragment_and_update_baseline(&mut fragment, Some(sequential_layout_state)); + positioning_context.adjust_static_position_of_hoisted_fragments( + &fragment, + positioning_context_length_before_layout, + ); + + fragment + }) + .collect() +} + +impl BlockLevelBox { + fn layout( + &self, + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + containing_block: &ContainingBlock, + sequential_layout_state: Option<&mut SequentialLayoutState>, + collapsible_with_parent_start_margin: Option<CollapsibleWithParentStartMargin>, + ignore_block_margins_for_stretch: LogicalSides1D<bool>, + ) -> Fragment { + let fragment = match self { + BlockLevelBox::SameFormattingContextBlock { base, contents, .. } => Fragment::Box( + ArcRefCell::new(positioning_context.layout_maybe_position_relative_fragment( + layout_context, + containing_block, + &base.style, + |positioning_context| { + layout_in_flow_non_replaced_block_level_same_formatting_context( + layout_context, + positioning_context, + containing_block, + base, + contents, + sequential_layout_state, + collapsible_with_parent_start_margin, + ignore_block_margins_for_stretch, + ) + }, + )), + ), + BlockLevelBox::Independent(independent) => Fragment::Box(ArcRefCell::new( + positioning_context.layout_maybe_position_relative_fragment( + layout_context, + containing_block, + independent.style(), + |positioning_context| { + independent.layout_in_flow_block_level( + layout_context, + positioning_context, + containing_block, + sequential_layout_state, + ignore_block_margins_for_stretch, + ) + }, + ), + )), + BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(box_) => { + // The static position of zero here is incorrect, however we do not know + // the correct positioning until later, in place_block_level_fragment, and + // this value will be adjusted there. + let hoisted_box = AbsolutelyPositionedBox::to_hoisted( + box_.clone(), + // This is incorrect, however we do not know the correct positioning + // until later, in PlacementState::place_fragment, and this value will be + // adjusted there + PhysicalRect::zero(), + LogicalVec2 { + inline: AlignFlags::START, + block: AlignFlags::START, + }, + containing_block.style.writing_mode, + ); + let hoisted_fragment = hoisted_box.fragment.clone(); + positioning_context.push(hoisted_box); + Fragment::AbsoluteOrFixedPositioned(hoisted_fragment) + }, + BlockLevelBox::OutOfFlowFloatBox(float_box) => Fragment::Float(ArcRefCell::new( + float_box.layout(layout_context, positioning_context, containing_block), + )), + BlockLevelBox::OutsideMarker(outside_marker) => outside_marker.layout( + layout_context, + containing_block, + positioning_context, + sequential_layout_state, + collapsible_with_parent_start_margin, + ), + }; + + self.with_base(|base| base.set_fragment(fragment.clone())); + + fragment + } + + fn inline_content_sizes( + &self, + layout_context: &LayoutContext, + constraint_space: &ConstraintSpace, + ) -> InlineContentSizesResult { + let independent_formatting_context = match self { + BlockLevelBox::Independent(independent) => independent, + BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(box_) => &box_.borrow().context, + BlockLevelBox::OutOfFlowFloatBox(float_box) => &float_box.contents, + BlockLevelBox::OutsideMarker(outside_marker) => { + return outside_marker.inline_content_sizes(layout_context, constraint_space); + }, + BlockLevelBox::SameFormattingContextBlock { base, contents, .. } => { + return base.inline_content_sizes(layout_context, constraint_space, contents); + }, + }; + independent_formatting_context.inline_content_sizes(layout_context, constraint_space) + } +} + +/// Lay out a normal flow non-replaced block that does not establish a new formatting +/// context. +/// +/// - <https://drafts.csswg.org/css2/visudet.html#blockwidth> +/// - <https://drafts.csswg.org/css2/visudet.html#normal-block> +#[allow(clippy::too_many_arguments)] +fn layout_in_flow_non_replaced_block_level_same_formatting_context( + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + containing_block: &ContainingBlock, + base: &LayoutBoxBase, + contents: &BlockContainer, + mut sequential_layout_state: Option<&mut SequentialLayoutState>, + collapsible_with_parent_start_margin: Option<CollapsibleWithParentStartMargin>, + ignore_block_margins_for_stretch: LogicalSides1D<bool>, +) -> BoxFragment { + let style = &base.style; + let layout_style = contents.layout_style(base); + let containing_block_writing_mode = containing_block.style.writing_mode; + let get_inline_content_sizes = |constraint_space: &ConstraintSpace| { + base.inline_content_sizes(layout_context, constraint_space, contents) + .sizes + }; + let ContainingBlockPaddingAndBorder { + containing_block: containing_block_for_children, + pbm, + block_sizes, + depends_on_block_constraints, + available_block_size, + } = solve_containing_block_padding_and_border_for_in_flow_box( + containing_block, + &layout_style, + get_inline_content_sizes, + ignore_block_margins_for_stretch, + ); + let ResolvedMargins { + margin, + effective_margin_inline_start, + } = solve_margins( + containing_block, + &pbm, + containing_block_for_children.size.inline, + ); + + let computed_block_size = style.content_block_size(); + let start_margin_can_collapse_with_children = + pbm.padding.block_start.is_zero() && pbm.border.block_start.is_zero(); + + let mut clearance = None; + let parent_containing_block_position_info; + match sequential_layout_state { + None => parent_containing_block_position_info = None, + Some(ref mut sequential_layout_state) => { + let clear = + Clear::from_style_and_container_writing_mode(style, containing_block_writing_mode); + let mut block_start_margin = CollapsedMargin::new(margin.block_start); + + // The block start margin may collapse with content margins, + // compute the resulting one in order to place floats correctly. + // Only need to do this if the element isn't also collapsing with its parent, + // otherwise we should have already included the margin in an ancestor. + // Note this lookahead stops when finding a descendant whose `clear` isn't `none` + // (since clearance prevents collapsing margins with the parent). + // But then we have to decide whether to actually add clearance or not, + // so look forward again regardless of `collapsible_with_parent_start_margin`. + // TODO: This isn't completely right: if we don't add actual clearance, + // the margin should have been included in the parent (or some ancestor). + // The lookahead should stop for actual clearance, not just for `clear`. + let collapsible_with_parent_start_margin = collapsible_with_parent_start_margin.expect( + "We should know whether we are collapsing the block start margin with the parent \ + when laying out sequentially", + ).0 && clear == Clear::None; + if !collapsible_with_parent_start_margin && start_margin_can_collapse_with_children { + if let BlockContainer::BlockLevelBoxes(child_boxes) = contents { + BlockLevelBox::find_block_margin_collapsing_with_parent_from_slice( + layout_context, + child_boxes, + &mut block_start_margin, + &containing_block_for_children, + ); + } + } + + // Introduce clearance if necessary. + clearance = sequential_layout_state.calculate_clearance(clear, &block_start_margin); + if clearance.is_some() { + sequential_layout_state.collapse_margins(); + } + sequential_layout_state.adjoin_assign(&block_start_margin); + if !start_margin_can_collapse_with_children { + sequential_layout_state.collapse_margins(); + } + + // NB: This will be a no-op if we're collapsing margins with our children since that + // can only happen if we have no block-start padding and border. + sequential_layout_state.advance_block_position( + pbm.padding.block_start + + pbm.border.block_start + + clearance.unwrap_or_else(Au::zero), + ); + + // We are about to lay out children. Update the offset between the block formatting + // context and the containing block that we create for them. This offset is used to + // ajust BFC relative coordinates to coordinates that are relative to our content box. + // Our content box establishes the containing block for non-abspos children, including + // floats. + let inline_start = sequential_layout_state + .floats + .containing_block_info + .inline_start + + pbm.padding.inline_start + + pbm.border.inline_start + + effective_margin_inline_start; + let new_cb_offsets = ContainingBlockPositionInfo { + block_start: sequential_layout_state.bfc_relative_block_position, + block_start_margins_not_collapsed: sequential_layout_state.current_margin, + inline_start, + inline_end: inline_start + containing_block_for_children.size.inline, + }; + parent_containing_block_position_info = Some( + sequential_layout_state.replace_containing_block_position_info(new_cb_offsets), + ); + }, + }; + + // https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing + // > If this is a block axis size, and the element is in a Block Layout formatting context, + // > and the parent element does not have a block-start border or padding and is not an + // > independent formatting context, treat the element’s block-start margin as zero + // > for the purpose of calculating this size. Do the same for the block-end margin. + let ignore_block_margins_for_stretch = LogicalSides1D::new( + pbm.border.block_start.is_zero() && pbm.padding.block_start.is_zero(), + pbm.border.block_end.is_zero() && pbm.padding.block_end.is_zero(), + ); + + let flow_layout = contents.layout( + layout_context, + positioning_context, + &containing_block_for_children, + sequential_layout_state.as_deref_mut(), + CollapsibleWithParentStartMargin(start_margin_can_collapse_with_children), + ignore_block_margins_for_stretch, + ); + let mut content_block_size: Au = flow_layout.content_block_size; + + // Update margins. + let mut block_margins_collapsed_with_children = CollapsedBlockMargins::from_margin(&margin); + let mut collapsible_margins_in_children = flow_layout.collapsible_margins_in_children; + if start_margin_can_collapse_with_children { + block_margins_collapsed_with_children + .start + .adjoin_assign(&collapsible_margins_in_children.start); + if collapsible_margins_in_children.collapsed_through { + block_margins_collapsed_with_children + .start + .adjoin_assign(&std::mem::replace( + &mut collapsible_margins_in_children.end, + CollapsedMargin::zero(), + )); + } + } + + let collapsed_through = collapsible_margins_in_children.collapsed_through && + pbm.padding_border_sums.block.is_zero() && + block_size_is_zero_or_intrinsic(computed_block_size, containing_block) && + block_size_is_zero_or_intrinsic(style.min_block_size(), containing_block); + block_margins_collapsed_with_children.collapsed_through = collapsed_through; + + let end_margin_can_collapse_with_children = collapsed_through || + (pbm.padding.block_end.is_zero() && + pbm.border.block_end.is_zero() && + !containing_block_for_children.size.block.is_definite()); + if end_margin_can_collapse_with_children { + block_margins_collapsed_with_children + .end + .adjoin_assign(&collapsible_margins_in_children.end); + } else { + content_block_size += collapsible_margins_in_children.end.solve(); + } + + let block_size = block_sizes.resolve( + Direction::Block, + Size::FitContent, + Au::zero, + available_block_size, + || content_block_size.into(), + false, /* is_table */ + ); + + if let Some(ref mut sequential_layout_state) = sequential_layout_state { + // Now that we're done laying out our children, we can restore the + // parent's containing block position information. + sequential_layout_state + .replace_containing_block_position_info(parent_containing_block_position_info.unwrap()); + + // Account for padding and border. We also might have to readjust the + // `bfc_relative_block_position` if it was different from the content size (i.e. was + // non-`auto` and/or was affected by min/max block size). + // + // If this adjustment is positive, that means that a block size was specified, but + // the content inside had a smaller block size. If this adjustment is negative, a + // block size was specified, but the content inside overflowed this container in + // the block direction. In that case, the ceiling for floats is effectively raised + // as long as no floats in the overflowing content lowered it. + sequential_layout_state.advance_block_position( + block_size - content_block_size + pbm.padding.block_end + pbm.border.block_end, + ); + + if !end_margin_can_collapse_with_children { + sequential_layout_state.collapse_margins(); + } + sequential_layout_state.adjoin_assign(&CollapsedMargin::new(margin.block_end)); + } + + let content_rect = LogicalRect { + start_corner: LogicalVec2 { + block: (pbm.padding.block_start + + pbm.border.block_start + + clearance.unwrap_or_else(Au::zero)), + inline: pbm.padding.inline_start + + pbm.border.inline_start + + effective_margin_inline_start, + }, + size: LogicalVec2 { + block: block_size, + inline: containing_block_for_children.size.inline, + }, + }; + + let mut base_fragment_info = base.base_fragment_info; + if depends_on_block_constraints { + base_fragment_info + .flags + .insert(FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM); + } + + BoxFragment::new( + base_fragment_info, + style.clone(), + flow_layout.fragments, + content_rect.as_physical(Some(containing_block)), + pbm.padding.to_physical(containing_block_writing_mode), + pbm.border.to_physical(containing_block_writing_mode), + margin.to_physical(containing_block_writing_mode), + clearance, + ) + .with_baselines(flow_layout.baselines) + .with_block_margins_collapsed_with_children(block_margins_collapsed_with_children) +} + +impl IndependentNonReplacedContents { + /// Lay out a normal in flow non-replaced block that establishes an independent + /// formatting context in its containing formatting context. + /// + /// - <https://drafts.csswg.org/css2/visudet.html#blockwidth> + /// - <https://drafts.csswg.org/css2/visudet.html#normal-block> + pub(crate) fn layout_in_flow_block_level( + &self, + base: &LayoutBoxBase, + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + containing_block: &ContainingBlock, + sequential_layout_state: Option<&mut SequentialLayoutState>, + ignore_block_margins_for_stretch: LogicalSides1D<bool>, + ) -> BoxFragment { + if let Some(sequential_layout_state) = sequential_layout_state { + return self.layout_in_flow_block_level_sequentially( + base, + layout_context, + positioning_context, + containing_block, + sequential_layout_state, + ignore_block_margins_for_stretch, + ); + } + + let get_inline_content_sizes = |constraint_space: &ConstraintSpace| { + base.inline_content_sizes(layout_context, constraint_space, self) + .sizes + }; + let layout_style = self.layout_style(base); + let ContainingBlockPaddingAndBorder { + containing_block: containing_block_for_children, + pbm, + block_sizes, + depends_on_block_constraints, + available_block_size, + } = solve_containing_block_padding_and_border_for_in_flow_box( + containing_block, + &layout_style, + get_inline_content_sizes, + ignore_block_margins_for_stretch, + ); + + let layout = self.layout( + layout_context, + positioning_context, + &containing_block_for_children, + containing_block, + base, + false, /* depends_on_block_constraints */ + ); + + let inline_size = layout + .content_inline_size_for_table + .unwrap_or(containing_block_for_children.size.inline); + let block_size = block_sizes.resolve( + Direction::Block, + Size::FitContent, + Au::zero, + available_block_size, + || layout.content_block_size.into(), + layout_style.is_table(), + ); + + let ResolvedMargins { + margin, + effective_margin_inline_start, + } = solve_margins(containing_block, &pbm, inline_size); + + let content_rect = LogicalRect { + start_corner: LogicalVec2 { + block: pbm.padding.block_start + pbm.border.block_start, + inline: pbm.padding.inline_start + + pbm.border.inline_start + + effective_margin_inline_start, + }, + size: LogicalVec2 { + block: block_size, + inline: inline_size, + }, + }; + + let block_margins_collapsed_with_children = CollapsedBlockMargins::from_margin(&margin); + let containing_block_writing_mode = containing_block.style.writing_mode; + + let mut base_fragment_info = base.base_fragment_info; + if depends_on_block_constraints { + base_fragment_info.flags.insert( + FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM, + ); + } + BoxFragment::new( + base_fragment_info, + base.style.clone(), + layout.fragments, + content_rect.as_physical(Some(containing_block)), + pbm.padding.to_physical(containing_block_writing_mode), + pbm.border.to_physical(containing_block_writing_mode), + margin.to_physical(containing_block_writing_mode), + None, /* clearance */ + ) + .with_baselines(layout.baselines) + .with_specific_layout_info(layout.specific_layout_info) + .with_block_margins_collapsed_with_children(block_margins_collapsed_with_children) + } + + /// Lay out a normal in flow non-replaced block that establishes an independent + /// formatting context in its containing formatting context but handling sequential + /// layout concerns, such clearing and placing the content next to floats. + fn layout_in_flow_block_level_sequentially( + &self, + base: &LayoutBoxBase, + layout_context: &LayoutContext<'_>, + positioning_context: &mut PositioningContext, + containing_block: &ContainingBlock<'_>, + sequential_layout_state: &mut SequentialLayoutState, + ignore_block_margins_for_stretch: LogicalSides1D<bool>, + ) -> BoxFragment { + let style = &base.style; + let containing_block_writing_mode = containing_block.style.writing_mode; + let ContentBoxSizesAndPBM { + content_box_sizes, + pbm, + depends_on_block_constraints, + .. + } = self + .layout_style(base) + .content_box_sizes_and_padding_border_margin(&containing_block.into()); + + let (margin_block_start, margin_block_end) = + solve_block_margins_for_in_flow_block_level(&pbm); + let collapsed_margin_block_start = CollapsedMargin::new(margin_block_start); + + // From https://drafts.csswg.org/css2/#floats: + // "The border box of a table, a block-level replaced element, or an element in + // the normal flow that establishes a new block formatting context (such as an + // element with overflow other than visible) must not overlap the margin box of + // any floats in the same block formatting context as the element itself. If + // necessary, implementations should clear the said element by placing it below + // any preceding floats, but may place it adjacent to such floats if there is + // sufficient space. They may even make the border box of said element narrower + // than defined by section 10.3.3. CSS 2 does not define when a UA may put said + // element next to the float or by how much said element may become narrower." + let mut content_size; + let mut layout; + let mut placement_rect; + + // First compute the clear position required by the 'clear' property. + // The code below may then add extra clearance when the element can't fit + // next to floats not covered by 'clear'. + let clear_position = sequential_layout_state.calculate_clear_position( + Clear::from_style_and_container_writing_mode(style, containing_block_writing_mode), + &collapsed_margin_block_start, + ); + let ceiling = clear_position.unwrap_or_else(|| { + sequential_layout_state.position_without_clearance(&collapsed_margin_block_start) + }); + + // Then compute a tentative block size, only taking extrinsic values into account. + let pbm_sums = pbm.sums_auto_is_zero(ignore_block_margins_for_stretch); + let available_block_size = containing_block + .size + .block + .to_definite() + .map(|block_size| Au::zero().max(block_size - pbm_sums.block)); + let (preferred_block_size, min_block_size, max_block_size) = content_box_sizes + .block + .resolve_each_extrinsic(Size::FitContent, Au::zero(), available_block_size); + let tentative_block_size = + SizeConstraint::new(preferred_block_size, min_block_size, max_block_size); + + // With the tentative block size we can compute the inline min/max-content sizes. + let get_inline_content_sizes = || { + let constraint_space = ConstraintSpace::new( + tentative_block_size, + style.writing_mode, + self.preferred_aspect_ratio(), + ); + base.inline_content_sizes(layout_context, &constraint_space, self) + .sizes + }; + + // TODO: the automatic inline size should take `justify-self` into account. + let is_table = self.is_table(); + let automatic_inline_size = if is_table { + Size::FitContent + } else { + Size::Stretch + }; + let compute_inline_size = |stretch_size| { + content_box_sizes.inline.resolve( + Direction::Inline, + automatic_inline_size, + Au::zero, + Some(stretch_size), + get_inline_content_sizes, + is_table, + ) + }; + + let compute_block_size = |layout: &CacheableLayoutResult| { + content_box_sizes.block.resolve( + Direction::Block, + Size::FitContent, + Au::zero, + available_block_size, + || layout.content_block_size.into(), + is_table, + ) + }; + + // The final inline size can depend on the available space, which depends on where + // we are placing the box, since floats reduce the available space. + // Here we assume that `compute_inline_size()` is a monotonically increasing function + // with respect to the available space. Therefore, if we get the same result for 0 + // and for MAX_AU, it means that the function is constant. + // TODO: `compute_inline_size()` may not be monotonic with `calc-size()`. For example, + // `calc-size(stretch, (1px / (size + 1px) + sign(size)) * 1px)` would result in 1px + // both when the available space is zero and infinity, but it's not constant. + let inline_size_with_no_available_space = compute_inline_size(Au::zero()); + if inline_size_with_no_available_space == compute_inline_size(MAX_AU) { + // If the inline size doesn't depend on the available inline space, we can just + // compute it with an available inline space of zero. Then, after layout we can + // compute the block size, and finally place among floats. + let inline_size = inline_size_with_no_available_space; + layout = self.layout( + layout_context, + positioning_context, + &ContainingBlock { + size: ContainingBlockSize { + inline: inline_size, + block: tentative_block_size, + }, + style, + }, + containing_block, + base, + false, /* depends_on_block_constraints */ + ); + + content_size = LogicalVec2 { + block: compute_block_size(&layout), + inline: layout.content_inline_size_for_table.unwrap_or(inline_size), + }; + + let mut placement = PlacementAmongFloats::new( + &sequential_layout_state.floats, + ceiling, + content_size + pbm.padding_border_sums, + &pbm, + ); + placement_rect = placement.place(); + } else { + // If the inline size depends on the available space, then we need to iterate + // the various placement candidates, resolve both the inline and block sizes + // on each one placement area, and then check if the box actually fits it. + // As an optimization, we first compute a lower bound of the final box size, + // and skip placement candidates where not even the lower bound would fit. + let minimum_size_of_block = LogicalVec2 { + // For the lower bound of the inline size, simply assume no available space. + // TODO: this won't work for things like `calc-size(stretch, 100px - size)`, + // which should result in a bigger size when the available space gets smaller. + inline: inline_size_with_no_available_space, + block: match tentative_block_size { + // If we were able to resolve the preferred and maximum block sizes, + // use the tentative block size (it takes the 3 sizes into account). + SizeConstraint::Definite(size) if max_block_size.is_some() => size, + // Oherwise the preferred or maximum block size might end up being zero, + // so can only rely on the minimum block size. + _ => min_block_size, + }, + } + pbm.padding_border_sums; + let mut placement = PlacementAmongFloats::new( + &sequential_layout_state.floats, + ceiling, + minimum_size_of_block, + &pbm, + ); + + loop { + // First try to place the block using the minimum size as the object size. + placement_rect = placement.place(); + let available_inline_size = + placement_rect.size.inline - pbm.padding_border_sums.inline; + let proposed_inline_size = compute_inline_size(available_inline_size); + + // Now lay out the block using the inline size we calculated from the placement. + // Later we'll check to see if the resulting block size is compatible with the + // placement. + let positioning_context_length = positioning_context.len(); + layout = self.layout( + layout_context, + positioning_context, + &ContainingBlock { + size: ContainingBlockSize { + inline: proposed_inline_size, + block: tentative_block_size, + }, + style, + }, + containing_block, + base, + false, /* depends_on_block_constraints */ + ); + + let inline_size = if let Some(inline_size) = layout.content_inline_size_for_table { + // This is a table that ended up being smaller than predicted because of + // collapsed columns. Note we don't backtrack to consider areas that we + // previously thought weren't big enough. + // TODO: Should `minimum_size_of_block.inline` be zero for tables? + debug_assert!(inline_size < proposed_inline_size); + inline_size + } else { + proposed_inline_size + }; + content_size = LogicalVec2 { + block: compute_block_size(&layout), + inline: inline_size, + }; + + // Now we know the block size of this attempted layout of a box with block + // size of auto. Try to fit it into our precalculated placement among the + // floats. If it fits, then we can stop trying layout candidates. + if placement.try_to_expand_for_auto_block_size( + content_size.block + pbm.padding_border_sums.block, + &placement_rect.size, + ) { + break; + } + + // The previous attempt to lay out this independent formatting context + // among the floats did not work, so we must unhoist any boxes from that + // attempt. + positioning_context.truncate(&positioning_context_length); + } + } + + // Only set clearance if we would have cleared or the placement among floats moves + // the block further in the block direction. These two situations are the ones that + // prevent margin collapse. + let has_clearance = clear_position.is_some() || placement_rect.start_corner.block > ceiling; + let clearance = has_clearance.then(|| { + placement_rect.start_corner.block - + sequential_layout_state + .position_with_zero_clearance(&collapsed_margin_block_start) + }); + + let ((margin_inline_start, margin_inline_end), effective_margin_inline_start) = + solve_inline_margins_avoiding_floats( + sequential_layout_state, + containing_block, + &pbm, + content_size.inline + pbm.padding_border_sums.inline, + placement_rect, + ); + + let margin = LogicalSides { + inline_start: margin_inline_start, + inline_end: margin_inline_end, + block_start: margin_block_start, + block_end: margin_block_end, + }; + + // Clearance prevents margin collapse between this block and previous ones, + // so in that case collapse margins before adjoining them below. + if clearance.is_some() { + sequential_layout_state.collapse_margins(); + } + sequential_layout_state.adjoin_assign(&collapsed_margin_block_start); + + // Margins can never collapse into independent formatting contexts. + sequential_layout_state.collapse_margins(); + sequential_layout_state.advance_block_position( + pbm.padding_border_sums.block + content_size.block + clearance.unwrap_or_else(Au::zero), + ); + sequential_layout_state.adjoin_assign(&CollapsedMargin::new(margin.block_end)); + + let content_rect = LogicalRect { + start_corner: LogicalVec2 { + block: pbm.padding.block_start + + pbm.border.block_start + + clearance.unwrap_or_else(Au::zero), + inline: pbm.padding.inline_start + + pbm.border.inline_start + + effective_margin_inline_start, + }, + size: content_size, + }; + + let mut base_fragment_info = base.base_fragment_info; + if depends_on_block_constraints { + base_fragment_info.flags.insert( + FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM, + ); + } + + BoxFragment::new( + base_fragment_info, + style.clone(), + layout.fragments, + content_rect.as_physical(Some(containing_block)), + pbm.padding.to_physical(containing_block_writing_mode), + pbm.border.to_physical(containing_block_writing_mode), + margin.to_physical(containing_block_writing_mode), + clearance, + ) + .with_baselines(layout.baselines) + .with_specific_layout_info(layout.specific_layout_info) + .with_block_margins_collapsed_with_children(CollapsedBlockMargins::from_margin(&margin)) + } +} + +impl ReplacedContents { + /// <https://drafts.csswg.org/css2/visudet.html#block-replaced-width> + /// <https://drafts.csswg.org/css2/visudet.html#inline-replaced-width> + /// <https://drafts.csswg.org/css2/visudet.html#inline-replaced-height> + fn layout_in_flow_block_level( + &self, + base: &LayoutBoxBase, + layout_context: &LayoutContext, + containing_block: &ContainingBlock, + mut sequential_layout_state: Option<&mut SequentialLayoutState>, + ignore_block_margins_for_stretch: LogicalSides1D<bool>, + ) -> BoxFragment { + let content_box_sizes_and_pbm = self + .layout_style(base) + .content_box_sizes_and_padding_border_margin(&containing_block.into()); + let pbm = &content_box_sizes_and_pbm.pbm; + let content_size = self.used_size_as_if_inline_element( + containing_block, + &base.style, + &content_box_sizes_and_pbm, + ignore_block_margins_for_stretch, + ); + + let margin_inline_start; + let margin_inline_end; + let effective_margin_inline_start; + let (margin_block_start, margin_block_end) = + solve_block_margins_for_in_flow_block_level(pbm); + + let containing_block_writing_mode = containing_block.style.writing_mode; + let physical_content_size = content_size.to_physical_size(containing_block_writing_mode); + let fragments = self.make_fragments(layout_context, &base.style, physical_content_size); + + let clearance; + if let Some(ref mut sequential_layout_state) = sequential_layout_state { + // From https://drafts.csswg.org/css2/#floats: + // "The border box of a table, a block-level replaced element, or an element in + // the normal flow that establishes a new block formatting context (such as an + // element with overflow other than visible) must not overlap the margin box of + // any floats in the same block formatting context as the element itself. If + // necessary, implementations should clear the said element by placing it below + // any preceding floats, but may place it adjacent to such floats if there is + // sufficient space. They may even make the border box of said element narrower + // than defined by section 10.3.3. CSS 2 does not define when a UA may put said + // element next to the float or by how much said element may become narrower." + let collapsed_margin_block_start = CollapsedMargin::new(margin_block_start); + let size = content_size + pbm.padding_border_sums; + let placement_rect; + (clearance, placement_rect) = sequential_layout_state + .calculate_clearance_and_inline_adjustment( + Clear::from_style_and_container_writing_mode( + &base.style, + containing_block.style.writing_mode, + ), + &collapsed_margin_block_start, + pbm, + size, + ); + ( + (margin_inline_start, margin_inline_end), + effective_margin_inline_start, + ) = solve_inline_margins_avoiding_floats( + sequential_layout_state, + containing_block, + pbm, + size.inline, + placement_rect, + ); + + // Clearance prevents margin collapse between this block and previous ones, + // so in that case collapse margins before adjoining them below. + if clearance.is_some() { + sequential_layout_state.collapse_margins(); + } + sequential_layout_state.adjoin_assign(&collapsed_margin_block_start); + + // Margins can never collapse into replaced elements. + sequential_layout_state.collapse_margins(); + sequential_layout_state + .advance_block_position(size.block + clearance.unwrap_or_else(Au::zero)); + sequential_layout_state.adjoin_assign(&CollapsedMargin::new(margin_block_end)); + } else { + clearance = None; + ( + (margin_inline_start, margin_inline_end), + effective_margin_inline_start, + ) = solve_inline_margins_for_in_flow_block_level( + containing_block, + pbm, + content_size.inline, + ); + }; + + let margin = LogicalSides { + inline_start: margin_inline_start, + inline_end: margin_inline_end, + block_start: margin_block_start, + block_end: margin_block_end, + }; + + let start_corner = LogicalVec2 { + block: pbm.padding.block_start + + pbm.border.block_start + + clearance.unwrap_or_else(Au::zero), + inline: pbm.padding.inline_start + + pbm.border.inline_start + + effective_margin_inline_start, + }; + let content_rect = LogicalRect { + start_corner, + size: content_size, + } + .as_physical(Some(containing_block)); + + let mut base_fragment_info = base.base_fragment_info; + if content_box_sizes_and_pbm.depends_on_block_constraints { + base_fragment_info.flags.insert( + FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM, + ); + } + + BoxFragment::new( + base_fragment_info, + base.style.clone(), + fragments, + content_rect, + pbm.padding.to_physical(containing_block_writing_mode), + pbm.border.to_physical(containing_block_writing_mode), + margin.to_physical(containing_block_writing_mode), + clearance, + ) + .with_block_margins_collapsed_with_children(CollapsedBlockMargins::from_margin(&margin)) + } +} + +struct ContainingBlockPaddingAndBorder<'a> { + containing_block: ContainingBlock<'a>, + pbm: PaddingBorderMargin, + block_sizes: Sizes, + depends_on_block_constraints: bool, + available_block_size: Option<Au>, +} + +struct ResolvedMargins { + /// Used value for the margin properties, as exposed in getComputedStyle(). + pub margin: LogicalSides<Au>, + + /// Distance between the border box and the containing block on the inline-start side. + /// This is typically the same as the inline-start margin, but can be greater when + /// the box is justified within the free space in the containing block. + /// The reason we aren't just adjusting the used margin-inline-start is that + /// this shouldn't be observable via getComputedStyle(). + /// <https://drafts.csswg.org/css-align/#justify-self-property> + pub effective_margin_inline_start: Au, +} + +/// Given the style for an in-flow box and its containing block, determine the containing +/// block for its children. +/// Note that in the presence of floats, this shouldn't be used for a block-level box +/// that establishes an independent formatting context (or is replaced), since the +/// inline size could then be incorrect. +fn solve_containing_block_padding_and_border_for_in_flow_box<'a>( + containing_block: &ContainingBlock<'_>, + layout_style: &'a LayoutStyle, + get_inline_content_sizes: impl FnOnce(&ConstraintSpace) -> ContentSizes, + ignore_block_margins_for_stretch: LogicalSides1D<bool>, +) -> ContainingBlockPaddingAndBorder<'a> { + let style = layout_style.style(); + if matches!(style.pseudo(), Some(PseudoElement::ServoAnonymousBox)) { + // <https://drafts.csswg.org/css2/#anonymous-block-level> + // > Anonymous block boxes are ignored when resolving percentage values that would + // > refer to it: the closest non-anonymous ancestor box is used instead. + let containing_block_for_children = ContainingBlock { + size: ContainingBlockSize { + inline: containing_block.size.inline, + block: containing_block.size.block, + }, + style, + }; + // <https://drafts.csswg.org/css2/#anonymous-block-level> + // > Non-inherited properties have their initial value. + return ContainingBlockPaddingAndBorder { + containing_block: containing_block_for_children, + pbm: PaddingBorderMargin::zero(), + block_sizes: Sizes::default(), + depends_on_block_constraints: false, + // The available block size may actually be definite, but it should be irrelevant + // since the sizing properties are set to their initial value. + available_block_size: None, + }; + } + + let ContentBoxSizesAndPBM { + content_box_sizes, + pbm, + depends_on_block_constraints, + .. + } = layout_style.content_box_sizes_and_padding_border_margin(&containing_block.into()); + + let pbm_sums = pbm.sums_auto_is_zero(ignore_block_margins_for_stretch); + let writing_mode = style.writing_mode; + let available_inline_size = Au::zero().max(containing_block.size.inline - pbm_sums.inline); + let available_block_size = containing_block + .size + .block + .to_definite() + .map(|block_size| Au::zero().max(block_size - pbm_sums.block)); + + // https://drafts.csswg.org/css2/#the-height-property + // https://drafts.csswg.org/css2/visudet.html#min-max-heights + let tentative_block_size = content_box_sizes.block.resolve_extrinsic( + Size::FitContent, + Au::zero(), + available_block_size, + ); + + // https://drafts.csswg.org/css2/#the-width-property + // https://drafts.csswg.org/css2/visudet.html#min-max-widths + let get_inline_content_sizes = || { + get_inline_content_sizes(&ConstraintSpace::new( + tentative_block_size, + writing_mode, + None, /* TODO: support preferred aspect ratios on non-replaced boxes */ + )) + }; + // TODO: the automatic inline size should take `justify-self` into account. + let is_table = layout_style.is_table(); + let automatic_inline_size = if is_table { + Size::FitContent + } else { + Size::Stretch + }; + let inline_size = content_box_sizes.inline.resolve( + Direction::Inline, + automatic_inline_size, + Au::zero, + Some(available_inline_size), + get_inline_content_sizes, + is_table, + ); + + let containing_block_for_children = ContainingBlock { + size: ContainingBlockSize { + inline: inline_size, + block: tentative_block_size, + }, + style, + }; + // https://drafts.csswg.org/css-writing-modes/#orthogonal-flows + assert_eq!( + containing_block.style.writing_mode.is_horizontal(), + containing_block_for_children + .style + .writing_mode + .is_horizontal(), + "Vertical writing modes are not supported yet" + ); + ContainingBlockPaddingAndBorder { + containing_block: containing_block_for_children, + pbm, + block_sizes: content_box_sizes.block, + depends_on_block_constraints, + available_block_size, + } +} + +/// Given the containing block and size of an in-flow box, determine the margins. +/// Note that in the presence of floats, this shouldn't be used for a block-level box +/// that establishes an independent formatting context (or is replaced), since the +/// margins could then be incorrect. +fn solve_margins( + containing_block: &ContainingBlock<'_>, + pbm: &PaddingBorderMargin, + inline_size: Au, +) -> ResolvedMargins { + let (inline_margins, effective_margin_inline_start) = + solve_inline_margins_for_in_flow_block_level(containing_block, pbm, inline_size); + let block_margins = solve_block_margins_for_in_flow_block_level(pbm); + ResolvedMargins { + margin: LogicalSides { + inline_start: inline_margins.0, + inline_end: inline_margins.1, + block_start: block_margins.0, + block_end: block_margins.1, + }, + effective_margin_inline_start, + } +} + +/// Resolves 'auto' margins of an in-flow block-level box in the block axis. +/// <https://drafts.csswg.org/css2/#normal-block> +/// <https://drafts.csswg.org/css2/#block-root-margin> +fn solve_block_margins_for_in_flow_block_level(pbm: &PaddingBorderMargin) -> (Au, Au) { + ( + pbm.margin.block_start.auto_is(Au::zero), + pbm.margin.block_end.auto_is(Au::zero), + ) +} + +/// This is supposed to handle 'justify-self', but no browser supports it on block boxes. +/// Instead, `<center>` and `<div align>` are implemented via internal 'text-align' values. +/// The provided free space should already take margins into account. In particular, +/// it should be zero if there is an auto margin. +/// <https://drafts.csswg.org/css-align/#justify-block> +fn justify_self_alignment(containing_block: &ContainingBlock, free_space: Au) -> Au { + let style = containing_block.style; + debug_assert!(free_space >= Au::zero()); + match style.clone_text_align() { + TextAlignKeyword::MozCenter => free_space / 2, + TextAlignKeyword::MozLeft if !style.writing_mode.line_left_is_inline_start() => free_space, + TextAlignKeyword::MozRight if style.writing_mode.line_left_is_inline_start() => free_space, + _ => Au::zero(), + } +} + +/// Resolves 'auto' margins of an in-flow block-level box in the inline axis, +/// distributing the free space in the containing block. +/// +/// This is based on CSS2.1 § 10.3.3 <https://drafts.csswg.org/css2/#blockwidth> +/// but without adjusting the margins in "over-contrained" cases, as mandated by +/// <https://drafts.csswg.org/css-align/#justify-block>. +/// +/// Note that in the presence of floats, this shouldn't be used for a block-level box +/// that establishes an independent formatting context (or is replaced). +/// +/// In addition to the used margins, it also returns the effective margin-inline-start +/// (see ContainingBlockPaddingAndBorder). +fn solve_inline_margins_for_in_flow_block_level( + containing_block: &ContainingBlock, + pbm: &PaddingBorderMargin, + inline_size: Au, +) -> ((Au, Au), Au) { + let free_space = containing_block.size.inline - pbm.padding_border_sums.inline - inline_size; + let mut justification = Au::zero(); + let inline_margins = match (pbm.margin.inline_start, pbm.margin.inline_end) { + (AuOrAuto::Auto, AuOrAuto::Auto) => { + let start = Au::zero().max(free_space / 2); + (start, free_space - start) + }, + (AuOrAuto::Auto, AuOrAuto::LengthPercentage(end)) => { + (Au::zero().max(free_space - end), end) + }, + (AuOrAuto::LengthPercentage(start), AuOrAuto::Auto) => (start, free_space - start), + (AuOrAuto::LengthPercentage(start), AuOrAuto::LengthPercentage(end)) => { + // In the cases above, the free space is zero after taking 'auto' margins into account. + // But here we may still have some free space to perform 'justify-self' alignment. + // This aligns the margin box within the containing block, or in other words, + // aligns the border box within the margin-shrunken containing block. + let free_space = Au::zero().max(free_space - start - end); + justification = justify_self_alignment(containing_block, free_space); + (start, end) + }, + }; + let effective_margin_inline_start = inline_margins.0 + justification; + (inline_margins, effective_margin_inline_start) +} + +/// Resolves 'auto' margins of an in-flow block-level box in the inline axis +/// similarly to |solve_inline_margins_for_in_flow_block_level|. However, +/// they align within the provided rect (instead of the containing block), +/// to avoid overlapping floats. +/// In addition to the used margins, it also returns the effective +/// margin-inline-start (see ContainingBlockPaddingAndBorder). +/// It may differ from the used inline-start margin if the computed value +/// wasn't 'auto' and there are floats to avoid or the box is justified. +/// See <https://github.com/w3c/csswg-drafts/issues/9174> +fn solve_inline_margins_avoiding_floats( + sequential_layout_state: &SequentialLayoutState, + containing_block: &ContainingBlock, + pbm: &PaddingBorderMargin, + inline_size: Au, + placement_rect: LogicalRect<Au>, +) -> ((Au, Au), Au) { + let free_space = placement_rect.size.inline - inline_size; + debug_assert!(free_space >= Au::zero()); + let cb_info = &sequential_layout_state.floats.containing_block_info; + let start_adjustment = placement_rect.start_corner.inline - cb_info.inline_start; + let end_adjustment = cb_info.inline_end - placement_rect.max_inline_position(); + let mut justification = Au::zero(); + let inline_margins = match (pbm.margin.inline_start, pbm.margin.inline_end) { + (AuOrAuto::Auto, AuOrAuto::Auto) => { + let half = free_space / 2; + (start_adjustment + half, end_adjustment + free_space - half) + }, + (AuOrAuto::Auto, AuOrAuto::LengthPercentage(end)) => (start_adjustment + free_space, end), + (AuOrAuto::LengthPercentage(start), AuOrAuto::Auto) => (start, end_adjustment + free_space), + (AuOrAuto::LengthPercentage(start), AuOrAuto::LengthPercentage(end)) => { + // The spec says 'justify-self' aligns the margin box within the float-shrunken + // containing block. That's wrong (https://github.com/w3c/csswg-drafts/issues/9963), + // and Blink and WebKit are broken anyways. So we match Gecko instead: this aligns + // the border box within the instersection of the float-shrunken containing-block + // and the margin-shrunken containing-block. + justification = justify_self_alignment(containing_block, free_space); + (start, end) + }, + }; + let effective_margin_inline_start = inline_margins.0.max(start_adjustment) + justification; + (inline_margins, effective_margin_inline_start) +} + +/// State that we maintain when placing blocks. +/// +/// In parallel mode, this placement is done after all child blocks are laid out. In +/// sequential mode, this is done right after each block is laid out. +struct PlacementState<'container> { + next_in_flow_margin_collapses_with_parent_start_margin: bool, + last_in_flow_margin_collapses_with_parent_end_margin: bool, + start_margin: CollapsedMargin, + current_margin: CollapsedMargin, + current_block_direction_position: Au, + inflow_baselines: Baselines, + is_inline_block_context: bool, + + /// If this [`PlacementState`] is laying out a list item with an outside marker. Record the + /// block size of that marker, because the content block size of the list item needs to be at + /// least as tall as the marker size -- even though the marker doesn't advance the block + /// position of the placement. + marker_block_size: Option<Au>, + + /// The [`ContainingBlock`] of the container into which this [`PlacementState`] is laying out + /// fragments. This is used to convert between physical and logical geometry. + containing_block: &'container ContainingBlock<'container>, +} + +impl<'container> PlacementState<'container> { + fn new( + collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin, + containing_block: &'container ContainingBlock<'container>, + ) -> PlacementState<'container> { + let is_inline_block_context = + containing_block.style.get_box().clone_display() == Display::InlineBlock; + PlacementState { + next_in_flow_margin_collapses_with_parent_start_margin: + collapsible_with_parent_start_margin.0, + last_in_flow_margin_collapses_with_parent_end_margin: true, + start_margin: CollapsedMargin::zero(), + current_margin: CollapsedMargin::zero(), + current_block_direction_position: Au::zero(), + inflow_baselines: Baselines::default(), + is_inline_block_context, + marker_block_size: None, + containing_block, + } + } + + fn place_fragment_and_update_baseline( + &mut self, + fragment: &mut Fragment, + sequential_layout_state: Option<&mut SequentialLayoutState>, + ) { + self.place_fragment(fragment, sequential_layout_state); + + let box_fragment = match fragment { + Fragment::Box(box_fragment) => box_fragment, + _ => return, + }; + let box_fragment = box_fragment.borrow(); + + // From <https://drafts.csswg.org/css-align-3/#baseline-export>: + // > When finding the first/last baseline set of an inline-block, any baselines + // > contributed by table boxes must be skipped. (This quirk is a legacy behavior from + // > [CSS2].) + if self.is_inline_block_context && box_fragment.is_table_wrapper() { + return; + } + + let box_block_offset = box_fragment + .content_rect + .origin + .to_logical(self.containing_block) + .block; + let box_fragment_baselines = + box_fragment.baselines(self.containing_block.style.writing_mode); + if let (None, Some(first)) = (self.inflow_baselines.first, box_fragment_baselines.first) { + self.inflow_baselines.first = Some(first + box_block_offset); + } + if let Some(last) = box_fragment_baselines.last { + self.inflow_baselines.last = Some(last + box_block_offset); + } + } + + /// Place a single [Fragment] in a block level context using the state so far and + /// information gathered from the [Fragment] itself. + fn place_fragment( + &mut self, + fragment: &mut Fragment, + sequential_layout_state: Option<&mut SequentialLayoutState>, + ) { + match fragment { + Fragment::Box(fragment) => { + // If this child is a marker positioned outside of a list item, then record its + // size, but also ensure that it doesn't advance the block position of the placment. + // This ensures item content is placed next to the marker. + // + // This is a pretty big hack because it doesn't properly handle all interactions + // between the marker and the item. For instance the marker should be positioned at + // the baseline of list item content and the first line of the item content should + // be at least as tall as the marker -- not the entire list item itself. + let fragment = &mut *fragment.borrow_mut(); + let is_outside_marker = fragment + .base + .flags + .contains(FragmentFlags::IS_OUTSIDE_LIST_ITEM_MARKER); + if is_outside_marker { + assert!(self.marker_block_size.is_none()); + self.marker_block_size = Some( + fragment + .content_rect + .size + .to_logical(self.containing_block.style.writing_mode) + .block, + ); + return; + } + + let fragment_block_margins = fragment.block_margins_collapsed_with_children(); + let mut fragment_block_size = fragment + .border_rect() + .size + .to_logical(self.containing_block.style.writing_mode) + .block; + + // We use `last_in_flow_margin_collapses_with_parent_end_margin` to implement + // this quote from https://drafts.csswg.org/css2/#collapsing-margins + // > If the top and bottom margins of an element with clearance are adjoining, + // > its margins collapse with the adjoining margins of following siblings but that + // > resulting margin does not collapse with the bottom margin of the parent block. + if let Some(clearance) = fragment.clearance { + fragment_block_size += clearance; + // Margins can't be adjoining if they are separated by clearance. + // Setting `next_in_flow_margin_collapses_with_parent_start_margin` to false + // prevents collapsing with the start margin of the parent, and will set + // `collapsed_through` to false, preventing the parent from collapsing through. + self.current_block_direction_position += self.current_margin.solve(); + self.current_margin = CollapsedMargin::zero(); + self.next_in_flow_margin_collapses_with_parent_start_margin = false; + if fragment_block_margins.collapsed_through { + self.last_in_flow_margin_collapses_with_parent_end_margin = false; + } + } else if !fragment_block_margins.collapsed_through { + self.last_in_flow_margin_collapses_with_parent_end_margin = true; + } + + if self.next_in_flow_margin_collapses_with_parent_start_margin { + debug_assert!(self.current_margin.solve().is_zero()); + self.start_margin + .adjoin_assign(&fragment_block_margins.start); + if fragment_block_margins.collapsed_through { + self.start_margin.adjoin_assign(&fragment_block_margins.end); + return; + } + self.next_in_flow_margin_collapses_with_parent_start_margin = false; + } else { + self.current_margin + .adjoin_assign(&fragment_block_margins.start); + } + + fragment.content_rect.origin += LogicalVec2 { + inline: Au::zero(), + block: self.current_margin.solve() + self.current_block_direction_position, + } + .to_physical_size(self.containing_block.style.writing_mode); + + if fragment_block_margins.collapsed_through { + // `fragment_block_size` is typically zero when collapsing through, + // but we still need to consider it in case there is clearance. + self.current_block_direction_position += fragment_block_size; + self.current_margin + .adjoin_assign(&fragment_block_margins.end); + } else { + self.current_block_direction_position += + self.current_margin.solve() + fragment_block_size; + self.current_margin = fragment_block_margins.end; + } + }, + Fragment::AbsoluteOrFixedPositioned(fragment) => { + // The alignment of absolutes in block flow layout is always "start", so the size of + // the static position rectangle does not matter. + fragment.borrow_mut().static_position_rect = LogicalRect { + start_corner: LogicalVec2 { + block: (self.current_margin.solve() + + self.current_block_direction_position), + inline: Au::zero(), + }, + size: LogicalVec2::zero(), + } + .as_physical(Some(self.containing_block)); + }, + Fragment::Float(box_fragment) => { + let sequential_layout_state = sequential_layout_state + .expect("Found float fragment without SequentialLayoutState"); + let block_offset_from_containing_block_top = + self.current_block_direction_position + self.current_margin.solve(); + let box_fragment = &mut *box_fragment.borrow_mut(); + sequential_layout_state.place_float_fragment( + box_fragment, + self.containing_block, + self.start_margin, + block_offset_from_containing_block_top, + ); + }, + Fragment::Positioning(_) => {}, + _ => unreachable!(), + } + } + + fn finish(mut self) -> (Au, CollapsedBlockMargins, Baselines) { + if !self.last_in_flow_margin_collapses_with_parent_end_margin { + self.current_block_direction_position += self.current_margin.solve(); + self.current_margin = CollapsedMargin::zero(); + } + let (total_block_size, collapsed_through) = match self.marker_block_size { + Some(marker_block_size) => ( + self.current_block_direction_position.max(marker_block_size), + // If this is a list item (even empty) with an outside marker, then it + // should not collapse through. + false, + ), + None => ( + self.current_block_direction_position, + self.next_in_flow_margin_collapses_with_parent_start_margin, + ), + }; + + ( + total_block_size, + CollapsedBlockMargins { + collapsed_through, + start: self.start_margin, + end: self.current_margin, + }, + self.inflow_baselines, + ) + } +} + +fn block_size_is_zero_or_intrinsic(size: &StyleSize, containing_block: &ContainingBlock) -> bool { + match size { + StyleSize::Auto | + StyleSize::MinContent | + StyleSize::MaxContent | + StyleSize::FitContent | + StyleSize::FitContentFunction(_) => true, + StyleSize::Stretch => { + // TODO: Should this return true when the containing block has a definite size of 0px? + !containing_block.size.block.is_definite() + }, + StyleSize::LengthPercentage(lp) => { + // TODO: Should this resolve definite percentages? Blink does it, Gecko and WebKit don't. + lp.is_definitely_zero() || + (lp.0.has_percentage() && !containing_block.size.block.is_definite()) + }, + StyleSize::AnchorSizeFunction(_) => unreachable!("anchor-size() should be disabled"), + } +} + +pub(crate) struct IndependentFloatOrAtomicLayoutResult { + pub fragment: BoxFragment, + pub baselines: Option<Baselines>, + pub pbm_sums: LogicalSides<Au>, +} + +impl IndependentFormattingContext { + pub(crate) fn layout_in_flow_block_level( + &self, + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + containing_block: &ContainingBlock, + sequential_layout_state: Option<&mut SequentialLayoutState>, + ignore_block_margins_for_stretch: LogicalSides1D<bool>, + ) -> BoxFragment { + match &self.contents { + IndependentFormattingContextContents::NonReplaced(contents) => contents + .layout_in_flow_block_level( + &self.base, + layout_context, + positioning_context, + containing_block, + sequential_layout_state, + ignore_block_margins_for_stretch, + ), + IndependentFormattingContextContents::Replaced(contents) => contents + .layout_in_flow_block_level( + &self.base, + layout_context, + containing_block, + sequential_layout_state, + ignore_block_margins_for_stretch, + ), + } + } + pub(crate) fn layout_float_or_atomic_inline( + &self, + layout_context: &LayoutContext, + child_positioning_context: &mut PositioningContext, + containing_block: &ContainingBlock, + ) -> IndependentFloatOrAtomicLayoutResult { + let style = self.style(); + let container_writing_mode = containing_block.style.writing_mode; + let layout_style = self.layout_style(); + let content_box_sizes_and_pbm = + layout_style.content_box_sizes_and_padding_border_margin(&containing_block.into()); + let pbm = &content_box_sizes_and_pbm.pbm; + let margin = pbm.margin.auto_is(Au::zero); + let pbm_sums = pbm.padding + pbm.border + margin; + + let (fragments, content_rect, baselines) = match &self.contents { + IndependentFormattingContextContents::Replaced(replaced) => { + // Floats and atomic inlines can't collapse margins with their parent, + // so don't ignore block margins when resolving a stretch block size. + // https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing + let ignore_block_margins_for_stretch = LogicalSides1D::new(false, false); + + // https://drafts.csswg.org/css2/visudet.html#float-replaced-width + // https://drafts.csswg.org/css2/visudet.html#inline-replaced-height + let content_size = replaced + .used_size_as_if_inline_element( + containing_block, + style, + &content_box_sizes_and_pbm, + ignore_block_margins_for_stretch, + ) + .to_physical_size(container_writing_mode); + let fragments = replaced.make_fragments(layout_context, style, content_size); + + let content_rect = PhysicalRect::new(PhysicalPoint::zero(), content_size); + (fragments, content_rect, None) + }, + IndependentFormattingContextContents::NonReplaced(non_replaced) => { + let writing_mode = self.style().writing_mode; + let available_inline_size = + Au::zero().max(containing_block.size.inline - pbm_sums.inline_sum()); + let available_block_size = containing_block + .size + .block + .to_definite() + .map(|block_size| Au::zero().max(block_size - pbm_sums.block_sum())); + let tentative_block_size = content_box_sizes_and_pbm + .content_box_sizes + .block + .resolve_extrinsic(Size::FitContent, Au::zero(), available_block_size); + + let get_content_size = || { + let constraint_space = ConstraintSpace::new( + tentative_block_size, + writing_mode, + non_replaced.preferred_aspect_ratio(), + ); + self.inline_content_sizes(layout_context, &constraint_space) + .sizes + }; + + let is_table = layout_style.is_table(); + let inline_size = content_box_sizes_and_pbm.content_box_sizes.inline.resolve( + Direction::Inline, + Size::FitContent, + Au::zero, + Some(available_inline_size), + get_content_size, + is_table, + ); + + let containing_block_for_children = ContainingBlock { + size: ContainingBlockSize { + inline: inline_size, + block: tentative_block_size, + }, + style: self.style(), + }; + assert_eq!( + container_writing_mode.is_horizontal(), + writing_mode.is_horizontal(), + "Mixed horizontal and vertical writing modes are not supported yet" + ); + + let independent_layout = non_replaced.layout( + layout_context, + child_positioning_context, + &containing_block_for_children, + containing_block, + &self.base, + false, /* depends_on_block_constraints */ + ); + let inline_size = independent_layout + .content_inline_size_for_table + .unwrap_or(inline_size); + let block_size = content_box_sizes_and_pbm.content_box_sizes.block.resolve( + Direction::Block, + Size::FitContent, + Au::zero, + available_block_size, + || independent_layout.content_block_size.into(), + is_table, + ); + + let content_size = LogicalVec2 { + block: block_size, + inline: inline_size, + } + .to_physical_size(container_writing_mode); + let content_rect = PhysicalRect::new(PhysicalPoint::zero(), content_size); + + ( + independent_layout.fragments, + content_rect, + Some(independent_layout.baselines), + ) + }, + }; + + let mut base_fragment_info = self.base_fragment_info(); + if content_box_sizes_and_pbm.depends_on_block_constraints { + base_fragment_info.flags.insert( + FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM, + ); + } + + let fragment = BoxFragment::new( + base_fragment_info, + self.style().clone(), + fragments, + content_rect, + pbm.padding.to_physical(container_writing_mode), + pbm.border.to_physical(container_writing_mode), + margin.to_physical(container_writing_mode), + // Floats can have clearance, but it's handled internally by the float placement logic, + // so there's no need to store it explicitly in the fragment. + // And atomic inlines don't have clearance. + None, /* clearance */ + ); + + IndependentFloatOrAtomicLayoutResult { + fragment, + baselines, + pbm_sums, + } + } +} diff --git a/components/layout/flow/root.rs b/components/layout/flow/root.rs new file mode 100644 index 00000000000..390b4664e60 --- /dev/null +++ b/components/layout/flow/root.rs @@ -0,0 +1,500 @@ +/* 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 atomic_refcell::AtomicRef; +use compositing_traits::display_list::AxesScrollSensitivity; +use malloc_size_of_derive::MallocSizeOf; +use script_layout_interface::wrapper_traits::{ + LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode, +}; +use script_layout_interface::{LayoutElementType, LayoutNodeType}; +use servo_arc::Arc; +use style::dom::OpaqueNode; +use style::properties::ComputedValues; +use style::values::computed::Overflow; +use style_traits::CSSPixel; + +use crate::cell::ArcRefCell; +use crate::context::LayoutContext; +use crate::dom::{LayoutBox, NodeExt}; +use crate::dom_traversal::{Contents, NodeAndStyleInfo, NonReplacedContents, iter_child_nodes}; +use crate::flexbox::FlexLevelBox; +use crate::flow::float::FloatBox; +use crate::flow::inline::InlineItem; +use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox}; +use crate::formatting_contexts::IndependentFormattingContext; +use crate::fragment_tree::FragmentTree; +use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize}; +use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; +use crate::replaced::ReplacedContents; +use crate::style_ext::{ComputedValuesExt, Display, DisplayGeneratingBox, DisplayInside}; +use crate::taffy::{TaffyItemBox, TaffyItemBoxInner}; +use crate::{DefiniteContainingBlock, PropagatedBoxTreeData}; + +#[derive(MallocSizeOf)] +pub struct BoxTree { + /// Contains typically exactly one block-level box, which was generated by the root element. + /// There may be zero if that element has `display: none`. + root: BlockFormattingContext, + + /// <https://drafts.csswg.org/css-backgrounds/#special-backgrounds> + canvas_background: CanvasBackground, + + /// Whether or not the viewport should be sensitive to scrolling input events in two axes + viewport_scroll_sensitivity: AxesScrollSensitivity, +} + +impl BoxTree { + pub fn construct<'dom, Node>(context: &LayoutContext, root_element: Node) -> Self + where + Node: 'dom + Copy + LayoutNode<'dom> + Send + Sync, + { + let boxes = construct_for_root_element(context, root_element); + + // Zero box for `:root { display: none }`, one for the root element otherwise. + assert!(boxes.len() <= 1); + + // From https://www.w3.org/TR/css-overflow-3/#overflow-propagation: + // > UAs must apply the overflow-* values set on the root element to the viewport when the + // > root element’s display value is not none. However, when the root element is an [HTML] + // > html element (including XML syntax for HTML) whose overflow value is visible (in both + // > axes), and that element has as a child a body element whose display value is also not + // > none, user agents must instead apply the overflow-* values of the first such child + // > element to the viewport. The element from which the value is propagated must then have a + // > used overflow value of visible. + let root_style = root_element.style(context); + + let mut viewport_overflow_x = root_style.clone_overflow_x(); + let mut viewport_overflow_y = root_style.clone_overflow_y(); + if viewport_overflow_x == Overflow::Visible && + viewport_overflow_y == Overflow::Visible && + !root_style.get_box().display.is_none() + { + for child in iter_child_nodes(root_element) { + if !child + .to_threadsafe() + .as_element() + .is_some_and(|element| element.is_body_element_of_html_element_root()) + { + continue; + } + + let style = child.style(context); + if !style.get_box().display.is_none() { + viewport_overflow_x = style.clone_overflow_x(); + viewport_overflow_y = style.clone_overflow_y(); + + break; + } + } + } + + let contents = BlockContainer::BlockLevelBoxes(boxes); + let contains_floats = contents.contains_floats(); + Self { + root: BlockFormattingContext { + contents, + contains_floats, + }, + canvas_background: CanvasBackground::for_root_element(context, root_element), + // From https://www.w3.org/TR/css-overflow-3/#overflow-propagation: + // > If visible is applied to the viewport, it must be interpreted as auto. + // > If clip is applied to the viewport, it must be interpreted as hidden. + viewport_scroll_sensitivity: AxesScrollSensitivity { + x: viewport_overflow_x.to_scrollable().into(), + y: viewport_overflow_y.to_scrollable().into(), + }, + } + } + + /// This method attempts to incrementally update the box tree from an + /// arbitrary node that is not necessarily the document's root element. + /// + /// If the node is not a valid candidate for incremental update, the method + /// loops over its parent. The only valid candidates for now are absolutely + /// positioned boxes which don't change their outside display mode (i.e. it + /// will not attempt to update from an absolutely positioned inline element + /// which became an absolutely positioned block element). The value `true` + /// is returned if an incremental update could be done, and `false` + /// otherwise. + /// + /// There are various pain points that need to be taken care of to extend + /// the set of valid candidates: + /// * it is not obvious how to incrementally check whether a block + /// formatting context still contains floats or not; + /// * the propagation of text decorations towards node descendants is + /// hard to do incrementally with our current representation of boxes + /// * how intrinsic content sizes are computed eagerly makes it hard + /// to update those sizes for ancestors of the node from which we + /// made an incremental update. + pub fn update<'dom, Node>(context: &LayoutContext, mut dirty_node: Node) -> bool + where + Node: 'dom + Copy + LayoutNode<'dom> + Send + Sync, + { + #[allow(clippy::enum_variant_names)] + enum UpdatePoint { + AbsolutelyPositionedBlockLevelBox(ArcRefCell<BlockLevelBox>), + AbsolutelyPositionedInlineLevelBox(ArcRefCell<InlineItem>, usize), + AbsolutelyPositionedFlexLevelBox(ArcRefCell<FlexLevelBox>), + AbsolutelyPositionedTaffyLevelBox(ArcRefCell<TaffyItemBox>), + } + + fn update_point<'dom, Node>( + node: Node, + ) -> Option<(Arc<ComputedValues>, DisplayInside, UpdatePoint)> + where + Node: NodeExt<'dom>, + { + if !node.is_element() { + return None; + } + + if node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLBodyElement) { + // This can require changes to the canvas background. + return None; + } + + // Don't update unstyled nodes or nodes that have pseudo-elements. + let element_data = node.style_data()?.element_data.borrow(); + if !element_data.styles.pseudos.is_empty() { + return None; + } + + let layout_data = node.layout_data()?; + if layout_data.pseudo_before_box.borrow().is_some() { + return None; + } + if layout_data.pseudo_after_box.borrow().is_some() { + return None; + } + + let primary_style = element_data.styles.primary(); + let box_style = primary_style.get_box(); + + if !box_style.position.is_absolutely_positioned() { + return None; + } + + let display_inside = match Display::from(box_style.display) { + Display::GeneratingBox(DisplayGeneratingBox::OutsideInside { inside, .. }) => { + inside + }, + _ => return None, + }; + + let update_point = + match &*AtomicRef::filter_map(layout_data.self_box.borrow(), Option::as_ref)? { + LayoutBox::DisplayContents => return None, + LayoutBox::BlockLevel(block_level_box) => match &*block_level_box.borrow() { + BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_) + if box_style.position.is_absolutely_positioned() => + { + UpdatePoint::AbsolutelyPositionedBlockLevelBox(block_level_box.clone()) + }, + _ => return None, + }, + LayoutBox::InlineLevel(inline_level_box) => match &*inline_level_box.borrow() { + InlineItem::OutOfFlowAbsolutelyPositionedBox(_, text_offset_index) + if box_style.position.is_absolutely_positioned() => + { + UpdatePoint::AbsolutelyPositionedInlineLevelBox( + inline_level_box.clone(), + *text_offset_index, + ) + }, + _ => return None, + }, + LayoutBox::FlexLevel(flex_level_box) => match &*flex_level_box.borrow() { + FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(_) + if box_style.position.is_absolutely_positioned() => + { + UpdatePoint::AbsolutelyPositionedFlexLevelBox(flex_level_box.clone()) + }, + _ => return None, + }, + LayoutBox::TableLevelBox(..) => return None, + LayoutBox::TaffyItemBox(taffy_level_box) => match &taffy_level_box + .borrow() + .taffy_level_box + { + TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(_) + if box_style.position.is_absolutely_positioned() => + { + UpdatePoint::AbsolutelyPositionedTaffyLevelBox(taffy_level_box.clone()) + }, + _ => return None, + }, + }; + Some((primary_style.clone(), display_inside, update_point)) + } + + loop { + let Some((primary_style, display_inside, update_point)) = update_point(dirty_node) + else { + dirty_node = match dirty_node.parent_node() { + Some(parent) => parent, + None => return false, + }; + continue; + }; + + let contents = ReplacedContents::for_element(dirty_node, context) + .map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced); + let info = NodeAndStyleInfo::new(dirty_node, Arc::clone(&primary_style)); + let out_of_flow_absolutely_positioned_box = ArcRefCell::new( + AbsolutelyPositionedBox::construct(context, &info, display_inside, contents), + ); + match update_point { + UpdatePoint::AbsolutelyPositionedBlockLevelBox(block_level_box) => { + *block_level_box.borrow_mut() = BlockLevelBox::OutOfFlowAbsolutelyPositionedBox( + out_of_flow_absolutely_positioned_box, + ); + }, + UpdatePoint::AbsolutelyPositionedInlineLevelBox( + inline_level_box, + text_offset_index, + ) => { + *inline_level_box.borrow_mut() = InlineItem::OutOfFlowAbsolutelyPositionedBox( + out_of_flow_absolutely_positioned_box, + text_offset_index, + ); + }, + UpdatePoint::AbsolutelyPositionedFlexLevelBox(flex_level_box) => { + *flex_level_box.borrow_mut() = FlexLevelBox::OutOfFlowAbsolutelyPositionedBox( + out_of_flow_absolutely_positioned_box, + ); + }, + UpdatePoint::AbsolutelyPositionedTaffyLevelBox(taffy_level_box) => { + taffy_level_box.borrow_mut().taffy_level_box = + TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox( + out_of_flow_absolutely_positioned_box, + ); + }, + } + break; + } + + // We are going to rebuild the box tree from the update point downward, but this update + // point is an absolute, which means that it needs to be laid out again in the containing + // block for absolutes, which is established by one of its ancestors. In addition, + // absolutes, when laid out, can produce more absolutes (either fixed or absolutely + // positioned) elements, so there may be yet more layout that has to happen in this + // ancestor. + // + // We do not know which ancestor is the one that established the containing block for this + // update point, so just invalidate the fragment cache of all ancestors, meaning that even + // though the box tree is preserved, the fragment tree from the root to the update point and + // all of its descendants will need to be rebuilt. This isn't as bad as it seems, because + // siblings and siblings of ancestors of this path through the tree will still have cached + // fragments. + // + // TODO: Do better. This is still a very crude way to do incremental layout. + while let Some(parent_node) = dirty_node.parent_node() { + parent_node.invalidate_cached_fragment(); + dirty_node = parent_node; + } + + true + } +} + +fn construct_for_root_element<'dom>( + context: &LayoutContext, + root_element: impl NodeExt<'dom>, +) -> Vec<ArcRefCell<BlockLevelBox>> { + let info = NodeAndStyleInfo::new(root_element, root_element.style(context)); + let box_style = info.style.get_box(); + + let display_inside = match Display::from(box_style.display) { + Display::None => { + root_element.unset_all_boxes(); + return Vec::new(); + }, + Display::Contents => { + // Unreachable because the style crate adjusts the computed values: + // https://drafts.csswg.org/css-display-3/#transformations + // “'display' of 'contents' computes to 'block' on the root element” + unreachable!() + }, + // The root element is blockified, ignore DisplayOutside + Display::GeneratingBox(display_generating_box) => display_generating_box.display_inside(), + }; + + let contents = ReplacedContents::for_element(root_element, context) + .map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced); + + let propagated_data = PropagatedBoxTreeData::default().union(&info.style); + let root_box = if box_style.position.is_absolutely_positioned() { + BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new( + AbsolutelyPositionedBox::construct(context, &info, display_inside, contents), + )) + } else if box_style.float.is_floating() { + BlockLevelBox::OutOfFlowFloatBox(FloatBox::construct( + context, + &info, + display_inside, + contents, + propagated_data, + )) + } else { + BlockLevelBox::Independent(IndependentFormattingContext::construct( + context, + &info, + display_inside, + contents, + propagated_data, + )) + }; + + let root_box = ArcRefCell::new(root_box); + root_element + .element_box_slot() + .set(LayoutBox::BlockLevel(root_box.clone())); + vec![root_box] +} + +impl BoxTree { + pub fn layout( + &self, + layout_context: &LayoutContext, + viewport: euclid::Size2D<f32, CSSPixel>, + ) -> FragmentTree { + let style = layout_context + .style_context + .stylist + .device() + .default_computed_values(); + + // FIXME: use the document’s mode: + // https://drafts.csswg.org/css-writing-modes/#principal-flow + let physical_containing_block = PhysicalRect::new( + PhysicalPoint::zero(), + PhysicalSize::new( + Au::from_f32_px(viewport.width), + Au::from_f32_px(viewport.height), + ), + ); + let initial_containing_block = DefiniteContainingBlock { + size: LogicalVec2 { + inline: physical_containing_block.size.width, + block: physical_containing_block.size.height, + }, + style, + }; + + let mut positioning_context = + PositioningContext::new_for_containing_block_for_all_descendants(); + let independent_layout = self.root.layout( + layout_context, + &mut positioning_context, + &(&initial_containing_block).into(), + false, /* depends_on_block_constraints */ + ); + + let mut root_fragments = independent_layout.fragments.into_iter().collect::<Vec<_>>(); + + // Zero box for `:root { display: none }`, one for the root element otherwise. + assert!(root_fragments.len() <= 1); + + // There may be more fragments at the top-level + // (for positioned boxes whose containing is the initial containing block) + // but only if there was one fragment for the root element. + positioning_context.layout_initial_containing_block_children( + layout_context, + &initial_containing_block, + &mut root_fragments, + ); + + let scrollable_overflow = root_fragments + .iter() + .fold(PhysicalRect::zero(), |acc, child| { + let child_overflow = child.scrollable_overflow(); + + // https://drafts.csswg.org/css-overflow/#scrolling-direction + // We want to clip scrollable overflow on box-start and inline-start + // sides of the scroll container. + // + // FIXME(mrobinson, bug 25564): This should take into account writing + // mode. + let child_overflow = PhysicalRect::new( + euclid::Point2D::zero(), + euclid::Size2D::new( + child_overflow.size.width + child_overflow.origin.x, + child_overflow.size.height + child_overflow.origin.y, + ), + ); + acc.union(&child_overflow) + }); + + FragmentTree { + root_fragments, + scrollable_overflow, + initial_containing_block: physical_containing_block, + canvas_background: self.canvas_background.clone(), + viewport_scroll_sensitivity: self.viewport_scroll_sensitivity, + } + } +} + +/// <https://drafts.csswg.org/css-backgrounds/#root-background> +#[derive(Clone, MallocSizeOf)] +pub struct CanvasBackground { + /// DOM node for the root element + pub root_element: OpaqueNode, + + /// The element whose style the canvas takes background properties from (see next field). + /// This can be the root element (same as the previous field), or the HTML `<body>` element. + /// See <https://drafts.csswg.org/css-backgrounds/#body-background> + pub from_element: OpaqueNode, + + /// The computed styles to take background properties from. + #[conditional_malloc_size_of] + pub style: Option<Arc<ComputedValues>>, +} + +impl CanvasBackground { + fn for_root_element<'dom>(context: &LayoutContext, root_element: impl NodeExt<'dom>) -> Self { + let root_style = root_element.style(context); + + let mut style = root_style; + let mut from_element = root_element; + + // https://drafts.csswg.org/css-backgrounds/#body-background + // “if the computed value of background-image on the root element is none + // and its background-color is transparent” + if style.background_is_transparent() && + // “For documents whose root element is an HTML `HTML` element + // or an XHTML `html` element” + root_element.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLHtmlElement) && + // Don’t try to access styles for an unstyled subtree + !matches!(style.clone_display().into(), Display::None) + { + // “that element’s first HTML `BODY` or XHTML `body` child element” + if let Some(body) = iter_child_nodes(root_element).find(|child| { + child.is_element() && + child.type_id() == + LayoutNodeType::Element(LayoutElementType::HTMLBodyElement) + }) { + style = body.style(context); + from_element = body; + } + } + + Self { + root_element: root_element.opaque(), + from_element: from_element.opaque(), + + // “However, if no boxes are generated for the element + // whose background would be used for the canvas + // (for example, if the root element has display: none), + // then the canvas background is transparent.” + style: if let Display::GeneratingBox(_) = style.clone_display().into() { + Some(style) + } else { + None + }, + } + } +} |