diff --git a/parley/src/analysis/mod.rs b/parley/src/analysis/mod.rs index 671a91aff..460e78d96 100644 --- a/parley/src/analysis/mod.rs +++ b/parley/src/analysis/mod.rs @@ -6,7 +6,7 @@ pub(crate) mod cluster; use alloc::vec::Vec; use core::marker::PhantomData; -use crate::resolve::{RangedStyle, ResolvedStyle}; +use crate::resolve::StyleRun; use crate::{Brush, LayoutContext, WordBreak}; use icu_normalizer::properties::{ @@ -217,7 +217,8 @@ pub(crate) enum Boundary { pub(crate) fn analyze_text(lcx: &mut LayoutContext, mut text: &str) { struct WordBreakSegmentIter<'a, I: Iterator, B: Brush> { text: &'a str, - styles: I, + style_runs: I, + lcx: &'a LayoutContext, char_indices: core::str::CharIndices<'a>, current_char: (usize, char), building_range_start: usize, @@ -228,19 +229,26 @@ pub(crate) fn analyze_text(lcx: &mut LayoutContext, mut text: &str) impl<'a, I, B: Brush + 'a> WordBreakSegmentIter<'a, I, B> where - I: Iterator>, + I: Iterator, { - fn new(text: &'a str, styles: I, first_style: &RangedStyle) -> Self { + fn new( + text: &'a str, + style_runs: I, + lcx: &'a LayoutContext, + first_style_run: &StyleRun, + ) -> Self { let mut char_indices = text.char_indices(); let current_char_len = char_indices.next().unwrap(); + let first_style = &lcx.style_table[first_style_run.style_index as usize]; Self { text, - styles, + style_runs, + lcx, char_indices, current_char: current_char_len, - building_range_start: first_style.range.start, - previous_word_break_style: first_style.style.word_break, + building_range_start: first_style_run.range.start, + previous_word_break_style: first_style.word_break, done: false, _phantom: PhantomData, } @@ -249,7 +257,7 @@ pub(crate) fn analyze_text(lcx: &mut LayoutContext, mut text: &str) impl<'a, I, B: Brush + 'a> Iterator for WordBreakSegmentIter<'a, I, B> where - I: Iterator>, + I: Iterator, { type Item = (&'a str, WordBreak, bool); @@ -258,11 +266,11 @@ pub(crate) fn analyze_text(lcx: &mut LayoutContext, mut text: &str) return None; } - for style in self.styles.by_ref() { + for style_run in self.style_runs.by_ref() { // Empty style ranges are disallowed. - assert!(style.range.start < style.range.end); + assert!(style_run.range.start < style_run.range.end); - let style_start_index = style.range.start; + let style_start_index = style_run.range.start; let mut prev_char_index = self.current_char; // Find the character at the style boundary @@ -271,7 +279,8 @@ pub(crate) fn analyze_text(lcx: &mut LayoutContext, mut text: &str) self.current_char = self.char_indices.next().unwrap(); } - let current_word_break_style = style.style.word_break; + let current_word_break_style = + self.lcx.style_table[style_run.style_index as usize].word_break; if self.previous_word_break_style == current_word_break_style { continue; } @@ -298,24 +307,19 @@ pub(crate) fn analyze_text(lcx: &mut LayoutContext, mut text: &str) if text.is_empty() { text = " "; - if lcx.styles.is_empty() { - lcx.styles.push(RangedStyle { - style: ResolvedStyle::default(), - range: 0..0, - }); - } } // Line boundaries (word break naming refers to the line boundary determination config). // // This breaks text into sequences with similar line boundary config (part of style // information). If this config is consistent for all text, we use a fast path through this. - let Some((first_style, rest)) = lcx.styles.split_first() else { - panic!("No style info"); - }; + let (first_style_run, rest_runs) = lcx + .style_runs + .split_first() + .expect("analyze_text requires at least one style run"); let contiguous_word_break_substrings = - WordBreakSegmentIter::new(text, rest.iter(), first_style); + WordBreakSegmentIter::new(text, rest_runs.iter(), lcx, first_style_run); let mut global_offset = 0; let mut line_boundary_positions: Vec = Vec::new(); for (substring_index, (substring, word_break_strength, last)) in diff --git a/parley/src/builder.rs b/parley/src/builder.rs index ce7b20786..ebd252c4a 100644 --- a/parley/src/builder.rs +++ b/parley/src/builder.rs @@ -13,7 +13,7 @@ use alloc::string::String; use core::ops::RangeBounds; use crate::inline_box::InlineBox; -use crate::resolve::tree::ItemKind; +use crate::resolve::{ResolvedStyle, StyleRun, tree::ItemKind}; /// Builder for constructing a text layout with ranged attributes. #[must_use] @@ -50,8 +50,10 @@ impl RangedBuilder<'_, B> { } pub fn build_into(self, layout: &mut Layout, text: impl AsRef) { - // Apply RangedStyleBuilder styles to LayoutContext - self.lcx.ranged_style_builder.finish(&mut self.lcx.styles); + // Apply RangedStyleBuilder styles directly to style-table/style-run state. + self.lcx + .ranged_style_builder + .finish(&mut self.lcx.style_table, &mut self.lcx.style_runs); // Call generic layout builder method build_into_layout( @@ -130,8 +132,11 @@ impl TreeBuilder<'_, B> { #[inline] pub fn build_into(self, layout: &mut Layout) -> String { - // Apply TreeStyleBuilder styles to LayoutContext - let text = self.lcx.tree_style_builder.finish(&mut self.lcx.styles); + // Apply TreeStyleBuilder styles to LayoutContext. + let text = self + .lcx + .tree_style_builder + .finish(&mut self.lcx.style_table, &mut self.lcx.style_runs); // Call generic layout builder method build_into_layout(layout, self.scale, self.quantize, &text, self.lcx, self.fcx); @@ -155,6 +160,18 @@ fn build_into_layout( lcx: &mut LayoutContext, fcx: &mut FontContext, ) { + if text.is_empty() && lcx.style_runs.is_empty() { + lcx.style_table.push(ResolvedStyle::default()); + lcx.style_runs.push(StyleRun { + style_index: 0, + range: 0..0, + }); + } + assert!( + !lcx.style_runs.is_empty(), + "at least one style run is required" + ); + crate::analysis::analyze_text(lcx, text); layout.data.clear(); @@ -164,9 +181,9 @@ fn build_into_layout( layout.data.text_len = text.len(); let mut char_index = 0; - for (i, style) in lcx.styles.iter().enumerate() { - for _ in text[style.range.clone()].chars() { - lcx.info[char_index].1 = i as u16; + for style_run in &lcx.style_runs { + for _ in text[style_run.range.clone()].chars() { + lcx.info[char_index].1 = style_run.style_index; char_index += 1; } } @@ -175,7 +192,7 @@ fn build_into_layout( layout .data .styles - .extend(lcx.styles.iter().map(|s| s.style.as_layout_style())); + .extend(lcx.style_table.iter().map(|s| s.as_layout_style())); // Sort the inline boxes as subsequent code assumes that they are in text index order. // Note: It's important that this is a stable sort to allow users to control the order of contiguous inline boxes @@ -186,7 +203,7 @@ fn build_into_layout( super::shape::shape_text( &lcx.rcx, query, - &lcx.styles, + &lcx.style_table, &lcx.inline_boxes, &lcx.info, lcx.bidi.levels(), diff --git a/parley/src/context.rs b/parley/src/context.rs index 364a01d77..ba40cc7d6 100644 --- a/parley/src/context.rs +++ b/parley/src/context.rs @@ -8,7 +8,7 @@ use alloc::{vec, vec::Vec}; use super::FontContext; use super::builder::RangedBuilder; use super::resolve::tree::TreeStyleBuilder; -use super::resolve::{RangedStyle, RangedStyleBuilder, ResolveContext, ResolvedStyle}; +use super::resolve::{RangedStyleBuilder, ResolveContext, ResolvedStyle, StyleRun}; use super::style::{Brush, TextStyle}; use crate::analysis::{AnalysisDataSources, CharInfo}; @@ -22,7 +22,8 @@ use crate::shape::ShapeContext; /// This type is designed to be a global resource with only one per-application (or per-thread). pub struct LayoutContext { pub(crate) rcx: ResolveContext, - pub(crate) styles: Vec>, + pub(crate) style_table: Vec>, + pub(crate) style_runs: Vec, pub(crate) inline_boxes: Vec, pub(crate) bidi: BidiResolver, @@ -42,7 +43,8 @@ impl LayoutContext { pub fn new() -> Self { Self { rcx: ResolveContext::default(), - styles: vec![], + style_table: vec![], + style_runs: vec![], inline_boxes: vec![], bidi: BidiResolver::new(), ranged_style_builder: RangedStyleBuilder::default(), @@ -148,7 +150,8 @@ impl LayoutContext { fn begin(&mut self) { self.rcx.clear(); - self.styles.clear(); + self.style_table.clear(); + self.style_runs.clear(); self.inline_boxes.clear(); self.info.clear(); self.bidi.clear(); diff --git a/parley/src/resolve/mod.rs b/parley/src/resolve/mod.rs index a680600f4..c979f0fe5 100644 --- a/parley/src/resolve/mod.rs +++ b/parley/src/resolve/mod.rs @@ -31,6 +31,13 @@ pub(crate) struct RangedStyle { pub(crate) range: Range, } +/// Run that references a style in a shared style table. +#[derive(Debug, Clone)] +pub(crate) struct StyleRun { + pub(crate) style_index: u16, + pub(crate) range: Range, +} + #[derive(Clone)] struct RangedProperty { property: ResolvedProperty, diff --git a/parley/src/resolve/range.rs b/parley/src/resolve/range.rs index ca77c9eda..08180d529 100644 --- a/parley/src/resolve/range.rs +++ b/parley/src/resolve/range.rs @@ -5,7 +5,7 @@ use alloc::vec; -use super::{Brush, RangedProperty, RangedStyle, ResolvedProperty, ResolvedStyle, Vec}; +use super::{Brush, RangedProperty, RangedStyle, ResolvedProperty, ResolvedStyle, StyleRun, Vec}; use core::ops::{Bound, Range, RangeBounds}; /// Builder for constructing an ordered sequence of non-overlapping ranged @@ -13,6 +13,7 @@ use core::ops::{Bound, Range, RangeBounds}; #[derive(Clone)] pub(crate) struct RangedStyleBuilder { properties: Vec>, + scratch_styles: Vec>, root_style: ResolvedStyle, len: usize, } @@ -21,6 +22,7 @@ impl Default for RangedStyleBuilder { fn default() -> Self { Self { properties: vec![], + scratch_styles: vec![], root_style: ResolvedStyle::default(), // We use `usize::MAX` as a sentinel that `begin` hasn't been called. // This is required (rather than requiring the root style in the constructor) @@ -36,6 +38,7 @@ impl RangedStyleBuilder { /// The provided `root_style` is the default style applied to all text unless overridden. pub(crate) fn begin(&mut self, root_style: ResolvedStyle, len: usize) { self.properties.clear(); + self.scratch_styles.clear(); self.root_style = root_style; self.len = len; } @@ -67,13 +70,21 @@ impl RangedStyleBuilder { self.properties.push(RangedProperty { property, range }); } - /// Computes the sequence of ranged styles. - pub(crate) fn finish(&mut self, styles: &mut Vec>) { + /// Computes style table + style runs for the ranged properties. + pub(crate) fn finish( + &mut self, + style_table: &mut Vec>, + style_runs: &mut Vec, + ) { + style_table.clear(); + style_runs.clear(); if self.len == usize::MAX { self.properties.clear(); + self.scratch_styles.clear(); self.root_style = ResolvedStyle::default(); return; } + let styles = &mut self.scratch_styles; styles.push(RangedStyle { style: self.root_style.clone(), range: 0..self.len, @@ -142,6 +153,16 @@ impl RangedStyleBuilder { } styles.truncate(styles.len() - merged_count); + style_table.reserve(styles.len()); + style_runs.reserve(styles.len()); + for (style_index, style) in styles.drain(..).enumerate() { + style_table.push(style.style); + style_runs.push(StyleRun { + style_index: style_index as u16, + range: style.range, + }); + } + self.properties.clear(); self.root_style = ResolvedStyle::default(); self.len = usize::MAX; diff --git a/parley/src/resolve/tree.rs b/parley/src/resolve/tree.rs index f6ec58456..9e280bd2b 100644 --- a/parley/src/resolve/tree.rs +++ b/parley/src/resolve/tree.rs @@ -2,17 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT //! Hierarchical tree based style application. -use alloc::borrow::Cow; use alloc::{string::String, vec::Vec}; use crate::style::WhiteSpaceCollapse; -use super::{Brush, RangedStyle, ResolvedProperty, ResolvedStyle}; +use super::{Brush, ResolvedProperty, ResolvedStyle, StyleRun}; #[derive(Debug, Clone)] struct StyleTreeNode { parent: Option, style: ResolvedStyle, + style_id: Option, } #[derive(Clone, Copy, PartialEq)] @@ -26,7 +26,8 @@ pub(crate) enum ItemKind { #[derive(Clone)] pub(crate) struct TreeStyleBuilder { tree: Vec>, - flatted_styles: Vec>, + style_table: Vec>, + style_runs: Vec, white_space_collapse: WhiteSpaceCollapse, text: String, uncommitted_text: String, @@ -45,7 +46,8 @@ impl Default for TreeStyleBuilder { fn default() -> Self { Self { tree: Vec::new(), - flatted_styles: Vec::new(), + style_table: Vec::new(), + style_runs: Vec::new(), white_space_collapse: WhiteSpaceCollapse::Preserve, text: String::new(), uncommitted_text: String::new(), @@ -62,7 +64,8 @@ impl TreeStyleBuilder { /// The provided `root_style` is the default style applied to all text unless overridden. pub(crate) fn begin(&mut self, root_style: ResolvedStyle) { self.tree.clear(); - self.flatted_styles.clear(); + self.style_table.clear(); + self.style_runs.clear(); self.white_space_collapse = WhiteSpaceCollapse::Preserve; self.text.clear(); self.uncommitted_text.clear(); @@ -70,6 +73,7 @@ impl TreeStyleBuilder { self.tree.push(StyleTreeNode { parent: None, style: root_style, + style_id: None, }); self.current_span = 0; self.is_span_first = true; @@ -88,10 +92,11 @@ impl TreeStyleBuilder { } pub(crate) fn push_uncommitted_text(&mut self, is_span_last: bool) { - let span_text: Cow<'_, str> = match self.white_space_collapse { - WhiteSpaceCollapse::Preserve => Cow::from(&self.uncommitted_text), + let uncommitted_text = core::mem::take(&mut self.uncommitted_text); + let span_text = match self.white_space_collapse { + WhiteSpaceCollapse::Preserve => uncommitted_text, WhiteSpaceCollapse::Collapse => { - let mut span_text = self.uncommitted_text.as_str(); + let mut span_text = uncommitted_text.as_str(); if self.is_span_first || (self.last_item_kind == ItemKind::TextRun @@ -109,7 +114,7 @@ impl TreeStyleBuilder { // Collapse spaces let mut last_char_whitespace = false; - let span_text: String = span_text + span_text .chars() .filter_map(|c: char| { let this_char_whitespace = c.is_ascii_whitespace(); @@ -126,28 +131,33 @@ impl TreeStyleBuilder { Some(c) } }) - .collect(); - - Cow::from(span_text) + .collect() } }; - let span_text = span_text.as_ref(); // Nothing to do if there is no uncommitted text. if span_text.is_empty() { - self.uncommitted_text.clear(); return; } let range = self.text.len()..(self.text.len() + span_text.len()); - let style = self.current_style(); - self.flatted_styles.push(RangedStyle { style, range }); - self.text.push_str(span_text); - self.uncommitted_text.clear(); + let style_index = self.resolve_current_style_id(); + self.style_runs.push(StyleRun { style_index, range }); + self.text.push_str(&span_text); self.is_span_first = false; self.last_item_kind = ItemKind::TextRun; } + fn resolve_current_style_id(&mut self) -> u16 { + if let Some(style_id) = self.tree[self.current_span].style_id { + return style_id; + } + let style_id = self.style_table.len() as u16; + self.style_table.push(self.current_style()); + self.tree[self.current_span].style_id = Some(style_id); + style_id + } + pub(crate) fn current_text_len(&self) -> usize { self.text.len() } @@ -158,6 +168,7 @@ impl TreeStyleBuilder { self.tree.push(StyleTreeNode { parent: Some(self.current_span), style, + style_id: None, }); self.current_span = self.tree.len() - 1; self.is_span_first = true; @@ -189,17 +200,111 @@ impl TreeStyleBuilder { } } - /// Computes the sequence of ranged styles. - pub(crate) fn finish(&mut self, styles: &mut Vec>) -> String { + /// Computes style table + style runs and returns the final text buffer. + pub(crate) fn finish( + &mut self, + style_table: &mut Vec>, + style_runs: &mut Vec, + ) -> String { while self.tree[self.current_span].parent.is_some() { self.pop_style_span(); } self.push_uncommitted_text(true); - styles.clear(); - styles.extend_from_slice(&self.flatted_styles); + style_table.clear(); + style_runs.clear(); + style_table.extend_from_slice(&self.style_table); + style_runs.extend_from_slice(&self.style_runs); core::mem::take(&mut self.text) } } + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec::Vec; + use core::ops::Range; + + #[test] + fn reuses_style_id_when_returning_to_parent_span() { + let mut builder = TreeStyleBuilder::::default(); + builder.begin(ResolvedStyle::default()); + builder.push_text("A"); + builder.push_style_modification_span([ResolvedProperty::FontSize(20.)].into_iter()); + builder.push_text("B"); + builder.pop_style_span(); + builder.push_text("C"); + + let mut style_table = Vec::new(); + let mut style_runs = Vec::new(); + let text = builder.finish(&mut style_table, &mut style_runs); + + assert_eq!(text, "ABC"); + assert_eq!(style_table.len(), 2); + assert_eq!(style_runs.len(), 3); + assert_eq!(style_runs[0].style_index, 0); + assert_eq!(style_runs[1].style_index, 1); + assert_eq!(style_runs[2].style_index, 0); + assert_eq!(style_runs[0].range, Range { start: 0, end: 1 }); + assert_eq!(style_runs[1].range, Range { start: 1, end: 2 }); + assert_eq!(style_runs[2].range, Range { start: 2, end: 3 }); + } + + #[test] + fn reuses_root_style_id_across_multiple_pop_return_cycles() { + let mut builder = TreeStyleBuilder::::default(); + builder.begin(ResolvedStyle::default()); + builder.push_text("A"); + builder.push_style_modification_span([ResolvedProperty::FontSize(20.)].into_iter()); + builder.push_text("B"); + builder.pop_style_span(); + builder.push_text("C"); + builder.push_style_modification_span([ResolvedProperty::LetterSpacing(1.)].into_iter()); + builder.push_text("D"); + builder.pop_style_span(); + builder.push_text("E"); + + let mut style_table = Vec::new(); + let mut style_runs = Vec::new(); + let text = builder.finish(&mut style_table, &mut style_runs); + + assert_eq!(text, "ABCDE"); + assert_eq!(style_table.len(), 3); + assert_eq!(style_runs.len(), 5); + assert_eq!(style_runs[0].style_index, 0); + assert_eq!(style_runs[1].style_index, 1); + assert_eq!(style_runs[2].style_index, 0); + assert_eq!(style_runs[3].style_index, 2); + assert_eq!(style_runs[4].style_index, 0); + } + + #[test] + fn reuses_parent_and_root_style_ids_after_nested_pop() { + let mut builder = TreeStyleBuilder::::default(); + builder.begin(ResolvedStyle::default()); + builder.push_text("R"); + builder.push_style_modification_span([ResolvedProperty::FontSize(20.)].into_iter()); + builder.push_text("A"); + builder.push_style_modification_span([ResolvedProperty::LetterSpacing(1.)].into_iter()); + builder.push_text("B"); + builder.pop_style_span(); + builder.push_text("C"); + builder.pop_style_span(); + builder.push_text("D"); + + let mut style_table = Vec::new(); + let mut style_runs = Vec::new(); + let text = builder.finish(&mut style_table, &mut style_runs); + + assert_eq!(text, "RABCD"); + assert_eq!(style_table.len(), 3); + assert_eq!(style_runs.len(), 5); + assert_eq!(style_runs[0].style_index, 0); + assert_eq!(style_runs[1].style_index, 1); + assert_eq!(style_runs[2].style_index, 2); + assert_eq!(style_runs[3].style_index, 1); + assert_eq!(style_runs[4].style_index, 0); + } +} diff --git a/parley/src/shape/mod.rs b/parley/src/shape/mod.rs index 0d6df1985..1f69ac621 100644 --- a/parley/src/shape/mod.rs +++ b/parley/src/shape/mod.rs @@ -9,7 +9,7 @@ use core::mem; use core::ops::RangeInclusive; use super::layout::Layout; -use super::resolve::{RangedStyle, ResolveContext, Resolved}; +use super::resolve::{ResolveContext, Resolved, ResolvedStyle}; use super::style::{Brush, FontFeature, FontVariation}; use crate::analysis::cluster::{Char, CharCluster, Status}; use crate::analysis::{AnalysisDataSources, CharInfo}; @@ -64,7 +64,7 @@ struct Item { pub(crate) fn shape_text<'a, B: Brush>( rcx: &'a ResolveContext, mut fq: Query<'a>, - styles: &'a [RangedStyle], + styles: &'a [ResolvedStyle], inline_boxes: &[InlineBox], infos: &[(CharInfo, u16)], levels: &[u8], @@ -89,7 +89,7 @@ pub(crate) fn shape_text<'a, B: Brush>( } // Setup mutable state for iteration - let mut style = &styles[0].style; + let mut style = &styles[0]; let mut item = Item { style_index: 0, size: style.font_size, @@ -124,7 +124,7 @@ pub(crate) fn shape_text<'a, B: Brush>( let level = levels.get(char_index).copied().unwrap_or(0); if item.style_index != *style_index { item.style_index = *style_index; - style = &styles[*style_index as usize].style; + style = &styles[*style_index as usize]; if !nearly_eq(style.font_size, item.size) || style.locale != item.locale || style.font_variations != item.variations @@ -267,7 +267,7 @@ fn fill_cluster_in_place( fn shape_item<'a, B: Brush>( fq: &mut Query<'a>, rcx: &'a ResolveContext, - styles: &'a [RangedStyle], + styles: &'a [ResolvedStyle], item: &Item, scx: &mut ShapeContext, text: &str, @@ -498,7 +498,7 @@ struct FontSelector<'a, 'b, B: Brush> { query: &'b mut Query<'a>, fonts_id: Option, rcx: &'a ResolveContext, - styles: &'a [RangedStyle], + styles: &'a [ResolvedStyle], style_index: u16, attrs: fontique::Attributes, variations: &'a [FontVariation], @@ -509,12 +509,12 @@ impl<'a, 'b, B: Brush> FontSelector<'a, 'b, B> { fn new( query: &'b mut Query<'a>, rcx: &'a ResolveContext, - styles: &'a [RangedStyle], + styles: &'a [ResolvedStyle], style_index: u16, fb_script: fontique::Script, locale: Option, ) -> Self { - let style = &styles[style_index as usize].style; + let style = &styles[style_index as usize]; let fonts_id = style.font_family.id(); let fonts = rcx.stack(style.font_family).unwrap_or(&[]); let attrs = fontique::Attributes { @@ -550,7 +550,7 @@ impl<'a, 'b, B: Brush> FontSelector<'a, 'b, B> { let is_emoji = cluster.is_emoji; if style_index != self.style_index || is_emoji || self.fonts_id.is_none() { self.style_index = style_index; - let style = &self.styles[style_index as usize].style; + let style = &self.styles[style_index as usize]; let fonts_id = style.font_family.id(); let fonts = self.rcx.stack(style.font_family).unwrap_or(&[]); diff --git a/parley/src/tests/utils/asserts.rs b/parley/src/tests/utils/asserts.rs index a9ff39f53..af401d135 100644 --- a/parley/src/tests/utils/asserts.rs +++ b/parley/src/tests/utils/asserts.rs @@ -3,10 +3,43 @@ //! Various helper functions to assert truths during testing. +use std::vec::Vec; + use crate::{Brush, data::LayoutData}; +fn canonicalize_layout_data(layout_data: &LayoutData) -> LayoutData { + let mut normalized = layout_data.clone(); + let mut canonical_styles = Vec::with_capacity(normalized.styles.len()); + let mut remap = Vec::with_capacity(normalized.styles.len()); + + for style in &normalized.styles { + if let Some(index) = canonical_styles + .iter() + .position(|existing| existing == style) + { + remap.push(index as u16); + } else { + let index = canonical_styles.len() as u16; + canonical_styles.push(style.clone()); + remap.push(index); + } + } + + for cluster in &mut normalized.clusters { + cluster.style_index = remap[cluster.style_index as usize]; + } + for glyph in &mut normalized.glyphs { + glyph.style_index = remap[glyph.style_index as usize]; + } + normalized.styles = canonical_styles; + normalized +} + /// Assert that the two provided `LayoutData` are equal. pub(crate) fn assert_eq_layout_data(a: &LayoutData, b: &LayoutData, case: &str) { + let a = canonicalize_layout_data(a); + let b = canonicalize_layout_data(b); + assert_eq!(a.scale, b.scale, "{case} scale mismatch"); assert_eq!(a.quantize, b.quantize, "{case} quantize mismatch"); assert_eq!(a.base_level, b.base_level, "{case} base_level mismatch");