Provide highlight function for setting the background color of text (#2076)

This commit is contained in:
LU Jialin 2023-09-11 16:46:39 +08:00 committed by GitHub
parent 09442d93ee
commit 71dccde031
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 209 additions and 54 deletions

View File

@ -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);

View File

@ -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,

View File

@ -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),
}
}
}

View File

@ -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() {

View File

@ -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

View File

@ -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]