layout: Add support for white-space-collapse: break-spaces (#32388)

This change adds support for `white-space-collapse: break-spaces` and
adds initial parsing support for `overflow-wrap` and `word-break`. The
later two properties are not fully supported, only in their interaction
with `break-spaces`. This is a preliminary change preparing to implement
them.

In addition, `break_and_shape` is now forked and added to Layout 2020.
This function is going to change a lot soon and forking is preparation
for this. More code that is only used by Layout 2013 is moved from `gfx`
to that crate.

Co-authored-by: Rakhi Sharma <atbrakhi@igalia.com>
This commit is contained in:
Martin Robinson 2024-05-30 07:33:07 +02:00 committed by GitHub
parent c0dedf06d6
commit 60b4b6c9f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
96 changed files with 410 additions and 537 deletions

View file

@ -1361,7 +1361,7 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> {
inline_size: Length,
flags: SegmentContentFlags,
) {
if flags.is_collapsible_whitespace() || flags.is_wrappable_whitespace() {
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 = Length::zero();
@ -1497,7 +1497,7 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> {
bitflags! {
pub struct SegmentContentFlags: u8 {
const COLLAPSIBLE_WHITESPACE = 0b00000001;
const WRAPPABLE_WHITESPACE = 0b00000010;
const WRAPPABLE_AND_HANGABLE_WHITESPACE = 0b00000010;
}
}
@ -1506,19 +1506,30 @@ impl SegmentContentFlags {
self.contains(Self::COLLAPSIBLE_WHITESPACE)
}
fn is_wrappable_whitespace(&self) -> bool {
self.contains(Self::WRAPPABLE_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();
if style_text.white_space_collapse != WhiteSpaceCollapse::Preserve {
// 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);
}
if style_text.text_wrap_mode == TextWrapMode::Wrap {
flags.insert(Self::WRAPPABLE_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
}

View file

@ -152,9 +152,10 @@ pub(super) struct TextRunLineItem {
impl TextRunLineItem {
fn trim_whitespace_at_end(&mut self, whitespace_trimmed: &mut Length) -> bool {
if self.parent_style.get_inherited_text().white_space_collapse ==
WhiteSpaceCollapse::Preserve
{
if matches!(
self.parent_style.get_inherited_text().white_space_collapse,
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces
) {
return false;
}
@ -177,9 +178,10 @@ impl TextRunLineItem {
}
fn trim_whitespace_at_start(&mut self, whitespace_trimmed: &mut Length) -> bool {
if self.parent_style.get_inherited_text().white_space_collapse ==
WhiteSpaceCollapse::Preserve
{
if matches!(
self.parent_style.get_inherited_text().white_space_collapse,
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces
) {
return false;
}

View file

@ -9,7 +9,7 @@ use app_units::Au;
use gfx::font::{FontRef, ShapingFlags, ShapingOptions};
use gfx::font_cache_thread::FontCacheThread;
use gfx::font_context::FontContext;
use gfx::text::text_run::GlyphRun;
use gfx::text::glyph::GlyphRun;
use gfx_traits::ByteIndex;
use log::warn;
use range::Range;
@ -18,7 +18,10 @@ 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::style_structs::InheritedText;
use style::properties::ComputedValues;
use style::str::char_is_whitespace;
use style::values::computed::OverflowWrap;
use style::values::specified::text::TextTransformCase;
use style::values::specified::TextTransform;
use unicode_script::Script;
@ -256,14 +259,13 @@ impl TextRun {
script: segment.script,
flags,
};
(segment.runs, segment.break_at_start) =
gfx::text::text_run::TextRun::break_and_shape(
font,
&self.text
[segment.range.begin().0 as usize..segment.range.end().0 as usize],
&shaping_options,
linebreaker,
);
(segment.runs, segment.break_at_start) = break_and_shape(
font,
&self.text[segment.range.begin().0 as usize..segment.range.end().0 as usize],
&inherited_text_style,
&shaping_options,
linebreaker,
);
segment
})
@ -590,8 +592,17 @@ where
// > 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 {
return self.char_iterator.next();
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() {
@ -830,3 +841,129 @@ where
return Some((character, self.next_character.clone()));
}
}
pub fn break_and_shape(
font: FontRef,
text: &str,
text_style: &InheritedText,
shaping_options: &ShapingOptions,
breaker: &mut Option<LineBreakLeafIter>,
) -> (Vec<GlyphRun>, bool) {
let mut glyphs = vec![];
if breaker.is_none() {
if text.is_empty() {
return (glyphs, true);
}
*breaker = Some(LineBreakLeafIter::new(text, 0));
}
let breaker = breaker.as_mut().unwrap();
let mut push_range = |range: &std::ops::Range<usize>, options: &ShapingOptions| {
glyphs.push(GlyphRun {
glyph_store: font.shape_text(&text[range.clone()], options),
range: Range::new(
ByteIndex(range.start as isize),
ByteIndex(range.len() as isize),
),
});
};
let can_break_anywhere = text_style.word_break == WordBreak::BreakAll ||
text_style.overflow_wrap == OverflowWrap::Anywhere ||
text_style.overflow_wrap == OverflowWrap::BreakWord;
let mut break_at_zero = false;
let mut last_slice_end = 0;
while last_slice_end != text.len() {
let (break_index, _is_hard_break) = breaker.next(text);
if break_index == 0 {
break_at_zero = true;
}
// Extend the slice to the next UAX#14 line break opportunity.
let mut slice = last_slice_end..break_index;
let word = &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 ends_with_newline = rev_char_indices.peek().map_or(false, |&(_, c)| c == '\n');
if let Some((first_white_space_index, first_white_space_character)) = rev_char_indices
.take_while(|&(_, c)| char_is_whitespace(c))
.last()
{
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();
}
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.
let can_break_anywhere = text_style.word_break == WordBreak::BreakAll ||
text_style.overflow_wrap == OverflowWrap::Anywhere ||
text_style.overflow_wrap == OverflowWrap::BreakWord;
if whitespace.is_empty() &&
break_index != text.len() &&
text_style.word_break == WordBreak::KeepAll &&
!can_break_anywhere
{
continue;
}
// Only advance the last_slice_end if we are not going to try to expand the slice.
last_slice_end = break_index;
// Push the non-whitespace part of the range.
if !slice.is_empty() {
push_range(&slice, shaping_options);
}
if whitespace.is_empty() {
continue;
}
let mut options = *shaping_options;
options
.flags
.insert(ShapingFlags::IS_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 text[whitespace].char_indices() {
let index = start_index + index;
push_range(&(index..index + character.len_utf8()), &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 {
push_range(&(whitespace.start..whitespace.end - 1), &options);
push_range(&(whitespace.end - 1..whitespace.end), &options);
} else {
push_range(&whitespace, &options);
}
}
(glyphs, break_at_zero)
}