diff options
| author | Martin Haug <mhaug@live.de> | 2022-02-03 15:21:12 +0100 |
|---|---|---|
| committer | Martin Haug <mhaug@live.de> | 2022-02-04 00:30:23 +0100 |
| commit | 9a9c6f22c49b4b28f0945aef4f1f620f74b9e605 (patch) | |
| tree | 784b6b3b40dfd93284ad52faa5e6818171b4ddfb /src | |
| parent | bd0d0e10d8ce2246aa1a6fd9a801707cc63dc9a6 (diff) | |
Add evasion of glyph shape for under- and overlines
Diffstat (limited to 'src')
| -rw-r--r-- | src/frame.rs | 12 | ||||
| -rw-r--r-- | src/library/deco.rs | 5 | ||||
| -rw-r--r-- | src/library/text.rs | 241 |
3 files changed, 227 insertions, 31 deletions
diff --git a/src/frame.rs b/src/frame.rs index f714fbbe..9954d132 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -42,6 +42,18 @@ impl Frame { self.elements.push((pos, element)); } + /// The layer the next item will be added on. This corresponds to the number + /// of elements in the frame. + pub fn layer(&self) -> usize { + self.elements.len() + } + + /// Insert an element at the given layer in the Frame. This method panics if + /// the layer is greater than the number of layers present. + pub fn insert(&mut self, layer: usize, pos: Point, element: Element) { + self.elements.insert(layer, (pos, element)); + } + /// Add a group element. pub fn push_frame(&mut self, pos: Point, frame: Arc<Self>) { self.elements.push((pos, Element::Group(Group::new(frame)))); diff --git a/src/library/deco.rs b/src/library/deco.rs index ccb657d1..6b38fa7a 100644 --- a/src/library/deco.rs +++ b/src/library/deco.rs @@ -15,6 +15,7 @@ impl<L: LineKind> DecoNode<L> { thickness: args.named::<Linear>("thickness")?.or_else(|| args.find()), offset: args.named("offset")?, extent: args.named("extent")?.unwrap_or_default(), + evade: args.named("evade")?.unwrap_or(true), }; Ok(args.expect::<Node>("body")?.styled(TextNode::LINES, vec![deco])) } @@ -36,6 +37,9 @@ pub struct Decoration { /// Amount that the line will be longer or shorter than its associated text /// (dependent on scaled font size). pub extent: Linear, + /// Whether the line skips sections in which it would collide + /// with the glyphs. Does not apply to strikethrough. + pub evade: bool, } impl From<DecoLine> for Decoration { @@ -46,6 +50,7 @@ impl From<DecoLine> for Decoration { thickness: None, offset: None, extent: Linear::zero(), + evade: true, } } } diff --git a/src/library/text.rs b/src/library/text.rs index c6c1ab80..c3a9b76c 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -5,8 +5,9 @@ use std::convert::TryInto; use std::fmt::{self, Debug, Formatter}; use std::ops::{BitXor, Range}; +use kurbo::{BezPath, Line, ParamCurve, Point as KPoint}; use rustybuzz::{Feature, UnicodeBuffer}; -use ttf_parser::Tag; +use ttf_parser::{GlyphId, OutlineBuilder, Tag}; use super::prelude::*; use super::{DecoLine, Decoration}; @@ -812,37 +813,14 @@ impl<'a> ShapedText<'a> { .collect(); let text = Text { face_id, size, fill, glyphs }; + let text_layer = frame.layer(); let width = text.width(); - frame.push(pos, Element::Text(text)); - - // Apply line decorations. - for deco in self.styles.get_cloned(TextNode::LINES) { - let face = fonts.get(face_id); - let metrics = match deco.line { - DecoLine::Underline => face.underline, - DecoLine::Strikethrough => face.strikethrough, - DecoLine::Overline => face.overline, - }; - - let extent = deco.extent.resolve(size); - let offset = deco - .offset - .map(|s| s.resolve(size)) - .unwrap_or(-metrics.position.resolve(size)); - - let stroke = Stroke { - paint: deco.stroke.unwrap_or(fill), - thickness: deco - .thickness - .map(|s| s.resolve(size)) - .unwrap_or(metrics.thickness.resolve(size)), - }; - - let subpos = Point::new(pos.x - extent, pos.y + offset); - let target = Point::new(width + 2.0 * extent, Length::zero()); - let shape = Shape::stroked(Geometry::Line(target), stroke); - frame.push(subpos, Element::Shape(shape)); - } + + self.add_line_decos( + &mut frame, fonts, &text, face_id, size, fill, pos, width, + ); + + frame.insert(text_layer, pos, Element::Text(text)); offset += width; } @@ -855,6 +833,155 @@ impl<'a> ShapedText<'a> { frame } + /// Add line decorations to a run of shaped text of a single font. + fn add_line_decos( + &self, + frame: &mut Frame, + fonts: &FontStore, + text: &Text, + face_id: FaceId, + size: Length, + fill: Paint, + pos: Point, + width: Length, + ) { + // Apply line decorations. + for deco in self.styles.get_cloned(TextNode::LINES) { + let face = fonts.get(face_id); + let metrics = match deco.line { + DecoLine::Underline => face.underline, + DecoLine::Strikethrough => face.strikethrough, + DecoLine::Overline => face.overline, + }; + + let evade = deco.evade && deco.line != DecoLine::Strikethrough; + + let extent = deco.extent.resolve(size); + let offset = deco + .offset + .map(|s| s.resolve(size)) + .unwrap_or(-metrics.position.resolve(size)); + + let stroke = Stroke { + paint: deco.stroke.unwrap_or(fill), + thickness: deco + .thickness + .map(|s| s.resolve(size)) + .unwrap_or(metrics.thickness.resolve(size)), + }; + + let line_y = pos.y + offset; + let gap_padding = size * 0.08; + + let gaps = if evade { + let line = Line::new( + KPoint::new(pos.x.to_raw(), offset.to_raw()), + KPoint::new((pos.x + width).to_raw(), offset.to_raw()), + ); + + let mut x_advance = pos.x; + + let mut intersections = vec![]; + + for glyph in text.glyphs.iter() { + let local_offset = glyph.x_offset.resolve(size) + x_advance; + + let mut builder = KurboOutlineBuilder::new( + face.units_per_em, + size, + local_offset.to_raw(), + ); + let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); + + x_advance += glyph.x_advance.resolve(size); + let path = match bbox { + Some(bbox) => { + let y_min = -face.to_em(bbox.y_max).resolve(size); + let y_max = -face.to_em(bbox.y_min).resolve(size); + + // The line does not intersect the glyph, continue + // with the next one. + if offset < y_min || offset > y_max { + continue; + } + + builder.finish() + } + None => continue, + }; + + // Collect all intersections of segments with the line and sort them. + intersections.extend( + path.segments() + .flat_map(|seg| seg.intersect_line(line)) + .map(|is| Length::raw(line.eval(is.line_t).x)), + ); + } + + intersections.sort(); + + let mut gaps = vec![]; + let mut inside = None; + + // Alternate between outside and inside and collect the gaps + // into the gap vector. + for intersection in intersections { + match inside { + Some(start) => { + gaps.push((start, intersection)); + inside = None; + } + None => inside = Some(intersection), + } + } + + gaps + } else { + vec![] + }; + + let mut start = pos.x - extent; + let end = pos.x + (width + 2.0 * extent); + + let min_width = 0.162 * size; + let mut push_segment = |from: Length, to: Length| { + let origin = Point::new(from, line_y); + let target = Point::new(to - from, Length::zero()); + + if target.x < min_width { + return; + } + + let shape = Shape::stroked(Geometry::Line(target), stroke); + frame.push(origin, Element::Shape(shape)); + }; + + + if evade { + for gap in + gaps.into_iter().map(|(a, b)| (a - gap_padding, b + gap_padding)) + { + if start >= end { + break; + } + + if start >= gap.0 { + start = gap.1; + continue; + } + + push_segment(start, gap.0); + start = gap.1; + } + } + + if start < end { + push_segment(start, end); + } + } + } + + /// Reshape a range of the shaped text, reusing information from this /// shaping process if possible. pub fn reshape( @@ -941,3 +1068,55 @@ enum Side { Left, Right, } + +struct KurboOutlineBuilder { + path: BezPath, + units_per_em: f64, + font_size: Length, + x_offset: f64, +} + +impl KurboOutlineBuilder { + pub fn new(units_per_em: f64, font_size: Length, x_offset: f64) -> Self { + Self { + path: BezPath::new(), + units_per_em, + font_size, + x_offset, + } + } + + pub fn finish(self) -> BezPath { + self.path + } + + fn p(&self, x: f32, y: f32) -> KPoint { + KPoint::new(self.s(x) + self.x_offset, -self.s(y)) + } + + fn s(&self, v: f32) -> f64 { + Em::from_units(v, self.units_per_em).resolve(self.font_size).to_raw() + } +} + +impl OutlineBuilder for KurboOutlineBuilder { + fn move_to(&mut self, x: f32, y: f32) { + self.path.move_to(self.p(x, y)); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.path.line_to(self.p(x, y)); + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + self.path.quad_to(self.p(x1, y1), self.p(x, y)); + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y)); + } + + fn close(&mut self) { + self.path.close_path(); + } +} |
