diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-02-25 20:48:38 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-02-25 20:48:38 +0100 |
| commit | b0f4b13f6d4a1fe7742707d08e11ba03f3542b58 (patch) | |
| tree | 13704c92d6e9f2b85f487533c2f0d1ca7cf58d84 /src/library/text.rs | |
| parent | efde5cac88078f10485f715be66a27efba2f23d8 (diff) | |
Basic justification
Diffstat (limited to 'src/library/text.rs')
| -rw-r--r-- | src/library/text.rs | 776 |
1 files changed, 396 insertions, 380 deletions
diff --git a/src/library/text.rs b/src/library/text.rs index 448ba9af..b76b60ee 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -406,6 +406,191 @@ impl Case { } } +/// The result of shaping text. +/// +/// This type contains owned or borrowed shaped text runs, which can be +/// measured, used to reshape substrings more quickly and converted into a +/// frame. +#[derive(Debug, Clone)] +pub struct ShapedText<'a> { + /// The text that was shaped. + pub text: Cow<'a, str>, + /// The text direction. + pub dir: Dir, + /// The text's style properties. + pub styles: StyleChain<'a>, + /// The size of the text's bounding box. + pub size: Size, + /// The baseline from the top of the frame. + pub baseline: Length, + /// The shaped glyphs. + pub glyphs: Cow<'a, [ShapedGlyph]>, +} + +/// A single glyph resulting from shaping. +#[derive(Debug, Copy, Clone)] +pub struct ShapedGlyph { + /// The font face the glyph is contained in. + pub face_id: FaceId, + /// The glyph's index in the face. + pub glyph_id: u16, + /// The advance width of the glyph. + pub x_advance: Em, + /// The horizontal offset of the glyph. + pub x_offset: Em, + /// The start index of the glyph in the source text. + pub text_index: usize, + /// Whether splitting the shaping result before this glyph would yield the + /// same results as shaping the parts to both sides of `text_index` + /// separately. + pub safe_to_break: bool, + /// Whether this glyph represents a space. + pub is_space: bool, +} + +/// A visual side. +enum Side { + Left, + Right, +} + +impl<'a> ShapedText<'a> { + /// Build the shaped text's frame. + /// + /// The `justification` defines how much extra advance width each + /// [space glyph](ShapedGlyph::is_space) will get. + pub fn build(&self, fonts: &FontStore, justification: Length) -> Frame { + let mut offset = Length::zero(); + let mut frame = Frame::new(self.size); + frame.baseline = Some(self.baseline); + + for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) { + let pos = Point::new(offset, self.baseline); + + let size = self.styles.get(TextNode::SIZE).abs; + let fill = self.styles.get(TextNode::FILL); + let glyphs = group + .iter() + .map(|glyph| Glyph { + id: glyph.glyph_id, + x_advance: glyph.x_advance + + if glyph.is_space { + frame.size.x += justification; + Em::from_length(justification, size) + } else { + Em::zero() + }, + x_offset: glyph.x_offset, + }) + .collect(); + + let text = Text { face_id, size, fill, glyphs }; + let text_layer = frame.layer(); + let width = text.width(); + + // Apply line decorations. + for deco in self.styles.get_cloned(TextNode::LINES) { + decorate(&mut frame, &deco, fonts, &text, pos, width); + } + + frame.insert(text_layer, pos, Element::Text(text)); + offset += width; + } + + // Apply link if it exists. + if let Some(url) = self.styles.get_ref(TextNode::LINK) { + frame.link(url); + } + + frame + } + + /// How many spaces the text contains. + pub fn spaces(&self) -> usize { + self.glyphs.iter().filter(|g| g.is_space).count() + } + + /// Reshape a range of the shaped text, reusing information from this + /// shaping process if possible. + pub fn reshape( + &'a self, + fonts: &mut FontStore, + text_range: Range<usize>, + ) -> ShapedText<'a> { + if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { + let (size, baseline) = measure(fonts, glyphs, self.styles); + Self { + text: Cow::Borrowed(&self.text[text_range]), + dir: self.dir, + styles: self.styles.clone(), + size, + baseline, + glyphs: Cow::Borrowed(glyphs), + } + } else { + shape(fonts, &self.text[text_range], self.styles.clone(), self.dir) + } + } + + /// Find the subslice of glyphs that represent the given text range if both + /// sides are safe to break. + fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> { + let Range { mut start, mut end } = text_range; + if !self.dir.is_positive() { + std::mem::swap(&mut start, &mut end); + } + + let left = self.find_safe_to_break(start, Side::Left)?; + let right = self.find_safe_to_break(end, Side::Right)?; + Some(&self.glyphs[left .. right]) + } + + /// Find the glyph offset matching the text index that is most towards the + /// given side and safe-to-break. + fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> { + let ltr = self.dir.is_positive(); + + // Handle edge cases. + let len = self.glyphs.len(); + if text_index == 0 { + return Some(if ltr { 0 } else { len }); + } else if text_index == self.text.len() { + return Some(if ltr { len } else { 0 }); + } + + // Find any glyph with the text index. + let mut idx = self + .glyphs + .binary_search_by(|g| { + let ordering = g.text_index.cmp(&text_index); + if ltr { ordering } else { ordering.reverse() } + }) + .ok()?; + + let next = match towards { + Side::Left => usize::checked_sub, + Side::Right => usize::checked_add, + }; + + // Search for the outermost glyph with the text index. + while let Some(next) = next(idx, 1) { + if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) { + break; + } + idx = next; + } + + // RTL needs offset one because the left side of the range should be + // exclusive and the right side inclusive, contrary to the normal + // behaviour of ranges. + if !ltr { + idx += 1; + } + + self.glyphs[idx].safe_to_break.then(|| idx) + } +} + /// Shape text into [`ShapedText`]. pub fn shape<'a>( fonts: &mut FontStore, @@ -446,6 +631,131 @@ pub fn shape<'a>( } } +/// Resolve the font variant with `STRONG` and `EMPH` factored in. +fn variant(styles: StyleChain) -> FontVariant { + let mut variant = FontVariant::new( + styles.get(TextNode::STYLE), + styles.get(TextNode::WEIGHT), + styles.get(TextNode::STRETCH), + ); + + if styles.get(TextNode::STRONG) { + variant.weight = variant.weight.thicken(300); + } + + if styles.get(TextNode::EMPH) { + variant.style = match variant.style { + FontStyle::Normal => FontStyle::Italic, + FontStyle::Italic => FontStyle::Normal, + FontStyle::Oblique => FontStyle::Normal, + } + } + + variant +} + +/// Resolve a prioritized iterator over the font families. +fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone { + let head = if styles.get(TextNode::MONOSPACED) { + styles.get_ref(TextNode::MONOSPACE).as_slice() + } else { + &[] + }; + + let core = styles.get_ref(TextNode::FAMILY).iter().flat_map(move |family| { + match family { + FontFamily::Named(name) => std::slice::from_ref(name), + FontFamily::Serif => styles.get_ref(TextNode::SERIF), + FontFamily::SansSerif => styles.get_ref(TextNode::SANS_SERIF), + FontFamily::Monospace => styles.get_ref(TextNode::MONOSPACE), + } + }); + + let tail: &[&str] = if styles.get(TextNode::FALLBACK) { + &["ibm plex sans", "latin modern math", "twitter color emoji"] + } else { + &[] + }; + + head.iter() + .chain(core) + .map(|named| named.as_str()) + .chain(tail.iter().copied()) +} + +/// Collect the tags of the OpenType features to apply. +fn tags(styles: StyleChain) -> Vec<Feature> { + let mut tags = vec![]; + let mut feat = |tag, value| { + tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); + }; + + // Features that are on by default in Harfbuzz are only added if disabled. + if !styles.get(TextNode::KERNING) { + feat(b"kern", 0); + } + + // Features that are off by default in Harfbuzz are only added if enabled. + if styles.get(TextNode::SMALLCAPS) { + feat(b"smcp", 1); + } + + if styles.get(TextNode::ALTERNATES) { + feat(b"salt", 1); + } + + let storage; + if let Some(set) = styles.get(TextNode::STYLISTIC_SET) { + storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; + feat(&storage, 1); + } + + if !styles.get(TextNode::LIGATURES) { + feat(b"liga", 0); + feat(b"clig", 0); + } + + if styles.get(TextNode::DISCRETIONARY_LIGATURES) { + feat(b"dlig", 1); + } + + if styles.get(TextNode::HISTORICAL_LIGATURES) { + feat(b"hilg", 1); + } + + match styles.get(TextNode::NUMBER_TYPE) { + Smart::Auto => {} + Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), + Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), + } + + match styles.get(TextNode::NUMBER_WIDTH) { + Smart::Auto => {} + Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), + Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), + } + + match styles.get(TextNode::NUMBER_POSITION) { + NumberPosition::Normal => {} + NumberPosition::Subscript => feat(b"subs", 1), + NumberPosition::Superscript => feat(b"sups", 1), + } + + if styles.get(TextNode::SLASHED_ZERO) { + feat(b"zero", 1); + } + + if styles.get(TextNode::FRACTIONS) { + feat(b"frac", 1); + } + + for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() { + tags.push(Feature::new(tag, value, ..)) + } + + tags +} + /// Shape text with font fallback using the `families` iterator. fn shape_segment<'a>( fonts: &mut FontStore, @@ -517,6 +827,7 @@ fn shape_segment<'a>( x_offset: face.to_em(pos[i].x_offset), text_index: base + cluster, safe_to_break: !info.unsafe_to_break(), + is_space: text[cluster ..].chars().next() == Some(' '), }); } else { // Determine the source text range for the tofu sequence. @@ -637,422 +948,127 @@ fn measure( (Size::new(width, top + bottom), top) } -/// Resolve the font variant with `STRONG` and `EMPH` factored in. -fn variant(styles: StyleChain) -> FontVariant { - let mut variant = FontVariant::new( - styles.get(TextNode::STYLE), - styles.get(TextNode::WEIGHT), - styles.get(TextNode::STRETCH), - ); - - if styles.get(TextNode::STRONG) { - variant.weight = variant.weight.thicken(300); - } - - if styles.get(TextNode::EMPH) { - variant.style = match variant.style { - FontStyle::Normal => FontStyle::Italic, - FontStyle::Italic => FontStyle::Normal, - FontStyle::Oblique => FontStyle::Normal, - } - } - - variant -} - -/// Resolve a prioritized iterator over the font families. -fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone { - let head = if styles.get(TextNode::MONOSPACED) { - styles.get_ref(TextNode::MONOSPACE).as_slice() - } else { - &[] - }; - - let core = styles.get_ref(TextNode::FAMILY).iter().flat_map(move |family| { - match family { - FontFamily::Named(name) => std::slice::from_ref(name), - FontFamily::Serif => styles.get_ref(TextNode::SERIF), - FontFamily::SansSerif => styles.get_ref(TextNode::SANS_SERIF), - FontFamily::Monospace => styles.get_ref(TextNode::MONOSPACE), - } - }); - - let tail: &[&str] = if styles.get(TextNode::FALLBACK) { - &["ibm plex sans", "latin modern math", "twitter color emoji"] - } else { - &[] +/// Add line decorations to a single run of shaped text. +fn decorate( + frame: &mut Frame, + deco: &Decoration, + fonts: &FontStore, + text: &Text, + pos: Point, + width: Length, +) { + let face = fonts.get(text.face_id); + let metrics = match deco.line { + super::STRIKETHROUGH => face.strikethrough, + super::OVERLINE => face.overline, + super::UNDERLINE | _ => face.underline, }; - head.iter() - .chain(core) - .map(|named| named.as_str()) - .chain(tail.iter().copied()) -} - -/// Collect the tags of the OpenType features to apply. -fn tags(styles: StyleChain) -> Vec<Feature> { - let mut tags = vec![]; - let mut feat = |tag, value| { - tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); + let evade = deco.evade && deco.line != super::STRIKETHROUGH; + let extent = deco.extent.resolve(text.size); + let offset = deco + .offset + .map(|s| s.resolve(text.size)) + .unwrap_or(-metrics.position.resolve(text.size)); + + let stroke = Stroke { + paint: deco.stroke.unwrap_or(text.fill), + thickness: deco + .thickness + .map(|s| s.resolve(text.size)) + .unwrap_or(metrics.thickness.resolve(text.size)), }; - // Features that are on by default in Harfbuzz are only added if disabled. - if !styles.get(TextNode::KERNING) { - feat(b"kern", 0); - } + let gap_padding = 0.08 * text.size; + let min_width = 0.162 * text.size; - // Features that are off by default in Harfbuzz are only added if enabled. - if styles.get(TextNode::SMALLCAPS) { - feat(b"smcp", 1); - } - - if styles.get(TextNode::ALTERNATES) { - feat(b"salt", 1); - } + let mut start = pos.x - extent; + let end = pos.x + (width + 2.0 * extent); - let storage; - if let Some(set) = styles.get(TextNode::STYLISTIC_SET) { - storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; - feat(&storage, 1); - } + let mut push_segment = |from: Length, to: Length| { + let origin = Point::new(from, pos.y + offset); + let target = Point::new(to - from, Length::zero()); - if !styles.get(TextNode::LIGATURES) { - feat(b"liga", 0); - feat(b"clig", 0); - } - - if styles.get(TextNode::DISCRETIONARY_LIGATURES) { - feat(b"dlig", 1); - } - - if styles.get(TextNode::HISTORICAL_LIGATURES) { - feat(b"hilg", 1); - } - - match styles.get(TextNode::NUMBER_TYPE) { - Smart::Auto => {} - Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), - Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), - } - - match styles.get(TextNode::NUMBER_WIDTH) { - Smart::Auto => {} - Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), - Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), - } - - match styles.get(TextNode::NUMBER_POSITION) { - NumberPosition::Normal => {} - NumberPosition::Subscript => feat(b"subs", 1), - NumberPosition::Superscript => feat(b"sups", 1), - } - - if styles.get(TextNode::SLASHED_ZERO) { - feat(b"zero", 1); - } - - if styles.get(TextNode::FRACTIONS) { - feat(b"frac", 1); - } + if target.x >= min_width || !evade { + let shape = Shape::stroked(Geometry::Line(target), stroke); + frame.push(origin, Element::Shape(shape)); + } + }; - for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() { - tags.push(Feature::new(tag, value, ..)) + if !evade { + push_segment(start, end); + return; } - tags -} - -/// The result of shaping text. -/// -/// This type contains owned or borrowed shaped text runs, which can be -/// measured, used to reshape substrings more quickly and converted into a -/// frame. -#[derive(Debug, Clone)] -pub struct ShapedText<'a> { - /// The text that was shaped. - pub text: Cow<'a, str>, - /// The text direction. - pub dir: Dir, - /// The text's style properties. - pub styles: StyleChain<'a>, - /// The font size. - pub size: Size, - /// The baseline from the top of the frame. - pub baseline: Length, - /// The shaped glyphs. - pub glyphs: Cow<'a, [ShapedGlyph]>, -} - -/// A single glyph resulting from shaping. -#[derive(Debug, Copy, Clone)] -pub struct ShapedGlyph { - /// The font face the glyph is contained in. - pub face_id: FaceId, - /// The glyph's index in the face. - pub glyph_id: u16, - /// The advance width of the glyph. - pub x_advance: Em, - /// The horizontal offset of the glyph. - pub x_offset: Em, - /// The start index of the glyph in the source text. - pub text_index: usize, - /// Whether splitting the shaping result before this glyph would yield the - /// same results as shaping the parts to both sides of `text_index` - /// separately. - pub safe_to_break: bool, -} - -impl<'a> ShapedText<'a> { - /// Build the shaped text's frame. - pub fn build(&self, fonts: &FontStore) -> Frame { - let mut offset = Length::zero(); - let mut frame = Frame::new(self.size); - frame.baseline = Some(self.baseline); + let line = Line::new( + kurbo::Point::new(pos.x.to_raw(), offset.to_raw()), + kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()), + ); - for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) { - let pos = Point::new(offset, self.baseline); + let mut x = pos.x; + let mut intersections = vec![]; - let size = self.styles.get(TextNode::SIZE).abs; - let fill = self.styles.get(TextNode::FILL); - let glyphs = group - .iter() - .map(|glyph| Glyph { - id: glyph.glyph_id, - x_advance: glyph.x_advance, - x_offset: glyph.x_offset, - }) - .collect(); + for glyph in text.glyphs.iter() { + let dx = glyph.x_offset.resolve(text.size) + x; + let mut builder = BezPathBuilder::new(face.units_per_em, text.size, dx.to_raw()); - let text = Text { face_id, size, fill, glyphs }; - let text_layer = frame.layer(); - let width = text.width(); + let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); + let path = builder.finish(); - // Apply line decorations. - for deco in self.styles.get_cloned(TextNode::LINES) { - self.decorate(&mut frame, &deco, fonts, &text, pos, width); - } + x += glyph.x_advance.resolve(text.size); - frame.insert(text_layer, pos, Element::Text(text)); - offset += width; - } + // Only do the costly segments intersection test if the line + // intersects the bounding box. + if bbox.map_or(false, |bbox| { + let y_min = -face.to_em(bbox.y_max).resolve(text.size); + let y_max = -face.to_em(bbox.y_min).resolve(text.size); - // Apply link if it exists. - if let Some(url) = self.styles.get_ref(TextNode::LINK) { - frame.link(url); + offset >= y_min && offset <= y_max + }) { + // Find all intersections of segments with the line. + intersections.extend( + path.segments() + .flat_map(|seg| seg.intersect_line(line)) + .map(|is| Length::raw(line.eval(is.line_t).x)), + ); } - - frame } - /// Add line decorations to a run of shaped text of a single font. - fn decorate( - &self, - frame: &mut Frame, - deco: &Decoration, - fonts: &FontStore, - text: &Text, - pos: Point, - width: Length, - ) { - let face = fonts.get(text.face_id); - let metrics = match deco.line { - super::STRIKETHROUGH => face.strikethrough, - super::OVERLINE => face.overline, - super::UNDERLINE | _ => face.underline, - }; - - let evade = deco.evade && deco.line != super::STRIKETHROUGH; - let extent = deco.extent.resolve(text.size); - let offset = deco - .offset - .map(|s| s.resolve(text.size)) - .unwrap_or(-metrics.position.resolve(text.size)); - - let stroke = Stroke { - paint: deco.stroke.unwrap_or(text.fill), - thickness: deco - .thickness - .map(|s| s.resolve(text.size)) - .unwrap_or(metrics.thickness.resolve(text.size)), - }; - - let gap_padding = 0.08 * text.size; - let min_width = 0.162 * text.size; - - let mut start = pos.x - extent; - let end = pos.x + (width + 2.0 * extent); + // When emitting the decorative line segments, we move from left to + // right. The intersections are not necessarily in this order, yet. + intersections.sort(); - let mut push_segment = |from: Length, to: Length| { - let origin = Point::new(from, pos.y + offset); - let target = Point::new(to - from, Length::zero()); + for gap in intersections.chunks_exact(2) { + let l = gap[0] - gap_padding; + let r = gap[1] + gap_padding; - if target.x >= min_width || !evade { - let shape = Shape::stroked(Geometry::Line(target), stroke); - frame.push(origin, Element::Shape(shape)); - } - }; - - if !evade { - push_segment(start, end); - return; + if start >= end { + break; } - let line = Line::new( - kurbo::Point::new(pos.x.to_raw(), offset.to_raw()), - kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()), - ); - - let mut x = pos.x; - let mut intersections = vec![]; - - for glyph in text.glyphs.iter() { - let dx = glyph.x_offset.resolve(text.size) + x; - let mut builder = - KurboPathBuilder::new(face.units_per_em, text.size, dx.to_raw()); - - let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); - let path = builder.finish(); - - x += glyph.x_advance.resolve(text.size); - - // Only do the costly segments intersection test if the line - // intersects the bounding box. - if bbox.map_or(false, |bbox| { - let y_min = -face.to_em(bbox.y_max).resolve(text.size); - let y_max = -face.to_em(bbox.y_min).resolve(text.size); - - offset >= y_min && offset <= y_max - }) { - // Find all intersections of segments with the line. - intersections.extend( - path.segments() - .flat_map(|seg| seg.intersect_line(line)) - .map(|is| Length::raw(line.eval(is.line_t).x)), - ); - } - } - - // When emitting the decorative line segments, we move from left to - // right. The intersections are not necessarily in this order, yet. - intersections.sort(); - - for gap in intersections.chunks_exact(2) { - let l = gap[0] - gap_padding; - let r = gap[1] + gap_padding; - - if start >= end { - break; - } - - if start >= l { - start = r; - continue; - } - - push_segment(start, l); + if start >= l { start = r; + continue; } - if start < end { - push_segment(start, end); - } + push_segment(start, l); + start = r; } - /// Reshape a range of the shaped text, reusing information from this - /// shaping process if possible. - pub fn reshape( - &'a self, - fonts: &mut FontStore, - text_range: Range<usize>, - ) -> ShapedText<'a> { - if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { - let (size, baseline) = measure(fonts, glyphs, self.styles); - Self { - text: Cow::Borrowed(&self.text[text_range]), - dir: self.dir, - styles: self.styles.clone(), - size, - baseline, - glyphs: Cow::Borrowed(glyphs), - } - } else { - shape(fonts, &self.text[text_range], self.styles.clone(), self.dir) - } + if start < end { + push_segment(start, end); } - - /// Find the subslice of glyphs that represent the given text range if both - /// sides are safe to break. - fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> { - let Range { mut start, mut end } = text_range; - if !self.dir.is_positive() { - std::mem::swap(&mut start, &mut end); - } - - let left = self.find_safe_to_break(start, Side::Left)?; - let right = self.find_safe_to_break(end, Side::Right)?; - Some(&self.glyphs[left .. right]) - } - - /// Find the glyph offset matching the text index that is most towards the - /// given side and safe-to-break. - fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> { - let ltr = self.dir.is_positive(); - - // Handle edge cases. - let len = self.glyphs.len(); - if text_index == 0 { - return Some(if ltr { 0 } else { len }); - } else if text_index == self.text.len() { - return Some(if ltr { len } else { 0 }); - } - - // Find any glyph with the text index. - let mut idx = self - .glyphs - .binary_search_by(|g| { - let ordering = g.text_index.cmp(&text_index); - if ltr { ordering } else { ordering.reverse() } - }) - .ok()?; - - let next = match towards { - Side::Left => usize::checked_sub, - Side::Right => usize::checked_add, - }; - - // Search for the outermost glyph with the text index. - while let Some(next) = next(idx, 1) { - if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) { - break; - } - idx = next; - } - - // RTL needs offset one because the left side of the range should be - // exclusive and the right side inclusive, contrary to the normal - // behaviour of ranges. - if !ltr { - idx += 1; - } - - self.glyphs[idx].safe_to_break.then(|| idx) - } -} - -/// A visual side. -enum Side { - Left, - Right, } -struct KurboPathBuilder { +/// Builds a kurbo [`BezPath`] for a glyph. +struct BezPathBuilder { path: BezPath, units_per_em: f64, font_size: Length, x_offset: f64, } -impl KurboPathBuilder { +impl BezPathBuilder { fn new(units_per_em: f64, font_size: Length, x_offset: f64) -> Self { Self { path: BezPath::new(), @@ -1075,7 +1091,7 @@ impl KurboPathBuilder { } } -impl OutlineBuilder for KurboPathBuilder { +impl OutlineBuilder for BezPathBuilder { fn move_to(&mut self, x: f32, y: f32) { self.path.move_to(self.p(x, y)); } |
