diff options
Diffstat (limited to 'components/layout_2020/flow/inline.rs')
-rw-r--r-- | components/layout_2020/flow/inline.rs | 1022 |
1 files changed, 793 insertions, 229 deletions
diff --git a/components/layout_2020/flow/inline.rs b/components/layout_2020/flow/inline.rs index a4d66c85c62..3da0673dcc8 100644 --- a/components/layout_2020/flow/inline.rs +++ b/components/layout_2020/flow/inline.rs @@ -15,10 +15,13 @@ use log::warn; use serde::Serialize; use servo_arc::Arc; use style::computed_values::white_space::T as WhiteSpace; +use style::context::QuirksMode; use style::logical_geometry::WritingMode; use style::properties::ComputedValues; -use style::values::computed::Length; +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; @@ -52,6 +55,10 @@ const XI_LINE_BREAKING_CLASS_GL: u8 = 12; const XI_LINE_BREAKING_CLASS_WJ: u8 = 30; const XI_LINE_BREAKING_CLASS_ZWJ: u8 = 40; +// From gfxFontConstants.h in Firefox. +static FONT_SUBSCRIPT_OFFSET_RATIO: f32 = 0.20; +static FONT_SUPERSCRIPT_OFFSET_RATIO: f32 = 0.34; + #[derive(Debug, Serialize)] pub(crate) struct InlineFormattingContext { pub(super) inline_level_boxes: Vec<ArcRefCell<InlineLevelBox>>, @@ -115,7 +122,9 @@ struct LineUnderConstruction { inline_position: Length, /// The maximum block size of all boxes that ended and are in progress in this line. - max_block_size: Length, + /// 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. @@ -145,7 +154,7 @@ impl LineUnderConstruction { Self { inline_position: start_position.inline.clone(), start_position: start_position, - max_block_size: Length::zero(), + max_block_size: LineBlockSizes::zero(), has_content: false, has_floats_waiting_to_be_placed: false, placement_among_floats: OnceCell::new(), @@ -184,12 +193,144 @@ impl LineUnderConstruction { } } +/// 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: Length, + baseline_relative_size_for_line_height: Option<BaselineRelativeSize>, + size_for_baseline_positioning: BaselineRelativeSize, +} + +impl LineBlockSizes { + fn zero() -> Self { + LineBlockSizes { + line_height: Length::zero(), + baseline_relative_size_for_line_height: None, + size_for_baseline_positioning: BaselineRelativeSize::zero(), + } + } + + fn resolve(&self) -> Length { + let height_from_ascent_and_descent = self + .baseline_relative_size_for_line_height + .as_ref() + .map(|size| Length::from((size.ascent + size.descent).abs())) + .unwrap_or_else(Length::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) { + self.baseline_relative_size_for_line_height + .as_mut() + .map(|size| 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) -> Length { + match self.baseline_relative_size_for_line_height.as_ref() { + Some(size) => size.ascent.into(), + 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) + .into(); + leading.scale_by(0.5) + self.size_for_baseline_positioning.ascent.into() + }, + } + } +} + /// 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. - size: LogicalVec2<Length>, + inline_size: Length, + + /// 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>, @@ -213,7 +354,12 @@ struct UnbreakableSegmentUnderConstruction { impl UnbreakableSegmentUnderConstruction { fn new() -> Self { Self { - size: LogicalVec2::zero(), + inline_size: Length::zero(), + max_block_size: LineBlockSizes { + line_height: Length::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, @@ -225,7 +371,8 @@ impl UnbreakableSegmentUnderConstruction { /// 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.size = LogicalVec2::zero(); + self.inline_size = Length::zero(); + self.max_block_size = LineBlockSizes::zero(); self.inline_box_hierarchy_depth = None; self.has_content = false; self.trailing_whitespace_size = Length::zero(); @@ -261,7 +408,7 @@ impl UnbreakableSegmentUnderConstruction { break; } } - self.size.inline -= whitespace_trimmed; + self.inline_size -= whitespace_trimmed; self.justification_opportunities -= spaces_trimmed; } @@ -323,6 +470,9 @@ impl UnbreakableSegmentUnderConstruction { } struct InlineContainerState { + /// The style of this inline container. + style: Arc<ComputedValues>, + /// 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: bool, @@ -333,11 +483,26 @@ struct InlineContainerState { // an IFC..." text_decoration_line: TextDecorationLine, - /// The block size of this inline container maxed with the block sizes of all inline - /// container ancestors. This isn't the block size of this container, but if this - /// container adds content to a line, this is the block size necessary for that new - /// content. - nested_block_size: Length, + /// 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). + baseline_offset: Au, + + /// The font metrics of the non-fallback font for this container. + font_metrics: FontMetrics, } struct InlineBoxContainerState { @@ -345,9 +510,6 @@ struct InlineBoxContainerState { /// [`InlineFormattingContext`]. base: InlineContainerState, - /// The style of this inline box. - style: Arc<ComputedValues>, - /// The [`BaseFragmentInfo`] of the [`InlineBox`] that this state tracks. base_fragment_info: BaseFragmentInfo, @@ -366,6 +528,22 @@ struct InlineFormattingContextState<'a, 'b> { sequential_layout_state: Option<&'a mut SequentialLayoutState>, layout_context: &'b LayoutContext<'b>, + /// 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<InlineBoxContainerState>, + /// A vector of fragment that are laid out. This includes one [`Fragment::Anonymous`] /// per line that is currently laid out plus fragments for all floats, which /// are currently laid out at the top-level of each [`InlineFormattingContext`]. @@ -377,6 +555,9 @@ struct InlineFormattingContextState<'a, 'b> { /// Information about the unbreakable line segment currently being laid out into [`LineItems`]s. current_line_segment: UnbreakableSegmentUnderConstruction, + /// The line breaking state for this inline formatting context. + linebreaker: Option<LineBreakLeafIter>, + /// 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 @@ -397,9 +578,6 @@ struct InlineFormattingContextState<'a, 'b> { /// [`InlineFormattingContextState::finish_inline_box()`]. linebreak_before_new_content: bool, - /// The line breaking state for this inline formatting context. - linebreaker: Option<LineBreakLeafIter>, - /// 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. @@ -411,20 +589,19 @@ struct InlineFormattingContextState<'a, 'b> { /// details. prevent_soft_wrap_opportunity_before_next_atomic: bool, + /// Whether or not this InlineFormattingContext has processed any in flow content at all. + had_inflow_content: bool, + /// The currently white-space setting of this line. This is stored on the /// [`InlineFormattingContextState`] because when a soft wrap opportunity is defined /// by the boundary between two characters, the white-space property of their nearest /// common ancestor is used. white_space: WhiteSpace, - /// The [`InlineContainerState`] for the container formed by the root of the - /// [`InlineFormattingContext`]. - 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<InlineBoxContainerState>, + /// The offset of the last baseline in the inline formatting context that we + /// are laying out. This is used to propagate baselines to the ancestors of + /// `display: inline-block` elements. + last_baseline_offset: Option<Length>, } impl<'a, 'b> InlineFormattingContextState<'a, 'b> { @@ -442,15 +619,15 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { } } - fn current_line_max_block_size(&self) -> Length { + fn current_line_max_block_size_including_nested_containers(&self) -> LineBlockSizes { self.current_inline_container_state() - .nested_block_size - .max(self.current_line.max_block_size) + .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.style, + Some(inline_box_state) => &inline_box_state.base.style, None => self.containing_block.style, }; self.white_space = style.get_inherited_text().white_space; @@ -459,17 +636,11 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { /// 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 (text_decoration_of_parent, nested_block_size_of_parent) = { - let parent = self.current_inline_container_state(); - (parent.text_decoration_line, parent.nested_block_size) - }; - let mut inline_box_state = InlineBoxContainerState::new( inline_box, &self.containing_block, - text_decoration_of_parent, - nested_block_size_of_parent, self.layout_context, + self.current_inline_container_state(), inline_box.is_last_fragment, ); @@ -483,11 +654,8 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { .auto_is(Length::zero); } - let line_item = inline_box_state.layout_into_line_item( - self.layout_context, - inline_box.is_first_fragment, - inline_box.is_last_fragment, - ); + let line_item = inline_box_state + .layout_into_line_item(inline_box.is_first_fragment, inline_box.is_last_fragment); self.push_line_item_to_unbreakable_segment(LineItem::StartInlineBox(line_item)); self.inline_box_state_stack.push(inline_box_state); } @@ -503,9 +671,8 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { self.push_line_item_to_unbreakable_segment(LineItem::EndInlineBox); self.current_line_segment - .size - .block - .max_assign(inline_box_state.base.nested_block_size); + .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 @@ -519,7 +686,7 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { 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(Length::zero); - self.current_line_segment.size.inline += pbm_end; + self.current_line_segment.inline_size += pbm_end; } } @@ -561,12 +728,12 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { had_inline_advance || self.linebreak_before_new_content { - self.current_line_max_block_size() + self.current_line_max_block_size_including_nested_containers() } else { - Length::zero() + LineBlockSizes::zero() }; - let block_end_position = block_start_position + effective_block_advance; + let block_end_position = block_start_position + effective_block_advance.resolve(); 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. @@ -579,13 +746,26 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { place_pending_floats(self, &mut line_items); } + // Set up the new line now that we no longer need the old one. + self.current_line = LineUnderConstruction::new(LogicalVec2 { + inline: Length::zero(), + block: block_end_position, + }); + + let baseline_offset = effective_block_advance.find_baseline_offset(); + let mut state = LineItemLayoutState { inline_position: inline_start_position, - inline_start_of_parent: Length::zero(), + parent_offset: LogicalVec2::zero(), + baseline_offset, ifc_containing_block: self.containing_block, positioning_context: &mut self.positioning_context, - line_block_start: block_start_position, justification_adjustment, + line_metrics: &LineMetrics { + block_offset: block_start_position, + block_size: effective_block_advance.resolve(), + baseline_block_offset: baseline_offset, + }, }; let positioning_context_length = state.positioning_context.len(); @@ -597,41 +777,42 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { &mut saw_end, ); - let size = LogicalVec2 { - inline: self.containing_block.inline_size, - block: effective_block_advance, - }; - - // 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: Length::zero(), - block: block_start_position, - }; - let line_had_content = !fragments.is_empty() || state.positioning_context.len() != positioning_context_length; - if line_had_content { - state - .positioning_context - .adjust_static_position_of_hoisted_fragments_with_offset( - &start_corner, - positioning_context_length, - ); - self.fragments - .push(Fragment::Anonymous(AnonymousFragment::new( - LogicalRect { start_corner, size }, - fragments, - self.containing_block.style.writing_mode, - ))); + // If the line doesn't have any fragments, we don't need to add a containing fragment for it. + if !line_had_content { + return; } - self.current_line = LineUnderConstruction::new(LogicalVec2 { - inline: Length::zero(), - block: block_end_position, - }); + self.last_baseline_offset = Some(baseline_offset + block_start_position); + let line_rect = LogicalRect { + // 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. + start_corner: LogicalVec2 { + inline: Length::zero(), + block: block_start_position, + }, + size: LogicalVec2 { + inline: self.containing_block.inline_size, + block: effective_block_advance.resolve(), + }, + }; + + state + .positioning_context + .adjust_static_position_of_hoisted_fragments_with_offset( + &line_rect.start_corner, + positioning_context_length, + ); + + self.fragments + .push(Fragment::Anonymous(AnonymousFragment::new( + line_rect, + fragments, + self.containing_block.style.writing_mode, + ))); } /// Given the amount of whitespace trimmed from the line and taking into consideration @@ -798,7 +979,7 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { // 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, + block: self.current_line.max_block_size.resolve(), }); self.current_line .replace_placement_among_floats(new_placement); @@ -937,10 +1118,11 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { // We need to ensure that the appropriate space for a linebox is created even if there // was no other content on this line. We mark the line as having content (needing a // advance) and having at least the height associated with this nesting of inline boxes. - //self.current_line.has_content = true; self.current_line .max_block_size - .max_assign(self.current_line_max_block_size()); + .max_assign(&self.current_line_max_block_size_including_nested_containers()); + + self.had_inflow_content = true; } fn possibly_flush_deferred_forced_line_break(&mut self) { @@ -966,69 +1148,84 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { font_metrics: &FontMetrics, font_key: FontInstanceKey, ) { - let inline_advance = Length::from(glyph_store.total_advance()); - - let is_non_preserved_whitespace = glyph_store.is_whitespace() && - !parent_style - .get_inherited_text() - .white_space - .preserve_spaces(); - if is_non_preserved_whitespace { - self.current_line_segment.trailing_whitespace_size = inline_advance; - } self.current_line_segment.justification_opportunities += glyph_store.total_word_separators() as usize; + let inline_advance = Length::from(glyph_store.total_advance()); + let preserve_spaces = parent_style + .get_inherited_text() + .white_space + .preserve_spaces(); + let is_collapsible_whitespace = glyph_store.is_whitespace() && !preserve_spaces; + + // 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. + // TODO(mrobinson): When we have font fallback, this should be calculating the + // block sizes of the fallback font. + let quirks_mode = self.layout_context.style_context.quirks_mode() != QuirksMode::NoQuirks; + let strut_size = if quirks_mode && !is_collapsible_whitespace { + self.current_inline_container_state() + .strut_block_sizes + .clone() + } else { + LineBlockSizes::zero() + }; + self.update_unbreakable_segment_for_new_content( + &strut_size, + inline_advance, + is_collapsible_whitespace, + ); + match self.current_line_segment.line_items.last_mut() { Some(LineItem::TextRun(text_run)) => { debug_assert!(font_key == text_run.font_key); text_run.text.push(glyph_store); - self.current_line_segment.size.inline += inline_advance; - - if !is_non_preserved_whitespace { - self.current_line_segment.has_content = true; - } return; }, _ => {}, } - self.push_content_line_item_to_unbreakable_segment( - inline_advance, - LineItem::TextRun(TextRunLineItem { - text: vec![glyph_store], - base_fragment_info: base_fragment_info.into(), - parent_style: parent_style.clone(), - font_metrics: font_metrics.clone(), - font_key, - text_decoration_line: self.current_inline_container_state().text_decoration_line, - }), - !is_non_preserved_whitespace, - ); + + self.push_line_item_to_unbreakable_segment(LineItem::TextRun(TextRunLineItem { + text: vec![glyph_store], + base_fragment_info: base_fragment_info.into(), + parent_style: parent_style.clone(), + font_metrics: font_metrics.clone(), + font_key, + text_decoration_line: self.current_inline_container_state().text_decoration_line, + })); } - fn push_content_line_item_to_unbreakable_segment( + fn update_unbreakable_segment_for_new_content( &mut self, + block_sizes_of_content: &LineBlockSizes, inline_size: Length, - line_item: LineItem, - counts_as_content: bool, + is_collapsible_whitespace: bool, ) { - if counts_as_content { + if !is_collapsible_whitespace { + self.current_line_segment.trailing_whitespace_size = Length::zero(); self.current_line_segment.has_content = true; + self.had_inflow_content = true; + } else { + self.current_line_segment.trailing_whitespace_size = inline_size; } - self.current_line_segment.size.inline += inline_size; + // 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 - .size - .block - .max_assign(self.current_inline_container_state().nested_block_size); + .max_block_size + .max_assign(container_max_block_size); self.current_line_segment - .size - .block - .max_assign(line_item.block_size()); - self.push_line_item_to_unbreakable_segment(line_item); + .max_block_size + .max_assign(block_sizes_of_content); - // We need to update the size of the current segment and also propagate the - // whitespace setting to the current nesting level. + self.current_line_segment.inline_size += inline_size; + + // Propagate the whitespace setting to the current nesting level. let current_nesting_level = self.current_inline_container_state_mut(); current_nesting_level.has_content = true; self.propagate_current_nesting_level_white_space_style(); @@ -1055,11 +1252,12 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { } let potential_line_size = LogicalVec2 { - inline: self.current_line.inline_position + self.current_line_segment.size.inline - + 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() - .max(self.current_line_segment.size.block), + .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) { @@ -1079,10 +1277,10 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { self.current_line_segment.trim_leading_whitespace(); } - self.current_line.inline_position += self.current_line_segment.size.inline; + self.current_line.inline_position += self.current_line_segment.inline_size; self.current_line.max_block_size = self - .current_line_max_block_size() - .max(self.current_line_segment.size.block); + .current_line_max_block_size_including_nested_containers() + .max(&self.current_line_segment.max_block_size); self.current_line.justification_opportunities += self.current_line_segment.justification_opportunities; let line_inline_size_without_trailing_whitespace = @@ -1109,7 +1307,7 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { 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.size.block, + block: self.current_line_segment.max_block_size.resolve(), }); assert!(!will_break); } @@ -1325,6 +1523,7 @@ impl InlineFormattingContext { Length::zero() }; + let style = containing_block.style; let mut ifc = InlineFormattingContextState { positioning_context, containing_block, @@ -1335,18 +1534,22 @@ impl InlineFormattingContext { inline: first_line_inline_start, block: Length::zero(), }), + root_nesting_level: InlineContainerState::new( + style.to_arc(), + layout_context, + None, /* parent_container */ + self.text_decoration_line, + inline_container_needs_strut(style, layout_context, None), + ), + linebreaker: None, + inline_box_state_stack: Vec::new(), current_line_segment: UnbreakableSegmentUnderConstruction::new(), linebreak_before_new_content: false, - white_space: containing_block.style.get_inherited_text().white_space, - linebreaker: None, have_deferred_soft_wrap_opportunity: false, prevent_soft_wrap_opportunity_before_next_atomic: false, - root_nesting_level: InlineContainerState { - nested_block_size: line_height_from_style(layout_context, &containing_block.style), - has_content: false, - text_decoration_line: self.text_decoration_line, - }, - inline_box_state_stack: Vec::new(), + had_inflow_content: false, + white_space: containing_block.style.get_inherited_text().white_space, + last_baseline_offset: None, }; // FIXME(pcwalton): This assumes that margins never collapse through inline formatting @@ -1411,13 +1614,15 @@ impl InlineFormattingContext { let mut collapsible_margins_in_children = CollapsedBlockMargins::zero(); let content_block_size = ifc.current_line.start_position.block; - collapsible_margins_in_children.collapsed_through = - content_block_size == Length::zero() && collapsible_with_parent_start_margin.0; + collapsible_margins_in_children.collapsed_through = !ifc.had_inflow_content && + content_block_size == Length::zero() && + collapsible_with_parent_start_margin.0; return FlowLayout { fragments: ifc.fragments, content_block_size, collapsible_margins_in_children, + last_inflow_baseline_offset: ifc.last_baseline_offset, }; } @@ -1445,44 +1650,210 @@ impl InlineFormattingContext { } } +impl InlineContainerState { + fn new( + style: Arc<ComputedValues>, + layout_context: &LayoutContext, + parent_container: Option<&InlineContainerState>, + parent_text_decoration_line: TextDecorationLine, + create_strut: bool, + ) -> Self { + let text_decoration_line = parent_text_decoration_line | style.clone_text_decoration_line(); + let font_metrics = font_metrics_from_style(layout_context, &style); + let line_height = line_height(&style, &font_metrics); + + let mut baseline_offset = Au::zero(); + let mut strut_block_sizes = + Self::get_block_sizes_with_style(&style, &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( + effective_vertical_for_inline_container(&style), + &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 create_strut { + nested_block_sizes.max_assign(&strut_block_sizes); + } + + Self { + style, + has_content: false, + text_decoration_line, + nested_strut_block_sizes: nested_block_sizes, + strut_block_sizes, + baseline_offset, + font_metrics, + } + } + + fn get_block_sizes_with_style( + style: &ComputedValues, + font_metrics: &FontMetrics, + line_height: Length, + ) -> LineBlockSizes { + let vertical_align = effective_vertical_for_inline_container(style); + 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_inherited_text().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_inherited_text().line_height != LineHeight::Normal { + let half_leading = + (Au::from_f32_px(line_height.px()) - (ascent + descent)).scale_by(0.5); + ascent = ascent + half_leading; + descent = descent + half_leading; + } + + LineBlockSizes { + line_height, + baseline_relative_size_for_line_height: Some(BaselineRelativeSize { ascent, descent }), + size_for_baseline_positioning, + } + } + + fn get_block_size_contribution(&self, font_metrics: &FontMetrics) -> LineBlockSizes { + Self::get_block_sizes_with_style( + &self.style, + font_metrics, + line_height(&self.style, &font_metrics), + ) + } + + fn get_cumulative_baseline_offset_for_child( + &self, + child_vertical_align: GenericVerticalAlign<LengthPercentage>, + child_block_size: &LineBlockSizes, + ) -> Au { + let block_size = self.get_block_size_contribution(&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. + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Baseline) | + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) | + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => Au::zero(), + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Sub) => Au::from_f32_px( + block_size + .resolve() + .scale_by(FONT_SUBSCRIPT_OFFSET_RATIO) + .px(), + ), + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Super) => -Au::from_f32_px( + block_size + .resolve() + .scale_by(FONT_SUPERSCRIPT_OFFSET_RATIO) + .px(), + ), + GenericVerticalAlign::Keyword(VerticalAlignKeyword::TextTop) => { + child_block_size.size_for_baseline_positioning.ascent - self.font_metrics.ascent + }, + GenericVerticalAlign::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) + }, + GenericVerticalAlign::Keyword(VerticalAlignKeyword::TextBottom) => { + (self.font_metrics.descent - + child_block_size.size_for_baseline_positioning.descent) + .into() + }, + GenericVerticalAlign::Length(length_percentage) => { + Au::from_f32_px(-length_percentage.resolve(child_block_size.line_height).px()) + }, + } + } +} + impl InlineBoxContainerState { fn new( inline_box: &InlineBox, containing_block: &ContainingBlock, - text_decoration_of_parent: TextDecorationLine, - nested_block_size_of_parent: Length, layout_context: &LayoutContext, + parent_container: &InlineContainerState, is_last_fragment: bool, ) -> Self { let style = inline_box.style.clone(); - let text_decoration_line = text_decoration_of_parent | style.clone_text_decoration_line(); + let pbm = style.padding_border_margin(containing_block); + let create_strut = inline_container_needs_strut(&style, layout_context, Some(&pbm)); Self { - base: InlineContainerState { - has_content: false, - text_decoration_line, - nested_block_size: nested_block_size_of_parent - .max(line_height_from_style(layout_context, &style)), - }, - style, + base: InlineContainerState::new( + style, + layout_context, + Some(parent_container), + parent_container.text_decoration_line, + create_strut, + ), base_fragment_info: inline_box.base_fragment_info, - pbm: inline_box.style.padding_border_margin(containing_block), + pbm, is_last_fragment, } } fn layout_into_line_item( &mut self, - layout_context: &LayoutContext, is_first_fragment: bool, is_last_fragment_of_ib_split: bool, ) -> InlineBoxLineItem { InlineBoxLineItem { base_fragment_info: self.base_fragment_info, - style: self.style.clone(), - block_size: line_gap_from_style(layout_context, &self.style), + style: self.base.style.clone(), pbm: self.pbm.clone(), is_first_fragment, is_last_fragment_of_ib_split, + font_metrics: self.base.font_metrics.clone(), + baseline_offset: self.base.baseline_offset, } } } @@ -1515,6 +1886,7 @@ impl IndependentFormattingContext { start_corner: pbm_sums.start_offset(), size, }; + BoxFragment::new( replaced.base_fragment_info, replaced.style.clone(), @@ -1523,7 +1895,8 @@ impl IndependentFormattingContext { pbm.padding, pbm.border, margin, - None, + None, /* clearance */ + None, /* last_inflow_baseline_offset */ CollapsedBlockMargins::zero(), ) }, @@ -1604,6 +1977,7 @@ impl IndependentFormattingContext { pbm.border, margin, None, + independent_layout.last_inflow_baseline_offset, CollapsedBlockMargins::zero(), ) }, @@ -1618,19 +1992,61 @@ impl IndependentFormattingContext { } let size = &pbm_sums.sum() + &fragment.content_rect.size; - ifc.push_content_line_item_to_unbreakable_segment( - size.inline, - LineItem::Atomic(AtomicLineItem { - fragment, - size, - positioning_context: child_positioning_context, - }), - true, - ); + let baseline_offset = fragment + .last_baseline_offset + .map(|baseline_offset| pbm_sums.block_start + baseline_offset) + .unwrap_or(size.block); + let baseline_offset = Au::from_f32_px(baseline_offset.px()); + + let (block_sizes, baseline_offset_in_parent) = + self.get_block_sizes_and_baseline_offset(ifc, size.block, baseline_offset); + ifc.update_unbreakable_segment_for_new_content(&block_sizes, size.inline, false); + ifc.push_line_item_to_unbreakable_segment(LineItem::Atomic(AtomicLineItem { + fragment, + size, + positioning_context: child_positioning_context, + baseline_offset_in_parent, + baseline_offset_in_item: baseline_offset, + })); // Defer a soft wrap opportunity for when we next process text content. ifc.have_deferred_soft_wrap_opportunity = true; } + + fn get_block_sizes_and_baseline_offset( + &self, + ifc: &InlineFormattingContextState, + block_size: Length, + 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: Au::from_f32_px(block_size.px()) - 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) + } } struct BreakAndShapeResult { @@ -1866,22 +2282,40 @@ 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 inline start position of the parent (the inline box that established this state) - /// relative to the edge of the containing block of this [`InlineFormattingCotnext`]. - inline_start_of_parent: Length, + /// The offset of the parent, relative to the start position of the line. + parent_offset: LogicalVec2<Length>, + + /// 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, - line_block_start: Length, /// 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( @@ -1979,20 +2413,6 @@ impl LineItem { LineItem::Float(_) => true, } } - - fn block_size(&self) -> Length { - match self { - LineItem::TextRun(text_run) => text_run.line_height(), - LineItem::StartInlineBox(_) => { - // TODO(mrobinson): This should get the line height from the font. - Length::zero() - }, - LineItem::EndInlineBox => Length::zero(), - LineItem::Atomic(atomic) => atomic.size.block, - LineItem::AbsolutelyPositioned(_) => Length::zero(), - LineItem::Float(_) => Length::zero(), - } - } } struct TextRunLineItem { @@ -2013,33 +2433,18 @@ fn line_height(parent_style: &ComputedValues, font_metrics: &FontMetrics) -> Len } } -fn line_gap_from_style(layout_context: &LayoutContext, style: &ComputedValues) -> Length { - crate::context::with_thread_local_font_context(layout_context, |font_context| { - let font_group = font_context.font_group(style.clone_font()); - let font = match font_group.borrow_mut().first(font_context) { - Some(font) => font, - None => { - warn!("Could not find find for TextRun."); - return Length::zero(); - }, - }; - let font = font.borrow(); - Length::from(font.metrics.line_gap) - }) -} - -fn line_height_from_style(layout_context: &LayoutContext, style: &ComputedValues) -> Length { +fn font_metrics_from_style(layout_context: &LayoutContext, style: &ComputedValues) -> FontMetrics { crate::context::with_thread_local_font_context(layout_context, |font_context| { let font_group = font_context.font_group(style.clone_font()); let font = match font_group.borrow_mut().first(font_context) { Some(font) => font, None => { warn!("Could not find find for TextRun."); - return Length::zero(); + return FontMetrics::empty(); }, }; let font = font.borrow(); - line_height(style, &font.metrics) + font.metrics.clone() }) } @@ -2108,10 +2513,6 @@ impl TextRunLineItem { self.text.is_empty() } - fn line_height(&self) -> Length { - line_height(&self.parent_style, &self.font_metrics) - } - fn layout(self, state: &mut LineItemLayoutState) -> Option<TextFragment> { if self.text.is_empty() { return None; @@ -2132,13 +2533,21 @@ impl TextRunLineItem { 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: LogicalVec2 { - block: Length::zero(), - inline: state.inline_position - state.inline_start_of_parent, - }, + start_corner, size: LogicalVec2 { - block: self.line_height(), + block: self.font_metrics.line_gap.into(), inline: inline_advance, }, }; @@ -2162,7 +2571,6 @@ struct InlineBoxLineItem { base_fragment_info: BaseFragmentInfo, style: Arc<ComputedValues>, pbm: PaddingBorderMargin, - block_size: Length, /// 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 @@ -2173,6 +2581,14 @@ struct InlineBoxLineItem { /// 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 { @@ -2200,6 +2616,9 @@ impl InlineBoxLineItem { 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, @@ -2209,11 +2628,15 @@ impl InlineBoxLineItem { let mut nested_state = LineItemLayoutState { inline_position: state.inline_position, - inline_start_of_parent: 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, - line_block_start: state.line_block_start, justification_adjustment: state.justification_adjustment, + line_metrics: state.line_metrics, + baseline_offset: block_start_offset + space_above_baseline, }; let mut saw_end = false; @@ -2244,16 +2667,17 @@ impl InlineBoxLineItem { let mut content_rect = LogicalRect { start_corner: LogicalVec2 { - inline: state.inline_position - state.inline_start_of_parent, - block: Length::zero(), + inline: state.inline_position, + block: block_start_offset, }, size: LogicalVec2 { inline: nested_state.inline_position - state.inline_position, - block: self.block_size, + block: self.font_metrics.line_gap.into(), }, }; - state.inline_position = nested_state.inline_position + pbm_sums.inline_end; + // 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. @@ -2269,10 +2693,16 @@ impl InlineBoxLineItem { padding, border, margin, - None, + 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); @@ -2292,22 +2722,76 @@ impl InlineBoxLineItem { 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<Length>, positioning_context: Option<PositioningContext>, + + /// 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 the PaddingBorderMargin sum - // start offset, which is the sum of the start component of the padding, - // border, and margin. Offset that value by the inline start position of the - // line layout. - self.fragment.content_rect.start_corner.inline += - state.inline_position - state.inline_start_of_parent; + // 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 += @@ -2326,6 +2810,24 @@ impl AtomicLineItem { 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 { @@ -2341,11 +2843,12 @@ impl AbsolutelyPositionedLineItem { LogicalVec2 { inline: match outside { DisplayOutside::Inline => { - state.inline_position - state.inline_start_of_parent + state.inline_position - state.parent_offset.inline }, DisplayOutside::Block => Length::zero(), }, - 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(_)) => { @@ -2387,8 +2890,8 @@ impl FloatLineItem { // formatting context, so that they are parented properly for StackingContext // properties such as opacity & filters. let distance_from_parent_to_ifc = LogicalVec2 { - inline: state.inline_start_of_parent, - block: state.line_block_start, + 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; @@ -2416,3 +2919,64 @@ fn char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character: ch class == XI_LINE_BREAKING_CLASS_WJ || class == XI_LINE_BREAKING_CLASS_ZWJ } + +fn is_baseline_relative(vertical_align: GenericVerticalAlign<LengthPercentage>) -> bool { + match vertical_align { + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) | + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => false, + _ => true, + } +} + +fn effective_vertical_for_inline_container( + style: &ComputedValues, +) -> GenericVerticalAlign<LengthPercentage> { + 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. +/// +/// 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) +} |