blob: dbc39b5d7db2234520f9e96e38cead4ddbf38d10 [file] [log] [blame] [edit]
/* 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
}