aboutsummaryrefslogtreecommitdiffstats
path: root/components/layout/flow/inline/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/layout/flow/inline/mod.rs')
-rw-r--r--components/layout/flow/inline/mod.rs2525
1 files changed, 2525 insertions, 0 deletions
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
+}