summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/text
diff options
context:
space:
mode:
authorLU Jialin <luxxxlucy@gmail.com>2023-09-11 16:46:39 +0800
committerGitHub <noreply@github.com>2023-09-11 10:46:39 +0200
commit71dccde031dedc285042257c7e16bafb152dde58 (patch)
tree9ca1f2c1acbde6af59e643b5457f0912cd606790 /crates/typst-library/src/text
parent09442d93eebf176e42d768cbe7b5c31d331dc907 (diff)
Provide `highlight` function for setting the background color of text (#2076)
Diffstat (limited to 'crates/typst-library/src/text')
-rw-r--r--crates/typst-library/src/text/deco.rs186
-rw-r--r--crates/typst-library/src/text/mod.rs17
-rw-r--r--crates/typst-library/src/text/shaping.rs18
3 files changed, 172 insertions, 49 deletions
diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs
index 9ec4ca32..c97ef325 100644
--- a/crates/typst-library/src/text/deco.rs
+++ b/crates/typst-library/src/text/deco.rs
@@ -1,7 +1,7 @@
use kurbo::{BezPath, Line, ParamCurve};
use ttf_parser::{GlyphId, OutlineBuilder};
-use super::TextElem;
+use super::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric};
use crate::prelude::*;
/// Underlines text.
@@ -73,11 +73,12 @@ impl Show for UnderlineElem {
#[tracing::instrument(name = "UnderlineElem::show", skip_all)]
fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
Ok(self.body().styled(TextElem::set_deco(Decoration {
- line: DecoLine::Underline,
- stroke: self.stroke(styles).unwrap_or_default(),
- offset: self.offset(styles),
+ line: DecoLine::Underline {
+ stroke: self.stroke(styles).unwrap_or_default(),
+ offset: self.offset(styles),
+ evade: self.evade(styles),
+ },
extent: self.extent(styles),
- evade: self.evade(styles),
})))
}
}
@@ -157,11 +158,12 @@ impl Show for OverlineElem {
#[tracing::instrument(name = "OverlineElem::show", skip_all)]
fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
Ok(self.body().styled(TextElem::set_deco(Decoration {
- line: DecoLine::Overline,
- stroke: self.stroke(styles).unwrap_or_default(),
- offset: self.offset(styles),
+ line: DecoLine::Overline {
+ stroke: self.stroke(styles).unwrap_or_default(),
+ offset: self.offset(styles),
+ evade: self.evade(styles),
+ },
extent: self.extent(styles),
- evade: self.evade(styles),
})))
}
}
@@ -226,23 +228,98 @@ impl Show for StrikeElem {
#[tracing::instrument(name = "StrikeElem::show", skip_all)]
fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
Ok(self.body().styled(TextElem::set_deco(Decoration {
- line: DecoLine::Strikethrough,
- stroke: self.stroke(styles).unwrap_or_default(),
- offset: self.offset(styles),
+ // Note that we do not support evade option for strikethrough.
+ line: DecoLine::Strikethrough {
+ stroke: self.stroke(styles).unwrap_or_default(),
+ offset: self.offset(styles),
+ },
extent: self.extent(styles),
- evade: false,
})))
}
}
-/// Defines a line that is positioned over, under or on top of text.
+/// Highlights text with a background color.
+///
+/// ## Example { #example }
+/// ```example
+/// This is #highlight[important].
+/// ```
+///
+/// Display: Highlight
+/// Category: text
+#[element(Show)]
+pub struct HighlightElem {
+ /// The color to highlight the text with.
+ /// (Default: 0xffff5f)
+ ///
+ /// ```example
+ /// This is #highlight(fill: blue)[with blue].
+ /// ```
+ #[default(Color::Rgba(RgbaColor::new(0xFF, 0xFF, 0x5F, 0xFF)).into())]
+ pub fill: Paint,
+
+ /// The top end of the background rectangle. Note that top edge will update
+ /// to be always higher than the glyph's bounding box.
+ /// (default: "ascender")
+ ///
+ /// ```example
+ /// #set highlight(top-edge: "ascender")
+ /// #highlight[a] #highlight[aib]
+ ///
+ /// #set highlight(top-edge: "x-height")
+ /// #highlight[a] #highlight[aib]
+ /// ```
+ #[default(TopEdge::Metric(TopEdgeMetric::Ascender))]
+ pub top_edge: TopEdge,
+
+ /// The bottom end of the background rectangle. Note that top edge will update
+ /// to be always lower than the glyph's bounding box.
+ /// (default: "descender")
+ ///
+ /// ```example
+ /// #set highlight(bottom-edge: "descender")
+ /// #highlight[a] #highlight[ap]
+ ///
+ /// #set highlight(bottom-edge: "baseline")
+ /// #highlight[a] #highlight[ap]
+ /// ```
+ #[default(BottomEdge::Metric(BottomEdgeMetric::Descender))]
+ pub bottom_edge: BottomEdge,
+
+ /// The amount by which to extend the background to the sides beyond
+ /// (or within if negative) the content.
+ ///
+ /// ```example
+ /// A long #highlight(extent: 4pt)[background].
+ /// ```
+ #[resolve]
+ pub extent: Length,
+
+ /// The content that should be highlighted.
+ #[required]
+ pub body: Content,
+}
+
+impl Show for HighlightElem {
+ #[tracing::instrument(name = "HighlightElem::show", skip_all)]
+ fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
+ Ok(self.body().styled(TextElem::set_deco(Decoration {
+ line: DecoLine::Highlight {
+ fill: self.fill(styles),
+ top_edge: self.top_edge(styles),
+ bottom_edge: self.bottom_edge(styles),
+ },
+ extent: self.extent(styles),
+ })))
+ }
+}
+
+/// Defines a line-based decoration that is positioned over, under or on top of text,
+/// or highlights the text with a background.
#[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,
+ line: DecoLine,
+ extent: Abs,
}
impl Fold for Decoration {
@@ -259,11 +336,12 @@ cast! {
}
/// A kind of decorative line.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum DecoLine {
- Underline,
- Strikethrough,
- Overline,
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+enum DecoLine {
+ Underline { stroke: PartialStroke<Abs>, offset: Smart<Abs>, evade: bool },
+ Strikethrough { stroke: PartialStroke<Abs>, offset: Smart<Abs> },
+ Overline { stroke: PartialStroke<Abs>, offset: Smart<Abs>, evade: bool },
+ Highlight { fill: Paint, top_edge: TopEdge, bottom_edge: BottomEdge },
}
/// Add line decorations to a single run of shaped text.
@@ -271,19 +349,36 @@ pub(super) fn decorate(
frame: &mut Frame,
deco: &Decoration,
text: &TextItem,
+ width: Abs,
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,
+
+ if let DecoLine::Highlight { fill, top_edge, bottom_edge } = &deco.line {
+ let (top, bottom) = determine_edges(text, *top_edge, *bottom_edge);
+ let rect = Geometry::Rect(Size::new(width + 2.0 * deco.extent, top - bottom))
+ .filled(fill.clone());
+ let origin = Point::new(pos.x - deco.extent, pos.y - top - shift);
+ frame.prepend(origin, FrameItem::Shape(rect, Span::detached()));
+ return;
+ }
+
+ let (stroke, metrics, offset, evade) = match &deco.line {
+ DecoLine::Strikethrough { stroke, offset } => {
+ (stroke, font_metrics.strikethrough, offset, false)
+ }
+ DecoLine::Overline { stroke, offset, evade } => {
+ (stroke, font_metrics.overline, offset, *evade)
+ }
+ DecoLine::Underline { stroke, offset, evade } => {
+ (stroke, font_metrics.underline, offset, *evade)
+ }
+ _ => return,
};
- let offset = deco.offset.unwrap_or(-metrics.position.at(text.size)) - shift;
- let stroke = deco.stroke.clone().unwrap_or(Stroke {
+ let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift;
+ let stroke = stroke.clone().unwrap_or(Stroke {
paint: text.fill.clone(),
thickness: metrics.thickness.at(text.size),
..Stroke::default()
@@ -299,13 +394,13 @@ pub(super) fn decorate(
let origin = Point::new(from, pos.y + offset);
let target = Point::new(to - from, Abs::zero());
- if target.x >= min_width || !deco.evade {
+ if target.x >= min_width || !evade {
let shape = Geometry::Line(target).stroked(stroke.clone());
frame.push(origin, FrameItem::Shape(shape, Span::detached()));
}
};
- if !deco.evade {
+ if !evade {
push_segment(start, end);
return;
}
@@ -366,6 +461,31 @@ pub(super) fn decorate(
}
}
+// 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 bbox = None;
+ if top_edge.is_bounds() || bottom_edge.is_bounds() {
+ let ttf = text.font.ttf();
+ bbox = text
+ .glyphs
+ .iter()
+ .filter_map(|g| ttf.glyph_bounding_box(ttf_parser::GlyphId(g.id)))
+ .reduce(|a, b| ttf_parser::Rect {
+ y_max: a.y_max.max(b.y_max),
+ y_min: a.y_min.min(b.y_min),
+ ..a
+ });
+ }
+
+ let top = top_edge.resolve(text.size, &text.font, bbox);
+ let bottom = bottom_edge.resolve(text.size, &text.font, bbox);
+ (top, bottom)
+}
+
/// Builds a kurbo [`BezPath`] for a glyph.
struct BezPathBuilder {
path: BezPath,
diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs
index 9ab18ad9..4f3c1591 100644
--- a/crates/typst-library/src/text/mod.rs
+++ b/crates/typst-library/src/text/mod.rs
@@ -35,6 +35,7 @@ pub(super) fn define(global: &mut Scope) {
global.define("super", SuperElem::func());
global.define("underline", UnderlineElem::func());
global.define("strike", StrikeElem::func());
+ global.define("highlight", HighlightElem::func());
global.define("overline", OverlineElem::func());
global.define("raw", RawElem::func());
global.define("lorem", lorem_func());
@@ -652,17 +653,17 @@ impl TopEdge {
}
/// Resolve the value of the text edge given a font's metrics.
- pub fn resolve(self, styles: StyleChain, font: &Font, bbox: Option<Rect>) -> Abs {
+ pub fn resolve(self, font_size: Abs, font: &Font, bbox: Option<Rect>) -> Abs {
match self {
TopEdge::Metric(metric) => {
if let Ok(metric) = metric.try_into() {
- font.metrics().vertical(metric).resolve(styles)
+ font.metrics().vertical(metric).at(font_size)
} else {
- bbox.map(|bbox| (font.to_em(bbox.y_max)).resolve(styles))
+ bbox.map(|bbox| (font.to_em(bbox.y_max)).at(font_size))
.unwrap_or_default()
}
}
- TopEdge::Length(length) => length.resolve(styles),
+ TopEdge::Length(length) => length.at(font_size),
}
}
}
@@ -722,17 +723,17 @@ impl BottomEdge {
}
/// Resolve the value of the text edge given a font's metrics.
- pub fn resolve(self, styles: StyleChain, font: &Font, bbox: Option<Rect>) -> Abs {
+ pub fn resolve(self, font_size: Abs, font: &Font, bbox: Option<Rect>) -> Abs {
match self {
BottomEdge::Metric(metric) => {
if let Ok(metric) = metric.try_into() {
- font.metrics().vertical(metric).resolve(styles)
+ font.metrics().vertical(metric).at(font_size)
} else {
- bbox.map(|bbox| (font.to_em(bbox.y_min)).resolve(styles))
+ bbox.map(|bbox| (font.to_em(bbox.y_min)).at(font_size))
.unwrap_or_default()
}
}
- BottomEdge::Length(length) => length.resolve(styles),
+ BottomEdge::Length(length) => length.at(font_size),
}
}
}
diff --git a/crates/typst-library/src/text/shaping.rs b/crates/typst-library/src/text/shaping.rs
index 53289e26..21331a78 100644
--- a/crates/typst-library/src/text/shaping.rs
+++ b/crates/typst-library/src/text/shaping.rs
@@ -293,15 +293,17 @@ impl<'a> ShapedText<'a> {
glyphs,
};
- let layer = frame.layer();
let width = item.width();
-
- // Apply line decorations.
- for deco in &decos {
- decorate(&mut frame, deco, &item, shift, pos, width);
+ if decos.is_empty() {
+ frame.push(pos, FrameItem::Text(item));
+ } else {
+ // Apply line decorations.
+ frame.push(pos, FrameItem::Text(item.clone()));
+ for deco in &decos {
+ decorate(&mut frame, deco, &item, width, shift, pos);
+ }
}
- frame.insert(layer, pos, FrameItem::Text(item));
offset += width;
}
@@ -321,8 +323,8 @@ impl<'a> ShapedText<'a> {
// Expand top and bottom by reading the font's vertical metrics.
let mut expand = |font: &Font, bbox: Option<ttf_parser::Rect>| {
- top.set_max(top_edge.resolve(self.styles, font, bbox));
- bottom.set_max(-bottom_edge.resolve(self.styles, font, bbox));
+ top.set_max(top_edge.resolve(self.size, font, bbox));
+ bottom.set_max(-bottom_edge.resolve(self.size, font, bbox));
};
if self.glyphs.is_empty() {