diff options
Diffstat (limited to 'crates/typst-layout/src/inline/deco.rs')
| -rw-r--r-- | crates/typst-layout/src/inline/deco.rs | 213 |
1 files changed, 213 insertions, 0 deletions
diff --git a/crates/typst-layout/src/inline/deco.rs b/crates/typst-layout/src/inline/deco.rs new file mode 100644 index 00000000..c01b369b --- /dev/null +++ b/crates/typst-layout/src/inline/deco.rs @@ -0,0 +1,213 @@ +use kurbo::{BezPath, Line, ParamCurve}; +use ttf_parser::{GlyphId, OutlineBuilder}; +use typst_library::layout::{Abs, Em, Frame, FrameItem, Point, Size}; +use typst_library::text::{ + BottomEdge, DecoLine, Decoration, TextEdgeBounds, TextItem, TopEdge, +}; +use typst_library::visualize::{FixedStroke, Geometry}; +use typst_syntax::Span; + +use crate::shapes::styled_rect; + +/// Add line decorations to a single run of shaped text. +pub fn decorate( + frame: &mut Frame, + deco: &Decoration, + text: &TextItem, + width: Abs, + shift: Abs, + pos: Point, +) { + let font_metrics = text.font.metrics(); + + if let DecoLine::Highlight { fill, stroke, top_edge, bottom_edge, radius } = + &deco.line + { + let (top, bottom) = determine_edges(text, *top_edge, *bottom_edge); + let size = Size::new(width + 2.0 * deco.extent, top + bottom); + let rects = styled_rect(size, radius, fill.clone(), stroke); + let origin = Point::new(pos.x - deco.extent, pos.y - top - shift); + frame.prepend_multiple( + rects + .into_iter() + .map(|shape| (origin, FrameItem::Shape(shape, Span::detached()))), + ); + return; + } + + let (stroke, metrics, offset, evade, background) = match &deco.line { + DecoLine::Strikethrough { stroke, offset, background } => { + (stroke, font_metrics.strikethrough, offset, false, *background) + } + DecoLine::Overline { stroke, offset, evade, background } => { + (stroke, font_metrics.overline, offset, *evade, *background) + } + DecoLine::Underline { stroke, offset, evade, background } => { + (stroke, font_metrics.underline, offset, *evade, *background) + } + _ => return, + }; + + let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift; + let stroke = stroke.clone().unwrap_or(FixedStroke::from_pair( + text.fill.as_decoration(), + metrics.thickness.at(text.size), + )); + + let gap_padding = 0.08 * text.size; + let min_width = 0.162 * text.size; + + let start = pos.x - deco.extent; + let end = pos.x + width + deco.extent; + + let mut push_segment = |from: Abs, to: Abs, prepend: bool| { + let origin = Point::new(from, pos.y + offset); + let target = Point::new(to - from, Abs::zero()); + + if target.x >= min_width || !evade { + let shape = Geometry::Line(target).stroked(stroke.clone()); + + if prepend { + frame.prepend(origin, FrameItem::Shape(shape, Span::detached())); + } else { + frame.push(origin, FrameItem::Shape(shape, Span::detached())); + } + } + }; + + if !evade { + push_segment(start, end, background); + return; + } + + 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.at(text.size) + x; + let mut builder = + BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw()); + + let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); + let path = builder.finish(); + + x += glyph.x_advance.at(text.size); + + // Only do the costly segments intersection test if the line + // intersects the bounding box. + let intersect = bbox.is_some_and(|bbox| { + let y_min = -text.font.to_em(bbox.y_max).at(text.size); + let y_max = -text.font.to_em(bbox.y_min).at(text.size); + offset >= y_min && offset <= y_max + }); + + if intersect { + // Find all intersections of segments with the line. + intersections.extend( + path.segments() + .flat_map(|seg| seg.intersect_line(line)) + .map(|is| Abs::raw(line.eval(is.line_t).x)), + ); + } + } + + // Add start and end points, taking padding into account. + intersections.push(start - gap_padding); + intersections.push(end + gap_padding); + // When emitting the decorative line segments, we move from left to + // right. The intersections are not necessarily in this order, yet. + intersections.sort(); + + for edge in intersections.windows(2) { + let l = edge[0]; + let r = edge[1]; + + // If we are too close, don't draw the segment + if r - l < gap_padding { + continue; + } else { + push_segment(l + gap_padding, r - gap_padding, background); + } + } +} + +// Return the top/bottom edge of the text given the metric of the font. +fn determine_edges( + text: &TextItem, + top_edge: TopEdge, + bottom_edge: BottomEdge, +) -> (Abs, Abs) { + let mut top = Abs::zero(); + let mut bottom = Abs::zero(); + + for g in text.glyphs.iter() { + let (t, b) = text.font.edges( + top_edge, + bottom_edge, + text.size, + TextEdgeBounds::Glyph(g.id), + ); + top.set_max(t); + bottom.set_max(b); + } + + (top, bottom) +} + +/// Builds a kurbo [`BezPath`] for a glyph. +struct BezPathBuilder { + path: BezPath, + units_per_em: f64, + font_size: Abs, + x_offset: f64, +} + +impl BezPathBuilder { + fn new(units_per_em: f64, font_size: Abs, x_offset: f64) -> Self { + Self { + path: BezPath::new(), + units_per_em, + font_size, + x_offset, + } + } + + fn finish(self) -> BezPath { + self.path + } + + fn p(&self, x: f32, y: f32) -> kurbo::Point { + kurbo::Point::new(self.s(x) + self.x_offset, -self.s(y)) + } + + fn s(&self, v: f32) -> f64 { + Em::from_units(v, self.units_per_em).at(self.font_size).to_raw() + } +} + +impl OutlineBuilder for BezPathBuilder { + 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(); + } +} |
