diff options
-rw-r--r-- | components/gfx/text/glyph.rs | 77 | ||||
-rw-r--r-- | components/layout/display_list/builder.rs | 2 | ||||
-rw-r--r-- | components/layout/inline.rs | 2 | ||||
-rw-r--r-- | components/layout_2020/display_list/mod.rs | 4 | ||||
-rw-r--r-- | components/layout_2020/flow/inline.rs | 10 | ||||
-rw-r--r-- | tests/wpt/meta/MANIFEST.json | 17 | ||||
-rw-r--r-- | tests/wpt/tests/css/css-text/text-justify/text-justify-word-separators-ref.html | 28 | ||||
-rw-r--r-- | tests/wpt/tests/css/css-text/text-justify/text-justify-word-separators.html | 47 |
8 files changed, 150 insertions, 37 deletions
diff --git a/components/gfx/text/glyph.rs b/components/gfx/text/glyph.rs index e136ffe81c7..2e30b1aaa95 100644 --- a/components/gfx/text/glyph.rs +++ b/components/gfx/text/glyph.rs @@ -71,7 +71,7 @@ pub type GlyphId = u32; // TODO: make this more type-safe. -const FLAG_CHAR_IS_SPACE: u32 = 0x40000000; +const FLAG_CHAR_IS_WORD_SEPARATOR: u32 = 0x40000000; const FLAG_IS_SIMPLE_GLYPH: u32 = 0x80000000; // glyph advance; in Au's. @@ -112,15 +112,19 @@ impl GlyphEntry { self.value & GLYPH_ID_MASK } - /// True if original char was normal (U+0020) space. Other chars may - /// map to space glyph, but this does not account for them. - fn char_is_space(&self) -> bool { - self.has_flag(FLAG_CHAR_IS_SPACE) + /// True if the original character was a word separator. These include spaces + /// (U+0020), non-breaking spaces (U+00A0), and a few other characters + /// non-exhaustively listed in the specification. Other characters may map to the same + /// glyphs, but this function does not take mapping into account. + /// + /// See https://drafts.csswg.org/css-text/#word-separator. + fn char_is_word_separator(&self) -> bool { + self.has_flag(FLAG_CHAR_IS_WORD_SEPARATOR) } #[inline(always)] - fn set_char_is_space(&mut self) { - self.value |= FLAG_CHAR_IS_SPACE; + fn set_char_is_word_separator(&mut self) { + self.value |= FLAG_CHAR_IS_WORD_SEPARATOR; } fn glyph_count(&self) -> u16 { @@ -384,13 +388,13 @@ impl<'a> GlyphInfo<'a> { } } - pub fn char_is_space(self) -> bool { + pub fn char_is_word_separator(self) -> bool { let (store, entry_i) = match self { GlyphInfo::Simple(store, entry_i) => (store, entry_i), GlyphInfo::Detail(store, entry_i, _) => (store, entry_i), }; - store.char_is_space(entry_i) + store.char_is_word_separator(entry_i) } } @@ -427,8 +431,10 @@ pub struct GlyphStore { /// A cache of the advance of the entire glyph store. total_advance: Au, - /// A cache of the number of spaces in the entire glyph store. - total_spaces: i32, + + /// A cache of the number of word separators in the entire glyph store. + /// See https://drafts.csswg.org/css-text/#word-separator. + total_word_separators: i32, /// Used to check if fast path should be used in glyph iteration. has_detailed_glyphs: bool, @@ -447,7 +453,7 @@ impl<'a> GlyphStore { entry_buffer: vec![GlyphEntry::initial(); length], detail_store: DetailedGlyphStore::new(), total_advance: Au(0), - total_spaces: 0, + total_word_separators: 0, has_detailed_glyphs: false, is_whitespace: is_whitespace, is_rtl: is_rtl, @@ -469,23 +475,28 @@ impl<'a> GlyphStore { self.is_whitespace } + #[inline] + pub fn total_word_separators(&self) -> i32 { + self.total_word_separators + } + pub fn finalize_changes(&mut self) { self.detail_store.ensure_sorted(); - self.cache_total_advance_and_spaces() + self.cache_total_advance_and_word_seperators() } #[inline(never)] - fn cache_total_advance_and_spaces(&mut self) { + fn cache_total_advance_and_word_seperators(&mut self) { let mut total_advance = Au(0); - let mut total_spaces = 0; + let mut total_word_separators = 0; for glyph in self.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), self.len())) { total_advance = total_advance + glyph.advance(); - if glyph.char_is_space() { - total_spaces += 1; + if glyph.char_is_word_separator() { + total_word_separators += 1; } } self.total_advance = total_advance; - self.total_spaces = total_spaces; + self.total_word_separators = total_word_separators; } /// Adds a single glyph. @@ -507,8 +518,20 @@ impl<'a> GlyphStore { GlyphEntry::complex(data.cluster_start, data.ligature_start, 1) }; - if character == ' ' { - entry.set_char_is_space() + // This list is taken from the non-exhaustive list of word separator characters in + // the CSS Text Module Level 3 Spec: + // See https://drafts.csswg.org/css-text/#word-separator + if matches!( + character, + ' ' | + '\u{00A0}' | // non-breaking space + '\u{1361}' | // Ethiopic word space + '\u{10100}' | // Aegean word separator + '\u{10101}' | // Aegean word separator + '\u{1039F}' | // Ugartic word divider + '\u{1091F}' // Phoenician word separator + ) { + entry.set_char_is_word_separator(); } self.entry_buffer[i.to_usize()] = entry; @@ -583,7 +606,7 @@ impl<'a> GlyphStore { let mut index = 0; let mut current_advance = Au(0); for glyph in self.iter_glyphs_for_byte_range(range) { - if glyph.char_is_space() { + if glyph.char_is_word_separator() { current_advance += glyph.advance() + extra_word_spacing } else { current_advance += glyph.advance() @@ -599,7 +622,7 @@ impl<'a> GlyphStore { #[inline] pub fn advance_for_byte_range(&self, range: &Range<ByteIndex>, extra_word_spacing: Au) -> Au { if range.begin() == ByteIndex(0) && range.end() == self.len() { - self.total_advance + extra_word_spacing * self.total_spaces + self.total_advance + extra_word_spacing * self.total_word_separators } else if !self.has_detailed_glyphs { self.advance_for_byte_range_simple_glyphs(range, extra_word_spacing) } else { @@ -615,7 +638,7 @@ impl<'a> GlyphStore { ) -> Au { self.iter_glyphs_for_byte_range(range) .fold(Au(0), |advance, glyph| { - if glyph.char_is_space() { + if glyph.char_is_word_separator() { advance + glyph.advance() + extra_word_spacing } else { advance + glyph.advance() @@ -623,15 +646,15 @@ impl<'a> GlyphStore { }) } - pub fn char_is_space(&self, i: ByteIndex) -> bool { + pub fn char_is_word_separator(&self, i: ByteIndex) -> bool { assert!(i < self.len()); - self.entry_buffer[i.to_usize()].char_is_space() + self.entry_buffer[i.to_usize()].char_is_word_separator() } - pub fn space_count_in_range(&self, range: &Range<ByteIndex>) -> u32 { + pub fn word_separator_count_in_range(&self, range: &Range<ByteIndex>) -> u32 { let mut spaces = 0; for index in range.each_index() { - if self.char_is_space(index) { + if self.char_is_word_separator(index) { spaces += 1 } } diff --git a/components/layout/display_list/builder.rs b/components/layout/display_list/builder.rs index 22a6934e117..6c786115248 100644 --- a/components/layout/display_list/builder.rs +++ b/components/layout/display_list/builder.rs @@ -3036,7 +3036,7 @@ fn convert_text_run_to_glyphs( for slice in text_run.natural_word_slices_in_visual_order(&range) { for glyph in slice.glyphs.iter_glyphs_for_byte_range(&slice.range) { - let glyph_advance = if glyph.char_is_space() { + let glyph_advance = if glyph.char_is_word_separator() { glyph.advance() + text_run.extra_word_spacing } else { glyph.advance() diff --git a/components/layout/inline.rs b/components/layout/inline.rs index e9ca463090e..d8e339d3a0d 100644 --- a/components/layout/inline.rs +++ b/components/layout/inline.rs @@ -1156,7 +1156,7 @@ impl InlineFlow { .run .character_slices_in_range(&fragment_range) { - expansion_opportunities += slice.glyphs.space_count_in_range(&slice.range) + expansion_opportunities += slice.glyphs.word_separator_count_in_range(&slice.range) } } diff --git a/components/layout_2020/display_list/mod.rs b/components/layout_2020/display_list/mod.rs index 78d6e6ff903..588e8d8b311 100644 --- a/components/layout_2020/display_list/mod.rs +++ b/components/layout_2020/display_list/mod.rs @@ -783,7 +783,9 @@ fn glyphs( point, }; glyphs.push(glyph); - } else { + } + + if glyph.char_is_word_separator() { origin.x += justification_adjustment; } origin.x += Length::from(glyph.advance()); diff --git a/components/layout_2020/flow/inline.rs b/components/layout_2020/flow/inline.rs index f67e95400f4..a4d66c85c62 100644 --- a/components/layout_2020/flow/inline.rs +++ b/components/layout_2020/flow/inline.rs @@ -976,10 +976,8 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { if is_non_preserved_whitespace { self.current_line_segment.trailing_whitespace_size = inline_advance; } - - if glyph_store.is_whitespace() { - self.current_line_segment.justification_opportunities += 1; - } + self.current_line_segment.justification_opportunities += + glyph_store.total_word_separators() as usize; match self.current_line_segment.line_items.last_mut() { Some(LineItem::TextRun(text_run)) => { @@ -2124,9 +2122,7 @@ impl TextRunLineItem { .text .iter() .map(|glyph_store| { - if glyph_store.is_whitespace() { - number_of_justification_opportunities += 1 - } + number_of_justification_opportunities += glyph_store.total_word_separators(); Length::from(glyph_store.total_advance()) }) .sum(); diff --git a/tests/wpt/meta/MANIFEST.json b/tests/wpt/meta/MANIFEST.json index 386963ef744..a63b2e1adfc 100644 --- a/tests/wpt/meta/MANIFEST.json +++ b/tests/wpt/meta/MANIFEST.json @@ -250748,6 +250748,19 @@ ], {} ] + ], + "text-justify-word-separators.html": [ + "028b69e40e0c6761187bbb1435873fb5955ff67e", + [ + null, + [ + [ + "/css/css-text/text-justify/text-justify-word-separators-ref.html", + "==" + ] + ], + {} + ] ] }, "text-spacing-trim": { @@ -418199,6 +418212,10 @@ "text-justify-none-001-ref.html": [ "c8500ac9f38a2a6c843e4bfbb383f6d20c77f8c3", [] + ], + "text-justify-word-separators-ref.html": [ + "99154cff3923b181a080c218b6b399dbbc558761", + [] ] }, "text-spacing-trim": { diff --git a/tests/wpt/tests/css/css-text/text-justify/text-justify-word-separators-ref.html b/tests/wpt/tests/css/css-text/text-justify/text-justify-word-separators-ref.html new file mode 100644 index 00000000000..99154cff392 --- /dev/null +++ b/tests/wpt/tests/css/css-text/text-justify/text-justify-word-separators-ref.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <link rel="author" title="Martin Robinson" href="mailto:mrobinson@igalia.com"> + <link rel="help" href="https://drafts.csswg.org/css-text/#word-separator"> + <meta name="assert" content="text-justify:inter-word should adjust spacing at all word separators."> + <link rel="stylesheet" type="text/css" href="/fonts/ahem.css" /> + <style> + .justified { + font: 10px/1 Ahem; + text-align: justify; + text-justify: inter-word; + width: 120px; + border: solid 1px black; + } + </style> +</head> +<body> + <div class="justified">XXXX XXXX XXXX</div> + <div class="justified">XXXX XXXX XXXX</div> + <div class="justified">XXXX XXXX XXXX</div> + <div class="justified">XXXX XXXX XXXX</div> + <div class="justified">XXXX XXXX XXXX</div> + <div class="justified">XXXX XXXX XXXX</div> + <div class="justified">XXXX XXXX XXXX</div> +</body> +</html> diff --git a/tests/wpt/tests/css/css-text/text-justify/text-justify-word-separators.html b/tests/wpt/tests/css/css-text/text-justify/text-justify-word-separators.html new file mode 100644 index 00000000000..028b69e40e0 --- /dev/null +++ b/tests/wpt/tests/css/css-text/text-justify/text-justify-word-separators.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>CSS Text 6.4. Justification Method: text-justify: inter-word</title> + <link rel="author" title="Martin Robinson" href="mailto:mrobinson@igalia.com"> + <link rel="help" href="https://drafts.csswg.org/css-text/#word-separator"> + <link rel='match' href='text-justify-word-separators-ref.html'> + <meta name="assert" content="text-justify:inter-word should adjust spacing at all word separators."> + <link rel="stylesheet" type="text/css" href="/fonts/ahem.css" /> + <style> + .justified { + font: 10px/1 Ahem; + text-align: justify; + text-justify: inter-word; + width: 120px; + border: solid 1px black; + } + /* Hide the word separators, in case the system doesn't + have an appropriate font installed and shows tofu. + Justification should still work in this case. */ + .hidden { + color: transparent; + } + </style> +</head> +<body> + <!-- A normal space --> + <div class="justified">XXXX XXXX XXXX</div> + + <!-- Non-breaking space --> + <div class="justified">XXXX<span class="hidden"> </span>XXXX XXXX</div> + + <!-- Ethiopic word space --> + <div class="justified">XXXX<span class="hidden">፡</span>XXXX XXXX</div> + + <!-- Aegean word separators --> + <div class="justified">XXXX<span class="hidden">𐄀</span>XXXX XXXX</div> + <div class="justified">XXXX<span class="hidden">𐄁</span>XXXX XXXX</div> + + <!-- Ugaritic word divider --> + <div class="justified">XXXX<span class="hidden">𐎟</span>XXXX XXXX</div> + + <!-- Phoenician word separator --> + <div class="justified">XXXX<span class="hidden">𐤟</span>XXXX XXXX</div> +</body> +</html> |