| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| //! # Inline Formatting Context Layout |
| //! |
| //! Inline layout is divided into three phases: |
| //! |
| //! 1. Box Tree Construction |
| //! 2. Box to Line Layout |
| //! 3. Line to Fragment Layout |
| //! |
| //! The first phase happens during normal box tree constrution, while the second two phases happen |
| //! during fragment tree construction (sometimes called just "layout"). |
| //! |
| //! ## Box Tree Construction |
| //! |
| //! During box tree construction, DOM elements are transformed into a box tree. This phase collects |
| //! all of the inline boxes, text, atomic inline elements (boxes with `display: inline-block` or |
| //! `display: inline-table` as well as things like images and canvas), absolutely positioned blocks, |
| //! and floated blocks. |
| //! |
| //! During the last part of this phase, whitespace is collapsed and text is segmented into |
| //! [`TextRun`]s based on script, chosen font, and line breaking opportunities. In addition, default |
| //! fonts are selected for every inline box. Each segment of text is shaped using HarfBuzz and |
| //! turned into a series of glyphs, which all have a size and a position relative to the origin of |
| //! the [`TextRun`] (calculated in later phases). |
| //! |
| //! The code for this phase is mainly in `construct.rs`, but text handling can also be found in |
| //! `text_runs.rs.` |
| //! |
| //! ## Box to Line Layout |
| //! |
| //! During the first phase of fragment tree construction, box tree items are laid out into |
| //! [`LineItem`]s and fragmented based on line boundaries. This is where line breaking happens. This |
| //! part of layout fragments boxes and their contents across multiple lines while positioning floats |
| //! and making sure non-floated contents flow around them. In addition, all atomic elements are laid |
| //! out, which may descend into their respective trees and create fragments. Finally, absolutely |
| //! positioned content is collected in order to later hoist it to the containing block for |
| //! absolutes. |
| //! |
| //! Note that during this phase, layout does not know the final block position of content. Only |
| //! during line to fragment layout, are the final block positions calculated based on the line's |
| //! final content and its vertical alignment. Instead, positions and line heights are calculated |
| //! relative to the line's final baseline which will be determined in the final phase. |
| //! |
| //! [`LineItem`]s represent a particular set of content on a line. Currently this is represented by |
| //! a linear series of items that describe the line's hierarchy of inline boxes and content. The |
| //! item types are: |
| //! |
| //! - [`LineItem::InlineStartBoxPaddingBorderMargin`] |
| //! - [`LineItem::InlineEndBoxPaddingBorderMargin`] |
| //! - [`LineItem::TextRun`] |
| //! - [`LineItem::Atomic`] |
| //! - [`LineItem::AbsolutelyPositioned`] |
| //! - [`LineItem::Float`] |
| //! |
| //! The code for this can be found by looking for methods of the form `layout_into_line_item()`. |
| //! |
| //! ## Line to Fragment Layout |
| //! |
| //! During the second phase of fragment tree construction, the final block position of [`LineItem`]s |
| //! is calculated and they are converted into [`Fragment`]s. After layout, the [`LineItem`]s are |
| //! discarded and the new fragments are incorporated into the fragment tree. The final static |
| //! position of absolutely positioned content is calculated and it is hoisted to its containing |
| //! block via [`PositioningContext`]. |
| //! |
| //! The code for this phase, can mainly be found in `line.rs`. |
| //! |
| |
| pub mod construct; |
| pub mod inline_box; |
| pub mod line; |
| mod line_breaker; |
| pub mod text_run; |
| |
| use std::cell::{OnceCell, RefCell}; |
| use std::mem; |
| use std::rc::Rc; |
| |
| use app_units::{Au, MAX_AU}; |
| use bitflags::bitflags; |
| use construct::InlineFormattingContextBuilder; |
| use fonts::{ByteIndex, FontMetrics, GlyphStore}; |
| use inline_box::{InlineBox, InlineBoxContainerState, InlineBoxIdentifier, InlineBoxes}; |
| use line::{ |
| AbsolutelyPositionedLineItem, AtomicLineItem, FloatLineItem, LineItem, LineItemLayout, |
| TextRunLineItem, |
| }; |
| use line_breaker::LineBreaker; |
| use malloc_size_of_derive::MallocSizeOf; |
| use range::Range; |
| use script::layout_dom::ServoThreadSafeLayoutNode; |
| use servo_arc::Arc; |
| use style::Zero; |
| use style::computed_values::text_wrap_mode::T as TextWrapMode; |
| use style::computed_values::vertical_align::T as VerticalAlign; |
| use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; |
| use style::context::{QuirksMode, SharedStyleContext}; |
| use style::properties::ComputedValues; |
| use style::properties::style_structs::InheritedText; |
| use style::values::generics::box_::VerticalAlignKeyword; |
| use style::values::generics::font::LineHeight; |
| use style::values::specified::box_::BaselineSource; |
| use style::values::specified::text::TextAlignKeyword; |
| use style::values::specified::{TextAlignLast, TextJustify}; |
| use text_run::{ |
| TextRun, XI_LINE_BREAKING_CLASS_GL, XI_LINE_BREAKING_CLASS_WJ, XI_LINE_BREAKING_CLASS_ZWJ, |
| add_or_get_font, get_font_for_first_font_for_style, |
| }; |
| use unicode_bidi::{BidiInfo, Level}; |
| use webrender_api::FontInstanceKey; |
| use xi_unicode::linebreak_property; |
| |
| use super::float::{Clear, PlacementAmongFloats}; |
| use super::{CacheableLayoutResult, IndependentFloatOrAtomicLayoutResult}; |
| use crate::cell::ArcRefCell; |
| use crate::context::LayoutContext; |
| use crate::dom_traversal::NodeAndStyleInfo; |
| use crate::flow::CollapsibleWithParentStartMargin; |
| use crate::flow::float::{FloatBox, SequentialLayoutState}; |
| use crate::formatting_contexts::{Baselines, IndependentFormattingContext}; |
| use crate::fragment_tree::{ |
| BoxFragment, CollapsedBlockMargins, CollapsedMargin, Fragment, FragmentFlags, |
| PositioningFragment, |
| }; |
| use crate::geom::{LogicalRect, 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, 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. |
| pub(super) inline_items: Vec<ArcRefCell<InlineItem>>, |
| |
| /// The tree of inline boxes in this [`InlineFormattingContext`]. These are stored in |
| /// a flat array with each being given a [`InlineBoxIdentifier`]. |
| pub(super) inline_boxes: InlineBoxes, |
| |
| /// The text content of this inline formatting context. |
| pub(super) text_content: String, |
| |
| /// A store of font information for all the shaped segments in this formatting |
| /// context in order to avoid duplicating this information. |
| pub font_metrics: Vec<FontKeyAndMetrics>, |
| |
| /// The [`SharedInlineStyles`] for the root of this [`InlineFormattingContext`] that are used to |
| /// share styles with all [`TextRun`] children. |
| pub(super) shared_inline_styles: SharedInlineStyles, |
| |
| /// Whether this IFC contains the 1st formatted line of an element: |
| /// <https://www.w3.org/TR/css-pseudo-4/#first-formatted-line>. |
| pub(super) has_first_formatted_line: bool, |
| |
| /// Whether or not this [`InlineFormattingContext`] contains floats. |
| pub(super) contains_floats: bool, |
| |
| /// Whether or not this is an [`InlineFormattingContext`] for a single line text input's inner |
| /// text container. |
| pub(super) is_single_line_text_input: bool, |
| |
| /// Whether or not this is an [`InlineFormattingContext`] has right-to-left content, which |
| /// will require reordering during layout. |
| pub(super) has_right_to_left_content: bool, |
| } |
| |
| /// [`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 From<&NodeAndStyleInfo<'_>> for SharedInlineStyles { |
| fn from(info: &NodeAndStyleInfo) -> Self { |
| Self { |
| style: SharedStyle::new(info.style.clone()), |
| selected: SharedStyle::new(info.node.selected_style()), |
| } |
| } |
| } |
| |
| /// A collection of data used to cache [`FontMetrics`] in the [`InlineFormattingContext`] |
| #[derive(Debug, MallocSizeOf)] |
| pub(crate) struct FontKeyAndMetrics { |
| pub key: FontInstanceKey, |
| pub pt_size: Au, |
| pub metrics: FontMetrics, |
| } |
| |
| #[derive(Debug, MallocSizeOf)] |
| pub(crate) enum InlineItem { |
| StartInlineBox(ArcRefCell<InlineBox>), |
| EndInlineBox, |
| TextRun(ArcRefCell<TextRun>), |
| OutOfFlowAbsolutelyPositionedBox( |
| ArcRefCell<AbsolutelyPositionedBox>, |
| usize, /* offset_in_text */ |
| ), |
| OutOfFlowFloatBox(ArcRefCell<FloatBox>), |
| Atomic( |
| ArcRefCell<IndependentFormattingContext>, |
| usize, /* offset_in_text */ |
| Level, /* bidi_level */ |
| ), |
| } |
| |
| impl InlineItem { |
| pub(crate) fn repair_style( |
| &self, |
| context: &SharedStyleContext, |
| node: &ServoThreadSafeLayoutNode, |
| new_style: &Arc<ComputedValues>, |
| ) { |
| match self { |
| InlineItem::StartInlineBox(inline_box) => { |
| inline_box.borrow_mut().repair_style(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) |
| }, |
| } |
| } |
| |
| pub(crate) fn clear_fragment_layout_cache(&self) { |
| match self { |
| InlineItem::StartInlineBox(inline_box) => { |
| inline_box.borrow().base.clear_fragment_layout_cache() |
| }, |
| InlineItem::EndInlineBox | InlineItem::TextRun(..) => {}, |
| InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => { |
| positioned_box |
| .borrow() |
| .context |
| .base |
| .clear_fragment_layout_cache(); |
| }, |
| InlineItem::OutOfFlowFloatBox(float_box) => float_box |
| .borrow() |
| .contents |
| .base |
| .clear_fragment_layout_cache(), |
| InlineItem::Atomic(independent_formatting_context, ..) => { |
| independent_formatting_context |
| .borrow() |
| .base |
| .clear_fragment_layout_cache() |
| }, |
| } |
| } |
| |
| 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) |
| }, |
| } |
| } |
| |
| pub(crate) fn with_base_mut(&mut self, callback: impl Fn(&mut LayoutBoxBase)) { |
| 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) |
| }, |
| } |
| } |
| } |
| |
| /// Information about the current line under construction for a particular |
| /// [`InlineFormattingContextLayout`]. This tracks position and size information while |
| /// [`LineItem`]s are collected and is used as input when those [`LineItem`]s are |
| /// converted into [`Fragment`]s during the final phase of line layout. Note that this |
| /// does not store the [`LineItem`]s themselves, as they are stored as part of the |
| /// nesting state in the [`InlineFormattingContextLayout`]. |
| struct LineUnderConstruction { |
| /// The position where this line will start once it is laid out. This includes any |
| /// offset from `text-indent`. |
| start_position: LogicalVec2<Au>, |
| |
| /// The current inline position in the line being laid out into [`LineItem`]s in this |
| /// [`InlineFormattingContext`] independent of the depth in the nesting level. |
| inline_position: Au, |
| |
| /// The maximum block size of all boxes that ended and are in progress in this line. |
| /// This uses [`LineBlockSizes`] instead of a simple value, because the final block size |
| /// depends on vertical alignment. |
| max_block_size: LineBlockSizes, |
| |
| /// Whether any active linebox has added a glyph or atomic element to this line, which |
| /// indicates that the next run that exceeds the line length can cause a line break. |
| has_content: bool, |
| |
| /// Whether or not there are floats that did not fit on the current line. Before |
| /// the [`LineItem`]s of this line are laid out, these floats will need to be |
| /// placed directly below this line, but still as children of this line's Fragments. |
| has_floats_waiting_to_be_placed: bool, |
| |
| /// A rectangular area (relative to the containing block / inline formatting |
| /// context boundaries) where we can fit the line box without overlapping floats. |
| /// Note that when this is not empty, its start corner takes precedence over |
| /// [`LineUnderConstruction::start_position`]. |
| placement_among_floats: OnceCell<LogicalRect<Au>>, |
| |
| /// The LineItems for the current line under construction that have already |
| /// been committed to this line. |
| line_items: Vec<LineItem>, |
| } |
| |
| impl LineUnderConstruction { |
| fn new(start_position: LogicalVec2<Au>) -> Self { |
| Self { |
| inline_position: start_position.inline, |
| start_position, |
| max_block_size: LineBlockSizes::zero(), |
| has_content: false, |
| has_floats_waiting_to_be_placed: false, |
| placement_among_floats: OnceCell::new(), |
| line_items: Vec::new(), |
| } |
| } |
| |
| fn line_block_start_considering_placement_among_floats(&self) -> Au { |
| match self.placement_among_floats.get() { |
| Some(placement_among_floats) => placement_among_floats.start_corner.block, |
| None => self.start_position.block, |
| } |
| } |
| |
| fn replace_placement_among_floats(&mut self, new_placement: LogicalRect<Au>) { |
| self.placement_among_floats.take(); |
| let _ = self.placement_among_floats.set(new_placement); |
| } |
| |
| /// Trim the trailing whitespace in this line and return the width of the whitespace trimmed. |
| fn trim_trailing_whitespace(&mut self) -> Au { |
| // From <https://www.w3.org/TR/css-text-3/#white-space-phase-2>: |
| // > 3. A sequence of collapsible spaces at the end of a line is removed, |
| // > as well as any trailing U+1680   OGHAM SPACE MARK whose white-space |
| // > property is normal, nowrap, or pre-line. |
| let mut whitespace_trimmed = Au::zero(); |
| for item in self.line_items.iter_mut().rev() { |
| if !item.trim_whitespace_at_end(&mut whitespace_trimmed) { |
| break; |
| } |
| } |
| |
| whitespace_trimmed |
| } |
| |
| /// Count the number of justification opportunities in this line. |
| fn count_justification_opportunities(&self) -> usize { |
| self.line_items |
| .iter() |
| .filter_map(|item| match item { |
| LineItem::TextRun(_, text_run) => Some( |
| text_run |
| .text |
| .iter() |
| .map(|glyph_store| glyph_store.total_word_separators()) |
| .sum::<usize>(), |
| ), |
| _ => None, |
| }) |
| .sum() |
| } |
| } |
| |
| /// A block size relative to a line's final baseline. This is to track the size |
| /// contribution of a particular element of a line above and below the baseline. |
| /// These sizes can be combined with other baseline relative sizes before the |
| /// final baseline position is known. The values here are relative to the |
| /// overall line's baseline and *not* the nested baseline of an inline box. |
| #[derive(Clone, Debug)] |
| struct BaselineRelativeSize { |
| /// The ascent above the baseline, where a positive value means a larger |
| /// ascent. Thus, the top of this size contribution is `baseline_offset - |
| /// ascent`. |
| ascent: Au, |
| |
| /// The descent below the baseline, where a positive value means a larger |
| /// descent. Thus, the bottom of this size contribution is `baseline_offset + |
| /// descent`. |
| descent: Au, |
| } |
| |
| impl BaselineRelativeSize { |
| fn zero() -> Self { |
| Self { |
| ascent: Au::zero(), |
| descent: Au::zero(), |
| } |
| } |
| |
| fn max(&self, other: &Self) -> Self { |
| BaselineRelativeSize { |
| ascent: self.ascent.max(other.ascent), |
| descent: self.descent.max(other.descent), |
| } |
| } |
| |
| /// Given an offset from the line's root baseline, adjust this [`BaselineRelativeSize`] |
| /// by that offset. This is used to adjust a [`BaselineRelativeSize`] for different kinds |
| /// of baseline-relative `vertical-align`. This will "move" measured size of a particular |
| /// inline box's block size. For example, in the following HTML: |
| /// |
| /// ```html |
| /// <div> |
| /// <span style="vertical-align: 5px">child content</span> |
| /// </div> |
| /// ```` |
| /// |
| /// If this [`BaselineRelativeSize`] is for the `<span>` then the adjustment |
| /// passed here would be equivalent to -5px. |
| fn adjust_for_nested_baseline_offset(&mut self, baseline_offset: Au) { |
| self.ascent -= baseline_offset; |
| self.descent += baseline_offset; |
| } |
| } |
| |
| #[derive(Clone, Debug)] |
| struct LineBlockSizes { |
| line_height: Au, |
| baseline_relative_size_for_line_height: Option<BaselineRelativeSize>, |
| size_for_baseline_positioning: BaselineRelativeSize, |
| } |
| |
| impl LineBlockSizes { |
| fn zero() -> Self { |
| LineBlockSizes { |
| line_height: Au::zero(), |
| baseline_relative_size_for_line_height: None, |
| size_for_baseline_positioning: BaselineRelativeSize::zero(), |
| } |
| } |
| |
| fn resolve(&self) -> Au { |
| let height_from_ascent_and_descent = self |
| .baseline_relative_size_for_line_height |
| .as_ref() |
| .map(|size| (size.ascent + size.descent).abs()) |
| .unwrap_or_else(Au::zero); |
| self.line_height.max(height_from_ascent_and_descent) |
| } |
| |
| fn max(&self, other: &LineBlockSizes) -> LineBlockSizes { |
| let baseline_relative_size = match ( |
| self.baseline_relative_size_for_line_height.as_ref(), |
| other.baseline_relative_size_for_line_height.as_ref(), |
| ) { |
| (Some(our_size), Some(other_size)) => Some(our_size.max(other_size)), |
| (our_size, other_size) => our_size.or(other_size).cloned(), |
| }; |
| Self { |
| line_height: self.line_height.max(other.line_height), |
| baseline_relative_size_for_line_height: baseline_relative_size, |
| size_for_baseline_positioning: self |
| .size_for_baseline_positioning |
| .max(&other.size_for_baseline_positioning), |
| } |
| } |
| |
| fn max_assign(&mut self, other: &LineBlockSizes) { |
| *self = self.max(other); |
| } |
| |
| fn adjust_for_baseline_offset(&mut self, baseline_offset: Au) { |
| if let Some(size) = self.baseline_relative_size_for_line_height.as_mut() { |
| size.adjust_for_nested_baseline_offset(baseline_offset) |
| } |
| self.size_for_baseline_positioning |
| .adjust_for_nested_baseline_offset(baseline_offset); |
| } |
| |
| /// From <https://drafts.csswg.org/css2/visudet.html#line-height>: |
| /// > The inline-level boxes are aligned vertically according to their 'vertical-align' |
| /// > property. In case they are aligned 'top' or 'bottom', they must be aligned so as |
| /// > to minimize the line box height. If such boxes are tall enough, there are multiple |
| /// > solutions and CSS 2 does not define the position of the line box's baseline (i.e., |
| /// > the position of the strut, see below). |
| fn find_baseline_offset(&self) -> Au { |
| match self.baseline_relative_size_for_line_height.as_ref() { |
| Some(size) => size.ascent, |
| None => { |
| // This is the case mentinoned above where there are multiple solutions. |
| // This code is putting the baseline roughly in the middle of the line. |
| let leading = self.resolve() - |
| (self.size_for_baseline_positioning.ascent + |
| self.size_for_baseline_positioning.descent); |
| leading.scale_by(0.5) + self.size_for_baseline_positioning.ascent |
| }, |
| } |
| } |
| } |
| |
| /// The current unbreakable segment under construction for an inline formatting context. |
| /// Items accumulate here until we reach a soft line break opportunity during processing |
| /// of inline content or we reach the end of the formatting context. |
| struct UnbreakableSegmentUnderConstruction { |
| /// The size of this unbreakable segment in both dimension. |
| inline_size: Au, |
| |
| /// The maximum block size that this segment has. This uses [`LineBlockSizes`] instead of a |
| /// simple value, because the final block size depends on vertical alignment. |
| max_block_size: LineBlockSizes, |
| |
| /// The LineItems for the segment under construction |
| line_items: Vec<LineItem>, |
| |
| /// The depth in the inline box hierarchy at the start of this segment. This is used |
| /// to prefix this segment when it is pushed to a new line. |
| inline_box_hierarchy_depth: Option<usize>, |
| |
| /// Whether any active linebox has added a glyph or atomic element to this line |
| /// segment, which indicates that the next run that exceeds the line length can cause |
| /// a line break. |
| has_content: bool, |
| |
| /// The inline size of any trailing whitespace in this segment. |
| trailing_whitespace_size: Au, |
| } |
| |
| impl UnbreakableSegmentUnderConstruction { |
| fn new() -> Self { |
| Self { |
| inline_size: Au::zero(), |
| max_block_size: LineBlockSizes { |
| line_height: Au::zero(), |
| baseline_relative_size_for_line_height: None, |
| size_for_baseline_positioning: BaselineRelativeSize::zero(), |
| }, |
| line_items: Vec::new(), |
| inline_box_hierarchy_depth: None, |
| has_content: false, |
| trailing_whitespace_size: Au::zero(), |
| } |
| } |
| |
| /// Reset this segment after its contents have been committed to a line. |
| fn reset(&mut self) { |
| assert!(self.line_items.is_empty()); // Preserve allocated memory. |
| self.inline_size = Au::zero(); |
| self.max_block_size = LineBlockSizes::zero(); |
| self.inline_box_hierarchy_depth = None; |
| self.has_content = false; |
| self.trailing_whitespace_size = Au::zero(); |
| } |
| |
| /// Push a single line item to this segment. In addition, record the inline box |
| /// hierarchy depth if this is the first segment. The hierarchy depth is used to |
| /// duplicate the necessary `StartInlineBox` tokens if this segment is ultimately |
| /// placed on a new empty line. |
| fn push_line_item(&mut self, line_item: LineItem, inline_box_hierarchy_depth: usize) { |
| if self.line_items.is_empty() { |
| self.inline_box_hierarchy_depth = Some(inline_box_hierarchy_depth); |
| } |
| self.line_items.push(line_item); |
| } |
| |
| /// Trim whitespace from the beginning of this UnbreakbleSegmentUnderConstruction. |
| /// |
| /// From <https://www.w3.org/TR/css-text-3/#white-space-phase-2>: |
| /// |
| /// > Then, the entire block is rendered. Inlines are laid out, taking bidi |
| /// > reordering into account, and wrapping as specified by the text-wrap |
| /// > property. As each line is laid out, |
| /// > 1. A sequence of collapsible spaces at the beginning of a line is removed. |
| /// |
| /// This prevents whitespace from being added to the beginning of a line. |
| fn trim_leading_whitespace(&mut self) { |
| let mut whitespace_trimmed = Au::zero(); |
| for item in self.line_items.iter_mut() { |
| if !item.trim_whitespace_at_start(&mut whitespace_trimmed) { |
| break; |
| } |
| } |
| self.inline_size -= whitespace_trimmed; |
| } |
| } |
| |
| bitflags! { |
| pub struct InlineContainerStateFlags: u8 { |
| const CREATE_STRUT = 0b0001; |
| const IS_SINGLE_LINE_TEXT_INPUT = 0b0010; |
| } |
| } |
| |
| pub(super) struct InlineContainerState { |
| /// The style of this inline container. |
| style: Arc<ComputedValues>, |
| |
| /// Flags which describe details of this [`InlineContainerState`]. |
| flags: InlineContainerStateFlags, |
| |
| /// Whether or not we have processed any content (an atomic element or text) for |
| /// this inline box on the current line OR any previous line. |
| has_content: RefCell<bool>, |
| |
| /// The block size contribution of this container's default font ie the size of the |
| /// "strut." Whether this is integrated into the [`Self::nested_strut_block_sizes`] |
| /// depends on the line-height quirk described in |
| /// <https://quirks.spec.whatwg.org/#the-line-height-calculation-quirk>. |
| strut_block_sizes: LineBlockSizes, |
| |
| /// The strut block size of this inline container maxed with the strut block |
| /// sizes of all inline container ancestors. In quirks mode, this will be |
| /// zero, until we know that an element has inline content. |
| nested_strut_block_sizes: LineBlockSizes, |
| |
| /// The baseline offset of this container from the baseline of the line. The is the |
| /// cumulative offset of this container and all of its parents. In contrast to the |
| /// `vertical-align` property a positive value indicates an offset "below" the |
| /// baseline while a negative value indicates one "above" it (when the block direction |
| /// is vertical). |
| pub baseline_offset: Au, |
| |
| /// The font metrics of the non-fallback font for this container. |
| font_metrics: FontMetrics, |
| } |
| |
| pub(super) struct InlineFormattingContextLayout<'layout_data> { |
| positioning_context: &'layout_data mut PositioningContext, |
| containing_block: &'layout_data ContainingBlock<'layout_data>, |
| sequential_layout_state: Option<&'layout_data mut SequentialLayoutState>, |
| layout_context: &'layout_data LayoutContext<'layout_data>, |
| |
| /// The [`InlineFormattingContext`] that we are laying out. |
| ifc: &'layout_data InlineFormattingContext, |
| |
| /// The [`InlineContainerState`] for the container formed by the root of the |
| /// [`InlineFormattingContext`]. This is effectively the "root inline box" described |
| /// by <https://drafts.csswg.org/css-inline/#model>: |
| /// |
| /// > The block container also generates a root inline box, which is an anonymous |
| /// > inline box that holds all of its inline-level contents. (Thus, all text in an |
| /// > inline formatting context is directly contained by an inline box, whether the root |
| /// > inline box or one of its descendants.) The root inline box inherits from its |
| /// > parent block container, but is otherwise unstyleable. |
| root_nesting_level: InlineContainerState, |
| |
| /// A stack of [`InlineBoxContainerState`] that is used to produce [`LineItem`]s either when we |
| /// reach the end of an inline box or when we reach the end of a line. Only at the end |
| /// of the inline box is the state popped from the stack. |
| inline_box_state_stack: Vec<Rc<InlineBoxContainerState>>, |
| |
| /// A collection of [`InlineBoxContainerState`] of all the inlines that are present |
| /// in this inline formatting context. We keep this as well as the stack, so that we |
| /// can access them during line layout, which may happen after relevant [`InlineBoxContainerState`]s |
| /// have been popped of the the stack. |
| inline_box_states: Vec<Rc<InlineBoxContainerState>>, |
| |
| /// A vector of fragment that are laid out. This includes one [`Fragment::Positioning`] |
| /// per line that is currently laid out plus fragments for all floats, which |
| /// are currently laid out at the top-level of each [`InlineFormattingContext`]. |
| fragments: Vec<Fragment>, |
| |
| /// Information about the line currently being laid out into [`LineItem`]s. |
| current_line: LineUnderConstruction, |
| |
| /// Information about the unbreakable line segment currently being laid out into [`LineItem`]s. |
| current_line_segment: UnbreakableSegmentUnderConstruction, |
| |
| /// After a forced line break (for instance from a `<br>` element) we wait to actually |
| /// break the line until seeing more content. This allows ongoing inline boxes to finish, |
| /// since in the case where they have no more content they should not be on the next |
| /// line. |
| /// |
| /// For instance: |
| /// |
| /// ``` html |
| /// <span style="border-right: 30px solid blue;"> |
| /// first line<br> |
| /// </span> |
| /// second line |
| /// ``` |
| /// |
| /// In this case, the `<span>` should not extend to the second line. If we linebreak |
| /// as soon as we encounter the `<br>` the `<span>`'s ending inline borders would be |
| /// placed on the second line, because we add those borders in |
| /// [`InlineFormattingContextLayout::finish_inline_box()`]. |
| linebreak_before_new_content: bool, |
| |
| /// When a `<br>` element has `clear`, this needs to be applied after the linebreak, |
| /// which will be processed *after* the `<br>` element is processed. This member |
| /// stores any deferred `clear` to apply after a linebreak. |
| deferred_br_clear: Clear, |
| |
| /// Whether or not a soft wrap opportunity is queued. Soft wrap opportunities are |
| /// queued after replaced content and they are processed when the next text content |
| /// is encountered. |
| pub have_deferred_soft_wrap_opportunity: bool, |
| |
| /// Whether or not this InlineFormattingContext has processed any in flow content at all. |
| had_inflow_content: bool, |
| |
| /// Whether or not the layout of this InlineFormattingContext depends on the block size |
| /// of its container for the purposes of flexbox layout. |
| depends_on_block_constraints: bool, |
| |
| /// The currently white-space-collapse setting of this line. This is stored on the |
| /// [`InlineFormattingContextLayout`] because when a soft wrap opportunity is defined |
| /// by the boundary between two characters, the white-space-collapse property of their |
| /// nearest common ancestor is used. |
| white_space_collapse: WhiteSpaceCollapse, |
| |
| /// The currently text-wrap-mode setting of this line. This is stored on the |
| /// [`InlineFormattingContextLayout`] because when a soft wrap opportunity is defined |
| /// by the boundary between two characters, the text-wrap-mode property of their nearest |
| /// common ancestor is used. |
| text_wrap_mode: TextWrapMode, |
| |
| /// The offset of the first and last baselines in the inline formatting context that we |
| /// are laying out. This is used to propagate baselines to the ancestors of |
| /// `display: inline-block` elements and table content. |
| baselines: Baselines, |
| } |
| |
| impl InlineFormattingContextLayout<'_> { |
| fn current_inline_container_state(&self) -> &InlineContainerState { |
| match self.inline_box_state_stack.last() { |
| Some(inline_box_state) => &inline_box_state.base, |
| None => &self.root_nesting_level, |
| } |
| } |
| |
| fn current_inline_box_identifier(&self) -> Option<InlineBoxIdentifier> { |
| self.inline_box_state_stack |
| .last() |
| .map(|state| state.identifier) |
| } |
| |
| fn current_line_max_block_size_including_nested_containers(&self) -> LineBlockSizes { |
| self.current_inline_container_state() |
| .nested_strut_block_sizes |
| .max(&self.current_line.max_block_size) |
| } |
| |
| fn propagate_current_nesting_level_white_space_style(&mut self) { |
| let style = match self.inline_box_state_stack.last() { |
| Some(inline_box_state) => &inline_box_state.base.style, |
| None => self.containing_block.style, |
| }; |
| let style_text = style.get_inherited_text(); |
| self.white_space_collapse = style_text.white_space_collapse; |
| self.text_wrap_mode = style_text.text_wrap_mode; |
| } |
| |
| fn processing_br_element(&self) -> bool { |
| self.inline_box_state_stack |
| .last() |
| .map(|state| { |
| state |
| .base_fragment_info |
| .flags |
| .contains(FragmentFlags::IS_BR_ELEMENT) |
| }) |
| .unwrap_or(false) |
| } |
| |
| /// Start laying out a particular [`InlineBox`] into line items. This will push |
| /// a new [`InlineBoxContainerState`] onto [`Self::inline_box_state_stack`]. |
| fn start_inline_box(&mut self, inline_box: &InlineBox) { |
| let inline_box_state = InlineBoxContainerState::new( |
| inline_box, |
| self.containing_block, |
| self.layout_context, |
| self.current_inline_container_state(), |
| inline_box.is_last_split, |
| inline_box |
| .default_font_index |
| .map(|index| &self.ifc.font_metrics[index].metrics), |
| ); |
| |
| self.depends_on_block_constraints |= inline_box |
| .base |
| .style |
| .depends_on_block_constraints_due_to_relative_positioning( |
| self.containing_block.style.writing_mode, |
| ); |
| |
| // If we are starting a `<br>` element prepare to clear after its deferred linebreak has been |
| // processed. Note that a `<br>` is composed of the element itself and the inner pseudo-element |
| // with the actual linebreak. Both will have this `FragmentFlag`; that's why this code only |
| // sets `deferred_br_clear` if it isn't set yet. |
| if inline_box_state |
| .base_fragment_info |
| .flags |
| .contains(FragmentFlags::IS_BR_ELEMENT) && |
| self.deferred_br_clear == Clear::None |
| { |
| self.deferred_br_clear = Clear::from_style_and_container_writing_mode( |
| &inline_box_state.base.style, |
| self.containing_block.style.writing_mode, |
| ); |
| } |
| |
| if inline_box.is_first_split { |
| self.current_line_segment.inline_size += inline_box_state.pbm.padding.inline_start + |
| inline_box_state.pbm.border.inline_start + |
| inline_box_state.pbm.margin.inline_start.auto_is(Au::zero); |
| self.current_line_segment |
| .line_items |
| .push(LineItem::InlineStartBoxPaddingBorderMargin( |
| inline_box.identifier, |
| )); |
| } |
| |
| let inline_box_state = Rc::new(inline_box_state); |
| |
| // Push the state onto the IFC-wide collection of states. Inline boxes are numbered in |
| // the order that they are encountered, so this should correspond to the order they |
| // are pushed onto `self.inline_box_states`. |
| assert_eq!( |
| self.inline_box_states.len(), |
| inline_box.identifier.index_in_inline_boxes as usize |
| ); |
| self.inline_box_states.push(inline_box_state.clone()); |
| self.inline_box_state_stack.push(inline_box_state); |
| } |
| |
| /// Finish laying out a particular [`InlineBox`] into line items. This will |
| /// pop its state off of [`Self::inline_box_state_stack`]. |
| fn finish_inline_box(&mut self) { |
| let inline_box_state = match self.inline_box_state_stack.pop() { |
| Some(inline_box_state) => inline_box_state, |
| None => return, // We are at the root. |
| }; |
| |
| self.current_line_segment |
| .max_block_size |
| .max_assign(&inline_box_state.base.nested_strut_block_sizes); |
| |
| // If the inline box that we just finished had any content at all, we want to propagate |
| // the `white-space` property of its parent to future inline children. This is because |
| // when a soft wrap opportunity is defined by the boundary between two elements, the |
| // `white-space` used is that of their nearest common ancestor. |
| if *inline_box_state.base.has_content.borrow() { |
| self.propagate_current_nesting_level_white_space_style(); |
| } |
| |
| if inline_box_state.is_last_fragment { |
| let pbm_end = inline_box_state.pbm.padding.inline_end + |
| inline_box_state.pbm.border.inline_end + |
| inline_box_state.pbm.margin.inline_end.auto_is(Au::zero); |
| self.current_line_segment.inline_size += pbm_end; |
| self.current_line_segment |
| .line_items |
| .push(LineItem::InlineEndBoxPaddingBorderMargin( |
| inline_box_state.identifier, |
| )) |
| } |
| } |
| |
| fn finish_last_line(&mut self) { |
| // We are at the end of the IFC, and we need to do a few things to make sure that |
| // the current segment is committed and that the final line is finished. |
| // |
| // A soft wrap opportunity makes it so the current segment is placed on a new line |
| // if it doesn't fit on the current line under construction. |
| self.process_soft_wrap_opportunity(); |
| |
| // `process_soft_line_wrap_opportunity` does not commit the segment to a line if |
| // there is no line wrapping, so this forces the segment into the current line. |
| self.commit_current_segment_to_line(); |
| |
| // Finally we finish the line itself and convert all of the LineItems into |
| // fragments. |
| self.finish_current_line_and_reset(true /* last_line_or_forced_line_break */); |
| } |
| |
| /// Finish layout of all inline boxes for the current line. This will gather all |
| /// [`LineItem`]s and turn them into [`Fragment`]s, then reset the |
| /// [`InlineFormattingContextLayout`] preparing it for laying out a new line. |
| fn finish_current_line_and_reset(&mut self, last_line_or_forced_line_break: bool) { |
| let whitespace_trimmed = self.current_line.trim_trailing_whitespace(); |
| let (inline_start_position, justification_adjustment) = self |
| .calculate_current_line_inline_start_and_justification_adjustment( |
| whitespace_trimmed, |
| last_line_or_forced_line_break, |
| ); |
| |
| let block_start_position = self |
| .current_line |
| .line_block_start_considering_placement_among_floats(); |
| let had_inline_advance = |
| self.current_line.inline_position != self.current_line.start_position.inline; |
| |
| let effective_block_advance = if self.current_line.has_content || |
| had_inline_advance || |
| self.linebreak_before_new_content |
| { |
| self.current_line_max_block_size_including_nested_containers() |
| } else { |
| LineBlockSizes::zero() |
| }; |
| |
| let resolved_block_advance = effective_block_advance.resolve(); |
| let mut block_end_position = block_start_position + resolved_block_advance; |
| if let Some(sequential_layout_state) = self.sequential_layout_state.as_mut() { |
| // This amount includes both the block size of the line and any extra space |
| // added to move the line down in order to avoid overlapping floats. |
| let increment = block_end_position - self.current_line.start_position.block; |
| sequential_layout_state.advance_block_position(increment); |
| |
| // This newline may have been triggered by a `<br>` with clearance, in which case we |
| // want to make sure that we make space not only for the current line, but any clearance |
| // from floats. |
| if let Some(clearance) = sequential_layout_state |
| .calculate_clearance(self.deferred_br_clear, &CollapsedMargin::zero()) |
| { |
| sequential_layout_state.advance_block_position(clearance); |
| block_end_position += clearance; |
| }; |
| self.deferred_br_clear = Clear::None; |
| } |
| |
| // Set up the new line now that we no longer need the old one. |
| let mut line_to_layout = std::mem::replace( |
| &mut self.current_line, |
| LineUnderConstruction::new(LogicalVec2 { |
| inline: Au::zero(), |
| block: block_end_position, |
| }), |
| ); |
| |
| if line_to_layout.has_floats_waiting_to_be_placed { |
| place_pending_floats(self, &mut line_to_layout.line_items); |
| } |
| |
| let start_position = LogicalVec2 { |
| block: block_start_position, |
| inline: inline_start_position, |
| }; |
| |
| let baseline_offset = effective_block_advance.find_baseline_offset(); |
| let start_positioning_context_length = self.positioning_context.len(); |
| let fragments = LineItemLayout::layout_line_items( |
| self, |
| line_to_layout.line_items, |
| start_position, |
| &effective_block_advance, |
| justification_adjustment, |
| ); |
| |
| // If the line doesn't have any fragments, we don't need to add a containing fragment for it. |
| if fragments.is_empty() && |
| self.positioning_context.len() == start_positioning_context_length |
| { |
| return; |
| } |
| |
| if line_to_layout.has_content { |
| let baseline = baseline_offset + block_start_position; |
| self.baselines.first.get_or_insert(baseline); |
| self.baselines.last = Some(baseline); |
| } |
| |
| // The inline part of this start offset was taken into account when determining |
| // the inline start of the line in `calculate_inline_start_for_current_line` so |
| // we do not need to include it in the `start_corner` of the line's main Fragment. |
| let start_corner = LogicalVec2 { |
| inline: Au::zero(), |
| block: block_start_position, |
| }; |
| |
| let logical_origin_in_physical_coordinates = |
| start_corner.to_physical_vector(self.containing_block.style.writing_mode); |
| self.positioning_context |
| .adjust_static_position_of_hoisted_fragments_with_offset( |
| &logical_origin_in_physical_coordinates, |
| start_positioning_context_length, |
| ); |
| |
| let physical_line_rect = LogicalRect { |
| start_corner, |
| size: LogicalVec2 { |
| inline: self.containing_block.size.inline, |
| block: effective_block_advance.resolve(), |
| }, |
| } |
| .as_physical(Some(self.containing_block)); |
| self.fragments |
| .push(Fragment::Positioning(PositioningFragment::new_anonymous( |
| self.root_nesting_level.style.clone(), |
| physical_line_rect, |
| fragments, |
| ))); |
| } |
| |
| /// Given the amount of whitespace trimmed from the line and taking into consideration |
| /// the `text-align` property, calculate where the line under construction starts in |
| /// the inline axis as well as the adjustment needed for every justification opportunity |
| /// to account for `text-align: justify`. |
| fn calculate_current_line_inline_start_and_justification_adjustment( |
| &self, |
| whitespace_trimmed: Au, |
| last_line_or_forced_line_break: bool, |
| ) -> (Au, Au) { |
| enum TextAlign { |
| Start, |
| Center, |
| End, |
| } |
| let style = self.containing_block.style; |
| let mut text_align_keyword = style.clone_text_align(); |
| |
| if last_line_or_forced_line_break { |
| text_align_keyword = match style.clone_text_align_last() { |
| TextAlignLast::Auto if text_align_keyword == TextAlignKeyword::Justify => { |
| TextAlignKeyword::Start |
| }, |
| TextAlignLast::Auto => text_align_keyword, |
| TextAlignLast::Start => TextAlignKeyword::Start, |
| TextAlignLast::End => TextAlignKeyword::End, |
| TextAlignLast::Left => TextAlignKeyword::Left, |
| TextAlignLast::Right => TextAlignKeyword::Right, |
| TextAlignLast::Center => TextAlignKeyword::Center, |
| TextAlignLast::Justify => TextAlignKeyword::Justify, |
| }; |
| } |
| |
| let text_align = match text_align_keyword { |
| TextAlignKeyword::Start => TextAlign::Start, |
| TextAlignKeyword::Center | TextAlignKeyword::MozCenter => TextAlign::Center, |
| TextAlignKeyword::End => TextAlign::End, |
| TextAlignKeyword::Left | TextAlignKeyword::MozLeft => { |
| if style.writing_mode.line_left_is_inline_start() { |
| TextAlign::Start |
| } else { |
| TextAlign::End |
| } |
| }, |
| TextAlignKeyword::Right | TextAlignKeyword::MozRight => { |
| if style.writing_mode.line_left_is_inline_start() { |
| TextAlign::End |
| } else { |
| TextAlign::Start |
| } |
| }, |
| TextAlignKeyword::Justify => TextAlign::Start, |
| }; |
| |
| let (line_start, available_space) = match self.current_line.placement_among_floats.get() { |
| Some(placement_among_floats) => ( |
| placement_among_floats.start_corner.inline, |
| placement_among_floats.size.inline, |
| ), |
| None => (Au::zero(), self.containing_block.size.inline), |
| }; |
| |
| // Properly handling text-indent requires that we do not align the text |
| // into the text-indent. |
| // See <https://drafts.csswg.org/css-text/#text-indent-property> |
| // "This property specifies the indentation applied to lines of inline content in |
| // a block. The indent is treated as a margin applied to the start edge of the |
| // line box." |
| let text_indent = self.current_line.start_position.inline; |
| let line_length = self.current_line.inline_position - whitespace_trimmed - text_indent; |
| let adjusted_line_start = line_start + |
| match text_align { |
| TextAlign::Start => text_indent, |
| TextAlign::End => (available_space - line_length).max(text_indent), |
| TextAlign::Center => (available_space - line_length + text_indent) |
| .scale_by(0.5) |
| .max(text_indent), |
| }; |
| |
| // Calculate the justification adjustment. This is simply the remaining space on the line, |
| // dividided by the number of justficiation opportunities that we recorded when building |
| // the line. |
| let text_justify = self.containing_block.style.clone_text_justify(); |
| let justification_adjustment = match (text_align_keyword, text_justify) { |
| // `text-justify: none` should disable text justification. |
| // TODO: Handle more `text-justify` values. |
| (TextAlignKeyword::Justify, TextJustify::None) => Au::zero(), |
| (TextAlignKeyword::Justify, _) => { |
| match self.current_line.count_justification_opportunities() { |
| 0 => Au::zero(), |
| num_justification_opportunities => { |
| (available_space - text_indent - line_length) |
| .scale_by(1. / num_justification_opportunities as f32) |
| }, |
| } |
| }, |
| _ => Au::zero(), |
| }; |
| |
| // If the content overflows the line, then justification adjustment will become negative. In |
| // that case, do not make any adjustment for justification. |
| let justification_adjustment = justification_adjustment.max(Au::zero()); |
| |
| (adjusted_line_start, justification_adjustment) |
| } |
| |
| fn place_float_fragment(&mut self, fragment: &mut BoxFragment) { |
| let state = self |
| .sequential_layout_state |
| .as_mut() |
| .expect("Tried to lay out a float with no sequential placement state!"); |
| |
| let block_offset_from_containining_block_top = state |
| .current_block_position_including_margins() - |
| state.current_containing_block_offset(); |
| state.place_float_fragment( |
| fragment, |
| self.containing_block, |
| CollapsedMargin::zero(), |
| block_offset_from_containining_block_top, |
| ); |
| } |
| |
| /// Place a FloatLineItem. This is done when an unbreakable segment is committed to |
| /// the current line. Placement of FloatLineItems might need to be deferred until the |
| /// line is complete in the case that floats stop fitting on the current line. |
| /// |
| /// When placing floats we do not want to take into account any trailing whitespace on |
| /// the line, because that whitespace will be trimmed in the case that the line is |
| /// broken. Thus this function takes as an argument the new size (without whitespace) of |
| /// the line that these floats are joining. |
| fn place_float_line_item_for_commit_to_line( |
| &mut self, |
| float_item: &mut FloatLineItem, |
| line_inline_size_without_trailing_whitespace: Au, |
| ) { |
| let mut float_fragment = float_item.fragment.borrow_mut(); |
| let logical_margin_rect_size = float_fragment |
| .margin_rect() |
| .size |
| .to_logical(self.containing_block.style.writing_mode); |
| let inline_size = logical_margin_rect_size.inline.max(Au::zero()); |
| |
| let available_inline_size = match self.current_line.placement_among_floats.get() { |
| Some(placement_among_floats) => placement_among_floats.size.inline, |
| None => self.containing_block.size.inline, |
| } - line_inline_size_without_trailing_whitespace; |
| |
| // If this float doesn't fit on the current line or a previous float didn't fit on |
| // the current line, we need to place it starting at the next line BUT still as |
| // children of this line's hierarchy of inline boxes (for the purposes of properly |
| // parenting in their stacking contexts). Once all the line content is gathered we |
| // will place them later. |
| let has_content = self.current_line.has_content || self.current_line_segment.has_content; |
| let fits_on_line = !has_content || inline_size <= available_inline_size; |
| let needs_placement_later = |
| self.current_line.has_floats_waiting_to_be_placed || !fits_on_line; |
| |
| if needs_placement_later { |
| self.current_line.has_floats_waiting_to_be_placed = true; |
| } else { |
| self.place_float_fragment(&mut float_fragment); |
| float_item.needs_placement = false; |
| } |
| |
| // We've added a new float to the IFC, but this may have actually changed the |
| // position of the current line. In order to determine that we regenerate the |
| // placement among floats for the current line, which may adjust its inline |
| // start position. |
| let new_placement = self.place_line_among_floats(&LogicalVec2 { |
| inline: line_inline_size_without_trailing_whitespace, |
| block: self.current_line.max_block_size.resolve(), |
| }); |
| self.current_line |
| .replace_placement_among_floats(new_placement); |
| } |
| |
| /// Given a new potential line size for the current line, create a "placement" for that line. |
| /// This tells us whether or not the new potential line will fit in the current block position |
| /// or need to be moved. In addition, the placement rect determines the inline start and end |
| /// of the line if it's used as the final placement among floats. |
| fn place_line_among_floats(&self, potential_line_size: &LogicalVec2<Au>) -> LogicalRect<Au> { |
| let sequential_layout_state = self |
| .sequential_layout_state |
| .as_ref() |
| .expect("Should not have called this function without having floats."); |
| |
| let ifc_offset_in_float_container = LogicalVec2 { |
| inline: sequential_layout_state |
| .floats |
| .containing_block_info |
| .inline_start, |
| block: sequential_layout_state.current_containing_block_offset(), |
| }; |
| |
| let ceiling = self |
| .current_line |
| .line_block_start_considering_placement_among_floats(); |
| let mut placement = PlacementAmongFloats::new( |
| &sequential_layout_state.floats, |
| ceiling + ifc_offset_in_float_container.block, |
| LogicalVec2 { |
| inline: potential_line_size.inline, |
| block: potential_line_size.block, |
| }, |
| &PaddingBorderMargin::zero(), |
| ); |
| |
| let mut placement_rect = placement.place(); |
| placement_rect.start_corner -= ifc_offset_in_float_container; |
| placement_rect |
| } |
| |
| /// Returns true if a new potential line size for the current line would require a line |
| /// break. This takes into account floats and will also update the "placement among |
| /// floats" for this line if the potential line size would not cause a line break. |
| /// Thus, calling this method has side effects and should only be done while in the |
| /// process of laying out line content that is always going to be committed to this |
| /// line or the next. |
| fn new_potential_line_size_causes_line_break( |
| &mut self, |
| potential_line_size: &LogicalVec2<Au>, |
| ) -> bool { |
| let available_line_space = if self.sequential_layout_state.is_some() { |
| self.current_line |
| .placement_among_floats |
| .get_or_init(|| self.place_line_among_floats(potential_line_size)) |
| .size |
| } else { |
| LogicalVec2 { |
| inline: self.containing_block.size.inline, |
| block: MAX_AU, |
| } |
| }; |
| |
| let inline_would_overflow = potential_line_size.inline > available_line_space.inline; |
| let block_would_overflow = potential_line_size.block > available_line_space.block; |
| |
| // The first content that is added to a line cannot trigger a line break and |
| // the `white-space` propertly can also prevent all line breaking. |
| let can_break = self.current_line.has_content; |
| |
| // If this is the first content on the line and we already have a float placement, |
| // that means that the placement was initialized by a leading float in the IFC. |
| // This placement needs to be updated, because the first line content might push |
| // the block start of the line downward. If there is no float placement, we want |
| // to make one to properly set the block position of the line. |
| if !can_break { |
| // Even if we cannot break, adding content to this line might change its position. |
| // In that case we need to redo our placement among floats. |
| if self.sequential_layout_state.is_some() && |
| (inline_would_overflow || block_would_overflow) |
| { |
| let new_placement = self.place_line_among_floats(potential_line_size); |
| self.current_line |
| .replace_placement_among_floats(new_placement); |
| } |
| |
| return false; |
| } |
| |
| // If the potential line is larger than the containing block we do not even need to consider |
| // floats. We definitely have to do a linebreak. |
| if potential_line_size.inline > self.containing_block.size.inline { |
| return true; |
| } |
| |
| // Not fitting in the block space means that our block size has changed and we had a |
| // placement among floats that is no longer valid. This same placement might just |
| // need to be expanded or perhaps we need to line break. |
| if block_would_overflow { |
| // If we have a limited block size then we are wedging this line between floats. |
| assert!(self.sequential_layout_state.is_some()); |
| let new_placement = self.place_line_among_floats(potential_line_size); |
| if new_placement.start_corner.block != |
| self.current_line |
| .line_block_start_considering_placement_among_floats() |
| { |
| return true; |
| } else { |
| self.current_line |
| .replace_placement_among_floats(new_placement); |
| return false; |
| } |
| } |
| |
| // Otherwise the new potential line size will require a newline if it fits in the |
| // inline space available for this line. This space may be smaller than the |
| // containing block if floats shrink the available inline space. |
| inline_would_overflow |
| } |
| |
| pub(super) fn defer_forced_line_break(&mut self) { |
| // If the current portion of the unbreakable segment does not fit on the current line |
| // we need to put it on a new line *before* actually triggering the hard line break. |
| if !self.unbreakable_segment_fits_on_line() { |
| self.process_line_break(false /* forced_line_break */); |
| } |
| |
| // Defer the actual line break until we've cleared all ending inline boxes. |
| self.linebreak_before_new_content = true; |
| |
| // In quirks mode, the line-height isn't automatically added to the line. If we consider a |
| // forced line break a kind of preserved white space, quirks mode requires that we add the |
| // line-height of the current element to the line box height. |
| // |
| // The exception here is `<br>` elements. They are implemented with `pre-line` in Servo, but |
| // this is an implementation detail. The "magic" behavior of `<br>` elements is that they |
| // add line-height to the line conditionally: only when they are on an otherwise empty line. |
| let line_is_empty = |
| !self.current_line_segment.has_content && !self.current_line.has_content; |
| if !self.processing_br_element() || line_is_empty { |
| let strut_size = self |
| .current_inline_container_state() |
| .strut_block_sizes |
| .clone(); |
| self.update_unbreakable_segment_for_new_content( |
| &strut_size, |
| Au::zero(), |
| SegmentContentFlags::empty(), |
| ); |
| } |
| |
| self.had_inflow_content = true; |
| } |
| |
| pub(super) fn possibly_flush_deferred_forced_line_break(&mut self) { |
| if !self.linebreak_before_new_content { |
| return; |
| } |
| |
| self.commit_current_segment_to_line(); |
| self.process_line_break(true /* forced_line_break */); |
| self.linebreak_before_new_content = false; |
| } |
| |
| fn push_line_item_to_unbreakable_segment(&mut self, line_item: LineItem) { |
| self.current_line_segment |
| .push_line_item(line_item, self.inline_box_state_stack.len()); |
| } |
| |
| pub(super) fn push_glyph_store_to_unbreakable_segment( |
| &mut self, |
| glyph_store: std::sync::Arc<GlyphStore>, |
| text_run: &TextRun, |
| font_index: usize, |
| bidi_level: Level, |
| range: range::Range<ByteIndex>, |
| ) { |
| let inline_advance = glyph_store.total_advance(); |
| let flags = if glyph_store.is_whitespace() { |
| SegmentContentFlags::from(text_run.inline_styles.style.borrow().get_inherited_text()) |
| } else { |
| SegmentContentFlags::empty() |
| }; |
| |
| // If the metrics of this font don't match the default font, we are likely using a fallback |
| // font and need to adjust the line size to account for a potentially different font. |
| // If somehow the metrics match, the line size won't change. |
| let ifc_font_info = &self.ifc.font_metrics[font_index]; |
| let font_metrics = ifc_font_info.metrics.clone(); |
| let using_fallback_font = |
| self.current_inline_container_state().font_metrics != font_metrics; |
| |
| let quirks_mode = self.layout_context.style_context.quirks_mode() != QuirksMode::NoQuirks; |
| let strut_size = if using_fallback_font { |
| // TODO(mrobinson): This value should probably be cached somewhere. |
| let container_state = self.current_inline_container_state(); |
| let vertical_align = effective_vertical_align( |
| &container_state.style, |
| self.inline_box_state_stack.last().map(|c| &c.base), |
| ); |
| let mut block_size = container_state.get_block_size_contribution( |
| vertical_align, |
| &font_metrics, |
| &container_state.font_metrics, |
| ); |
| block_size.adjust_for_baseline_offset(container_state.baseline_offset); |
| block_size |
| } else if quirks_mode && !flags.is_collapsible_whitespace() { |
| // Normally, the strut is incorporated into the nested block size. In quirks mode though |
| // if we find any text that isn't collapsed whitespace, we need to incorporate the strut. |
| // TODO(mrobinson): This isn't quite right for situations where collapsible white space |
| // ultimately does not collapse because it is between two other pieces of content. |
| self.current_inline_container_state() |
| .strut_block_sizes |
| .clone() |
| } else { |
| LineBlockSizes::zero() |
| }; |
| self.update_unbreakable_segment_for_new_content(&strut_size, inline_advance, flags); |
| |
| let current_inline_box_identifier = self.current_inline_box_identifier(); |
| match self.current_line_segment.line_items.last_mut() { |
| Some(LineItem::TextRun(inline_box_identifier, line_item)) |
| if *inline_box_identifier == current_inline_box_identifier && |
| line_item.can_merge(ifc_font_info.key, bidi_level) => |
| { |
| line_item.text.push(glyph_store); |
| return; |
| }, |
| _ => {}, |
| } |
| |
| let selection_range = if let Some(selection) = &text_run.selection_range { |
| let intersection = selection.intersect(&range); |
| if intersection.is_empty() { |
| let insertion_point_index = selection.begin(); |
| // We only allow the caret to be shown in the start of the fragment if it is the first fragment. |
| // Otherwise this will cause duplicate caret, especially apparent when encountered line break. |
| if insertion_point_index >= range.begin() && |
| insertion_point_index <= range.end() && |
| (range.begin() != insertion_point_index || range.begin().0 == 0) |
| { |
| Some(Range::new( |
| insertion_point_index - range.begin(), |
| ByteIndex(0), |
| )) |
| } else { |
| None |
| } |
| } else { |
| Some(Range::new( |
| intersection.begin() - range.begin(), |
| intersection.length(), |
| )) |
| } |
| } else { |
| None |
| }; |
| |
| self.push_line_item_to_unbreakable_segment(LineItem::TextRun( |
| current_inline_box_identifier, |
| TextRunLineItem { |
| text: vec![glyph_store], |
| base_fragment_info: text_run.base_fragment_info, |
| inline_styles: text_run.inline_styles.clone(), |
| font_metrics, |
| font_key: ifc_font_info.key, |
| bidi_level, |
| selection_range, |
| }, |
| )); |
| } |
| |
| fn update_unbreakable_segment_for_new_content( |
| &mut self, |
| block_sizes_of_content: &LineBlockSizes, |
| inline_size: Au, |
| flags: SegmentContentFlags, |
| ) { |
| if flags.is_collapsible_whitespace() || flags.is_wrappable_and_hangable() { |
| self.current_line_segment.trailing_whitespace_size = inline_size; |
| } else { |
| self.current_line_segment.trailing_whitespace_size = Au::zero(); |
| } |
| if !flags.is_collapsible_whitespace() { |
| self.current_line_segment.has_content = true; |
| self.had_inflow_content = true; |
| } |
| |
| // This may or may not include the size of the strut depending on the quirks mode setting. |
| let container_max_block_size = &self |
| .current_inline_container_state() |
| .nested_strut_block_sizes |
| .clone(); |
| self.current_line_segment |
| .max_block_size |
| .max_assign(container_max_block_size); |
| self.current_line_segment |
| .max_block_size |
| .max_assign(block_sizes_of_content); |
| |
| self.current_line_segment.inline_size += inline_size; |
| |
| // Propagate the whitespace setting to the current nesting level. |
| *self |
| .current_inline_container_state() |
| .has_content |
| .borrow_mut() = true; |
| self.propagate_current_nesting_level_white_space_style(); |
| } |
| |
| fn process_line_break(&mut self, forced_line_break: bool) { |
| self.current_line_segment.trim_leading_whitespace(); |
| self.finish_current_line_and_reset(forced_line_break); |
| } |
| |
| pub(super) fn unbreakable_segment_fits_on_line(&mut self) -> bool { |
| let potential_line_size = LogicalVec2 { |
| inline: self.current_line.inline_position + self.current_line_segment.inline_size - |
| self.current_line_segment.trailing_whitespace_size, |
| block: self |
| .current_line_max_block_size_including_nested_containers() |
| .max(&self.current_line_segment.max_block_size) |
| .resolve(), |
| }; |
| |
| !self.new_potential_line_size_causes_line_break(&potential_line_size) |
| } |
| |
| /// Process a soft wrap opportunity. This will either commit the current unbreakble |
| /// segment to the current line, if it fits within the containing block and float |
| /// placement boundaries, or do a line break and then commit the segment. |
| pub(super) fn process_soft_wrap_opportunity(&mut self) { |
| if self.current_line_segment.line_items.is_empty() { |
| return; |
| } |
| if self.text_wrap_mode == TextWrapMode::Nowrap { |
| return; |
| } |
| |
| let potential_line_size = LogicalVec2 { |
| inline: self.current_line.inline_position + self.current_line_segment.inline_size - |
| self.current_line_segment.trailing_whitespace_size, |
| block: self |
| .current_line_max_block_size_including_nested_containers() |
| .max(&self.current_line_segment.max_block_size) |
| .resolve(), |
| }; |
| |
| if self.new_potential_line_size_causes_line_break(&potential_line_size) { |
| self.process_line_break(false /* forced_line_break */); |
| } |
| self.commit_current_segment_to_line(); |
| } |
| |
| /// Commit the current unbrekable segment to the current line. In addition, this will |
| /// place all floats in the unbreakable segment and expand the line dimensions. |
| fn commit_current_segment_to_line(&mut self) { |
| // The line segments might have no items and have content after processing a forced |
| // linebreak on an empty line. |
| if self.current_line_segment.line_items.is_empty() && !self.current_line_segment.has_content |
| { |
| return; |
| } |
| |
| if !self.current_line.has_content { |
| self.current_line_segment.trim_leading_whitespace(); |
| } |
| |
| self.current_line.inline_position += self.current_line_segment.inline_size; |
| self.current_line.max_block_size = self |
| .current_line_max_block_size_including_nested_containers() |
| .max(&self.current_line_segment.max_block_size); |
| let line_inline_size_without_trailing_whitespace = |
| self.current_line.inline_position - self.current_line_segment.trailing_whitespace_size; |
| |
| // Place all floats in this unbreakable segment. |
| let mut segment_items = mem::take(&mut self.current_line_segment.line_items); |
| for item in segment_items.iter_mut() { |
| if let LineItem::Float(_, float_item) = item { |
| self.place_float_line_item_for_commit_to_line( |
| float_item, |
| line_inline_size_without_trailing_whitespace, |
| ); |
| } |
| } |
| |
| // If the current line was never placed among floats, we need to do that now based on the |
| // new size. Calling `new_potential_line_size_causes_line_break()` here triggers the |
| // new line to be positioned among floats. This should never ask for a line |
| // break because it is the first content on the line. |
| if self.current_line.line_items.is_empty() { |
| let will_break = self.new_potential_line_size_causes_line_break(&LogicalVec2 { |
| inline: line_inline_size_without_trailing_whitespace, |
| block: self.current_line_segment.max_block_size.resolve(), |
| }); |
| assert!(!will_break); |
| } |
| |
| self.current_line.line_items.extend(segment_items); |
| self.current_line.has_content |= self.current_line_segment.has_content; |
| |
| self.current_line_segment.reset(); |
| } |
| } |
| |
| bitflags! { |
| pub struct SegmentContentFlags: u8 { |
| const COLLAPSIBLE_WHITESPACE = 0b00000001; |
| const WRAPPABLE_AND_HANGABLE_WHITESPACE = 0b00000010; |
| } |
| } |
| |
| impl SegmentContentFlags { |
| fn is_collapsible_whitespace(&self) -> bool { |
| self.contains(Self::COLLAPSIBLE_WHITESPACE) |
| } |
| |
| fn is_wrappable_and_hangable(&self) -> bool { |
| self.contains(Self::WRAPPABLE_AND_HANGABLE_WHITESPACE) |
| } |
| } |
| |
| impl From<&InheritedText> for SegmentContentFlags { |
| fn from(style_text: &InheritedText) -> Self { |
| let mut flags = Self::empty(); |
| |
| // White-space with `white-space-collapse: break-spaces` or `white-space-collapse: preserve` |
| // never collapses. |
| if !matches!( |
| style_text.white_space_collapse, |
| WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces |
| ) { |
| flags.insert(Self::COLLAPSIBLE_WHITESPACE); |
| } |
| |
| // White-space with `white-space-collapse: break-spaces` never hangs and always takes up |
| // space. |
| if style_text.text_wrap_mode == TextWrapMode::Wrap && |
| style_text.white_space_collapse != WhiteSpaceCollapse::BreakSpaces |
| { |
| flags.insert(Self::WRAPPABLE_AND_HANGABLE_WHITESPACE); |
| } |
| flags |
| } |
| } |
| |
| impl InlineFormattingContext { |
| #[servo_tracing::instrument(name = "InlineFormattingContext::new_with_builder", skip_all)] |
| pub(super) fn new_with_builder( |
| 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 mut font_metrics = Vec::new(); |
| |
| let bidi_info = BidiInfo::new(&text_content, Some(starting_bidi_level)); |
| let has_right_to_left_content = bidi_info.has_rtl(); |
| |
| let mut new_linebreaker = LineBreaker::new(text_content.as_str()); |
| for item in builder.inline_items.iter() { |
| match &mut *item.borrow_mut() { |
| InlineItem::TextRun(text_run) => { |
| text_run.borrow_mut().segment_and_shape( |
| &text_content, |
| &layout_context.font_context, |
| &mut new_linebreaker, |
| &mut font_metrics, |
| &bidi_info, |
| ); |
| }, |
| InlineItem::StartInlineBox(inline_box) => { |
| let inline_box = &mut *inline_box.borrow_mut(); |
| if let Some(font) = get_font_for_first_font_for_style( |
| &inline_box.base.style, |
| &layout_context.font_context, |
| ) { |
| inline_box.default_font_index = Some(add_or_get_font( |
| &font, |
| &mut font_metrics, |
| &layout_context.font_context, |
| )); |
| } |
| }, |
| InlineItem::Atomic(_, index_in_text, bidi_level) => { |
| *bidi_level = bidi_info.levels[*index_in_text]; |
| }, |
| InlineItem::OutOfFlowAbsolutelyPositionedBox(..) | |
| InlineItem::OutOfFlowFloatBox(_) | |
| InlineItem::EndInlineBox => {}, |
| } |
| } |
| |
| InlineFormattingContext { |
| text_content, |
| inline_items: builder.inline_items, |
| inline_boxes: builder.inline_boxes, |
| font_metrics, |
| shared_inline_styles: builder |
| .shared_inline_styles_stack |
| .last() |
| .expect("Should have at least one SharedInlineStyle for the root of an IFC") |
| .clone(), |
| has_first_formatted_line, |
| contains_floats: builder.contains_floats, |
| is_single_line_text_input, |
| has_right_to_left_content, |
| } |
| } |
| |
| pub(crate) fn repair_style( |
| &self, |
| node: &ServoThreadSafeLayoutNode, |
| new_style: &Arc<ComputedValues>, |
| ) { |
| *self.shared_inline_styles.style.borrow_mut() = new_style.clone(); |
| *self.shared_inline_styles.selected.borrow_mut() = node.selected_style(); |
| } |
| |
| pub(super) fn layout( |
| &self, |
| layout_context: &LayoutContext, |
| positioning_context: &mut PositioningContext, |
| containing_block: &ContainingBlock, |
| sequential_layout_state: Option<&mut SequentialLayoutState>, |
| collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin, |
| ) -> CacheableLayoutResult { |
| let first_line_inline_start = if self.has_first_formatted_line { |
| containing_block |
| .style |
| .get_inherited_text() |
| .text_indent |
| .length |
| .to_used_value(containing_block.size.inline) |
| } else { |
| Au::zero() |
| }; |
| |
| // Clear any cached inline fragments from previous layouts. |
| for inline_box in self.inline_boxes.iter() { |
| inline_box.borrow().base.clear_fragments(); |
| } |
| |
| let style = containing_block.style; |
| |
| // It's unfortunate that it isn't possible to get this during IFC text processing, but in |
| // that situation the style of the containing block is unknown. |
| let default_font_metrics = |
| get_font_for_first_font_for_style(style, &layout_context.font_context) |
| .map(|font| font.metrics.clone()); |
| |
| let style_text = containing_block.style.get_inherited_text(); |
| let mut inline_container_state_flags = InlineContainerStateFlags::empty(); |
| if inline_container_needs_strut(style, layout_context, None) { |
| inline_container_state_flags.insert(InlineContainerStateFlags::CREATE_STRUT); |
| } |
| if self.is_single_line_text_input { |
| inline_container_state_flags |
| .insert(InlineContainerStateFlags::IS_SINGLE_LINE_TEXT_INPUT); |
| } |
| |
| let mut layout = InlineFormattingContextLayout { |
| positioning_context, |
| containing_block, |
| sequential_layout_state, |
| layout_context, |
| ifc: self, |
| fragments: Vec::new(), |
| current_line: LineUnderConstruction::new(LogicalVec2 { |
| inline: first_line_inline_start, |
| block: Au::zero(), |
| }), |
| root_nesting_level: InlineContainerState::new( |
| style.to_arc(), |
| inline_container_state_flags, |
| None, /* parent_container */ |
| default_font_metrics.as_ref(), |
| ), |
| inline_box_state_stack: Vec::new(), |
| inline_box_states: Vec::with_capacity(self.inline_boxes.len()), |
| current_line_segment: UnbreakableSegmentUnderConstruction::new(), |
| linebreak_before_new_content: false, |
| deferred_br_clear: Clear::None, |
| have_deferred_soft_wrap_opportunity: false, |
| had_inflow_content: false, |
| depends_on_block_constraints: false, |
| white_space_collapse: style_text.white_space_collapse, |
| text_wrap_mode: style_text.text_wrap_mode, |
| baselines: Baselines::default(), |
| }; |
| |
| // FIXME(pcwalton): This assumes that margins never collapse through inline formatting |
| // contexts (i.e. that inline formatting contexts are never empty). Is that right? |
| // FIXME(mrobinson): This should not happen if the IFC collapses through. |
| if let Some(ref mut sequential_layout_state) = layout.sequential_layout_state { |
| sequential_layout_state.collapse_margins(); |
| // FIXME(mrobinson): Collapse margins in the containing block offsets as well?? |
| } |
| |
| for item in self.inline_items.iter() { |
| let item = &*item.borrow(); |
| |
| // Any new box should flush a pending hard line break. |
| if !matches!(item, InlineItem::EndInlineBox) { |
| layout.possibly_flush_deferred_forced_line_break(); |
| } |
| |
| match item { |
| InlineItem::StartInlineBox(inline_box) => { |
| layout.start_inline_box(&inline_box.borrow()); |
| }, |
| InlineItem::EndInlineBox => layout.finish_inline_box(), |
| InlineItem::TextRun(run) => run.borrow().layout_into_line_items(&mut layout), |
| InlineItem::Atomic(atomic_formatting_context, offset_in_text, bidi_level) => { |
| atomic_formatting_context.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(), |
| }, |
| )); |
| }, |
| InlineItem::OutOfFlowFloatBox(float_box) => { |
| float_box.borrow().layout_into_line_items(&mut layout); |
| }, |
| } |
| } |
| |
| layout.finish_last_line(); |
| |
| let mut collapsible_margins_in_children = CollapsedBlockMargins::zero(); |
| let content_block_size = layout.current_line.start_position.block; |
| collapsible_margins_in_children.collapsed_through = !layout.had_inflow_content && |
| content_block_size.is_zero() && |
| collapsible_with_parent_start_margin.0; |
| |
| CacheableLayoutResult { |
| fragments: layout.fragments, |
| content_block_size, |
| collapsible_margins_in_children, |
| baselines: layout.baselines, |
| depends_on_block_constraints: layout.depends_on_block_constraints, |
| content_inline_size_for_table: None, |
| specific_layout_info: None, |
| } |
| } |
| |
| fn next_character_prevents_soft_wrap_opportunity(&self, index: usize) -> bool { |
| let Some(character) = self.text_content[index..].chars().nth(1) else { |
| return false; |
| }; |
| char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character) |
| } |
| |
| fn previous_character_prevents_soft_wrap_opportunity(&self, index: usize) -> bool { |
| let Some(character) = self.text_content[0..index].chars().next_back() else { |
| return false; |
| }; |
| char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character) |
| } |
| } |
| |
| impl InlineContainerState { |
| fn new( |
| style: Arc<ComputedValues>, |
| flags: InlineContainerStateFlags, |
| parent_container: Option<&InlineContainerState>, |
| font_metrics: Option<&FontMetrics>, |
| ) -> Self { |
| let font_metrics = font_metrics.cloned().unwrap_or_else(FontMetrics::empty); |
| let line_height = line_height( |
| &style, |
| &font_metrics, |
| flags.contains(InlineContainerStateFlags::IS_SINGLE_LINE_TEXT_INPUT), |
| ); |
| |
| let mut baseline_offset = Au::zero(); |
| let mut strut_block_sizes = Self::get_block_sizes_with_style( |
| effective_vertical_align(&style, parent_container), |
| &style, |
| &font_metrics, |
| &font_metrics, |
| line_height, |
| ); |
| if let Some(parent_container) = parent_container { |
| // The baseline offset from `vertical-align` might adjust where our block size contribution is |
| // within the line. |
| baseline_offset = parent_container.get_cumulative_baseline_offset_for_child( |
| style.clone_vertical_align(), |
| &strut_block_sizes, |
| ); |
| strut_block_sizes.adjust_for_baseline_offset(baseline_offset); |
| } |
| |
| let mut nested_block_sizes = parent_container |
| .map(|container| container.nested_strut_block_sizes.clone()) |
| .unwrap_or_else(LineBlockSizes::zero); |
| if flags.contains(InlineContainerStateFlags::CREATE_STRUT) { |
| nested_block_sizes.max_assign(&strut_block_sizes); |
| } |
| |
| Self { |
| style, |
| flags, |
| has_content: RefCell::new(false), |
| nested_strut_block_sizes: nested_block_sizes, |
| strut_block_sizes, |
| baseline_offset, |
| font_metrics, |
| } |
| } |
| |
| fn get_block_sizes_with_style( |
| vertical_align: VerticalAlign, |
| style: &ComputedValues, |
| font_metrics: &FontMetrics, |
| font_metrics_of_first_font: &FontMetrics, |
| line_height: Au, |
| ) -> LineBlockSizes { |
| if !is_baseline_relative(vertical_align) { |
| return LineBlockSizes { |
| line_height, |
| baseline_relative_size_for_line_height: None, |
| size_for_baseline_positioning: BaselineRelativeSize::zero(), |
| }; |
| } |
| |
| // From https://drafts.csswg.org/css-inline/#inline-height |
| // > If line-height computes to `normal` and either `text-box-edge` is `leading` or this |
| // > is the root inline box, the font’s line gap metric may also be incorporated |
| // > into A and D by adding half to each side as half-leading. |
| // |
| // `text-box-edge` isn't implemented (and this is a draft specification), so it's |
| // always effectively `leading`, which means we always take into account the line gap |
| // when `line-height` is normal. |
| let mut ascent = font_metrics.ascent; |
| let mut descent = font_metrics.descent; |
| if style.get_font().line_height == LineHeight::Normal { |
| let half_leading_from_line_gap = |
| (font_metrics.line_gap - descent - ascent).scale_by(0.5); |
| ascent += half_leading_from_line_gap; |
| descent += half_leading_from_line_gap; |
| } |
| |
| // The ascent and descent we use for computing the line's final line height isn't |
| // the same the ascent and descent we use for finding the baseline. For finding |
| // the baseline we want the content rect. |
| let size_for_baseline_positioning = BaselineRelativeSize { ascent, descent }; |
| |
| // From https://drafts.csswg.org/css-inline/#inline-height |
| // > When its computed line-height is not normal, its layout bounds are derived solely |
| // > from metrics of its first available font (ignoring glyphs from other fonts), and |
| // > leading is used to adjust the effective A and D to add up to the used line-height. |
| // > Calculate the leading L as L = line-height - (A + D). Half the leading (its |
| // > half-leading) is added above A of the first available font, and the other half |
| // > below D of the first available font, giving an effective ascent above the baseline |
| // > of A′ = A + L/2, and an effective descent of D′ = D + L/2. |
| // |
| // Note that leading might be negative here and the line-height might be zero. In |
| // the case where the height is zero, ascent and descent will move to the same |
| // point in the block axis. Even though the contribution to the line height is |
| // zero in this case, the line may get some height when taking them into |
| // considering with other zero line height boxes that converge on other block axis |
| // locations when using the above formula. |
| if style.get_font().line_height != LineHeight::Normal { |
| ascent = font_metrics_of_first_font.ascent; |
| descent = font_metrics_of_first_font.descent; |
| let half_leading = (line_height - (ascent + descent)).scale_by(0.5); |
| // We want the sum of `ascent` and `descent` to equal `line_height`. |
| // If we just add `half_leading` to both, then we may not get `line_height` |
| // due to precision limitations of `Au`. Instead, we set `descent` to |
| // the value that will guarantee the correct sum. |
| ascent += half_leading; |
| descent = line_height - ascent; |
| } |
| |
| LineBlockSizes { |
| line_height, |
| baseline_relative_size_for_line_height: Some(BaselineRelativeSize { ascent, descent }), |
| size_for_baseline_positioning, |
| } |
| } |
| |
| fn get_block_size_contribution( |
| &self, |
| vertical_align: VerticalAlign, |
| font_metrics: &FontMetrics, |
| font_metrics_of_first_font: &FontMetrics, |
| ) -> LineBlockSizes { |
| Self::get_block_sizes_with_style( |
| vertical_align, |
| &self.style, |
| font_metrics, |
| font_metrics_of_first_font, |
| line_height( |
| &self.style, |
| font_metrics, |
| self.flags |
| .contains(InlineContainerStateFlags::IS_SINGLE_LINE_TEXT_INPUT), |
| ), |
| ) |
| } |
| |
| fn get_cumulative_baseline_offset_for_child( |
| &self, |
| child_vertical_align: VerticalAlign, |
| child_block_size: &LineBlockSizes, |
| ) -> Au { |
| let block_size = self.get_block_size_contribution( |
| child_vertical_align.clone(), |
| &self.font_metrics, |
| &self.font_metrics, |
| ); |
| self.baseline_offset + |
| match child_vertical_align { |
| // `top` and `bottom are not actually relative to the baseline, but this value is unused |
| // in those cases. |
| // TODO: We should distinguish these from `baseline` in order to implement "aligned subtrees" properly. |
| // See https://drafts.csswg.org/css2/#aligned-subtree. |
| VerticalAlign::Keyword(VerticalAlignKeyword::Baseline) | |
| VerticalAlign::Keyword(VerticalAlignKeyword::Top) | |
| VerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => Au::zero(), |
| VerticalAlign::Keyword(VerticalAlignKeyword::Sub) => { |
| block_size.resolve().scale_by(FONT_SUBSCRIPT_OFFSET_RATIO) |
| }, |
| VerticalAlign::Keyword(VerticalAlignKeyword::Super) => { |
| -block_size.resolve().scale_by(FONT_SUPERSCRIPT_OFFSET_RATIO) |
| }, |
| VerticalAlign::Keyword(VerticalAlignKeyword::TextTop) => { |
| child_block_size.size_for_baseline_positioning.ascent - self.font_metrics.ascent |
| }, |
| VerticalAlign::Keyword(VerticalAlignKeyword::Middle) => { |
| // "Align the vertical midpoint of the box with the baseline of the parent |
| // box plus half the x-height of the parent." |
| (child_block_size.size_for_baseline_positioning.ascent - |
| child_block_size.size_for_baseline_positioning.descent - |
| self.font_metrics.x_height) |
| .scale_by(0.5) |
| }, |
| VerticalAlign::Keyword(VerticalAlignKeyword::TextBottom) => { |
| self.font_metrics.descent - |
| child_block_size.size_for_baseline_positioning.descent |
| }, |
| VerticalAlign::Length(length_percentage) => { |
| -length_percentage.to_used_value(child_block_size.line_height) |
| }, |
| } |
| } |
| } |
| |
| impl IndependentFormattingContext { |
| fn layout_into_line_items( |
| &self, |
| layout: &mut InlineFormattingContextLayout, |
| offset_in_text: usize, |
| bidi_level: Level, |
| ) { |
| // We need to know the inline size of the atomic before deciding whether to do the line break. |
| let mut child_positioning_context = PositioningContext::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.content_rect = fragment |
| .content_rect |
| .translate(pbm_physical_offset.to_vector()); |
| |
| // 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 |
| .content_rect |
| .size |
| .to_logical(container_writing_mode); |
| let baseline_offset = self |
| .pick_baseline(&fragment.baselines(container_writing_mode)) |
| .map(|baseline| pbm_sums.block_start + baseline) |
| .unwrap_or(size.block); |
| |
| let (block_sizes, baseline_offset_in_parent) = |
| self.get_block_sizes_and_baseline_offset(layout, size.block, baseline_offset); |
| layout.update_unbreakable_segment_for_new_content( |
| &block_sizes, |
| size.inline, |
| SegmentContentFlags::empty(), |
| ); |
| |
| let fragment = ArcRefCell::new(fragment); |
| self.base.set_fragment(Fragment::Box(fragment.clone())); |
| |
| layout.push_line_item_to_unbreakable_segment(LineItem::Atomic( |
| layout.current_inline_box_identifier(), |
| AtomicLineItem { |
| fragment, |
| size, |
| positioning_context, |
| baseline_offset_in_parent, |
| baseline_offset_in_item: baseline_offset, |
| bidi_level, |
| }, |
| )); |
| |
| // If there's a soft wrap opportunity following this atomic, defer a soft wrap opportunity |
| // for when we next process text content. |
| if !layout |
| .ifc |
| .next_character_prevents_soft_wrap_opportunity(offset_in_text) |
| { |
| layout.have_deferred_soft_wrap_opportunity = true; |
| } |
| } |
| |
| /// Picks either the first or the last baseline, depending on `baseline-source`. |
| /// TODO: clarify that this is not to be used for box alignment in flex/grid |
| /// <https://drafts.csswg.org/css-inline/#baseline-source> |
| fn pick_baseline(&self, baselines: &Baselines) -> Option<Au> { |
| match self.style().clone_baseline_source() { |
| BaselineSource::First => baselines.first, |
| BaselineSource::Last => baselines.last, |
| BaselineSource::Auto 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_vertical_align()) { |
| LineBlockSizes { |
| line_height: block_size, |
| baseline_relative_size_for_line_height: None, |
| size_for_baseline_positioning: BaselineRelativeSize::zero(), |
| } |
| } else { |
| let baseline_relative_size = BaselineRelativeSize { |
| ascent: baseline_offset_in_content_area, |
| descent: block_size - baseline_offset_in_content_area, |
| }; |
| LineBlockSizes { |
| line_height: block_size, |
| baseline_relative_size_for_line_height: Some(baseline_relative_size.clone()), |
| size_for_baseline_positioning: baseline_relative_size, |
| } |
| }; |
| |
| let baseline_offset = ifc |
| .current_inline_container_state() |
| .get_cumulative_baseline_offset_for_child( |
| self.style().clone_vertical_align(), |
| &contribution, |
| ); |
| contribution.adjust_for_baseline_offset(baseline_offset); |
| |
| (contribution, baseline_offset) |
| } |
| } |
| |
| impl FloatBox { |
| fn layout_into_line_items(&self, layout: &mut InlineFormattingContextLayout) { |
| let fragment = ArcRefCell::new(self.layout( |
| layout.layout_context, |
| layout.positioning_context, |
| layout.containing_block, |
| )); |
| |
| self.contents |
| .base |
| .set_fragment(Fragment::Box(fragment.clone())); |
| layout.push_line_item_to_unbreakable_segment(LineItem::Float( |
| layout.current_inline_box_identifier(), |
| FloatLineItem { |
| fragment, |
| needs_placement: true, |
| }, |
| )); |
| } |
| } |
| |
| fn place_pending_floats(ifc: &mut InlineFormattingContextLayout, line_items: &mut [LineItem]) { |
| for item in line_items.iter_mut() { |
| if let LineItem::Float(_, float_line_item) = item { |
| if float_line_item.needs_placement { |
| ifc.place_float_fragment(&mut float_line_item.fragment.borrow_mut()); |
| } |
| } |
| } |
| } |
| |
| fn line_height( |
| parent_style: &ComputedValues, |
| font_metrics: &FontMetrics, |
| is_single_line_text_input: bool, |
| ) -> Au { |
| let font = parent_style.get_font(); |
| let font_size = font.font_size.computed_size(); |
| let mut line_height = match font.line_height { |
| LineHeight::Normal => font_metrics.line_gap, |
| LineHeight::Number(number) => (font_size * number.0).into(), |
| LineHeight::Length(length) => length.0.into(), |
| }; |
| |
| // 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 is_single_line_text_input { |
| line_height.max_assign(font_metrics.line_gap); |
| } |
| |
| line_height |
| } |
| |
| fn effective_vertical_align( |
| style: &ComputedValues, |
| container: Option<&InlineContainerState>, |
| ) -> VerticalAlign { |
| if container.is_none() { |
| // If we are at the root of the inline formatting context, we shouldn't use the |
| // computed `vertical-align`, since it has no effect on the contents of this IFC |
| // (it can just affect how the block container is aligned within the parent IFC). |
| VerticalAlign::Keyword(VerticalAlignKeyword::Baseline) |
| } else { |
| style.clone_vertical_align() |
| } |
| } |
| |
| fn is_baseline_relative(vertical_align: VerticalAlign) -> bool { |
| !matches!( |
| vertical_align, |
| VerticalAlign::Keyword(VerticalAlignKeyword::Top) | |
| VerticalAlign::Keyword(VerticalAlignKeyword::Bottom) |
| ) |
| } |
| |
| /// Whether or not a strut should be created for an inline container. Normally |
| /// all inline containers get struts. In quirks mode this isn't always the case |
| /// though. |
| /// |
| /// From <https://quirks.spec.whatwg.org/#the-line-height-calculation-quirk> |
| /// |
| /// > ### § 3.3. The line height calculation quirk |
| /// > In quirks mode and limited-quirks mode, an inline box that matches the following |
| /// > conditions, must, for the purpose of line height calculation, act as if the box had a |
| /// > line-height of zero. |
| /// > |
| /// > - The border-top-width, border-bottom-width, padding-top and padding-bottom |
| /// > properties have a used value of zero and the box has a vertical writing mode, or the |
| /// > border-right-width, border-left-width, padding-right and padding-left properties have |
| /// > a used value of zero and the box has a horizontal writing mode. |
| /// > - It either contains no text or it contains only collapsed whitespace. |
| /// > |
| /// > ### § 3.4. The blocks ignore line-height quirk |
| /// > In quirks mode and limited-quirks mode, for a block container element whose content is |
| /// > composed of inline-level elements, the element’s line-height must be ignored for the |
| /// > purpose of calculating the minimal height of line boxes within the element. |
| /// |
| /// Since we incorporate the size of the strut into the line-height calculation when |
| /// adding text, we can simply not incorporate the strut at the start of inline box |
| /// processing. This also works the same for the root of the IFC. |
| fn inline_container_needs_strut( |
| style: &ComputedValues, |
| layout_context: &LayoutContext, |
| pbm: Option<&PaddingBorderMargin>, |
| ) -> bool { |
| if layout_context.style_context.quirks_mode() == QuirksMode::NoQuirks { |
| return true; |
| } |
| |
| // This is not in a standard yet, but all browsers disable this quirk for list items. |
| // See https://github.com/whatwg/quirks/issues/38. |
| if style.get_box().display.is_list_item() { |
| return true; |
| } |
| |
| pbm.map(|pbm| !pbm.padding_border_sums.inline.is_zero()) |
| .unwrap_or(false) |
| } |
| |
| impl ComputeInlineContentSizes for InlineFormattingContext { |
| // This works on an already-constructed `InlineFormattingContext`, |
| // Which would have to change if/when |
| // `BlockContainer::construct` parallelize their construction. |
| fn compute_inline_content_sizes( |
| &self, |
| layout_context: &LayoutContext, |
| constraint_space: &ConstraintSpace, |
| ) -> InlineContentSizesResult { |
| ContentSizesComputation::compute(self, layout_context, constraint_space) |
| } |
| } |
| |
| /// A struct which takes care of computing [`ContentSizes`] for an [`InlineFormattingContext`]. |
| struct ContentSizesComputation<'layout_data> { |
| layout_context: &'layout_data LayoutContext<'layout_data>, |
| constraint_space: &'layout_data ConstraintSpace, |
| paragraph: ContentSizes, |
| current_line: ContentSizes, |
| /// Size for whitespace pending to be added to this line. |
| pending_whitespace: ContentSizes, |
| /// Whether or not the current line has seen any content (excluding collapsed whitespace), |
| /// when sizing under a min-content constraint. |
| had_content_yet_for_min_content: bool, |
| /// Whether or not the current line has seen any content (excluding collapsed whitespace), |
| /// when sizing under a max-content constraint. |
| had_content_yet_for_max_content: bool, |
| /// Stack of ending padding, margin, and border to add to the length |
| /// when an inline box finishes. |
| ending_inline_pbm_stack: Vec<Au>, |
| depends_on_block_constraints: bool, |
| } |
| |
| impl<'layout_data> ContentSizesComputation<'layout_data> { |
| fn traverse( |
| mut self, |
| inline_formatting_context: &InlineFormattingContext, |
| ) -> InlineContentSizesResult { |
| for inline_item in inline_formatting_context.inline_items.iter() { |
| self.process_item(&inline_item.borrow(), inline_formatting_context); |
| } |
| self.forced_line_break(); |
| |
| InlineContentSizesResult { |
| sizes: self.paragraph, |
| depends_on_block_constraints: self.depends_on_block_constraints, |
| } |
| } |
| |
| fn process_item( |
| &mut self, |
| inline_item: &InlineItem, |
| inline_formatting_context: &InlineFormattingContext, |
| ) { |
| match inline_item { |
| InlineItem::StartInlineBox(inline_box) => { |
| // For margins and paddings, a cyclic percentage is resolved against zero |
| // for determining intrinsic size contributions. |
| // https://drafts.csswg.org/css-sizing-3/#min-percentage-contribution |
| let inline_box = inline_box.borrow(); |
| let zero = Au::zero(); |
| let writing_mode = self.constraint_space.writing_mode; |
| let layout_style = inline_box.layout_style(); |
| let padding = layout_style |
| .padding(writing_mode) |
| .percentages_relative_to(zero); |
| let border = layout_style.border_width(writing_mode); |
| let margin = inline_box |
| .base |
| .style |
| .margin(writing_mode) |
| .percentages_relative_to(zero) |
| .auto_is(Au::zero); |
| |
| let pbm = margin + padding + border; |
| if inline_box.is_first_split { |
| self.add_inline_size(pbm.inline_start); |
| } |
| if inline_box.is_last_split { |
| self.ending_inline_pbm_stack.push(pbm.inline_end); |
| } else { |
| self.ending_inline_pbm_stack.push(Au::zero()); |
| } |
| }, |
| InlineItem::EndInlineBox => { |
| let length = self.ending_inline_pbm_stack.pop().unwrap_or_else(Au::zero); |
| self.add_inline_size(length); |
| }, |
| InlineItem::TextRun(text_run) => { |
| let text_run = &*text_run.borrow(); |
| let parent_style = text_run.inline_styles.style.borrow(); |
| for segment in text_run.shaped_text.iter() { |
| 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. |
| if can_wrap && segment.break_at_start { |
| self.line_break_opportunity() |
| } |
| |
| for run in segment.runs.iter() { |
| let advance = run.glyph_store.total_advance(); |
| if run.glyph_store.is_whitespace() { |
| // If this run is a forced line break, we *must* break the line |
| // and start measuring from the inline origin once more. |
| if run.is_single_preserved_newline() { |
| self.forced_line_break(); |
| continue; |
| } |
| if !matches!( |
| style_text.white_space_collapse, |
| WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces |
| ) { |
| if can_wrap { |
| self.line_break_opportunity(); |
| } else if self.had_content_yet_for_min_content { |
| self.pending_whitespace.min_content += advance; |
| } |
| if self.had_content_yet_for_max_content { |
| self.pending_whitespace.max_content += advance; |
| } |
| continue; |
| } |
| if can_wrap { |
| self.pending_whitespace.max_content += advance; |
| self.commit_pending_whitespace(); |
| self.line_break_opportunity(); |
| continue; |
| } |
| } |
| |
| self.commit_pending_whitespace(); |
| self.add_inline_size(advance); |
| |
| // Typically whitespace glyphs are placed in a separate store, |
| // but for `white-space: break-spaces` we place the first whitespace |
| // with the preceding text. That prevents a line break before that |
| // first space, but we still need to allow a line break after it. |
| if can_wrap && run.glyph_store.ends_with_whitespace() { |
| self.line_break_opportunity(); |
| } |
| } |
| } |
| }, |
| InlineItem::Atomic(atomic, offset_in_text, _level) => { |
| // TODO: need to handle TextWrapMode::Nowrap. |
| if !inline_formatting_context |
| .previous_character_prevents_soft_wrap_opportunity(*offset_in_text) |
| { |
| self.line_break_opportunity(); |
| } |
| |
| let InlineContentSizesResult { |
| sizes: outer, |
| depends_on_block_constraints, |
| } = atomic.borrow().outer_inline_content_sizes( |
| self.layout_context, |
| &self.constraint_space.into(), |
| &LogicalVec2::zero(), |
| false, /* auto_block_size_stretches_to_containing_block */ |
| ); |
| self.depends_on_block_constraints |= depends_on_block_constraints; |
| |
| if !inline_formatting_context |
| .next_character_prevents_soft_wrap_opportunity(*offset_in_text) |
| { |
| self.line_break_opportunity(); |
| } |
| |
| self.commit_pending_whitespace(); |
| self.current_line += outer; |
| }, |
| _ => {}, |
| } |
| } |
| |
| fn add_inline_size(&mut self, l: Au) { |
| self.current_line.min_content += l; |
| self.current_line.max_content += l; |
| } |
| |
| fn line_break_opportunity(&mut self) { |
| // Clear the pending whitespace, assuming that at the end of the line |
| // it needs to either hang or be removed. If that isn't the case, |
| // `commit_pending_whitespace()` should be called first. |
| self.pending_whitespace.min_content = Au::zero(); |
| let current_min_content = mem::take(&mut self.current_line.min_content); |
| self.paragraph.min_content.max_assign(current_min_content); |
| self.had_content_yet_for_min_content = false; |
| } |
| |
| fn forced_line_break(&mut self) { |
| // Handle the line break for min-content sizes. |
| self.line_break_opportunity(); |
| |
| // Repeat the same logic, but now for max-content sizes. |
| self.pending_whitespace.max_content = Au::zero(); |
| let current_max_content = mem::take(&mut self.current_line.max_content); |
| self.paragraph.max_content.max_assign(current_max_content); |
| self.had_content_yet_for_max_content = false; |
| } |
| |
| fn commit_pending_whitespace(&mut self) { |
| self.current_line += mem::take(&mut self.pending_whitespace); |
| self.had_content_yet_for_min_content = true; |
| self.had_content_yet_for_max_content = true; |
| } |
| |
| /// Compute the [`ContentSizes`] of the given [`InlineFormattingContext`]. |
| fn compute( |
| inline_formatting_context: &InlineFormattingContext, |
| layout_context: &'layout_data LayoutContext, |
| constraint_space: &'layout_data ConstraintSpace, |
| ) -> InlineContentSizesResult { |
| Self { |
| layout_context, |
| constraint_space, |
| paragraph: ContentSizes::zero(), |
| current_line: ContentSizes::zero(), |
| pending_whitespace: ContentSizes::zero(), |
| had_content_yet_for_min_content: false, |
| had_content_yet_for_max_content: false, |
| ending_inline_pbm_stack: Vec::new(), |
| depends_on_block_constraints: false, |
| } |
| .traverse(inline_formatting_context) |
| } |
| } |
| |
| /// Whether or not this character will rpevent a soft wrap opportunity when it |
| /// comes before or after an atomic inline element. |
| /// |
| /// From <https://www.w3.org/TR/css-text-3/#line-break-details>: |
| /// |
| /// > For Web-compatibility there is a soft wrap opportunity before and after each |
| /// > replaced element or other atomic inline, even when adjacent to a character that |
| /// > would normally suppress them, including U+00A0 NO-BREAK SPACE. However, with |
| /// > the exception of U+00A0 NO-BREAK SPACE, there must be no soft wrap opportunity |
| /// > between atomic inlines and adjacent characters belonging to the Unicode GL, WJ, |
| /// > or ZWJ line breaking classes. |
| fn char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character: char) -> bool { |
| if character == '\u{00A0}' { |
| return false; |
| } |
| let class = linebreak_property(character); |
| class == XI_LINE_BREAKING_CLASS_GL || |
| class == XI_LINE_BREAKING_CLASS_WJ || |
| class == XI_LINE_BREAKING_CLASS_ZWJ |
| } |