Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions parley/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,16 @@ impl<'b, B: Brush> StyleRunBuilder<'b, B> {
}
}

/// The text pushed to a [`TreeBuilder`] so far, as returned by [`TreeBuilder::text_so_far`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TextSoFar<'a> {
/// The white space processed text.
pub text: &'a str,
/// Whether `text` is followed by collapsible whitespace, which will become a single space if
/// it is followed by more content, or be removed otherwise.
pub pending_whitespace: bool,
}

/// Builder for constructing a text layout with a tree of attributes.
#[must_use]
pub struct TreeBuilder<'a, B: Brush> {
Expand Down Expand Up @@ -258,16 +268,27 @@ impl<'b, B: Brush> TreeBuilder<'b, B> {
self.lcx.tree_style_builder.push_text(text);
}

pub fn push_inline_box(&mut self, mut inline_box: InlineBox) {
self.lcx.tree_style_builder.commit_uncommitted_text();
/// Returns the text pushed so far, after white space processing.
///
/// This is the text that will be laid out, excluding any trailing collapsible whitespace:
/// whether that is kept (as a single space) depends on the content that follows it. Its
/// presence is indicated by [`TextSoFar::pending_whitespace`].
pub fn text_so_far(&self) -> TextSoFar<'_> {
let (text, pending_whitespace) = self.lcx.tree_style_builder.text_so_far();
TextSoFar {
text,
pending_whitespace,
}
}

pub fn push_inline_box(&mut self, mut inline_box: InlineBox) {
if inline_box.kind == InlineBoxKind::InFlow {
self.lcx.tree_style_builder.flush_pending_whitespace();
self.lcx.tree_style_builder.set_last_item_is_inline_box();
}

// TODO: arrange type better here to factor out the index
inline_box.index = self.lcx.tree_style_builder.committed_text_len();
inline_box.index = self.lcx.tree_style_builder.text_len();
self.lcx.inline_boxes.push(inline_box);
}

Expand Down
2 changes: 1 addition & 1 deletion parley/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ pub use parley_engine::break_overrides::{
LineBreakContext, LineBreakOverrideFn,
};

pub use builder::{RangedBuilder, StyleRunBuilder, TreeBuilder};
pub use builder::{RangedBuilder, StyleRunBuilder, TextSoFar, TreeBuilder};
pub use context::LayoutContext;
pub use font::FontContext;
pub use inline_box::{InlineBox, InlineBoxKind};
Expand Down
128 changes: 101 additions & 27 deletions parley/src/resolve/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,40 @@ fn is_segment_break(c: char) -> bool {
matches!(c, '\n' | '\r' | '\u{2028}' | '\u{2029}')
}

/// The length of the prefix of `text` that is unchanged by white space collapsing in `mode`,
/// given that `text` starts with a character that is neither collapsible nor a segment break.
///
/// This is the text up to the first collapsible whitespace or segment break, extended past any
/// single spaces that are followed by such a character, since those are already collapsed.
#[inline(always)]
fn collapsed_len(text: &str, mode: WhiteSpaceCollapse) -> usize {
// Collapsible whitespace is always ASCII, and the only non-ASCII segment breaks are LS
// (U+2028) and PS (U+2029), so the text can be scanned bytewise.
let bytes = text.as_bytes();
let needs_processing = |i: usize| match bytes[i] {
b @ 0..0x80 => mode.is_collapsible(b as char) || matches!(b, b'\n' | b'\r'),
0xE2 => bytes.get(i + 1) == Some(&0x80) && matches!(bytes.get(i + 2), Some(0xA8 | 0xA9)),
_ => false,
};
let mut len = 0;
while len < bytes.len() {
if needs_processing(len)
&& (bytes[len] != b' ' || len + 1 == bytes.len() || needs_processing(len + 1))
{
break;
}
len += 1;
}
len
}

/// Builder for constructing a tree of styles
#[derive(Clone)]
pub(crate) struct TreeStyleBuilder<B: Brush> {
tree: Vec<StyleTreeNode<B>>,
style_table: Vec<ResolvedStyle<B>>,
style_runs: Vec<StyleRun>,
text: String,
uncommitted_text: String,
current_span: usize,
/// The span that a not-yet-committed collapsible whitespace sequence belongs to.
///
Expand All @@ -60,7 +86,6 @@ impl<B: Brush> Default for TreeStyleBuilder<B> {
style_table: Vec::new(),
style_runs: Vec::new(),
text: String::new(),
uncommitted_text: String::new(),
current_span: usize::MAX,
pending_whitespace: None,
last_item_is_inline_box: false,
Expand All @@ -82,7 +107,6 @@ impl<B: Brush> TreeStyleBuilder<B> {
self.style_table.clear();
self.style_runs.clear();
self.text.clear();
self.uncommitted_text.clear();
self.pending_whitespace = None;
self.last_item_is_inline_box = false;

Expand All @@ -101,28 +125,27 @@ impl<B: Brush> TreeStyleBuilder<B> {
self.last_item_is_inline_box = true;
}

/// Applies white space processing to the buffered text and commits the result, leaving any
/// trailing collapsible whitespace pending.
pub(crate) fn commit_uncommitted_text(&mut self) {
let uncommitted_text = core::mem::take(&mut self.uncommitted_text);
if uncommitted_text.is_empty() {
/// Applies white space processing to `text` in the current span and commits the result, leaving
/// any trailing collapsible whitespace pending.
pub(crate) fn push_text(&mut self, text: &str) {
if text.is_empty() {
return;
}

let span = self.current_span;
match self.tree[span].style.white_space_collapse {
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces => {
if uncommitted_text.starts_with(is_segment_break) {
if text.starts_with(is_segment_break) {
// Pending whitespace is always from a `WhiteSpaceCollapse::Collapse` or
// `WhiteSpaceCollapse::PreserveBreaks` span, and following CSS Text 4 § 4.3.1 Rule 1
// must be removed if it immediately precedes a preserved segment break.
self.pending_whitespace = None;
}
self.flush_pending_whitespace();
self.commit_text(span, &uncommitted_text);
self.commit_text(span, text);
}
mode @ (WhiteSpaceCollapse::Collapse | WhiteSpaceCollapse::PreserveBreaks) => {
let mut rest = uncommitted_text.as_str();
let mut rest = text;
while !rest.is_empty() {
let whitespace_len =
rest.find(|c| !mode.is_collapsible(c)).unwrap_or(rest.len());
Expand Down Expand Up @@ -151,9 +174,7 @@ impl<B: Brush> TreeStyleBuilder<B> {
rest = &rest[break_len..];
continue;
}
let text_len = rest
.find(|c| mode.is_collapsible(c) || is_segment_break(c))
.unwrap_or(rest.len());
let text_len = collapsed_len(rest, mode);
self.flush_pending_whitespace();
self.commit_text(span, &rest[..text_len]);
rest = &rest[text_len..];
Expand Down Expand Up @@ -234,15 +255,19 @@ impl<B: Brush> TreeStyleBuilder<B> {
style_id
}

/// The length in bytes of the text committed so far, excluding buffered text.
pub(crate) fn committed_text_len(&self) -> usize {
/// The white space processed text so far and whether it is followed by pending collapsible
/// whitespace.
pub(crate) fn text_so_far(&self) -> (&str, bool) {
(&self.text, self.pending_whitespace.is_some())
}

/// The length in bytes of the text committed so far.
pub(crate) fn text_len(&self) -> usize {
self.text.len()
}

/// Begins a child span with the given style, which subsequent text is attributed to.
pub(crate) fn push_style_span(&mut self, style: ResolvedStyle<B>) {
self.commit_uncommitted_text();

self.tree.push(StyleTreeNode {
parent: Some(self.current_span),
style,
Expand All @@ -266,18 +291,11 @@ impl<B: Brush> TreeStyleBuilder<B> {

/// Ends the current span, returning to its parent.
pub(crate) fn pop_style_span(&mut self) {
self.commit_uncommitted_text();

self.current_span = self.tree[self.current_span]
.parent
.expect("Popped root style");
}

/// Buffers text in the current span, to be white space processed when it is committed.
pub(crate) fn push_text(&mut self, text: &str) {
self.uncommitted_text.push_str(text);
}

/// Computes style table + style runs and returns the final text buffer.
pub(crate) fn finish(
&mut self,
Expand All @@ -288,8 +306,6 @@ impl<B: Brush> TreeStyleBuilder<B> {
self.pop_style_span();
}

self.commit_uncommitted_text();

style_table.clear();
style_runs.clear();
style_table.extend_from_slice(&self.style_table);
Expand Down Expand Up @@ -546,4 +562,62 @@ mod tests {
assert_eq!(style_runs[3].style_index, 1);
assert_eq!(style_runs[4].style_index, 0);
}

#[test]
fn splitting_text_does_not_affect_white_space_processing() {
for mode in [
WhiteSpaceCollapse::Collapse,
WhiteSpaceCollapse::PreserveBreaks,
WhiteSpaceCollapse::Preserve,
WhiteSpaceCollapse::BreakSpaces,
] {
let style = ResolvedStyle {
white_space_collapse: mode,
..ResolvedStyle::default()
};
for input in [" a \t b\n c ", " \ta \t\r \t\n \tb \t", "a \u{2028} \tb"] {
let mut builder = TreeStyleBuilder::<u32>::default();
builder.begin(style.clone());
builder.push_text(input);
let mut expected_runs = Vec::new();
let expected = builder.finish(&mut Vec::new(), &mut expected_runs);

for split in (0..=input.len()).filter(|&i| input.is_char_boundary(i)) {
let mut builder = TreeStyleBuilder::<u32>::default();
builder.begin(style.clone());
builder.push_text(&input[..split]);
let (text_so_far, _) = builder.text_so_far();
assert!(expected.starts_with(text_so_far));
builder.push_text(&input[split..]);
let mut runs = Vec::new();
let text = builder.finish(&mut Vec::new(), &mut runs);
assert_eq!(text, expected, "{mode:?} {input:?} split at {split}");
assert_eq!(
runs.iter().map(|r| r.range.clone()).collect::<Vec<_>>(),
expected_runs
.iter()
.map(|r| r.range.clone())
.collect::<Vec<_>>(),
"{mode:?} {input:?} split at {split}"
);
}
}
}
}

#[test]
fn text_so_far_reports_pending_whitespace() {
let mut builder = TreeStyleBuilder::<u32>::default();
builder.begin(ResolvedStyle {
white_space_collapse: WhiteSpaceCollapse::Collapse,
..ResolvedStyle::default()
});
assert_eq!(builder.text_so_far(), ("", false));
builder.push_text(" a ");
assert_eq!(builder.text_so_far(), ("a", true));
builder.push_style_modification_span([ResolvedProperty::FontSize(20.)].into_iter());
builder.push_text(" b");
assert_eq!(builder.text_so_far(), ("a b", false));
builder.pop_style_span();
}
}
3 changes: 3 additions & 0 deletions parley_bench/benches/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use parley_bench::benches::{
};
use parley_bench::fontique_benches::system_fonts_init;
use parley_bench::query_benches::cursor;
use parley_bench::tree_builder_benches::{tree_builder_build, tree_builder_push};

tango_benchmarks!(
defaults(),
Expand All @@ -23,5 +24,7 @@ tango_benchmarks!(
long_line(),
page(),
cursor(),
tree_builder_push(),
tree_builder_build(),
system_fonts_init()
);
2 changes: 1 addition & 1 deletion parley_bench/src/benches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ pub fn line_breaking() -> Vec<Benchmark> {
/// Get the byte ranges of each consecutive chunk of `char_len` characters.
///
/// The last chunk holds the characters remaining.
fn chunks(text: &str, char_len: usize) -> impl Iterator<Item = Range<usize>> {
pub(crate) fn chunks(text: &str, char_len: usize) -> impl Iterator<Item = Range<usize>> {
let mut starts = text
.char_indices()
.map(|(byte_idx, _)| byte_idx)
Expand Down
1 change: 1 addition & 0 deletions parley_bench/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use parley::{
pub mod benches;
pub mod fontique_benches;
pub mod query_benches;
pub mod tree_builder_benches;

/// A color brush.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
Expand Down
Loading
Loading