mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
253 lines
6.9 KiB
Rust
253 lines
6.9 KiB
Rust
use kurbo::{BezPath, Line, ParamCurve};
|
|
use ttf_parser::{GlyphId, OutlineBuilder};
|
|
|
|
use super::TextNode;
|
|
use crate::prelude::*;
|
|
|
|
/// Typeset underline, stricken-through or overlined text.
|
|
#[derive(Debug, Hash)]
|
|
pub struct DecoNode<const L: DecoLine>(pub Content);
|
|
|
|
/// Typeset underlined text.
|
|
pub type UnderlineNode = DecoNode<UNDERLINE>;
|
|
|
|
/// Typeset stricken-through text.
|
|
pub type StrikethroughNode = DecoNode<STRIKETHROUGH>;
|
|
|
|
/// Typeset overlined text.
|
|
pub type OverlineNode = DecoNode<OVERLINE>;
|
|
|
|
#[node(Show)]
|
|
impl<const L: DecoLine> DecoNode<L> {
|
|
/// How to stroke the line. The text color and thickness are read from the
|
|
/// font tables if `auto`.
|
|
#[property(shorthand, resolve, fold)]
|
|
pub const STROKE: Smart<PartialStroke> = Smart::Auto;
|
|
/// Position of the line relative to the baseline, read from the font tables
|
|
/// if `auto`.
|
|
#[property(resolve)]
|
|
pub const OFFSET: Smart<Length> = Smart::Auto;
|
|
/// Amount that the line will be longer or shorter than its associated text.
|
|
#[property(resolve)]
|
|
pub const EXTENT: Length = Length::zero();
|
|
/// Whether the line skips sections in which it would collide
|
|
/// with the glyphs. Does not apply to strikethrough.
|
|
pub const EVADE: bool = true;
|
|
|
|
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
|
Ok(Self(args.expect("body")?).pack())
|
|
}
|
|
|
|
fn field(&self, name: &str) -> Option<Value> {
|
|
match name {
|
|
"body" => Some(Value::Content(self.0.clone())),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<const L: DecoLine> Show for DecoNode<L> {
|
|
fn show(&self, _: Tracked<dyn World>, styles: StyleChain) -> SourceResult<Content> {
|
|
Ok(self.0.clone().styled(
|
|
TextNode::DECO,
|
|
Decoration {
|
|
line: L,
|
|
stroke: styles.get(Self::STROKE).unwrap_or_default(),
|
|
offset: styles.get(Self::OFFSET),
|
|
extent: styles.get(Self::EXTENT),
|
|
evade: styles.get(Self::EVADE),
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Defines a line that is positioned over, under or on top of text.
|
|
///
|
|
/// For more details, see [`DecoNode`].
|
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
|
pub(super) struct Decoration {
|
|
pub line: DecoLine,
|
|
pub stroke: PartialStroke<Abs>,
|
|
pub offset: Smart<Abs>,
|
|
pub extent: Abs,
|
|
pub evade: bool,
|
|
}
|
|
|
|
impl Fold for Decoration {
|
|
type Output = Vec<Self>;
|
|
|
|
fn fold(self, mut outer: Self::Output) -> Self::Output {
|
|
outer.insert(0, self);
|
|
outer
|
|
}
|
|
}
|
|
|
|
/// A kind of decorative line.
|
|
pub type DecoLine = usize;
|
|
|
|
/// A line under text.
|
|
pub const UNDERLINE: DecoLine = 0;
|
|
|
|
/// A line through text.
|
|
pub const STRIKETHROUGH: DecoLine = 1;
|
|
|
|
/// A line over text.
|
|
pub const OVERLINE: DecoLine = 2;
|
|
|
|
/// Add line decorations to a single run of shaped text.
|
|
pub(super) fn decorate(
|
|
frame: &mut Frame,
|
|
deco: &Decoration,
|
|
text: &Text,
|
|
shift: Abs,
|
|
pos: Point,
|
|
width: Abs,
|
|
) {
|
|
let font_metrics = text.font.metrics();
|
|
let metrics = match deco.line {
|
|
STRIKETHROUGH => font_metrics.strikethrough,
|
|
OVERLINE => font_metrics.overline,
|
|
UNDERLINE | _ => font_metrics.underline,
|
|
};
|
|
|
|
let evade = deco.evade && deco.line != STRIKETHROUGH;
|
|
let offset = deco.offset.unwrap_or(-metrics.position.at(text.size)) - shift;
|
|
let stroke = deco.stroke.unwrap_or(Stroke {
|
|
paint: text.fill,
|
|
thickness: metrics.thickness.at(text.size),
|
|
});
|
|
|
|
let gap_padding = 0.08 * text.size;
|
|
let min_width = 0.162 * text.size;
|
|
|
|
let mut start = pos.x - deco.extent;
|
|
let end = pos.x + (width + 2.0 * deco.extent);
|
|
|
|
let mut push_segment = |from: Abs, to: Abs| {
|
|
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);
|
|
frame.push(origin, Element::Shape(shape));
|
|
}
|
|
};
|
|
|
|
if !evade {
|
|
push_segment(start, end);
|
|
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.
|
|
if bbox.map_or(false, |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
|
|
}) {
|
|
// 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)),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
start = r;
|
|
}
|
|
|
|
if start < end {
|
|
push_segment(start, end);
|
|
}
|
|
}
|
|
|
|
/// 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();
|
|
}
|
|
}
|