diff options
author | Martin Robinson <mrobinson@igalia.com> | 2024-06-03 16:46:53 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-03 14:46:53 +0000 |
commit | 48ab8d8847eadd0c94f43307860e880d4802a075 (patch) | |
tree | 97245f3828503dc45d5ee95210435e6e618fb060 /components/layout_2020/flow/construct.rs | |
parent | 00b77ce73cc743c56551c43dbbe66362a5f9eb36 (diff) | |
download | servo-48ab8d8847eadd0c94f43307860e880d4802a075.tar.gz servo-48ab8d8847eadd0c94f43307860e880d4802a075.zip |
layout: Add a `InlineFormattingContextBuilder` (#32415)
The main change here is that collapsed and `text-transform`'d text is
computed as it's processed by DOM traversal. This single transformed
text is stored in the root of the `InlineFormattingContext`.
This will eventually allow performing linebreaking and shaping of the
entire inline formatting context at once. Allowing for intelligent
processing of linebreaking and also shaping across elements. This
matches more closely what LayoutNG does.
This shouldn't have any (or negligable) behavioral changes, but will
allow us to prevent linebreaking inside of clusters in a followup
change.
Co-authored-by: Rakhi Sharma <atbrakhi@igalia.com>
Diffstat (limited to 'components/layout_2020/flow/construct.rs')
-rw-r--r-- | components/layout_2020/flow/construct.rs | 393 |
1 files changed, 147 insertions, 246 deletions
diff --git a/components/layout_2020/flow/construct.rs b/components/layout_2020/flow/construct.rs index 586a109e918..bba6e2eec1f 100644 --- a/components/layout_2020/flow/construct.rs +++ b/components/layout_2020/flow/construct.rs @@ -13,6 +13,7 @@ use style::selector_parser::PseudoElement; use style::str::char_is_whitespace; use style::values::specified::text::TextDecorationLine; +use super::inline::construct::InlineFormattingContextBuilder; use super::OutsideMarker; use crate::cell::ArcRefCell; use crate::context::LayoutContext; @@ -21,8 +22,7 @@ use crate::dom_traversal::{ Contents, NodeAndStyleInfo, NonReplacedContents, PseudoElementContentItem, TraversalHandler, }; use crate::flow::float::FloatBox; -use crate::flow::inline::{InlineBox, InlineFormattingContext, InlineLevelBox}; -use crate::flow::text_run::TextRun; +use crate::flow::inline::{InlineFormattingContext, InlineLevelBox}; use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox}; use crate::formatting_contexts::IndependentFormattingContext; use crate::positioned::AbsolutelyPositionedBox; @@ -56,28 +56,6 @@ impl BlockFormattingContext { contains_floats, } } - - pub fn construct_for_text_runs( - runs: impl Iterator<Item = TextRun>, - layout_context: &LayoutContext, - text_decoration_line: TextDecorationLine, - ) -> Self { - let inline_level_boxes = runs - .map(|run| ArcRefCell::new(InlineLevelBox::TextRun(run))) - .collect(); - - let ifc = InlineFormattingContext { - inline_level_boxes, - font_metrics: Vec::new(), - text_decoration_line, - has_first_formatted_line: true, - contains_floats: false, - }; - Self { - contents: BlockContainer::construct_inline_formatting_context(layout_context, ifc), - contains_floats: false, - } - } } struct BlockLevelJob<'dom, Node> { @@ -151,29 +129,14 @@ pub(crate) struct BlockContainerBuilder<'dom, 'style, Node> { /// (see `handle_block_level_element`). block_level_boxes: Vec<BlockLevelJob<'dom, Node>>, - /// The ongoing inline formatting context of the builder. - /// - /// Contains all the complete inline level boxes we found traversing the - /// tree so far. If a block-level box is found during traversal, - /// this inline formatting context is pushed as a block level box to - /// the list of block-level boxes of the builder - /// (see `end_ongoing_inline_formatting_context`). - ongoing_inline_formatting_context: InlineFormattingContext, - - /// 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`). - /// - /// Whenever the end of a DOM element that represents an inline box is - /// reached, the inline box at the top of this stack is complete and ready - /// to be pushed to the children of the next last ongoing inline box - /// the ongoing inline formatting context if the stack is now empty, - /// which means we reached the end of a child of the actual - /// container root (see `move_to_next_sibling`). - ongoing_inline_boxes_stack: Vec<InlineBox>, + /// Whether or not this builder has yet produced a block which would be + /// be considered the first line for the purposes of `text-indent`. + have_already_seen_first_line_for_text_indent: bool, + + /// The propagated [`TextDecorationLine`]. + text_decoration_line: TextDecorationLine, + + inline_formatting_context_builder: InlineFormattingContextBuilder, /// The style of the anonymous block boxes pushed to the list of block-level /// boxes, if any (see `end_ongoing_inline_formatting_context`). @@ -215,16 +178,6 @@ impl BlockContainer { contents.traverse(context, info, &mut builder); builder.finish() } - - pub(super) fn construct_inline_formatting_context( - layout_context: &LayoutContext, - mut ifc: InlineFormattingContext, - ) -> Self { - // TODO(mrobinson): Perhaps it would be better to iteratively break and shape the contents - // of the IFC, and not wait until it is completely built. - ifc.break_and_shape_text(layout_context); - BlockContainer::InlineFormattingContext(ifc) - } } impl<'dom, 'style, Node> BlockContainerBuilder<'dom, 'style, Node> @@ -242,29 +195,33 @@ where context, info, block_level_boxes: Vec::new(), - ongoing_inline_formatting_context: InlineFormattingContext::new( - text_decoration_line, - /* has_first_formatted_line = */ true, - ), - ongoing_inline_boxes_stack: Vec::new(), + text_decoration_line, + have_already_seen_first_line_for_text_indent: false, anonymous_style: None, anonymous_table_content: Vec::new(), + inline_formatting_context_builder: InlineFormattingContextBuilder::new(), } } pub(crate) fn finish(mut self) -> BlockContainer { - debug_assert!(self.ongoing_inline_boxes_stack.is_empty()); + debug_assert!(!self + .inline_formatting_context_builder + .currently_processing_inline_box()); self.finish_anonymous_table_if_needed(); - if !self.ongoing_inline_formatting_context.is_empty() { + if let Some(inline_formatting_context) = self.inline_formatting_context_builder.finish( + self.context, + self.text_decoration_line, + !self.have_already_seen_first_line_for_text_indent, + ) { + // There are two options here. This block was composed of both one or more inline formatting contexts + // and child blocks OR this block was a single inline formatting context. In the latter case, we + // just return the inline formatting context as the block itself. if self.block_level_boxes.is_empty() { - return BlockContainer::construct_inline_formatting_context( - self.context, - self.ongoing_inline_formatting_context, - ); + return BlockContainer::InlineFormattingContext(inline_formatting_context); } - self.end_ongoing_inline_formatting_context(); + self.push_block_level_job_for_inline_formatting_context(inline_formatting_context); } let context = self.context; @@ -288,16 +245,26 @@ where return; } + // From https://drafts.csswg.org/css-tables/#fixup-algorithm: + // > If the box’s parent is an inline, run-in, or ruby box (or any box that would perform + // > inlinification of its children), then an inline-table box must be generated; otherwise + // > it must be a table box. + // + // Note that text content in the inline formatting context isn't enough to force the + // creation of an inline table. It requires the parent to be an inline box. + let inline_table = self + .inline_formatting_context_builder + .currently_processing_inline_box(); + // Text decorations are not propagated to atomic inline-level descendants. // From https://drafts.csswg.org/css2/#lining-striking-props: // > Note that text decorations are not propagated to floating and absolutely // > positioned descendants, nor to the contents of atomic inline-level descendants // > such as inline blocks and inline tables. - let inline_table = !self.ongoing_inline_boxes_stack.is_empty(); let propagated_text_decoration_line = if inline_table { TextDecorationLine::NONE } else { - self.ongoing_inline_formatting_context.text_decoration_line + self.text_decoration_line }; let contents: Vec<AnonymousTableContent<'dom, Node>> = @@ -315,12 +282,11 @@ where ); if inline_table { - self.current_inline_level_boxes() - .push(ArcRefCell::new(InlineLevelBox::Atomic(ifc))); + self.inline_formatting_context_builder.push_atomic(ifc); } else { - self.end_ongoing_inline_formatting_context(); let anonymous_info = self.info.new_anonymous(ifc.style().clone()); let table_block = ArcRefCell::new(BlockLevelBox::Independent(ifc)); + self.end_ongoing_inline_formatting_context(); self.block_level_boxes.push(BlockLevelJob { info: anonymous_info, box_slot: BoxSlot::dummy(), @@ -390,39 +356,23 @@ where } } - fn handle_text(&mut self, info: &NodeAndStyleInfo<Node>, input: Cow<'dom, str>) { - if input.is_empty() { + fn handle_text(&mut self, info: &NodeAndStyleInfo<Node>, text: Cow<'dom, str>) { + if text.is_empty() { return; } // If we are building an anonymous table ie this text directly followed internal // table elements that did not have a `<table>` ancestor, then we forward all // whitespace to the table builder. - if !self.anonymous_table_content.is_empty() && input.chars().all(char_is_whitespace) { + if !self.anonymous_table_content.is_empty() && text.chars().all(char_is_whitespace) { self.anonymous_table_content - .push(AnonymousTableContent::Text(info.clone(), input)); + .push(AnonymousTableContent::Text(info.clone(), text)); return; } else { self.finish_anonymous_table_if_needed(); } - // TODO: We can do better here than `push_str` and wait until we are breaking and - // shaping text to allocate space big enough for the final text. It would require - // collecting all Cow strings into a vector and passing them along to text breaking - // and shaping during final InlineFormattingContext construction. - let inlines = self.current_inline_level_boxes(); - if let Some(mut last_box) = inlines.last_mut().map(|last| last.borrow_mut()) { - if let InlineLevelBox::TextRun(ref mut text_run) = *last_box { - text_run.text.push_str(&input); - return; - } - } - - inlines.push(ArcRefCell::new(InlineLevelBox::TextRun(TextRun::new( - info.into(), - Arc::clone(&info.style), - input.into(), - )))); + self.inline_formatting_context_builder.push_text(text, info); } } @@ -471,45 +421,11 @@ where display_inside: DisplayInside, contents: Contents, ) -> ArcRefCell<InlineLevelBox> { - let box_ = if let (DisplayInside::Flow { is_list_item }, false) = + let (DisplayInside::Flow { is_list_item }, false) = (display_inside, contents.is_replaced()) - { - // We found un inline box. - // Whatever happened before, all we need to do before recurring - // is to remember this ongoing inline level box. - self.ongoing_inline_boxes_stack.push(InlineBox { - base_fragment_info: info.into(), - style: info.style.clone(), - is_first_fragment: true, - is_last_fragment: false, - children: vec![], - default_font_index: None, - }); - - if is_list_item { - if let Some(marker_contents) = crate::lists::make_marker(self.context, info) { - // Ignore `list-style-position` here: - // “If the list item is an inline box: this value is equivalent to `inside`.” - // https://drafts.csswg.org/css-lists/#list-style-position-outside - self.handle_list_item_marker_inside(info, marker_contents) - } - } - - // `unwrap` doesn’t panic here because `is_replaced` returned `false`. - NonReplacedContents::try_from(contents) - .unwrap() - .traverse(self.context, info, self); - - self.finish_anonymous_table_if_needed(); - - let mut inline_box = self - .ongoing_inline_boxes_stack - .pop() - .expect("no ongoing inline level box found"); - inline_box.is_last_fragment = true; - ArcRefCell::new(InlineLevelBox::InlineBox(inline_box)) - } else { - ArcRefCell::new(InlineLevelBox::Atomic( + else { + // If this inline element is an atomic, handle it and return. + return self.inline_formatting_context_builder.push_atomic( IndependentFormattingContext::construct( self.context, info, @@ -518,10 +434,32 @@ where // Text decorations are not propagated to atomic inline-level descendants. TextDecorationLine::NONE, ), - )) + ); }; - self.current_inline_level_boxes().push(box_.clone()); - box_ + + // Otherwise, this is just a normal inline box. Whatever happened before, all we need to do + // before recurring is to remember this ongoing inline level box. + self.inline_formatting_context_builder + .start_inline_box(info); + + if is_list_item { + if let Some(marker_contents) = crate::lists::make_marker(self.context, info) { + // Ignore `list-style-position` here: + // “If the list item is an inline box: this value is equivalent to `inside`.” + // https://drafts.csswg.org/css-lists/#list-style-position-outside + self.handle_list_item_marker_inside(info, marker_contents) + } + } + + // `unwrap` doesn’t panic here because `is_replaced` returned `false`. + NonReplacedContents::try_from(contents) + .unwrap() + .traverse(self.context, info, self); + + self.finish_anonymous_table_if_needed(); + + // Finish the inline box in our IFC builder and return it for `display: contents`. + self.inline_formatting_context_builder.end_inline_box() } fn handle_block_level_element( @@ -532,53 +470,23 @@ where box_slot: BoxSlot<'dom>, ) { // We just found a block level element, all ongoing inline level boxes - // need to be split around it. We iterate on the fragmented inline - // level box stack to take their contents and set their first_fragment - // field to false, for the fragmented inline level boxes that will - // come after the block level element. - let mut fragmented_inline_boxes = - self.ongoing_inline_boxes_stack - .iter_mut() - .rev() - .map(|ongoing| { - let fragmented = InlineBox { - base_fragment_info: ongoing.base_fragment_info, - style: ongoing.style.clone(), - is_first_fragment: ongoing.is_first_fragment, - // The fragmented boxes before the block level element - // are obviously not the last fragment. - is_last_fragment: false, - children: std::mem::take(&mut ongoing.children), - default_font_index: None, - }; - ongoing.is_first_fragment = false; - fragmented - }); - - if let Some(last) = fragmented_inline_boxes.next() { - // There were indeed some ongoing inline level boxes before - // the block, we accumulate them as a single inline level box - // to be pushed to the ongoing inline formatting context. - let mut fragmented_inline = InlineLevelBox::InlineBox(last); - for mut fragmented_parent_inline_box in fragmented_inline_boxes { - fragmented_parent_inline_box - .children - .push(ArcRefCell::new(fragmented_inline)); - fragmented_inline = InlineLevelBox::InlineBox(fragmented_parent_inline_box); - } - - self.ongoing_inline_formatting_context - .inline_level_boxes - .push(ArcRefCell::new(fragmented_inline)); + // need to be split around it. + // + // After calling `split_around_block_and_finish`, + // `self.inline_formatting_context_builder` is set up with the state + // that we want to have after we push the block below. + if let Some(inline_formatting_context) = self + .inline_formatting_context_builder + .split_around_block_and_finish( + self.context, + self.text_decoration_line, + !self.have_already_seen_first_line_for_text_indent, + ) + { + self.push_block_level_job_for_inline_formatting_context(inline_formatting_context); } - let propagated_text_decoration_line = - self.ongoing_inline_formatting_context.text_decoration_line; - - // We found a block level element, so the ongoing inline formatting - // context needs to be ended. - self.end_ongoing_inline_formatting_context(); - + let propagated_text_decoration_line = self.text_decoration_line; let kind = match contents.try_into() { Ok(contents) => match display_inside { DisplayInside::Flow { is_list_item } @@ -612,6 +520,10 @@ where box_slot, kind, }); + + // Any block also counts as the first line for the purposes of text indent. Even if + // they don't actually indent. + self.have_already_seen_first_line_for_text_indent = true; } fn handle_absolutely_positioned_element( @@ -621,28 +533,28 @@ where contents: Contents, box_slot: BoxSlot<'dom>, ) { - if !self.has_ongoing_inline_formatting_context() { - let kind = BlockLevelCreator::OutOfFlowAbsolutelyPositionedBox { - contents, - display_inside, - }; - self.block_level_boxes.push(BlockLevelJob { - info: info.clone(), - box_slot, - kind, - }); - } else { - let box_ = ArcRefCell::new(InlineLevelBox::OutOfFlowAbsolutelyPositionedBox( - ArcRefCell::new(AbsolutelyPositionedBox::construct( + if !self.inline_formatting_context_builder.is_empty() { + let inline_level_box = self + .inline_formatting_context_builder + .push_absolutely_positioned_box(AbsolutelyPositionedBox::construct( self.context, info, display_inside, contents, - )), - )); - self.current_inline_level_boxes().push(box_.clone()); - box_slot.set(LayoutBox::InlineLevel(box_)) + )); + box_slot.set(LayoutBox::InlineLevel(inline_level_box)); + return; } + + let kind = BlockLevelCreator::OutOfFlowAbsolutelyPositionedBox { + contents, + display_inside, + }; + self.block_level_boxes.push(BlockLevelJob { + info: info.clone(), + box_slot, + kind, + }); } fn handle_float_element( @@ -652,56 +564,57 @@ where contents: Contents, box_slot: BoxSlot<'dom>, ) { - if !self.has_ongoing_inline_formatting_context() { - let kind = BlockLevelCreator::OutOfFlowFloatBox { - contents, - display_inside, - }; - self.block_level_boxes.push(BlockLevelJob { - info: info.clone(), - box_slot, - kind, - }); - } else { - let box_ = ArcRefCell::new(InlineLevelBox::OutOfFlowFloatBox(FloatBox::construct( - self.context, - info, - display_inside, - contents, - ))); - self.ongoing_inline_formatting_context.contains_floats = true; - self.current_inline_level_boxes().push(box_.clone()); - box_slot.set(LayoutBox::InlineLevel(box_)) + if !self.inline_formatting_context_builder.is_empty() { + let inline_level_box = + self.inline_formatting_context_builder + .push_float_box(FloatBox::construct( + self.context, + info, + display_inside, + contents, + )); + box_slot.set(LayoutBox::InlineLevel(inline_level_box)); + return; } + + let kind = BlockLevelCreator::OutOfFlowFloatBox { + contents, + display_inside, + }; + self.block_level_boxes.push(BlockLevelJob { + info: info.clone(), + box_slot, + kind, + }); } fn end_ongoing_inline_formatting_context(&mut self) { - if self.ongoing_inline_formatting_context.is_empty() { - // There should never be an empty inline formatting context. - self.ongoing_inline_formatting_context - .has_first_formatted_line = false; - return; + if let Some(inline_formatting_context) = self.inline_formatting_context_builder.finish( + self.context, + self.text_decoration_line, + !self.have_already_seen_first_line_for_text_indent, + ) { + self.push_block_level_job_for_inline_formatting_context(inline_formatting_context); } + } - let context = self.context; + fn push_block_level_job_for_inline_formatting_context( + &mut self, + inline_formatting_context: InlineFormattingContext, + ) { let block_container_style = &self.info.style; + let layout_context = self.context; let anonymous_style = self.anonymous_style.get_or_insert_with(|| { - context + layout_context .shared_context() .stylist .style_for_anonymous::<Node::ConcreteElement>( - &context.shared_context().guards, + &layout_context.shared_context().guards, &PseudoElement::ServoAnonymousBox, block_container_style, ) }); - let mut ifc = InlineFormattingContext::new( - self.ongoing_inline_formatting_context.text_decoration_line, - /* has_first_formatted_line = */ false, - ); - std::mem::swap(&mut self.ongoing_inline_formatting_context, &mut ifc); - let info = self.info.new_anonymous(anonymous_style.clone()); self.block_level_boxes.push(BlockLevelJob { info, @@ -709,24 +622,12 @@ where box_slot: BoxSlot::dummy(), kind: BlockLevelCreator::SameFormattingContextBlock( IntermediateBlockContainer::InlineFormattingContext( - BlockContainer::construct_inline_formatting_context(self.context, ifc), + BlockContainer::InlineFormattingContext(inline_formatting_context), ), ), }); - } - - // Retrieves the mutable reference of inline boxes either from the last - // element of a stack or directly from the formatting context, depending on the situation. - fn current_inline_level_boxes(&mut self) -> &mut Vec<ArcRefCell<InlineLevelBox>> { - match self.ongoing_inline_boxes_stack.last_mut() { - Some(last) => &mut last.children, - None => &mut self.ongoing_inline_formatting_context.inline_level_boxes, - } - } - fn has_ongoing_inline_formatting_context(&self) -> bool { - !self.ongoing_inline_formatting_context.is_empty() || - !self.ongoing_inline_boxes_stack.is_empty() + self.have_already_seen_first_line_for_text_indent = true; } } |