typst/library/src/text/deco.rs

460 lines
12 KiB
Rust

use kurbo::{BezPath, Line, ParamCurve};
use ttf_parser::{GlyphId, OutlineBuilder};
use super::TextNode;
use crate::prelude::*;
/// # Underline
/// Underline text.
///
/// ## Example
/// ```example
/// This is #underline[important].
/// ```
///
/// ## Parameters
/// - body: `Content` (positional, required)
/// The content to underline.
///
/// ## Category
/// text
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
pub struct UnderlineNode(pub Content);
#[node]
impl UnderlineNode {
/// How to stroke the line. The text color and thickness are read from the
/// font tables if `{auto}`.
///
/// ```example
/// Take #underline(
/// stroke: 1.5pt + red,
/// offset: 2pt,
/// [care],
/// )
/// ```
#[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}`.
///
/// ```example
/// #underline(offset: 5pt)[
/// The Tale Of A Faraway Line I
/// ]
/// ```
#[property(resolve)]
pub const OFFSET: Smart<Length> = Smart::Auto;
/// Amount that the line will be longer or shorter than its associated text.
///
/// ```example
/// #align(center,
/// underline(extent: 2pt)[Chapter 1]
/// )
/// ```
#[property(resolve)]
pub const EXTENT: Length = Length::zero();
/// Whether the line skips sections in which it would collide with the
/// glyphs.
///
/// ```example
/// This #underline(evade: true)[is great].
/// This #underline(evade: false)[is less great].
/// ```
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 Show for UnderlineNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled(
TextNode::DECO,
Decoration {
line: DecoLine::Underline,
stroke: styles.get(Self::STROKE).unwrap_or_default(),
offset: styles.get(Self::OFFSET),
extent: styles.get(Self::EXTENT),
evade: styles.get(Self::EVADE),
},
))
}
}
/// # Overline
/// Add a line over text.
///
/// ## Example
/// ```example
/// #overline[A line over text.]
/// ```
///
/// ## Parameters
/// - body: `Content` (positional, required)
/// The content to add a line over.
///
/// ## Category
/// text
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
pub struct OverlineNode(pub Content);
#[node]
impl OverlineNode {
/// How to stroke the line. The text color and thickness are read from the
/// font tables if `{auto}`.
///
/// ```example
/// #set text(fill: olive)
/// #overline(
/// stroke: green.darken(20%),
/// offset: -12pt,
/// [The Forest Theme],
/// )
/// ```
#[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}`.
///
/// ```example
/// #overline(offset: -1.2em)[
/// The Tale Of A Faraway Line II
/// ]
/// ```
#[property(resolve)]
pub const OFFSET: Smart<Length> = Smart::Auto;
/// Amount that the line will be longer or shorter than its associated text.
///
/// ```example
/// #set overline(extent: 4pt)
/// #set underline(extent: 4pt)
/// #overline(underline[Typography Today])
/// ```
#[property(resolve)]
pub const EXTENT: Length = Length::zero();
/// Whether the line skips sections in which it would collide with the
/// glyphs.
///
/// ```example
/// #overline(
/// evade: false,
/// offset: -7.5pt,
/// stroke: 1pt,
/// extent: 3pt,
/// [Temple],
/// )
/// ```
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 Show for OverlineNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled(
TextNode::DECO,
Decoration {
line: DecoLine::Overline,
stroke: styles.get(Self::STROKE).unwrap_or_default(),
offset: styles.get(Self::OFFSET),
extent: styles.get(Self::EXTENT),
evade: styles.get(Self::EVADE),
},
))
}
}
/// # Strikethrough
/// Strike through text.
///
/// ## Example
/// ```example
/// This is #strike[not] relevant.
/// ```
///
/// ## Parameters
/// - body: `Content` (positional, required)
/// The content to strike through.
///
/// ## Category
/// text
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
pub struct StrikeNode(pub Content);
#[node]
impl StrikeNode {
/// How to stroke the line. The text color and thickness are read from the
/// font tables if `{auto}`.
///
/// _Note:_ Please don't use this for real redaction as you can still
/// copy paste the text.
///
/// ```example
/// This is #strike(stroke: 1.5pt + red)[very stricken through]. \
/// This is #strike(stroke: 10pt)[redacted].
/// ```
#[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}`.
///
/// This is useful if you are unhappy with the offset your font provides.
///
/// ```example
/// #set text(family: "Inria Serif")
/// This is #strike(offset: auto)[low-ish]. \
/// This is #strike(offset: -3.5pt)[on-top].
/// ```
#[property(resolve)]
pub const OFFSET: Smart<Length> = Smart::Auto;
/// Amount that the line will be longer or shorter than its associated text.
///
/// ```example
/// This #strike(extent: -2pt)[skips] parts of the word.
/// This #strike(extent: 2pt)[extends] beyond the word.
/// ```
#[property(resolve)]
pub const EXTENT: Length = Length::zero();
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 Show for StrikeNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled(
TextNode::DECO,
Decoration {
line: DecoLine::Strikethrough,
stroke: styles.get(Self::STROKE).unwrap_or_default(),
offset: styles.get(Self::OFFSET),
extent: styles.get(Self::EXTENT),
evade: false,
},
))
}
}
/// Defines a line that is positioned over, under or on top of text.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub 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.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum DecoLine {
Underline,
Strikethrough,
Overline,
}
/// 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 {
DecoLine::Strikethrough => font_metrics.strikethrough,
DecoLine::Overline => font_metrics.overline,
DecoLine::Underline => font_metrics.underline,
};
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 || !deco.evade {
let shape = Geometry::Line(target).stroked(stroke);
frame.push(origin, Element::Shape(shape));
}
};
if !deco.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();
}
}