/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/layout/flow/inline/mod.rs
3 183 строки
137 KB
Simon Sapin
Resolve DOM/glyph positions during hit testing (#47048)
07 авг 2026, 10:07
Не верифицирован
07 авг 2026, 10:07
bffc115
Код
Авторство
О чём код?
/* 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; mod shaping_queue; pub mod text_run; pub mod text_transform; use std::cell::{Cell, OnceCell}; use std::mem; use std::ops::Range; use std::rc::Rc; use std::sync::{Arc, OnceLock}; use app_units::{Au, MAX_AU}; use atomic_refcell::AtomicRef; use bitflags::bitflags; use construct::InlineFormattingContextBuilder; use fonts::{FontMetrics, FontRef, ShapedTextSlice}; use icu_locid::LanguageIdentifier; use icu_locid::subtags::{Language, language}; use icu_properties::{self, LineBreak as ICULineBreak}; use icu_segmenter::{LineBreakOptions, LineBreakStrictness, LineBreakWordOption}; use inline_box::{InlineBox, InlineBoxContainerState, InlineBoxIdentifier, InlineBoxes}; use layout_api::LayoutNode; use line::{ AbsolutelyPositionedLineItem, AtomicLineItem, FloatLineItem, LineItem, LineItemLayout, TextRunLineItem, }; use malloc_size_of_derive::MallocSizeOf; use script::layout_dom::ServoLayoutNode; use servo_arc::Arc as ServoArc; use servo_base::text::Utf32CodeUnits; use style::Zero; use style::computed_values::line_break::T as LineBreak; use style::computed_values::text_wrap_mode::T as TextWrapMode; use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; use style::computed_values::word_break::T as WordBreak; use style::context::{QuirksMode, SharedStyleContext}; use style::properties::ComputedValues; use style::properties::style_structs::InheritedText; use style::values::computed::BaselineShift; use style::values::generics::box_::BaselineShiftKeyword; use style::values::generics::font::LineHeight; use style::values::specified::box_::BaselineSource; use style::values::specified::text::TextAlignKeyword; use style::values::specified::{AlignmentBaseline, TextAlignLast, TextJustify}; use text_run::{TextRun, get_font_for_first_font_for_style}; use unicode_bidi::{BidiInfo, Level}; use super::float::{Clear, PlacementAmongFloats}; use super::{IndependentFloatOrAtomicLayoutResult, IndependentFormattingContextLayoutResult}; use crate::cell::{ArcRefCell, WeakRefCell}; use crate::context::LayoutContext; use crate::dom::WeakLayoutBox; use crate::dom_traversal::NodeAndStyleInfo; use crate::flow::float::{FloatBox, SequentialLayoutState}; use crate::flow::inline::shaping_queue::ShapingQueue; use crate::flow::inline::text_run::{ CaretPlaceholder, FontAndScriptInfo, TextRunItem, TextRunSegment, }; use crate::flow::{ BlockLevelBox, CollapsibleWithParentStartMargin, FloatSide, PlacementState, compute_inline_content_sizes_for_block_level_boxes, layout_block_level_child, }; use crate::formatting_contexts::{Baselines, IndependentFormattingContext}; use crate::fragment_tree::{CollapsedMargin, Fragment, FragmentFlags, PositioningFragment}; use crate::geom::{LogicalRect, LogicalSides1D, LogicalVec2, ToLogical}; use crate::layout_box_base::LayoutBoxBase; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; use crate::sizing::{ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult}; use crate::style_ext::{ComputedValuesExt, PaddingBorderMargin}; use crate::{ConstraintSpace, ContainingBlock, IndefiniteContainingBlock, SharedStyle}; // 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. inline_items: Vec<InlineItem>, /// The tree of inline boxes in this [`InlineFormattingContext`]. These are stored in /// a flat array with each being given a [`InlineBoxIdentifier`]. inline_boxes: InlineBoxes, /// The text content of this inline formatting context. text_content: String, /// The [`SharedInlineStyles`] for the root of this [`InlineFormattingContext`] that are used to /// share styles with all [`TextRun`] children. shared_inline_styles: SharedInlineStyles, /// The default font that is used for the root of this [`InlineFormattingContext`]. This is the /// font used when the font fallback code path is not taken. It may be `None` if no default /// font was found (this typically means that no characters can be rendered). default_font: Option<FontRef>, /// Whether this IFC contains the 1st formatted line of an element: /// <https://www.w3.org/TR/css-pseudo-4/#first-formatted-line>. 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's inner /// text container. is_single_line_text_input: bool, /// Whether or not this is an [`InlineFormattingContext`] has right-to-left content, which /// will require reordering during layout. has_right_to_left_content: bool, /// The cached multiplier for `tab-size: <number>`: /// <https://drafts.csswg.org/css-text/#tab-size-property> /// > the advance width of the space character (U+0020) of the nearest block container ancestor /// > of the preserved tab, including its associated `letter-spacing` and `word-spacing`. tab_size_multiplier: OnceLock<Au>, } /// [`TextRun`] and `TextFragment`s need a handle on their parent inline box (or inline /// formatting context root)'s style. In order to implement incremental layout, these are /// wrapped in [`SharedStyle`]. This allows updating the parent box tree element without /// updating every single descendant box tree node and fragment. #[derive(Clone, Debug, MallocSizeOf)] pub(crate) struct SharedInlineStyles { pub style: SharedStyle, pub selected: SharedStyle, } impl SharedInlineStyles { pub(crate) fn ptr_eq(&self, other: &Self) -> bool { self.style.ptr_eq(&other.style) && self.selected.ptr_eq(&other.selected) } pub(crate) fn from_info_and_context(info: &NodeAndStyleInfo, context: &LayoutContext) -> Self { Self { style: SharedStyle::new(info.style.clone()), selected: SharedStyle::new(info.node.selected_style(&context.style_context)), } } } impl BlockLevelBox { fn layout_into_line_items(&self, layout: &mut InlineFormattingContextLayout) { layout.process_soft_wrap_opportunity(); layout.commit_current_segment_to_line(); layout.process_line_break( true, /* forced_line_break */ true, /* for_block_level */ ); let fragment = layout_block_level_child( layout.layout_context, layout.positioning_context, self, layout.sequential_layout_state.as_deref_mut(), &mut layout.placement_state, layout.ignore_block_margins_for_stretch, true, /* has_inline_parent */ ); let Some(fragment) = fragment.retrieve_box_fragment() else { unreachable!("The fragment should be a Fragment::Box()"); }; // 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, ); layout.push_line_item_to_unbreakable_segment(LineItem::BlockLevel( layout.current_inline_box_identifier(), fragment.clone(), )); layout.commit_current_segment_to_line(); layout.process_line_break( true, /* forced_line_break */ false, /* for_block_level */ ); } } #[derive(Clone, Debug, MallocSizeOf)] pub(crate) enum InlineItem { StartInlineBox(ArcRefCell<InlineBox>), EndInlineBox(ArcRefCell<InlineBox>), TextRun(ArcRefCell<TextRun>), OutOfFlowAbsolutelyPositionedBox( ArcRefCell<AbsolutelyPositionedBox>, usize, /* offset_in_text */ ), OutOfFlowFloatBox(ArcRefCell<FloatBox>), Atomic( ArcRefCell<IndependentFormattingContext>, usize, /* offset_in_text */ Level, /* bidi_level */ ), BlockLevel(ArcRefCell<BlockLevelBox>), } impl InlineItem { pub(crate) fn repair_style( &self, context: &SharedStyleContext, node: &ServoLayoutNode, new_style: &ServoArc<ComputedValues>, ) { match self { InlineItem::StartInlineBox(inline_box) => { inline_box .borrow_mut() .repair_style(context, node, new_style); }, InlineItem::EndInlineBox(..) => {}, // TextRun holds a handle the `InlineSharedStyles` which is updated when repairing inline box // and `display: contents` styles. InlineItem::TextRun(..) => {}, InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => positioned_box .borrow_mut() .context .repair_style(context, node, new_style), InlineItem::OutOfFlowFloatBox(float_box) => float_box .borrow_mut() .contents .repair_style(context, node, new_style), InlineItem::Atomic(atomic, ..) => { atomic.borrow_mut().repair_style(context, node, new_style) }, InlineItem::BlockLevel(block_level) => block_level .borrow_mut() .repair_style(context, node, new_style), } } pub(crate) fn with_base<T>(&self, callback: impl FnOnce(&LayoutBoxBase) -> T) -> T { match self { InlineItem::StartInlineBox(inline_box) => callback(&inline_box.borrow().base), InlineItem::EndInlineBox(..) | InlineItem::TextRun(..) => { unreachable!("Should never have these kind of fragments attached to a DOM node") }, InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => { callback(&positioned_box.borrow().context.base) }, InlineItem::OutOfFlowFloatBox(float_box) => callback(&float_box.borrow().contents.base), InlineItem::Atomic(independent_formatting_context, ..) => { callback(&independent_formatting_context.borrow().base) }, InlineItem::BlockLevel(block_level) => block_level.borrow().with_base(callback), } } pub(crate) fn with_base_mut<T>(&self, callback: impl FnOnce(&mut LayoutBoxBase) -> T) -> T { match self { InlineItem::StartInlineBox(inline_box) => callback(&mut inline_box.borrow_mut().base), InlineItem::EndInlineBox(..) | InlineItem::TextRun(..) => { unreachable!("Should never have these kind of fragments attached to a DOM node") }, InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => { callback(&mut positioned_box.borrow_mut().context.base) }, InlineItem::OutOfFlowFloatBox(float_box) => { callback(&mut float_box.borrow_mut().contents.base) }, InlineItem::Atomic(independent_formatting_context, ..) => { callback(&mut independent_formatting_context.borrow_mut().base) }, InlineItem::BlockLevel(block_level) => block_level.borrow_mut().with_base_mut(callback), } } pub(crate) fn attached_to_tree(&self, layout_box: WeakLayoutBox) { match self { Self::StartInlineBox(_) | InlineItem::EndInlineBox(..) => { // The parentage of inline items within an inline box is handled when the entire // inline formatting context is attached to the tree. }, Self::TextRun(_) => { // Text runs can't have children, so no need to do anything. }, Self::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => { positioned_box.borrow().context.attached_to_tree(layout_box) }, Self::OutOfFlowFloatBox(float_box) => { float_box.borrow().contents.attached_to_tree(layout_box) }, Self::Atomic(atomic, ..) => atomic.borrow().attached_to_tree(layout_box), Self::BlockLevel(block_level) => block_level.borrow().attached_to_tree(layout_box), } } pub(crate) fn downgrade(&self) -> WeakInlineItem { match self { Self::StartInlineBox(inline_box) => { WeakInlineItem::StartInlineBox(inline_box.downgrade()) }, Self::EndInlineBox(inline_box) => WeakInlineItem::EndInlineBox(inline_box.downgrade()), Self::TextRun(text_run) => WeakInlineItem::TextRun(text_run.downgrade()), Self::OutOfFlowAbsolutelyPositionedBox(positioned_box, offset_in_text) => { WeakInlineItem::OutOfFlowAbsolutelyPositionedBox( positioned_box.downgrade(), *offset_in_text, ) }, Self::OutOfFlowFloatBox(float_box) => { WeakInlineItem::OutOfFlowFloatBox(float_box.downgrade()) }, Self::Atomic(atomic, offset_in_text, bidi_level) => { WeakInlineItem::Atomic(atomic.downgrade(), *offset_in_text, *bidi_level) }, Self::BlockLevel(block_level) => WeakInlineItem::BlockLevel(block_level.downgrade()), } } } #[derive(Clone, Debug, MallocSizeOf)] pub(crate) enum WeakInlineItem { StartInlineBox(WeakRefCell<InlineBox>), EndInlineBox(WeakRefCell<InlineBox>), TextRun(WeakRefCell<TextRun>), OutOfFlowAbsolutelyPositionedBox( WeakRefCell<AbsolutelyPositionedBox>, usize, /* offset_in_text */ ), OutOfFlowFloatBox(WeakRefCell<FloatBox>), Atomic( WeakRefCell<IndependentFormattingContext>, usize, /* offset_in_text */ Level, /* bidi_level */ ), BlockLevel(WeakRefCell<BlockLevelBox>), } impl WeakInlineItem { pub(crate) fn upgrade(&self) -> Option<InlineItem> { Some(match self { Self::StartInlineBox(inline_box) => InlineItem::StartInlineBox(inline_box.upgrade()?), Self::EndInlineBox(inline_box) => InlineItem::EndInlineBox(inline_box.upgrade()?), Self::TextRun(text_run) => InlineItem::TextRun(text_run.upgrade()?), Self::OutOfFlowAbsolutelyPositionedBox(positioned_box, offset_in_text) => { InlineItem::OutOfFlowAbsolutelyPositionedBox( positioned_box.upgrade()?, *offset_in_text, ) }, Self::OutOfFlowFloatBox(float_box) => { InlineItem::OutOfFlowFloatBox(float_box.upgrade()?) }, Self::Atomic(atomic, offset_in_text, bidi_level) => { InlineItem::Atomic(atomic.upgrade()?, *offset_in_text, *bidi_level) }, Self::BlockLevel(block_level) => InlineItem::BlockLevel(block_level.upgrade()?), }) } } /// 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 any active linebox has added some inline-axis padding, border or margin /// to this line. has_inline_pbm: 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>, /// Whether the current line is for a block-level box. for_block_level: bool, /// If this line is empty and contains a selection, this field will be used to create /// an empty [`TextFragment`] for holding a text caret. caret_placeholder: Option<CaretPlaceholder>, } 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_inline_pbm: false, has_floats_waiting_to_be_placed: false, placement_among_floats: OnceCell::new(), line_items: Vec::new(), for_block_level: false, caret_placeholder: None, } } 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(|shaped_text_slice| shaped_text_slice.total_word_separators()) .sum::<usize>(), ), _ => None, }) .sum() } /// Whether this is a phantom line box. /// <https://drafts.csswg.org/css-inline-3/#invisible-line-boxes> fn is_phantom(&self) -> bool { // Keep this logic in sync with `UnbreakableSegmentUnderConstruction::is_phantom()`. !self.has_content && !self.has_inline_pbm } } /// 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, /// Whether any active linebox has added some inline-axis padding, border or margin /// to this line segment. has_inline_pbm: 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, has_inline_pbm: 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.has_inline_pbm = 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; } /// Whether this is segment is phantom. If false, its line box won't be phantom. /// <https://drafts.csswg.org/css-inline-3/#invisible-line-boxes> fn is_phantom(&self) -> bool { // Keep this logic in sync with `LineUnderConstruction::is_phantom()`. !self.has_content && !self.has_inline_pbm } } bitflags! { struct InlineContainerStateFlags: u8 { const CREATE_STRUT = 0b0001; const IS_SINGLE_LINE_TEXT_INPUT = 0b0010; } } struct InlineContainerState { /// The style of this inline container. style: ServoArc<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: Cell<bool>, /// 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 primary font used for this container, if one exists. This is the font that is /// used when not falling back. default_font: Option<FontRef>, /// The font metrics of the non-fallback font for this container. font_metrics: Arc<FontMetrics>, } struct InlineFormattingContextLayout<'layout_data> { positioning_context: &'layout_data mut PositioningContext, placement_state: PlacementState<'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>>, /// The amount of space that will be taken up by all end-side paddings, borders and margins of /// all inline boxes with `box-decoration-break: clone` that we are currently inside of. cloneable_inline_box_end_pbm_size: Au, /// 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 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()`]. /// /// If this field is `true`, a hard line break should be processed before any new content. force_line_break_before_new_content: bool, /// When deferring a forced line break, this field stores a potential caret placeholder /// used to create a [`TextFragment`] to hold a caret on an otherwise empty line. caret_placeholder: Option<CaretPlaceholder>, /// 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 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, /// Whether block-level boxes inside this inline formatting context should ignore their /// margins for the purpose of stretching in the block axis. ignore_block_margins_for_stretch: LogicalSides1D<bool>, } 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 current_line_block_start_considering_placement_among_floats(&self) -> Au { self.current_line.placement_among_floats.get().map_or( self.current_line.start_position.block, |placement_among_floats| placement_among_floats.start_corner.block, ) } 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.placement_state.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().is_some_and(|state| { state .base_fragment_info .flags .contains(FragmentFlags::IS_BR_ELEMENT) }) } /// 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 containing_block = self.containing_block(); let inline_box_state = InlineBoxContainerState::new( inline_box, containing_block, self.layout_context, self.current_inline_container_state(), inline_box.default_font.clone(), ); self.depends_on_block_constraints |= inline_box .base .style .depends_on_block_constraints_due_to_relative_positioning( 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, ); } let padding = inline_box_state.pbm.padding.inline_start; let border = inline_box_state.pbm.border.inline_start; let margin = inline_box_state.pbm.margin.inline_start.auto_is(Au::zero); // We can't just check if the sum is zero because the margin can be negative, // we need to check the values separately. if !padding.is_zero() || !border.is_zero() || !margin.is_zero() { self.current_line_segment.has_inline_pbm = true; } self.current_line_segment.inline_size += padding + border + margin; self.current_line_segment .line_items .push(LineItem::InlineStartBoxPaddingBorderMargin( inline_box.identifier, )); let inline_box_state = Rc::new(inline_box_state); if inline_box_state.should_clone_pbm() { self.cloneable_inline_box_end_pbm_size += inline_box_state.pbm.padding.inline_end; self.cloneable_inline_box_end_pbm_size += inline_box_state.pbm.border.inline_end; self.cloneable_inline_box_end_pbm_size += inline_box_state.pbm.margin.inline_end.auto_is(Au::zero); } // 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. }; if inline_box_state.should_clone_pbm() { self.cloneable_inline_box_end_pbm_size -= inline_box_state.pbm.padding.inline_end; self.cloneable_inline_box_end_pbm_size -= inline_box_state.pbm.border.inline_end; self.cloneable_inline_box_end_pbm_size -= inline_box_state.pbm.margin.inline_end.auto_is(Au::zero); } 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.get() { self.propagate_current_nesting_level_white_space_style(); } let padding = inline_box_state.pbm.padding.inline_end; let border = inline_box_state.pbm.border.inline_end; let margin = inline_box_state.pbm.margin.inline_end.auto_is(Au::zero); // We can't just check if the sum is zero because the margin can be negative, // we need to check the values separately. if !padding.is_zero() || !border.is_zero() || !margin.is_zero() { self.current_line_segment.has_inline_pbm = true; } self.current_line_segment.inline_size += padding + border + margin; self.current_line_segment .line_items .push(LineItem::InlineEndBoxPaddingBorderMargin( inline_box_state.identifier, )); } fn finish_last_line(&mut self) { // First, process any deferred forced line breaks. self.possibly_flush_deferred_forced_line_break(); // 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 */ false, /* for_block_level */ ); } /// 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, for_block_level: bool, ) { self.possibly_push_empty_text_run_to_line_for_text_caret(); let whitespace_trimmed = self.current_line.trim_trailing_whitespace(); // At the end of a line, we need to insert any paddings, borders or margins that might need to be // duplicated due to box-decoration-break if !self.current_line.for_block_level { for inline_box in self.inline_box_state_stack.iter().rev() { if inline_box.should_clone_pbm() { self.current_line_segment.line_items.push( LineItem::InlineEndBoxPaddingBorderMargin(inline_box.identifier), ); } } } let (inline_start_position, justification_adjustment) = self .calculate_current_line_inline_start_and_justification_adjustment( whitespace_trimmed, last_line_or_forced_line_break, ); // https://drafts.csswg.org/css-inline-3/#invisible-line-boxes // > Line boxes that contain no text, no preserved white space, no inline boxes with non-zero // > inline-axis margins, padding, or borders, and no other in-flow content (such as atomic // > inlines or ruby annotations), and do not end with a forced line break are phantom line boxes. // > Such boxes must be treated as zero-height line boxes for the purposes of determining the // > positions of any descendant content (such as absolutely positioned boxes), and both the // > line box and its in-flow content must be treated as not existing for any other layout or // > rendering purpose. let is_phantom_line = self.current_line.is_phantom(); if !is_phantom_line { self.current_line.start_position.block += self.placement_state.current_margin.solve(); self.placement_state.current_margin = CollapsedMargin::zero(); } let block_start_position = self.current_line_block_start_considering_placement_among_floats(); let effective_block_advance = if is_phantom_line { LineBlockSizes::zero() } else { self.current_line_max_block_size_including_nested_containers() }; let resolved_block_advance = effective_block_advance.resolve(); let block_end_position = if self.current_line.for_block_level { self.placement_state.current_block_direction_position } else { let mut block_end_position = block_start_position + resolved_block_advance; if let Some(sequential_layout_state) = self.sequential_layout_state.as_mut() { if !is_phantom_line { sequential_layout_state.commit_margin(); } // 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; } block_end_position }; // Set up the new line now that we no longer need the old one. let line_to_layout = std::mem::replace( &mut self.current_line, LineUnderConstruction::new(LogicalVec2 { inline: Au::zero(), block: block_end_position, }), ); self.current_line.for_block_level = for_block_level; // At the start of the next line, we need to insert any paddings, borders or margins that might need to be // duplicated due to box-decoration-break if !for_block_level { for inline_box in self.inline_box_state_stack.iter() { if inline_box.should_clone_pbm() { self.current_line_segment.line_items.push( LineItem::InlineStartBoxPaddingBorderMargin(inline_box.identifier), ); } } } if !line_to_layout.for_block_level { self.placement_state.current_block_direction_position = block_end_position; } if line_to_layout.has_floats_waiting_to_be_placed { place_pending_floats(self, &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, is_phantom_line, line_to_layout.for_block_level, ); if !is_phantom_line { let baseline = baseline_offset + block_start_position; self.placement_state .inflow_baselines .first .get_or_insert(baseline); self.placement_state.inflow_baselines.last = Some(baseline); self.placement_state .next_in_flow_margin_collapses_with_parent_start_margin = false; } // 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; } // 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 containing_block = self.containing_block(); let physical_line_rect = LogicalRect { start_corner, size: LogicalVec2 { inline: containing_block.size.inline, block: effective_block_advance.resolve(), }, } .as_physical(Some(containing_block)); self.fragments .push(Fragment::Positioning(PositioningFragment::new_anonymous( self.root_nesting_level.style.clone(), physical_line_rect, fragments, true, /* is_line_box */ ))); } /// 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 containing_block = self.containing_block(); let style = 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(), 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 = 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, float: &FloatLineItem) { 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( &float.fragment, self.placement_state.containing_block, CollapsedMargin::zero(), block_offset_from_containining_block_top, ); self.positioning_context .adjust_static_position_of_hoisted_fragments_in_range( &float.fragment.base.rect().origin.to_vector(), &float.range, ) } /// 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 containing_block = self.containing_block(); let float_fragment = &float_item.fragment; let logical_margin_rect_size = float_fragment .margin_rect() .size .to_logical(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 => 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(float_item); 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_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 containing_block = self.containing_block(); 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: 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 > 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_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. potential_line_size.inline + self.cloneable_inline_box_end_pbm_size > available_line_space.inline } fn defer_forced_line_break_at_character_offset( &mut self, caret_placeholder: &Option<CaretPlaceholder>, ) { // 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 */ false, /* for_block_level */ ); } // Defer the actual line break until we've cleared all ending inline boxes. self.force_line_break_before_new_content = true; self.caret_placeholder = caret_placeholder.clone(); // 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(), ); } } fn possibly_flush_deferred_forced_line_break(&mut self) { if !self.force_line_break_before_new_content { return; } self.force_line_break_before_new_content = false; self.commit_current_segment_to_line(); self.process_line_break( true, /* forced_line_break */ false, /* for_block_level */ ); self.current_line.caret_placeholder = self.caret_placeholder.take(); } 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()); } fn push_glyph_store_to_unbreakable_segment( &mut self, glyph_store: Arc<ShapedTextSlice>, text_run: &TextRun, info: &FontAndScriptInfo, character_range: Range<Utf32CodeUnits>, ) { let inline_advance = glyph_store.total_advance(); let flags = if glyph_store.is_whitespace() { SegmentContentFlags::from(text_run.inline_styles().style.borrow().get_inherited_text()) } else { SegmentContentFlags::empty() }; let mut block_contribution = LineBlockSizes::zero(); let quirks_mode = self.layout_context.style_context.quirks_mode() != QuirksMode::NoQuirks; let current_inline_container_state = self.current_inline_container_state(); 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. block_contribution.max_assign(¤t_inline_container_state.strut_block_sizes); } // If the metrics of this font don't match the default font, we are likely using another // font from the font list or a fallback and should incorporate its block size into the block // size of the container. let font_metrics = &info.font_info.font.metrics; if current_inline_container_state .font_metrics .block_metrics_meaningfully_differ(font_metrics) { // TODO(mrobinson): This value should probably be cached somewhere. let baseline_shift = effective_baseline_shift( ¤t_inline_container_state.style, self.inline_box_state_stack.last().map(|c| &c.base), ); let mut font_block_conribution = current_inline_container_state .get_block_size_contribution( baseline_shift, font_metrics, ¤t_inline_container_state.font_metrics, ); font_block_conribution .adjust_for_baseline_offset(current_inline_container_state.baseline_offset); block_contribution.max_assign(&font_block_conribution); } self.update_unbreakable_segment_for_new_content(&block_contribution, inline_advance, flags); let current_inline_box_identifier = self.current_inline_box_identifier(); self.push_line_item_to_unbreakable_segment(LineItem::TextRun( current_inline_box_identifier, TextRunLineItem { text: vec![glyph_store], text_fragment_run_data: text_run.run_data.clone(), base_fragment_info: text_run.base_fragment_info, info: info.clone(), character_range_in_dom_node: character_range, is_empty_for_text_cursor: false, }, )); } /// If the current line is empty and this [`InlineFormattingContext`] has a selection, push an /// empty [`LineItem::TextRun`] so that text carets can be placed on otherwise empty lines. fn possibly_push_empty_text_run_to_line_for_text_caret(&mut self) { let Some(caret_placeholder) = self.current_line.caret_placeholder.take() else { return; }; // If the last content line item is a text item, then the placeholder for the text caret is not necessary. if self .current_line .line_items .iter() .rev() .find(|line_item| line_item.is_in_flow_content()) .is_some_and(|line_item| matches!(line_item, LineItem::TextRun(..))) { return; } let inline_container_state = self.current_inline_container_state(); let Some(font) = inline_container_state.default_font.clone() else { return; }; self.push_line_item_to_unbreakable_segment(LineItem::TextRun( self.current_inline_box_identifier(), TextRunLineItem { text: Default::default(), text_fragment_run_data: caret_placeholder.run_data, base_fragment_info: caret_placeholder.base_fragment_info, info: FontAndScriptInfo::simple_for_font(font), character_range_in_dom_node: Utf32CodeUnits(caret_placeholder.character_index).. Utf32CodeUnits(caret_placeholder.character_index + 1), is_empty_for_text_cursor: true, }, )); self.current_line_segment.has_content = true; self.commit_current_segment_to_line(); } 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; } // 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.set(true); self.propagate_current_nesting_level_white_space_style(); } fn process_line_break(&mut self, forced_line_break: bool, for_block_level: bool) { self.current_line_segment.trim_leading_whitespace(); self.finish_current_line_and_reset(forced_line_break, for_block_level); } fn potential_line_size(&self) -> LogicalVec2<Au> { LogicalVec2 { inline: self.current_line.inline_position + self.current_line_segment.inline_size, block: self .current_line_max_block_size_including_nested_containers() .max(&self.current_line_segment.max_block_size) .resolve(), } } fn unbreakable_segment_fits_on_line(&mut self) -> bool { let potential_line_size_without_hanging_whitespace = self.potential_line_size() - LogicalVec2 { inline: self.current_line_segment.trailing_whitespace_size, block: Au::zero(), }; !self.new_potential_line_size_causes_line_break( &potential_line_size_without_hanging_whitespace, ) } /// 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. 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; } if !self.unbreakable_segment_fits_on_line() { self.process_line_break( false, /* forced_line_break */ false, /* for_block_level */ ); } 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.has_inline_pbm |= self.current_line_segment.has_inline_pbm; self.current_line_segment.reset(); } #[inline] fn containing_block(&self) -> &ContainingBlock<'_> { self.placement_state.containing_block } } bitflags! { 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 { #[servo_tracing::instrument(name = "InlineFormattingContext::new_with_builder", skip_all)] fn new_with_builder( mut builder: InlineFormattingContextBuilder, layout_context: &LayoutContext, 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 bidi_levels = BidiLevels { info: builder .has_right_to_left_content .then(|| BidiInfo::new(&text_content, Some(starting_bidi_level))), }; let shared_inline_styles = builder .shared_inline_styles_stack .last() .expect("Should have at least one SharedInlineStyle for the root of an IFC") .clone(); let (word_break, line_break, lang) = { let styles = shared_inline_styles.style.borrow(); let text_style = styles.get_inherited_text(); ( text_style.word_break, text_style.line_break, styles.get_font()._x_lang.clone(), ) }; let mut options = LineBreakOptions::default(); options.strictness = match line_break { LineBreak::Loose => LineBreakStrictness::Loose, LineBreak::Normal => LineBreakStrictness::Normal, LineBreak::Strict => LineBreakStrictness::Strict, LineBreak::Anywhere => LineBreakStrictness::Anywhere, // For `auto`, the UA determines the set of line-breaking restrictions to use. // So it's fine if we always treat it as `normal`. LineBreak::Auto => LineBreakStrictness::Normal, }; options.word_option = match word_break { WordBreak::Normal => LineBreakWordOption::Normal, WordBreak::BreakAll => LineBreakWordOption::BreakAll, WordBreak::KeepAll => LineBreakWordOption::KeepAll, }; // Enable Chinese/Japanese line breaking behavior when this inline formatting context // has a Japanese or Chinese language set. options.ja_zh = { lang.0.parse::<LanguageIdentifier>().is_ok_and(|lang_id| { const JA: Language = language!("ja"); const ZH: Language = language!("zh"); matches!(lang_id.language, JA | ZH) }) }; let mut shaping_queue = ShapingQueue::new(&text_content, options); for item in &mut builder.inline_items { match item { InlineItem::TextRun(text_run) => { let shaping_queue_entries = text_run.borrow_mut().segment( text_run.clone(), &text_content, layout_context, &bidi_levels, ); for entry in shaping_queue_entries.into_iter() { shaping_queue.push(entry); } }, 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 = Some(font); } if inline_box.breaks_shaping_at_start { shaping_queue.flush(); } }, InlineItem::Atomic(_, index_in_text, bidi_level) => { shaping_queue.flush(); *bidi_level = bidi_levels.level(*index_in_text); }, InlineItem::EndInlineBox(inline_box) => { if inline_box.borrow().breaks_shaping_at_end { shaping_queue.flush(); } }, InlineItem::OutOfFlowAbsolutelyPositionedBox(..) | InlineItem::OutOfFlowFloatBox(_) | InlineItem::BlockLevel { .. } => {}, } } shaping_queue.flush(); let default_font = get_font_for_first_font_for_style( &shared_inline_styles.style.borrow(), &layout_context.font_context, ); let has_right_to_left_content = bidi_levels.info.as_ref().is_some_and(BidiInfo::has_rtl); InlineFormattingContext { text_content, inline_items: builder.inline_items, inline_boxes: builder.inline_boxes, shared_inline_styles, default_font, has_first_formatted_line, contains_floats: builder.contains_floats, is_single_line_text_input, has_right_to_left_content, tab_size_multiplier: Default::default(), } } pub(crate) fn repair_style( &self, context: &SharedStyleContext, node: &ServoLayoutNode, new_style: &ServoArc<ComputedValues>, ) { *self.shared_inline_styles.style.borrow_mut() = new_style.clone(); *self.shared_inline_styles.selected.borrow_mut() = node.selected_style(context); } fn inline_start_for_first_line(&self, containing_block: IndefiniteContainingBlock) -> Au { if !self.has_first_formatted_line { return Au::zero(); } containing_block .style .get_inherited_text() .text_indent .length .to_used_value(containing_block.size.inline.unwrap_or_default()) } 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, ignore_block_margins_for_stretch: LogicalSides1D<bool>, ) -> IndependentFormattingContextLayoutResult { // 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; 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 placement_state = PlacementState::new(collapsible_with_parent_start_margin, containing_block); let mut layout = InlineFormattingContextLayout { positioning_context, placement_state, sequential_layout_state, layout_context, ifc: self, fragments: Vec::new(), current_line: LineUnderConstruction::new(LogicalVec2 { inline: self.inline_start_for_first_line(containing_block.into()), block: Au::zero(), }), root_nesting_level: InlineContainerState::new( style.to_arc(), inline_container_state_flags, None, /* parent_container */ self.default_font.clone(), ), inline_box_state_stack: Vec::new(), cloneable_inline_box_end_pbm_size: Au::zero(), inline_box_states: Vec::with_capacity(self.inline_boxes.len()), current_line_segment: UnbreakableSegmentUnderConstruction::new(), force_line_break_before_new_content: false, caret_placeholder: None, deferred_br_clear: Clear::None, have_deferred_soft_wrap_opportunity: false, depends_on_block_constraints: false, white_space_collapse: style_text.white_space_collapse, text_wrap_mode: style_text.text_wrap_mode, ignore_block_margins_for_stretch, }; for item in self.inline_items.iter() { // 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.borrow().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(), preceding_line_content_would_produce_phantom_line: layout .current_line .is_phantom() && layout.current_line_segment.is_phantom(), }, )); }, InlineItem::OutOfFlowFloatBox(float_box) => { float_box.borrow().layout_into_line_items(&mut layout); }, InlineItem::BlockLevel(block_level) => { block_level.borrow().layout_into_line_items(&mut layout); }, } } layout.finish_last_line(); let (content_block_size, collapsible_margins_in_children, baselines) = layout.placement_state.finish(); IndependentFormattingContextLayoutResult { fragments: layout.fragments, content_block_size, collapsible_margins_in_children, baselines, depends_on_block_constraints: layout.depends_on_block_constraints, content_inline_size_for_table: None, specific_layout_info: None, } } pub(crate) fn subtree_size(&self) -> usize { self.inline_items .iter() .map(|item| match item { InlineItem::StartInlineBox(..) => 1, InlineItem::EndInlineBox(..) => 0, InlineItem::TextRun(..) => 1, InlineItem::OutOfFlowAbsolutelyPositionedBox(absolutely_positioned_box, _) => { absolutely_positioned_box .borrow() .context .base .subtree_size() }, InlineItem::OutOfFlowFloatBox(..) => 1, InlineItem::Atomic(..) => 1, InlineItem::BlockLevel(block_level_box) => block_level_box.borrow().subtree_size(), }) .sum() } 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) } pub(crate) fn find_block_margin_collapsing_with_parent( &self, layout_context: &LayoutContext, collected_margin: &mut CollapsedMargin, containing_block_for_children: &ContainingBlock, ) -> bool { // Margins can't collapse through line boxes, unless they are phantom line boxes. // <https://drafts.csswg.org/css-inline-3/#invisible-line-boxes> // > Line boxes that contain no text, no preserved white space, no inline boxes with non-zero // > inline-axis margins, padding, or borders, and no other in-flow content (such as atomic // > inlines or ruby annotations), and do not end with a forced line break are phantom line boxes. let mut items_iter = self.inline_items.iter(); items_iter.all(|inline_item| match inline_item { InlineItem::StartInlineBox(inline_box) => { let pbm = inline_box .borrow() .layout_style() .padding_border_margin(containing_block_for_children); pbm.padding.inline_start.is_zero() && pbm.border.inline_start.is_zero() && pbm.margin.inline_start.auto_is(Au::zero).is_zero() }, InlineItem::EndInlineBox(inline_box) => { let pbm = inline_box .borrow() .layout_style() .padding_border_margin(containing_block_for_children); pbm.padding.inline_end.is_zero() && pbm.border.inline_end.is_zero() && pbm.margin.inline_end.auto_is(Au::zero).is_zero() }, InlineItem::TextRun(text_run) => { let text_run = &*text_run.borrow(); let parent_style = text_run.inline_styles().style.borrow(); text_run.items.iter().all(|item| match item { TextRunItem::LineBreak { .. } => false, TextRunItem::Tab { .. } => false, TextRunItem::TextSegment(segment) => segment.runs.iter().all(|run| { run.is_whitespace() && !matches!( parent_style.get_inherited_text().white_space_collapse, WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces ) }), }) }, InlineItem::OutOfFlowAbsolutelyPositionedBox(..) => true, InlineItem::OutOfFlowFloatBox(..) => true, InlineItem::Atomic(..) => false, InlineItem::BlockLevel(block_level) => block_level .borrow() .find_block_margin_collapsing_with_parent( layout_context, collected_margin, containing_block_for_children, ), }) } pub(crate) fn attached_to_tree(&self, layout_box: WeakLayoutBox) { let mut parent_box_stack = Vec::new(); let current_parent_box = |parent_box_stack: &[WeakLayoutBox]| { parent_box_stack.last().unwrap_or(&layout_box).clone() }; for inline_item in &self.inline_items { match inline_item { InlineItem::StartInlineBox(inline_box) => { inline_box .borrow_mut() .base .parent_box .replace(current_parent_box(&parent_box_stack)); parent_box_stack.push(WeakLayoutBox::InlineLevel( WeakInlineItem::StartInlineBox(inline_box.downgrade()), )); }, InlineItem::EndInlineBox(..) => { parent_box_stack.pop(); }, InlineItem::TextRun(text_run) => { text_run .borrow_mut() .parent_box .replace(current_parent_box(&parent_box_stack)); }, _ => inline_item.with_base_mut(|base| { base.parent_box .replace(current_parent_box(&parent_box_stack)); }), } } } pub(crate) fn next_tab_stop_after_inline_advance( &self, style: &ServoArc<ComputedValues>, current_inline_advance: Au, ) -> Au { let Some(font) = self.default_font.as_ref() else { return Au::zero(); }; let tab_size_multiplier = *self.tab_size_multiplier.get_or_init(|| { let root_style = self.shared_inline_styles.style.borrow(); let inherited_text_style = root_style.get_inherited_text(); let font_size = root_style.get_font().font_size.computed_size().into(); let letter_spacing = inherited_text_style .letter_spacing .0 .to_used_value(font_size); let word_spacing = inherited_text_style.word_spacing.to_used_value(font_size); // Each "space" character in the tab is considered both a letter and a word separator for // the purposes of applying word spacing and letter spacing. font.metrics.space_advance + word_spacing + letter_spacing }); let tab_stop_advance = match style.get_inherited_text().tab_size { style::values::generics::length::LengthOrNumber::Number(number_of_spaces) => { tab_size_multiplier.scale_by(number_of_spaces.0) }, // When a length is provided we do not apply word spacing or letter spacing. style::values::generics::length::LengthOrNumber::Length(length) => length.into(), }; if tab_stop_advance.is_zero() { return Au::zero(); } // From <https://drafts.csswg.org/css-text-4/#ref-for-tab-size-dfn> // > If this distance is less than 0.5ch, then the subsequent tab stop is used instead. // From <https://drafts.csswg.org/css-values/#ch> // > In the cases where it is impossible or impractical to determine the measure of the “0” // > glyph, it must be assumed to be 0.5em wide by 1em tall. let half_ch_advance = font .metrics .zero_horizontal_advance .unwrap_or(font.metrics.em_size.scale_by(0.5)) .scale_by(0.5); let number_of_tab_stops = (current_inline_advance + half_ch_advance).to_f32_px() / tab_stop_advance.to_f32_px(); let number_of_tab_stops = number_of_tab_stops.ceil(); tab_stop_advance.scale_by(number_of_tab_stops) - current_inline_advance } } impl InlineContainerState { fn new( style: ServoArc<ComputedValues>, flags: InlineContainerStateFlags, parent_container: Option<&InlineContainerState>, default_font: Option<FontRef>, ) -> Self { let font_metrics = default_font .as_ref() .map(|font| font.metrics.clone()) .unwrap_or_else(FontMetrics::empty); let mut baseline_offset = Au::zero(); let mut strut_block_sizes = { Self::get_block_sizes_with_style( effective_baseline_shift(&style, parent_container), &style, &font_metrics, &font_metrics, &flags, ) }; 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_alignment_baseline(), style.clone_baseline_shift(), &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: Cell::new(false), nested_strut_block_sizes: nested_block_sizes, strut_block_sizes, baseline_offset, default_font, font_metrics, } } fn get_block_sizes_with_style( baseline_shift: BaselineShift, style: &ComputedValues, font_metrics: &FontMetrics, font_metrics_of_first_font: &FontMetrics, flags: &InlineContainerStateFlags, ) -> LineBlockSizes { let line_height = line_height(style, font_metrics, flags); if !is_baseline_relative(baseline_shift) { 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, baseline_shift: BaselineShift, font_metrics: &FontMetrics, font_metrics_of_first_font: &FontMetrics, ) -> LineBlockSizes { Self::get_block_sizes_with_style( baseline_shift, &self.style, font_metrics, font_metrics_of_first_font, &self.flags, ) } fn get_cumulative_baseline_offset_for_child( &self, child_alignment_baseline: AlignmentBaseline, child_baseline_shift: BaselineShift, child_block_size: &LineBlockSizes, ) -> Au { let block_size = self.get_block_size_contribution( child_baseline_shift.clone(), &self.font_metrics, &self.font_metrics, ); self.baseline_offset + match child_alignment_baseline { AlignmentBaseline::Baseline => Au::zero(), AlignmentBaseline::TextTop => { child_block_size.size_for_baseline_positioning.ascent - self.font_metrics.ascent }, AlignmentBaseline::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) }, AlignmentBaseline::TextBottom => { self.font_metrics.descent - child_block_size.size_for_baseline_positioning.descent }, } + match child_baseline_shift { // `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. BaselineShift::Keyword( BaselineShiftKeyword::Top | BaselineShiftKeyword::Bottom | BaselineShiftKeyword::Center, ) => Au::zero(), BaselineShift::Keyword(BaselineShiftKeyword::Sub) => { block_size.resolve().scale_by(FONT_SUBSCRIPT_OFFSET_RATIO) }, BaselineShift::Keyword(BaselineShiftKeyword::Super) => { -block_size.resolve().scale_by(FONT_SUPERSCRIPT_OFFSET_RATIO) }, BaselineShift::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::default(); 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.base.translate_rect(pbm_physical_offset); // Apply baselines. fragment = fragment.with_baselines(baselines); // 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.base.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 = Arc::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 if self.is_block_container() => baselines.last, BaselineSource::Auto => 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_baseline_shift()) { 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 style = self.style(); let baseline_offset = ifc .current_inline_container_state() .get_cumulative_baseline_offset_for_child( style.clone_alignment_baseline(), style.clone_baseline_shift(), &contribution, ); contribution.adjust_for_baseline_offset(baseline_offset); (contribution, baseline_offset) } } impl FloatBox { fn layout_into_line_items(&self, layout: &mut InlineFormattingContextLayout) { let old_len = layout.positioning_context.len(); let fragment = Arc::new(self.layout( layout.layout_context, layout.positioning_context, layout.placement_state.containing_block, )); let new_len = layout.positioning_context.len(); 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, range: old_len..new_len, }, )); } } fn place_pending_floats(ifc: &mut InlineFormattingContextLayout, line_items: &[LineItem]) { for item in line_items.iter() { if let LineItem::Float(_, float_line_item) = item && float_line_item.needs_placement { ifc.place_float_fragment(float_line_item); } } } fn line_height( parent_style: &ComputedValues, font_metrics: &FontMetrics, flags: &InlineContainerStateFlags, ) -> 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(), }; // The line height of a single-line text input's inner text container is clamped to // the size of `normal`. // <https://html.spec.whatwg.org/multipage/#the-input-element-as-a-text-entry-widget> if flags.contains(InlineContainerStateFlags::IS_SINGLE_LINE_TEXT_INPUT) { line_height.max_assign(font_metrics.line_gap); } line_height } fn effective_baseline_shift( style: &ComputedValues, container: Option<&InlineContainerState>, ) -> BaselineShift { if container.is_none() { // If we are at the root of the inline formatting context, we shouldn't use the // computed `baseline-shift`, 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). BaselineShift::zero() } else { style.clone_baseline_shift() } } fn is_baseline_relative(baseline_shift: BaselineShift) -> bool { !matches!( baseline_shift, BaselineShift::Keyword( BaselineShiftKeyword::Top | BaselineShiftKeyword::Bottom | BaselineShiftKeyword::Center ) ) } /// 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.is_some_and(|pbm| !pbm.padding_border_sums.inline.is_zero()) } 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<'layout_data>, paragraph: ContentSizes, current_line: ContentSizes, /// Size for whitespace pending to be added to this line. pending_whitespace: ContentSizes, /// The size of the not yet cleared floats in the inline axis of the containing block. uncleared_floats: LogicalSides1D<ContentSizes>, /// The size of the already cleared floats in the inline axis of the containing block. cleared_floats: LogicalSides1D<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>, /// Whether the inline content size depends on block constraints. depends_on_block_constraints: bool, } impl<'layout_data> ContentSizesComputation<'layout_data> { fn traverse( mut self, inline_formatting_context: &InlineFormattingContext, ) -> InlineContentSizesResult { self.add_inline_size( inline_formatting_context.inline_start_for_first_line(self.constraint_space.into()), ); for inline_item in &inline_formatting_context.inline_items { self.process_item(inline_item, inline_formatting_context); } self.forced_line_break(); self.flush_floats(); 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.style.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; self.add_inline_size(pbm.inline_start); self.ending_inline_pbm_stack.push(pbm.inline_end); }, 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(); let parent_style = text_run.inline_styles().style.borrow(); for item in text_run.items.iter() { match item { TextRunItem::LineBreak { .. } => { // If this run is a forced line break, we *must* break the line // and start measuring from the inline origin once more. self.forced_line_break(); }, TextRunItem::Tab { .. } => { self.process_preserved_tab(&parent_style, inline_formatting_context) }, TextRunItem::TextSegment(segment) => { self.process_text_segment(&parent_style, segment) }, } } }, InlineItem::Atomic(atomic, offset_in_text, _level) => { // TODO: need to handle TextWrapMode::Nowrap. if self.had_content_yet_for_min_content && !inline_formatting_context .previous_character_prevents_soft_wrap_opportunity(*offset_in_text) { self.line_break_opportunity(); } self.commit_pending_whitespace(); let outer = self.outer_inline_content_sizes_of_float_or_atomic(&atomic.borrow()); self.current_line += outer; // TODO: need to handle TextWrapMode::Nowrap. if !inline_formatting_context .next_character_prevents_soft_wrap_opportunity(*offset_in_text) { self.line_break_opportunity(); } }, InlineItem::OutOfFlowFloatBox(float_box) => { let float_box = float_box.borrow(); let sizes = self.outer_inline_content_sizes_of_float_or_atomic(&float_box.contents); let style = &float_box.contents.style(); let container_writing_mode = self.constraint_space.style.writing_mode; let clear = Clear::from_style_and_container_writing_mode(style, container_writing_mode); self.clear_floats(clear); let float_side = FloatSide::from_style_and_container_writing_mode(style, container_writing_mode); match float_side.expect("A float box needs to float to some side") { FloatSide::InlineStart => self.uncleared_floats.start.union_assign(&sizes), FloatSide::InlineEnd => self.uncleared_floats.end.union_assign(&sizes), } }, InlineItem::BlockLevel(block_level) => { self.forced_line_break(); self.flush_floats(); let inline_content_sizes_result = compute_inline_content_sizes_for_block_level_boxes( std::slice::from_ref(block_level), self.layout_context, &self.constraint_space.into(), ); self.depends_on_block_constraints |= inline_content_sizes_result.depends_on_block_constraints; self.current_line = inline_content_sizes_result.sizes; self.forced_line_break(); }, InlineItem::OutOfFlowAbsolutelyPositionedBox(..) => {}, } } fn process_text_segment( &mut self, parent_style: &AtomicRef<'_, ServoArc<ComputedValues>>, segment: &TextRunSegment, ) { let style_text = 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. let break_at_start = segment.break_at_start && self.had_content_yet_for_min_content; for (run_index, run) in segment.runs.iter().enumerate() { // Break before each unbreakable run in this TextRun, except the first unless the // linebreaker was set to break before the first run. if can_wrap && (run_index != 0 || break_at_start) { self.line_break_opportunity(); } let advance = run.total_advance(); if run.is_whitespace() { if !matches!( style_text.white_space_collapse, WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces ) { if self.had_content_yet_for_min_content { if can_wrap { self.line_break_opportunity(); } else { 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.ends_with_whitespace() { self.line_break_opportunity(); } } } fn process_preserved_tab( &mut self, parent_style: &AtomicRef<'_, ServoArc<ComputedValues>>, inline_formatting_context: &InlineFormattingContext, ) { // If there is a preserved tab, that means that all whitespace is preserved. self.commit_pending_whitespace(); self.current_line.min_content += inline_formatting_context .next_tab_stop_after_inline_advance(parent_style, self.current_line.min_content); self.current_line.max_content += inline_formatting_context .next_tab_stop_after_inline_advance(parent_style, self.current_line.max_content); if parent_style.get_inherited_text().text_wrap_mode == TextWrapMode::Wrap { self.line_break_opportunity(); } } 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; } fn outer_inline_content_sizes_of_float_or_atomic( &mut self, context: &IndependentFormattingContext, ) -> ContentSizes { let result = context.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 |= result.depends_on_block_constraints; result.sizes } fn clear_floats(&mut self, clear: Clear) { match clear { Clear::InlineStart => { let start_floats = mem::take(&mut self.uncleared_floats.start); self.cleared_floats.start.max_assign(start_floats); }, Clear::InlineEnd => { let end_floats = mem::take(&mut self.uncleared_floats.end); self.cleared_floats.end.max_assign(end_floats); }, Clear::Both => { let start_floats = mem::take(&mut self.uncleared_floats.start); let end_floats = mem::take(&mut self.uncleared_floats.end); self.cleared_floats.start.max_assign(start_floats); self.cleared_floats.end.max_assign(end_floats); }, Clear::None => {}, } } fn flush_floats(&mut self) { self.clear_floats(Clear::Both); let start_floats = mem::take(&mut self.cleared_floats.start); let end_floats = mem::take(&mut self.cleared_floats.end); self.paragraph.union_assign(&start_floats); self.paragraph.union_assign(&end_floats); } /// 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(), uncleared_floats: LogicalSides1D::default(), cleared_floats: LogicalSides1D::default(), 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) } } pub(crate) struct BidiLevels<'a> { info: Option<BidiInfo<'a>>, } impl BidiLevels<'_> { fn level(&self, byte_offset_in_ifc_text: usize) -> Level { self.info .as_ref() .map_or_else(Level::ltr, |info| info.levels[byte_offset_in_ifc_text]) } } /// 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; } matches!( icu_properties::maps::line_break().get(character), ICULineBreak::Glue | ICULineBreak::WordJoiner | ICULineBreak::ZWJ ) }