Tobias Schmitz ad8abf5812
feat: [WIP] make more things locatable
skip-checks:true
2025-06-18 10:42:58 +02:00

404 lines
11 KiB
Rust

use smallvec::smallvec;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Show, Smart, StyleChain};
use crate::introspection::Locatable;
use crate::layout::{Abs, Corners, Length, Rel, Sides};
use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric};
use crate::visualize::{Color, FixedStroke, Paint, Stroke};
/// Underlines text.
///
/// # Example
/// ```example
/// This is #underline[important].
/// ```
#[elem(Locatable, Show)]
pub struct UnderlineElem {
/// How to [stroke] the line.
///
/// If set to `{auto}`, takes on the text's color and a thickness defined in
/// the current font.
///
/// ```example
/// Take #underline(
/// stroke: 1.5pt + red,
/// offset: 2pt,
/// [care],
/// )
/// ```
#[resolve]
#[fold]
pub stroke: Smart<Stroke>,
/// The 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
/// ]
/// ```
#[resolve]
pub offset: Smart<Length>,
/// The amount by which to extend the line beyond (or within if negative)
/// the content.
///
/// ```example
/// #align(center,
/// underline(extent: 2pt)[Chapter 1]
/// )
/// ```
#[resolve]
pub extent: Length,
/// 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].
/// ```
#[default(true)]
pub evade: bool,
/// Whether the line is placed behind the content it underlines.
///
/// ```example
/// #set underline(stroke: (thickness: 1em, paint: maroon, cap: "round"))
/// #underline(background: true)[This is stylized.] \
/// #underline(background: false)[This is partially hidden.]
/// ```
#[default(false)]
pub background: bool,
/// The content to underline.
#[required]
pub body: Content,
}
impl Show for Packed<UnderlineElem> {
#[typst_macros::time(name = "underline", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
line: DecoLine::Underline {
stroke: self.stroke(styles).unwrap_or_default(),
offset: self.offset(styles),
evade: self.evade(styles),
background: self.background(styles),
},
extent: self.extent(styles),
}])))
}
}
/// Adds a line over text.
///
/// # Example
/// ```example
/// #overline[A line over text.]
/// ```
#[elem(Locatable, Show)]
pub struct OverlineElem {
/// How to [stroke] the line.
///
/// If set to `{auto}`, takes on the text's color and a thickness defined in
/// the current font.
///
/// ```example
/// #set text(fill: olive)
/// #overline(
/// stroke: green.darken(20%),
/// offset: -12pt,
/// [The Forest Theme],
/// )
/// ```
#[resolve]
#[fold]
pub stroke: Smart<Stroke>,
/// The 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
/// ]
/// ```
#[resolve]
pub offset: Smart<Length>,
/// The amount by which to extend the line beyond (or within if negative)
/// the content.
///
/// ```example
/// #set overline(extent: 4pt)
/// #set underline(extent: 4pt)
/// #overline(underline[Typography Today])
/// ```
#[resolve]
pub extent: Length,
/// 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],
/// )
/// ```
#[default(true)]
pub evade: bool,
/// Whether the line is placed behind the content it overlines.
///
/// ```example
/// #set overline(stroke: (thickness: 1em, paint: maroon, cap: "round"))
/// #overline(background: true)[This is stylized.] \
/// #overline(background: false)[This is partially hidden.]
/// ```
#[default(false)]
pub background: bool,
/// The content to add a line over.
#[required]
pub body: Content,
}
impl Show for Packed<OverlineElem> {
#[typst_macros::time(name = "overline", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
line: DecoLine::Overline {
stroke: self.stroke(styles).unwrap_or_default(),
offset: self.offset(styles),
evade: self.evade(styles),
background: self.background(styles),
},
extent: self.extent(styles),
}])))
}
}
/// Strikes through text.
///
/// # Example
/// ```example
/// This is #strike[not] relevant.
/// ```
#[elem(title = "Strikethrough", Locatable, Show)]
pub struct StrikeElem {
/// How to [stroke] the line.
///
/// If set to `{auto}`, takes on the text's color and a thickness defined in
/// the current font.
///
/// _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].
/// ```
#[resolve]
#[fold]
pub stroke: Smart<Stroke>,
/// The 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(font: "Inria Serif")
/// This is #strike(offset: auto)[low-ish]. \
/// This is #strike(offset: -3.5pt)[on-top].
/// ```
#[resolve]
pub offset: Smart<Length>,
/// The amount by which to extend the line beyond (or within if negative)
/// the content.
///
/// ```example
/// This #strike(extent: -2pt)[skips] parts of the word.
/// This #strike(extent: 2pt)[extends] beyond the word.
/// ```
#[resolve]
pub extent: Length,
/// Whether the line is placed behind the content.
///
/// ```example
/// #set strike(stroke: red)
/// #strike(background: true)[This is behind.] \
/// #strike(background: false)[This is in front.]
/// ```
#[default(false)]
pub background: bool,
/// The content to strike through.
#[required]
pub body: Content,
}
impl Show for Packed<StrikeElem> {
#[typst_macros::time(name = "strike", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
// Note that we do not support evade option for strikethrough.
line: DecoLine::Strikethrough {
stroke: self.stroke(styles).unwrap_or_default(),
offset: self.offset(styles),
background: self.background(styles),
},
extent: self.extent(styles),
}])))
}
}
/// Highlights text with a background color.
///
/// # Example
/// ```example
/// This is #highlight[important].
/// ```
#[elem(Locatable, Show)]
pub struct HighlightElem {
/// The color to highlight the text with.
///
/// ```example
/// This is #highlight(
/// fill: blue
/// )[highlighted with blue].
/// ```
#[default(Some(Color::from_u8(0xFF, 0xFD, 0x11, 0xA1).into()))]
pub fill: Option<Paint>,
/// The highlight's border color. See the
/// [rectangle's documentation]($rect.stroke) for more details.
///
/// ```example
/// This is a #highlight(
/// stroke: fuchsia
/// )[stroked highlighting].
/// ```
#[resolve]
#[fold]
pub stroke: Sides<Option<Option<Stroke>>>,
/// The top end of the background rectangle.
///
/// ```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.
///
/// ```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,
/// How much to round the highlight's corners. See the
/// [rectangle's documentation]($rect.radius) for more details.
///
/// ```example
/// Listen #highlight(
/// radius: 5pt, extent: 2pt
/// )[carefully], it will be on the test.
/// ```
#[resolve]
#[fold]
pub radius: Corners<Option<Rel<Length>>>,
/// The content that should be highlighted.
#[required]
pub body: Content,
}
impl Show for Packed<HighlightElem> {
#[typst_macros::time(name = "highlight", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
line: DecoLine::Highlight {
fill: self.fill(styles),
stroke: self
.stroke(styles)
.unwrap_or_default()
.map(|stroke| stroke.map(Stroke::unwrap_or_default)),
top_edge: self.top_edge(styles),
bottom_edge: self.bottom_edge(styles),
radius: self.radius(styles).unwrap_or_default(),
},
extent: self.extent(styles),
}])))
}
}
/// A text decoration.
///
/// Can be positioned over, under, or on top of text, or highlight the text with
/// a background.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Decoration {
pub line: DecoLine,
pub extent: Abs,
}
/// A kind of decorative line.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[allow(clippy::large_enum_variant)]
pub enum DecoLine {
Underline {
stroke: Stroke<Abs>,
offset: Smart<Abs>,
evade: bool,
background: bool,
},
Strikethrough {
stroke: Stroke<Abs>,
offset: Smart<Abs>,
background: bool,
},
Overline {
stroke: Stroke<Abs>,
offset: Smart<Abs>,
evade: bool,
background: bool,
},
Highlight {
fill: Option<Paint>,
stroke: Sides<Option<FixedStroke>>,
top_edge: TopEdge,
bottom_edge: BottomEdge,
radius: Corners<Rel<Abs>>,
},
}