From ea09c5ba5a0b4ca30f48ca36b6f9842fd5df370e Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Mon, 4 Sep 2023 11:38:40 +0200 Subject: [PATCH] Fix handling of `white-space` property for wrapping (#30259) This still fails some tests because on element boundaries we want the `white-space` property of the first shared ancestor. --- components/layout_2020/flow/inline.rs | 140 ++++++++++++------ .../css/CSS2/floats/float-nowrap-2.html.ini | 2 - .../css/CSS2/floats/float-nowrap-3.html.ini | 2 - .../generated-content/content-174.xht.ini | 2 - .../content-white-space-003.xht.ini | 2 - .../inline-formatting-context-012.xht.ini | 2 - .../css/CSS2/normal-flow/blocks-018.xht.ini | 2 - .../css/CSS2/normal-flow/blocks-019.xht.ini | 2 - .../text/text-align-white-space-001.xht.ini | 2 - .../CSS2/text/white-space-nowrap-001.xht.ini | 2 - .../white-space-nowrap-attribute-001.xht.ini | 2 - ...reak-anywhere-and-white-space-001.html.ini | 2 - ...reak-anywhere-and-white-space-002.html.ini | 2 - ...reak-anywhere-and-white-space-003.html.ini | 2 - .../line-breaking-atomic-nowrap-001.html.ini | 12 -- .../trailing-ideographic-space-009.html.ini | 2 - .../white-space-normal-011.html.ini | 2 + .../white-space-nowrap-011.html.ini | 2 - ...white-space-wrap-after-nowrap-001.html.ini | 2 + .../css/css-ui/text-overflow-001.html.ini | 2 - .../css/css-ui/text-overflow-003.html.ini | 2 - .../text-overflow-ellipsis-width-001.html.ini | 3 - .../meta/css/whitespace_nowrap_a.html.ini | 2 - ...whitespace_nowrap_line_breaking_a.html.ini | 2 + 24 files changed, 101 insertions(+), 96 deletions(-) delete mode 100644 tests/wpt/meta/css/CSS2/floats/float-nowrap-2.html.ini delete mode 100644 tests/wpt/meta/css/CSS2/floats/float-nowrap-3.html.ini delete mode 100644 tests/wpt/meta/css/CSS2/generated-content/content-174.xht.ini delete mode 100644 tests/wpt/meta/css/CSS2/generated-content/content-white-space-003.xht.ini delete mode 100644 tests/wpt/meta/css/CSS2/linebox/inline-formatting-context-012.xht.ini delete mode 100644 tests/wpt/meta/css/CSS2/normal-flow/blocks-018.xht.ini delete mode 100644 tests/wpt/meta/css/CSS2/normal-flow/blocks-019.xht.ini delete mode 100644 tests/wpt/meta/css/CSS2/text/text-align-white-space-001.xht.ini delete mode 100644 tests/wpt/meta/css/CSS2/text/white-space-nowrap-001.xht.ini delete mode 100644 tests/wpt/meta/css/CSS2/text/white-space-nowrap-attribute-001.xht.ini delete mode 100644 tests/wpt/meta/css/css-text/line-break/line-break-anywhere-and-white-space-001.html.ini delete mode 100644 tests/wpt/meta/css/css-text/line-break/line-break-anywhere-and-white-space-002.html.ini delete mode 100644 tests/wpt/meta/css/css-text/line-break/line-break-anywhere-and-white-space-003.html.ini delete mode 100644 tests/wpt/meta/css/css-text/line-breaking/line-breaking-atomic-nowrap-001.html.ini delete mode 100644 tests/wpt/meta/css/css-text/white-space/trailing-ideographic-space-009.html.ini create mode 100644 tests/wpt/meta/css/css-text/white-space/white-space-normal-011.html.ini delete mode 100644 tests/wpt/meta/css/css-text/white-space/white-space-nowrap-011.html.ini create mode 100644 tests/wpt/meta/css/css-text/white-space/white-space-wrap-after-nowrap-001.html.ini delete mode 100644 tests/wpt/meta/css/css-ui/text-overflow-001.html.ini delete mode 100644 tests/wpt/meta/css/css-ui/text-overflow-003.html.ini delete mode 100644 tests/wpt/meta/css/css-ui/text-overflow-ellipsis-width-001.html.ini delete mode 100644 tests/wpt/mozilla/meta/css/whitespace_nowrap_a.html.ini create mode 100644 tests/wpt/mozilla/meta/css/whitespace_nowrap_line_breaking_a.html.ini diff --git a/components/layout_2020/flow/inline.rs b/components/layout_2020/flow/inline.rs index e822c46dd3c..f561ddc4a9a 100644 --- a/components/layout_2020/flow/inline.rs +++ b/components/layout_2020/flow/inline.rs @@ -90,7 +90,9 @@ pub(crate) struct TextRun { struct InlineNestingLevelState<'box_tree> { remaining_boxes: InlineBoxChildIter<'box_tree>, line_items_so_far: Vec, - white_space: WhiteSpace, + /// Whether or not we have processed any content (an atomic element or text) for + /// this inline box on the current line OR any previous line. + has_content: bool, /// 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 @@ -197,6 +199,12 @@ struct InlineFormattingContextState<'box_tree, 'a, 'b> { /// The line breaking state for this inline formatting context. linebreaker: Option, + /// The currently white-space setting of this line. This is stored on the + /// [`InlineFormattingContextState`] because when a soft wrap opportunity is defined + /// by the boundary between two characters, the white-space property of their nearest + /// common ancestor is used. + white_space: WhiteSpace, + partial_inline_boxes_stack: Vec>, current_nesting_level: InlineNestingLevelState<'box_tree>, } @@ -216,7 +224,18 @@ impl<'box_tree, 'a, 'b> InlineFormattingContextState<'box_tree, 'a, 'b> { self.current_line .block_size .max_assign(line_item.block_size()); + self.current_nesting_level.line_items_so_far.push(line_item); + self.current_nesting_level.has_content = true; + self.propagate_current_nesting_level_white_space_style(); + } + + fn propagate_current_nesting_level_white_space_style(&mut self) { + let style = match self.partial_inline_boxes_stack.last() { + Some(partial) => &partial.style, + None => self.containing_block.style, + }; + self.white_space = style.get_inherited_text().white_space; } /// Finish layout of all the partial inline boxes in the current line, @@ -450,53 +469,74 @@ impl<'box_tree, 'a, 'b> InlineFormattingContextState<'box_tree, 'a, 'b> { &mut self, potential_line_size: &Vec2, ) -> 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 + .clone() + } else { + Vec2 { + inline: self.containing_block.inline_size, + block: Length::new(f32::INFINITY), + } + }; + + 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 && self.white_space.allow_wrap(); + // 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 !self.current_line.has_content && self.sequential_layout_state.is_some() { - let new_placement = self.place_line_among_floats(potential_line_size); - self.current_line - .replace_placement_among_floats(new_placement); + // 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.inline_size { return true; } - // If we already have a placement among floats for this line, and the new potential - // line size causes a change in the block position, then we will need a line - // break. This block of code also takes the opportunity to update the placement - // among floats in the case that line does fit at the same block position (because - // its inline start may change). - let old_placement = self.current_line.placement_among_floats.get().cloned(); - if let Some(old_placement) = old_placement { - if potential_line_size.block > old_placement.size.block { - let new_placement = self.place_line_among_floats(potential_line_size); - if new_placement.start_corner.block != old_placement.start_corner.block { - return true; - } else { - self.current_line - .replace_placement_among_floats(new_placement); - return false; - } + // 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. - let available_inline_space = if self.sequential_layout_state.is_some() { - let placement_among_floats = self - .current_line - .placement_among_floats - .get_or_init(|| self.place_line_among_floats(potential_line_size)); - placement_among_floats.size.inline - } else { - self.containing_block.inline_size - }; - - potential_line_size.inline > available_inline_space + inline_would_overflow } } @@ -680,12 +720,13 @@ impl InlineFormattingContext { inline: first_line_inline_start, block: Length::zero(), }), + white_space: containing_block.style.get_inherited_text().white_space, linebreaker: None, partial_inline_boxes_stack: Vec::new(), current_nesting_level: InlineNestingLevelState { remaining_boxes: InlineBoxChildIter::from_formatting_context(self), line_items_so_far: Vec::with_capacity(self.inline_level_boxes.len()), - white_space: containing_block.style.clone_inherited_text().white_space, + has_content: false, text_decoration_line: self.text_decoration_line, }, }; @@ -731,7 +772,17 @@ impl InlineFormattingContext { &mut ifc.current_line.inline_position, true, /* at_end_of_inline_element */ ); - ifc.current_nesting_level = partial.parent_nesting_level + + let had_content = ifc.current_nesting_level.has_content; + ifc.current_nesting_level = partial.parent_nesting_level; + + // 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 had_content { + ifc.propagate_current_nesting_level_white_space_style(); + } } else { // We reached the end of the entire IFC. break; @@ -798,8 +849,6 @@ impl<'box_tree> PartialInlineBoxFragment<'box_tree> { let text_decoration_line = ifc.current_nesting_level.text_decoration_line | style.clone_text_decoration_line(); - let white_space = style.clone_inherited_text().white_space; - PartialInlineBoxFragment { base_fragment_info: inline_box.base_fragment_info, style, @@ -812,7 +861,7 @@ impl<'box_tree> PartialInlineBoxFragment<'box_tree> { this_inline_level_box, ), line_items_so_far: Vec::with_capacity(inline_box.children.len()), - white_space, + has_content: false, text_decoration_line: text_decoration_line, }, ), @@ -987,9 +1036,7 @@ impl IndependentFormattingContext { block: ifc.current_line.block_size.max(size.block), }; - let can_break = ifc.current_nesting_level.white_space.allow_wrap() && - ifc.current_nesting_level.line_items_so_far.len() != 0; - if ifc.new_potential_line_size_causes_line_break(&new_potential_line_size) && can_break { + if ifc.new_potential_line_size_causes_line_break(&new_potential_line_size) { ifc.finish_line_and_reset(layout_context); } @@ -1169,10 +1216,12 @@ impl TextRun { block: new_max_height_of_line, }; - // We can only break the line, if this isn't the first actual content (non-whitespace or - // preserved whitespace) on the line and this isn't the unbreakable run of this text run - // (or we can break at the start according to the text breaker). - let can_break = ifc.current_line.has_content && (break_at_start || run_index != 0); + // If we cannot break at the start according to the text breaker and this is the first + // unbreakable run of glyphs then we cannot break in any case. + // TODO(mrobinson): If this doesn't fit on the current line and there is content we + // need to line break, but this requires rewinding LineItems and adding them to the + // next line. + let can_break = break_at_start || run_index != 0; if ifc.new_potential_line_size_causes_line_break(&new_potential_line_size) && can_break { add_glyphs_to_current_line( @@ -1205,6 +1254,7 @@ impl TextRun { advance_from_text_run += Length::from(run.glyph_store.total_advance()); glyphs.push(run.glyph_store.clone()); ifc.current_line.has_content = true; + ifc.propagate_current_nesting_level_white_space_style(); } add_glyphs_to_current_line( diff --git a/tests/wpt/meta/css/CSS2/floats/float-nowrap-2.html.ini b/tests/wpt/meta/css/CSS2/floats/float-nowrap-2.html.ini deleted file mode 100644 index 7d7dc81b6bd..00000000000 --- a/tests/wpt/meta/css/CSS2/floats/float-nowrap-2.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[float-nowrap-2.html] - expected: FAIL diff --git a/tests/wpt/meta/css/CSS2/floats/float-nowrap-3.html.ini b/tests/wpt/meta/css/CSS2/floats/float-nowrap-3.html.ini deleted file mode 100644 index 36f0336a409..00000000000 --- a/tests/wpt/meta/css/CSS2/floats/float-nowrap-3.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[float-nowrap-3.html] - expected: FAIL diff --git a/tests/wpt/meta/css/CSS2/generated-content/content-174.xht.ini b/tests/wpt/meta/css/CSS2/generated-content/content-174.xht.ini deleted file mode 100644 index ba563d8637a..00000000000 --- a/tests/wpt/meta/css/CSS2/generated-content/content-174.xht.ini +++ /dev/null @@ -1,2 +0,0 @@ -[content-174.xht] - expected: FAIL diff --git a/tests/wpt/meta/css/CSS2/generated-content/content-white-space-003.xht.ini b/tests/wpt/meta/css/CSS2/generated-content/content-white-space-003.xht.ini deleted file mode 100644 index f0f93605ea7..00000000000 --- a/tests/wpt/meta/css/CSS2/generated-content/content-white-space-003.xht.ini +++ /dev/null @@ -1,2 +0,0 @@ -[content-white-space-003.xht] - expected: FAIL diff --git a/tests/wpt/meta/css/CSS2/linebox/inline-formatting-context-012.xht.ini b/tests/wpt/meta/css/CSS2/linebox/inline-formatting-context-012.xht.ini deleted file mode 100644 index 6c2be1ee817..00000000000 --- a/tests/wpt/meta/css/CSS2/linebox/inline-formatting-context-012.xht.ini +++ /dev/null @@ -1,2 +0,0 @@ -[inline-formatting-context-012.xht] - expected: FAIL diff --git a/tests/wpt/meta/css/CSS2/normal-flow/blocks-018.xht.ini b/tests/wpt/meta/css/CSS2/normal-flow/blocks-018.xht.ini deleted file mode 100644 index 1c5cc6facb8..00000000000 --- a/tests/wpt/meta/css/CSS2/normal-flow/blocks-018.xht.ini +++ /dev/null @@ -1,2 +0,0 @@ -[blocks-018.xht] - expected: FAIL diff --git a/tests/wpt/meta/css/CSS2/normal-flow/blocks-019.xht.ini b/tests/wpt/meta/css/CSS2/normal-flow/blocks-019.xht.ini deleted file mode 100644 index 701285001c9..00000000000 --- a/tests/wpt/meta/css/CSS2/normal-flow/blocks-019.xht.ini +++ /dev/null @@ -1,2 +0,0 @@ -[blocks-019.xht] - expected: FAIL diff --git a/tests/wpt/meta/css/CSS2/text/text-align-white-space-001.xht.ini b/tests/wpt/meta/css/CSS2/text/text-align-white-space-001.xht.ini deleted file mode 100644 index 2bd3488d04f..00000000000 --- a/tests/wpt/meta/css/CSS2/text/text-align-white-space-001.xht.ini +++ /dev/null @@ -1,2 +0,0 @@ -[text-align-white-space-001.xht] - expected: FAIL diff --git a/tests/wpt/meta/css/CSS2/text/white-space-nowrap-001.xht.ini b/tests/wpt/meta/css/CSS2/text/white-space-nowrap-001.xht.ini deleted file mode 100644 index 147d334c746..00000000000 --- a/tests/wpt/meta/css/CSS2/text/white-space-nowrap-001.xht.ini +++ /dev/null @@ -1,2 +0,0 @@ -[white-space-nowrap-001.xht] - expected: FAIL diff --git a/tests/wpt/meta/css/CSS2/text/white-space-nowrap-attribute-001.xht.ini b/tests/wpt/meta/css/CSS2/text/white-space-nowrap-attribute-001.xht.ini deleted file mode 100644 index ee0688f90cf..00000000000 --- a/tests/wpt/meta/css/CSS2/text/white-space-nowrap-attribute-001.xht.ini +++ /dev/null @@ -1,2 +0,0 @@ -[white-space-nowrap-attribute-001.xht] - expected: FAIL diff --git a/tests/wpt/meta/css/css-text/line-break/line-break-anywhere-and-white-space-001.html.ini b/tests/wpt/meta/css/css-text/line-break/line-break-anywhere-and-white-space-001.html.ini deleted file mode 100644 index 261a0798809..00000000000 --- a/tests/wpt/meta/css/css-text/line-break/line-break-anywhere-and-white-space-001.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[line-break-anywhere-and-white-space-001.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-text/line-break/line-break-anywhere-and-white-space-002.html.ini b/tests/wpt/meta/css/css-text/line-break/line-break-anywhere-and-white-space-002.html.ini deleted file mode 100644 index 0ee443dab5f..00000000000 --- a/tests/wpt/meta/css/css-text/line-break/line-break-anywhere-and-white-space-002.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[line-break-anywhere-and-white-space-002.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-text/line-break/line-break-anywhere-and-white-space-003.html.ini b/tests/wpt/meta/css/css-text/line-break/line-break-anywhere-and-white-space-003.html.ini deleted file mode 100644 index e3889ce1ae9..00000000000 --- a/tests/wpt/meta/css/css-text/line-break/line-break-anywhere-and-white-space-003.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[line-break-anywhere-and-white-space-003.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-text/line-breaking/line-breaking-atomic-nowrap-001.html.ini b/tests/wpt/meta/css/css-text/line-breaking/line-breaking-atomic-nowrap-001.html.ini deleted file mode 100644 index 141bb2db924..00000000000 --- a/tests/wpt/meta/css/css-text/line-breaking/line-breaking-atomic-nowrap-001.html.ini +++ /dev/null @@ -1,12 +0,0 @@ -[line-breaking-atomic-nowrap-001.html] - [CSS Text Test: Check atomic inline+NBSP quirk does not apply if nowrap] - expected: FAIL - - [CSS Text Test: Check atomic inline+NBSP quirk does not apply if nowrap 1] - expected: FAIL - - [CSS Text Test: Check atomic inline+NBSP quirk does not apply if nowrap 2] - expected: FAIL - - [CSS Text Test: Check atomic inline+NBSP quirk does not apply if nowrap 3] - expected: FAIL diff --git a/tests/wpt/meta/css/css-text/white-space/trailing-ideographic-space-009.html.ini b/tests/wpt/meta/css/css-text/white-space/trailing-ideographic-space-009.html.ini deleted file mode 100644 index c991922a4d4..00000000000 --- a/tests/wpt/meta/css/css-text/white-space/trailing-ideographic-space-009.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[trailing-ideographic-space-009.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-text/white-space/white-space-normal-011.html.ini b/tests/wpt/meta/css/css-text/white-space/white-space-normal-011.html.ini new file mode 100644 index 00000000000..c0ad8b521d7 --- /dev/null +++ b/tests/wpt/meta/css/css-text/white-space/white-space-normal-011.html.ini @@ -0,0 +1,2 @@ +[white-space-normal-011.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-text/white-space/white-space-nowrap-011.html.ini b/tests/wpt/meta/css/css-text/white-space/white-space-nowrap-011.html.ini deleted file mode 100644 index af8cf59b08c..00000000000 --- a/tests/wpt/meta/css/css-text/white-space/white-space-nowrap-011.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[white-space-nowrap-011.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-text/white-space/white-space-wrap-after-nowrap-001.html.ini b/tests/wpt/meta/css/css-text/white-space/white-space-wrap-after-nowrap-001.html.ini new file mode 100644 index 00000000000..b54652e802f --- /dev/null +++ b/tests/wpt/meta/css/css-text/white-space/white-space-wrap-after-nowrap-001.html.ini @@ -0,0 +1,2 @@ +[white-space-wrap-after-nowrap-001.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-ui/text-overflow-001.html.ini b/tests/wpt/meta/css/css-ui/text-overflow-001.html.ini deleted file mode 100644 index a891dd92952..00000000000 --- a/tests/wpt/meta/css/css-ui/text-overflow-001.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[text-overflow-001.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-ui/text-overflow-003.html.ini b/tests/wpt/meta/css/css-ui/text-overflow-003.html.ini deleted file mode 100644 index c708106682b..00000000000 --- a/tests/wpt/meta/css/css-ui/text-overflow-003.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[text-overflow-003.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-ui/text-overflow-ellipsis-width-001.html.ini b/tests/wpt/meta/css/css-ui/text-overflow-ellipsis-width-001.html.ini deleted file mode 100644 index 66bc4507e15..00000000000 --- a/tests/wpt/meta/css/css-ui/text-overflow-ellipsis-width-001.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[text-overflow-ellipsis-width-001.html] - [Ellipsizing should not affect `offsetWidth` of inline boxes.] - expected: FAIL diff --git a/tests/wpt/mozilla/meta/css/whitespace_nowrap_a.html.ini b/tests/wpt/mozilla/meta/css/whitespace_nowrap_a.html.ini deleted file mode 100644 index 895e127ca34..00000000000 --- a/tests/wpt/mozilla/meta/css/whitespace_nowrap_a.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[whitespace_nowrap_a.html] - expected: FAIL diff --git a/tests/wpt/mozilla/meta/css/whitespace_nowrap_line_breaking_a.html.ini b/tests/wpt/mozilla/meta/css/whitespace_nowrap_line_breaking_a.html.ini new file mode 100644 index 00000000000..f044587a204 --- /dev/null +++ b/tests/wpt/mozilla/meta/css/whitespace_nowrap_line_breaking_a.html.ini @@ -0,0 +1,2 @@ +[whitespace_nowrap_line_breaking_a.html] + expected: FAIL