mirror of
https://github.com/servo/servo.git
synced 2025-08-02 20:20:14 +01:00
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:
parent
c0dedf06d6
commit
60b4b6c9f0
96 changed files with 410 additions and 537 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue