diff options
author | Martin Robinson <mrobinson@igalia.com> | 2025-04-19 12:17:03 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-04-19 10:17:03 +0000 |
commit | 7787cab521ccc6b4d8533ebe9b45563046e0463d (patch) | |
tree | d1277fa3846f24cc99859310da3d7f099c73bfc5 /components/layout/flow/inline | |
parent | 3ab5b8c4472129798b63cfb40b63ae672763b653 (diff) | |
download | servo-7787cab521ccc6b4d8533ebe9b45563046e0463d.tar.gz servo-7787cab521ccc6b4d8533ebe9b45563046e0463d.zip |
layout: Combine `layout_2020` and `layout_thread_2020` into a crate called `layout` (#36613)
Now that legacy layout has been removed, the name `layout_2020` doesn't
make much sense any longer, also it's 2025 now for better or worse. The
split between the "layout thread" and "layout" also doesn't make as much
sense since layout doesn't run on it's own thread. There's a possibility
that it will in the future, but that should be something that the user
of the crate controls rather than layout iself.
This is part of the larger layout interface cleanup and optimization
that
@Looriool and I are doing.
Testing: Covered by existing tests as this is just code movement.
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Diffstat (limited to 'components/layout/flow/inline')
-rw-r--r-- | components/layout/flow/inline/construct.rs | 636 | ||||
-rw-r--r-- | components/layout/flow/inline/inline_box.rs | 257 | ||||
-rw-r--r-- | components/layout/flow/inline/line.rs | 911 | ||||
-rw-r--r-- | components/layout/flow/inline/line_breaker.rs | 120 | ||||
-rw-r--r-- | components/layout/flow/inline/mod.rs | 2525 | ||||
-rw-r--r-- | components/layout/flow/inline/text_run.rs | 640 |
6 files changed, 5089 insertions, 0 deletions
diff --git a/components/layout/flow/inline/construct.rs b/components/layout/flow/inline/construct.rs new file mode 100644 index 00000000000..7c668751ef6 --- /dev/null +++ b/components/layout/flow/inline/construct.rs @@ -0,0 +1,636 @@ +/* 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::borrow::Cow; +use std::char::{ToLowercase, ToUppercase}; + +use icu_segmenter::WordSegmenter; +use servo_arc::Arc; +use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; +use style::values::specified::text::TextTransformCase; +use unicode_bidi::Level; + +use super::text_run::TextRun; +use super::{InlineBox, InlineBoxIdentifier, InlineBoxes, InlineFormattingContext, InlineItem}; +use crate::PropagatedBoxTreeData; +use crate::cell::ArcRefCell; +use crate::context::LayoutContext; +use crate::dom::NodeExt; +use crate::dom_traversal::NodeAndStyleInfo; +use crate::flow::float::FloatBox; +use crate::formatting_contexts::IndependentFormattingContext; +use crate::positioned::AbsolutelyPositionedBox; +use crate::style_ext::ComputedValuesExt; + +#[derive(Default)] +pub(crate) struct InlineFormattingContextBuilder { + /// The collection of text strings that make up this [`InlineFormattingContext`] under + /// construction. + pub text_segments: Vec<String>, + + /// The current offset in the final text string of this [`InlineFormattingContext`], + /// used to properly set the text range of new [`InlineItem::TextRun`]s. + current_text_offset: usize, + + /// Whether the last processed node ended with whitespace. This is used to + /// implement rule 4 of <https://www.w3.org/TR/css-text-3/#collapse>: + /// + /// > Any collapsible space immediately following another collapsible space—even one + /// > outside the boundary of the inline containing that space, provided both spaces are + /// > within the same inline formatting context—is collapsed to have zero advance width. + /// > (It is invisible, but retains its soft wrap opportunity, if any.) + last_inline_box_ended_with_collapsible_white_space: bool, + + /// Whether or not the current state of the inline formatting context is on a word boundary + /// for the purposes of `text-transform: capitalize`. + on_word_boundary: bool, + + /// Whether or not this inline formatting context will contain floats. + pub contains_floats: bool, + + /// The current list of [`InlineItem`]s in this [`InlineFormattingContext`] under + /// construction. This is stored in a flat list to make it easy to access the last + /// item. + pub inline_items: Vec<ArcRefCell<InlineItem>>, + + /// The current [`InlineBox`] tree of this [`InlineFormattingContext`] under construction. + pub inline_boxes: InlineBoxes, + + /// The ongoing stack of inline boxes stack of the builder. + /// + /// Contains all the currently ongoing inline boxes we entered so far. + /// The traversal is at all times as deep in the tree as this stack is, + /// which is why the code doesn't need to keep track of the actual + /// container root (see `handle_inline_level_element`). + /// + /// When an inline box ends, it's removed from this stack. + inline_box_stack: Vec<InlineBoxIdentifier>, + + /// Whether or not the inline formatting context under construction has any + /// uncollapsible text content. + pub has_uncollapsible_text_content: bool, +} + +impl InlineFormattingContextBuilder { + pub(crate) fn new() -> Self { + // For the purposes of `text-transform: capitalize` the start of the IFC is a word boundary. + Self { + on_word_boundary: true, + ..Default::default() + } + } + + pub(crate) fn currently_processing_inline_box(&self) -> bool { + !self.inline_box_stack.is_empty() + } + + fn push_control_character_string(&mut self, string_to_push: &str) { + self.text_segments.push(string_to_push.to_owned()); + self.current_text_offset += string_to_push.len(); + } + + /// Return true if this [`InlineFormattingContextBuilder`] is empty for the purposes of ignoring + /// during box tree construction. An IFC is empty if it only contains TextRuns with + /// completely collapsible whitespace. When that happens it can be ignored completely. + pub(crate) fn is_empty(&self) -> bool { + if self.has_uncollapsible_text_content { + return false; + } + + if !self.inline_box_stack.is_empty() { + return false; + } + + fn inline_level_box_is_empty(inline_level_box: &InlineItem) -> bool { + match inline_level_box { + InlineItem::StartInlineBox(_) => false, + InlineItem::EndInlineBox => false, + // Text content is handled by `self.has_uncollapsible_text` content above in order + // to avoid having to iterate through the character once again. + InlineItem::TextRun(_) => true, + InlineItem::OutOfFlowAbsolutelyPositionedBox(..) => false, + InlineItem::OutOfFlowFloatBox(_) => false, + InlineItem::Atomic(..) => false, + } + } + + self.inline_items + .iter() + .all(|inline_level_box| inline_level_box_is_empty(&inline_level_box.borrow())) + } + + pub(crate) fn push_atomic( + &mut self, + independent_formatting_context: IndependentFormattingContext, + ) -> ArcRefCell<InlineItem> { + let inline_level_box = ArcRefCell::new(InlineItem::Atomic( + Arc::new(independent_formatting_context), + self.current_text_offset, + Level::ltr(), /* This will be assigned later if necessary. */ + )); + self.inline_items.push(inline_level_box.clone()); + + // Push an object replacement character for this atomic, which will ensure that the line breaker + // inserts a line breaking opportunity here. + self.push_control_character_string("\u{fffc}"); + + self.last_inline_box_ended_with_collapsible_white_space = false; + self.on_word_boundary = true; + + inline_level_box + } + + pub(crate) fn push_absolutely_positioned_box( + &mut self, + absolutely_positioned_box: AbsolutelyPositionedBox, + ) -> ArcRefCell<InlineItem> { + let absolutely_positioned_box = ArcRefCell::new(absolutely_positioned_box); + let inline_level_box = ArcRefCell::new(InlineItem::OutOfFlowAbsolutelyPositionedBox( + absolutely_positioned_box, + self.current_text_offset, + )); + + self.inline_items.push(inline_level_box.clone()); + inline_level_box + } + + pub(crate) fn push_float_box(&mut self, float_box: FloatBox) -> ArcRefCell<InlineItem> { + let inline_level_box = ArcRefCell::new(InlineItem::OutOfFlowFloatBox(Arc::new(float_box))); + self.inline_items.push(inline_level_box.clone()); + self.contains_floats = true; + inline_level_box + } + + pub(crate) fn start_inline_box(&mut self, inline_box: InlineBox) -> ArcRefCell<InlineItem> { + self.push_control_character_string(inline_box.base.style.bidi_control_chars().0); + + let (identifier, inline_box) = self.inline_boxes.start_inline_box(inline_box); + let inline_level_box = ArcRefCell::new(InlineItem::StartInlineBox(inline_box)); + self.inline_items.push(inline_level_box.clone()); + self.inline_box_stack.push(identifier); + inline_level_box + } + + pub(crate) fn end_inline_box(&mut self) -> ArcRefCell<InlineBox> { + let identifier = self.end_inline_box_internal(); + let inline_level_box = self.inline_boxes.get(&identifier); + inline_level_box.borrow_mut().is_last_fragment = true; + + self.push_control_character_string( + inline_level_box.borrow().base.style.bidi_control_chars().1, + ); + + inline_level_box + } + + fn end_inline_box_internal(&mut self) -> InlineBoxIdentifier { + let identifier = self + .inline_box_stack + .pop() + .expect("Ended non-existent inline box"); + self.inline_items + .push(ArcRefCell::new(InlineItem::EndInlineBox)); + + self.inline_boxes.end_inline_box(identifier); + identifier + } + + pub(crate) fn push_text<'dom, Node: NodeExt<'dom>>( + &mut self, + text: Cow<'dom, str>, + info: &NodeAndStyleInfo<Node>, + ) { + let white_space_collapse = info.style.clone_white_space_collapse(); + let collapsed = WhitespaceCollapse::new( + text.chars(), + white_space_collapse, + self.last_inline_box_ended_with_collapsible_white_space, + ); + + // TODO: Not all text transforms are about case, this logic should stop ignoring + // TextTransform::FULL_WIDTH and TextTransform::FULL_SIZE_KANA. + let text_transform = info.style.clone_text_transform().case(); + let capitalized_text: String; + let char_iterator: Box<dyn Iterator<Item = char>> = match text_transform { + TextTransformCase::None => Box::new(collapsed), + TextTransformCase::Capitalize => { + // `TextTransformation` doesn't support capitalization, so we must capitalize the whole + // string at once and make a copy. Here `on_word_boundary` indicates whether or not the + // inline formatting context as a whole is on a word boundary. This is different from + // `last_inline_box_ended_with_collapsible_white_space` because the word boundaries are + // between atomic inlines and at the start of the IFC, and because preserved spaces + // are a word boundary. + let collapsed_string: String = collapsed.collect(); + capitalized_text = capitalize_string(&collapsed_string, self.on_word_boundary); + Box::new(capitalized_text.chars()) + }, + _ => { + // If `text-transform` is active, wrap the `WhitespaceCollapse` iterator in + // a `TextTransformation` iterator. + Box::new(TextTransformation::new(collapsed, text_transform)) + }, + }; + + let white_space_collapse = info.style.clone_white_space_collapse(); + let new_text: String = char_iterator + .inspect(|&character| { + self.has_uncollapsible_text_content |= matches!( + white_space_collapse, + WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces + ) || !character.is_ascii_whitespace() || + (character == '\n' && white_space_collapse != WhiteSpaceCollapse::Collapse); + }) + .collect(); + + if new_text.is_empty() { + return; + } + + let selection_range = info.get_selection_range(); + let selected_style = info.get_selected_style(); + + if let Some(last_character) = new_text.chars().next_back() { + self.on_word_boundary = last_character.is_whitespace(); + self.last_inline_box_ended_with_collapsible_white_space = + self.on_word_boundary && white_space_collapse != WhiteSpaceCollapse::Preserve; + } + + let new_range = self.current_text_offset..self.current_text_offset + new_text.len(); + self.current_text_offset = new_range.end; + self.text_segments.push(new_text); + + if let Some(inline_item) = self.inline_items.last() { + if let InlineItem::TextRun(text_run) = &mut *inline_item.borrow_mut() { + text_run.borrow_mut().text_range.end = new_range.end; + return; + } + } + + self.inline_items + .push(ArcRefCell::new(InlineItem::TextRun(ArcRefCell::new( + TextRun::new( + info.into(), + info.style.clone(), + new_range, + selection_range, + selected_style, + ), + )))); + } + + pub(crate) fn split_around_block_and_finish( + &mut self, + layout_context: &LayoutContext, + propagated_data: PropagatedBoxTreeData, + has_first_formatted_line: bool, + default_bidi_level: Level, + ) -> Option<InlineFormattingContext> { + if self.is_empty() { + return None; + } + + // Create a new inline builder which will be active after the block splits this inline formatting + // context. It has the same inline box structure as this builder, except the boxes are + // marked as not being the first fragment. No inline content is carried over to this new + // builder. + let mut new_builder = InlineFormattingContextBuilder::new(); + for identifier in self.inline_box_stack.iter() { + new_builder.start_inline_box( + self.inline_boxes + .get(identifier) + .borrow() + .split_around_block(), + ); + } + let mut inline_builder_from_before_split = std::mem::replace(self, new_builder); + + // End all ongoing inline boxes in the first builder, but ensure that they are not + // marked as the final fragments, so that they do not get inline end margin, borders, + // and padding. + while !inline_builder_from_before_split.inline_box_stack.is_empty() { + inline_builder_from_before_split.end_inline_box_internal(); + } + + inline_builder_from_before_split.finish( + layout_context, + propagated_data, + has_first_formatted_line, + /* is_single_line_text_input = */ false, + default_bidi_level, + ) + } + + /// Finish the current inline formatting context, returning [`None`] if the context was empty. + pub(crate) fn finish( + &mut self, + layout_context: &LayoutContext, + propagated_data: PropagatedBoxTreeData, + has_first_formatted_line: bool, + is_single_line_text_input: bool, + default_bidi_level: Level, + ) -> Option<InlineFormattingContext> { + if self.is_empty() { + return None; + } + + let old_builder = std::mem::replace(self, InlineFormattingContextBuilder::new()); + assert!(old_builder.inline_box_stack.is_empty()); + + Some(InlineFormattingContext::new_with_builder( + old_builder, + layout_context, + propagated_data, + has_first_formatted_line, + is_single_line_text_input, + default_bidi_level, + )) + } +} + +fn preserve_segment_break() -> bool { + true +} + +pub struct WhitespaceCollapse<InputIterator> { + char_iterator: InputIterator, + white_space_collapse: WhiteSpaceCollapse, + + /// Whether or not we should collapse white space completely at the start of the string. + /// This is true when the last character handled in our owning [`super::InlineFormattingContext`] + /// was collapsible white space. + remove_collapsible_white_space_at_start: bool, + + /// Whether or not the last character produced was newline. There is special behavior + /// we do after each newline. + following_newline: bool, + + /// Whether or not we have seen any non-white space characters, indicating that we are not + /// in a collapsible white space section at the beginning of the string. + have_seen_non_white_space_characters: bool, + + /// Whether the last character that we processed was a non-newline white space character. When + /// collapsing white space we need to wait until the next non-white space character or the end + /// of the string to push a single white space. + inside_white_space: bool, + + /// When we enter a collapsible white space region, we may need to wait to produce a single + /// white space character as soon as we encounter a non-white space character. When that + /// happens we queue up the non-white space character for the next iterator call. + character_pending_to_return: Option<char>, +} + +impl<InputIterator> WhitespaceCollapse<InputIterator> { + pub fn new( + char_iterator: InputIterator, + white_space_collapse: WhiteSpaceCollapse, + trim_beginning_white_space: bool, + ) -> Self { + Self { + char_iterator, + white_space_collapse, + remove_collapsible_white_space_at_start: trim_beginning_white_space, + inside_white_space: false, + following_newline: false, + have_seen_non_white_space_characters: false, + character_pending_to_return: None, + } + } + + fn is_leading_trimmed_white_space(&self) -> bool { + !self.have_seen_non_white_space_characters && self.remove_collapsible_white_space_at_start + } + + /// Whether or not we need to produce a space character if the next character is not a newline + /// and not white space. This happens when we are exiting a section of white space and we + /// waited to produce a single space character for the entire section of white space (but + /// not following or preceding a newline). + fn need_to_produce_space_character_after_white_space(&self) -> bool { + self.inside_white_space && !self.following_newline && !self.is_leading_trimmed_white_space() + } +} + +impl<InputIterator> Iterator for WhitespaceCollapse<InputIterator> +where + InputIterator: Iterator<Item = char>, +{ + type Item = char; + + fn next(&mut self) -> Option<Self::Item> { + // Point 4.1.1 first bullet: + // > If white-space is set to normal, nowrap, or pre-line, whitespace + // > characters are considered collapsible + // If whitespace is not considered collapsible, it is preserved entirely, which + // means that we can simply return the input string exactly. + if self.white_space_collapse == WhiteSpaceCollapse::Preserve || + self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces + { + // From <https://drafts.csswg.org/css-text-3/#white-space-processing>: + // > Carriage returns (U+000D) are treated identically to spaces (U+0020) in all respects. + // + // In the non-preserved case these are converted to space below. + return match self.char_iterator.next() { + Some('\r') => Some(' '), + next => next, + }; + } + + if let Some(character) = self.character_pending_to_return.take() { + self.inside_white_space = false; + self.have_seen_non_white_space_characters = true; + self.following_newline = false; + return Some(character); + } + + while let Some(character) = self.char_iterator.next() { + // Don't push non-newline whitespace immediately. Instead wait to push it until we + // know that it isn't followed by a newline. See `push_pending_whitespace_if_needed` + // above. + if character.is_ascii_whitespace() && character != '\n' { + self.inside_white_space = true; + continue; + } + + // Point 4.1.1: + // > 2. Collapsible segment breaks are transformed for rendering according to the + // > segment break transformation rules. + if character == '\n' { + // From <https://drafts.csswg.org/css-text-3/#line-break-transform> + // (4.1.3 -- the segment break transformation rules): + // + // > When white-space is pre, pre-wrap, or pre-line, segment breaks are not + // > collapsible and are instead transformed into a preserved line feed" + if self.white_space_collapse != WhiteSpaceCollapse::Collapse { + self.inside_white_space = false; + self.following_newline = true; + return Some(character); + + // Point 4.1.3: + // > 1. First, any collapsible segment break immediately following another + // > collapsible segment break is removed. + // > 2. Then any remaining segment break is either transformed into a space (U+0020) + // > or removed depending on the context before and after the break. + } else if !self.following_newline && + preserve_segment_break() && + !self.is_leading_trimmed_white_space() + { + self.inside_white_space = false; + self.following_newline = true; + return Some(' '); + } else { + self.following_newline = true; + continue; + } + } + + // Point 4.1.1: + // > 2. Any sequence of collapsible spaces and tabs immediately preceding or + // > following a segment break is removed. + // > 3. Every collapsible tab is converted to a collapsible space (U+0020). + // > 4. Any collapsible space immediately following another collapsible space—even + // > one outside the boundary of the inline containing that space, provided both + // > spaces are within the same inline formatting context—is collapsed to have zero + // > advance width. + if self.need_to_produce_space_character_after_white_space() { + self.inside_white_space = false; + self.character_pending_to_return = Some(character); + return Some(' '); + } + + self.inside_white_space = false; + self.have_seen_non_white_space_characters = true; + self.following_newline = false; + return Some(character); + } + + if self.need_to_produce_space_character_after_white_space() { + self.inside_white_space = false; + return Some(' '); + } + + None + } + + fn size_hint(&self) -> (usize, Option<usize>) { + self.char_iterator.size_hint() + } + + fn count(self) -> usize + where + Self: Sized, + { + self.char_iterator.count() + } +} + +enum PendingCaseConversionResult { + Uppercase(ToUppercase), + Lowercase(ToLowercase), +} + +impl PendingCaseConversionResult { + fn next(&mut self) -> Option<char> { + match self { + PendingCaseConversionResult::Uppercase(to_uppercase) => to_uppercase.next(), + PendingCaseConversionResult::Lowercase(to_lowercase) => to_lowercase.next(), + } + } +} + +/// This is an interator that consumes a char iterator and produces character transformed +/// by the given CSS `text-transform` value. It currently does not support +/// `text-transform: capitalize` because Unicode segmentation libraries do not support +/// streaming input one character at a time. +pub struct TextTransformation<InputIterator> { + /// The input character iterator. + char_iterator: InputIterator, + /// The `text-transform` value to use. + text_transform: TextTransformCase, + /// If an uppercasing or lowercasing produces more than one character, this + /// caches them so that they can be returned in subsequent iterator calls. + pending_case_conversion_result: Option<PendingCaseConversionResult>, +} + +impl<InputIterator> TextTransformation<InputIterator> { + pub fn new(char_iterator: InputIterator, text_transform: TextTransformCase) -> Self { + Self { + char_iterator, + text_transform, + pending_case_conversion_result: None, + } + } +} + +impl<InputIterator> Iterator for TextTransformation<InputIterator> +where + InputIterator: Iterator<Item = char>, +{ + type Item = char; + + fn next(&mut self) -> Option<Self::Item> { + if let Some(character) = self + .pending_case_conversion_result + .as_mut() + .and_then(|result| result.next()) + { + return Some(character); + } + self.pending_case_conversion_result = None; + + for character in self.char_iterator.by_ref() { + match self.text_transform { + TextTransformCase::None => return Some(character), + TextTransformCase::Uppercase => { + let mut pending_result = + PendingCaseConversionResult::Uppercase(character.to_uppercase()); + if let Some(character) = pending_result.next() { + self.pending_case_conversion_result = Some(pending_result); + return Some(character); + } + }, + TextTransformCase::Lowercase => { + let mut pending_result = + PendingCaseConversionResult::Lowercase(character.to_lowercase()); + if let Some(character) = pending_result.next() { + self.pending_case_conversion_result = Some(pending_result); + return Some(character); + } + }, + // `text-transform: capitalize` currently cannot work on a per-character basis, + // so must be handled outside of this iterator. + TextTransformCase::Capitalize => return Some(character), + } + } + None + } +} + +/// Given a string and whether the start of the string represents a word boundary, create a copy of +/// the string with letters after word boundaries capitalized. +fn capitalize_string(string: &str, allow_word_at_start: bool) -> String { + let mut output_string = String::new(); + output_string.reserve(string.len()); + + let word_segmenter = WordSegmenter::new_auto(); + let mut bounds = word_segmenter.segment_str(string).peekable(); + let mut byte_index = 0; + for character in string.chars() { + let current_byte_index = byte_index; + byte_index += character.len_utf8(); + + if let Some(next_index) = bounds.peek() { + if *next_index == current_byte_index { + bounds.next(); + + if current_byte_index != 0 || allow_word_at_start { + output_string.extend(character.to_uppercase()); + continue; + } + } + } + + output_string.push(character); + } + + output_string +} diff --git a/components/layout/flow/inline/inline_box.rs b/components/layout/flow/inline/inline_box.rs new file mode 100644 index 00000000000..97398d6e708 --- /dev/null +++ b/components/layout/flow/inline/inline_box.rs @@ -0,0 +1,257 @@ +/* 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 fonts::FontMetrics; +use malloc_size_of_derive::MallocSizeOf; + +use super::{InlineContainerState, InlineContainerStateFlags, inline_container_needs_strut}; +use crate::ContainingBlock; +use crate::cell::ArcRefCell; +use crate::context::LayoutContext; +use crate::dom::NodeExt; +use crate::dom_traversal::NodeAndStyleInfo; +use crate::fragment_tree::BaseFragmentInfo; +use crate::layout_box_base::LayoutBoxBase; +use crate::style_ext::{LayoutStyle, PaddingBorderMargin}; + +#[derive(Debug, MallocSizeOf)] +pub(crate) struct InlineBox { + pub base: LayoutBoxBase, + /// The identifier of this inline box in the containing [`super::InlineFormattingContext`]. + pub(super) identifier: InlineBoxIdentifier, + pub is_first_fragment: bool, + pub is_last_fragment: bool, + /// The index of the default font in the [`super::InlineFormattingContext`]'s font metrics store. + /// This is initialized during IFC shaping. + pub default_font_index: Option<usize>, +} + +impl InlineBox { + pub(crate) fn new<'dom, Node: NodeExt<'dom>>(info: &NodeAndStyleInfo<Node>) -> Self { + Self { + base: LayoutBoxBase::new(info.into(), info.style.clone()), + // This will be assigned later, when the box is actually added to the IFC. + identifier: InlineBoxIdentifier::default(), + is_first_fragment: true, + is_last_fragment: false, + default_font_index: None, + } + } + + pub(crate) fn split_around_block(&self) -> Self { + Self { + base: LayoutBoxBase::new(self.base.base_fragment_info, self.base.style.clone()), + is_first_fragment: false, + is_last_fragment: false, + ..*self + } + } + + #[inline] + pub(crate) fn layout_style(&self) -> LayoutStyle { + LayoutStyle::Default(&self.base.style) + } +} + +#[derive(Debug, Default, MallocSizeOf)] +pub(crate) struct InlineBoxes { + /// A collection of all inline boxes in a particular [`super::InlineFormattingContext`]. + inline_boxes: Vec<ArcRefCell<InlineBox>>, + + /// A list of tokens that represent the actual tree of inline boxes, while allowing + /// easy traversal forward and backwards through the tree. This structure is also + /// stored in the [`super::InlineFormattingContext::inline_items`], but this version is + /// faster to iterate. + inline_box_tree: Vec<InlineBoxTreePathToken>, +} + +impl InlineBoxes { + pub(super) fn len(&self) -> usize { + self.inline_boxes.len() + } + + pub(super) fn iter(&self) -> impl Iterator<Item = &ArcRefCell<InlineBox>> { + self.inline_boxes.iter() + } + + pub(super) fn get(&self, identifier: &InlineBoxIdentifier) -> ArcRefCell<InlineBox> { + self.inline_boxes[identifier.index_in_inline_boxes as usize].clone() + } + + pub(super) fn end_inline_box(&mut self, identifier: InlineBoxIdentifier) { + self.inline_box_tree + .push(InlineBoxTreePathToken::End(identifier)); + } + + pub(super) fn start_inline_box( + &mut self, + mut inline_box: InlineBox, + ) -> (InlineBoxIdentifier, ArcRefCell<InlineBox>) { + assert!(self.inline_boxes.len() <= u32::MAX as usize); + assert!(self.inline_box_tree.len() <= u32::MAX as usize); + + let index_in_inline_boxes = self.inline_boxes.len() as u32; + let index_of_start_in_tree = self.inline_box_tree.len() as u32; + + let identifier = InlineBoxIdentifier { + index_of_start_in_tree, + index_in_inline_boxes, + }; + inline_box.identifier = identifier; + let inline_box = ArcRefCell::new(inline_box); + + self.inline_boxes.push(inline_box.clone()); + self.inline_box_tree + .push(InlineBoxTreePathToken::Start(identifier)); + + (identifier, inline_box) + } + + pub(super) fn get_path( + &self, + from: Option<InlineBoxIdentifier>, + to: InlineBoxIdentifier, + ) -> IntoIter<InlineBoxTreePathToken> { + if from == Some(to) { + return Vec::new().into_iter(); + } + + let mut from_index = match from { + Some(InlineBoxIdentifier { + index_of_start_in_tree, + .. + }) => index_of_start_in_tree as usize, + None => 0, + }; + let mut to_index = to.index_of_start_in_tree as usize; + let is_reversed = to_index < from_index; + + // Do not include the first or final token, depending on direction. These can be equal + // if we are starting or going to the the root of the inline formatting context, in which + // case we don't want to adjust. + if to_index > from_index && from.is_some() { + from_index += 1; + } else if to_index < from_index { + to_index += 1; + } + + let mut path = Vec::with_capacity(from_index.abs_diff(to_index)); + let min = from_index.min(to_index); + let max = from_index.max(to_index); + + for token in &self.inline_box_tree[min..=max] { + // Skip useless recursion into inline boxes; we are looking for a direct path. + if Some(&token.reverse()) == path.last() { + path.pop(); + } else { + path.push(*token); + } + } + + if is_reversed { + path.reverse(); + for token in path.iter_mut() { + *token = token.reverse(); + } + } + + path.into_iter() + } +} + +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)] +pub(super) enum InlineBoxTreePathToken { + Start(InlineBoxIdentifier), + End(InlineBoxIdentifier), +} + +impl InlineBoxTreePathToken { + fn reverse(&self) -> Self { + match self { + Self::Start(index) => Self::End(*index), + Self::End(index) => Self::Start(*index), + } + } +} + +/// An identifier for a particular [`InlineBox`] to be used to fetch it from an [`InlineBoxes`] +/// store of inline boxes. +/// +/// [`u32`] is used for the index, in order to save space. The value refers to the token +/// in the start tree data structure which can be fetched to find the actual index of +/// of the [`InlineBox`] in [`InlineBoxes::inline_boxes`]. +#[derive(Clone, Copy, Debug, Default, Eq, Hash, MallocSizeOf, PartialEq)] +pub(crate) struct InlineBoxIdentifier { + pub index_of_start_in_tree: u32, + pub index_in_inline_boxes: u32, +} + +pub(super) struct InlineBoxContainerState { + /// The container state common to both [`InlineBox`] and the root of the + /// [`super::InlineFormattingContext`]. + pub base: InlineContainerState, + + /// The [`InlineBoxIdentifier`] of this inline container state. If this is the root + /// the identifier is [`None`]. + pub identifier: InlineBoxIdentifier, + + /// The [`BaseFragmentInfo`] of the [`InlineBox`] that this state tracks. + pub base_fragment_info: BaseFragmentInfo, + + /// The [`PaddingBorderMargin`] of the [`InlineBox`] that this state tracks. + pub pbm: PaddingBorderMargin, + + /// Whether this is the last fragment of this InlineBox. This may not be the case if + /// the InlineBox is split due to an block-in-inline-split and this is not the last of + /// that split. + pub is_last_fragment: bool, +} + +impl InlineBoxContainerState { + pub(super) fn new( + inline_box: &InlineBox, + containing_block: &ContainingBlock, + layout_context: &LayoutContext, + parent_container: &InlineContainerState, + is_last_fragment: bool, + font_metrics: Option<&FontMetrics>, + ) -> Self { + let style = inline_box.base.style.clone(); + let pbm = inline_box + .layout_style() + .padding_border_margin(containing_block); + + let mut flags = InlineContainerStateFlags::empty(); + if inline_container_needs_strut(&style, layout_context, Some(&pbm)) { + flags.insert(InlineContainerStateFlags::CREATE_STRUT); + } + + Self { + base: InlineContainerState::new( + style, + flags, + Some(parent_container), + parent_container.text_decoration_line, + font_metrics, + ), + identifier: inline_box.identifier, + base_fragment_info: inline_box.base.base_fragment_info, + pbm, + is_last_fragment, + } + } + + pub(super) fn calculate_space_above_baseline(&self) -> Au { + let (ascent, descent, line_gap) = ( + self.base.font_metrics.ascent, + self.base.font_metrics.descent, + self.base.font_metrics.line_gap, + ); + let leading = line_gap - (ascent + descent); + leading.scale_by(0.5) + ascent + } +} diff --git a/components/layout/flow/inline/line.rs b/components/layout/flow/inline/line.rs new file mode 100644 index 00000000000..c42f32c9242 --- /dev/null +++ b/components/layout/flow/inline/line.rs @@ -0,0 +1,911 @@ +/* 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 app_units::Au; +use bitflags::bitflags; +use fonts::{ByteIndex, FontMetrics, GlyphStore}; +use itertools::Either; +use range::Range; +use servo_arc::Arc; +use style::Zero; +use style::computed_values::position::T as Position; +use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; +use style::properties::ComputedValues; +use style::values::generics::box_::{GenericVerticalAlign, VerticalAlignKeyword}; +use style::values::generics::font::LineHeight; +use style::values::specified::align::AlignFlags; +use style::values::specified::box_::DisplayOutside; +use style::values::specified::text::TextDecorationLine; +use unicode_bidi::{BidiInfo, Level}; +use webrender_api::FontInstanceKey; + +use super::inline_box::{InlineBoxContainerState, InlineBoxIdentifier, InlineBoxTreePathToken}; +use super::{InlineFormattingContextLayout, LineBlockSizes}; +use crate::cell::ArcRefCell; +use crate::fragment_tree::{BaseFragmentInfo, BoxFragment, Fragment, TextFragment}; +use crate::geom::{LogicalRect, LogicalVec2, PhysicalRect, ToLogical}; +use crate::positioned::{ + AbsolutelyPositionedBox, PositioningContext, PositioningContextLength, relative_adjustement, +}; +use crate::{ContainingBlock, ContainingBlockSize}; + +pub(super) struct LineMetrics { + /// The block offset of the line start in the containing + /// [`crate::flow::InlineFormattingContext`]. + pub block_offset: Au, + + /// The block size of this line. + pub block_size: Au, + + /// The block offset of this line's baseline from [`Self::block_offset`]. + pub baseline_block_offset: Au, +} + +bitflags! { + struct LineLayoutInlineContainerFlags: u8 { + /// Whether or not any line items were processed for this inline box, this includes + /// any child inline boxes. + const HAD_ANY_LINE_ITEMS = 1 << 0; + /// Whether or not the starting inline border, padding, or margin of the inline box + /// was encountered. + const HAD_INLINE_START_PBM = 1 << 2; + /// Whether or not the ending inline border, padding, or margin of the inline box + /// was encountered. + const HAD_INLINE_END_PBM = 1 << 3; + /// Whether or not any floats were encountered while laying out this inline box. + const HAD_ANY_FLOATS = 1 << 4; + } +} + +/// The state used when laying out a collection of [`LineItem`]s into a line. This state is stored +/// per-inline container. For instance, when laying out the conents of a `<span>` a fresh +/// [`LineItemLayoutInlineContainerState`] is pushed onto [`LineItemLayout`]'s stack of states. +pub(super) struct LineItemLayoutInlineContainerState { + /// If this inline container is not the root inline container, the identifier of the [`super::InlineBox`] + /// that is currently being laid out. + pub identifier: Option<InlineBoxIdentifier>, + + /// The fragments and their logical rectangle relative within the current inline box (or + /// line). These logical rectangles will be converted into physical ones and the Fragment's + /// `content_rect` will be updated once the inline box's final size is known in + /// [`LineItemLayout::end_inline_box`]. + pub fragments: Vec<(Fragment, LogicalRect<Au>)>, + + /// The current inline advance of the layout in the coordinates of this inline box. + pub inline_advance: Au, + + /// Flags which track various features during layout. + flags: LineLayoutInlineContainerFlags, + + /// The offset of the parent, relative to the start position of the line, not including + /// any inline start and end borders which are only processed when the inline box is + /// finished. + pub parent_offset: LogicalVec2<Au>, + + /// The block offset of the parent's baseline relative to the block start of the line. This + /// is often the same as [`Self::parent_offset`], but can be different for the root + /// element. + pub baseline_offset: Au, + + /// If this inline box establishes a containing block for positioned elements, this + /// is a fresh positioning context to contain them. Otherwise, this holds the starting + /// offset in the *parent* positioning context so that static positions can be updated + /// at the end of layout. + pub positioning_context_or_start_offset_in_parent: + Either<PositioningContext, PositioningContextLength>, +} + +impl LineItemLayoutInlineContainerState { + fn new( + identifier: Option<InlineBoxIdentifier>, + parent_offset: LogicalVec2<Au>, + baseline_offset: Au, + positioning_context_or_start_offset_in_parent: Either< + PositioningContext, + PositioningContextLength, + >, + ) -> Self { + Self { + identifier, + fragments: Vec::new(), + inline_advance: Au::zero(), + flags: LineLayoutInlineContainerFlags::empty(), + parent_offset, + baseline_offset, + positioning_context_or_start_offset_in_parent, + } + } + + fn root(starting_inline_advance: Au, baseline_offset: Au) -> Self { + let mut state = Self::new( + None, + LogicalVec2::zero(), + baseline_offset, + Either::Right(PositioningContextLength::zero()), + ); + state.inline_advance = starting_inline_advance; + state + } +} + +/// The second phase of [`super::InlineFormattingContext`] layout: once items are gathered +/// for a line, we must lay them out and create fragments for them, properly positioning them +/// according to their baselines and also handling absolutely positioned children. +pub(super) struct LineItemLayout<'layout_data, 'layout> { + /// The state of the overall [`super::InlineFormattingContext`] layout. + layout: &'layout mut InlineFormattingContextLayout<'layout_data>, + + /// The set of [`LineItemLayoutInlineContainerState`] created while laying out items + /// on this line. This does not include the current level of recursion. + pub state_stack: Vec<LineItemLayoutInlineContainerState>, + + /// The current [`LineItemLayoutInlineContainerState`]. + pub current_state: LineItemLayoutInlineContainerState, + + /// The metrics of this line, which should remain constant throughout the + /// layout process. + pub line_metrics: LineMetrics, + + /// The amount of space to add to each justification opportunity in order to implement + /// `text-align: justify`. + pub justification_adjustment: Au, +} + +impl LineItemLayout<'_, '_> { + pub(super) fn layout_line_items( + layout: &mut InlineFormattingContextLayout, + line_items: Vec<LineItem>, + start_position: LogicalVec2<Au>, + effective_block_advance: &LineBlockSizes, + justification_adjustment: Au, + ) -> Vec<Fragment> { + let baseline_offset = effective_block_advance.find_baseline_offset(); + LineItemLayout { + layout, + state_stack: Vec::new(), + current_state: LineItemLayoutInlineContainerState::root( + start_position.inline, + baseline_offset, + ), + line_metrics: LineMetrics { + block_offset: start_position.block, + block_size: effective_block_advance.resolve(), + baseline_block_offset: baseline_offset, + }, + justification_adjustment, + } + .layout(line_items) + } + + /// Start and end inline boxes in tree order, so that it reflects the given inline box. + fn prepare_layout_for_inline_box(&mut self, new_inline_box: Option<InlineBoxIdentifier>) { + // Optimize the case where we are moving to the root of the inline box stack. + let Some(new_inline_box) = new_inline_box else { + while !self.state_stack.is_empty() { + self.end_inline_box(); + } + return; + }; + + // Otherwise, follow the path given to us by our collection of inline boxes, so we know which + // inline boxes to start and end. + let path = self + .layout + .ifc + .inline_boxes + .get_path(self.current_state.identifier, new_inline_box); + for token in path { + match token { + InlineBoxTreePathToken::Start(ref identifier) => self.start_inline_box(identifier), + InlineBoxTreePathToken::End(_) => self.end_inline_box(), + } + } + } + + pub(super) fn layout(&mut self, mut line_items: Vec<LineItem>) -> Vec<Fragment> { + let mut last_level = Level::ltr(); + let levels: Vec<_> = line_items + .iter() + .map(|item| { + let level = match item { + LineItem::TextRun(_, text_run) => text_run.bidi_level, + // TODO: This level needs either to be last_level, or if there were + // unicode characters inserted for the inline box, we need to get the + // level from them. + LineItem::InlineStartBoxPaddingBorderMargin(_) => last_level, + LineItem::InlineEndBoxPaddingBorderMargin(_) => last_level, + LineItem::Atomic(_, atomic) => atomic.bidi_level, + LineItem::AbsolutelyPositioned(..) => last_level, + LineItem::Float(..) => { + // At this point the float is already positioned, so it doesn't really matter what + // position it's fragment has in the order of line items. + last_level + }, + }; + last_level = level; + level + }) + .collect(); + + if self.layout.ifc.has_right_to_left_content { + sort_by_indices_in_place(&mut line_items, BidiInfo::reorder_visual(&levels)); + } + + // `BidiInfo::reorder_visual` will reorder the contents of the line so that they + // are in the correct order as if one was looking at the line from left-to-right. + // During this layout we do not lay out from left to right. Instead we lay out + // from inline-start to inline-end. If the overall line contents have been flipped + // for BiDi, flip them again so that they are in line start-to-end order rather + // than left-to-right order. + let line_item_iterator = if self + .layout + .containing_block + .style + .writing_mode + .is_bidi_ltr() + { + Either::Left(line_items.into_iter()) + } else { + Either::Right(line_items.into_iter().rev()) + }; + + for item in line_item_iterator.into_iter().by_ref() { + // When preparing to lay out a new line item, start and end inline boxes, so that the current + // inline box state reflects the item's parent. Items in the line are not necessarily in tree + // order due to BiDi and other reordering so the inline box of the item could potentially be + // any in the inline formatting context. + self.prepare_layout_for_inline_box(item.inline_box_identifier()); + + self.current_state + .flags + .insert(LineLayoutInlineContainerFlags::HAD_ANY_LINE_ITEMS); + match item { + LineItem::InlineStartBoxPaddingBorderMargin(_) => { + self.current_state + .flags + .insert(LineLayoutInlineContainerFlags::HAD_INLINE_START_PBM); + }, + LineItem::InlineEndBoxPaddingBorderMargin(_) => { + self.current_state + .flags + .insert(LineLayoutInlineContainerFlags::HAD_INLINE_END_PBM); + }, + LineItem::TextRun(_, text_run) => self.layout_text_run(text_run), + LineItem::Atomic(_, atomic) => self.layout_atomic(atomic), + LineItem::AbsolutelyPositioned(_, absolute) => self.layout_absolute(absolute), + LineItem::Float(_, float) => self.layout_float(float), + } + } + + // Move back to the root of the inline box tree, so that all boxes are ended. + self.prepare_layout_for_inline_box(None); + + let fragments_and_rectangles = std::mem::take(&mut self.current_state.fragments); + fragments_and_rectangles + .into_iter() + .map(|(mut fragment, logical_rect)| { + if matches!(fragment, Fragment::Float(_)) { + return fragment; + } + + // We do not know the actual physical position of a logically laid out inline element, until + // we know the width of the containing inline block. This step converts the logical rectangle + // into a physical one based on the inline formatting context width. + fragment.mutate_content_rect(|content_rect| { + *content_rect = logical_rect.as_physical(Some(self.layout.containing_block)) + }); + + fragment + }) + .collect() + } + + fn current_positioning_context_mut(&mut self) -> &mut PositioningContext { + if let Either::Left(ref mut positioning_context) = self + .current_state + .positioning_context_or_start_offset_in_parent + { + return positioning_context; + } + self.state_stack + .iter_mut() + .rev() + .find_map( + |state| match state.positioning_context_or_start_offset_in_parent { + Either::Left(ref mut positioning_context) => Some(positioning_context), + Either::Right(_) => None, + }, + ) + .unwrap_or(self.layout.positioning_context) + } + + fn start_inline_box(&mut self, identifier: &InlineBoxIdentifier) { + let inline_box_state = + &*self.layout.inline_box_states[identifier.index_in_inline_boxes as usize]; + let inline_box = self.layout.ifc.inline_boxes.get(identifier); + let inline_box = &*(inline_box.borrow()); + + let style = &inline_box.base.style; + let space_above_baseline = inline_box_state.calculate_space_above_baseline(); + let block_start_offset = + self.calculate_inline_box_block_start(inline_box_state, space_above_baseline); + + let positioning_context_or_start_offset_in_parent = + match PositioningContext::new_for_style(style) { + Some(positioning_context) => Either::Left(positioning_context), + None => Either::Right(self.current_positioning_context_mut().len()), + }; + + let parent_offset = LogicalVec2 { + inline: self.current_state.inline_advance + self.current_state.parent_offset.inline, + block: block_start_offset, + }; + + let outer_state = std::mem::replace( + &mut self.current_state, + LineItemLayoutInlineContainerState::new( + Some(*identifier), + parent_offset, + block_start_offset + space_above_baseline, + positioning_context_or_start_offset_in_parent, + ), + ); + + self.state_stack.push(outer_state); + } + + fn end_inline_box(&mut self) { + let outer_state = self.state_stack.pop().expect("Ended unknown inline box"); + let inner_state = std::mem::replace(&mut self.current_state, outer_state); + + let identifier = inner_state.identifier.expect("Ended unknown inline box"); + let inline_box_state = + &*self.layout.inline_box_states[identifier.index_in_inline_boxes as usize]; + let inline_box = self.layout.ifc.inline_boxes.get(&identifier); + let inline_box = &*(inline_box.borrow()); + + let mut padding = inline_box_state.pbm.padding; + let mut border = inline_box_state.pbm.border; + let mut margin = inline_box_state.pbm.margin.auto_is(Au::zero); + + let mut had_start = inner_state + .flags + .contains(LineLayoutInlineContainerFlags::HAD_INLINE_START_PBM); + let mut had_end = inner_state + .flags + .contains(LineLayoutInlineContainerFlags::HAD_INLINE_END_PBM); + + let containing_block_writing_mode = self.layout.containing_block.style.writing_mode; + if containing_block_writing_mode.is_bidi_ltr() != + inline_box.base.style.writing_mode.is_bidi_ltr() + { + std::mem::swap(&mut had_start, &mut had_end) + } + + if !had_start { + padding.inline_start = Au::zero(); + border.inline_start = Au::zero(); + margin.inline_start = Au::zero(); + } + if !had_end { + padding.inline_end = Au::zero(); + border.inline_end = Au::zero(); + margin.inline_end = Au::zero(); + } + // If the inline box didn't have any content at all and it isn't the first fragment for + // an element (needed for layout queries currently) and it didn't have any padding, border, + // or margin do not make a fragment for it. + // + // Note: This is an optimization, but also has side effects. Any fragments on a line will + // force the baseline to advance in the parent IFC. + let pbm_sums = padding + border + margin; + if inner_state.fragments.is_empty() && !had_start && pbm_sums.inline_sum().is_zero() { + return; + } + + // Make `content_rect` relative to the parent Fragment. + let mut content_rect = LogicalRect { + start_corner: LogicalVec2 { + inline: self.current_state.inline_advance + pbm_sums.inline_start, + block: inner_state.parent_offset.block - self.current_state.parent_offset.block, + }, + size: LogicalVec2 { + inline: inner_state.inline_advance, + block: inline_box_state.base.font_metrics.line_gap, + }, + }; + + // Relative adjustment should not affect the rest of line layout, so we can + // do it right before creating the Fragment. + let style = &inline_box.base.style; + if style.get_box().position == Position::Relative { + content_rect.start_corner += relative_adjustement(style, self.layout.containing_block); + } + + let ifc_writing_mode = self.layout.containing_block.style.writing_mode; + let inline_box_containing_block = ContainingBlock { + size: ContainingBlockSize { + inline: content_rect.size.inline, + block: Default::default(), + }, + style: self.layout.containing_block.style, + }; + let fragments = inner_state + .fragments + .into_iter() + .map(|(mut fragment, logical_rect)| { + let is_float = matches!(fragment, Fragment::Float(_)); + fragment.mutate_content_rect(|content_rect| { + if is_float { + content_rect.origin -= + pbm_sums.start_offset().to_physical_size(ifc_writing_mode); + } else { + // We do not know the actual physical position of a logically laid out inline element, until + // we know the width of the containing inline block. This step converts the logical rectangle + // into a physical one now that we've computed inline size of the containing inline block above. + *content_rect = logical_rect.as_physical(Some(&inline_box_containing_block)) + } + }); + fragment + }) + .collect(); + + // Previously all the fragment's children were positioned relative to the linebox, + // but they need to be made relative to this fragment. + let physical_content_rect = content_rect.as_physical(Some(self.layout.containing_block)); + let mut fragment = BoxFragment::new( + inline_box.base.base_fragment_info, + style.clone(), + fragments, + physical_content_rect, + padding.to_physical(ifc_writing_mode), + border.to_physical(ifc_writing_mode), + margin.to_physical(ifc_writing_mode), + None, /* clearance */ + ); + + let offset_from_parent_ifc = LogicalVec2 { + inline: pbm_sums.inline_start + self.current_state.inline_advance, + block: content_rect.start_corner.block, + } + .to_physical_vector(self.layout.containing_block.style.writing_mode); + + match inner_state.positioning_context_or_start_offset_in_parent { + Either::Left(mut positioning_context) => { + positioning_context + .layout_collected_children(self.layout.layout_context, &mut fragment); + positioning_context.adjust_static_position_of_hoisted_fragments_with_offset( + &offset_from_parent_ifc, + PositioningContextLength::zero(), + ); + self.current_positioning_context_mut() + .append(positioning_context); + }, + Either::Right(start_offset) => { + self.current_positioning_context_mut() + .adjust_static_position_of_hoisted_fragments_with_offset( + &offset_from_parent_ifc, + start_offset, + ); + }, + } + + self.current_state.inline_advance += inner_state.inline_advance + pbm_sums.inline_sum(); + + let fragment = Fragment::Box(ArcRefCell::new(fragment)); + inline_box.base.add_fragment(fragment.clone()); + + self.current_state.fragments.push((fragment, content_rect)); + } + + fn calculate_inline_box_block_start( + &self, + inline_box_state: &InlineBoxContainerState, + space_above_baseline: Au, + ) -> Au { + let font_metrics = &inline_box_state.base.font_metrics; + let style = &inline_box_state.base.style; + let line_gap = 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 inline_box_state.base.style.clone_vertical_align() { + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => { + let line_height: Au = line_height(style, font_metrics); + (line_height - line_gap).scale_by(0.5) + }, + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => { + let line_height: Au = line_height(style, font_metrics); + let half_leading = (line_height - line_gap).scale_by(0.5); + self.line_metrics.block_size - line_height + half_leading + }, + _ => { + self.line_metrics.baseline_block_offset + inline_box_state.base.baseline_offset - + space_above_baseline + }, + } + } + + fn layout_text_run(&mut self, text_item: TextRunLineItem) { + if text_item.text.is_empty() { + return; + } + + let mut number_of_justification_opportunities = 0; + let mut inline_advance = text_item + .text + .iter() + .map(|glyph_store| { + number_of_justification_opportunities += glyph_store.total_word_separators(); + glyph_store.total_advance() + }) + .sum(); + + if !self.justification_adjustment.is_zero() { + inline_advance += self + .justification_adjustment + .scale_by(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 relative alignment, it might be different. + let start_corner = LogicalVec2 { + inline: self.current_state.inline_advance, + block: self.current_state.baseline_offset - + text_item.font_metrics.ascent - + self.current_state.parent_offset.block, + }; + let content_rect = LogicalRect { + start_corner, + size: LogicalVec2 { + block: text_item.font_metrics.line_gap, + inline: inline_advance, + }, + }; + + self.current_state.inline_advance += inline_advance; + self.current_state.fragments.push(( + Fragment::Text(ArcRefCell::new(TextFragment { + base: text_item.base_fragment_info.into(), + parent_style: text_item.parent_style, + rect: PhysicalRect::zero(), + font_metrics: text_item.font_metrics, + font_key: text_item.font_key, + glyphs: text_item.text, + text_decoration_line: text_item.text_decoration_line, + justification_adjustment: self.justification_adjustment, + selection_range: text_item.selection_range, + selected_style: text_item.selected_style, + })), + content_rect, + )); + } + + fn layout_atomic(&mut self, atomic: AtomicLineItem) { + // 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. + // Make the final result relative to the parent box. + let ifc_writing_mode = self.layout.containing_block.style.writing_mode; + let content_rect = { + let block_start = atomic.calculate_block_start(&self.line_metrics); + let atomic_fragment = atomic.fragment.borrow_mut(); + let padding_border_margin_sides = atomic_fragment + .padding_border_margin() + .to_logical(ifc_writing_mode); + + let mut atomic_offset = LogicalVec2 { + inline: self.current_state.inline_advance + + padding_border_margin_sides.inline_start, + block: block_start - self.current_state.parent_offset.block + + padding_border_margin_sides.block_start, + }; + + if atomic_fragment.style.get_box().position == Position::Relative { + atomic_offset += + relative_adjustement(&atomic_fragment.style, self.layout.containing_block); + } + + // Reconstruct a logical rectangle relative to the inline box container that will be used + // after the inline box is procesed to find a final physical rectangle. + LogicalRect { + start_corner: atomic_offset, + size: atomic_fragment + .content_rect + .size + .to_logical(ifc_writing_mode), + } + }; + + if let Some(mut positioning_context) = atomic.positioning_context { + let physical_rect_as_if_in_root = + content_rect.as_physical(Some(self.layout.containing_block)); + positioning_context.adjust_static_position_of_hoisted_fragments_with_offset( + &physical_rect_as_if_in_root.origin.to_vector(), + PositioningContextLength::zero(), + ); + + self.current_positioning_context_mut() + .append(positioning_context); + } + + self.current_state.inline_advance += atomic.size.inline; + + self.current_state + .fragments + .push((Fragment::Box(atomic.fragment), content_rect)); + } + + fn layout_absolute(&mut self, absolute: AbsolutelyPositionedLineItem) { + let absolutely_positioned_box = (*absolute.absolutely_positioned_box).borrow(); + let style = absolutely_positioned_box.context.style(); + + // From https://drafts.csswg.org/css2/#abs-non-replaced-width + // > The static-position containing block is the containing block of a + // > hypothetical box that would have been the first box of the element if its + // > specified position value had been static and its specified float had been + // > none. (Note that due to the rules in section 9.7 this hypothetical + // > calculation might require also assuming a different computed value for + // > display.) + // + // This box is different based on the original `display` value of the + // absolutely positioned element. If it's `inline` it would be placed inline + // at the top of the line, but if it's block it would be placed in a new + // block position after the linebox established by this line. + let initial_start_corner = + if style.get_box().original_display.outside() == DisplayOutside::Inline { + // Top of the line at the current inline position. + LogicalVec2 { + inline: self.current_state.inline_advance, + block: -self.current_state.parent_offset.block, + } + } else { + // After the bottom of the line at the start of the inline formatting context. + LogicalVec2 { + inline: -self.current_state.parent_offset.inline, + block: self.line_metrics.block_size - self.current_state.parent_offset.block, + } + }; + + // Since alignment of absolutes in inlines is currently always `start`, the size of + // of the static position rectangle does not matter. + let static_position_rect = LogicalRect { + start_corner: initial_start_corner, + size: LogicalVec2::zero(), + } + .as_physical(Some(self.layout.containing_block)); + + let hoisted_box = AbsolutelyPositionedBox::to_hoisted( + absolute.absolutely_positioned_box.clone(), + static_position_rect, + LogicalVec2 { + inline: AlignFlags::START, + block: AlignFlags::START, + }, + self.layout.containing_block.style.writing_mode, + ); + + let hoisted_fragment = hoisted_box.fragment.clone(); + self.current_positioning_context_mut().push(hoisted_box); + self.current_state.fragments.push(( + Fragment::AbsoluteOrFixedPositioned(hoisted_fragment), + LogicalRect::zero(), + )); + } + + fn layout_float(&mut self, float: FloatLineItem) { + self.current_state + .flags + .insert(LineLayoutInlineContainerFlags::HAD_ANY_FLOATS); + + // 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. Float + // 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: self.current_state.parent_offset.inline, + block: self.line_metrics.block_offset + self.current_state.parent_offset.block, + }; + float.fragment.borrow_mut().content_rect.origin -= distance_from_parent_to_ifc + .to_physical_size(self.layout.containing_block.style.writing_mode); + + self.current_state + .fragments + .push((Fragment::Float(float.fragment), LogicalRect::zero())); + } +} + +pub(super) enum LineItem { + InlineStartBoxPaddingBorderMargin(InlineBoxIdentifier), + InlineEndBoxPaddingBorderMargin(InlineBoxIdentifier), + TextRun(Option<InlineBoxIdentifier>, TextRunLineItem), + Atomic(Option<InlineBoxIdentifier>, AtomicLineItem), + AbsolutelyPositioned(Option<InlineBoxIdentifier>, AbsolutelyPositionedLineItem), + Float(Option<InlineBoxIdentifier>, FloatLineItem), +} + +impl LineItem { + fn inline_box_identifier(&self) -> Option<InlineBoxIdentifier> { + match self { + LineItem::InlineStartBoxPaddingBorderMargin(identifier) => Some(*identifier), + LineItem::InlineEndBoxPaddingBorderMargin(identifier) => Some(*identifier), + LineItem::TextRun(identifier, _) => *identifier, + LineItem::Atomic(identifier, _) => *identifier, + LineItem::AbsolutelyPositioned(identifier, _) => *identifier, + LineItem::Float(identifier, _) => *identifier, + } + } + + pub(super) fn trim_whitespace_at_end(&mut self, whitespace_trimmed: &mut Au) -> bool { + match self { + LineItem::InlineStartBoxPaddingBorderMargin(_) => true, + LineItem::InlineEndBoxPaddingBorderMargin(_) => true, + LineItem::TextRun(_, item) => item.trim_whitespace_at_end(whitespace_trimmed), + LineItem::Atomic(..) => false, + LineItem::AbsolutelyPositioned(..) => true, + LineItem::Float(..) => true, + } + } + + pub(super) fn trim_whitespace_at_start(&mut self, whitespace_trimmed: &mut Au) -> bool { + match self { + LineItem::InlineStartBoxPaddingBorderMargin(_) => true, + LineItem::InlineEndBoxPaddingBorderMargin(_) => true, + LineItem::TextRun(_, item) => item.trim_whitespace_at_start(whitespace_trimmed), + LineItem::Atomic(..) => false, + LineItem::AbsolutelyPositioned(..) => true, + LineItem::Float(..) => true, + } + } +} + +pub(super) struct TextRunLineItem { + pub base_fragment_info: BaseFragmentInfo, + pub parent_style: Arc<ComputedValues>, + pub text: Vec<std::sync::Arc<GlyphStore>>, + pub font_metrics: FontMetrics, + pub font_key: FontInstanceKey, + pub text_decoration_line: TextDecorationLine, + /// The BiDi level of this [`TextRunLineItem`] to enable reordering. + pub bidi_level: Level, + pub selection_range: Option<Range<ByteIndex>>, + pub selected_style: Arc<ComputedValues>, +} + +impl TextRunLineItem { + fn trim_whitespace_at_end(&mut self, whitespace_trimmed: &mut Au) -> bool { + if matches!( + self.parent_style.get_inherited_text().white_space_collapse, + WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces + ) { + 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); + *whitespace_trimmed += self + .text + .drain(first_whitespace_index..) + .map(|glyph| 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 Au) -> bool { + if matches!( + self.parent_style.get_inherited_text().white_space_collapse, + WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces + ) { + return false; + } + + let index_of_first_non_whitespace = self + .text + .iter() + .position(|glyph| !glyph.is_whitespace()) + .unwrap_or(self.text.len()); + + *whitespace_trimmed += self + .text + .drain(0..index_of_first_non_whitespace) + .map(|glyph| glyph.total_advance()) + .sum(); + + // Only keep going if we only encountered whitespace. + self.text.is_empty() + } + + pub(crate) fn can_merge(&self, font_key: FontInstanceKey, bidi_level: Level) -> bool { + self.font_key == font_key && self.bidi_level == bidi_level + } +} + +pub(super) struct AtomicLineItem { + pub fragment: ArcRefCell<BoxFragment>, + pub size: LogicalVec2<Au>, + pub 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. + pub baseline_offset_in_parent: Au, + + /// The offset of the baseline inside this item. + pub baseline_offset_in_item: Au, + + /// The BiDi level of this [`AtomicLineItem`] to enable reordering. + pub bidi_level: Level, +} + +impl AtomicLineItem { + /// 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) -> Au { + match self.fragment.borrow().style.clone_vertical_align() { + GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => Au::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 + self.baseline_offset_in_parent; + baseline - self.baseline_offset_in_item + }, + } + } +} + +pub(super) struct AbsolutelyPositionedLineItem { + pub absolutely_positioned_box: ArcRefCell<AbsolutelyPositionedBox>, +} + +pub(super) struct FloatLineItem { + pub fragment: ArcRefCell<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, +} + +fn line_height(parent_style: &ComputedValues, font_metrics: &FontMetrics) -> Au { + let font = parent_style.get_font(); + let font_size = font.font_size.computed_size(); + match font.line_height { + LineHeight::Normal => font_metrics.line_gap, + LineHeight::Number(number) => (font_size * number.0).into(), + LineHeight::Length(length) => length.0.into(), + } +} + +/// Sort a mutable slice by the the given indices array in place, reording the slice so that final +/// value of `slice[x]` is `slice[indices[x]]`. +fn sort_by_indices_in_place<T>(data: &mut [T], mut indices: Vec<usize>) { + for idx in 0..data.len() { + if indices[idx] == idx { + continue; + } + + let mut current_idx = idx; + loop { + let target_idx = indices[current_idx]; + indices[current_idx] = current_idx; + if indices[target_idx] == target_idx { + break; + } + data.swap(current_idx, target_idx); + current_idx = target_idx; + } + } +} diff --git a/components/layout/flow/inline/line_breaker.rs b/components/layout/flow/inline/line_breaker.rs new file mode 100644 index 00000000000..28301fdadf8 --- /dev/null +++ b/components/layout/flow/inline/line_breaker.rs @@ -0,0 +1,120 @@ +/* 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::ops::Range; + +use icu_segmenter::LineSegmenter; + +pub(crate) struct LineBreaker { + linebreaks: Vec<usize>, + current_offset: usize, +} + +impl LineBreaker { + pub(crate) fn new(string: &str) -> Self { + let line_segmenter = LineSegmenter::new_auto(); + Self { + // From https://docs.rs/icu_segmenter/1.5.0/icu_segmenter/struct.LineSegmenter.html + // > For consistency with the grapheme, word, and sentence segmenters, there is always a + // > breakpoint returned at index 0, but this breakpoint is not a meaningful line break + // > opportunity. + // + // Skip this first line break opportunity, as it isn't interesting to us. + linebreaks: line_segmenter.segment_str(string).skip(1).collect(), + current_offset: 0, + } + } + + pub(crate) fn advance_to_linebreaks_in_range(&mut self, text_range: Range<usize>) -> &[usize] { + let linebreaks_in_range = self.linebreaks_in_range_after_current_offset(text_range); + self.current_offset = linebreaks_in_range.end; + &self.linebreaks[linebreaks_in_range] + } + + fn linebreaks_in_range_after_current_offset(&self, text_range: Range<usize>) -> Range<usize> { + assert!(text_range.start <= text_range.end); + + let mut linebreaks_range = self.current_offset..self.linebreaks.len(); + + while self.linebreaks[linebreaks_range.start] < text_range.start && + linebreaks_range.len() > 1 + { + linebreaks_range.start += 1; + } + + let mut ending_linebreak_index = linebreaks_range.start; + while self.linebreaks[ending_linebreak_index] < text_range.end && + ending_linebreak_index < self.linebreaks.len() - 1 + { + ending_linebreak_index += 1; + } + linebreaks_range.end = ending_linebreak_index; + linebreaks_range + } +} + +#[test] +fn test_linebreaker_ranges() { + let linebreaker = LineBreaker::new("abc def"); + assert_eq!(linebreaker.linebreaks, [4, 7]); + assert_eq!( + linebreaker.linebreaks_in_range_after_current_offset(0..5), + 0..1 + ); + // The last linebreak should not be included for the text range we are interested in. + assert_eq!( + linebreaker.linebreaks_in_range_after_current_offset(0..7), + 0..1 + ); + + let linebreaker = LineBreaker::new("abc d def"); + assert_eq!(linebreaker.linebreaks, [4, 6, 9]); + assert_eq!( + linebreaker.linebreaks_in_range_after_current_offset(0..5), + 0..1 + ); + assert_eq!( + linebreaker.linebreaks_in_range_after_current_offset(0..7), + 0..2 + ); + assert_eq!( + linebreaker.linebreaks_in_range_after_current_offset(0..9), + 0..2 + ); + + assert_eq!( + linebreaker.linebreaks_in_range_after_current_offset(4..9), + 0..2 + ); + + std::panic::catch_unwind(|| { + let linebreaker = LineBreaker::new("abc def"); + linebreaker.linebreaks_in_range_after_current_offset(5..2); + }) + .expect_err("Reversed range should cause an assertion failure."); +} + +#[test] +fn test_linebreaker_stateful_advance() { + let mut linebreaker = LineBreaker::new("abc d def"); + assert_eq!(linebreaker.linebreaks, [4, 6, 9]); + assert!(linebreaker.advance_to_linebreaks_in_range(0..7) == &[4, 6]); + assert!(linebreaker.advance_to_linebreaks_in_range(8..9).is_empty()); + + // We've already advanced, so a range from the beginning shouldn't affect things. + assert!(linebreaker.advance_to_linebreaks_in_range(0..9).is_empty()); + + linebreaker.current_offset = 0; + + // Sending a value out of range shoudn't break things. + assert!(linebreaker.advance_to_linebreaks_in_range(0..999) == &[4, 6]); + + linebreaker.current_offset = 0; + + std::panic::catch_unwind(|| { + let mut linebreaker = LineBreaker::new("abc d def"); + linebreaker.advance_to_linebreaks_in_range(2..0); + }) + .expect_err("Reversed range should cause an assertion failure."); +} diff --git a/components/layout/flow/inline/mod.rs b/components/layout/flow/inline/mod.rs new file mode 100644 index 00000000000..490917d95a3 --- /dev/null +++ b/components/layout/flow/inline/mod.rs @@ -0,0 +1,2525 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! # Inline Formatting Context Layout +//! +//! Inline layout is divided into three phases: +//! +//! 1. Box Tree Construction +//! 2. Box to Line Layout +//! 3. Line to Fragment Layout +//! +//! The first phase happens during normal box tree constrution, while the second two phases happen +//! during fragment tree construction (sometimes called just "layout"). +//! +//! ## Box Tree Construction +//! +//! During box tree construction, DOM elements are transformed into a box tree. This phase collects +//! all of the inline boxes, text, atomic inline elements (boxes with `display: inline-block` or +//! `display: inline-table` as well as things like images and canvas), absolutely positioned blocks, +//! and floated blocks. +//! +//! During the last part of this phase, whitespace is collapsed and text is segmented into +//! [`TextRun`]s based on script, chosen font, and line breaking opportunities. In addition, default +//! fonts are selected for every inline box. Each segment of text is shaped using HarfBuzz and +//! turned into a series of glyphs, which all have a size and a position relative to the origin of +//! the [`TextRun`] (calculated in later phases). +//! +//! The code for this phase is mainly in `construct.rs`, but text handling can also be found in +//! `text_runs.rs.` +//! +//! ## Box to Line Layout +//! +//! During the first phase of fragment tree construction, box tree items are laid out into +//! [`LineItem`]s and fragmented based on line boundaries. This is where line breaking happens. This +//! part of layout fragments boxes and their contents across multiple lines while positioning floats +//! and making sure non-floated contents flow around them. In addition, all atomic elements are laid +//! out, which may descend into their respective trees and create fragments. Finally, absolutely +//! positioned content is collected in order to later hoist it to the containing block for +//! absolutes. +//! +//! Note that during this phase, layout does not know the final block position of content. Only +//! during line to fragment layout, are the final block positions calculated based on the line's +//! final content and its vertical alignment. Instead, positions and line heights are calculated +//! relative to the line's final baseline which will be determined in the final phase. +//! +//! [`LineItem`]s represent a particular set of content on a line. Currently this is represented by +//! a linear series of items that describe the line's hierarchy of inline boxes and content. The +//! item types are: +//! +//! - [`LineItem::InlineStartBoxPaddingBorderMargin`] +//! - [`LineItem::InlineEndBoxPaddingBorderMargin`] +//! - [`LineItem::TextRun`] +//! - [`LineItem::Atomic`] +//! - [`LineItem::AbsolutelyPositioned`] +//! - [`LineItem::Float`] +//! +//! The code for this can be found by looking for methods of the form `layout_into_line_item()`. +//! +//! ## Line to Fragment Layout +//! +//! During the second phase of fragment tree construction, the final block position of [`LineItem`]s +//! is calculated and they are converted into [`Fragment`]s. After layout, the [`LineItem`]s are +//! discarded and the new fragments are incorporated into the fragment tree. The final static +//! position of absolutely positioned content is calculated and it is hoisted to its containing +//! block via [`PositioningContext`]. +//! +//! The code for this phase, can mainly be found in `line.rs`. +//! + +pub mod construct; +pub mod inline_box; +pub mod line; +mod line_breaker; +pub mod text_run; + +use std::cell::{OnceCell, RefCell}; +use std::mem; +use std::rc::Rc; + +use app_units::{Au, MAX_AU}; +use bitflags::bitflags; +use construct::InlineFormattingContextBuilder; +use fonts::{ByteIndex, FontMetrics, GlyphStore}; +use inline_box::{InlineBox, InlineBoxContainerState, InlineBoxIdentifier, InlineBoxes}; +use line::{ + AbsolutelyPositionedLineItem, AtomicLineItem, FloatLineItem, LineItem, LineItemLayout, + TextRunLineItem, +}; +use line_breaker::LineBreaker; +use malloc_size_of_derive::MallocSizeOf; +use range::Range; +use servo_arc::Arc; +use style::Zero; +use style::computed_values::text_wrap_mode::T as TextWrapMode; +use style::computed_values::vertical_align::T as VerticalAlign; +use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; +use style::context::QuirksMode; +use style::properties::ComputedValues; +use style::properties::style_structs::InheritedText; +use style::values::generics::box_::VerticalAlignKeyword; +use style::values::generics::font::LineHeight; +use style::values::specified::box_::BaselineSource; +use style::values::specified::text::{TextAlignKeyword, TextDecorationLine}; +use style::values::specified::{TextAlignLast, TextJustify}; +use text_run::{ + TextRun, XI_LINE_BREAKING_CLASS_GL, XI_LINE_BREAKING_CLASS_WJ, XI_LINE_BREAKING_CLASS_ZWJ, + add_or_get_font, get_font_for_first_font_for_style, +}; +use unicode_bidi::{BidiInfo, Level}; +use webrender_api::FontInstanceKey; +use xi_unicode::linebreak_property; + +use super::float::{Clear, PlacementAmongFloats}; +use super::{ + CacheableLayoutResult, IndependentFloatOrAtomicLayoutResult, + IndependentFormattingContextContents, +}; +use crate::cell::ArcRefCell; +use crate::context::LayoutContext; +use crate::flow::CollapsibleWithParentStartMargin; +use crate::flow::float::{FloatBox, SequentialLayoutState}; +use crate::formatting_contexts::{ + Baselines, IndependentFormattingContext, IndependentNonReplacedContents, +}; +use crate::fragment_tree::{ + BoxFragment, CollapsedBlockMargins, CollapsedMargin, Fragment, FragmentFlags, + PositioningFragment, +}; +use crate::geom::{LogicalRect, LogicalVec2, ToLogical}; +use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; +use crate::sizing::{ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult}; +use crate::style_ext::{ComputedValuesExt, PaddingBorderMargin}; +use crate::{ConstraintSpace, ContainingBlock, PropagatedBoxTreeData}; + +// From gfxFontConstants.h in Firefox. +static FONT_SUBSCRIPT_OFFSET_RATIO: f32 = 0.20; +static FONT_SUPERSCRIPT_OFFSET_RATIO: f32 = 0.34; + +#[derive(Debug, MallocSizeOf)] +pub(crate) struct InlineFormattingContext { + /// All [`InlineItem`]s in this [`InlineFormattingContext`] stored in a flat array. + /// [`InlineItem::StartInlineBox`] and [`InlineItem::EndInlineBox`] allow representing + /// the tree of inline boxes within the formatting context, but a flat array allows + /// easy iteration through all inline items. + pub(super) inline_items: Vec<ArcRefCell<InlineItem>>, + + /// The tree of inline boxes in this [`InlineFormattingContext`]. These are stored in + /// a flat array with each being given a [`InlineBoxIdentifier`]. + pub(super) inline_boxes: InlineBoxes, + + /// The text content of this inline formatting context. + pub(super) text_content: String, + + /// A store of font information for all the shaped segments in this formatting + /// context in order to avoid duplicating this information. + pub font_metrics: Vec<FontKeyAndMetrics>, + + pub(super) text_decoration_line: TextDecorationLine, + + /// Whether this IFC contains the 1st formatted line of an element: + /// <https://www.w3.org/TR/css-pseudo-4/#first-formatted-line>. + pub(super) has_first_formatted_line: bool, + + /// Whether or not this [`InlineFormattingContext`] contains floats. + pub(super) contains_floats: bool, + + /// Whether or not this is an [`InlineFormattingContext`] for a single line text input. + pub(super) is_single_line_text_input: bool, + + /// Whether or not this is an [`InlineFormattingContext`] has right-to-left content, which + /// will require reordering during layout. + pub(super) has_right_to_left_content: bool, +} + +/// A collection of data used to cache [`FontMetrics`] in the [`InlineFormattingContext`] +#[derive(Debug, MallocSizeOf)] +pub(crate) struct FontKeyAndMetrics { + pub key: FontInstanceKey, + pub pt_size: Au, + pub metrics: FontMetrics, +} + +#[derive(Debug, MallocSizeOf)] +pub(crate) enum InlineItem { + StartInlineBox(ArcRefCell<InlineBox>), + EndInlineBox, + TextRun(ArcRefCell<TextRun>), + OutOfFlowAbsolutelyPositionedBox( + ArcRefCell<AbsolutelyPositionedBox>, + usize, /* offset_in_text */ + ), + OutOfFlowFloatBox(#[conditional_malloc_size_of] Arc<FloatBox>), + Atomic( + #[conditional_malloc_size_of] Arc<IndependentFormattingContext>, + usize, /* offset_in_text */ + Level, /* bidi_level */ + ), +} + +impl InlineItem { + pub(crate) fn invalidate_cached_fragment(&self) { + match self { + InlineItem::StartInlineBox(inline_box) => { + inline_box.borrow().base.invalidate_cached_fragment() + }, + InlineItem::EndInlineBox | InlineItem::TextRun(..) => {}, + InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => { + positioned_box + .borrow() + .context + .base + .invalidate_cached_fragment(); + }, + InlineItem::OutOfFlowFloatBox(float_box) => { + float_box.contents.base.invalidate_cached_fragment() + }, + InlineItem::Atomic(independent_formatting_context, ..) => { + independent_formatting_context + .base + .invalidate_cached_fragment(); + }, + } + } + + pub(crate) fn fragments(&self) -> Vec<Fragment> { + match self { + InlineItem::StartInlineBox(inline_box) => inline_box.borrow().base.fragments(), + InlineItem::EndInlineBox | InlineItem::TextRun(..) => { + unreachable!("Should never have these kind of fragments attached to a DOM node") + }, + InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => { + positioned_box.borrow().context.base.fragments() + }, + InlineItem::OutOfFlowFloatBox(float_box) => float_box.contents.base.fragments(), + InlineItem::Atomic(independent_formatting_context, ..) => { + independent_formatting_context.base.fragments() + }, + } + } +} + +/// Information about the current line under construction for a particular +/// [`InlineFormattingContextLayout`]. This tracks position and size information while +/// [`LineItem`]s are collected and is used as input when those [`LineItem`]s are +/// converted into [`Fragment`]s during the final phase of line layout. Note that this +/// does not store the [`LineItem`]s themselves, as they are stored as part of the +/// nesting state in the [`InlineFormattingContextLayout`]. +struct LineUnderConstruction { + /// The position where this line will start once it is laid out. This includes any + /// offset from `text-indent`. + start_position: LogicalVec2<Au>, + + /// The current inline position in the line being laid out into [`LineItem`]s in this + /// [`InlineFormattingContext`] independent of the depth in the nesting level. + inline_position: Au, + + /// The maximum block size of all boxes that ended and are in progress in this line. + /// This uses [`LineBlockSizes`] instead of a simple value, because the final block size + /// depends on vertical alignment. + max_block_size: LineBlockSizes, + + /// Whether any active linebox has added a glyph or atomic element to this line, which + /// indicates that the next run that exceeds the line length can cause a line break. + has_content: bool, + + /// Whether or not there are floats that did not fit on the current line. Before + /// the [`LineItem`]s of this line are laid out, these floats will need to be + /// placed directly below this line, but still as children of this line's Fragments. + has_floats_waiting_to_be_placed: bool, + + /// A rectangular area (relative to the containing block / inline formatting + /// context boundaries) where we can fit the line box without overlapping floats. + /// Note that when this is not empty, its start corner takes precedence over + /// [`LineUnderConstruction::start_position`]. + placement_among_floats: OnceCell<LogicalRect<Au>>, + + /// The LineItems for the current line under construction that have already + /// been committed to this line. + line_items: Vec<LineItem>, +} + +impl LineUnderConstruction { + fn new(start_position: LogicalVec2<Au>) -> Self { + Self { + inline_position: start_position.inline, + start_position, + max_block_size: LineBlockSizes::zero(), + has_content: false, + has_floats_waiting_to_be_placed: false, + placement_among_floats: OnceCell::new(), + line_items: Vec::new(), + } + } + + fn line_block_start_considering_placement_among_floats(&self) -> Au { + match self.placement_among_floats.get() { + Some(placement_among_floats) => placement_among_floats.start_corner.block, + None => self.start_position.block, + } + } + + fn replace_placement_among_floats(&mut self, new_placement: LogicalRect<Au>) { + self.placement_among_floats.take(); + let _ = self.placement_among_floats.set(new_placement); + } + + /// Trim the trailing whitespace in this line and return the width of the whitespace trimmed. + fn trim_trailing_whitespace(&mut self) -> Au { + // From <https://www.w3.org/TR/css-text-3/#white-space-phase-2>: + // > 3. A sequence of collapsible spaces at the end of a line is removed, + // > as well as any trailing U+1680 OGHAM SPACE MARK whose white-space + // > property is normal, nowrap, or pre-line. + let mut whitespace_trimmed = Au::zero(); + for item in self.line_items.iter_mut().rev() { + if !item.trim_whitespace_at_end(&mut whitespace_trimmed) { + break; + } + } + + whitespace_trimmed + } + + /// Count the number of justification opportunities in this line. + fn count_justification_opportunities(&self) -> usize { + self.line_items + .iter() + .filter_map(|item| match item { + LineItem::TextRun(_, text_run) => Some( + text_run + .text + .iter() + .map(|glyph_store| glyph_store.total_word_separators()) + .sum::<usize>(), + ), + _ => None, + }) + .sum() + } +} + +/// A block size relative to a line's final baseline. This is to track the size +/// contribution of a particular element of a line above and below the baseline. +/// These sizes can be combined with other baseline relative sizes before the +/// final baseline position is known. The values here are relative to the +/// overall line's baseline and *not* the nested baseline of an inline box. +#[derive(Clone, Debug)] +struct BaselineRelativeSize { + /// The ascent above the baseline, where a positive value means a larger + /// ascent. Thus, the top of this size contribution is `baseline_offset - + /// ascent`. + ascent: Au, + + /// The descent below the baseline, where a positive value means a larger + /// descent. Thus, the bottom of this size contribution is `baseline_offset + + /// descent`. + descent: Au, +} + +impl BaselineRelativeSize { + fn zero() -> Self { + Self { + ascent: Au::zero(), + descent: Au::zero(), + } + } + + fn max(&self, other: &Self) -> Self { + BaselineRelativeSize { + ascent: self.ascent.max(other.ascent), + descent: self.descent.max(other.descent), + } + } + + /// Given an offset from the line's root baseline, adjust this [`BaselineRelativeSize`] + /// by that offset. This is used to adjust a [`BaselineRelativeSize`] for different kinds + /// of baseline-relative `vertical-align`. This will "move" measured size of a particular + /// inline box's block size. For example, in the following HTML: + /// + /// ```html + /// <div> + /// <span style="vertical-align: 5px">child content</span> + /// </div> + /// ```` + /// + /// If this [`BaselineRelativeSize`] is for the `<span>` then the adjustment + /// passed here would be equivalent to -5px. + fn adjust_for_nested_baseline_offset(&mut self, baseline_offset: Au) { + self.ascent -= baseline_offset; + self.descent += baseline_offset; + } +} + +#[derive(Clone, Debug)] +struct LineBlockSizes { + line_height: Au, + baseline_relative_size_for_line_height: Option<BaselineRelativeSize>, + size_for_baseline_positioning: BaselineRelativeSize, +} + +impl LineBlockSizes { + fn zero() -> Self { + LineBlockSizes { + line_height: Au::zero(), + baseline_relative_size_for_line_height: None, + size_for_baseline_positioning: BaselineRelativeSize::zero(), + } + } + + fn resolve(&self) -> Au { + let height_from_ascent_and_descent = self + .baseline_relative_size_for_line_height + .as_ref() + .map(|size| (size.ascent + size.descent).abs()) + .unwrap_or_else(Au::zero); + self.line_height.max(height_from_ascent_and_descent) + } + + fn max(&self, other: &LineBlockSizes) -> LineBlockSizes { + let baseline_relative_size = match ( + self.baseline_relative_size_for_line_height.as_ref(), + other.baseline_relative_size_for_line_height.as_ref(), + ) { + (Some(our_size), Some(other_size)) => Some(our_size.max(other_size)), + (our_size, other_size) => our_size.or(other_size).cloned(), + }; + Self { + line_height: self.line_height.max(other.line_height), + baseline_relative_size_for_line_height: baseline_relative_size, + size_for_baseline_positioning: self + .size_for_baseline_positioning + .max(&other.size_for_baseline_positioning), + } + } + + fn max_assign(&mut self, other: &LineBlockSizes) { + *self = self.max(other); + } + + fn adjust_for_baseline_offset(&mut self, baseline_offset: Au) { + if let Some(size) = self.baseline_relative_size_for_line_height.as_mut() { + size.adjust_for_nested_baseline_offset(baseline_offset) + } + self.size_for_baseline_positioning + .adjust_for_nested_baseline_offset(baseline_offset); + } + + /// From <https://drafts.csswg.org/css2/visudet.html#line-height>: + /// > The inline-level boxes are aligned vertically according to their 'vertical-align' + /// > property. In case they are aligned 'top' or 'bottom', they must be aligned so as + /// > to minimize the line box height. If such boxes are tall enough, there are multiple + /// > solutions and CSS 2 does not define the position of the line box's baseline (i.e., + /// > the position of the strut, see below). + fn find_baseline_offset(&self) -> Au { + match self.baseline_relative_size_for_line_height.as_ref() { + Some(size) => size.ascent, + None => { + // This is the case mentinoned above where there are multiple solutions. + // This code is putting the baseline roughly in the middle of the line. + let leading = self.resolve() - + (self.size_for_baseline_positioning.ascent + + self.size_for_baseline_positioning.descent); + leading.scale_by(0.5) + self.size_for_baseline_positioning.ascent + }, + } + } +} + +/// The current unbreakable segment under construction for an inline formatting context. +/// Items accumulate here until we reach a soft line break opportunity during processing +/// of inline content or we reach the end of the formatting context. +struct UnbreakableSegmentUnderConstruction { + /// The size of this unbreakable segment in both dimension. + inline_size: Au, + + /// The maximum block size that this segment has. This uses [`LineBlockSizes`] instead of a + /// simple value, because the final block size depends on vertical alignment. + max_block_size: LineBlockSizes, + + /// The LineItems for the segment under construction + line_items: Vec<LineItem>, + + /// The depth in the inline box hierarchy at the start of this segment. This is used + /// to prefix this segment when it is pushed to a new line. + inline_box_hierarchy_depth: Option<usize>, + + /// Whether any active linebox has added a glyph or atomic element to this line + /// segment, which indicates that the next run that exceeds the line length can cause + /// a line break. + has_content: bool, + + /// The inline size of any trailing whitespace in this segment. + trailing_whitespace_size: Au, +} + +impl UnbreakableSegmentUnderConstruction { + fn new() -> Self { + Self { + inline_size: Au::zero(), + max_block_size: LineBlockSizes { + line_height: Au::zero(), + baseline_relative_size_for_line_height: None, + size_for_baseline_positioning: BaselineRelativeSize::zero(), + }, + line_items: Vec::new(), + inline_box_hierarchy_depth: None, + has_content: false, + trailing_whitespace_size: Au::zero(), + } + } + + /// Reset this segment after its contents have been committed to a line. + fn reset(&mut self) { + assert!(self.line_items.is_empty()); // Preserve allocated memory. + self.inline_size = Au::zero(); + self.max_block_size = LineBlockSizes::zero(); + self.inline_box_hierarchy_depth = None; + self.has_content = false; + self.trailing_whitespace_size = Au::zero(); + } + + /// Push a single line item to this segment. In addition, record the inline box + /// hierarchy depth if this is the first segment. The hierarchy depth is used to + /// duplicate the necessary `StartInlineBox` tokens if this segment is ultimately + /// placed on a new empty line. + fn push_line_item(&mut self, line_item: LineItem, inline_box_hierarchy_depth: usize) { + if self.line_items.is_empty() { + self.inline_box_hierarchy_depth = Some(inline_box_hierarchy_depth); + } + self.line_items.push(line_item); + } + + /// Trim whitespace from the beginning of this UnbreakbleSegmentUnderConstruction. + /// + /// From <https://www.w3.org/TR/css-text-3/#white-space-phase-2>: + /// + /// > Then, the entire block is rendered. Inlines are laid out, taking bidi + /// > reordering into account, and wrapping as specified by the text-wrap + /// > property. As each line is laid out, + /// > 1. A sequence of collapsible spaces at the beginning of a line is removed. + /// + /// This prevents whitespace from being added to the beginning of a line. + fn trim_leading_whitespace(&mut self) { + let mut whitespace_trimmed = Au::zero(); + for item in self.line_items.iter_mut() { + if !item.trim_whitespace_at_start(&mut whitespace_trimmed) { + break; + } + } + self.inline_size -= whitespace_trimmed; + } +} + +bitflags! { + pub struct InlineContainerStateFlags: u8 { + const CREATE_STRUT = 0b0001; + const IS_SINGLE_LINE_TEXT_INPUT = 0b0010; + } +} + +pub(super) struct InlineContainerState { + /// The style of this inline container. + style: Arc<ComputedValues>, + + /// Flags which describe details of this [`InlineContainerState`]. + flags: InlineContainerStateFlags, + + /// Whether or not we have processed any content (an atomic element or text) for + /// this inline box on the current line OR any previous line. + has_content: RefCell<bool>, + + /// Indicates whether this nesting level have text decorations in effect. + /// From <https://drafts.csswg.org/css-text-decor/#line-decoration> + // "When specified on or propagated to a block container that establishes + // an IFC..." + text_decoration_line: TextDecorationLine, + + /// The block size contribution of this container's default font ie the size of the + /// "strut." Whether this is integrated into the [`Self::nested_strut_block_sizes`] + /// depends on the line-height quirk described in + /// <https://quirks.spec.whatwg.org/#the-line-height-calculation-quirk>. + strut_block_sizes: LineBlockSizes, + + /// The strut block size of this inline container maxed with the strut block + /// sizes of all inline container ancestors. In quirks mode, this will be + /// zero, until we know that an element has inline content. + nested_strut_block_sizes: LineBlockSizes, + + /// The baseline offset of this container from the baseline of the line. The is the + /// cumulative offset of this container and all of its parents. In contrast to the + /// `vertical-align` property a positive value indicates an offset "below" the + /// baseline while a negative value indicates one "above" it (when the block direction + /// is vertical). + pub baseline_offset: Au, + + /// The font metrics of the non-fallback font for this container. + font_metrics: FontMetrics, +} + +pub(super) struct InlineFormattingContextLayout<'layout_data> { + positioning_context: &'layout_data mut PositioningContext, + containing_block: &'layout_data ContainingBlock<'layout_data>, + sequential_layout_state: Option<&'layout_data mut SequentialLayoutState>, + layout_context: &'layout_data LayoutContext<'layout_data>, + + /// The [`InlineFormattingContext`] that we are laying out. + ifc: &'layout_data InlineFormattingContext, + + /// The [`InlineContainerState`] for the container formed by the root of the + /// [`InlineFormattingContext`]. This is effectively the "root inline box" described + /// by <https://drafts.csswg.org/css-inline/#model>: + /// + /// > The block container also generates a root inline box, which is an anonymous + /// > inline box that holds all of its inline-level contents. (Thus, all text in an + /// > inline formatting context is directly contained by an inline box, whether the root + /// > inline box or one of its descendants.) The root inline box inherits from its + /// > parent block container, but is otherwise unstyleable. + root_nesting_level: InlineContainerState, + + /// A stack of [`InlineBoxContainerState`] that is used to produce [`LineItem`]s either when we + /// reach the end of an inline box or when we reach the end of a line. Only at the end + /// of the inline box is the state popped from the stack. + inline_box_state_stack: Vec<Rc<InlineBoxContainerState>>, + + /// A collection of [`InlineBoxContainerState`] of all the inlines that are present + /// in this inline formatting context. We keep this as well as the stack, so that we + /// can access them during line layout, which may happen after relevant [`InlineBoxContainerState`]s + /// have been popped of the the stack. + inline_box_states: Vec<Rc<InlineBoxContainerState>>, + + /// A vector of fragment that are laid out. This includes one [`Fragment::Positioning`] + /// per line that is currently laid out plus fragments for all floats, which + /// are currently laid out at the top-level of each [`InlineFormattingContext`]. + fragments: Vec<Fragment>, + + /// Information about the line currently being laid out into [`LineItem`]s. + current_line: LineUnderConstruction, + + /// Information about the unbreakable line segment currently being laid out into [`LineItem`]s. + current_line_segment: UnbreakableSegmentUnderConstruction, + + /// After a forced line break (for instance from a `<br>` element) we wait to actually + /// break the line until seeing more content. This allows ongoing inline boxes to finish, + /// since in the case where they have no more content they should not be on the next + /// line. + /// + /// For instance: + /// + /// ``` html + /// <span style="border-right: 30px solid blue;"> + /// first line<br> + /// </span> + /// second line + /// ``` + /// + /// In this case, the `<span>` should not extend to the second line. If we linebreak + /// as soon as we encounter the `<br>` the `<span>`'s ending inline borders would be + /// placed on the second line, because we add those borders in + /// [`InlineFormattingContextLayout::finish_inline_box()`]. + linebreak_before_new_content: bool, + + /// When a `<br>` element has `clear`, this needs to be applied after the linebreak, + /// which will be processed *after* the `<br>` element is processed. This member + /// stores any deferred `clear` to apply after a linebreak. + deferred_br_clear: Clear, + + /// Whether or not a soft wrap opportunity is queued. Soft wrap opportunities are + /// queued after replaced content and they are processed when the next text content + /// is encountered. + pub have_deferred_soft_wrap_opportunity: bool, + + /// Whether or not this InlineFormattingContext has processed any in flow content at all. + had_inflow_content: bool, + + /// Whether or not the layout of this InlineFormattingContext depends on the block size + /// of its container for the purposes of flexbox layout. + depends_on_block_constraints: bool, + + /// The currently white-space-collapse setting of this line. This is stored on the + /// [`InlineFormattingContextLayout`] because when a soft wrap opportunity is defined + /// by the boundary between two characters, the white-space-collapse property of their + /// nearest common ancestor is used. + white_space_collapse: WhiteSpaceCollapse, + + /// The currently text-wrap-mode setting of this line. This is stored on the + /// [`InlineFormattingContextLayout`] because when a soft wrap opportunity is defined + /// by the boundary between two characters, the text-wrap-mode property of their nearest + /// common ancestor is used. + text_wrap_mode: TextWrapMode, + + /// The offset of the first and last baselines in the inline formatting context that we + /// are laying out. This is used to propagate baselines to the ancestors of + /// `display: inline-block` elements and table content. + baselines: Baselines, +} + +impl InlineFormattingContextLayout<'_> { + fn current_inline_container_state(&self) -> &InlineContainerState { + match self.inline_box_state_stack.last() { + Some(inline_box_state) => &inline_box_state.base, + None => &self.root_nesting_level, + } + } + + fn current_inline_box_identifier(&self) -> Option<InlineBoxIdentifier> { + self.inline_box_state_stack + .last() + .map(|state| state.identifier) + } + + fn current_line_max_block_size_including_nested_containers(&self) -> LineBlockSizes { + self.current_inline_container_state() + .nested_strut_block_sizes + .max(&self.current_line.max_block_size) + } + + fn propagate_current_nesting_level_white_space_style(&mut self) { + let style = match self.inline_box_state_stack.last() { + Some(inline_box_state) => &inline_box_state.base.style, + None => self.containing_block.style, + }; + let style_text = style.get_inherited_text(); + self.white_space_collapse = style_text.white_space_collapse; + self.text_wrap_mode = style_text.text_wrap_mode; + } + + fn processing_br_element(&self) -> bool { + self.inline_box_state_stack + .last() + .map(|state| { + state + .base_fragment_info + .flags + .contains(FragmentFlags::IS_BR_ELEMENT) + }) + .unwrap_or(false) + } + + /// Start laying out a particular [`InlineBox`] into line items. This will push + /// a new [`InlineBoxContainerState`] onto [`Self::inline_box_state_stack`]. + fn start_inline_box(&mut self, inline_box: &InlineBox) { + let inline_box_state = InlineBoxContainerState::new( + inline_box, + self.containing_block, + self.layout_context, + self.current_inline_container_state(), + inline_box.is_last_fragment, + inline_box + .default_font_index + .map(|index| &self.ifc.font_metrics[index].metrics), + ); + + self.depends_on_block_constraints |= inline_box + .base + .style + .depends_on_block_constraints_due_to_relative_positioning( + self.containing_block.style.writing_mode, + ); + + // If we are starting a `<br>` element prepare to clear after its deferred linebreak has been + // processed. Note that a `<br>` is composed of the element itself and the inner pseudo-element + // with the actual linebreak. Both will have this `FragmentFlag`; that's why this code only + // sets `deferred_br_clear` if it isn't set yet. + if inline_box_state + .base_fragment_info + .flags + .contains(FragmentFlags::IS_BR_ELEMENT) && + self.deferred_br_clear == Clear::None + { + self.deferred_br_clear = Clear::from_style_and_container_writing_mode( + &inline_box_state.base.style, + self.containing_block.style.writing_mode, + ); + } + + if inline_box.is_first_fragment { + self.current_line_segment.inline_size += inline_box_state.pbm.padding.inline_start + + inline_box_state.pbm.border.inline_start + + inline_box_state.pbm.margin.inline_start.auto_is(Au::zero); + self.current_line_segment + .line_items + .push(LineItem::InlineStartBoxPaddingBorderMargin( + inline_box.identifier, + )); + } + + let inline_box_state = Rc::new(inline_box_state); + + // Push the state onto the IFC-wide collection of states. Inline boxes are numbered in + // the order that they are encountered, so this should correspond to the order they + // are pushed onto `self.inline_box_states`. + assert_eq!( + self.inline_box_states.len(), + inline_box.identifier.index_in_inline_boxes as usize + ); + self.inline_box_states.push(inline_box_state.clone()); + self.inline_box_state_stack.push(inline_box_state); + } + + /// Finish laying out a particular [`InlineBox`] into line items. This will + /// pop its state off of [`Self::inline_box_state_stack`]. + fn finish_inline_box(&mut self) { + let inline_box_state = match self.inline_box_state_stack.pop() { + Some(inline_box_state) => inline_box_state, + None => return, // We are at the root. + }; + + self.current_line_segment + .max_block_size + .max_assign(&inline_box_state.base.nested_strut_block_sizes); + + // If the inline box that we just finished had any content at all, we want to propagate + // the `white-space` property of its parent to future inline children. This is because + // when a soft wrap opportunity is defined by the boundary between two elements, the + // `white-space` used is that of their nearest common ancestor. + if *inline_box_state.base.has_content.borrow() { + self.propagate_current_nesting_level_white_space_style(); + } + + if inline_box_state.is_last_fragment { + let pbm_end = inline_box_state.pbm.padding.inline_end + + inline_box_state.pbm.border.inline_end + + inline_box_state.pbm.margin.inline_end.auto_is(Au::zero); + self.current_line_segment.inline_size += pbm_end; + self.current_line_segment + .line_items + .push(LineItem::InlineEndBoxPaddingBorderMargin( + inline_box_state.identifier, + )) + } + } + + fn finish_last_line(&mut self) { + // We are at the end of the IFC, and we need to do a few things to make sure that + // the current segment is committed and that the final line is finished. + // + // A soft wrap opportunity makes it so the current segment is placed on a new line + // if it doesn't fit on the current line under construction. + self.process_soft_wrap_opportunity(); + + // `process_soft_line_wrap_opportunity` does not commit the segment to a line if + // there is no line wrapping, so this forces the segment into the current line. + self.commit_current_segment_to_line(); + + // Finally we finish the line itself and convert all of the LineItems into + // fragments. + self.finish_current_line_and_reset(true /* last_line_or_forced_line_break */); + } + + /// Finish layout of all inline boxes for the current line. This will gather all + /// [`LineItem`]s and turn them into [`Fragment`]s, then reset the + /// [`InlineFormattingContextLayout`] preparing it for laying out a new line. + fn finish_current_line_and_reset(&mut self, last_line_or_forced_line_break: bool) { + let whitespace_trimmed = self.current_line.trim_trailing_whitespace(); + let (inline_start_position, justification_adjustment) = self + .calculate_current_line_inline_start_and_justification_adjustment( + whitespace_trimmed, + last_line_or_forced_line_break, + ); + + let block_start_position = self + .current_line + .line_block_start_considering_placement_among_floats(); + let had_inline_advance = + self.current_line.inline_position != self.current_line.start_position.inline; + + let effective_block_advance = if self.current_line.has_content || + had_inline_advance || + self.linebreak_before_new_content + { + self.current_line_max_block_size_including_nested_containers() + } else { + LineBlockSizes::zero() + }; + + let resolved_block_advance = effective_block_advance.resolve(); + let mut block_end_position = block_start_position + resolved_block_advance; + if let Some(sequential_layout_state) = self.sequential_layout_state.as_mut() { + // This amount includes both the block size of the line and any extra space + // added to move the line down in order to avoid overlapping floats. + let increment = block_end_position - self.current_line.start_position.block; + sequential_layout_state.advance_block_position(increment); + + // This newline may have been triggered by a `<br>` with clearance, in which case we + // want to make sure that we make space not only for the current line, but any clearance + // from floats. + if let Some(clearance) = sequential_layout_state + .calculate_clearance(self.deferred_br_clear, &CollapsedMargin::zero()) + { + sequential_layout_state.advance_block_position(clearance); + block_end_position += clearance; + }; + self.deferred_br_clear = Clear::None; + } + + // Set up the new line now that we no longer need the old one. + let mut line_to_layout = std::mem::replace( + &mut self.current_line, + LineUnderConstruction::new(LogicalVec2 { + inline: Au::zero(), + block: block_end_position, + }), + ); + + if line_to_layout.has_floats_waiting_to_be_placed { + place_pending_floats(self, &mut line_to_layout.line_items); + } + + let start_position = LogicalVec2 { + block: block_start_position, + inline: inline_start_position, + }; + + let baseline_offset = effective_block_advance.find_baseline_offset(); + let start_positioning_context_length = self.positioning_context.len(); + let fragments = LineItemLayout::layout_line_items( + self, + line_to_layout.line_items, + start_position, + &effective_block_advance, + justification_adjustment, + ); + + // If the line doesn't have any fragments, we don't need to add a containing fragment for it. + if fragments.is_empty() && + self.positioning_context.len() == start_positioning_context_length + { + return; + } + + let baseline = baseline_offset + block_start_position; + self.baselines.first.get_or_insert(baseline); + self.baselines.last = Some(baseline); + + // The inline part of this start offset was taken into account when determining + // the inline start of the line in `calculate_inline_start_for_current_line` so + // we do not need to include it in the `start_corner` of the line's main Fragment. + let start_corner = LogicalVec2 { + inline: Au::zero(), + block: block_start_position, + }; + + let logical_origin_in_physical_coordinates = + start_corner.to_physical_vector(self.containing_block.style.writing_mode); + self.positioning_context + .adjust_static_position_of_hoisted_fragments_with_offset( + &logical_origin_in_physical_coordinates, + start_positioning_context_length, + ); + + let physical_line_rect = LogicalRect { + start_corner, + size: LogicalVec2 { + inline: self.containing_block.size.inline, + block: effective_block_advance.resolve(), + }, + } + .as_physical(Some(self.containing_block)); + self.fragments + .push(Fragment::Positioning(PositioningFragment::new_anonymous( + physical_line_rect, + fragments, + ))); + } + + /// Given the amount of whitespace trimmed from the line and taking into consideration + /// the `text-align` property, calculate where the line under construction starts in + /// the inline axis as well as the adjustment needed for every justification opportunity + /// to account for `text-align: justify`. + fn calculate_current_line_inline_start_and_justification_adjustment( + &self, + whitespace_trimmed: Au, + last_line_or_forced_line_break: bool, + ) -> (Au, Au) { + enum TextAlign { + Start, + Center, + End, + } + let style = self.containing_block.style; + let mut text_align_keyword = style.clone_text_align(); + + if last_line_or_forced_line_break { + text_align_keyword = match style.clone_text_align_last() { + TextAlignLast::Auto if text_align_keyword == TextAlignKeyword::Justify => { + TextAlignKeyword::Start + }, + TextAlignLast::Auto => text_align_keyword, + TextAlignLast::Start => TextAlignKeyword::Start, + TextAlignLast::End => TextAlignKeyword::End, + TextAlignLast::Left => TextAlignKeyword::Left, + TextAlignLast::Right => TextAlignKeyword::Right, + TextAlignLast::Center => TextAlignKeyword::Center, + TextAlignLast::Justify => TextAlignKeyword::Justify, + }; + } + + let text_align = match text_align_keyword { + TextAlignKeyword::Start => TextAlign::Start, + TextAlignKeyword::Center | TextAlignKeyword::MozCenter => TextAlign::Center, + TextAlignKeyword::End => TextAlign::End, + TextAlignKeyword::Left | TextAlignKeyword::MozLeft => { + if style.writing_mode.line_left_is_inline_start() { + TextAlign::Start + } else { + TextAlign::End + } + }, + TextAlignKeyword::Right | TextAlignKeyword::MozRight => { + if style.writing_mode.line_left_is_inline_start() { + TextAlign::End + } else { + TextAlign::Start + } + }, + TextAlignKeyword::Justify => TextAlign::Start, + }; + + let (line_start, available_space) = match self.current_line.placement_among_floats.get() { + Some(placement_among_floats) => ( + placement_among_floats.start_corner.inline, + placement_among_floats.size.inline, + ), + None => (Au::zero(), self.containing_block.size.inline), + }; + + // Properly handling text-indent requires that we do not align the text + // into the text-indent. + // See <https://drafts.csswg.org/css-text/#text-indent-property> + // "This property specifies the indentation applied to lines of inline content in + // a block. The indent is treated as a margin applied to the start edge of the + // line box." + let text_indent = self.current_line.start_position.inline; + let line_length = self.current_line.inline_position - whitespace_trimmed - text_indent; + let adjusted_line_start = line_start + + match text_align { + TextAlign::Start => text_indent, + TextAlign::End => (available_space - line_length).max(text_indent), + TextAlign::Center => (available_space - line_length + text_indent) + .scale_by(0.5) + .max(text_indent), + }; + + // Calculate the justification adjustment. This is simply the remaining space on the line, + // dividided by the number of justficiation opportunities that we recorded when building + // the line. + let text_justify = self.containing_block.style.clone_text_justify(); + let justification_adjustment = match (text_align_keyword, text_justify) { + // `text-justify: none` should disable text justification. + // TODO: Handle more `text-justify` values. + (TextAlignKeyword::Justify, TextJustify::None) => Au::zero(), + (TextAlignKeyword::Justify, _) => { + match self.current_line.count_justification_opportunities() { + 0 => Au::zero(), + num_justification_opportunities => { + (available_space - text_indent - line_length) + .scale_by(1. / num_justification_opportunities as f32) + }, + } + }, + _ => Au::zero(), + }; + + // If the content overflows the line, then justification adjustment will become negative. In + // that case, do not make any adjustment for justification. + let justification_adjustment = justification_adjustment.max(Au::zero()); + + (adjusted_line_start, justification_adjustment) + } + + fn place_float_fragment(&mut self, fragment: &mut BoxFragment) { + let state = self + .sequential_layout_state + .as_mut() + .expect("Tried to lay out a float with no sequential placement state!"); + + let block_offset_from_containining_block_top = state + .current_block_position_including_margins() - + state.current_containing_block_offset(); + state.place_float_fragment( + fragment, + self.containing_block, + CollapsedMargin::zero(), + block_offset_from_containining_block_top, + ); + } + + /// Place a FloatLineItem. This is done when an unbreakable segment is committed to + /// the current line. Placement of FloatLineItems might need to be deferred until the + /// line is complete in the case that floats stop fitting on the current line. + /// + /// When placing floats we do not want to take into account any trailing whitespace on + /// the line, because that whitespace will be trimmed in the case that the line is + /// broken. Thus this function takes as an argument the new size (without whitespace) of + /// the line that these floats are joining. + fn place_float_line_item_for_commit_to_line( + &mut self, + float_item: &mut FloatLineItem, + line_inline_size_without_trailing_whitespace: Au, + ) { + let mut float_fragment = float_item.fragment.borrow_mut(); + let logical_margin_rect_size = float_fragment + .margin_rect() + .size + .to_logical(self.containing_block.style.writing_mode); + let inline_size = logical_margin_rect_size.inline.max(Au::zero()); + + let available_inline_size = match self.current_line.placement_among_floats.get() { + Some(placement_among_floats) => placement_among_floats.size.inline, + None => self.containing_block.size.inline, + } - line_inline_size_without_trailing_whitespace; + + // If this float doesn't fit on the current line or a previous float didn't fit on + // the current line, we need to place it starting at the next line BUT still as + // children of this line's hierarchy of inline boxes (for the purposes of properly + // parenting in their stacking contexts). Once all the line content is gathered we + // will place them later. + let has_content = self.current_line.has_content || self.current_line_segment.has_content; + let fits_on_line = !has_content || inline_size <= available_inline_size; + let needs_placement_later = + self.current_line.has_floats_waiting_to_be_placed || !fits_on_line; + + if needs_placement_later { + self.current_line.has_floats_waiting_to_be_placed = true; + } else { + self.place_float_fragment(&mut float_fragment); + float_item.needs_placement = false; + } + + // We've added a new float to the IFC, but this may have actually changed the + // position of the current line. In order to determine that we regenerate the + // placement among floats for the current line, which may adjust its inline + // start position. + let new_placement = self.place_line_among_floats(&LogicalVec2 { + inline: line_inline_size_without_trailing_whitespace, + block: self.current_line.max_block_size.resolve(), + }); + self.current_line + .replace_placement_among_floats(new_placement); + } + + /// Given a new potential line size for the current line, create a "placement" for that line. + /// This tells us whether or not the new potential line will fit in the current block position + /// or need to be moved. In addition, the placement rect determines the inline start and end + /// of the line if it's used as the final placement among floats. + fn place_line_among_floats(&self, potential_line_size: &LogicalVec2<Au>) -> LogicalRect<Au> { + let sequential_layout_state = self + .sequential_layout_state + .as_ref() + .expect("Should not have called this function without having floats."); + + let ifc_offset_in_float_container = LogicalVec2 { + inline: sequential_layout_state + .floats + .containing_block_info + .inline_start, + block: sequential_layout_state.current_containing_block_offset(), + }; + + let ceiling = self + .current_line + .line_block_start_considering_placement_among_floats(); + let mut placement = PlacementAmongFloats::new( + &sequential_layout_state.floats, + ceiling + ifc_offset_in_float_container.block, + LogicalVec2 { + inline: potential_line_size.inline, + block: potential_line_size.block, + }, + &PaddingBorderMargin::zero(), + ); + + let mut placement_rect = placement.place(); + placement_rect.start_corner -= ifc_offset_in_float_container; + placement_rect + } + + /// Returns true if a new potential line size for the current line would require a line + /// break. This takes into account floats and will also update the "placement among + /// floats" for this line if the potential line size would not cause a line break. + /// Thus, calling this method has side effects and should only be done while in the + /// process of laying out line content that is always going to be committed to this + /// line or the next. + fn new_potential_line_size_causes_line_break( + &mut self, + potential_line_size: &LogicalVec2<Au>, + ) -> bool { + let available_line_space = if self.sequential_layout_state.is_some() { + self.current_line + .placement_among_floats + .get_or_init(|| self.place_line_among_floats(potential_line_size)) + .size + } else { + LogicalVec2 { + inline: self.containing_block.size.inline, + block: MAX_AU, + } + }; + + let inline_would_overflow = potential_line_size.inline > available_line_space.inline; + let block_would_overflow = potential_line_size.block > available_line_space.block; + + // The first content that is added to a line cannot trigger a line break and + // the `white-space` propertly can also prevent all line breaking. + let can_break = self.current_line.has_content; + + // If this is the first content on the line and we already have a float placement, + // that means that the placement was initialized by a leading float in the IFC. + // This placement needs to be updated, because the first line content might push + // the block start of the line downward. If there is no float placement, we want + // to make one to properly set the block position of the line. + if !can_break { + // Even if we cannot break, adding content to this line might change its position. + // In that case we need to redo our placement among floats. + if self.sequential_layout_state.is_some() && + (inline_would_overflow || block_would_overflow) + { + let new_placement = self.place_line_among_floats(potential_line_size); + self.current_line + .replace_placement_among_floats(new_placement); + } + + return false; + } + + // If the potential line is larger than the containing block we do not even need to consider + // floats. We definitely have to do a linebreak. + if potential_line_size.inline > self.containing_block.size.inline { + return true; + } + + // Not fitting in the block space means that our block size has changed and we had a + // placement among floats that is no longer valid. This same placement might just + // need to be expanded or perhaps we need to line break. + if block_would_overflow { + // If we have a limited block size then we are wedging this line between floats. + assert!(self.sequential_layout_state.is_some()); + let new_placement = self.place_line_among_floats(potential_line_size); + if new_placement.start_corner.block != + self.current_line + .line_block_start_considering_placement_among_floats() + { + return true; + } else { + self.current_line + .replace_placement_among_floats(new_placement); + return false; + } + } + + // Otherwise the new potential line size will require a newline if it fits in the + // inline space available for this line. This space may be smaller than the + // containing block if floats shrink the available inline space. + inline_would_overflow + } + + pub(super) fn defer_forced_line_break(&mut self) { + // If the current portion of the unbreakable segment does not fit on the current line + // we need to put it on a new line *before* actually triggering the hard line break. + if !self.unbreakable_segment_fits_on_line() { + self.process_line_break(false /* forced_line_break */); + } + + // Defer the actual line break until we've cleared all ending inline boxes. + self.linebreak_before_new_content = true; + + // In quirks mode, the line-height isn't automatically added to the line. If we consider a + // forced line break a kind of preserved white space, quirks mode requires that we add the + // line-height of the current element to the line box height. + // + // The exception here is `<br>` elements. They are implemented with `pre-line` in Servo, but + // this is an implementation detail. The "magic" behavior of `<br>` elements is that they + // add line-height to the line conditionally: only when they are on an otherwise empty line. + let line_is_empty = + !self.current_line_segment.has_content && !self.current_line.has_content; + if !self.processing_br_element() || line_is_empty { + let strut_size = self + .current_inline_container_state() + .strut_block_sizes + .clone(); + self.update_unbreakable_segment_for_new_content( + &strut_size, + Au::zero(), + SegmentContentFlags::empty(), + ); + } + + self.had_inflow_content = true; + } + + pub(super) fn possibly_flush_deferred_forced_line_break(&mut self) { + if !self.linebreak_before_new_content { + return; + } + + self.commit_current_segment_to_line(); + self.process_line_break(true /* forced_line_break */); + self.linebreak_before_new_content = false; + } + + fn push_line_item_to_unbreakable_segment(&mut self, line_item: LineItem) { + self.current_line_segment + .push_line_item(line_item, self.inline_box_state_stack.len()); + } + + pub(super) fn push_glyph_store_to_unbreakable_segment( + &mut self, + glyph_store: std::sync::Arc<GlyphStore>, + text_run: &TextRun, + font_index: usize, + bidi_level: Level, + range: range::Range<ByteIndex>, + ) { + let inline_advance = glyph_store.total_advance(); + let flags = if glyph_store.is_whitespace() { + SegmentContentFlags::from(text_run.parent_style.get_inherited_text()) + } else { + SegmentContentFlags::empty() + }; + + // If the metrics of this font don't match the default font, we are likely using a fallback + // font and need to adjust the line size to account for a potentially different font. + // If somehow the metrics match, the line size won't change. + let ifc_font_info = &self.ifc.font_metrics[font_index]; + let font_metrics = ifc_font_info.metrics.clone(); + let using_fallback_font = + self.current_inline_container_state().font_metrics != font_metrics; + + let quirks_mode = self.layout_context.style_context.quirks_mode() != QuirksMode::NoQuirks; + let strut_size = if using_fallback_font { + // TODO(mrobinson): This value should probably be cached somewhere. + let container_state = self.current_inline_container_state(); + let vertical_align = effective_vertical_align( + &container_state.style, + self.inline_box_state_stack.last().map(|c| &c.base), + ); + let mut block_size = container_state.get_block_size_contribution( + vertical_align, + &font_metrics, + &container_state.font_metrics, + ); + block_size.adjust_for_baseline_offset(container_state.baseline_offset); + block_size + } else if quirks_mode && !flags.is_collapsible_whitespace() { + // Normally, the strut is incorporated into the nested block size. In quirks mode though + // if we find any text that isn't collapsed whitespace, we need to incorporate the strut. + // TODO(mrobinson): This isn't quite right for situations where collapsible white space + // ultimately does not collapse because it is between two other pieces of content. + self.current_inline_container_state() + .strut_block_sizes + .clone() + } else { + LineBlockSizes::zero() + }; + self.update_unbreakable_segment_for_new_content(&strut_size, inline_advance, flags); + + let current_inline_box_identifier = self.current_inline_box_identifier(); + match self.current_line_segment.line_items.last_mut() { + Some(LineItem::TextRun(inline_box_identifier, line_item)) + if *inline_box_identifier == current_inline_box_identifier && + line_item.can_merge(ifc_font_info.key, bidi_level) => + { + line_item.text.push(glyph_store); + return; + }, + _ => {}, + } + + let selection_range = if let Some(selection) = &text_run.selection_range { + let intersection = selection.intersect(&range); + if intersection.is_empty() { + let insertion_point_index = selection.begin(); + // We only allow the caret to be shown in the start of the fragment if it is the first fragment. + // Otherwise this will cause duplicate caret, especially apparent when encountered line break. + if insertion_point_index >= range.begin() && + insertion_point_index <= range.end() && + (range.begin() != insertion_point_index || range.begin().0 == 0) + { + Some(Range::new( + insertion_point_index - range.begin(), + ByteIndex(0), + )) + } else { + None + } + } else { + Some(Range::new( + intersection.begin() - range.begin(), + intersection.length(), + )) + } + } else { + None + }; + + self.push_line_item_to_unbreakable_segment(LineItem::TextRun( + current_inline_box_identifier, + TextRunLineItem { + text: vec![glyph_store], + base_fragment_info: text_run.base_fragment_info, + parent_style: text_run.parent_style.clone(), + font_metrics, + font_key: ifc_font_info.key, + text_decoration_line: self.current_inline_container_state().text_decoration_line, + bidi_level, + selection_range, + selected_style: text_run.selected_style.clone(), + }, + )); + } + + fn update_unbreakable_segment_for_new_content( + &mut self, + block_sizes_of_content: &LineBlockSizes, + inline_size: Au, + flags: SegmentContentFlags, + ) { + if flags.is_collapsible_whitespace() || flags.is_wrappable_and_hangable() { + self.current_line_segment.trailing_whitespace_size = inline_size; + } else { + self.current_line_segment.trailing_whitespace_size = Au::zero(); + } + if !flags.is_collapsible_whitespace() { + self.current_line_segment.has_content = true; + self.had_inflow_content = true; + } + + // This may or may not include the size of the strut depending on the quirks mode setting. + let container_max_block_size = &self + .current_inline_container_state() + .nested_strut_block_sizes + .clone(); + self.current_line_segment + .max_block_size + .max_assign(container_max_block_size); + self.current_line_segment + .max_block_size + .max_assign(block_sizes_of_content); + + self.current_line_segment.inline_size += inline_size; + + // Propagate the whitespace setting to the current nesting level. + *self + .current_inline_container_state() + .has_content + .borrow_mut() = true; + self.propagate_current_nesting_level_white_space_style(); + } + + fn process_line_break(&mut self, forced_line_break: bool) { + self.current_line_segment.trim_leading_whitespace(); + self.finish_current_line_and_reset(forced_line_break); + } + + pub(super) fn unbreakable_segment_fits_on_line(&mut self) -> bool { + let potential_line_size = LogicalVec2 { + inline: self.current_line.inline_position + self.current_line_segment.inline_size - + self.current_line_segment.trailing_whitespace_size, + block: self + .current_line_max_block_size_including_nested_containers() + .max(&self.current_line_segment.max_block_size) + .resolve(), + }; + + !self.new_potential_line_size_causes_line_break(&potential_line_size) + } + + /// Process a soft wrap opportunity. This will either commit the current unbreakble + /// segment to the current line, if it fits within the containing block and float + /// placement boundaries, or do a line break and then commit the segment. + pub(super) fn process_soft_wrap_opportunity(&mut self) { + if self.current_line_segment.line_items.is_empty() { + return; + } + if self.text_wrap_mode == TextWrapMode::Nowrap { + return; + } + + let potential_line_size = LogicalVec2 { + inline: self.current_line.inline_position + self.current_line_segment.inline_size - + self.current_line_segment.trailing_whitespace_size, + block: self + .current_line_max_block_size_including_nested_containers() + .max(&self.current_line_segment.max_block_size) + .resolve(), + }; + + if self.new_potential_line_size_causes_line_break(&potential_line_size) { + self.process_line_break(false /* forced_line_break */); + } + self.commit_current_segment_to_line(); + } + + /// Commit the current unbrekable segment to the current line. In addition, this will + /// place all floats in the unbreakable segment and expand the line dimensions. + fn commit_current_segment_to_line(&mut self) { + // The line segments might have no items and have content after processing a forced + // linebreak on an empty line. + if self.current_line_segment.line_items.is_empty() && !self.current_line_segment.has_content + { + return; + } + + if !self.current_line.has_content { + self.current_line_segment.trim_leading_whitespace(); + } + + self.current_line.inline_position += self.current_line_segment.inline_size; + self.current_line.max_block_size = self + .current_line_max_block_size_including_nested_containers() + .max(&self.current_line_segment.max_block_size); + let line_inline_size_without_trailing_whitespace = + self.current_line.inline_position - self.current_line_segment.trailing_whitespace_size; + + // Place all floats in this unbreakable segment. + let mut segment_items = mem::take(&mut self.current_line_segment.line_items); + for item in segment_items.iter_mut() { + if let LineItem::Float(_, float_item) = item { + self.place_float_line_item_for_commit_to_line( + float_item, + line_inline_size_without_trailing_whitespace, + ); + } + } + + // If the current line was never placed among floats, we need to do that now based on the + // new size. Calling `new_potential_line_size_causes_line_break()` here triggers the + // new line to be positioned among floats. This should never ask for a line + // break because it is the first content on the line. + if self.current_line.line_items.is_empty() { + let will_break = self.new_potential_line_size_causes_line_break(&LogicalVec2 { + inline: line_inline_size_without_trailing_whitespace, + block: self.current_line_segment.max_block_size.resolve(), + }); + assert!(!will_break); + } + + self.current_line.line_items.extend(segment_items); + self.current_line.has_content |= self.current_line_segment.has_content; + + self.current_line_segment.reset(); + } +} + +bitflags! { + pub struct SegmentContentFlags: u8 { + const COLLAPSIBLE_WHITESPACE = 0b00000001; + const WRAPPABLE_AND_HANGABLE_WHITESPACE = 0b00000010; + } +} + +impl SegmentContentFlags { + fn is_collapsible_whitespace(&self) -> bool { + self.contains(Self::COLLAPSIBLE_WHITESPACE) + } + + fn is_wrappable_and_hangable(&self) -> bool { + self.contains(Self::WRAPPABLE_AND_HANGABLE_WHITESPACE) + } +} + +impl From<&InheritedText> for SegmentContentFlags { + fn from(style_text: &InheritedText) -> Self { + let mut flags = Self::empty(); + + // White-space with `white-space-collapse: break-spaces` or `white-space-collapse: preserve` + // never collapses. + if !matches!( + style_text.white_space_collapse, + WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces + ) { + flags.insert(Self::COLLAPSIBLE_WHITESPACE); + } + + // White-space with `white-space-collapse: break-spaces` never hangs and always takes up + // space. + if style_text.text_wrap_mode == TextWrapMode::Wrap && + style_text.white_space_collapse != WhiteSpaceCollapse::BreakSpaces + { + flags.insert(Self::WRAPPABLE_AND_HANGABLE_WHITESPACE); + } + flags + } +} + +impl InlineFormattingContext { + #[cfg_attr( + feature = "tracing", + tracing::instrument( + name = "InlineFormattingContext::new_with_builder", + skip_all, + fields(servo_profiling = true), + level = "trace", + ) + )] + pub(super) fn new_with_builder( + builder: InlineFormattingContextBuilder, + layout_context: &LayoutContext, + propagated_data: PropagatedBoxTreeData, + has_first_formatted_line: bool, + is_single_line_text_input: bool, + starting_bidi_level: Level, + ) -> Self { + // This is to prevent a double borrow. + let text_content: String = builder.text_segments.into_iter().collect(); + let mut font_metrics = Vec::new(); + + let bidi_info = BidiInfo::new(&text_content, Some(starting_bidi_level)); + let has_right_to_left_content = bidi_info.has_rtl(); + + let mut new_linebreaker = LineBreaker::new(text_content.as_str()); + for item in builder.inline_items.iter() { + match &mut *item.borrow_mut() { + InlineItem::TextRun(text_run) => { + text_run.borrow_mut().segment_and_shape( + &text_content, + &layout_context.font_context, + &mut new_linebreaker, + &mut font_metrics, + &bidi_info, + ); + }, + InlineItem::StartInlineBox(inline_box) => { + let inline_box = &mut *inline_box.borrow_mut(); + if let Some(font) = get_font_for_first_font_for_style( + &inline_box.base.style, + &layout_context.font_context, + ) { + inline_box.default_font_index = Some(add_or_get_font( + &font, + &mut font_metrics, + &layout_context.font_context, + )); + } + }, + InlineItem::Atomic(_, index_in_text, bidi_level) => { + *bidi_level = bidi_info.levels[*index_in_text]; + }, + InlineItem::OutOfFlowAbsolutelyPositionedBox(..) | + InlineItem::OutOfFlowFloatBox(_) | + InlineItem::EndInlineBox => {}, + } + } + + InlineFormattingContext { + text_content, + inline_items: builder.inline_items, + inline_boxes: builder.inline_boxes, + font_metrics, + text_decoration_line: propagated_data.text_decoration, + has_first_formatted_line, + contains_floats: builder.contains_floats, + is_single_line_text_input, + has_right_to_left_content, + } + } + + pub(super) fn layout( + &self, + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + containing_block: &ContainingBlock, + sequential_layout_state: Option<&mut SequentialLayoutState>, + collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin, + ) -> CacheableLayoutResult { + let first_line_inline_start = if self.has_first_formatted_line { + containing_block + .style + .get_inherited_text() + .text_indent + .length + .to_used_value(containing_block.size.inline) + } else { + Au::zero() + }; + + // Clear any cached inline fragments from previous layouts. + for inline_box in self.inline_boxes.iter() { + inline_box.borrow().base.clear_fragments(); + } + + let style = containing_block.style; + + // It's unfortunate that it isn't possible to get this during IFC text processing, but in + // that situation the style of the containing block is unknown. + let default_font_metrics = + get_font_for_first_font_for_style(style, &layout_context.font_context) + .map(|font| font.metrics.clone()); + + let style_text = containing_block.style.get_inherited_text(); + let mut inline_container_state_flags = InlineContainerStateFlags::empty(); + if inline_container_needs_strut(style, layout_context, None) { + inline_container_state_flags.insert(InlineContainerStateFlags::CREATE_STRUT); + } + if self.is_single_line_text_input { + inline_container_state_flags + .insert(InlineContainerStateFlags::IS_SINGLE_LINE_TEXT_INPUT); + } + + let mut layout = InlineFormattingContextLayout { + positioning_context, + containing_block, + sequential_layout_state, + layout_context, + ifc: self, + fragments: Vec::new(), + current_line: LineUnderConstruction::new(LogicalVec2 { + inline: first_line_inline_start, + block: Au::zero(), + }), + root_nesting_level: InlineContainerState::new( + style.to_arc(), + inline_container_state_flags, + None, /* parent_container */ + self.text_decoration_line, + default_font_metrics.as_ref(), + ), + inline_box_state_stack: Vec::new(), + inline_box_states: Vec::with_capacity(self.inline_boxes.len()), + current_line_segment: UnbreakableSegmentUnderConstruction::new(), + linebreak_before_new_content: false, + deferred_br_clear: Clear::None, + have_deferred_soft_wrap_opportunity: false, + had_inflow_content: false, + depends_on_block_constraints: false, + white_space_collapse: style_text.white_space_collapse, + text_wrap_mode: style_text.text_wrap_mode, + baselines: Baselines::default(), + }; + + // FIXME(pcwalton): This assumes that margins never collapse through inline formatting + // contexts (i.e. that inline formatting contexts are never empty). Is that right? + // FIXME(mrobinson): This should not happen if the IFC collapses through. + if let Some(ref mut sequential_layout_state) = layout.sequential_layout_state { + sequential_layout_state.collapse_margins(); + // FIXME(mrobinson): Collapse margins in the containing block offsets as well?? + } + + for item in self.inline_items.iter() { + let item = &*item.borrow(); + + // Any new box should flush a pending hard line break. + if !matches!(item, InlineItem::EndInlineBox) { + layout.possibly_flush_deferred_forced_line_break(); + } + + match item { + InlineItem::StartInlineBox(inline_box) => { + layout.start_inline_box(&inline_box.borrow()); + }, + InlineItem::EndInlineBox => layout.finish_inline_box(), + InlineItem::TextRun(run) => run.borrow().layout_into_line_items(&mut layout), + InlineItem::Atomic(atomic_formatting_context, offset_in_text, bidi_level) => { + atomic_formatting_context.layout_into_line_items( + &mut layout, + *offset_in_text, + *bidi_level, + ); + }, + InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, _) => { + layout.push_line_item_to_unbreakable_segment(LineItem::AbsolutelyPositioned( + layout.current_inline_box_identifier(), + AbsolutelyPositionedLineItem { + absolutely_positioned_box: positioned_box.clone(), + }, + )); + }, + InlineItem::OutOfFlowFloatBox(float_box) => { + float_box.layout_into_line_items(&mut layout); + }, + } + } + + layout.finish_last_line(); + + let mut collapsible_margins_in_children = CollapsedBlockMargins::zero(); + let content_block_size = layout.current_line.start_position.block; + collapsible_margins_in_children.collapsed_through = !layout.had_inflow_content && + content_block_size.is_zero() && + collapsible_with_parent_start_margin.0; + + CacheableLayoutResult { + fragments: layout.fragments, + content_block_size, + collapsible_margins_in_children, + baselines: layout.baselines, + depends_on_block_constraints: layout.depends_on_block_constraints, + content_inline_size_for_table: None, + specific_layout_info: None, + } + } + + fn next_character_prevents_soft_wrap_opportunity(&self, index: usize) -> bool { + let Some(character) = self.text_content[index..].chars().nth(1) else { + return false; + }; + char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character) + } + + fn previous_character_prevents_soft_wrap_opportunity(&self, index: usize) -> bool { + let Some(character) = self.text_content[0..index].chars().next_back() else { + return false; + }; + char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character) + } +} + +impl InlineContainerState { + fn new( + style: Arc<ComputedValues>, + flags: InlineContainerStateFlags, + parent_container: Option<&InlineContainerState>, + parent_text_decoration_line: TextDecorationLine, + font_metrics: Option<&FontMetrics>, + ) -> Self { + let text_decoration_line = parent_text_decoration_line | style.clone_text_decoration_line(); + let font_metrics = font_metrics.cloned().unwrap_or_else(FontMetrics::empty); + let line_height = line_height( + &style, + &font_metrics, + flags.contains(InlineContainerStateFlags::IS_SINGLE_LINE_TEXT_INPUT), + ); + + let mut baseline_offset = Au::zero(); + let mut strut_block_sizes = Self::get_block_sizes_with_style( + effective_vertical_align(&style, parent_container), + &style, + &font_metrics, + &font_metrics, + line_height, + ); + if let Some(parent_container) = parent_container { + // The baseline offset from `vertical-align` might adjust where our block size contribution is + // within the line. + baseline_offset = parent_container.get_cumulative_baseline_offset_for_child( + style.clone_vertical_align(), + &strut_block_sizes, + ); + strut_block_sizes.adjust_for_baseline_offset(baseline_offset); + } + + let mut nested_block_sizes = parent_container + .map(|container| container.nested_strut_block_sizes.clone()) + .unwrap_or_else(LineBlockSizes::zero); + if flags.contains(InlineContainerStateFlags::CREATE_STRUT) { + nested_block_sizes.max_assign(&strut_block_sizes); + } + + Self { + style, + flags, + has_content: RefCell::new(false), + text_decoration_line, + nested_strut_block_sizes: nested_block_sizes, + strut_block_sizes, + baseline_offset, + font_metrics, + } + } + + fn get_block_sizes_with_style( + vertical_align: VerticalAlign, + style: &ComputedValues, + font_metrics: &FontMetrics, + font_metrics_of_first_font: &FontMetrics, + line_height: Au, + ) -> LineBlockSizes { + if !is_baseline_relative(vertical_align) { + return LineBlockSizes { + line_height, + baseline_relative_size_for_line_height: None, + size_for_baseline_positioning: BaselineRelativeSize::zero(), + }; + } + + // From https://drafts.csswg.org/css-inline/#inline-height + // > If line-height computes to `normal` and either `text-box-edge` is `leading` or this + // > is the root inline box, the font’s line gap metric may also be incorporated + // > into A and D by adding half to each side as half-leading. + // + // `text-box-edge` isn't implemented (and this is a draft specification), so it's + // always effectively `leading`, which means we always take into account the line gap + // when `line-height` is normal. + let mut ascent = font_metrics.ascent; + let mut descent = font_metrics.descent; + if style.get_font().line_height == LineHeight::Normal { + let half_leading_from_line_gap = + (font_metrics.line_gap - descent - ascent).scale_by(0.5); + ascent += half_leading_from_line_gap; + descent += half_leading_from_line_gap; + } + + // The ascent and descent we use for computing the line's final line height isn't + // the same the ascent and descent we use for finding the baseline. For finding + // the baseline we want the content rect. + let size_for_baseline_positioning = BaselineRelativeSize { ascent, descent }; + + // From https://drafts.csswg.org/css-inline/#inline-height + // > When its computed line-height is not normal, its layout bounds are derived solely + // > from metrics of its first available font (ignoring glyphs from other fonts), and + // > leading is used to adjust the effective A and D to add up to the used line-height. + // > Calculate the leading L as L = line-height - (A + D). Half the leading (its + // > half-leading) is added above A of the first available font, and the other half + // > below D of the first available font, giving an effective ascent above the baseline + // > of A′ = A + L/2, and an effective descent of D′ = D + L/2. + // + // Note that leading might be negative here and the line-height might be zero. In + // the case where the height is zero, ascent and descent will move to the same + // point in the block axis. Even though the contribution to the line height is + // zero in this case, the line may get some height when taking them into + // considering with other zero line height boxes that converge on other block axis + // locations when using the above formula. + if style.get_font().line_height != LineHeight::Normal { + ascent = font_metrics_of_first_font.ascent; + descent = font_metrics_of_first_font.descent; + let half_leading = (line_height - (ascent + descent)).scale_by(0.5); + // We want the sum of `ascent` and `descent` to equal `line_height`. + // If we just add `half_leading` to both, then we may not get `line_height` + // due to precision limitations of `Au`. Instead, we set `descent` to + // the value that will guarantee the correct sum. + ascent += half_leading; + descent = line_height - ascent; + } + + LineBlockSizes { + line_height, + baseline_relative_size_for_line_height: Some(BaselineRelativeSize { ascent, descent }), + size_for_baseline_positioning, + } + } + + fn get_block_size_contribution( + &self, + vertical_align: VerticalAlign, + font_metrics: &FontMetrics, + font_metrics_of_first_font: &FontMetrics, + ) -> LineBlockSizes { + Self::get_block_sizes_with_style( + vertical_align, + &self.style, + font_metrics, + font_metrics_of_first_font, + line_height( + &self.style, + font_metrics, + self.flags + .contains(InlineContainerStateFlags::IS_SINGLE_LINE_TEXT_INPUT), + ), + ) + } + + fn get_cumulative_baseline_offset_for_child( + &self, + child_vertical_align: VerticalAlign, + child_block_size: &LineBlockSizes, + ) -> Au { + let block_size = self.get_block_size_contribution( + child_vertical_align.clone(), + &self.font_metrics, + &self.font_metrics, + ); + self.baseline_offset + + match child_vertical_align { + // `top` and `bottom are not actually relative to the baseline, but this value is unused + // in those cases. + // TODO: We should distinguish these from `baseline` in order to implement "aligned subtrees" properly. + // See https://drafts.csswg.org/css2/#aligned-subtree. + VerticalAlign::Keyword(VerticalAlignKeyword::Baseline) | + VerticalAlign::Keyword(VerticalAlignKeyword::Top) | + VerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => Au::zero(), + VerticalAlign::Keyword(VerticalAlignKeyword::Sub) => { + block_size.resolve().scale_by(FONT_SUBSCRIPT_OFFSET_RATIO) + }, + VerticalAlign::Keyword(VerticalAlignKeyword::Super) => { + -block_size.resolve().scale_by(FONT_SUPERSCRIPT_OFFSET_RATIO) + }, + VerticalAlign::Keyword(VerticalAlignKeyword::TextTop) => { + child_block_size.size_for_baseline_positioning.ascent - self.font_metrics.ascent + }, + VerticalAlign::Keyword(VerticalAlignKeyword::Middle) => { + // "Align the vertical midpoint of the box with the baseline of the parent + // box plus half the x-height of the parent." + (child_block_size.size_for_baseline_positioning.ascent - + child_block_size.size_for_baseline_positioning.descent - + self.font_metrics.x_height) + .scale_by(0.5) + }, + VerticalAlign::Keyword(VerticalAlignKeyword::TextBottom) => { + self.font_metrics.descent - + child_block_size.size_for_baseline_positioning.descent + }, + VerticalAlign::Length(length_percentage) => { + -length_percentage.to_used_value(child_block_size.line_height) + }, + } + } +} + +impl IndependentFormattingContext { + fn layout_into_line_items( + &self, + layout: &mut InlineFormattingContextLayout, + offset_in_text: usize, + bidi_level: Level, + ) { + // We need to know the inline size of the atomic before deciding whether to do the line break. + let mut child_positioning_context = PositioningContext::new_for_style(self.style()) + .unwrap_or_else(|| PositioningContext::new_for_subtree(true)); + let IndependentFloatOrAtomicLayoutResult { + mut fragment, + baselines, + pbm_sums, + } = self.layout_float_or_atomic_inline( + layout.layout_context, + &mut child_positioning_context, + layout.containing_block, + ); + + // If this Fragment's layout depends on the block size of the containing block, + // then the entire layout of the inline formatting context does as well. + layout.depends_on_block_constraints |= fragment.base.flags.contains( + FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM, + ); + + // Offset the content rectangle by the physical offset of the padding, border, and margin. + let container_writing_mode = layout.containing_block.style.writing_mode; + let pbm_physical_offset = pbm_sums + .start_offset() + .to_physical_size(container_writing_mode); + fragment.content_rect = fragment + .content_rect + .translate(pbm_physical_offset.to_vector()); + + // Apply baselines if necessary. + let mut fragment = match baselines { + Some(baselines) => fragment.with_baselines(baselines), + None => fragment, + }; + + // Lay out absolutely positioned children if this new atomic establishes a containing block + // for absolutes. + let positioning_context = if self.is_replaced() { + None + } else { + if fragment + .style + .establishes_containing_block_for_absolute_descendants(fragment.base.flags) + { + child_positioning_context + .layout_collected_children(layout.layout_context, &mut fragment); + } + Some(child_positioning_context) + }; + + if layout.text_wrap_mode == TextWrapMode::Wrap && + !layout + .ifc + .previous_character_prevents_soft_wrap_opportunity(offset_in_text) + { + layout.process_soft_wrap_opportunity(); + } + + let size = pbm_sums.sum() + + fragment + .content_rect + .size + .to_logical(container_writing_mode); + let baseline_offset = self + .pick_baseline(&fragment.baselines(container_writing_mode)) + .map(|baseline| pbm_sums.block_start + baseline) + .unwrap_or(size.block); + + let (block_sizes, baseline_offset_in_parent) = + self.get_block_sizes_and_baseline_offset(layout, size.block, baseline_offset); + layout.update_unbreakable_segment_for_new_content( + &block_sizes, + size.inline, + SegmentContentFlags::empty(), + ); + + let fragment = ArcRefCell::new(fragment); + self.base.set_fragment(Fragment::Box(fragment.clone())); + + layout.push_line_item_to_unbreakable_segment(LineItem::Atomic( + layout.current_inline_box_identifier(), + AtomicLineItem { + fragment, + size, + positioning_context, + baseline_offset_in_parent, + baseline_offset_in_item: baseline_offset, + bidi_level, + }, + )); + + // If there's a soft wrap opportunity following this atomic, defer a soft wrap opportunity + // for when we next process text content. + if !layout + .ifc + .next_character_prevents_soft_wrap_opportunity(offset_in_text) + { + layout.have_deferred_soft_wrap_opportunity = true; + } + } + + /// Picks either the first or the last baseline, depending on `baseline-source`. + /// TODO: clarify that this is not to be used for box alignment in flex/grid + /// <https://drafts.csswg.org/css-inline/#baseline-source> + fn pick_baseline(&self, baselines: &Baselines) -> Option<Au> { + match self.style().clone_baseline_source() { + BaselineSource::First => baselines.first, + BaselineSource::Last => baselines.last, + BaselineSource::Auto => match &self.contents { + IndependentFormattingContextContents::NonReplaced( + IndependentNonReplacedContents::Flow(_), + ) => baselines.last, + _ => baselines.first, + }, + } + } + + fn get_block_sizes_and_baseline_offset( + &self, + ifc: &InlineFormattingContextLayout, + block_size: Au, + baseline_offset_in_content_area: Au, + ) -> (LineBlockSizes, Au) { + let mut contribution = if !is_baseline_relative(self.style().clone_vertical_align()) { + LineBlockSizes { + line_height: block_size, + baseline_relative_size_for_line_height: None, + size_for_baseline_positioning: BaselineRelativeSize::zero(), + } + } else { + let baseline_relative_size = BaselineRelativeSize { + ascent: baseline_offset_in_content_area, + descent: block_size - baseline_offset_in_content_area, + }; + LineBlockSizes { + line_height: block_size, + baseline_relative_size_for_line_height: Some(baseline_relative_size.clone()), + size_for_baseline_positioning: baseline_relative_size, + } + }; + + let baseline_offset = ifc + .current_inline_container_state() + .get_cumulative_baseline_offset_for_child( + self.style().clone_vertical_align(), + &contribution, + ); + contribution.adjust_for_baseline_offset(baseline_offset); + + (contribution, baseline_offset) + } +} + +impl FloatBox { + fn layout_into_line_items(&self, layout: &mut InlineFormattingContextLayout) { + let fragment = ArcRefCell::new(self.layout( + layout.layout_context, + layout.positioning_context, + layout.containing_block, + )); + + self.contents + .base + .set_fragment(Fragment::Box(fragment.clone())); + layout.push_line_item_to_unbreakable_segment(LineItem::Float( + layout.current_inline_box_identifier(), + FloatLineItem { + fragment, + needs_placement: true, + }, + )); + } +} + +fn place_pending_floats(ifc: &mut InlineFormattingContextLayout, line_items: &mut [LineItem]) { + for item in line_items.iter_mut() { + if let LineItem::Float(_, float_line_item) = item { + if float_line_item.needs_placement { + ifc.place_float_fragment(&mut float_line_item.fragment.borrow_mut()); + } + } + } +} + +fn line_height( + parent_style: &ComputedValues, + font_metrics: &FontMetrics, + is_single_line_text_input: bool, +) -> Au { + let font = parent_style.get_font(); + let font_size = font.font_size.computed_size(); + let mut line_height = match font.line_height { + LineHeight::Normal => font_metrics.line_gap, + LineHeight::Number(number) => (font_size * number.0).into(), + LineHeight::Length(length) => length.0.into(), + }; + + // Single line text inputs line height is clamped to the size of `normal`. See + // <https://github.com/whatwg/html/pull/5462>. + if is_single_line_text_input { + line_height.max_assign(font_metrics.line_gap); + } + + line_height +} + +fn effective_vertical_align( + style: &ComputedValues, + container: Option<&InlineContainerState>, +) -> VerticalAlign { + if container.is_none() { + // If we are at the root of the inline formatting context, we shouldn't use the + // computed `vertical-align`, since it has no effect on the contents of this IFC + // (it can just affect how the block container is aligned within the parent IFC). + VerticalAlign::Keyword(VerticalAlignKeyword::Baseline) + } else { + style.clone_vertical_align() + } +} + +fn is_baseline_relative(vertical_align: VerticalAlign) -> bool { + !matches!( + vertical_align, + VerticalAlign::Keyword(VerticalAlignKeyword::Top) | + VerticalAlign::Keyword(VerticalAlignKeyword::Bottom) + ) +} + +/// Whether or not a strut should be created for an inline container. Normally +/// all inline containers get struts. In quirks mode this isn't always the case +/// though. +/// +/// From <https://quirks.spec.whatwg.org/#the-line-height-calculation-quirk> +/// +/// > ### § 3.3. The line height calculation quirk +/// > In quirks mode and limited-quirks mode, an inline box that matches the following +/// > conditions, must, for the purpose of line height calculation, act as if the box had a +/// > line-height of zero. +/// > +/// > - The border-top-width, border-bottom-width, padding-top and padding-bottom +/// > properties have a used value of zero and the box has a vertical writing mode, or the +/// > border-right-width, border-left-width, padding-right and padding-left properties have +/// > a used value of zero and the box has a horizontal writing mode. +/// > - It either contains no text or it contains only collapsed whitespace. +/// > +/// > ### § 3.4. The blocks ignore line-height quirk +/// > In quirks mode and limited-quirks mode, for a block container element whose content is +/// > composed of inline-level elements, the element’s line-height must be ignored for the +/// > purpose of calculating the minimal height of line boxes within the element. +/// +/// Since we incorporate the size of the strut into the line-height calculation when +/// adding text, we can simply not incorporate the strut at the start of inline box +/// processing. This also works the same for the root of the IFC. +fn inline_container_needs_strut( + style: &ComputedValues, + layout_context: &LayoutContext, + pbm: Option<&PaddingBorderMargin>, +) -> bool { + if layout_context.style_context.quirks_mode() == QuirksMode::NoQuirks { + return true; + } + + // This is not in a standard yet, but all browsers disable this quirk for list items. + // See https://github.com/whatwg/quirks/issues/38. + if style.get_box().display.is_list_item() { + return true; + } + + pbm.map(|pbm| !pbm.padding_border_sums.inline.is_zero()) + .unwrap_or(false) +} + +impl ComputeInlineContentSizes for InlineFormattingContext { + // This works on an already-constructed `InlineFormattingContext`, + // Which would have to change if/when + // `BlockContainer::construct` parallelize their construction. + fn compute_inline_content_sizes( + &self, + layout_context: &LayoutContext, + constraint_space: &ConstraintSpace, + ) -> InlineContentSizesResult { + ContentSizesComputation::compute(self, layout_context, constraint_space) + } +} + +/// A struct which takes care of computing [`ContentSizes`] for an [`InlineFormattingContext`]. +struct ContentSizesComputation<'layout_data> { + layout_context: &'layout_data LayoutContext<'layout_data>, + constraint_space: &'layout_data ConstraintSpace, + paragraph: ContentSizes, + current_line: ContentSizes, + /// Size for whitespace pending to be added to this line. + pending_whitespace: ContentSizes, + /// Whether or not the current line has seen any content (excluding collapsed whitespace), + /// when sizing under a min-content constraint. + had_content_yet_for_min_content: bool, + /// Whether or not the current line has seen any content (excluding collapsed whitespace), + /// when sizing under a max-content constraint. + had_content_yet_for_max_content: bool, + /// Stack of ending padding, margin, and border to add to the length + /// when an inline box finishes. + ending_inline_pbm_stack: Vec<Au>, + depends_on_block_constraints: bool, +} + +impl<'layout_data> ContentSizesComputation<'layout_data> { + fn traverse( + mut self, + inline_formatting_context: &InlineFormattingContext, + ) -> InlineContentSizesResult { + for inline_item in inline_formatting_context.inline_items.iter() { + self.process_item(&inline_item.borrow(), inline_formatting_context); + } + self.forced_line_break(); + + InlineContentSizesResult { + sizes: self.paragraph, + depends_on_block_constraints: self.depends_on_block_constraints, + } + } + + fn process_item( + &mut self, + inline_item: &InlineItem, + inline_formatting_context: &InlineFormattingContext, + ) { + match inline_item { + InlineItem::StartInlineBox(inline_box) => { + // For margins and paddings, a cyclic percentage is resolved against zero + // for determining intrinsic size contributions. + // https://drafts.csswg.org/css-sizing-3/#min-percentage-contribution + let inline_box = inline_box.borrow(); + let zero = Au::zero(); + let writing_mode = self.constraint_space.writing_mode; + let layout_style = inline_box.layout_style(); + let padding = layout_style + .padding(writing_mode) + .percentages_relative_to(zero); + let border = layout_style.border_width(writing_mode); + let margin = inline_box + .base + .style + .margin(writing_mode) + .percentages_relative_to(zero) + .auto_is(Au::zero); + + let pbm = margin + padding + border; + if inline_box.is_first_fragment { + self.add_inline_size(pbm.inline_start); + } + if inline_box.is_last_fragment { + self.ending_inline_pbm_stack.push(pbm.inline_end); + } else { + self.ending_inline_pbm_stack.push(Au::zero()); + } + }, + InlineItem::EndInlineBox => { + let length = self.ending_inline_pbm_stack.pop().unwrap_or_else(Au::zero); + self.add_inline_size(length); + }, + InlineItem::TextRun(text_run) => { + let text_run = &*text_run.borrow(); + for segment in text_run.shaped_text.iter() { + let style_text = text_run.parent_style.get_inherited_text(); + let can_wrap = style_text.text_wrap_mode == TextWrapMode::Wrap; + + // TODO: This should take account whether or not the first and last character prevent + // linebreaks after atomics as in layout. + if can_wrap && segment.break_at_start { + self.line_break_opportunity() + } + + for run in segment.runs.iter() { + let advance = run.glyph_store.total_advance(); + if run.glyph_store.is_whitespace() { + // If this run is a forced line break, we *must* break the line + // and start measuring from the inline origin once more. + if run.is_single_preserved_newline() { + self.forced_line_break(); + continue; + } + if !matches!( + style_text.white_space_collapse, + WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces + ) { + if can_wrap { + self.line_break_opportunity(); + } else if self.had_content_yet_for_min_content { + self.pending_whitespace.min_content += advance; + } + if self.had_content_yet_for_max_content { + self.pending_whitespace.max_content += advance; + } + continue; + } + if can_wrap { + self.pending_whitespace.max_content += advance; + self.commit_pending_whitespace(); + self.line_break_opportunity(); + continue; + } + } + + self.commit_pending_whitespace(); + self.add_inline_size(advance); + + // Typically whitespace glyphs are placed in a separate store, + // but for `white-space: break-spaces` we place the first whitespace + // with the preceding text. That prevents a line break before that + // first space, but we still need to allow a line break after it. + if can_wrap && run.glyph_store.ends_with_whitespace() { + self.line_break_opportunity(); + } + } + } + }, + InlineItem::Atomic(atomic, offset_in_text, _level) => { + // TODO: need to handle TextWrapMode::Nowrap. + if !inline_formatting_context + .previous_character_prevents_soft_wrap_opportunity(*offset_in_text) + { + self.line_break_opportunity(); + } + + let InlineContentSizesResult { + sizes: outer, + depends_on_block_constraints, + } = atomic.outer_inline_content_sizes( + self.layout_context, + &self.constraint_space.into(), + &LogicalVec2::zero(), + false, /* auto_block_size_stretches_to_containing_block */ + ); + self.depends_on_block_constraints |= depends_on_block_constraints; + + if !inline_formatting_context + .next_character_prevents_soft_wrap_opportunity(*offset_in_text) + { + self.line_break_opportunity(); + } + + self.commit_pending_whitespace(); + self.current_line += outer; + }, + _ => {}, + } + } + + fn add_inline_size(&mut self, l: Au) { + self.current_line.min_content += l; + self.current_line.max_content += l; + } + + fn line_break_opportunity(&mut self) { + // Clear the pending whitespace, assuming that at the end of the line + // it needs to either hang or be removed. If that isn't the case, + // `commit_pending_whitespace()` should be called first. + self.pending_whitespace.min_content = Au::zero(); + let current_min_content = mem::take(&mut self.current_line.min_content); + self.paragraph.min_content.max_assign(current_min_content); + self.had_content_yet_for_min_content = false; + } + + fn forced_line_break(&mut self) { + // Handle the line break for min-content sizes. + self.line_break_opportunity(); + + // Repeat the same logic, but now for max-content sizes. + self.pending_whitespace.max_content = Au::zero(); + let current_max_content = mem::take(&mut self.current_line.max_content); + self.paragraph.max_content.max_assign(current_max_content); + self.had_content_yet_for_max_content = false; + } + + fn commit_pending_whitespace(&mut self) { + self.current_line += mem::take(&mut self.pending_whitespace); + self.had_content_yet_for_min_content = true; + self.had_content_yet_for_max_content = true; + } + + /// Compute the [`ContentSizes`] of the given [`InlineFormattingContext`]. + fn compute( + inline_formatting_context: &InlineFormattingContext, + layout_context: &'layout_data LayoutContext, + constraint_space: &'layout_data ConstraintSpace, + ) -> InlineContentSizesResult { + Self { + layout_context, + constraint_space, + paragraph: ContentSizes::zero(), + current_line: ContentSizes::zero(), + pending_whitespace: ContentSizes::zero(), + had_content_yet_for_min_content: false, + had_content_yet_for_max_content: false, + ending_inline_pbm_stack: Vec::new(), + depends_on_block_constraints: false, + } + .traverse(inline_formatting_context) + } +} + +/// Whether or not this character will rpevent a soft wrap opportunity when it +/// comes before or after an atomic inline element. +/// +/// From <https://www.w3.org/TR/css-text-3/#line-break-details>: +/// +/// > For Web-compatibility there is a soft wrap opportunity before and after each +/// > replaced element or other atomic inline, even when adjacent to a character that +/// > would normally suppress them, including U+00A0 NO-BREAK SPACE. However, with +/// > the exception of U+00A0 NO-BREAK SPACE, there must be no soft wrap opportunity +/// > between atomic inlines and adjacent characters belonging to the Unicode GL, WJ, +/// > or ZWJ line breaking classes. +fn char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character: char) -> bool { + if character == '\u{00A0}' { + return false; + } + let class = linebreak_property(character); + class == XI_LINE_BREAKING_CLASS_GL || + class == XI_LINE_BREAKING_CLASS_WJ || + class == XI_LINE_BREAKING_CLASS_ZWJ +} diff --git a/components/layout/flow/inline/text_run.rs b/components/layout/flow/inline/text_run.rs new file mode 100644 index 00000000000..0d0c6398017 --- /dev/null +++ b/components/layout/flow/inline/text_run.rs @@ -0,0 +1,640 @@ +/* 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::mem; +use std::ops::Range; + +use app_units::Au; +use base::text::is_bidi_control; +use fonts::{ + FontContext, FontRef, GlyphRun, LAST_RESORT_GLYPH_ADVANCE, ShapingFlags, ShapingOptions, +}; +use fonts_traits::ByteIndex; +use log::warn; +use malloc_size_of_derive::MallocSizeOf; +use range::Range as ServoRange; +use servo_arc::Arc; +use style::computed_values::text_rendering::T as TextRendering; +use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; +use style::computed_values::word_break::T as WordBreak; +use style::properties::ComputedValues; +use style::str::char_is_whitespace; +use style::values::computed::OverflowWrap; +use unicode_bidi::{BidiInfo, Level}; +use unicode_script::Script; +use xi_unicode::linebreak_property; + +use super::line_breaker::LineBreaker; +use super::{FontKeyAndMetrics, InlineFormattingContextLayout}; +use crate::fragment_tree::BaseFragmentInfo; + +// These constants are the xi-unicode line breaking classes that are defined in +// `table.rs`. Unfortunately, they are only identified by number. +pub(crate) const XI_LINE_BREAKING_CLASS_CM: u8 = 9; +pub(crate) const XI_LINE_BREAKING_CLASS_GL: u8 = 12; +pub(crate) const XI_LINE_BREAKING_CLASS_ZW: u8 = 28; +pub(crate) const XI_LINE_BREAKING_CLASS_WJ: u8 = 30; +pub(crate) const XI_LINE_BREAKING_CLASS_ZWJ: u8 = 42; + +/// <https://www.w3.org/TR/css-display-3/#css-text-run> +#[derive(Debug, MallocSizeOf)] +pub(crate) struct TextRun { + pub base_fragment_info: BaseFragmentInfo, + #[conditional_malloc_size_of] + pub parent_style: Arc<ComputedValues>, + pub text_range: Range<usize>, + + /// The text of this [`TextRun`] with a font selected, broken into unbreakable + /// segments, and shaped. + pub shaped_text: Vec<TextRunSegment>, + pub selection_range: Option<ServoRange<ByteIndex>>, + #[conditional_malloc_size_of] + pub selected_style: Arc<ComputedValues>, +} + +// There are two reasons why we might want to break at the start: +// +// 1. The line breaker told us that a break was necessary between two separate +// instances of sending text to it. +// 2. We are following replaced content ie `have_deferred_soft_wrap_opportunity`. +// +// In both cases, we don't want to do this if the first character prevents a +// soft wrap opportunity. +#[derive(PartialEq)] +enum SegmentStartSoftWrapPolicy { + Force, + FollowLinebreaker, +} + +#[derive(Debug, MallocSizeOf)] +pub(crate) struct TextRunSegment { + /// The index of this font in the parent [`super::InlineFormattingContext`]'s collection of font + /// information. + pub font_index: usize, + + /// The [`Script`] of this segment. + pub script: Script, + + /// The bidi Level of this segment. + pub bidi_level: Level, + + /// The range of bytes in the parent [`super::InlineFormattingContext`]'s text content. + pub range: Range<usize>, + + /// Whether or not the linebreaker said that we should allow a line break at the start of this + /// segment. + pub break_at_start: bool, + + /// The shaped runs within this segment. + pub runs: Vec<GlyphRun>, +} + +impl TextRunSegment { + fn new(font_index: usize, script: Script, bidi_level: Level, start_offset: usize) -> Self { + Self { + font_index, + script, + bidi_level, + range: start_offset..start_offset, + runs: Vec::new(), + break_at_start: false, + } + } + + /// Update this segment if the Font and Script are compatible. The update will only + /// ever make the Script specific. Returns true if the new Font and Script are + /// compatible with this segment or false otherwise. + fn update_if_compatible( + &mut self, + new_font: &FontRef, + script: Script, + bidi_level: Level, + fonts: &[FontKeyAndMetrics], + font_context: &FontContext, + ) -> bool { + fn is_specific(script: Script) -> bool { + script != Script::Common && script != Script::Inherited + } + + if bidi_level != self.bidi_level { + return false; + } + + let current_font_key_and_metrics = &fonts[self.font_index]; + if new_font.key(font_context) != current_font_key_and_metrics.key || + new_font.descriptor.pt_size != current_font_key_and_metrics.pt_size + { + return false; + } + + if !is_specific(self.script) && is_specific(script) { + self.script = script; + } + script == self.script || !is_specific(script) + } + + fn layout_into_line_items( + &self, + text_run: &TextRun, + mut soft_wrap_policy: SegmentStartSoftWrapPolicy, + ifc: &mut InlineFormattingContextLayout, + ) { + if self.break_at_start && soft_wrap_policy == SegmentStartSoftWrapPolicy::FollowLinebreaker + { + soft_wrap_policy = SegmentStartSoftWrapPolicy::Force; + } + + let mut byte_processed = ByteIndex(0); + for (run_index, run) in self.runs.iter().enumerate() { + ifc.possibly_flush_deferred_forced_line_break(); + + // If this whitespace forces a line break, queue up a hard line break the next time we + // see any content. We don't line break immediately, because we'd like to finish processing + // any ongoing inline boxes before ending the line. + if run.is_single_preserved_newline() { + byte_processed = byte_processed + run.range.length(); + ifc.defer_forced_line_break(); + continue; + } + // Break before each unbreakable run in this TextRun, except the first unless the + // linebreaker was set to break before the first run. + if run_index != 0 || soft_wrap_policy == SegmentStartSoftWrapPolicy::Force { + ifc.process_soft_wrap_opportunity(); + } + ifc.push_glyph_store_to_unbreakable_segment( + run.glyph_store.clone(), + text_run, + self.font_index, + self.bidi_level, + ServoRange::<ByteIndex>::new( + byte_processed + ByteIndex(self.range.start as isize), + ByteIndex(self.range.len() as isize) - byte_processed, + ), + ); + byte_processed = byte_processed + run.range.length(); + } + } + + fn shape_and_push_range( + &mut self, + range: &Range<usize>, + formatting_context_text: &str, + segment_font: &FontRef, + options: &ShapingOptions, + ) { + self.runs.push(GlyphRun { + glyph_store: segment_font.shape_text(&formatting_context_text[range.clone()], options), + range: ServoRange::new( + ByteIndex(range.start as isize), + ByteIndex(range.len() as isize), + ), + }); + } + + /// Shape the text of this [`TextRunSegment`], first finding "words" for the shaper by processing + /// the linebreaks found in the owning [`super::InlineFormattingContext`]. Linebreaks are filtered, + /// based on the style of the parent inline box. + fn shape_text( + &mut self, + parent_style: &ComputedValues, + formatting_context_text: &str, + linebreaker: &mut LineBreaker, + shaping_options: &ShapingOptions, + font: FontRef, + ) { + // Gather the linebreaks that apply to this segment from the inline formatting context's collection + // of line breaks. Also add a simulated break at the end of the segment in order to ensure the final + // piece of text is processed. + let range = self.range.clone(); + let linebreaks = linebreaker.advance_to_linebreaks_in_range(self.range.clone()); + let linebreak_iter = linebreaks.iter().chain(std::iter::once(&range.end)); + + self.runs.clear(); + self.runs.reserve(linebreaks.len()); + self.break_at_start = false; + + let text_style = parent_style.get_inherited_text().clone(); + let can_break_anywhere = text_style.word_break == WordBreak::BreakAll || + text_style.overflow_wrap == OverflowWrap::Anywhere || + text_style.overflow_wrap == OverflowWrap::BreakWord; + + let mut last_slice = self.range.start..self.range.start; + for break_index in linebreak_iter { + if *break_index == self.range.start { + self.break_at_start = true; + continue; + } + + let mut options = *shaping_options; + + // Extend the slice to the next UAX#14 line break opportunity. + let mut slice = last_slice.end..*break_index; + let word = &formatting_context_text[slice.clone()]; + + // Split off any trailing whitespace into a separate glyph run. + let mut whitespace = slice.end..slice.end; + let mut rev_char_indices = word.char_indices().rev().peekable(); + + let mut ends_with_whitespace = false; + let ends_with_newline = rev_char_indices + .peek() + .is_some_and(|&(_, character)| character == '\n'); + if let Some((first_white_space_index, first_white_space_character)) = rev_char_indices + .take_while(|&(_, character)| char_is_whitespace(character)) + .last() + { + ends_with_whitespace = true; + whitespace.start = slice.start + first_white_space_index; + + // If line breaking for a piece of text that has `white-space-collapse: break-spaces` there + // is a line break opportunity *after* every preserved space, but not before. This means + // that we should not split off the first whitespace, unless that white-space is a preserved + // newline. + // + // An exception to this is if the style tells us that we can break in the middle of words. + if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces && + first_white_space_character != '\n' && + !can_break_anywhere + { + whitespace.start += first_white_space_character.len_utf8(); + options + .flags + .insert(ShapingFlags::ENDS_WITH_WHITESPACE_SHAPING_FLAG); + } + + slice.end = whitespace.start; + } + + // If there's no whitespace and `word-break` is set to `keep-all`, try increasing the slice. + // TODO: This should only happen for CJK text. + if !ends_with_whitespace && + *break_index != self.range.end && + text_style.word_break == WordBreak::KeepAll && + !can_break_anywhere + { + continue; + } + + // Only advance the last slice if we are not going to try to expand the slice. + last_slice = slice.start..*break_index; + + // Push the non-whitespace part of the range. + if !slice.is_empty() { + self.shape_and_push_range(&slice, formatting_context_text, &font, &options); + } + + if whitespace.is_empty() { + continue; + } + + options.flags.insert( + ShapingFlags::IS_WHITESPACE_SHAPING_FLAG | + ShapingFlags::ENDS_WITH_WHITESPACE_SHAPING_FLAG, + ); + + // If `white-space-collapse: break-spaces` is active, insert a line breaking opportunity + // between each white space character in the white space that we trimmed off. + if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces { + let start_index = whitespace.start; + for (index, character) in formatting_context_text[whitespace].char_indices() { + let index = start_index + index; + self.shape_and_push_range( + &(index..index + character.len_utf8()), + formatting_context_text, + &font, + &options, + ); + } + continue; + } + + // The breaker breaks after every newline, so either there is none, + // or there is exactly one at the very end. In the latter case, + // split it into a different run. That's because shaping considers + // a newline to have the same advance as a space, but during layout + // we want to treat the newline as having no advance. + if ends_with_newline && whitespace.len() > 1 { + self.shape_and_push_range( + &(whitespace.start..whitespace.end - 1), + formatting_context_text, + &font, + &options, + ); + self.shape_and_push_range( + &(whitespace.end - 1..whitespace.end), + formatting_context_text, + &font, + &options, + ); + } else { + self.shape_and_push_range(&whitespace, formatting_context_text, &font, &options); + } + } + } +} + +impl TextRun { + pub(crate) fn new( + base_fragment_info: BaseFragmentInfo, + parent_style: Arc<ComputedValues>, + text_range: Range<usize>, + selection_range: Option<ServoRange<ByteIndex>>, + selected_style: Arc<ComputedValues>, + ) -> Self { + Self { + base_fragment_info, + parent_style, + text_range, + shaped_text: Vec::new(), + selection_range, + selected_style, + } + } + + pub(super) fn segment_and_shape( + &mut self, + formatting_context_text: &str, + font_context: &FontContext, + linebreaker: &mut LineBreaker, + font_cache: &mut Vec<FontKeyAndMetrics>, + bidi_info: &BidiInfo, + ) { + let inherited_text_style = self.parent_style.get_inherited_text().clone(); + let letter_spacing = inherited_text_style + .letter_spacing + .0 + .resolve(self.parent_style.clone_font().font_size.computed_size()); + let letter_spacing = if letter_spacing.px() != 0. { + Some(app_units::Au::from(letter_spacing)) + } else { + None + }; + + let mut flags = ShapingFlags::empty(); + if letter_spacing.is_some() { + flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG); + } + if inherited_text_style.text_rendering == TextRendering::Optimizespeed { + flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG); + flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG) + } + + let specified_word_spacing = &inherited_text_style.word_spacing; + let style_word_spacing: Option<Au> = specified_word_spacing.to_length().map(|l| l.into()); + + let segments = self + .segment_text_by_font(formatting_context_text, font_context, font_cache, bidi_info) + .into_iter() + .map(|(mut segment, font)| { + let word_spacing = style_word_spacing.unwrap_or_else(|| { + let space_width = font + .glyph_index(' ') + .map(|glyph_id| font.glyph_h_advance(glyph_id)) + .unwrap_or(LAST_RESORT_GLYPH_ADVANCE); + specified_word_spacing.to_used_value(Au::from_f64_px(space_width)) + }); + + let mut flags = flags; + if segment.bidi_level.is_rtl() { + flags.insert(ShapingFlags::RTL_FLAG); + } + let shaping_options = ShapingOptions { + letter_spacing, + word_spacing, + script: segment.script, + flags, + }; + + segment.shape_text( + &self.parent_style, + formatting_context_text, + linebreaker, + &shaping_options, + font, + ); + segment + }) + .collect(); + + let _ = std::mem::replace(&mut self.shaped_text, segments); + } + + /// Take the [`TextRun`]'s text and turn it into [`TextRunSegment`]s. Each segment has a matched + /// font and script. Fonts may differ when glyphs are found in fallback fonts. Fonts are stored + /// in the `font_cache` which is a cache of all font keys and metrics used in this + /// [`super::InlineFormattingContext`]. + fn segment_text_by_font( + &mut self, + formatting_context_text: &str, + font_context: &FontContext, + font_cache: &mut Vec<FontKeyAndMetrics>, + bidi_info: &BidiInfo, + ) -> Vec<(TextRunSegment, FontRef)> { + let font_group = font_context.font_group(self.parent_style.clone_font()); + let mut current: Option<(TextRunSegment, FontRef)> = None; + let mut results = Vec::new(); + + let text_run_text = &formatting_context_text[self.text_range.clone()]; + let char_iterator = TwoCharsAtATimeIterator::new(text_run_text.chars()); + let mut next_byte_index = self.text_range.start; + for (character, next_character) in char_iterator { + let current_byte_index = next_byte_index; + next_byte_index += character.len_utf8(); + + if char_does_not_change_font(character) { + continue; + } + + // If the script and BiDi level do not change, use the current font as the first fallback. This + // can potentially speed up fallback on long font lists or with uncommon scripts which might be + // at the bottom of the list. + let script = Script::from(character); + let bidi_level = bidi_info.levels[current_byte_index]; + let current_font = current.as_ref().and_then(|(text_run_segment, font)| { + if text_run_segment.bidi_level == bidi_level && text_run_segment.script == script { + Some(font.clone()) + } else { + None + } + }); + + let Some(font) = font_group.write().find_by_codepoint( + font_context, + character, + next_character, + current_font, + ) else { + continue; + }; + + // If the existing segment is compatible with the character, keep going. + if let Some(current) = current.as_mut() { + if current.0.update_if_compatible( + &font, + script, + bidi_level, + font_cache, + font_context, + ) { + continue; + } + } + + let font_index = add_or_get_font(&font, font_cache, font_context); + + // Add the new segment and finish the existing one, if we had one. If the first + // characters in the run were control characters we may be creating the first + // segment in the middle of the run (ie the start should be the start of this + // text run's text). + let start_byte_index = match current { + Some(_) => current_byte_index, + None => self.text_range.start, + }; + let new = ( + TextRunSegment::new(font_index, script, bidi_level, start_byte_index), + font, + ); + if let Some(mut finished) = current.replace(new) { + // The end of the previous segment is the start of the next one. + finished.0.range.end = current_byte_index; + results.push(finished); + } + } + + // Either we have a current segment or we only had control character and whitespace. In both + // of those cases, just use the first font. + if current.is_none() { + current = font_group.write().first(font_context).map(|font| { + let font_index = add_or_get_font(&font, font_cache, font_context); + ( + TextRunSegment::new( + font_index, + Script::Common, + Level::ltr(), + self.text_range.start, + ), + font, + ) + }) + } + + // Extend the last segment to the end of the string and add it to the results. + if let Some(mut last_segment) = current.take() { + last_segment.0.range.end = self.text_range.end; + results.push(last_segment); + } + + results + } + + pub(super) fn layout_into_line_items(&self, ifc: &mut InlineFormattingContextLayout) { + if self.text_range.is_empty() { + return; + } + + // If we are following replaced content, we should have a soft wrap opportunity, unless the + // first character of this `TextRun` prevents that soft wrap opportunity. If we see such a + // character it should also override the LineBreaker's indication to break at the start. + let have_deferred_soft_wrap_opportunity = + mem::replace(&mut ifc.have_deferred_soft_wrap_opportunity, false); + let mut soft_wrap_policy = match have_deferred_soft_wrap_opportunity { + true => SegmentStartSoftWrapPolicy::Force, + false => SegmentStartSoftWrapPolicy::FollowLinebreaker, + }; + + for segment in self.shaped_text.iter() { + segment.layout_into_line_items(self, soft_wrap_policy, ifc); + soft_wrap_policy = SegmentStartSoftWrapPolicy::FollowLinebreaker; + } + } +} + +/// Whether or not this character should be able to change the font during segmentation. Certain +/// character are not rendered at all, so it doesn't matter what font we use to render them. They +/// should just be added to the current segment. +fn char_does_not_change_font(character: char) -> bool { + if character.is_control() { + return true; + } + if character == '\u{00A0}' { + return true; + } + if is_bidi_control(character) { + return false; + } + + let class = linebreak_property(character); + class == XI_LINE_BREAKING_CLASS_CM || + class == XI_LINE_BREAKING_CLASS_GL || + class == XI_LINE_BREAKING_CLASS_ZW || + class == XI_LINE_BREAKING_CLASS_WJ || + class == XI_LINE_BREAKING_CLASS_ZWJ +} + +pub(super) fn add_or_get_font( + font: &FontRef, + ifc_fonts: &mut Vec<FontKeyAndMetrics>, + font_context: &FontContext, +) -> usize { + let font_instance_key = font.key(font_context); + for (index, ifc_font_info) in ifc_fonts.iter().enumerate() { + if ifc_font_info.key == font_instance_key && + ifc_font_info.pt_size == font.descriptor.pt_size + { + return index; + } + } + ifc_fonts.push(FontKeyAndMetrics { + metrics: font.metrics.clone(), + key: font_instance_key, + pt_size: font.descriptor.pt_size, + }); + ifc_fonts.len() - 1 +} + +pub(super) fn get_font_for_first_font_for_style( + style: &ComputedValues, + font_context: &FontContext, +) -> Option<FontRef> { + let font = font_context + .font_group(style.clone_font()) + .write() + .first(font_context); + if font.is_none() { + warn!("Could not find font for style: {:?}", style.clone_font()); + } + font +} +pub(crate) struct TwoCharsAtATimeIterator<InputIterator> { + /// The input character iterator. + iterator: InputIterator, + /// The first character to produce in the next run of the iterator. + next_character: Option<char>, +} + +impl<InputIterator> TwoCharsAtATimeIterator<InputIterator> { + fn new(iterator: InputIterator) -> Self { + Self { + iterator, + next_character: None, + } + } +} + +impl<InputIterator> Iterator for TwoCharsAtATimeIterator<InputIterator> +where + InputIterator: Iterator<Item = char>, +{ + type Item = (char, Option<char>); + + fn next(&mut self) -> Option<Self::Item> { + // If the iterator isn't initialized do that now. + if self.next_character.is_none() { + self.next_character = self.iterator.next(); + } + let character = self.next_character?; + self.next_character = self.iterator.next(); + Some((character, self.next_character)) + } +} |