mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
layout: Linebreak the entire InlineFormattingContext at once (#32483)
Instead of linebreaking inside each single-font text segment, linebreak the entire inline formatting context at once. This has several benefits: 1. It allows us to use `icu_segmenter` (already in use from style), which is written against a newer version of the Unicode spec -- preventing breaking emoji clusters. 2. Opens up the possibility of changing the way that linebreaking and shaping work -- eventually allowing shaping across inline box boundaries and line breaking *after* shaping. Co-authored-by: Rakhi Sharma <atbrakhi@igalia.com>
This commit is contained in:
parent
43a7dd5da0
commit
bae9f6d844
50 changed files with 356 additions and 220 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3338,6 +3338,7 @@ dependencies = [
|
|||
"gfx",
|
||||
"gfx_traits",
|
||||
"html5ever",
|
||||
"icu_segmenter",
|
||||
"ipc-channel",
|
||||
"lazy_static",
|
||||
"log",
|
||||
|
|
|
@ -55,6 +55,7 @@ http = "0.2"
|
|||
hyper = "0.14"
|
||||
hyper-rustls = { version = "0.24", default-features = false, features = ["acceptor", "http1", "http2", "logging", "tls12", "webpki-tokio"] }
|
||||
hyper_serde = { path = "components/hyper_serde" }
|
||||
icu_segmenter = "1.5.0"
|
||||
image = "0.24"
|
||||
imsz = "0.2"
|
||||
indexmap = { version = "2.2.6", features = ["std"] }
|
||||
|
|
|
@ -9,7 +9,7 @@ publish = false
|
|||
[lib]
|
||||
name = "layout_2020"
|
||||
path = "lib.rs"
|
||||
test = false
|
||||
test = true
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
|
@ -26,6 +26,7 @@ fxhash = { workspace = true }
|
|||
gfx = { path = "../gfx" }
|
||||
gfx_traits = { workspace = true }
|
||||
html5ever = { workspace = true }
|
||||
icu_segmenter = { workspace = true }
|
||||
ipc-channel = { workspace = true }
|
||||
log = { workspace = true }
|
||||
net_traits = { workspace = true }
|
||||
|
|
|
@ -131,8 +131,16 @@ impl InlineFormattingContextBuilder {
|
|||
ArcRefCell::new(InlineLevelBox::Atomic(independent_formatting_context));
|
||||
self.current_inline_level_boxes()
|
||||
.push(inline_level_box.clone());
|
||||
|
||||
// Push an object replacement character for this atomic, which will ensure that the line breaker
|
||||
// inserts a line breaking opportunity here.
|
||||
let string_to_push = "\u{fffc}";
|
||||
self.text_segments.push(string_to_push.to_owned());
|
||||
self.current_text_offset += string_to_push.len();
|
||||
|
||||
self.last_inline_box_ended_with_collapsible_white_space = false;
|
||||
self.on_word_boundary = true;
|
||||
|
||||
inline_level_box
|
||||
}
|
||||
|
||||
|
|
120
components/layout_2020/flow/inline/line_breaker.rs
Normal file
120
components/layout_2020/flow/inline/line_breaker.rs
Normal file
|
@ -0,0 +1,120 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 std::ops::Range;
|
||||
|
||||
use icu_segmenter::LineSegmenter;
|
||||
|
||||
pub(crate) struct LineBreaker {
|
||||
linebreaks: Vec<usize>,
|
||||
current_offset: usize,
|
||||
}
|
||||
|
||||
impl LineBreaker {
|
||||
pub(crate) fn new(string: &str) -> Self {
|
||||
let line_segmenter = LineSegmenter::new_auto();
|
||||
Self {
|
||||
// From https://docs.rs/icu_segmenter/1.5.0/icu_segmenter/struct.LineSegmenter.html
|
||||
// > For consistency with the grapheme, word, and sentence segmenters, there is always a
|
||||
// > breakpoint returned at index 0, but this breakpoint is not a meaningful line break
|
||||
// > opportunity.
|
||||
//
|
||||
// Skip this first line break opportunity, as it isn't interesting to us.
|
||||
linebreaks: line_segmenter.segment_str(string).skip(1).collect(),
|
||||
current_offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn advance_to_linebreaks_in_range(&mut self, text_range: Range<usize>) -> &[usize] {
|
||||
let linebreaks_in_range = self.linebreaks_in_range_after_current_offset(text_range);
|
||||
self.current_offset = linebreaks_in_range.end;
|
||||
&self.linebreaks[linebreaks_in_range]
|
||||
}
|
||||
|
||||
fn linebreaks_in_range_after_current_offset(&self, text_range: Range<usize>) -> Range<usize> {
|
||||
assert!(text_range.start <= text_range.end);
|
||||
|
||||
let mut linebreaks_range = self.current_offset..self.linebreaks.len();
|
||||
|
||||
while self.linebreaks[linebreaks_range.start] < text_range.start &&
|
||||
linebreaks_range.len() > 1
|
||||
{
|
||||
linebreaks_range.start += 1;
|
||||
}
|
||||
|
||||
let mut ending_linebreak_index = linebreaks_range.start;
|
||||
while self.linebreaks[ending_linebreak_index] < text_range.end &&
|
||||
ending_linebreak_index < self.linebreaks.len() - 1
|
||||
{
|
||||
ending_linebreak_index += 1;
|
||||
}
|
||||
linebreaks_range.end = ending_linebreak_index;
|
||||
linebreaks_range
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_linebreaker_ranges() {
|
||||
let linebreaker = LineBreaker::new("abc def");
|
||||
assert_eq!(linebreaker.linebreaks, [4, 7]);
|
||||
assert_eq!(
|
||||
linebreaker.linebreaks_in_range_after_current_offset(0..5),
|
||||
0..1
|
||||
);
|
||||
// The last linebreak should not be included for the text range we are interested in.
|
||||
assert_eq!(
|
||||
linebreaker.linebreaks_in_range_after_current_offset(0..7),
|
||||
0..1
|
||||
);
|
||||
|
||||
let linebreaker = LineBreaker::new("abc d def");
|
||||
assert_eq!(linebreaker.linebreaks, [4, 6, 9]);
|
||||
assert_eq!(
|
||||
linebreaker.linebreaks_in_range_after_current_offset(0..5),
|
||||
0..1
|
||||
);
|
||||
assert_eq!(
|
||||
linebreaker.linebreaks_in_range_after_current_offset(0..7),
|
||||
0..2
|
||||
);
|
||||
assert_eq!(
|
||||
linebreaker.linebreaks_in_range_after_current_offset(0..9),
|
||||
0..2
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
linebreaker.linebreaks_in_range_after_current_offset(4..9),
|
||||
0..2
|
||||
);
|
||||
|
||||
std::panic::catch_unwind(|| {
|
||||
let linebreaker = LineBreaker::new("abc def");
|
||||
linebreaker.linebreaks_in_range_after_current_offset(5..2);
|
||||
})
|
||||
.expect_err("Reversed range should cause an assertion failure.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_linebreaker_stateful_advance() {
|
||||
let mut linebreaker = LineBreaker::new("abc d def");
|
||||
assert_eq!(linebreaker.linebreaks, [4, 6, 9]);
|
||||
assert!(linebreaker.advance_to_linebreaks_in_range(0..7) == &[4, 6]);
|
||||
assert!(linebreaker.advance_to_linebreaks_in_range(8..9).is_empty());
|
||||
|
||||
// We've already advanced, so a range from the beginning shouldn't affect things.
|
||||
assert!(linebreaker.advance_to_linebreaks_in_range(0..9).is_empty());
|
||||
|
||||
linebreaker.current_offset = 0;
|
||||
|
||||
// Sending a value out of range shoudn't break things.
|
||||
assert!(linebreaker.advance_to_linebreaks_in_range(0..999) == &[4, 6]);
|
||||
|
||||
linebreaker.current_offset = 0;
|
||||
|
||||
std::panic::catch_unwind(|| {
|
||||
let mut linebreaker = LineBreaker::new("abc d def");
|
||||
linebreaker.advance_to_linebreaks_in_range(2..0);
|
||||
})
|
||||
.expect_err("Reversed range should cause an assertion failure.");
|
||||
}
|
|
@ -70,6 +70,7 @@
|
|||
|
||||
pub mod construct;
|
||||
pub mod line;
|
||||
mod line_breaker;
|
||||
pub mod text_run;
|
||||
|
||||
use std::cell::OnceCell;
|
||||
|
@ -84,6 +85,7 @@ use line::{
|
|||
layout_line_items, AbsolutelyPositionedLineItem, AtomicLineItem, FloatLineItem,
|
||||
InlineBoxLineItem, LineItem, LineItemLayoutState, LineMetrics, TextRunLineItem,
|
||||
};
|
||||
use line_breaker::LineBreaker;
|
||||
use serde::Serialize;
|
||||
use servo_arc::Arc;
|
||||
use style::computed_values::text_wrap_mode::T as TextWrapMode;
|
||||
|
@ -1593,13 +1595,13 @@ impl InlineFormattingContext {
|
|||
let text_content: String = builder.text_segments.into_iter().collect();
|
||||
let mut font_metrics = Vec::new();
|
||||
|
||||
let mut linebreaker = None;
|
||||
let mut new_linebreaker = LineBreaker::new(text_content.as_str());
|
||||
inline_formatting_context.foreach(|iter_item| match iter_item {
|
||||
InlineFormattingContextIterItem::Item(InlineLevelBox::TextRun(ref mut text_run)) => {
|
||||
text_run.break_and_shape(
|
||||
&text_content[text_run.text_range.clone()],
|
||||
text_run.segment_and_shape(
|
||||
&text_content,
|
||||
&layout_context.font_context,
|
||||
&mut linebreaker,
|
||||
&mut new_linebreaker,
|
||||
&mut font_metrics,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -18,13 +18,13 @@ 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 unicode_script::Script;
|
||||
use xi_unicode::{linebreak_property, LineBreakLeafIter};
|
||||
use xi_unicode::linebreak_property;
|
||||
|
||||
use super::line_breaker::LineBreaker;
|
||||
use super::{FontKeyAndMetrics, InlineFormattingContextState};
|
||||
use crate::fragment_tree::BaseFragmentInfo;
|
||||
|
||||
|
@ -84,8 +84,8 @@ pub(crate) struct TextRunSegment {
|
|||
#[serde(skip_serializing)]
|
||||
pub script: Script,
|
||||
|
||||
/// The range of bytes in the [`TextRun`]'s text that this segment covers.
|
||||
pub range: ServoRange<ByteIndex>,
|
||||
/// The range of bytes in the parent [`super::InlineFormattingContext`]'s text content.
|
||||
pub range: Range<usize>,
|
||||
|
||||
/// Whether or not the linebreaker said that we should allow a line break at the start of this
|
||||
/// segment.
|
||||
|
@ -96,11 +96,11 @@ pub(crate) struct TextRunSegment {
|
|||
}
|
||||
|
||||
impl TextRunSegment {
|
||||
fn new(font_index: usize, script: Script, byte_index: ByteIndex) -> Self {
|
||||
fn new(font_index: usize, script: Script, start_offset: usize) -> Self {
|
||||
Self {
|
||||
script,
|
||||
font_index,
|
||||
range: ServoRange::new(byte_index, ByteIndex(0)),
|
||||
range: start_offset..start_offset,
|
||||
runs: Vec::new(),
|
||||
break_at_start: false,
|
||||
}
|
||||
|
@ -167,6 +167,158 @@ impl TextRunSegment {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn shape_and_push_range(
|
||||
&mut self,
|
||||
range: &Range<usize>,
|
||||
formatting_context_text: &str,
|
||||
segment_font: &FontRef,
|
||||
options: &ShapingOptions,
|
||||
) {
|
||||
self.runs.push(GlyphRun {
|
||||
glyph_store: segment_font.shape_text(&formatting_context_text[range.clone()], options),
|
||||
range: ServoRange::new(
|
||||
ByteIndex(range.start as isize),
|
||||
ByteIndex(range.len() as isize),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
/// Shape the text of this [`TextRunSegment`], first finding "words" for the shaper by processing
|
||||
/// the linebreaks found in the owning [`super::InlineFormattingContext`]. Linebreaks are filtered,
|
||||
/// based on the style of the parent inline box.
|
||||
fn shape_text(
|
||||
&mut self,
|
||||
parent_style: &ComputedValues,
|
||||
formatting_context_text: &str,
|
||||
linebreaker: &mut LineBreaker,
|
||||
shaping_options: &ShapingOptions,
|
||||
font: FontRef,
|
||||
) {
|
||||
// Gather the linebreaks that apply to this segment from the inline formatting context's collection
|
||||
// of line breaks. Also add a simulated break at the end of the segment in order to ensure the final
|
||||
// piece of text is processed.
|
||||
let range = self.range.clone();
|
||||
let linebreaks = linebreaker.advance_to_linebreaks_in_range(self.range.clone());
|
||||
let linebreak_iter = linebreaks.iter().chain(std::iter::once(&range.end));
|
||||
|
||||
self.runs.clear();
|
||||
self.runs.reserve(linebreaks.len());
|
||||
self.break_at_start = false;
|
||||
|
||||
let text_style = parent_style.get_inherited_text().clone();
|
||||
let can_break_anywhere = text_style.word_break == WordBreak::BreakAll ||
|
||||
text_style.overflow_wrap == OverflowWrap::Anywhere ||
|
||||
text_style.overflow_wrap == OverflowWrap::BreakWord;
|
||||
|
||||
let mut last_slice_end = self.range.start;
|
||||
for break_index in linebreak_iter {
|
||||
if *break_index == self.range.start {
|
||||
self.break_at_start = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extend the slice to the next UAX#14 line break opportunity.
|
||||
let mut slice = last_slice_end..*break_index;
|
||||
let word = &formatting_context_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, |&(_, character)| character == '\n');
|
||||
if let Some((first_white_space_index, first_white_space_character)) = rev_char_indices
|
||||
.take_while(|&(_, character)| char_is_whitespace(character))
|
||||
.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 != self.range.end &&
|
||||
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() {
|
||||
self.shape_and_push_range(&slice, formatting_context_text, &font, 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 formatting_context_text[whitespace].char_indices() {
|
||||
let index = start_index + index;
|
||||
self.shape_and_push_range(
|
||||
&(index..index + character.len_utf8()),
|
||||
formatting_context_text,
|
||||
&font,
|
||||
&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 {
|
||||
self.shape_and_push_range(
|
||||
&(whitespace.start..whitespace.end - 1),
|
||||
formatting_context_text,
|
||||
&font,
|
||||
&options,
|
||||
);
|
||||
self.shape_and_push_range(
|
||||
&(whitespace.end - 1..whitespace.end),
|
||||
formatting_context_text,
|
||||
&font,
|
||||
&options,
|
||||
);
|
||||
} else {
|
||||
self.shape_and_push_range(&whitespace, formatting_context_text, &font, &options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextRun {
|
||||
|
@ -185,14 +337,13 @@ impl TextRun {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn break_and_shape(
|
||||
pub(super) fn segment_and_shape(
|
||||
&mut self,
|
||||
text_content: &str,
|
||||
formatting_context_text: &str,
|
||||
font_context: &FontContext<FontCacheThread>,
|
||||
linebreaker: &mut Option<LineBreakLeafIter>,
|
||||
linebreaker: &mut LineBreaker,
|
||||
font_cache: &mut Vec<FontKeyAndMetrics>,
|
||||
) {
|
||||
let segment_results = self.segment_text(text_content, font_context, font_cache);
|
||||
let inherited_text_style = self.parent_style.get_inherited_text().clone();
|
||||
let letter_spacing = if inherited_text_style.letter_spacing.0.px() != 0. {
|
||||
Some(app_units::Au::from(inherited_text_style.letter_spacing.0))
|
||||
|
@ -208,13 +359,12 @@ impl TextRun {
|
|||
flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG);
|
||||
flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG)
|
||||
}
|
||||
if inherited_text_style.word_break == WordBreak::KeepAll {
|
||||
flags.insert(ShapingFlags::KEEP_ALL_FLAG);
|
||||
}
|
||||
|
||||
let specified_word_spacing = &inherited_text_style.word_spacing;
|
||||
let style_word_spacing: Option<Au> = specified_word_spacing.to_length().map(|l| l.into());
|
||||
let segments = segment_results
|
||||
|
||||
let segments = self
|
||||
.segment_text_by_font(formatting_context_text, font_context, font_cache)
|
||||
.into_iter()
|
||||
.map(|(mut segment, font)| {
|
||||
let word_spacing = style_word_spacing.unwrap_or_else(|| {
|
||||
|
@ -224,20 +374,21 @@ impl TextRun {
|
|||
.unwrap_or(gfx::font::LAST_RESORT_GLYPH_ADVANCE);
|
||||
specified_word_spacing.to_used_value(Au::from_f64_px(space_width))
|
||||
});
|
||||
|
||||
let shaping_options = ShapingOptions {
|
||||
letter_spacing,
|
||||
word_spacing,
|
||||
script: segment.script,
|
||||
flags,
|
||||
};
|
||||
(segment.runs, segment.break_at_start) = break_and_shape(
|
||||
font,
|
||||
&text_content[segment.range.begin().0 as usize..segment.range.end().0 as usize],
|
||||
&inherited_text_style,
|
||||
&shaping_options,
|
||||
linebreaker,
|
||||
);
|
||||
|
||||
segment.shape_text(
|
||||
&self.parent_style,
|
||||
formatting_context_text,
|
||||
linebreaker,
|
||||
&shaping_options,
|
||||
font,
|
||||
);
|
||||
segment
|
||||
})
|
||||
.collect();
|
||||
|
@ -249,9 +400,9 @@ impl TextRun {
|
|||
/// font and script. Fonts may differ when glyphs are found in fallback fonts. Fonts are stored
|
||||
/// in the `font_cache` which is a cache of all font keys and metrics used in this
|
||||
/// [`super::InlineFormattingContext`].
|
||||
fn segment_text(
|
||||
fn segment_text_by_font(
|
||||
&mut self,
|
||||
text_content: &str,
|
||||
formatting_context_text: &str,
|
||||
font_context: &FontContext<FontCacheThread>,
|
||||
font_cache: &mut Vec<FontKeyAndMetrics>,
|
||||
) -> Vec<(TextRunSegment, FontRef)> {
|
||||
|
@ -259,15 +410,16 @@ impl TextRun {
|
|||
let mut current: Option<(TextRunSegment, FontRef)> = None;
|
||||
let mut results = Vec::new();
|
||||
|
||||
let char_iterator = TwoCharsAtATimeIterator::new(text_content.chars());
|
||||
let mut next_byte_index = 0;
|
||||
let text_run_text = &formatting_context_text[self.text_range.clone()];
|
||||
let char_iterator = TwoCharsAtATimeIterator::new(text_run_text.chars());
|
||||
let mut next_byte_index = self.text_range.start;
|
||||
for (character, next_character) in char_iterator {
|
||||
let current_byte_index = next_byte_index;
|
||||
next_byte_index += character.len_utf8();
|
||||
|
||||
let prevents_soft_wrap_opportunity =
|
||||
char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character);
|
||||
if current_byte_index == 0 && prevents_soft_wrap_opportunity {
|
||||
if current_byte_index == self.text_range.start && prevents_soft_wrap_opportunity {
|
||||
self.prevent_soft_wrap_opportunity_at_start = true;
|
||||
}
|
||||
self.prevent_soft_wrap_opportunity_at_end = prevents_soft_wrap_opportunity;
|
||||
|
@ -296,17 +448,19 @@ impl TextRun {
|
|||
|
||||
// Add the new segment and finish the existing one, if we had one. If the first
|
||||
// characters in the run were control characters we may be creating the first
|
||||
// segment in the middle of the run (ie the start should be 0).
|
||||
// segment in the middle of the run (ie the start should be the start of this
|
||||
// text run's text).
|
||||
let start_byte_index = match current {
|
||||
Some(_) => ByteIndex(current_byte_index as isize),
|
||||
None => ByteIndex(0_isize),
|
||||
Some(_) => current_byte_index,
|
||||
None => self.text_range.start,
|
||||
};
|
||||
let new = (
|
||||
TextRunSegment::new(font_index, script, start_byte_index),
|
||||
font,
|
||||
);
|
||||
if let Some(mut finished) = current.replace(new) {
|
||||
finished.0.range.extend_to(start_byte_index);
|
||||
// The end of the previous segment is the start of the next one.
|
||||
finished.0.range.end = current_byte_index;
|
||||
results.push(finished);
|
||||
}
|
||||
}
|
||||
|
@ -317,7 +471,7 @@ impl TextRun {
|
|||
current = font_group.write().first(font_context).map(|font| {
|
||||
let font_index = add_or_get_font(&font, font_cache);
|
||||
(
|
||||
TextRunSegment::new(font_index, Script::Common, ByteIndex(0)),
|
||||
TextRunSegment::new(font_index, Script::Common, self.text_range.start),
|
||||
font,
|
||||
)
|
||||
})
|
||||
|
@ -325,10 +479,7 @@ impl TextRun {
|
|||
|
||||
// Extend the last segment to the end of the string and add it to the results.
|
||||
if let Some(mut last_segment) = current.take() {
|
||||
last_segment
|
||||
.0
|
||||
.range
|
||||
.extend_to(ByteIndex(text_content.len() as isize));
|
||||
last_segment.0.range.end = self.text_range.end;
|
||||
results.push(last_segment);
|
||||
}
|
||||
|
||||
|
@ -463,129 +614,3 @@ where
|
|||
Some((character, self.next_character))
|
||||
}
|
||||
}
|
||||
|
||||
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: &Range<usize>, options: &ShapingOptions| {
|
||||
glyphs.push(GlyphRun {
|
||||
glyph_store: font.shape_text(&text[range.clone()], options),
|
||||
range: ServoRange::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)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
[line-break-anywhere-overrides-uax-behavior-015.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[line-break-normal-015b.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[line-break-strict-015b.xht]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[line-breaking-013.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[line-breaking-014.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[line-breaking-021.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[line-breaking-024.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[line-breaking-025.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[line-breaking-026.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[line-breaking-027.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[line-breaking-atomic-010.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[line-breaking-atomic-011.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[shaping-001-ref.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[shaping-002-ref.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[shaping-003-ref.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[shaping-008-ref.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[shaping-009-ref.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[shaping-010-ref.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[shaping-011-ref.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[shaping-014-ref.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[shaping-016-ref.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[shaping-020-ref.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[shaping-021-ref.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[shaping-022-ref.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-001.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-001.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-001.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-002.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-002.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-002.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-003.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-003.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-003.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-008.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-008.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-008.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-014.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-014.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-014.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-016.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-016.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-016.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-017.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-017.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-017.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-018.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-018.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-018.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-020.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-020.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-020.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-021.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-021.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-021.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-022.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-022.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-022.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-023.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-023.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-023.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-024.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-024.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-024.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/css/css-text/shaping/shaping-025.html.ini
Normal file
2
tests/wpt/meta/css/css-text/shaping/shaping-025.html.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[shaping-025.html]
|
||||
expected: FAIL
|
|
@ -0,0 +1,2 @@
|
|||
[word-break-manual-001.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[word-break-normal-002.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[word-break-normal-003.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[word-break-normal-th-000.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[word-break-normal-th-001.html]
|
||||
expected: FAIL
|
Loading…
Add table
Add a link
Reference in a new issue