mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
layout: Do whitespace collapse during breaking and shaping (#31322)
This moves white space collapse to right before breaking and shaping happens, which is more similar to what happens in legacy layout. This is the first step toward making this procedure more efficient (avoiding string copies) and also implementing support for `text-transform`. Co-authored-by: Rakhi Sharma <atbrakhi@igalia.com>
This commit is contained in:
parent
6fe7cec569
commit
6d73832009
5 changed files with 333 additions and 253 deletions
|
@ -153,7 +153,6 @@ where
|
|||
(&run.info).into(),
|
||||
run.info.style,
|
||||
run.text.into(),
|
||||
false, /* has_uncollapsible_content */
|
||||
)
|
||||
});
|
||||
let bfc = BlockFormattingContext::construct_for_text_runs(
|
||||
|
|
|
@ -7,7 +7,6 @@ use std::convert::{TryFrom, TryInto};
|
|||
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use servo_arc::Arc;
|
||||
use style::computed_values::white_space::T as WhiteSpace;
|
||||
use style::properties::longhands::list_style_position::computed_value::T as ListStylePosition;
|
||||
use style::properties::ComputedValues;
|
||||
use style::selector_parser::PseudoElement;
|
||||
|
@ -60,7 +59,6 @@ impl BlockFormattingContext {
|
|||
layout_context: &LayoutContext,
|
||||
text_decoration_line: TextDecorationLine,
|
||||
) -> Self {
|
||||
// FIXME: do white space collapsing
|
||||
let inline_level_boxes = runs
|
||||
.map(|run| ArcRefCell::new(InlineLevelBox::TextRun(run)))
|
||||
.collect();
|
||||
|
@ -71,7 +69,6 @@ impl BlockFormattingContext {
|
|||
text_decoration_line,
|
||||
has_first_formatted_line: true,
|
||||
contains_floats: false,
|
||||
ends_with_whitespace: false,
|
||||
};
|
||||
Self {
|
||||
contents: BlockContainer::construct_inline_formatting_context(layout_context, ifc),
|
||||
|
@ -114,7 +111,7 @@ enum BlockLevelCreator {
|
|||
///
|
||||
/// Deferring allows using rayon’s `into_par_iter`.
|
||||
enum IntermediateBlockContainer {
|
||||
InlineFormattingContext(InlineFormattingContext),
|
||||
InlineFormattingContext(BlockContainer),
|
||||
Deferred {
|
||||
contents: NonReplacedContents,
|
||||
propagated_text_decoration_line: TextDecorationLine,
|
||||
|
@ -247,7 +244,6 @@ where
|
|||
ongoing_inline_formatting_context: InlineFormattingContext::new(
|
||||
text_decoration_line,
|
||||
/* has_first_formatted_line = */ true,
|
||||
/* ends_with_whitespace */ false,
|
||||
),
|
||||
ongoing_inline_boxes_stack: Vec::new(),
|
||||
anonymous_style: None,
|
||||
|
@ -318,7 +314,6 @@ where
|
|||
);
|
||||
|
||||
if inline_table {
|
||||
self.ongoing_inline_formatting_context.ends_with_whitespace = false;
|
||||
self.current_inline_level_boxes()
|
||||
.push(ArcRefCell::new(InlineLevelBox::Atomic(ifc)));
|
||||
} else {
|
||||
|
@ -409,24 +404,15 @@ where
|
|||
self.finish_anonymous_table_if_needed();
|
||||
}
|
||||
|
||||
let (output, has_uncollapsible_content) = collapse_and_transform_whitespace(
|
||||
&input,
|
||||
info.style.get_inherited_text().white_space,
|
||||
self.ongoing_inline_formatting_context.ends_with_whitespace,
|
||||
);
|
||||
if output.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.ongoing_inline_formatting_context.ends_with_whitespace =
|
||||
output.chars().last().unwrap().is_ascii_whitespace();
|
||||
|
||||
// TODO: We can do better here than `push_str` and wait until we are breaking and
|
||||
// shaping text to allocate space big enough for the final text. It would require
|
||||
// collecting all Cow strings into a vector and passing them along to text breaking
|
||||
// and shaping during final InlineFormattingContext construction.
|
||||
let inlines = self.current_inline_level_boxes();
|
||||
match inlines.last_mut().map(|last| last.borrow_mut()) {
|
||||
Some(mut last_box) => match *last_box {
|
||||
InlineLevelBox::TextRun(ref mut text_run) => {
|
||||
text_run.text.push_str(&output);
|
||||
text_run.has_uncollapsible_content |= has_uncollapsible_content;
|
||||
text_run.text.push_str(&input);
|
||||
return;
|
||||
},
|
||||
_ => {},
|
||||
|
@ -437,130 +423,11 @@ where
|
|||
inlines.push(ArcRefCell::new(InlineLevelBox::TextRun(TextRun::new(
|
||||
info.into(),
|
||||
Arc::clone(&info.style),
|
||||
output,
|
||||
has_uncollapsible_content,
|
||||
input.into(),
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
fn preserve_segment_break() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Collapse and transform whitespace in the given input according to the rules in
|
||||
/// <https://drafts.csswg.org/css-text-3/#white-space-phase-1>. This method doesn't
|
||||
/// follow the steps exactly since they are defined in a multi-pass appraoach, but it
|
||||
/// tries to be effectively the same transformation.
|
||||
///
|
||||
/// Returns the transformed text as a [String] and also whether or not the input had
|
||||
/// any uncollapsible content.
|
||||
fn collapse_and_transform_whitespace(
|
||||
input: &str,
|
||||
white_space: WhiteSpace,
|
||||
trim_beginning_white_space: bool,
|
||||
) -> (String, bool) {
|
||||
// Point 4.1.1 first bullet:
|
||||
// > If white-space is set to normal, nowrap, or pre-line, whitespace
|
||||
// > 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 white_space.preserve_spaces() {
|
||||
return (input.to_owned(), true);
|
||||
}
|
||||
|
||||
let mut output = String::with_capacity(input.len());
|
||||
let mut has_uncollapsible_content = false;
|
||||
let mut had_whitespace = false;
|
||||
let mut following_newline = false;
|
||||
let mut in_whitespace_at_beginning = true;
|
||||
|
||||
let is_leading_trimmed_whitespace =
|
||||
|in_whitespace_at_beginning: bool| in_whitespace_at_beginning && trim_beginning_white_space;
|
||||
|
||||
// Point 4.1.1:
|
||||
// > 2. Any sequence of collapsible spaces and tabs immediately preceding or
|
||||
// > following a segment break is removed.
|
||||
// > 3. Every collapsible tab is converted to a collapsible space (U+0020).
|
||||
// > 4. Any collapsible space immediately following another collapsible space—even
|
||||
// > one outside the boundary of the inline containing that space, provided both
|
||||
// > spaces are within the same inline formatting context—is collapsed to have zero
|
||||
// > advance width.
|
||||
let push_pending_whitespace_if_needed =
|
||||
|output: &mut String,
|
||||
had_whitespace: bool,
|
||||
following_newline: bool,
|
||||
in_whitespace_at_beginning: bool| {
|
||||
if had_whitespace &&
|
||||
!following_newline &&
|
||||
!is_leading_trimmed_whitespace(in_whitespace_at_beginning)
|
||||
{
|
||||
output.push(' ');
|
||||
}
|
||||
};
|
||||
|
||||
for character in input.chars() {
|
||||
// Don't push non-newline whitespace immediately. Instead wait to push it until we
|
||||
// know that it isn't followed by a newline. See `push_pending_whitespace_if_needed`
|
||||
// above.
|
||||
if character.is_ascii_whitespace() && character != '\n' {
|
||||
had_whitespace = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Point 4.1.1:
|
||||
// > 2. Collapsible segment breaks are transformed for rendering according to the
|
||||
// > segment break transformation rules.
|
||||
if character == '\n' {
|
||||
// From <https://drafts.csswg.org/css-text-3/#line-break-transform>
|
||||
// (4.1.3 -- the segment break transformation rules):
|
||||
//
|
||||
// > When white-space is pre, pre-wrap, or pre-line, segment breaks are not
|
||||
// > collapsible and are instead transformed into a preserved line feed"
|
||||
if white_space == WhiteSpace::PreLine {
|
||||
has_uncollapsible_content = true;
|
||||
had_whitespace = false;
|
||||
output.push('\n');
|
||||
|
||||
// Point 4.1.3:
|
||||
// > 1. First, any collapsible segment break immediately following another
|
||||
// > collapsible segment break is removed.
|
||||
// > 2. Then any remaining segment break is either transformed into a space (U+0020)
|
||||
// > or removed depending on the context before and after the break.
|
||||
} else if !following_newline &&
|
||||
preserve_segment_break() &&
|
||||
!is_leading_trimmed_whitespace(in_whitespace_at_beginning)
|
||||
{
|
||||
had_whitespace = false;
|
||||
output.push(' ');
|
||||
}
|
||||
following_newline = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
push_pending_whitespace_if_needed(
|
||||
&mut output,
|
||||
had_whitespace,
|
||||
following_newline,
|
||||
in_whitespace_at_beginning,
|
||||
);
|
||||
|
||||
has_uncollapsible_content = true;
|
||||
had_whitespace = false;
|
||||
in_whitespace_at_beginning = false;
|
||||
following_newline = false;
|
||||
output.push(character);
|
||||
}
|
||||
|
||||
push_pending_whitespace_if_needed(
|
||||
&mut output,
|
||||
had_whitespace,
|
||||
following_newline,
|
||||
in_whitespace_at_beginning,
|
||||
);
|
||||
|
||||
(output, has_uncollapsible_content)
|
||||
}
|
||||
|
||||
impl<'dom, Node> BlockContainerBuilder<'dom, '_, Node>
|
||||
where
|
||||
Node: NodeExt<'dom>,
|
||||
|
@ -630,7 +497,6 @@ where
|
|||
inline_box.is_last_fragment = true;
|
||||
ArcRefCell::new(InlineLevelBox::InlineBox(inline_box))
|
||||
} else {
|
||||
self.ongoing_inline_formatting_context.ends_with_whitespace = false;
|
||||
ArcRefCell::new(InlineLevelBox::Atomic(
|
||||
IndependentFormattingContext::construct(
|
||||
self.context,
|
||||
|
@ -802,7 +668,6 @@ where
|
|||
// There should never be an empty inline formatting context.
|
||||
self.ongoing_inline_formatting_context
|
||||
.has_first_formatted_line = false;
|
||||
self.ongoing_inline_formatting_context.ends_with_whitespace = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -822,7 +687,6 @@ where
|
|||
let mut ifc = InlineFormattingContext::new(
|
||||
self.ongoing_inline_formatting_context.text_decoration_line,
|
||||
/* has_first_formatted_line = */ false,
|
||||
/* ends_with_whitespace */ false,
|
||||
);
|
||||
std::mem::swap(&mut self.ongoing_inline_formatting_context, &mut ifc);
|
||||
|
||||
|
@ -832,7 +696,9 @@ where
|
|||
// FIXME(nox): We should be storing this somewhere.
|
||||
box_slot: BoxSlot::dummy(),
|
||||
kind: BlockLevelCreator::SameFormattingContextBlock(
|
||||
IntermediateBlockContainer::InlineFormattingContext(ifc),
|
||||
IntermediateBlockContainer::InlineFormattingContext(
|
||||
BlockContainer::construct_inline_formatting_context(self.context, ifc),
|
||||
),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -932,56 +798,7 @@ impl IntermediateBlockContainer {
|
|||
propagated_text_decoration_line,
|
||||
is_list_item,
|
||||
),
|
||||
IntermediateBlockContainer::InlineFormattingContext(ifc) => {
|
||||
BlockContainer::construct_inline_formatting_context(context, ifc)
|
||||
},
|
||||
IntermediateBlockContainer::InlineFormattingContext(block_container) => block_container,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collapase_and_transform_whitespace() {
|
||||
let output = collapse_and_transform_whitespace("H ", WhiteSpace::Normal, false);
|
||||
assert_eq!(output.0, "H ");
|
||||
assert!(output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace(" W", WhiteSpace::Normal, true);
|
||||
assert_eq!(output.0, "W");
|
||||
assert!(output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace(" W", WhiteSpace::Normal, false);
|
||||
assert_eq!(output.0, " W");
|
||||
assert!(output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace(" H W", WhiteSpace::Normal, false);
|
||||
assert_eq!(output.0, " H W");
|
||||
assert!(output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace("\n H \n \t W", WhiteSpace::Normal, false);
|
||||
assert_eq!(output.0, " H W");
|
||||
|
||||
let output = collapse_and_transform_whitespace("\n H \n \t W \n", WhiteSpace::Pre, false);
|
||||
assert_eq!(output.0, "\n H \n \t W \n");
|
||||
assert!(output.1);
|
||||
|
||||
let output =
|
||||
collapse_and_transform_whitespace("\n H \n \t W \n ", WhiteSpace::PreLine, false);
|
||||
assert_eq!(output.0, "\nH\nW\n");
|
||||
assert!(output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace(" ", WhiteSpace::Normal, true);
|
||||
assert_eq!(output.0, "");
|
||||
assert!(!output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace(" ", WhiteSpace::Normal, false);
|
||||
assert_eq!(output.0, " ");
|
||||
assert!(!output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace("\n ", WhiteSpace::Normal, true);
|
||||
assert_eq!(output.0, "");
|
||||
assert!(!output.1);
|
||||
|
||||
let output = collapse_and_transform_whitespace("\n ", WhiteSpace::Normal, false);
|
||||
assert_eq!(output.0, " ");
|
||||
assert!(!output.1);
|
||||
}
|
||||
|
|
|
@ -64,15 +64,6 @@ pub(crate) struct InlineFormattingContext {
|
|||
|
||||
/// Whether or not this [`InlineFormattingContext`] contains floats.
|
||||
pub(super) contains_floats: bool,
|
||||
|
||||
/// Whether this IFC being constructed currently ends with whitespace. This is used to
|
||||
/// implement rule 4 of <https://www.w3.org/TR/css-text-3/#collapse>:
|
||||
///
|
||||
/// > Any collapsible space immediately following another collapsible space—even one
|
||||
/// > outside the boundary of the inline containing that space, provided both spaces are
|
||||
/// > within the same inline formatting context—is collapsed to have zero advance width.
|
||||
/// > (It is invisible, but retains its soft wrap opportunity, if any.)
|
||||
pub(super) ends_with_whitespace: bool,
|
||||
}
|
||||
|
||||
/// A collection of data used to cache [`FontMetrics`] in the [`InlineFormattingContext`]
|
||||
|
@ -1355,7 +1346,6 @@ impl InlineFormattingContext {
|
|||
pub(super) fn new(
|
||||
text_decoration_line: TextDecorationLine,
|
||||
has_first_formatted_line: bool,
|
||||
ends_with_whitespace: bool,
|
||||
) -> InlineFormattingContext {
|
||||
InlineFormattingContext {
|
||||
inline_level_boxes: Default::default(),
|
||||
|
@ -1363,7 +1353,6 @@ impl InlineFormattingContext {
|
|||
text_decoration_line,
|
||||
has_first_formatted_line,
|
||||
contains_floats: false,
|
||||
ends_with_whitespace,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1565,7 +1554,7 @@ impl InlineFormattingContext {
|
|||
fn inline_level_box_is_empty(inline_level_box: &InlineLevelBox) -> bool {
|
||||
match inline_level_box {
|
||||
InlineLevelBox::InlineBox(_) => false,
|
||||
InlineLevelBox::TextRun(text_run) => !text_run.has_uncollapsible_content,
|
||||
InlineLevelBox::TextRun(text_run) => !text_run.has_uncollapsible_content(),
|
||||
InlineLevelBox::OutOfFlowAbsolutelyPositionedBox(_) => false,
|
||||
InlineLevelBox::OutOfFlowFloatBox(_) => false,
|
||||
InlineLevelBox::Atomic(_) => false,
|
||||
|
@ -1579,13 +1568,28 @@ impl InlineFormattingContext {
|
|||
/// all font matching and FontMetrics collection.
|
||||
pub(crate) fn break_and_shape_text(&mut self, layout_context: &LayoutContext) {
|
||||
let mut ifc_fonts = Vec::new();
|
||||
|
||||
// Whether the last processed node ended with whitespace. This is used to
|
||||
// implement rule 4 of <https://www.w3.org/TR/css-text-3/#collapse>:
|
||||
//
|
||||
// > Any collapsible space immediately following another collapsible space—even one
|
||||
// > outside the boundary of the inline containing that space, provided both spaces are
|
||||
// > within the same inline formatting context—is collapsed to have zero advance width.
|
||||
// > (It is invisible, but retains its soft wrap opportunity, if any.)
|
||||
let mut last_inline_box_ended_with_white_space = false;
|
||||
|
||||
crate::context::with_thread_local_font_context(layout_context, |font_context| {
|
||||
let mut linebreaker = None;
|
||||
self.foreach(|iter_item| match iter_item {
|
||||
InlineFormattingContextIterItem::Item(InlineLevelBox::TextRun(
|
||||
ref mut text_run,
|
||||
)) => {
|
||||
text_run.break_and_shape(font_context, &mut linebreaker, &mut ifc_fonts);
|
||||
text_run.break_and_shape(
|
||||
font_context,
|
||||
&mut linebreaker,
|
||||
&mut ifc_fonts,
|
||||
&mut last_inline_box_ended_with_white_space,
|
||||
);
|
||||
},
|
||||
InlineFormattingContextIterItem::Item(InlineLevelBox::InlineBox(inline_box)) => {
|
||||
if let Some(font) =
|
||||
|
@ -1595,6 +1599,9 @@ impl InlineFormattingContext {
|
|||
Some(add_or_get_font(&font, &mut ifc_fonts));
|
||||
}
|
||||
},
|
||||
InlineFormattingContextIterItem::Item(InlineLevelBox::Atomic(_)) => {
|
||||
last_inline_box_ended_with_white_space = false;
|
||||
},
|
||||
_ => {},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use std::mem;
|
||||
use std::str::Chars;
|
||||
|
||||
use app_units::Au;
|
||||
use gfx::font::{FontRef, ShapingFlags, ShapingOptions};
|
||||
|
@ -15,6 +16,7 @@ use range::Range;
|
|||
use serde::Serialize;
|
||||
use servo_arc::Arc;
|
||||
use style::computed_values::text_rendering::T as TextRendering;
|
||||
use style::computed_values::white_space::T as WhiteSpace;
|
||||
use style::computed_values::word_break::T as WordBreak;
|
||||
use style::properties::ComputedValues;
|
||||
use unicode_script::Script;
|
||||
|
@ -43,10 +45,6 @@ pub(crate) struct TextRun {
|
|||
/// segments, and shaped.
|
||||
pub shaped_text: Vec<TextRunSegment>,
|
||||
|
||||
/// Whether or not this [`TextRun`] has uncollapsible content. This is used
|
||||
/// to determine if an [`super::InlineFormattingContext`] is considered empty or not.
|
||||
pub has_uncollapsible_content: bool,
|
||||
|
||||
/// Whether or not to prevent a soft wrap opportunity at the start of this [`TextRun`].
|
||||
/// This depends on the whether the first character in the run prevents a soft wrap
|
||||
/// opportunity.
|
||||
|
@ -174,26 +172,49 @@ impl TextRun {
|
|||
base_fragment_info: BaseFragmentInfo,
|
||||
parent_style: Arc<ComputedValues>,
|
||||
text: String,
|
||||
has_uncollapsible_content: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
base_fragment_info,
|
||||
parent_style,
|
||||
text,
|
||||
has_uncollapsible_content,
|
||||
shaped_text: Vec::new(),
|
||||
prevent_soft_wrap_opportunity_at_start: false,
|
||||
prevent_soft_wrap_opportunity_at_end: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not this [`TextRun`] has uncollapsible content. This is used
|
||||
/// to determine if an [`super::InlineFormattingContext`] is considered empty or not.
|
||||
pub(super) fn has_uncollapsible_content(&self) -> bool {
|
||||
let white_space = self.parent_style.clone_white_space();
|
||||
if white_space.preserve_spaces() && !self.text.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
for character in self.text.chars() {
|
||||
if !character.is_ascii_whitespace() {
|
||||
return true;
|
||||
}
|
||||
if character == '\n' && white_space.preserve_newlines() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub(super) fn break_and_shape(
|
||||
&mut self,
|
||||
font_context: &mut FontContext<FontCacheThread>,
|
||||
linebreaker: &mut Option<LineBreakLeafIter>,
|
||||
font_cache: &mut Vec<FontKeyAndMetrics>,
|
||||
last_inline_box_ended_with_white_space: &mut bool,
|
||||
) {
|
||||
let segment_results = self.segment_text(font_context, font_cache);
|
||||
let segment_results = self.segment_text(
|
||||
font_context,
|
||||
font_cache,
|
||||
last_inline_box_ended_with_white_space,
|
||||
);
|
||||
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))
|
||||
|
@ -256,54 +277,76 @@ impl TextRun {
|
|||
&mut self,
|
||||
font_context: &mut FontContext<FontCacheThread>,
|
||||
font_cache: &mut Vec<FontKeyAndMetrics>,
|
||||
last_inline_box_ended_with_white_space: &mut bool,
|
||||
) -> Vec<(TextRunSegment, FontRef)> {
|
||||
let font_group = font_context.font_group(self.parent_style.clone_font());
|
||||
let mut current: Option<(TextRunSegment, FontRef)> = None;
|
||||
let mut results = Vec::new();
|
||||
|
||||
for (byte_index, character) in self.text.char_indices() {
|
||||
let prevents_soft_wrap_opportunity =
|
||||
char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character);
|
||||
if byte_index == 0 && prevents_soft_wrap_opportunity {
|
||||
self.prevent_soft_wrap_opportunity_at_start = true;
|
||||
}
|
||||
self.prevent_soft_wrap_opportunity_at_end = prevents_soft_wrap_opportunity;
|
||||
let text = std::mem::replace(&mut self.text, String::new());
|
||||
let collapsed = WhitespaceCollapse::new(
|
||||
text.as_str(),
|
||||
self.parent_style.clone_white_space(),
|
||||
*last_inline_box_ended_with_white_space,
|
||||
);
|
||||
|
||||
if char_does_not_change_font(character) {
|
||||
continue;
|
||||
}
|
||||
let mut next_byte_index = 0;
|
||||
let text = collapsed
|
||||
.map(|character| {
|
||||
let current_byte_index = next_byte_index;
|
||||
next_byte_index += character.len_utf8();
|
||||
|
||||
let font = match font_group
|
||||
.borrow_mut()
|
||||
.find_by_codepoint(font_context, character)
|
||||
{
|
||||
Some(font) => font,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// If the existing segment is compatible with the character, keep going.
|
||||
let script = Script::from(character);
|
||||
if let Some(current) = current.as_mut() {
|
||||
if current.0.update_if_compatible(&font, script, font_cache) {
|
||||
continue;
|
||||
*last_inline_box_ended_with_white_space = character.is_whitespace();
|
||||
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 {
|
||||
self.prevent_soft_wrap_opportunity_at_start = true;
|
||||
}
|
||||
}
|
||||
self.prevent_soft_wrap_opportunity_at_end = prevents_soft_wrap_opportunity;
|
||||
|
||||
let font_index = add_or_get_font(&font, font_cache);
|
||||
if char_does_not_change_font(character) {
|
||||
return character;
|
||||
}
|
||||
|
||||
// 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).
|
||||
let byte_index = match current {
|
||||
Some(_) => ByteIndex(byte_index as isize),
|
||||
None => ByteIndex(0 as isize),
|
||||
};
|
||||
let new = (TextRunSegment::new(font_index, script, byte_index), font);
|
||||
if let Some(mut finished) = current.replace(new) {
|
||||
finished.0.range.extend_to(byte_index);
|
||||
results.push(finished);
|
||||
}
|
||||
}
|
||||
let font = match font_group
|
||||
.borrow_mut()
|
||||
.find_by_codepoint(font_context, character)
|
||||
{
|
||||
Some(font) => font,
|
||||
None => return character,
|
||||
};
|
||||
|
||||
// If the existing segment is compatible with the character, keep going.
|
||||
let script = Script::from(character);
|
||||
if let Some(current) = current.as_mut() {
|
||||
if current.0.update_if_compatible(&font, script, font_cache) {
|
||||
return character;
|
||||
}
|
||||
}
|
||||
|
||||
let font_index = add_or_get_font(&font, font_cache);
|
||||
|
||||
// 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).
|
||||
let start_byte_index = match current {
|
||||
Some(_) => ByteIndex(current_byte_index as isize),
|
||||
None => ByteIndex(0 as isize),
|
||||
};
|
||||
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);
|
||||
results.push(finished);
|
||||
}
|
||||
|
||||
character
|
||||
})
|
||||
.collect();
|
||||
|
||||
let _ = std::mem::replace(&mut self.text, text);
|
||||
|
||||
// Either we have a current segment or we only had control character and whitespace. In both
|
||||
// of those cases, just use the first font.
|
||||
|
@ -443,3 +486,162 @@ pub(super) fn get_font_for_first_font_for_style(
|
|||
}
|
||||
font
|
||||
}
|
||||
|
||||
fn preserve_segment_break() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub struct WhitespaceCollapse<'a> {
|
||||
char_iterator: Chars<'a>,
|
||||
white_space: WhiteSpace,
|
||||
|
||||
/// Whether or not we should collapse white space completely at the start of the string.
|
||||
/// This is true when the last character handled in our owning [`InlineFormattingContext`]
|
||||
/// was collapsible white space.
|
||||
remove_collapsible_white_space_at_start: bool,
|
||||
|
||||
/// Whether or not the last character produced was newline. There is special behavior
|
||||
/// we do after each newline.
|
||||
following_newline: bool,
|
||||
|
||||
/// Whether or not we have seen any non-white space characters, indicating that we are not
|
||||
/// in a collapsible white space section at the beginning of the string.
|
||||
have_seen_non_white_space_characters: bool,
|
||||
|
||||
/// Whether the last character that we processed was a non-newline white space character. When
|
||||
/// collapsing white space we need to wait until the next non-white space character or the end
|
||||
/// of the string to push a single white space.
|
||||
inside_white_space: bool,
|
||||
|
||||
/// When we enter a collapsible white space region, we may need to wait to produce a single
|
||||
/// white space character as soon as we encounter a non-white space character. When that
|
||||
/// happens we queue up the non-white space character for the next iterator call.
|
||||
character_pending_to_return: Option<char>,
|
||||
}
|
||||
|
||||
impl<'a> WhitespaceCollapse<'a> {
|
||||
pub fn new(input: &'a str, white_space: WhiteSpace, trim_beginning_white_space: bool) -> Self {
|
||||
Self {
|
||||
char_iterator: input.chars(),
|
||||
white_space,
|
||||
remove_collapsible_white_space_at_start: trim_beginning_white_space,
|
||||
inside_white_space: false,
|
||||
following_newline: false,
|
||||
have_seen_non_white_space_characters: false,
|
||||
character_pending_to_return: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_leading_trimmed_white_space(&self) -> bool {
|
||||
!self.have_seen_non_white_space_characters && self.remove_collapsible_white_space_at_start
|
||||
}
|
||||
|
||||
/// Whether or not we need to produce a space character if the next character is not a newline
|
||||
/// and not white space. This happens when we are exiting a section of white space and we
|
||||
/// waited to produce a single space character for the entire section of white space (but
|
||||
/// not following or preceding a newline).
|
||||
fn need_to_produce_space_character_after_white_space(&self) -> bool {
|
||||
self.inside_white_space && !self.following_newline && !self.is_leading_trimmed_white_space()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for WhitespaceCollapse<'a> {
|
||||
type Item = char;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// Point 4.1.1 first bullet:
|
||||
// > If white-space is set to normal, nowrap, or pre-line, whitespace
|
||||
// > 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.preserve_spaces() {
|
||||
return self.char_iterator.next();
|
||||
}
|
||||
|
||||
if let Some(character) = self.character_pending_to_return.take() {
|
||||
self.inside_white_space = false;
|
||||
self.have_seen_non_white_space_characters = true;
|
||||
self.following_newline = false;
|
||||
return Some(character);
|
||||
}
|
||||
|
||||
while let Some(character) = self.char_iterator.next() {
|
||||
// Don't push non-newline whitespace immediately. Instead wait to push it until we
|
||||
// know that it isn't followed by a newline. See `push_pending_whitespace_if_needed`
|
||||
// above.
|
||||
if character.is_ascii_whitespace() && character != '\n' {
|
||||
self.inside_white_space = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Point 4.1.1:
|
||||
// > 2. Collapsible segment breaks are transformed for rendering according to the
|
||||
// > segment break transformation rules.
|
||||
if character == '\n' {
|
||||
// From <https://drafts.csswg.org/css-text-3/#line-break-transform>
|
||||
// (4.1.3 -- the segment break transformation rules):
|
||||
//
|
||||
// > When white-space is pre, pre-wrap, or pre-line, segment breaks are not
|
||||
// > collapsible and are instead transformed into a preserved line feed"
|
||||
if self.white_space == WhiteSpace::PreLine {
|
||||
self.inside_white_space = false;
|
||||
self.following_newline = true;
|
||||
return Some(character);
|
||||
|
||||
// Point 4.1.3:
|
||||
// > 1. First, any collapsible segment break immediately following another
|
||||
// > collapsible segment break is removed.
|
||||
// > 2. Then any remaining segment break is either transformed into a space (U+0020)
|
||||
// > or removed depending on the context before and after the break.
|
||||
} else if !self.following_newline &&
|
||||
preserve_segment_break() &&
|
||||
!self.is_leading_trimmed_white_space()
|
||||
{
|
||||
self.inside_white_space = false;
|
||||
self.following_newline = true;
|
||||
return Some(' ');
|
||||
} else {
|
||||
self.following_newline = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Point 4.1.1:
|
||||
// > 2. Any sequence of collapsible spaces and tabs immediately preceding or
|
||||
// > following a segment break is removed.
|
||||
// > 3. Every collapsible tab is converted to a collapsible space (U+0020).
|
||||
// > 4. Any collapsible space immediately following another collapsible space—even
|
||||
// > one outside the boundary of the inline containing that space, provided both
|
||||
// > spaces are within the same inline formatting context—is collapsed to have zero
|
||||
// > advance width.
|
||||
if self.need_to_produce_space_character_after_white_space() {
|
||||
self.inside_white_space = false;
|
||||
self.character_pending_to_return = Some(character);
|
||||
return Some(' ');
|
||||
}
|
||||
|
||||
self.inside_white_space = false;
|
||||
self.have_seen_non_white_space_characters = true;
|
||||
self.following_newline = false;
|
||||
return Some(character);
|
||||
}
|
||||
|
||||
if self.need_to_produce_space_character_after_white_space() {
|
||||
self.inside_white_space = false;
|
||||
return Some(' ');
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.char_iterator.size_hint()
|
||||
}
|
||||
|
||||
fn count(self) -> usize
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.char_iterator.count()
|
||||
}
|
||||
}
|
||||
|
|
55
components/layout_2020/tests/text.rs
Normal file
55
components/layout_2020/tests/text.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
/* 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/. */
|
||||
|
||||
mod text {
|
||||
use layout_2020::flow::text_run::WhitespaceCollapse;
|
||||
use style::computed_values::white_space::T as WhiteSpace;
|
||||
|
||||
#[test]
|
||||
fn test_collapse_whitespace() {
|
||||
let collapse = |input, white_space, trim_beginning_white_space| {
|
||||
WhitespaceCollapse::new(input, white_space, trim_beginning_white_space)
|
||||
.collect::<String>()
|
||||
};
|
||||
|
||||
let output = collapse("H ", WhiteSpace::Normal, false);
|
||||
assert_eq!(output, "H ");
|
||||
|
||||
let output = collapse(" W", WhiteSpace::Normal, true);
|
||||
assert_eq!(output, "W");
|
||||
|
||||
let output = collapse(" W", WhiteSpace::Normal, false);
|
||||
assert_eq!(output, " W");
|
||||
|
||||
let output = collapse(" H W", WhiteSpace::Normal, false);
|
||||
assert_eq!(output, " H W");
|
||||
|
||||
let output = collapse("\n H \n \t W", WhiteSpace::Normal, false);
|
||||
assert_eq!(output, " H W");
|
||||
|
||||
let output = collapse("\n H \n \t W \n", WhiteSpace::Pre, false);
|
||||
assert_eq!(output, "\n H \n \t W \n");
|
||||
|
||||
let output = collapse("\n H \n \t W \n ", WhiteSpace::PreLine, false);
|
||||
assert_eq!(output, "\nH\nW\n");
|
||||
|
||||
let output = collapse("Hello \n World", WhiteSpace::PreLine, true);
|
||||
assert_eq!(output, "Hello\nWorld");
|
||||
|
||||
let output = collapse(" \n World", WhiteSpace::PreLine, true);
|
||||
assert_eq!(output, "\nWorld");
|
||||
|
||||
let output = collapse(" ", WhiteSpace::Normal, true);
|
||||
assert_eq!(output, "");
|
||||
|
||||
let output = collapse(" ", WhiteSpace::Normal, false);
|
||||
assert_eq!(output, " ");
|
||||
|
||||
let output = collapse("\n ", WhiteSpace::Normal, true);
|
||||
assert_eq!(output, "");
|
||||
|
||||
let output = collapse("\n ", WhiteSpace::Normal, false);
|
||||
assert_eq!(output, " ");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue