Skip to content
Merged
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
106 changes: 105 additions & 1 deletion parley/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use super::style::{Brush, StyleProperty, TextStyle, WhiteSpaceCollapse};
use super::layout::Layout;

use alloc::string::String;
use core::ops::RangeBounds;
use core::ops::{Bound, Range, RangeBounds};

use crate::inline_box::InlineBox;
use crate::resolve::{ResolvedStyle, StyleRun, tree::ItemKind};
Expand Down Expand Up @@ -73,6 +73,96 @@ impl<B: Brush> RangedBuilder<'_, B> {
}
}

/// Builder for constructing a text layout from a style table and
/// indexed style runs.
#[must_use]
pub struct StyleRunBuilder<'a, B: Brush> {
pub(crate) scale: f32,
pub(crate) quantize: bool,
pub(crate) len: usize,
pub(crate) lcx: &'a mut LayoutContext<B>,
pub(crate) fcx: &'a mut FontContext,
pub(crate) cursor: usize,
}

impl<B: Brush> StyleRunBuilder<'_, B> {
/// Reserves additional capacity for styles and runs.
///
/// This is an optional optimization for callers that know counts
/// up front; call it before pushing styles and runs to reduce
/// reallocations.
pub fn reserve(&mut self, additional_styles: usize, additional_runs: usize) {
self.lcx.style_table.reserve(additional_styles);
self.lcx.style_runs.reserve(additional_runs);
}
Comment on lines +89 to +97
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be interesting at some point to have some reuse, so that there's not a reallocation cycle every time... but I guess I won't know until I'm using it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are reused already. This is just another knob.


/// Adds a fully-specified style to the shared style table and
/// returns its index.
pub fn push_style<'family, 'settings>(
&mut self,
style: TextStyle<'family, 'settings, B>,
) -> u16 {
let resolved = self
.lcx
.rcx
.resolve_entire_style_set(self.fcx, &style, self.scale);
let style_index = self.lcx.style_table.len();
assert!(style_index <= u16::MAX as usize, "too many styles");
self.lcx.style_table.push(resolved);
style_index as u16
}

/// Adds a style run referencing an entry from the style table.
///
/// Runs must be contiguous and non-overlapping, and must cover
/// `0..text.len()` once all runs have been added.
pub fn push_style_run(&mut self, style_index: u16, range: impl RangeBounds<usize>) {
let range = resolve_range(range, self.len);
assert!(
range.start == self.cursor,
"StyleRunBuilder expects contiguous non-overlapping runs"
);
assert!(
range.start <= range.end,
"StyleRunBuilder expects ordered ranges"
);
assert!(
(style_index as usize) < self.lcx.style_table.len(),
"StyleRunBuilder expects style indices that were previously added via push_style"
);
self.lcx.style_runs.push(StyleRun {
style_index,
range: range.clone(),
});
self.cursor = range.end;
}

pub fn push_inline_box(&mut self, inline_box: InlineBox) {
self.lcx.inline_boxes.push(inline_box);
}

pub fn build_into(self, layout: &mut Layout<B>, text: impl AsRef<str>) {
assert!(
self.cursor == self.len,
"StyleRunBuilder requires runs that cover the full text"
);
build_into_layout(
layout,
self.scale,
self.quantize,
text.as_ref(),
self.lcx,
self.fcx,
);
}

pub fn build(self, text: impl AsRef<str>) -> Layout<B> {
let mut layout = Layout::default();
self.build_into(&mut layout, text);
layout
}
}

/// Builder for constructing a text layout with a tree of attributes.
#[must_use]
pub struct TreeBuilder<'a, B: Brush> {
Expand Down Expand Up @@ -220,3 +310,17 @@ fn build_into_layout<B: Brush>(

layout.data.finish();
}

fn resolve_range(range: impl RangeBounds<usize>, len: usize) -> Range<usize> {
let start = match range.start_bound() {
Bound::Unbounded => 0,
Bound::Included(n) => *n,
Bound::Excluded(n) => *n + 1,
};
let end = match range.end_bound() {
Bound::Unbounded => len,
Bound::Included(n) => *n + 1,
Bound::Excluded(n) => *n,
};
start.min(len)..end.min(len)
}
31 changes: 30 additions & 1 deletion parley/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use alloc::{vec, vec::Vec};

use super::FontContext;
use super::builder::RangedBuilder;
use super::builder::{RangedBuilder, StyleRunBuilder};
use super::resolve::tree::TreeStyleBuilder;
use super::resolve::{RangedStyleBuilder, ResolveContext, ResolvedStyle, StyleRun};
use super::style::{Brush, TextStyle};
Expand Down Expand Up @@ -107,6 +107,35 @@ impl<B: Brush> LayoutContext<B> {
}
}

/// Create a builder for constructing a layout from indexed style runs.
///
/// Unlike [`Self::ranged_builder`], this builder expects callers to provide:
/// - a style table of fully specified [`TextStyle`] values (via [`StyleRunBuilder::push_style`])
/// - a complete sequence of **contiguous**, **non-overlapping** spans that cover
/// `0..text.len()` and reference style indices (via [`StyleRunBuilder::push_style_run`])
///
/// Parley then skips its internal range-splitting logic.
pub fn style_run_builder<'a>(
&'a mut self,
fcx: &'a mut FontContext,
text: &'a str,
scale: f32,
quantize: bool,
) -> StyleRunBuilder<'a, B> {
self.begin();

fcx.source_cache.prune(128, false);

StyleRunBuilder {
scale,
quantize,
len: text.len(),
lcx: self,
fcx,
cursor: 0,
}
}

/// Create a tree style layout builder.
///
/// Set `quantize` as `true` to have the layout coordinates aligned to pixel boundaries.
Expand Down
15 changes: 9 additions & 6 deletions parley/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
//! - [`FontContext`] and [`LayoutContext`] are resources which should be shared globally (or at coarse-grained boundaries).
//! - [`FontContext`] is database of fonts.
//! - [`LayoutContext`] is scratch space that allows for reuse of allocations between layouts.
//! - [`RangedBuilder`] and [`TreeBuilder`] which are builders for creating a [`Layout`].
//! - [`RangedBuilder`] allows styles to be specified as a flat `Vec` of spans
//! - [`TreeBuilder`] allows styles to be specified as a tree of spans
//! - Builders for creating a [`Layout`]:
//! - [`RangedBuilder`]: styles specified as a flat `Vec` of property spans
//! - [`TreeBuilder`]: styles specified as a tree of spans
//! - [`StyleRunBuilder`]: styles specified as a shared style table plus non-overlapping indexed runs
//!
//! They are constructed using the [`ranged_builder`](LayoutContext::ranged_builder) and [`tree_builder`](LayoutContext::ranged_builder) methods on [`LayoutContext`].
//! They are constructed using [`LayoutContext::ranged_builder`], [`LayoutContext::tree_builder`],
//! and [`LayoutContext::style_run_builder`].
//! - [`Layout`] which represents styled paragraph(s) of text and can perform shaping, line-breaking, bidi-reordering, and alignment of that text.
//!
//! `Layout` supports re-linebreaking and re-aligning many times (in case the width at which wrapping should occur changes). But if the text content or
//! the styles applied to that content change then a new `Layout` must be created using a new `RangedBuilder` or `TreeBuilder`.
//! the styles applied to that content change then a new `Layout` must be created using a new
//! `RangedBuilder`, `TreeBuilder`, or `StyleRunBuilder`.
//!
//! ## Usage Example
//!
Expand Down Expand Up @@ -128,7 +131,7 @@ mod tests;
pub use linebender_resource_handle::FontData;
pub use util::BoundingBox;

pub use builder::{RangedBuilder, TreeBuilder};
pub use builder::{RangedBuilder, StyleRunBuilder, TreeBuilder};
pub use context::LayoutContext;
pub use font::FontContext;
pub use inline_box::InlineBox;
Expand Down
80 changes: 79 additions & 1 deletion parley/src/tests/test_builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use text_primitives::FontFamilyName;
use super::utils::{ColorBrush, asserts::assert_eq_layout_data};
use crate::{
FontContext, FontFamily, FontFeatures, FontVariations, Layout, LayoutContext, LineHeight,
OverflowWrap, RangedBuilder, StyleProperty, TextStyle, TextWrapMode, TreeBuilder, WordBreak,
OverflowWrap, RangedBuilder, StyleProperty, StyleRunBuilder, TextStyle, TextWrapMode,
TreeBuilder, WordBreak,
};

// TODO: `FONT_FAMILY_LIST`, `load_fonts`, and `create_font_context` are
Expand Down Expand Up @@ -113,6 +114,20 @@ fn build_layout_with_tree(
layout
}

/// Generates a `Layout` with a style run builder.
fn build_layout_with_style_runs(
fcx: &mut FontContext,
lcx: &mut LayoutContext<ColorBrush>,
opts: &RangedOptions<'_>,
with_builder: impl Fn(&mut StyleRunBuilder<'_, ColorBrush>),
) -> Layout<ColorBrush> {
let mut rb = lcx.style_run_builder(fcx, opts.text, opts.scale, opts.quantize);
with_builder(&mut rb);
let mut layout = rb.build(opts.text);
layout.break_all_lines(opts.max_advance);
layout
}

/// Computes layout in various ways to ensure they all produce the same result.
///
/// ```text
Expand Down Expand Up @@ -306,6 +321,69 @@ fn builders_default() {
);
}

/// Test that `StyleRunBuilder` produces the same result as `RangedBuilder` when given equivalent
/// styles.
#[test]
fn builders_style_runs_match_ranged() {
let text = "Builders often wear hard hats.";
let scale = 2.;
let quantize = false;
let max_advance = Some(120.);

let root_style: TextStyle<'static, 'static, ColorBrush> = TextStyle {
font_family: FontFamily::from(FONT_FAMILY_LIST),
..TextStyle::default()
};

let split = text.len() / 2;
let mut modified_style = root_style.clone();
modified_style.font_size = 40.;
modified_style.letter_spacing = 1.25;

let mut fcx = create_font_context();
let mut lcx_a: LayoutContext<ColorBrush> = LayoutContext::new();
let mut lcx_b: LayoutContext<ColorBrush> = LayoutContext::new();

let ropts = RangedOptions {
scale,
quantize,
max_advance,
text,
};

let ranged = build_layout_with_ranged(&mut fcx, &mut lcx_a, &ropts, |rb| {
rb.push_default(FontFamily::from(FONT_FAMILY_LIST));
rb.push(
StyleProperty::FontSize(modified_style.font_size),
split..text.len(),
);
rb.push(
StyleProperty::LetterSpacing(modified_style.letter_spacing),
split..text.len(),
);
});

let runs = build_layout_with_style_runs(&mut fcx, &mut lcx_b, &ropts, |rb| {
let family: FontFamily<'static> = root_style.font_family.clone().into_owned();
let root_run: TextStyle<'static, 'static, ColorBrush> = TextStyle {
font_family: family.clone(),
..root_style.clone()
};

let modified_run: TextStyle<'static, 'static, ColorBrush> = TextStyle {
font_family: family,
..modified_style.clone()
};

let root_index = rb.push_style(root_run);
let modified_index = rb.push_style(modified_run);
rb.push_style_run(root_index, 0..split);
rb.push_style_run(modified_index, split..text.len());
});

assert_eq_layout_data(&ranged.data, &runs.data, "style_runs_match_ranged");
}

/// Test that all the builders behave the same when given the same root style.
#[test]
fn builders_root_only() {
Expand Down