From 355567186400d0531657d0eedda7625de9d0e93a Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Wed, 10 Jan 2024 06:42:11 +0100 Subject: [PATCH] layout: Split `LineItem` layout into a new file (#31039) This is just a bit of code movement that trims down the size of the `inline.rs` file in order to make it a bit more manageable. It leads the way to more refactoring and cleanup in the future. --- components/layout_2020/flow/inline.rs | 611 +------------------------ components/layout_2020/flow/line.rs | 633 ++++++++++++++++++++++++++ components/layout_2020/flow/mod.rs | 1 + components/layout_2020/style_ext.rs | 16 +- 4 files changed, 658 insertions(+), 603 deletions(-) create mode 100644 components/layout_2020/flow/line.rs diff --git a/components/layout_2020/flow/inline.rs b/components/layout_2020/flow/inline.rs index 3da0673dcc8..e980983552a 100644 --- a/components/layout_2020/flow/inline.rs +++ b/components/layout_2020/flow/inline.rs @@ -4,10 +4,8 @@ use std::cell::OnceCell; use std::mem; -use std::vec::IntoIter; use app_units::Au; -use atomic_refcell::AtomicRef; use gfx::font::FontMetrics; use gfx::text::glyph::GlyphStore; use gfx::text::text_run::GlyphRun; @@ -21,7 +19,6 @@ use style::properties::ComputedValues; use style::values::computed::{Length, LengthPercentage}; use style::values::generics::box_::{GenericVerticalAlign, VerticalAlignKeyword}; use style::values::generics::text::LineHeight; -use style::values::specified::box_::DisplayOutside as StyloDisplayOutside; use style::values::specified::text::{TextAlignKeyword, TextDecorationLine}; use style::values::specified::{TextAlignLast, TextJustify}; use style::Zero; @@ -29,6 +26,10 @@ use webrender_api::FontInstanceKey; use xi_unicode::{linebreak_property, LineBreakLeafIter}; use super::float::PlacementAmongFloats; +use super::line::{ + layout_line_items, AbsolutelyPositionedLineItem, AtomicLineItem, FloatLineItem, + InlineBoxLineItem, LineItem, LineItemLayoutState, LineMetrics, TextRunLineItem, +}; use super::CollapsibleWithParentStartMargin; use crate::cell::ArcRefCell; use crate::context::LayoutContext; @@ -37,16 +38,12 @@ use crate::flow::FlowLayout; use crate::formatting_contexts::IndependentFormattingContext; use crate::fragment_tree::{ AnonymousFragment, BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, CollapsedMargin, - Fragment, HoistedSharedFragment, TextFragment, + Fragment, }; use crate::geom::{LogicalRect, LogicalVec2}; -use crate::positioned::{ - relative_adjustement, AbsolutelyPositionedBox, PositioningContext, PositioningContextLength, -}; +use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; use crate::sizing::ContentSizes; -use crate::style_ext::{ - ComputedValuesExt, Display, DisplayGeneratingBox, DisplayOutside, PaddingBorderMargin, -}; +use crate::style_ext::{ComputedValuesExt, PaddingBorderMargin}; use crate::ContainingBlock; // These constants are the xi-unicode line breaking classes that are defined in @@ -1669,7 +1666,7 @@ impl InlineContainerState { // 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( - effective_vertical_for_inline_container(&style), + style.effective_vertical_align_for_inline_layout(), &strut_block_sizes, ); strut_block_sizes.adjust_for_baseline_offset(baseline_offset); @@ -1698,7 +1695,7 @@ impl InlineContainerState { font_metrics: &FontMetrics, line_height: Length, ) -> LineBlockSizes { - let vertical_align = effective_vertical_for_inline_container(style); + let vertical_align = style.effective_vertical_align_for_inline_layout(); if !is_baseline_relative(vertical_align) { return LineBlockSizes { line_height, @@ -2282,81 +2279,6 @@ impl<'box_tree> Iterator for InlineBoxChildIter<'box_tree> { } } -struct LineMetrics { - /// The block offset of the line start in the containing [`InlineFormattingContext`]. - block_offset: Length, - - /// The block size of this line. - block_size: Length, - - /// The block offset of this line's baseline from [`Self:block_offset`]. - baseline_block_offset: Length, -} - -/// State used when laying out the [`LineItem`]s collected for the line currently being -/// laid out. -struct LineItemLayoutState<'a> { - inline_position: Length, - - /// The offset of the parent, relative to the start position of the line. - parent_offset: LogicalVec2, - - /// The block offset of the parent's baseline relative to the block start of the line. This - /// is often the same as [`Self::block_offset_of_parent`], but can be different for the root - /// element. - baseline_offset: Length, - - ifc_containing_block: &'a ContainingBlock<'a>, - positioning_context: &'a mut PositioningContext, - - /// The amount of space to add to each justification opportunity in order to implement - /// `text-align: justify`. - justification_adjustment: Length, - - /// The metrics of this line, which should remain constant throughout the - /// layout process. - line_metrics: &'a LineMetrics, -} - -fn layout_line_items( - iterator: &mut IntoIter, - layout_context: &LayoutContext, - state: &mut LineItemLayoutState, - saw_end: &mut bool, -) -> Vec { - let mut fragments = vec![]; - while let Some(item) = iterator.next() { - match item { - LineItem::TextRun(text_line_item) => { - if let Some(fragment) = text_line_item.layout(state) { - fragments.push(Fragment::Text(fragment)); - } - }, - LineItem::StartInlineBox(box_line_item) => { - if let Some(fragment) = box_line_item.layout(iterator, layout_context, state) { - fragments.push(Fragment::Box(fragment)) - } - }, - LineItem::EndInlineBox => { - *saw_end = true; - break; - }, - LineItem::Atomic(atomic_line_item) => { - fragments.push(Fragment::Box(atomic_line_item.layout(state))); - }, - LineItem::AbsolutelyPositioned(absolute_line_item) => { - fragments.push(Fragment::AbsoluteOrFixedPositioned( - absolute_line_item.layout(state), - )); - }, - LineItem::Float(float_line_item) => { - fragments.push(Fragment::Float(float_line_item.layout(state))); - }, - } - } - fragments -} - fn place_pending_floats(ifc: &mut InlineFormattingContextState, line_items: &mut Vec) { for item in line_items.into_iter() { match item { @@ -2370,60 +2292,6 @@ fn place_pending_floats(ifc: &mut InlineFormattingContextState, line_items: &mut } } -enum LineItem { - TextRun(TextRunLineItem), - StartInlineBox(InlineBoxLineItem), - EndInlineBox, - Atomic(AtomicLineItem), - AbsolutelyPositioned(AbsolutelyPositionedLineItem), - Float(FloatLineItem), -} - -impl LineItem { - fn trim_whitespace_at_end( - &mut self, - whitespace_trimmed: &mut Length, - spaces_trimmed: &mut usize, - ) -> bool { - match self { - LineItem::TextRun(ref mut item) => { - item.trim_whitespace_at_end(whitespace_trimmed, spaces_trimmed) - }, - LineItem::StartInlineBox(_) => true, - LineItem::EndInlineBox => true, - LineItem::Atomic(_) => false, - LineItem::AbsolutelyPositioned(_) => true, - LineItem::Float(_) => true, - } - } - - fn trim_whitespace_at_start( - &mut self, - whitespace_trimmed: &mut Length, - spaces_trimmed: &mut usize, - ) -> bool { - match self { - LineItem::TextRun(ref mut item) => { - item.trim_whitespace_at_start(whitespace_trimmed, spaces_trimmed) - }, - LineItem::StartInlineBox(_) => true, - LineItem::EndInlineBox => true, - LineItem::Atomic(_) => false, - LineItem::AbsolutelyPositioned(_) => true, - LineItem::Float(_) => true, - } - } -} - -struct TextRunLineItem { - base_fragment_info: BaseFragmentInfo, - parent_style: Arc, - text: Vec>, - font_metrics: FontMetrics, - font_key: FontInstanceKey, - text_decoration_line: TextDecorationLine, -} - fn line_height(parent_style: &ComputedValues, font_metrics: &FontMetrics) -> Length { let font_size = parent_style.get_font().font_size.computed_size(); match parent_style.get_inherited_text().line_height { @@ -2448,458 +2316,6 @@ fn font_metrics_from_style(layout_context: &LayoutContext, style: &ComputedValue }) } -impl TextRunLineItem { - fn trim_whitespace_at_end( - &mut self, - whitespace_trimmed: &mut Length, - spaces_trimmed: &mut usize, - ) -> bool { - if self - .parent_style - .get_inherited_text() - .white_space - .preserve_spaces() - { - return false; - } - - let index_of_last_non_whitespace = self - .text - .iter() - .rev() - .position(|glyph| !glyph.is_whitespace()) - .map(|offset_from_end| self.text.len() - offset_from_end); - - let first_whitespace_index = index_of_last_non_whitespace.unwrap_or(0); - *spaces_trimmed += self.text.len() - first_whitespace_index; - *whitespace_trimmed += self - .text - .drain(first_whitespace_index..) - .map(|glyph| Length::from(glyph.total_advance())) - .sum(); - - // Only keep going if we only encountered whitespace. - index_of_last_non_whitespace.is_none() - } - - fn trim_whitespace_at_start( - &mut self, - whitespace_trimmed: &mut Length, - spaces_trimmed: &mut usize, - ) -> bool { - if self - .parent_style - .get_inherited_text() - .white_space - .preserve_spaces() - { - return false; - } - - let index_of_first_non_whitespace = self - .text - .iter() - .position(|glyph| !glyph.is_whitespace()) - .unwrap_or(self.text.len()); - - *spaces_trimmed += index_of_first_non_whitespace; - *whitespace_trimmed += self - .text - .drain(0..index_of_first_non_whitespace) - .map(|glyph| Length::from(glyph.total_advance())) - .sum(); - - // Only keep going if we only encountered whitespace. - self.text.is_empty() - } - - fn layout(self, state: &mut LineItemLayoutState) -> Option { - if self.text.is_empty() { - return None; - } - - let mut number_of_justification_opportunities = 0; - let mut inline_advance: Length = self - .text - .iter() - .map(|glyph_store| { - number_of_justification_opportunities += glyph_store.total_word_separators(); - Length::from(glyph_store.total_advance()) - }) - .sum(); - - if !state.justification_adjustment.is_zero() { - inline_advance += - state.justification_adjustment * number_of_justification_opportunities as f32; - } - - // The block start of the TextRun is often zero (meaning it has the same font metrics as the - // inline box's strut), but for children of the inline formatting context root or for - // fallback fonts that use baseline relatve alignment, it might be different. - let mut start_corner = &LogicalVec2 { - inline: state.inline_position, - block: state.baseline_offset - self.font_metrics.ascent.into(), - } - &state.parent_offset; - if !is_baseline_relative(effective_vertical_for_inline_container(&self.parent_style)) { - start_corner.block = Length::zero(); - } - - let rect = LogicalRect { - start_corner, - size: LogicalVec2 { - block: self.font_metrics.line_gap.into(), - inline: inline_advance, - }, - }; - - state.inline_position += inline_advance; - Some(TextFragment { - base: self.base_fragment_info.into(), - parent_style: self.parent_style, - rect, - font_metrics: self.font_metrics, - font_key: self.font_key, - glyphs: self.text, - text_decoration_line: self.text_decoration_line, - justification_adjustment: state.justification_adjustment, - }) - } -} - -#[derive(Clone)] -struct InlineBoxLineItem { - base_fragment_info: BaseFragmentInfo, - style: Arc, - pbm: PaddingBorderMargin, - - /// Whether this is the first fragment for this inline box. This means that it's the - /// first potentially split box of a block-in-inline-split (or only if there's no - /// split) and also the first appearance of this fragment on any line. - is_first_fragment: bool, - - /// Whether this is the last fragment for this inline box. This means that it's the - /// last potentially split box of a block-in-inline-split (or the only fragment if - /// there's no split). - is_last_fragment_of_ib_split: bool, - - /// The FontMetrics for the default font used in this inline box. - font_metrics: FontMetrics, - - /// The block offset of this baseline relative to the baseline of the line. This will be - /// zero for boxes with `vertical-align: top` and `vertical-align: bottom` since their - /// baselines are calculated late in layout. - baseline_offset: Au, -} - -impl InlineBoxLineItem { - fn layout( - self, - iterator: &mut IntoIter, - layout_context: &LayoutContext, - state: &mut LineItemLayoutState, - ) -> Option { - let style = self.style.clone(); - let mut padding = self.pbm.padding.clone(); - let mut border = self.pbm.border.clone(); - let mut margin = self.pbm.margin.auto_is(Length::zero); - - if !self.is_first_fragment { - padding.inline_start = Length::zero(); - border.inline_start = Length::zero(); - margin.inline_start = Length::zero(); - } - if !self.is_last_fragment_of_ib_split { - padding.inline_end = Length::zero(); - border.inline_end = Length::zero(); - margin.inline_end = Length::zero(); - } - let pbm_sums = &(&padding + &border) + &margin; - state.inline_position += pbm_sums.inline_start; - - let space_above_baseline = self.calculate_space_above_baseline(); - let block_start_offset = self.calculate_block_start(state, space_above_baseline); - - let mut positioning_context = PositioningContext::new_for_style(&style); - let nested_positioning_context = match positioning_context.as_mut() { - Some(positioning_context) => positioning_context, - None => &mut state.positioning_context, - }; - let original_nested_positioning_context_length = nested_positioning_context.len(); - - let mut nested_state = LineItemLayoutState { - inline_position: state.inline_position, - parent_offset: LogicalVec2 { - inline: state.inline_position, - block: block_start_offset, - }, - ifc_containing_block: state.ifc_containing_block, - positioning_context: nested_positioning_context, - justification_adjustment: state.justification_adjustment, - line_metrics: state.line_metrics, - baseline_offset: block_start_offset + space_above_baseline, - }; - - let mut saw_end = false; - let fragments = - layout_line_items(iterator, layout_context, &mut nested_state, &mut saw_end); - - // Only add ending padding, border, margin if this is the last fragment of a - // potential block-in-inline split and this line included the actual end of this - // fragment (it doesn't continue on the next line). - if !self.is_last_fragment_of_ib_split || !saw_end { - padding.inline_end = Length::zero(); - border.inline_end = Length::zero(); - margin.inline_end = Length::zero(); - } - let pbm_sums = &(&padding + &border) + &margin; - - // If the inline box didn't have any content at all, don't add a Fragment for it. - let box_has_padding_border_or_margin = pbm_sums.inline_sum() > Length::zero(); - let box_had_absolutes = - original_nested_positioning_context_length != nested_state.positioning_context.len(); - if !self.is_first_fragment && - fragments.is_empty() && - !box_has_padding_border_or_margin && - !box_had_absolutes - { - return None; - } - - let mut content_rect = LogicalRect { - start_corner: LogicalVec2 { - inline: state.inline_position, - block: block_start_offset, - }, - size: LogicalVec2 { - inline: nested_state.inline_position - state.inline_position, - block: self.font_metrics.line_gap.into(), - }, - }; - - // Make `content_rect` relative to the parent Fragment. - content_rect.start_corner = &content_rect.start_corner - &state.parent_offset; - - // Relative adjustment should not affect the rest of line layout, so we can - // do it right before creating the Fragment. - if style.clone_position().is_relative() { - content_rect.start_corner += &relative_adjustement(&style, state.ifc_containing_block); - } - - let mut fragment = BoxFragment::new( - self.base_fragment_info, - self.style.clone(), - fragments, - content_rect, - padding, - border, - margin, - None, /* clearance */ - // There is no need to set a baseline offset for this BoxFragment, because - // the last baseline of this InlineFormattingContext is what will propagate - // to `display: inline-block` ancestors. - None, /* last_inflow_baseline_offset */ - CollapsedBlockMargins::zero(), - ); - - state.inline_position = nested_state.inline_position + pbm_sums.inline_end; - - if let Some(mut positioning_context) = positioning_context.take() { - assert!(original_nested_positioning_context_length == PositioningContextLength::zero()); - positioning_context.layout_collected_children(layout_context, &mut fragment); - positioning_context.adjust_static_position_of_hoisted_fragments_with_offset( - &fragment.content_rect.start_corner, - PositioningContextLength::zero(), - ); - state.positioning_context.append(positioning_context); - } else { - state - .positioning_context - .adjust_static_position_of_hoisted_fragments_with_offset( - &fragment.content_rect.start_corner, - original_nested_positioning_context_length, - ); - } - - Some(fragment) - } - - /// Given our font metrics, calculate the space above the baseline we need for our content. - /// Note that this space does not include space for any content in child inline boxes, as - /// they are not included in our content rect. - fn calculate_space_above_baseline(&self) -> Length { - let (ascent, descent, line_gap) = ( - self.font_metrics.ascent, - self.font_metrics.descent, - self.font_metrics.line_gap, - ); - let leading = line_gap - (ascent + descent); - (leading.scale_by(0.5) + ascent).into() - } - - /// Given the state for a line item layout and the space above the baseline for this inline - /// box, find the block start position relative to the line block start position. - fn calculate_block_start( - &self, - state: &LineItemLayoutState, - space_above_baseline: Length, - ) -> Length { - let vertical_align = effective_vertical_for_inline_container(&self.style); - let line_gap = self.font_metrics.line_gap; - - // The baseline offset that we have in `Self::baseline_offset` is relative to the line - // baseline, so we need to make it relative to the line block start. - match vertical_align { - GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => { - let line_height = line_height(&self.style, &self.font_metrics); - (line_height - line_gap.into()).scale_by(0.5) - }, - GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => { - let line_height = line_height(&self.style, &self.font_metrics); - let half_leading = (line_height - line_gap.into()).scale_by(0.5); - state.line_metrics.block_size - line_height + half_leading - }, - _ => { - state.line_metrics.baseline_block_offset + Length::from(self.baseline_offset) - - space_above_baseline - }, - } - } -} - -struct AtomicLineItem { - fragment: BoxFragment, - size: LogicalVec2, - positioning_context: Option, - - /// The block offset of this items' baseline relative to the baseline of the line. - /// This will be zero for boxes with `vertical-align: top` and `vertical-align: - /// bottom` since their baselines are calculated late in layout. - baseline_offset_in_parent: Au, - - /// The offset of the baseline inside this item. - baseline_offset_in_item: Au, -} - -impl AtomicLineItem { - fn layout(mut self, state: &mut LineItemLayoutState) -> BoxFragment { - // The initial `start_corner` of the Fragment is only the PaddingBorderMargin sum start - // offset, which is the sum of the start component of the padding, border, and margin. - // This needs to be added to the calculated block and inline positions. - self.fragment.content_rect.start_corner.inline += state.inline_position; - self.fragment.content_rect.start_corner.block += - self.calculate_block_start(&state.line_metrics); - - // Make the final result relative to the parent box. - self.fragment.content_rect.start_corner = - &self.fragment.content_rect.start_corner - &state.parent_offset; - - if self.fragment.style.clone_position().is_relative() { - self.fragment.content_rect.start_corner += - &relative_adjustement(&self.fragment.style, state.ifc_containing_block); - } - - state.inline_position += self.size.inline; - - if let Some(mut positioning_context) = self.positioning_context { - positioning_context.adjust_static_position_of_hoisted_fragments_with_offset( - &self.fragment.content_rect.start_corner, - PositioningContextLength::zero(), - ); - state.positioning_context.append(positioning_context); - } - - self.fragment - } - - /// Given the metrics for a line, our vertical alignment, and our block size, find a block start - /// position relative to the top of the line. - fn calculate_block_start(&self, line_metrics: &LineMetrics) -> Length { - match self.fragment.style.clone_vertical_align() { - GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => Length::zero(), - GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => { - line_metrics.block_size - self.size.block - }, - - // This covers all baseline-relative vertical alignment. - _ => { - let baseline = line_metrics.baseline_block_offset + - Length::from(self.baseline_offset_in_parent); - baseline - Length::from(self.baseline_offset_in_item) - }, - } - } -} - -struct AbsolutelyPositionedLineItem { - absolutely_positioned_box: ArcRefCell, -} - -impl AbsolutelyPositionedLineItem { - fn layout(self, state: &mut LineItemLayoutState) -> ArcRefCell { - let box_ = self.absolutely_positioned_box; - let style = AtomicRef::map(box_.borrow(), |box_| box_.context.style()); - let initial_start_corner = match Display::from(style.get_box().original_display) { - Display::GeneratingBox(DisplayGeneratingBox::OutsideInside { outside, inside: _ }) => { - LogicalVec2 { - inline: match outside { - DisplayOutside::Inline => { - state.inline_position - state.parent_offset.inline - }, - DisplayOutside::Block => Length::zero(), - }, - // The blocks start position of the absolute should be at the top of the line. - block: -state.parent_offset.block, - } - }, - Display::GeneratingBox(DisplayGeneratingBox::LayoutInternal(_)) => { - unreachable!( - "The result of blockification should never be a layout-internal value." - ); - }, - Display::Contents => { - panic!("display:contents does not generate an abspos box") - }, - Display::None => { - panic!("display:none does not generate an abspos box") - }, - }; - let hoisted_box = AbsolutelyPositionedBox::to_hoisted( - box_.clone(), - initial_start_corner, - state.ifc_containing_block, - ); - let hoisted_fragment = hoisted_box.fragment.clone(); - state.positioning_context.push(hoisted_box); - hoisted_fragment - } -} - -struct FloatLineItem { - fragment: BoxFragment, - /// Whether or not this float Fragment has been placed yet. Fragments that - /// do not fit on a line need to be placed after the hypothetical block start - /// of the next line. - needs_placement: bool, -} - -impl FloatLineItem { - fn layout(mut self, state: &mut LineItemLayoutState<'_>) -> BoxFragment { - // The `BoxFragment` for this float is positioned relative to the IFC, so we need - // to move it to be positioned relative to our parent InlineBox line item. Floats - // fragments are children of these InlineBoxes and not children of the inline - // formatting context, so that they are parented properly for StackingContext - // properties such as opacity & filters. - let distance_from_parent_to_ifc = LogicalVec2 { - inline: state.parent_offset.inline, - block: state.line_metrics.block_offset + state.parent_offset.block, - }; - self.fragment.content_rect.start_corner = - &self.fragment.content_rect.start_corner - &distance_from_parent_to_ifc; - self.fragment - } -} - -/// Whether or not this character prevents a soft line wrap opportunity when it /// comes before or after an atomic inline element. /// /// From https://www.w3.org/TR/css-text-3/#line-break-details: @@ -2928,15 +2344,6 @@ fn is_baseline_relative(vertical_align: GenericVerticalAlign) } } -fn effective_vertical_for_inline_container( - style: &ComputedValues, -) -> GenericVerticalAlign { - match style.clone_display().outside() { - StyloDisplayOutside::Block => GenericVerticalAlign::Keyword(VerticalAlignKeyword::Baseline), - _ => style.clone_vertical_align(), - } -} - /// 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. diff --git a/components/layout_2020/flow/line.rs b/components/layout_2020/flow/line.rs new file mode 100644 index 00000000000..42df588538e --- /dev/null +++ b/components/layout_2020/flow/line.rs @@ -0,0 +1,633 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::vec::IntoIter; + +use app_units::Au; +use atomic_refcell::AtomicRef; +use gfx::font::FontMetrics; +use gfx::text::glyph::GlyphStore; +use servo_arc::Arc; +use style::properties::ComputedValues; +use style::values::computed::{Length, LengthPercentage}; +use style::values::generics::box_::{GenericVerticalAlign, VerticalAlignKeyword}; +use style::values::generics::text::LineHeight; +use style::values::specified::text::TextDecorationLine; +use style::Zero; +use webrender_api::FontInstanceKey; + +use crate::cell::ArcRefCell; +use crate::context::LayoutContext; +use crate::fragment_tree::{ + BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, Fragment, HoistedSharedFragment, + TextFragment, +}; +use crate::geom::{LogicalRect, LogicalVec2}; +use crate::positioned::{ + relative_adjustement, AbsolutelyPositionedBox, PositioningContext, PositioningContextLength, +}; +use crate::style_ext::{ + ComputedValuesExt, Display, DisplayGeneratingBox, DisplayOutside, PaddingBorderMargin, +}; +use crate::ContainingBlock; + +pub(super) struct LineMetrics { + /// The block offset of the line start in the containing [`InlineFormattingContext`]. + pub block_offset: Length, + + /// The block size of this line. + pub block_size: Length, + + /// The block offset of this line's baseline from [`Self:block_offset`]. + pub baseline_block_offset: Length, +} + +/// State used when laying out the [`LineItem`]s collected for the line currently being +/// laid out. +pub(super) struct LineItemLayoutState<'a> { + pub inline_position: Length, + + /// The offset of the parent, relative to the start position of the line. + pub parent_offset: LogicalVec2, + + /// The block offset of the parent's baseline relative to the block start of the line. This + /// is often the same as [`Self::block_offset_of_parent`], but can be different for the root + /// element. + pub baseline_offset: Length, + + pub ifc_containing_block: &'a ContainingBlock<'a>, + pub positioning_context: &'a mut PositioningContext, + + /// The amount of space to add to each justification opportunity in order to implement + /// `text-align: justify`. + pub justification_adjustment: Length, + + /// The metrics of this line, which should remain constant throughout the + /// layout process. + pub line_metrics: &'a LineMetrics, +} + +pub(super) fn layout_line_items( + iterator: &mut IntoIter, + layout_context: &LayoutContext, + state: &mut LineItemLayoutState, + saw_end: &mut bool, +) -> Vec { + let mut fragments = vec![]; + while let Some(item) = iterator.next() { + match item { + LineItem::TextRun(text_line_item) => { + if let Some(fragment) = text_line_item.layout(state) { + fragments.push(Fragment::Text(fragment)); + } + }, + LineItem::StartInlineBox(box_line_item) => { + if let Some(fragment) = box_line_item.layout(iterator, layout_context, state) { + fragments.push(Fragment::Box(fragment)) + } + }, + LineItem::EndInlineBox => { + *saw_end = true; + break; + }, + LineItem::Atomic(atomic_line_item) => { + fragments.push(Fragment::Box(atomic_line_item.layout(state))); + }, + LineItem::AbsolutelyPositioned(absolute_line_item) => { + fragments.push(Fragment::AbsoluteOrFixedPositioned( + absolute_line_item.layout(state), + )); + }, + LineItem::Float(float_line_item) => { + fragments.push(Fragment::Float(float_line_item.layout(state))); + }, + } + } + fragments +} + +pub(super) enum LineItem { + TextRun(TextRunLineItem), + StartInlineBox(InlineBoxLineItem), + EndInlineBox, + Atomic(AtomicLineItem), + AbsolutelyPositioned(AbsolutelyPositionedLineItem), + Float(FloatLineItem), +} + +impl LineItem { + pub(super) fn trim_whitespace_at_end( + &mut self, + whitespace_trimmed: &mut Length, + spaces_trimmed: &mut usize, + ) -> bool { + match self { + LineItem::TextRun(ref mut item) => { + item.trim_whitespace_at_end(whitespace_trimmed, spaces_trimmed) + }, + LineItem::StartInlineBox(_) => true, + LineItem::EndInlineBox => true, + LineItem::Atomic(_) => false, + LineItem::AbsolutelyPositioned(_) => true, + LineItem::Float(_) => true, + } + } + + pub(super) fn trim_whitespace_at_start( + &mut self, + whitespace_trimmed: &mut Length, + spaces_trimmed: &mut usize, + ) -> bool { + match self { + LineItem::TextRun(ref mut item) => { + item.trim_whitespace_at_start(whitespace_trimmed, spaces_trimmed) + }, + LineItem::StartInlineBox(_) => true, + LineItem::EndInlineBox => true, + LineItem::Atomic(_) => false, + LineItem::AbsolutelyPositioned(_) => true, + LineItem::Float(_) => true, + } + } +} + +pub(super) struct TextRunLineItem { + pub base_fragment_info: BaseFragmentInfo, + pub parent_style: Arc, + pub text: Vec>, + pub font_metrics: FontMetrics, + pub font_key: FontInstanceKey, + pub text_decoration_line: TextDecorationLine, +} + +impl TextRunLineItem { + fn trim_whitespace_at_end( + &mut self, + whitespace_trimmed: &mut Length, + spaces_trimmed: &mut usize, + ) -> bool { + if self + .parent_style + .get_inherited_text() + .white_space + .preserve_spaces() + { + return false; + } + + let index_of_last_non_whitespace = self + .text + .iter() + .rev() + .position(|glyph| !glyph.is_whitespace()) + .map(|offset_from_end| self.text.len() - offset_from_end); + + let first_whitespace_index = index_of_last_non_whitespace.unwrap_or(0); + *spaces_trimmed += self.text.len() - first_whitespace_index; + *whitespace_trimmed += self + .text + .drain(first_whitespace_index..) + .map(|glyph| Length::from(glyph.total_advance())) + .sum(); + + // Only keep going if we only encountered whitespace. + index_of_last_non_whitespace.is_none() + } + + fn trim_whitespace_at_start( + &mut self, + whitespace_trimmed: &mut Length, + spaces_trimmed: &mut usize, + ) -> bool { + if self + .parent_style + .get_inherited_text() + .white_space + .preserve_spaces() + { + return false; + } + + let index_of_first_non_whitespace = self + .text + .iter() + .position(|glyph| !glyph.is_whitespace()) + .unwrap_or(self.text.len()); + + *spaces_trimmed += index_of_first_non_whitespace; + *whitespace_trimmed += self + .text + .drain(0..index_of_first_non_whitespace) + .map(|glyph| Length::from(glyph.total_advance())) + .sum(); + + // Only keep going if we only encountered whitespace. + self.text.is_empty() + } + + fn layout(self, state: &mut LineItemLayoutState) -> Option { + if self.text.is_empty() { + return None; + } + + let mut number_of_justification_opportunities = 0; + let mut inline_advance: Length = self + .text + .iter() + .map(|glyph_store| { + number_of_justification_opportunities += glyph_store.total_word_separators(); + Length::from(glyph_store.total_advance()) + }) + .sum(); + + if !state.justification_adjustment.is_zero() { + inline_advance += + state.justification_adjustment * number_of_justification_opportunities as f32; + } + + // The block start of the TextRun is often zero (meaning it has the same font metrics as the + // inline box's strut), but for children of the inline formatting context root or for + // fallback fonts that use baseline relatve alignment, it might be different. + let mut start_corner = &LogicalVec2 { + inline: state.inline_position, + block: state.baseline_offset - self.font_metrics.ascent.into(), + } - &state.parent_offset; + if !is_baseline_relative( + self.parent_style + .effective_vertical_align_for_inline_layout(), + ) { + start_corner.block = Length::zero(); + } + + let rect = LogicalRect { + start_corner, + size: LogicalVec2 { + block: self.font_metrics.line_gap.into(), + inline: inline_advance, + }, + }; + + state.inline_position += inline_advance; + Some(TextFragment { + base: self.base_fragment_info.into(), + parent_style: self.parent_style, + rect, + font_metrics: self.font_metrics, + font_key: self.font_key, + glyphs: self.text, + text_decoration_line: self.text_decoration_line, + justification_adjustment: state.justification_adjustment, + }) + } +} + +#[derive(Clone)] +pub(super) struct InlineBoxLineItem { + pub base_fragment_info: BaseFragmentInfo, + pub style: Arc, + pub pbm: PaddingBorderMargin, + + /// Whether this is the first fragment for this inline box. This means that it's the + /// first potentially split box of a block-in-inline-split (or only if there's no + /// split) and also the first appearance of this fragment on any line. + pub is_first_fragment: bool, + + /// Whether this is the last fragment for this inline box. This means that it's the + /// last potentially split box of a block-in-inline-split (or the only fragment if + /// there's no split). + pub is_last_fragment_of_ib_split: bool, + + /// The FontMetrics for the default font used in this inline box. + pub font_metrics: FontMetrics, + + /// The block offset of this baseline relative to the baseline of the line. This will be + /// zero for boxes with `vertical-align: top` and `vertical-align: bottom` since their + /// baselines are calculated late in layout. + pub baseline_offset: Au, +} + +impl InlineBoxLineItem { + fn layout( + self, + iterator: &mut IntoIter, + layout_context: &LayoutContext, + state: &mut LineItemLayoutState, + ) -> Option { + let style = self.style.clone(); + let mut padding = self.pbm.padding.clone(); + let mut border = self.pbm.border.clone(); + let mut margin = self.pbm.margin.auto_is(Length::zero); + + if !self.is_first_fragment { + padding.inline_start = Length::zero(); + border.inline_start = Length::zero(); + margin.inline_start = Length::zero(); + } + if !self.is_last_fragment_of_ib_split { + padding.inline_end = Length::zero(); + border.inline_end = Length::zero(); + margin.inline_end = Length::zero(); + } + let pbm_sums = &(&padding + &border) + &margin; + state.inline_position += pbm_sums.inline_start; + + let space_above_baseline = self.calculate_space_above_baseline(); + let block_start_offset = self.calculate_block_start(state, space_above_baseline); + + let mut positioning_context = PositioningContext::new_for_style(&style); + let nested_positioning_context = match positioning_context.as_mut() { + Some(positioning_context) => positioning_context, + None => &mut state.positioning_context, + }; + let original_nested_positioning_context_length = nested_positioning_context.len(); + + let mut nested_state = LineItemLayoutState { + inline_position: state.inline_position, + parent_offset: LogicalVec2 { + inline: state.inline_position, + block: block_start_offset, + }, + ifc_containing_block: state.ifc_containing_block, + positioning_context: nested_positioning_context, + justification_adjustment: state.justification_adjustment, + line_metrics: state.line_metrics, + baseline_offset: block_start_offset + space_above_baseline, + }; + + let mut saw_end = false; + let fragments = + layout_line_items(iterator, layout_context, &mut nested_state, &mut saw_end); + + // Only add ending padding, border, margin if this is the last fragment of a + // potential block-in-inline split and this line included the actual end of this + // fragment (it doesn't continue on the next line). + if !self.is_last_fragment_of_ib_split || !saw_end { + padding.inline_end = Length::zero(); + border.inline_end = Length::zero(); + margin.inline_end = Length::zero(); + } + let pbm_sums = &(&padding + &border) + &margin; + + // If the inline box didn't have any content at all, don't add a Fragment for it. + let box_has_padding_border_or_margin = pbm_sums.inline_sum() > Length::zero(); + let box_had_absolutes = + original_nested_positioning_context_length != nested_state.positioning_context.len(); + if !self.is_first_fragment && + fragments.is_empty() && + !box_has_padding_border_or_margin && + !box_had_absolutes + { + return None; + } + + let mut content_rect = LogicalRect { + start_corner: LogicalVec2 { + inline: state.inline_position, + block: block_start_offset, + }, + size: LogicalVec2 { + inline: nested_state.inline_position - state.inline_position, + block: self.font_metrics.line_gap.into(), + }, + }; + + // Make `content_rect` relative to the parent Fragment. + content_rect.start_corner = &content_rect.start_corner - &state.parent_offset; + + // Relative adjustment should not affect the rest of line layout, so we can + // do it right before creating the Fragment. + if style.clone_position().is_relative() { + content_rect.start_corner += &relative_adjustement(&style, state.ifc_containing_block); + } + + let mut fragment = BoxFragment::new( + self.base_fragment_info, + self.style.clone(), + fragments, + content_rect, + padding, + border, + margin, + None, /* clearance */ + // There is no need to set a baseline offset for this BoxFragment, because + // the last baseline of this InlineFormattingContext is what will propagate + // to `display: inline-block` ancestors. + None, /* last_inflow_baseline_offset */ + CollapsedBlockMargins::zero(), + ); + + state.inline_position = nested_state.inline_position + pbm_sums.inline_end; + + if let Some(mut positioning_context) = positioning_context.take() { + assert!(original_nested_positioning_context_length == PositioningContextLength::zero()); + positioning_context.layout_collected_children(layout_context, &mut fragment); + positioning_context.adjust_static_position_of_hoisted_fragments_with_offset( + &fragment.content_rect.start_corner, + PositioningContextLength::zero(), + ); + state.positioning_context.append(positioning_context); + } else { + state + .positioning_context + .adjust_static_position_of_hoisted_fragments_with_offset( + &fragment.content_rect.start_corner, + original_nested_positioning_context_length, + ); + } + + Some(fragment) + } + + /// Given our font metrics, calculate the space above the baseline we need for our content. + /// Note that this space does not include space for any content in child inline boxes, as + /// they are not included in our content rect. + fn calculate_space_above_baseline(&self) -> Length { + let (ascent, descent, line_gap) = ( + self.font_metrics.ascent, + self.font_metrics.descent, + self.font_metrics.line_gap, + ); + let leading = line_gap - (ascent + descent); + (leading.scale_by(0.5) + ascent).into() + } + + /// Given the state for a line item layout and the space above the baseline for this inline + /// box, find the block start position relative to the line block start position. + fn calculate_block_start( + &self, + state: &LineItemLayoutState, + space_above_baseline: Length, + ) -> Length { + let vertical_align = self.style.effective_vertical_align_for_inline_layout(); + let line_gap = self.font_metrics.line_gap; + + // The baseline offset that we have in `Self::baseline_offset` is relative to the line + // baseline, so we need to make it relative to the line block start. + match vertical_align { + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => { + let line_height = line_height(&self.style, &self.font_metrics); + (line_height - line_gap.into()).scale_by(0.5) + }, + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => { + let line_height = line_height(&self.style, &self.font_metrics); + let half_leading = (line_height - line_gap.into()).scale_by(0.5); + state.line_metrics.block_size - line_height + half_leading + }, + _ => { + state.line_metrics.baseline_block_offset + Length::from(self.baseline_offset) - + space_above_baseline + }, + } + } +} + +pub(super) struct AtomicLineItem { + pub fragment: BoxFragment, + pub size: LogicalVec2, + pub positioning_context: Option, + + /// The block offset of this items' baseline relative to the baseline of the line. + /// This will be zero for boxes with `vertical-align: top` and `vertical-align: + /// bottom` since their baselines are calculated late in layout. + pub baseline_offset_in_parent: Au, + + /// The offset of the baseline inside this item. + pub baseline_offset_in_item: Au, +} + +impl AtomicLineItem { + fn layout(mut self, state: &mut LineItemLayoutState) -> BoxFragment { + // The initial `start_corner` of the Fragment is only the PaddingBorderMargin sum start + // offset, which is the sum of the start component of the padding, border, and margin. + // This needs to be added to the calculated block and inline positions. + self.fragment.content_rect.start_corner.inline += state.inline_position; + self.fragment.content_rect.start_corner.block += + self.calculate_block_start(&state.line_metrics); + + // Make the final result relative to the parent box. + self.fragment.content_rect.start_corner = + &self.fragment.content_rect.start_corner - &state.parent_offset; + + if self.fragment.style.clone_position().is_relative() { + self.fragment.content_rect.start_corner += + &relative_adjustement(&self.fragment.style, state.ifc_containing_block); + } + + state.inline_position += self.size.inline; + + if let Some(mut positioning_context) = self.positioning_context { + positioning_context.adjust_static_position_of_hoisted_fragments_with_offset( + &self.fragment.content_rect.start_corner, + PositioningContextLength::zero(), + ); + state.positioning_context.append(positioning_context); + } + + self.fragment + } + + /// Given the metrics for a line, our vertical alignment, and our block size, find a block start + /// position relative to the top of the line. + fn calculate_block_start(&self, line_metrics: &LineMetrics) -> Length { + match self.fragment.style.clone_vertical_align() { + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => Length::zero(), + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => { + line_metrics.block_size - self.size.block + }, + + // This covers all baseline-relative vertical alignment. + _ => { + let baseline = line_metrics.baseline_block_offset + + Length::from(self.baseline_offset_in_parent); + baseline - Length::from(self.baseline_offset_in_item) + }, + } + } +} + +pub(super) struct AbsolutelyPositionedLineItem { + pub absolutely_positioned_box: ArcRefCell, +} + +impl AbsolutelyPositionedLineItem { + fn layout(self, state: &mut LineItemLayoutState) -> ArcRefCell { + let box_ = self.absolutely_positioned_box; + let style = AtomicRef::map(box_.borrow(), |box_| box_.context.style()); + let initial_start_corner = match Display::from(style.get_box().original_display) { + Display::GeneratingBox(DisplayGeneratingBox::OutsideInside { outside, inside: _ }) => { + LogicalVec2 { + inline: match outside { + DisplayOutside::Inline => { + state.inline_position - state.parent_offset.inline + }, + DisplayOutside::Block => Length::zero(), + }, + // The blocks start position of the absolute should be at the top of the line. + block: -state.parent_offset.block, + } + }, + Display::GeneratingBox(DisplayGeneratingBox::LayoutInternal(_)) => { + unreachable!( + "The result of blockification should never be a layout-internal value." + ); + }, + Display::Contents => { + panic!("display:contents does not generate an abspos box") + }, + Display::None => { + panic!("display:none does not generate an abspos box") + }, + }; + let hoisted_box = AbsolutelyPositionedBox::to_hoisted( + box_.clone(), + initial_start_corner, + state.ifc_containing_block, + ); + let hoisted_fragment = hoisted_box.fragment.clone(); + state.positioning_context.push(hoisted_box); + hoisted_fragment + } +} + +pub(super) struct FloatLineItem { + pub fragment: BoxFragment, + /// Whether or not this float Fragment has been placed yet. Fragments that + /// do not fit on a line need to be placed after the hypothetical block start + /// of the next line. + pub needs_placement: bool, +} + +impl FloatLineItem { + fn layout(mut self, state: &mut LineItemLayoutState<'_>) -> BoxFragment { + // The `BoxFragment` for this float is positioned relative to the IFC, so we need + // to move it to be positioned relative to our parent InlineBox line item. Floats + // fragments are children of these InlineBoxes and not children of the inline + // formatting context, so that they are parented properly for StackingContext + // properties such as opacity & filters. + let distance_from_parent_to_ifc = LogicalVec2 { + inline: state.parent_offset.inline, + block: state.line_metrics.block_offset + state.parent_offset.block, + }; + self.fragment.content_rect.start_corner = + &self.fragment.content_rect.start_corner - &distance_from_parent_to_ifc; + self.fragment + } +} + +fn is_baseline_relative(vertical_align: GenericVerticalAlign) -> bool { + match vertical_align { + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) | + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => false, + _ => true, + } +} + +fn line_height(parent_style: &ComputedValues, font_metrics: &FontMetrics) -> Length { + let font_size = parent_style.get_font().font_size.computed_size(); + match parent_style.get_inherited_text().line_height { + LineHeight::Normal => Length::from(font_metrics.line_gap), + LineHeight::Number(number) => font_size * number.0, + LineHeight::Length(length) => length.0, + } +} diff --git a/components/layout_2020/flow/mod.rs b/components/layout_2020/flow/mod.rs index 4d0d96dee80..95c03655980 100644 --- a/components/layout_2020/flow/mod.rs +++ b/components/layout_2020/flow/mod.rs @@ -36,6 +36,7 @@ use crate::ContainingBlock; mod construct; pub mod float; pub mod inline; +mod line; mod root; pub(crate) use construct::BlockContainerBuilder; diff --git a/components/layout_2020/style_ext.rs b/components/layout_2020/style_ext.rs index 9220656f8ab..a2017e5d0d5 100644 --- a/components/layout_2020/style_ext.rs +++ b/components/layout_2020/style_ext.rs @@ -13,8 +13,9 @@ use style::properties::longhands::column_span::computed_value::T as ColumnSpan; use style::properties::ComputedValues; use style::values::computed::image::Image as ComputedImageLayer; use style::values::computed::{Length, LengthPercentage, NonNegativeLengthPercentage, Size}; -use style::values::generics::box_::Perspective; +use style::values::generics::box_::{GenericVerticalAlign, Perspective, VerticalAlignKeyword}; use style::values::generics::length::MaxSize; +use style::values::specified::box_::DisplayOutside as StyloDisplayOutside; use style::values::specified::{box_ as stylo, Overflow}; use style::Zero; use webrender_api as wr; @@ -168,6 +169,7 @@ pub(crate) trait ComputedValuesExt { fn establishes_containing_block_for_all_descendants(&self) -> bool; fn background_is_transparent(&self) -> bool; fn get_webrender_primitive_flags(&self) -> wr::PrimitiveFlags; + fn effective_vertical_align_for_inline_layout(&self) -> GenericVerticalAlign; } impl ComputedValuesExt for ComputedValues { @@ -533,6 +535,18 @@ impl ComputedValuesExt for ComputedValues { BackfaceVisiblity::Hidden => wr::PrimitiveFlags::empty(), } } + + /// Get the effective `vertical-align` property for inline layout. Essentially, if this style + /// has outside block display, this is the inline formatting context root and `vertical-align` + /// doesn't come into play for inline layout. + fn effective_vertical_align_for_inline_layout(&self) -> GenericVerticalAlign { + match self.clone_display().outside() { + StyloDisplayOutside::Block => { + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Baseline) + }, + _ => self.clone_vertical_align(), + } + } } impl From for Display {