aboutsummaryrefslogtreecommitdiffstats
path: root/components/layout/flow
diff options
context:
space:
mode:
Diffstat (limited to 'components/layout/flow')
-rw-r--r--components/layout/flow/construct.rs762
-rw-r--r--components/layout/flow/float.rs1183
-rw-r--r--components/layout/flow/inline/construct.rs636
-rw-r--r--components/layout/flow/inline/inline_box.rs257
-rw-r--r--components/layout/flow/inline/line.rs911
-rw-r--r--components/layout/flow/inline/line_breaker.rs120
-rw-r--r--components/layout/flow/inline/mod.rs2525
-rw-r--r--components/layout/flow/inline/text_run.rs640
-rw-r--r--components/layout/flow/mod.rs2370
-rw-r--r--components/layout/flow/root.rs500
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
+ },
+ }
+ }
+}