summaryrefslogtreecommitdiff
path: root/crates/typst-layout/src/inline/deco.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-10-27 19:04:55 +0100
committerGitHub <noreply@github.com>2024-10-27 18:04:55 +0000
commitbe7cfc85d08c545abfac08098b7b33b4bd71f37e (patch)
treef4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-layout/src/inline/deco.rs
parentb8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff)
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-layout/src/inline/deco.rs')
-rw-r--r--crates/typst-layout/src/inline/deco.rs213
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();
+ }
+}