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

28
Cargo.lock generated
View file

@ -1265,7 +1265,7 @@ dependencies = [
[[package]]
name = "derive_common"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
dependencies = [
"darling",
"proc-macro2",
@ -3454,7 +3454,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.5",
]
[[package]]
@ -3625,7 +3625,7 @@ dependencies = [
[[package]]
name = "malloc_size_of"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
dependencies = [
"accountable-refcell",
"app_units",
@ -5221,7 +5221,7 @@ dependencies = [
[[package]]
name = "selectors"
version = "0.24.0"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
dependencies = [
"bitflags 2.5.0",
"cssparser",
@ -5509,7 +5509,7 @@ dependencies = [
[[package]]
name = "servo_arc"
version = "0.2.0"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
dependencies = [
"nodrop",
"serde",
@ -5519,7 +5519,7 @@ dependencies = [
[[package]]
name = "servo_atoms"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
dependencies = [
"string_cache",
"string_cache_codegen",
@ -5717,7 +5717,7 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "size_of_test"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
dependencies = [
"static_assertions",
]
@ -5858,7 +5858,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "static_prefs"
version = "0.1.0"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
[[package]]
name = "strict-num"
@ -5895,7 +5895,7 @@ dependencies = [
[[package]]
name = "style"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
dependencies = [
"app_units",
"arrayvec",
@ -5953,7 +5953,7 @@ dependencies = [
[[package]]
name = "style_config"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
dependencies = [
"lazy_static",
]
@ -5961,7 +5961,7 @@ dependencies = [
[[package]]
name = "style_derive"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
dependencies = [
"darling",
"derive_common",
@ -5992,7 +5992,7 @@ dependencies = [
[[package]]
name = "style_traits"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
dependencies = [
"app_units",
"bitflags 2.5.0",
@ -6356,7 +6356,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "to_shmem"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
dependencies = [
"cssparser",
"servo_arc",
@ -6369,7 +6369,7 @@ dependencies = [
[[package]]
name = "to_shmem_derive"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#5e112c7bc6bf6dc78d2b5eeb0251b5ddb3e6585f"
source = "git+https://github.com/servo/stylo?branch=2024-05-15#71b0b0ac1d42b221fccee9034da06bfbf481f0d0"
dependencies = [
"darling",
"derive_common",

View file

@ -2,108 +2,7 @@
* 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 gfx::text::util::{is_cjk, transform_text, CompressionMode};
#[test]
fn test_transform_compress_none() {
let test_strs = [
" foo bar",
"foo bar ",
"foo\n bar",
"foo \nbar",
" foo bar \nbaz",
"foo bar baz",
"foobarbaz\n\n",
];
let mode = CompressionMode::CompressNone;
for &test in test_strs.iter() {
let mut trimmed_str = String::new();
transform_text(test, mode, true, &mut trimmed_str);
assert_eq!(trimmed_str, test)
}
}
#[test]
fn test_transform_discard_newline() {
let test_strs = [
(" foo bar", " foo bar"),
("foo bar ", "foo bar "),
("foo\n bar", "foo bar"),
("foo \nbar", "foo bar"),
(" foo bar \nbaz", " foo bar baz"),
("foo bar baz", "foo bar baz"),
("foobarbaz\n\n", "foobarbaz"),
];
let mode = CompressionMode::DiscardNewline;
for &(test, oracle) in test_strs.iter() {
let mut trimmed_str = String::new();
transform_text(test, mode, true, &mut trimmed_str);
assert_eq!(trimmed_str, oracle)
}
}
#[test]
fn test_transform_compress_whitespace() {
let test_strs = [
(" foo bar", "foo bar"),
("foo bar ", "foo bar "),
("foo\n bar", "foo\n bar"),
("foo \nbar", "foo \nbar"),
(" foo bar \nbaz", "foo bar \nbaz"),
("foo bar baz", "foo bar baz"),
("foobarbaz\n\n", "foobarbaz\n\n"),
];
let mode = CompressionMode::CompressWhitespace;
for &(test, oracle) in test_strs.iter() {
let mut trimmed_str = String::new();
transform_text(test, mode, true, &mut trimmed_str);
assert_eq!(&*trimmed_str, oracle)
}
}
#[test]
fn test_transform_compress_whitespace_newline() {
let test_strs = vec![
(" foo bar", "foo bar"),
("foo bar ", "foo bar "),
("foo\n bar", "foo bar"),
("foo \nbar", "foo bar"),
(" foo bar \nbaz", "foo bar baz"),
("foo bar baz", "foo bar baz"),
("foobarbaz\n\n", "foobarbaz "),
];
let mode = CompressionMode::CompressWhitespaceNewline;
for &(test, oracle) in test_strs.iter() {
let mut trimmed_str = String::new();
transform_text(test, mode, true, &mut trimmed_str);
assert_eq!(&*trimmed_str, oracle)
}
}
#[test]
fn test_transform_compress_whitespace_newline_no_incoming() {
let test_strs = [
(" foo bar", " foo bar"),
("\nfoo bar", " foo bar"),
("foo bar ", "foo bar "),
("foo\n bar", "foo bar"),
("foo \nbar", "foo bar"),
(" foo bar \nbaz", " foo bar baz"),
("foo bar baz", "foo bar baz"),
("foobarbaz\n\n", "foobarbaz "),
];
let mode = CompressionMode::CompressWhitespaceNewline;
for &(test, oracle) in test_strs.iter() {
let mut trimmed_str = String::new();
transform_text(test, mode, false, &mut trimmed_str);
assert_eq!(trimmed_str, oracle)
}
}
use gfx::text::util::is_cjk;
#[test]
fn test_is_cjk() {

View file

@ -3,6 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cmp::{Ordering, PartialOrd};
use std::sync::Arc;
use std::vec::Vec;
use std::{fmt, mem, u16};
@ -773,3 +774,24 @@ impl<'a> Iterator for GlyphIterator<'a> {
}
}
}
/// A single series of glyphs within a text run.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct GlyphRun {
/// The glyphs.
pub glyph_store: Arc<GlyphStore>,
/// The byte range of characters in the containing run.
pub range: Range<ByteIndex>,
}
impl GlyphRun {
pub fn compare(&self, key: &ByteIndex) -> Ordering {
if *key < self.range.begin() {
Ordering::Greater
} else if *key >= self.range.end() {
Ordering::Less
} else {
Ordering::Equal
}
}
}

View file

@ -5,11 +5,9 @@
use unicode_properties::{emoji, UnicodeEmoji};
pub use crate::text::shaping::Shaper;
pub use crate::text::text_run::TextRun;
pub mod glyph;
pub mod shaping;
pub mod text_run;
pub mod util;
#[derive(Clone, Copy, Debug)]

View file

@ -4,106 +4,6 @@
use ucd::{Codepoint, UnicodeBlock};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CompressionMode {
CompressNone,
CompressWhitespace,
CompressWhitespaceNewline,
DiscardNewline,
}
// ported from Gecko's nsTextFrameUtils::TransformText.
//
// High level TODOs:
//
// * Issue #113: consider incoming text state (arabic, etc)
// and propagate outgoing text state (dual of above)
//
// * Issue #114: record skipped and kept chars for mapping original to new text
//
// * Untracked: various edge cases for bidi, CJK, etc.
pub fn transform_text(
text: &str,
mode: CompressionMode,
incoming_whitespace: bool,
output_text: &mut String,
) -> bool {
let out_whitespace = match mode {
CompressionMode::CompressNone | CompressionMode::DiscardNewline => {
for ch in text.chars() {
if is_discardable_char(ch, mode) {
// TODO: record skipped char
} else {
// TODO: record kept char
if ch == '\t' {
// TODO: set "has tab" flag
}
output_text.push(ch);
}
}
false
},
CompressionMode::CompressWhitespace | CompressionMode::CompressWhitespaceNewline => {
let mut in_whitespace: bool = incoming_whitespace;
for ch in text.chars() {
// TODO: discard newlines between CJK chars
let mut next_in_whitespace: bool = is_in_whitespace(ch, mode);
if !next_in_whitespace {
if is_always_discardable_char(ch) {
// revert whitespace setting, since this char was discarded
next_in_whitespace = in_whitespace;
// TODO: record skipped char
} else {
// TODO: record kept char
output_text.push(ch);
}
} else {
/* next_in_whitespace; possibly add a space char */
if in_whitespace {
// TODO: record skipped char
} else {
// TODO: record kept char
output_text.push(' ');
}
}
// save whitespace context for next char
in_whitespace = next_in_whitespace;
} /* /for str::each_char */
in_whitespace
},
};
return out_whitespace;
fn is_in_whitespace(ch: char, mode: CompressionMode) -> bool {
match (ch, mode) {
(' ', _) => true,
('\t', _) => true,
('\n', CompressionMode::CompressWhitespaceNewline) => true,
(_, _) => false,
}
}
fn is_discardable_char(ch: char, mode: CompressionMode) -> bool {
if is_always_discardable_char(ch) {
return true;
}
match mode {
CompressionMode::DiscardNewline | CompressionMode::CompressWhitespaceNewline => {
ch == '\n'
},
_ => false,
}
}
fn is_always_discardable_char(ch: char) -> bool {
// TODO: check for soft hyphens.
is_bidi_control(ch)
}
}
pub fn float_to_fixed(before: usize, f: f64) -> i32 {
((1i32 << before) as f64 * f) as i32
}

View file

@ -9,7 +9,7 @@ publish = false
[lib]
name = "layout_2013"
path = "lib.rs"
test = false
test = true
doctest = false
[dependencies]

View file

@ -21,7 +21,6 @@ use euclid::default::{Point2D, Rect, SideOffsets2D as UntypedSideOffsets2D, Size
use euclid::{rect, SideOffsets2D};
use fnv::FnvHashMap;
use gfx::text::glyph::ByteIndex;
use gfx::text::TextRun;
use ipc_channel::ipc;
use log::{debug, warn};
use net_traits::image_cache::UsePlaceholder;
@ -73,6 +72,7 @@ use crate::fragment::{
use crate::inline::InlineFragmentNodeFlags;
use crate::model::MaybeAuto;
use crate::table_cell::CollapsedBordersForCell;
use crate::text_run::TextRun;
static THREAD_TINT_COLORS: [ColorF; 8] = [
ColorF {

View file

@ -16,7 +16,6 @@ use bitflags::bitflags;
use canvas_traits::canvas::{CanvasId, CanvasMsg};
use euclid::default::{Point2D, Rect, Size2D, Vector2D};
use gfx::text::glyph::ByteIndex;
use gfx::text::text_run::{TextRun, TextRunSlice};
use html5ever::{local_name, namespace_url, ns};
use ipc_channel::ipc::IpcSender;
use log::debug;
@ -72,6 +71,7 @@ use crate::model::{
self, style_length, IntrinsicISizes, IntrinsicISizesContribution, MaybeAuto, SizeConstraint,
};
use crate::text::TextRunScanner;
use crate::text_run::{TextRun, TextRunSlice};
use crate::wrapper::ThreadSafeLayoutNodeHelpers;
use crate::{text, ServoArc};

View file

@ -1502,7 +1502,9 @@ impl Flow for InlineFlow {
.union_nonbreaking_inline(&intrinsic_sizes_for_fragment)
},
(
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::PreserveBreaks,
WhiteSpaceCollapse::Preserve |
WhiteSpaceCollapse::PreserveBreaks |
WhiteSpaceCollapse::BreakSpaces,
TextWrapMode::Nowrap,
) => {
intrinsic_sizes_for_nonbroken_run
@ -1520,7 +1522,9 @@ impl Flow for InlineFlow {
}
},
(
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::PreserveBreaks,
WhiteSpaceCollapse::Preserve |
WhiteSpaceCollapse::PreserveBreaks |
WhiteSpaceCollapse::BreakSpaces,
TextWrapMode::Wrap,
) => {
// Flush the intrinsic sizes we were gathering up for the nonbroken run, if

View file

@ -36,6 +36,7 @@ mod table_row;
mod table_rowgroup;
mod table_wrapper;
mod text;
mod text_run;
pub mod traversal;
pub mod wrapper;

View file

@ -12,8 +12,7 @@ use app_units::Au;
use gfx::font::{self, FontMetrics, FontRef, RunMetrics, ShapingFlags, ShapingOptions};
use gfx::font_cache_thread::FontIdentifier;
use gfx::text::glyph::ByteIndex;
use gfx::text::text_run::TextRun;
use gfx::text::util::{self, CompressionMode};
use gfx::text::util::is_bidi_control;
use log::{debug, warn};
use range::Range;
use style::computed_values::text_rendering::T as TextRendering;
@ -35,6 +34,7 @@ use crate::fragment::{
};
use crate::inline::{InlineFragmentNodeFlags, InlineFragments};
use crate::linked_list::split_off_head;
use crate::text_run::TextRun;
/// Returns the concatenated text of a list of unscanned text fragments.
fn text(fragments: &LinkedList<Fragment>) -> String {
@ -192,7 +192,9 @@ impl TextRunScanner {
font_group = font_context.font_group(font_style);
compression = match in_fragment.white_space_collapse() {
WhiteSpaceCollapse::Collapse => CompressionMode::CompressWhitespaceNewline,
WhiteSpaceCollapse::Preserve => CompressionMode::CompressNone,
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces => {
CompressionMode::CompressNone
},
WhiteSpaceCollapse::PreserveBreaks => CompressionMode::CompressWhitespace,
};
text_transform = inherited_text_style.text_transform;
@ -712,7 +714,7 @@ impl RunMapping {
) {
let was_empty = *start_position == end_position;
let old_byte_length = run_info.text.len();
*last_whitespace = util::transform_text(
*last_whitespace = transform_text(
&text[(*start_position)..end_position],
compression,
*last_whitespace,
@ -828,3 +830,181 @@ fn is_compatible(a: Script, b: Script) -> bool {
fn is_specific(script: Script) -> bool {
script != Script::Common && script != Script::Inherited
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CompressionMode {
CompressNone,
CompressWhitespace,
CompressWhitespaceNewline,
}
// ported from Gecko's nsTextFrameUtils::TransformText.
//
// High level TODOs:
//
// * Issue #113: consider incoming text state (arabic, etc)
// and propagate outgoing text state (dual of above)
//
// * Issue #114: record skipped and kept chars for mapping original to new text
//
// * Untracked: various edge cases for bidi, CJK, etc.
pub fn transform_text(
text: &str,
mode: CompressionMode,
incoming_whitespace: bool,
output_text: &mut String,
) -> bool {
let out_whitespace = match mode {
CompressionMode::CompressNone => {
for ch in text.chars() {
if is_discardable_char(ch, mode) {
// TODO: record skipped char
} else {
// TODO: record kept char
if ch == '\t' {
// TODO: set "has tab" flag
}
output_text.push(ch);
}
}
false
},
CompressionMode::CompressWhitespace | CompressionMode::CompressWhitespaceNewline => {
let mut in_whitespace: bool = incoming_whitespace;
for ch in text.chars() {
// TODO: discard newlines between CJK chars
let mut next_in_whitespace: bool = is_in_whitespace(ch, mode);
if !next_in_whitespace {
if is_always_discardable_char(ch) {
// revert whitespace setting, since this char was discarded
next_in_whitespace = in_whitespace;
// TODO: record skipped char
} else {
// TODO: record kept char
output_text.push(ch);
}
} else {
/* next_in_whitespace; possibly add a space char */
if in_whitespace {
// TODO: record skipped char
} else {
// TODO: record kept char
output_text.push(' ');
}
}
// save whitespace context for next char
in_whitespace = next_in_whitespace;
} /* /for str::each_char */
in_whitespace
},
};
return out_whitespace;
fn is_in_whitespace(ch: char, mode: CompressionMode) -> bool {
match (ch, mode) {
(' ', _) => true,
('\t', _) => true,
('\n', CompressionMode::CompressWhitespaceNewline) => true,
(_, _) => false,
}
}
fn is_discardable_char(ch: char, mode: CompressionMode) -> bool {
if is_always_discardable_char(ch) {
return true;
}
match mode {
CompressionMode::CompressWhitespaceNewline => ch == '\n',
_ => false,
}
}
fn is_always_discardable_char(ch: char) -> bool {
// TODO: check for soft hyphens.
is_bidi_control(ch)
}
}
#[test]
fn test_transform_compress_none() {
let test_strs = [
" foo bar",
"foo bar ",
"foo\n bar",
"foo \nbar",
" foo bar \nbaz",
"foo bar baz",
"foobarbaz\n\n",
];
let mode = CompressionMode::CompressNone;
for &test in test_strs.iter() {
let mut trimmed_str = String::new();
transform_text(test, mode, true, &mut trimmed_str);
assert_eq!(trimmed_str, test)
}
}
#[test]
fn test_transform_compress_whitespace() {
let test_strs = [
(" foo bar", "foo bar"),
("foo bar ", "foo bar "),
("foo\n bar", "foo\n bar"),
("foo \nbar", "foo \nbar"),
(" foo bar \nbaz", "foo bar \nbaz"),
("foo bar baz", "foo bar baz"),
("foobarbaz\n\n", "foobarbaz\n\n"),
];
let mode = CompressionMode::CompressWhitespace;
for &(test, oracle) in test_strs.iter() {
let mut trimmed_str = String::new();
transform_text(test, mode, true, &mut trimmed_str);
assert_eq!(&*trimmed_str, oracle)
}
}
#[test]
fn test_transform_compress_whitespace_newline() {
let test_strs = vec![
(" foo bar", "foo bar"),
("foo bar ", "foo bar "),
("foo\n bar", "foo bar"),
("foo \nbar", "foo bar"),
(" foo bar \nbaz", "foo bar baz"),
("foo bar baz", "foo bar baz"),
("foobarbaz\n\n", "foobarbaz "),
];
let mode = CompressionMode::CompressWhitespaceNewline;
for &(test, oracle) in test_strs.iter() {
let mut trimmed_str = String::new();
transform_text(test, mode, true, &mut trimmed_str);
assert_eq!(&*trimmed_str, oracle)
}
}
#[test]
fn test_transform_compress_whitespace_newline_no_incoming() {
let test_strs = [
(" foo bar", " foo bar"),
("\nfoo bar", " foo bar"),
("foo bar ", "foo bar "),
("foo\n bar", "foo bar"),
("foo \nbar", "foo bar"),
(" foo bar \nbaz", " foo bar baz"),
("foo bar baz", "foo bar baz"),
("foobarbaz\n\n", "foobarbaz "),
];
let mode = CompressionMode::CompressWhitespaceNewline;
for &(test, oracle) in test_strs.iter() {
let mut trimmed_str = String::new();
transform_text(test, mode, false, &mut trimmed_str);
assert_eq!(trimmed_str, oracle)
}
}

View file

@ -3,11 +3,13 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::Cell;
use std::cmp::{max, Ordering};
use std::cmp::max;
use std::slice::Iter;
use std::sync::Arc;
use app_units::Au;
use gfx::font::{FontMetrics, FontRef, RunMetrics, ShapingFlags, ShapingOptions};
use gfx::text::glyph::{ByteIndex, GlyphRun, GlyphStore};
use log::debug;
use range::Range;
use serde::{Deserialize, Serialize};
@ -16,9 +18,6 @@ use unicode_bidi as bidi;
use webrender_api::FontInstanceKey;
use xi_unicode::LineBreakLeafIter;
use crate::font::{FontMetrics, FontRef, RunMetrics, ShapingFlags, ShapingOptions};
use crate::text::glyph::{ByteIndex, GlyphStore};
thread_local! {
static INDEX_OF_FIRST_GLYPH_RUN_CACHE: Cell<Option<(*const TextRun, ByteIndex, usize)>> =
Cell::new(None)
@ -51,15 +50,6 @@ impl Drop for TextRun {
}
}
/// A single series of glyphs within a text run.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct GlyphRun {
/// The glyphs.
pub glyph_store: Arc<GlyphStore>,
/// The byte range of characters in the containing run.
pub range: Range<ByteIndex>,
}
pub struct NaturalWordSliceIterator<'a> {
glyphs: &'a [GlyphRun],
index: usize,
@ -67,18 +57,6 @@ pub struct NaturalWordSliceIterator<'a> {
reverse: bool,
}
impl GlyphRun {
fn compare(&self, key: &ByteIndex) -> Ordering {
if *key < self.range.begin() {
Ordering::Greater
} else if *key >= self.range.end() {
Ordering::Less
} else {
Ordering::Equal
}
}
}
/// A "slice" of a text run is a series of contiguous glyphs that all belong to the same glyph
/// store. Line breaking strategies yield these.
pub struct TextRunSlice<'a> {

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,11 +259,10 @@ impl TextRun {
script: segment.script,
flags,
};
(segment.runs, segment.break_at_start) =
gfx::text::text_run::TextRun::break_and_shape(
(segment.runs, segment.break_at_start) = break_and_shape(
font,
&self.text
[segment.range.begin().0 as usize..segment.range.end().0 as usize],
&self.text[segment.range.begin().0 as usize..segment.range.end().0 as usize],
&inherited_text_style,
&shaping_options,
linebreaker,
);
@ -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)
}

View file

@ -1,3 +0,0 @@
[white-space-collapse-computed.html]
[Property white-space-collapse value 'break-spaces']
expected: FAIL

View file

@ -1,3 +0,0 @@
[white-space-collapse-valid.html]
[e.style['white-space-collapse'\] = "break-spaces" should set the property value]
expected: FAIL

View file

@ -1,7 +1,3 @@
[white-space-computed.html]
[Property white-space value 'break-spaces' computes to 'break-spaces']
expected: FAIL
[Property white-space value 'break-spaces']
expected: FAIL

View file

@ -1,22 +1,4 @@
[white-space-shorthand.html]
[e.style['white-space'\] = "break-spaces" should set the property value]
expected: FAIL
[Property white-space value 'break-spaces']
expected: FAIL
[e.style['white-space'\] = "break-spaces wrap" should set the property value]
expected: FAIL
[Property white-space value 'break-spaces wrap']
expected: FAIL
[e.style['white-space'\] = "wrap break-spaces" should set the property value]
expected: FAIL
[Property white-space value 'wrap break-spaces']
expected: FAIL
[e.style['white-space'\] = "balance" should set the property value]
expected: FAIL

View file

@ -1,4 +0,0 @@
[white-space-valid.html]
[e.style['white-space'\] = "break-spaces" should set the property value]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-tab-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-tab-002.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[trailing-space-and-text-alignment-004.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[white-space-letter-spacing-001.html]
expected: FAIL

View file

@ -35,24 +35,6 @@
[Property text-wrap inherits]
expected: FAIL
[Property overflow-wrap has initial value normal]
expected: FAIL
[Property overflow-wrap inherits]
expected: FAIL
[Property word-break has initial value normal]
expected: FAIL
[Property word-break inherits]
expected: FAIL
[Property word-wrap has initial value normal]
expected: FAIL
[Property word-wrap inherits]
expected: FAIL
[Property text-wrap-style has initial value auto]
expected: FAIL

View file

@ -1,2 +0,0 @@
[overflow-wrap-anywhere-003.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[overflow-wrap-break-word-003.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[overflow-wrap-break-word-006.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[overflow-wrap-break-word-008.html]
expected: FAIL

View file

@ -0,0 +1,2 @@
[overflow-wrap-normal-keep-all-001.html]
expected: FAIL

View file

@ -1,3 +0,0 @@
[word-wrap-alias.html]
[word-wrap should be defined as an alias of overflow-wrap]
expected: FAIL

View file

@ -1,9 +0,0 @@
[overflow-wrap-computed.html]
[Property overflow-wrap value 'normal']
expected: FAIL
[Property overflow-wrap value 'break-word']
expected: FAIL
[Property overflow-wrap value 'anywhere']
expected: FAIL

View file

@ -1,9 +0,0 @@
[overflow-wrap-valid.html]
[e.style['overflow-wrap'\] = "normal" should set the property value]
expected: FAIL
[e.style['overflow-wrap'\] = "break-word" should set the property value]
expected: FAIL
[e.style['overflow-wrap'\] = "anywhere" should set the property value]
expected: FAIL

View file

@ -1,3 +0,0 @@
[white-space-collapse-computed.html]
[Property white-space-collapse value 'break-spaces']
expected: FAIL

View file

@ -1,3 +0,0 @@
[white-space-collapse-valid.html]
[e.style['white-space-collapse'\] = "break-spaces" should set the property value]
expected: FAIL

View file

@ -1,3 +0,0 @@
[white-space-computed.html]
[Property white-space value 'break-spaces']
expected: FAIL

View file

@ -1,22 +1,4 @@
[white-space-shorthand.html]
[e.style['white-space'\] = "break-spaces" should set the property value]
expected: FAIL
[Property white-space value 'break-spaces']
expected: FAIL
[e.style['white-space'\] = "break-spaces wrap" should set the property value]
expected: FAIL
[Property white-space value 'break-spaces wrap']
expected: FAIL
[e.style['white-space'\] = "wrap break-spaces" should set the property value]
expected: FAIL
[Property white-space value 'wrap break-spaces']
expected: FAIL
[e.style['white-space'\] = "balance" should set the property value]
expected: FAIL

View file

@ -1,3 +0,0 @@
[white-space-valid.html]
[e.style['white-space'\] = "break-spaces" should set the property value]
expected: FAIL

View file

@ -1,13 +1,4 @@
[word-break-computed.html]
[Property word-break value 'normal']
expected: FAIL
[Property word-break value 'keep-all']
expected: FAIL
[Property word-break value 'break-all']
expected: FAIL
[Property word-break value 'break-word']
expected: FAIL

View file

@ -1,13 +1,4 @@
[word-break-valid.html]
[e.style['word-break'\] = "normal" should set the property value]
expected: FAIL
[e.style['word-break'\] = "keep-all" should set the property value]
expected: FAIL
[e.style['word-break'\] = "break-all" should set the property value]
expected: FAIL
[e.style['word-break'\] = "break-word" should set the property value]
expected: FAIL

View file

@ -1,9 +0,0 @@
[word-wrap-computed.html]
[Property word-wrap value 'normal']
expected: FAIL
[Property word-wrap value 'break-word']
expected: FAIL
[Property word-wrap value 'anywhere']
expected: FAIL

View file

@ -1,9 +0,0 @@
[word-wrap-valid.html]
[e.style['word-wrap'\] = "normal" should set the property value]
expected: FAIL
[e.style['word-wrap'\] = "break-word" should set the property value]
expected: FAIL
[e.style['word-wrap'\] = "anywhere" should set the property value]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-003.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-004.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-005.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-007.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-010.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-011.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-051.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-052.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-before-first-char-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-before-first-char-008.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-before-first-char-009.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-before-first-char-010.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-before-first-char-011.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-before-first-char-012.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-before-first-char-013.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-before-first-char-016.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-before-first-char-017.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-before-first-char-018.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-newline-011.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-newline-012.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-newline-013.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-newline-014.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-newline-015.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-newline-016.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-tab-003.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-tab-004.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-with-overflow-wrap-003.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-with-overflow-wrap-004.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-with-overflow-wrap-005.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-with-overflow-wrap-006.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-with-overflow-wrap-007.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-with-overflow-wrap-008.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-with-overflow-wrap-009.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[break-spaces-with-overflow-wrap-010.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[hanging-whitespace-001.tentative.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[textarea-break-spaces-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[white-space-letter-spacing-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[ws-break-spaces-applies-to-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[ws-break-spaces-applies-to-002.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[ws-break-spaces-applies-to-003.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[ws-break-spaces-applies-to-005.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[ws-break-spaces-applies-to-006.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[ws-break-spaces-applies-to-007.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[ws-break-spaces-applies-to-008.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[ws-break-spaces-applies-to-009.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[ws-break-spaces-applies-to-010.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[ws-break-spaces-applies-to-011.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[ws-break-spaces-applies-to-014.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[word-break-keep-all-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[word-break-keep-all-002.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[word-break-keep-all-010.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[word-break-keep-all-006.htm]
expected: FAIL

View file

@ -1,2 +0,0 @@
[word-break-keep-all-007.htm]
expected: FAIL