diff --git a/parley/src/builder.rs b/parley/src/builder.rs index ebd252c4..526eae77 100644 --- a/parley/src/builder.rs +++ b/parley/src/builder.rs @@ -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}; @@ -73,6 +73,96 @@ impl 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, + pub(crate) fcx: &'a mut FontContext, + pub(crate) cursor: usize, +} + +impl 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); + } + + /// 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) { + 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, text: impl AsRef) { + 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) -> Layout { + 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> { @@ -220,3 +310,17 @@ fn build_into_layout( layout.data.finish(); } + +fn resolve_range(range: impl RangeBounds, len: usize) -> Range { + 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) +} diff --git a/parley/src/context.rs b/parley/src/context.rs index ba40cc7d..261191ab 100644 --- a/parley/src/context.rs +++ b/parley/src/context.rs @@ -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}; @@ -107,6 +107,35 @@ impl LayoutContext { } } + /// 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. diff --git a/parley/src/lib.rs b/parley/src/lib.rs index 00960121..713f7a07 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -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 //! @@ -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; diff --git a/parley/src/tests/test_builders.rs b/parley/src/tests/test_builders.rs index acce8398..8110d358 100644 --- a/parley/src/tests/test_builders.rs +++ b/parley/src/tests/test_builders.rs @@ -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 @@ -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, + opts: &RangedOptions<'_>, + with_builder: impl Fn(&mut StyleRunBuilder<'_, ColorBrush>), +) -> Layout { + 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 @@ -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 = LayoutContext::new(); + let mut lcx_b: LayoutContext = 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() {