diff --git a/src/eval/collapse.rs b/src/eval/collapse.rs index 31581986b..5e7a25f88 100644 --- a/src/eval/collapse.rs +++ b/src/eval/collapse.rs @@ -15,7 +15,7 @@ enum Last { Supportive, } -impl<'a, T: Merge> CollapsingBuilder<'a, T> { +impl<'a, T> CollapsingBuilder<'a, T> { /// Create a new style-vec builder. pub fn new() -> Self { Self { @@ -82,45 +82,19 @@ impl<'a, T: Merge> CollapsingBuilder<'a, T> { fn flush(&mut self, supportive: bool) { for (item, styles, strength) in self.staged.drain(..) { if supportive || strength.is_none() { - push_merging(&mut self.builder, item, styles); + self.builder.push(item, styles); } } } /// Push a new item into the style vector. fn push(&mut self, item: T, styles: StyleChain<'a>) { - push_merging(&mut self.builder, item, styles); + self.builder.push(item, styles); } } -/// Push an item into a style-vec builder, trying to merging it with the -/// previous item. -fn push_merging<'a, T: Merge>( - builder: &mut StyleVecBuilder<'a, T>, - item: T, - styles: StyleChain<'a>, -) { - if let Some((prev_item, prev_styles)) = builder.last_mut() { - if styles == prev_styles { - if prev_item.merge(&item) { - return; - } - } - } - - builder.push(item, styles); -} - -impl<'a, T: Merge> Default for CollapsingBuilder<'a, T> { +impl<'a, T> Default for CollapsingBuilder<'a, T> { fn default() -> Self { Self::new() } } - -/// Defines if and how to merge two adjacent items in a [`CollapsingBuilder`]. -pub trait Merge { - /// Try to merge the items, returning whether they were merged. - /// - /// Defaults to not merging. - fn merge(&mut self, next: &Self) -> bool; -} diff --git a/src/geom/fraction.rs b/src/geom/fraction.rs index f71886032..a913c0c28 100644 --- a/src/geom/fraction.rs +++ b/src/geom/fraction.rs @@ -111,3 +111,9 @@ assign_impl!(Fraction += Fraction); assign_impl!(Fraction -= Fraction); assign_impl!(Fraction *= f64); assign_impl!(Fraction /= f64); + +impl Sum for Fraction { + fn sum>(iter: I) -> Self { + Self(iter.map(|s| s.0).sum()) + } +} diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs index a3947e346..6b43c8b7f 100644 --- a/src/library/layout/flow.rs +++ b/src/library/layout/flow.rs @@ -62,12 +62,6 @@ impl Layout for FlowNode { } } -impl Merge for FlowChild { - fn merge(&mut self, _: &Self) -> bool { - false - } -} - impl Debug for FlowNode { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_str("Flow ")?; diff --git a/src/library/prelude.rs b/src/library/prelude.rs index a1ebe6eff..a79f3c607 100644 --- a/src/library/prelude.rs +++ b/src/library/prelude.rs @@ -9,8 +9,8 @@ pub use typst_macros::node; pub use crate::diag::{with_alternative, At, Error, StrResult, TypError, TypResult}; pub use crate::eval::{ - Arg, Args, Array, Cast, Content, Dict, Fold, Func, Key, Layout, LayoutNode, Merge, - Node, RawAlign, RawLength, RawStroke, Regions, Resolve, Scope, Show, ShowNode, Smart, + Arg, Args, Array, Cast, Content, Dict, Fold, Func, Key, Layout, LayoutNode, Node, + RawAlign, RawLength, RawStroke, Regions, Resolve, Scope, Show, ShowNode, Smart, StyleChain, StyleMap, StyleVec, Value, }; pub use crate::frame::*; diff --git a/src/library/text/par.rs b/src/library/text/par.rs index a2f8160f0..57ced1896 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -7,7 +7,7 @@ use super::{shape, Lang, ShapedText, TextNode}; use crate::font::FontStore; use crate::library::layout::Spacing; use crate::library::prelude::*; -use crate::util::{ArcExt, EcoString, RangeExt, SliceExt}; +use crate::util::{ArcExt, EcoString, SliceExt}; /// Arrange text, spacing and inline-level nodes into a paragraph. #[derive(Hash)] @@ -54,38 +54,6 @@ impl ParNode { } } -impl ParNode { - /// Concatenate all text in the paragraph into one string, replacing spacing - /// with a space character and other non-text nodes with the object - /// replacement character. - fn collect_text(&self) -> String { - let mut text = String::new(); - for string in self.strings() { - text.push_str(string); - } - text - } - - /// The range of each item in the collected text. - fn ranges(&self) -> impl Iterator + '_ { - let mut cursor = 0; - self.strings().map(move |string| { - let start = cursor; - cursor += string.len(); - start .. cursor - }) - } - - /// The string representation of each child. - fn strings(&self) -> impl Iterator { - self.0.items().map(|child| match child { - ParChild::Text(text) => text, - ParChild::Spacing(_) => " ", - ParChild::Node(_) => "\u{FFFC}", - }) - } -} - impl Layout for ParNode { fn layout( &self, @@ -93,12 +61,13 @@ impl Layout for ParNode { regions: &Regions, styles: StyleChain, ) -> TypResult>> { - // Collect all text into one string and perform BiDi analysis. - let text = self.collect_text(); + // Collect all text into one string for BiDi analysis. + let (text, segments) = collect(self, &styles); - // Prepare paragraph layout by building a representation on which we can - // do line breaking without layouting each and every line from scratch. - let p = prepare(ctx, self, &text, regions, &styles)?; + // Perform BiDi analysis and then prepare paragraph layout by building a + // representation on which we can do line breaking without layouting + // each and every line from scratch. + let p = prepare(ctx, self, &text, segments, regions, styles)?; // Break the paragraph into lines. let lines = linebreak(&p, &mut ctx.fonts, regions.first.x); @@ -125,17 +94,6 @@ impl Debug for ParChild { } } -impl Merge for ParChild { - fn merge(&mut self, next: &Self) -> bool { - if let (Self::Text(left), Self::Text(right)) = (self, next) { - left.push_str(right); - true - } else { - false - } - } -} - /// A horizontal alignment. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct HorizontalAlign(pub RawAlign); @@ -213,6 +171,11 @@ impl LinebreakNode { /// Range of a substring of text. type Range = std::ops::Range; +// The characters by which spacing and nodes are replaced in the paragraph's +// full text. +const SPACING_REPLACE: char = ' '; +const NODE_REPLACE: char = '\u{FFFC}'; + /// A paragraph representation in which children are already layouted and text /// is already preshaped. /// @@ -222,25 +185,32 @@ type Range = std::ops::Range; struct Preparation<'a> { /// Bidirectional text embedding levels for the paragraph. bidi: BidiInfo<'a>, + /// Text runs, spacing and layouted nodes. + items: Vec>, + /// The styles shared by all children. + styles: StyleChain<'a>, /// The paragraph's children. children: &'a StyleVec, - /// Spacing, separated text runs and layouted nodes. - items: Vec>, - /// The ranges of the items in `bidi.text`. - ranges: Vec, - /// The shared styles. - styles: StyleChain<'a>, } impl<'a> Preparation<'a> { - /// Find the item whose range contains the `text_offset`. - fn find(&self, text_offset: usize) -> Option<&ParItem<'a>> { - self.find_idx(text_offset).map(|idx| &self.items[idx]) + /// Find the item which is at the `text_offset`. + fn find(&self, text_offset: usize) -> Option<&Item<'a>> { + self.find_idx_and_offset(text_offset).map(|(idx, _)| &self.items[idx]) } - /// Find the index of the item whose range contains the `text_offset`. - fn find_idx(&self, text_offset: usize) -> Option { - self.ranges.binary_search_by(|r| r.locate(text_offset)).ok() + /// Find the index and text offset of the item which is at the + /// `text_offset`. + fn find_idx_and_offset(&self, text_offset: usize) -> Option<(usize, usize)> { + let mut cursor = 0; + for (idx, item) in self.items.iter().enumerate() { + let end = cursor + item.len(); + if (cursor .. end).contains(&text_offset) { + return Some((idx, cursor)); + } + cursor = end; + } + None } /// Get a style property, but only if it is the same for all children of the @@ -253,19 +223,43 @@ impl<'a> Preparation<'a> { } } +/// A segment of one or multiple collapsed children. +#[derive(Debug, Copy, Clone)] +enum Segment<'a> { + /// One or multiple collapsed text or text-equivalent children. Stores how + /// long the segment is (in bytes of the full text string). + Text(usize), + /// Horizontal spacing between other segments. + Spacing(Spacing), + /// An arbitrary inline-level layout node. + Node(&'a LayoutNode), +} + +impl Segment<'_> { + /// The text length of the item. + fn len(&self) -> usize { + match *self { + Self::Text(len) => len, + Self::Spacing(_) => SPACING_REPLACE.len_utf8(), + Self::Node(_) => NODE_REPLACE.len_utf8(), + } + } +} + /// A prepared item in a paragraph layout. -enum ParItem<'a> { +#[derive(Debug)] +enum Item<'a> { + /// A shaped text run with consistent direction. + Text(ShapedText<'a>), /// Absolute spacing between other items. Absolute(Length), /// Fractional spacing between other items. Fractional(Fraction), - /// A shaped text run with consistent direction. - Text(ShapedText<'a>), /// A layouted child node. Frame(Frame), } -impl<'a> ParItem<'a> { +impl<'a> Item<'a> { /// If this a text item, return it. fn text(&self) -> Option<&ShapedText<'a>> { match self { @@ -273,6 +267,25 @@ impl<'a> ParItem<'a> { _ => None, } } + + /// The text length of the item. + fn len(&self) -> usize { + match self { + Self::Text(shaped) => shaped.text.len(), + Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(), + Self::Frame(_) => NODE_REPLACE.len_utf8(), + } + } + + /// The natural width of the item. + fn width(&self) -> Length { + match self { + Item::Text(shaped) => shaped.width, + Item::Absolute(v) => *v, + Item::Fractional(_) => Length::zero(), + Item::Frame(frame) => frame.size.x, + } + } } /// A layouted line, consisting of a sequence of layouted paragraph items that @@ -287,23 +300,17 @@ impl<'a> ParItem<'a> { struct Line<'a> { /// Bidi information about the paragraph. bidi: &'a BidiInfo<'a>, - /// The range the line spans in the paragraph. + /// The (untrimmed) range the line spans in the paragraph. range: Range, /// A reshaped text item if the line sliced up a text item at the start. - first: Option>, + first: Option>, /// Middle items which don't need to be reprocessed. - items: &'a [ParItem<'a>], + items: &'a [Item<'a>], /// A reshaped text item if the line sliced up a text item at the end. If /// there is only one text item, this takes precedence over `first`. - last: Option>, - /// The ranges, indexed as `[first, ..items, last]`. The ranges for `first` - /// and `last` aren't trimmed to the line, but it doesn't matter because - /// we're just checking which range an index falls into. - ranges: &'a [Range], + last: Option>, /// The width of the line. width: Length, - /// The sum of fractions in the line. - fr: Fraction, /// Whether the line ends at a mandatory break. mandatory: bool, /// Whether the line ends with a hyphen or dash, either naturally or through @@ -313,24 +320,36 @@ struct Line<'a> { impl<'a> Line<'a> { /// Iterate over the line's items. - fn items(&self) -> impl Iterator> { + fn items(&self) -> impl Iterator> { self.first.iter().chain(self.items).chain(&self.last) } - /// Find the index of the item whose range contains the `text_offset`. - fn find(&self, text_offset: usize) -> Option { - self.ranges.binary_search_by(|r| r.locate(text_offset)).ok() + /// Get the item at the index. + fn get(&self, index: usize) -> Option<&Item<'a>> { + self.items().nth(index) } - /// Get the item at the index. - fn get(&self, index: usize) -> Option<&ParItem<'a>> { - self.items().nth(index) + /// Find the index of the item whose range contains the `text_offset`. + fn find(&self, text_offset: usize) -> usize { + let mut idx = 0; + let mut cursor = self.range.start; + + for item in self.items() { + let end = cursor + item.len(); + if (cursor .. end).contains(&text_offset) { + return idx; + } + cursor = end; + idx += 1; + } + + idx.saturating_sub(1) } // How many justifiable glyphs the line contains. fn justifiables(&self) -> usize { let mut count = 0; - for shaped in self.items().filter_map(ParItem::text) { + for shaped in self.items().filter_map(Item::text) { count += shaped.justifiables(); } count @@ -339,11 +358,67 @@ impl<'a> Line<'a> { /// How much of the line is stretchable spaces. fn stretch(&self) -> Length { let mut stretch = Length::zero(); - for shaped in self.items().filter_map(ParItem::text) { + for shaped in self.items().filter_map(Item::text) { stretch += shaped.stretch(); } stretch } + + /// The sum of fractions in the line. + fn fr(&self) -> Fraction { + self.items() + .filter_map(|item| match item { + Item::Fractional(fr) => Some(*fr), + _ => None, + }) + .sum() + } +} + +/// Collect all text of the paragraph into one string. This also performs +/// string-level preprocessing like case transformations. +fn collect<'a>( + par: &'a ParNode, + styles: &'a StyleChain<'a>, +) -> (String, Vec<(Segment<'a>, StyleChain<'a>)>) { + let mut full = String::new(); + let mut segments = vec![]; + + for (child, map) in par.0.iter() { + let styles = map.chain(&styles); + let segment = match child { + ParChild::Text(text) => { + let prev = full.len(); + if let Some(case) = styles.get(TextNode::CASE) { + full.push_str(&case.apply(text)); + } else { + full.push_str(text); + } + Segment::Text(full.len() - prev) + } + ParChild::Spacing(spacing) => { + full.push(SPACING_REPLACE); + Segment::Spacing(*spacing) + } + ParChild::Node(node) => { + full.push(NODE_REPLACE); + Segment::Node(node) + } + }; + + if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) = + (segments.last_mut(), segment) + { + if *last_styles == styles { + *last_len += len; + continue; + } + } + + segments.push((segment, styles)); + } + + (full, segments) } /// Prepare paragraph layout by shaping the whole paragraph and layouting all @@ -352,8 +427,9 @@ fn prepare<'a>( ctx: &mut Context, par: &'a ParNode, text: &'a str, + segments: Vec<(Segment<'a>, StyleChain<'a>)>, regions: &Regions, - styles: &'a StyleChain, + styles: StyleChain<'a>, ) -> TypResult> { let bidi = BidiInfo::new(&text, match styles.get(TextNode::DIR) { Dir::LTR => Some(Level::ltr()), @@ -362,38 +438,33 @@ fn prepare<'a>( }); let mut items = vec![]; - let mut ranges = vec![]; + let mut cursor = 0; // Layout the children and collect them into items. - for (range, (child, map)) in par.ranges().zip(par.0.iter()) { - let styles = map.chain(styles); - match child { - ParChild::Text(_) => { - // TODO: Also split by language. - let mut cursor = range.start; - for (level, count) in bidi.levels[range].group() { - let start = cursor; - cursor += count; - let subrange = start .. cursor; - let text = &bidi.text[subrange.clone()]; + for (segment, styles) in segments { + match segment { + Segment::Text(len) => { + // TODO: Also split by script. + let mut start = cursor; + for (level, count) in bidi.levels[cursor .. cursor + len].group() { + let end = start + count; + let text = &bidi.text[start .. end]; let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; let shaped = shape(&mut ctx.fonts, text, styles, dir); - items.push(ParItem::Text(shaped)); - ranges.push(subrange); + items.push(Item::Text(shaped)); + start = end; } } - ParChild::Spacing(spacing) => match *spacing { + Segment::Spacing(spacing) => match spacing { Spacing::Relative(v) => { let resolved = v.resolve(styles).relative_to(regions.base.x); - items.push(ParItem::Absolute(resolved)); - ranges.push(range); + items.push(Item::Absolute(resolved)); } Spacing::Fractional(v) => { - items.push(ParItem::Fractional(v)); - ranges.push(range); + items.push(Item::Fractional(v)); } }, - ParChild::Node(node) => { + Segment::Node(node) => { // Prevent margin overhang in the inline node except if there's // just this one. let local; @@ -407,19 +478,14 @@ fn prepare<'a>( let size = Size::new(regions.first.x, regions.base.y); let pod = Regions::one(size, regions.base, Spec::splat(false)); let frame = node.layout(ctx, &pod, styles)?.remove(0); - items.push(ParItem::Frame(Arc::take(frame))); - ranges.push(range); + items.push(Item::Frame(Arc::take(frame))); } } + + cursor += segment.len(); } - Ok(Preparation { - bidi, - children: &par.0, - items, - ranges, - styles: *styles, - }) + Ok(Preparation { bidi, items, styles, children: &par.0 }) } /// Find suitable linebreaks. @@ -428,12 +494,10 @@ fn linebreak<'a>( fonts: &mut FontStore, width: Length, ) -> Vec> { - let breaker = match p.styles.get(ParNode::LINEBREAKS) { - Linebreaks::Simple => linebreak_simple, - Linebreaks::Optimized => linebreak_optimized, - }; - - breaker(p, fonts, width) + match p.styles.get(ParNode::LINEBREAKS) { + Linebreaks::Simple => linebreak_simple(p, fonts, width), + Linebreaks::Optimized => linebreak_optimized(p, fonts, width), + } } /// Perform line breaking in simple first-fit style. This means that we build @@ -578,10 +642,12 @@ fn linebreak_optimized<'a>( ratio.powi(3).abs() }; - // Penalize hyphens and especially two consecutive hyphens. + // Penalize hyphens. if hyphen { cost += HYPH_COST; } + + // Penalize two consecutive dashes (not necessarily hyphens) extra. if attempt.dash && pred.line.dash { cost += CONSECUTIVE_DASH_COST; } @@ -617,7 +683,7 @@ fn linebreak_optimized<'a>( /// Returns for each breakpoint the text index, whether the break is mandatory /// (after `\n`) and whether a hyphen is required (when breaking inside of a /// word). -fn breakpoints<'a>(p: &'a Preparation) -> impl Iterator + 'a { +fn breakpoints<'a>(p: &'a Preparation) -> Breakpoints<'a> { Breakpoints { p, linebreaks: LineBreakIterator::new(p.bidi.text), @@ -726,26 +792,31 @@ fn line<'a>( mandatory: bool, hyphen: bool, ) -> Line<'a> { - // Find the items which bound the text range. - let last_idx = p.find_idx(range.end.saturating_sub(1)).unwrap(); - let first_idx = if range.is_empty() { - last_idx + // Find the last item. + let (last_idx, last_offset) = + p.find_idx_and_offset(range.end.saturating_sub(1)).unwrap(); + + // Find the first item. + let (first_idx, first_offset) = if range.is_empty() { + (last_idx, last_offset) } else { - p.find_idx(range.start).unwrap() + p.find_idx_and_offset(range.start).unwrap() }; // Slice out the relevant items. let mut items = &p.items[first_idx ..= last_idx]; + let mut width = Length::zero(); // Reshape the last item if it's split in half. let mut last = None; let mut dash = false; - if let Some((ParItem::Text(shaped), before)) = items.split_last() { + if let Some((Item::Text(shaped), before)) = items.split_last() { // Compute the range we want to shape, trimming whitespace at the // end of the line. - let base = p.ranges[last_idx].start; - let start = range.start.max(base); - let trimmed = p.bidi.text[start .. range.end].trim_end(); + let base = last_offset; + let start = range.start.max(last_offset); + let end = range.end; + let trimmed = p.bidi.text[start .. end].trim_end(); let shy = trimmed.ends_with('\u{ad}'); dash = hyphen || shy || trimmed.ends_with(['-', '–', '—']); @@ -766,7 +837,8 @@ fn line<'a>( if hyphen || shy { reshaped.push_hyphen(fonts); } - last = Some(ParItem::Text(reshaped)); + width += reshaped.width; + last = Some(Item::Text(reshaped)); } items = before; @@ -775,35 +847,28 @@ fn line<'a>( // Reshape the start item if it's split in half. let mut first = None; - if let Some((ParItem::Text(shaped), after)) = items.split_first() { + if let Some((Item::Text(shaped), after)) = items.split_first() { // Compute the range we want to shape. - let Range { start: base, end: first_end } = p.ranges[first_idx]; + let base = first_offset; let start = range.start; - let end = range.end.min(first_end); + let end = range.end.min(first_offset + shaped.text.len()); // Reshape if necessary. if end - start < shaped.text.len() { if start < end { let shifted = start - base .. end - base; let reshaped = shaped.reshape(fonts, shifted); - first = Some(ParItem::Text(reshaped)); + width += reshaped.width; + first = Some(Item::Text(reshaped)); } items = after; } } - let mut width = Length::zero(); - let mut fr = Fraction::zero(); - - // Measure the size of the line. - for item in first.iter().chain(items).chain(&last) { - match item { - ParItem::Absolute(v) => width += *v, - ParItem::Fractional(v) => fr += *v, - ParItem::Text(shaped) => width += shaped.width, - ParItem::Frame(frame) => width += frame.size.x, - } + // Measure the inner items. + for item in items { + width += item.width(); } Line { @@ -812,9 +877,7 @@ fn line<'a>( first, items, last, - ranges: &p.ranges[first_idx ..= last_idx], width, - fr, mandatory, dash, } @@ -834,7 +897,7 @@ fn stack( // Determine the paragraph's width: Full width of the region if we // should expand or there's fractional spacing, fit-to-width otherwise. let mut width = regions.first.x; - if !regions.expand.x && lines.iter().all(|line| line.fr.is_zero()) { + if !regions.expand.x && lines.iter().all(|line| line.fr().is_zero()) { width = lines.iter().map(|line| line.width).max().unwrap_or_default(); } @@ -887,7 +950,7 @@ fn commit( let reordered = reorder(line); // Handle hanging punctuation to the left. - if let Some(ParItem::Text(text)) = reordered.first() { + if let Some(Item::Text(text)) = reordered.first() { if let Some(glyph) = text.glyphs.first() { if text.styles.get(TextNode::OVERHANG) { let start = text.dir.is_positive(); @@ -899,7 +962,7 @@ fn commit( } // Handle hanging punctuation to the right. - if let Some(ParItem::Text(text)) = reordered.last() { + if let Some(Item::Text(text)) = reordered.last() { if let Some(glyph) = text.glyphs.last() { if text.styles.get(TextNode::OVERHANG) && (reordered.len() > 1 || text.glyphs.len() > 1) @@ -912,12 +975,13 @@ fn commit( } // Determine how much to justify each space. + let fr = line.fr(); let mut justification = Length::zero(); if remaining < Length::zero() || (justify && !line.mandatory && line.range.end < line.bidi.text.len() - && line.fr.is_zero()) + && fr.is_zero()) { let justifiables = line.justifiables(); if justifiables > 0 { @@ -933,16 +997,16 @@ fn commit( let mut frames = vec![]; for item in reordered { let frame = match item { - ParItem::Absolute(v) => { + Item::Absolute(v) => { offset += *v; continue; } - ParItem::Fractional(v) => { - offset += v.share(line.fr, remaining); + Item::Fractional(v) => { + offset += v.share(fr, remaining); continue; } - ParItem::Text(shaped) => shaped.build(fonts, justification), - ParItem::Frame(frame) => frame.clone(), + Item::Text(shaped) => shaped.build(fonts, justification), + Item::Frame(frame) => frame.clone(), }; let width = frame.size.x; @@ -967,7 +1031,7 @@ fn commit( } /// Return a line's items in visual order. -fn reorder<'a>(line: &'a Line<'a>) -> Vec<&'a ParItem<'a>> { +fn reorder<'a>(line: &'a Line<'a>) -> Vec<&'a Item<'a>> { let mut reordered = vec![]; // The bidi crate doesn't like empty lines. @@ -988,8 +1052,8 @@ fn reorder<'a>(line: &'a Line<'a>) -> Vec<&'a ParItem<'a>> { // Collect the reordered items. for run in runs { - let first_idx = line.find(run.start).unwrap(); - let last_idx = line.find(run.end - 1).unwrap(); + let first_idx = line.find(run.start); + let last_idx = line.find(run.end - 1); let range = first_idx ..= last_idx; // Provide the items forwards or backwards depending on the run's diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs index a73e0effa..0a480c83c 100644 --- a/src/library/text/shaping.rs +++ b/src/library/text/shaping.rs @@ -15,7 +15,7 @@ use crate::util::SliceExt; #[derive(Debug, Clone)] pub struct ShapedText<'a> { /// The text that was shaped. - pub text: Cow<'a, str>, + pub text: &'a str, /// The text direction. pub dir: Dir, /// The text's style properties. @@ -182,7 +182,7 @@ impl<'a> ShapedText<'a> { ) -> ShapedText<'a> { if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { Self { - text: Cow::Borrowed(&self.text[text_range]), + text: &self.text[text_range], dir: self.dir, styles: self.styles, size: self.size, @@ -298,10 +298,6 @@ pub fn shape<'a>( dir: Dir, ) -> ShapedText<'a> { let size = styles.get(TextNode::SIZE); - let text = match styles.get(TextNode::CASE) { - Some(case) => Cow::Owned(case.apply(text)), - None => Cow::Borrowed(text), - }; let mut ctx = ShapingContext { fonts,