mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
214 lines
6.3 KiB
Rust
214 lines
6.3 KiB
Rust
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();
|
|
}
|
|
}
|