mirror of
https://github.com/typst/typst
synced 2025-08-12 22:27:56 +08:00
Provide highlight
function for setting the background color of text (#2076)
This commit is contained in:
parent
09442d93ee
commit
71dccde031
@ -289,10 +289,11 @@ impl Layout for EquationElem {
|
||||
frame.push_frame(Point::new(x, y), counter)
|
||||
}
|
||||
} else {
|
||||
let font_size = TextElem::size_in(styles);
|
||||
let slack = ParElem::leading_in(styles) * 0.7;
|
||||
let top_edge = TextElem::top_edge_in(styles).resolve(styles, &font, None);
|
||||
let top_edge = TextElem::top_edge_in(styles).resolve(font_size, &font, None);
|
||||
let bottom_edge =
|
||||
-TextElem::bottom_edge_in(styles).resolve(styles, &font, None);
|
||||
-TextElem::bottom_edge_in(styles).resolve(font_size, &font, None);
|
||||
|
||||
let ascent = top_edge.max(frame.ascent() - slack);
|
||||
let descent = bottom_edge.max(frame.descent() - slack);
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -34,6 +34,11 @@ impl Length {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to an absolute length at the given font size.
|
||||
pub fn at(self, font_size: Abs) -> Abs {
|
||||
self.abs + self.em.at(font_size)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Length {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 47 KiB |
@ -20,14 +20,40 @@
|
||||
|
||||
---
|
||||
#let redact = strike.with(stroke: 10pt, extent: 0.05em)
|
||||
#let highlight = strike.with(stroke: 10pt + rgb("abcdef88"), extent: 0.05em)
|
||||
#let highlight-custom = strike.with(stroke: 10pt + rgb("abcdef88"), extent: 0.05em)
|
||||
|
||||
// Abuse thickness and transparency for redacting and highlighting stuff.
|
||||
Sometimes, we work #redact[in secret].
|
||||
There might be #highlight[redacted] things.
|
||||
underline()
|
||||
There might be #highlight-custom[redacted] things.
|
||||
|
||||
---
|
||||
// Test stroke folding.
|
||||
#set underline(stroke: 2pt, offset: 2pt)
|
||||
#underline(text(red, [DANGER!]))
|
||||
|
||||
---
|
||||
// Test highlight.
|
||||
This is the built-in #highlight[highlight with default color].
|
||||
We can also specify a customized value
|
||||
#highlight(fill: green.lighten(80%))[to highlight].
|
||||
|
||||
---
|
||||
// Test default highlight bounds.
|
||||
#highlight[ace],
|
||||
#highlight[base],
|
||||
#highlight[super],
|
||||
#highlight[phone #sym.integral]
|
||||
|
||||
---
|
||||
// Test a tighter highlight.
|
||||
#set highlight(top-edge: "x-height", bottom-edge: "baseline")
|
||||
#highlight[ace],
|
||||
#highlight[base],
|
||||
#highlight[super],
|
||||
#highlight[phone #sym.integral]
|
||||
|
||||
---
|
||||
// Test a bounds highlight.
|
||||
#set highlight(top-edge: "bounds", bottom-edge: "bounds")
|
||||
#highlight[abc]
|
||||
#highlight[abc #sym.integral]
|
||||
|
Loading…
x
Reference in New Issue
Block a user