diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index 6fd56bb63..6d46305bf 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -60,6 +60,16 @@ pub struct UnderlineElem { #[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, @@ -73,6 +83,7 @@ impl Show for UnderlineElem { stroke: self.stroke(styles).unwrap_or_default(), offset: self.offset(styles), evade: self.evade(styles), + background: self.background(styles), }, extent: self.extent(styles), }))) @@ -141,6 +152,16 @@ pub struct OverlineElem { #[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, @@ -154,6 +175,7 @@ impl Show for OverlineElem { stroke: self.stroke(styles).unwrap_or_default(), offset: self.offset(styles), evade: self.evade(styles), + background: self.background(styles), }, extent: self.extent(styles), }))) @@ -207,6 +229,16 @@ pub struct StrikeElem { #[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, @@ -220,6 +252,7 @@ impl Show for StrikeElem { line: DecoLine::Strikethrough { stroke: self.stroke(styles).unwrap_or_default(), offset: self.offset(styles), + background: self.background(styles), }, extent: self.extent(styles), }))) @@ -320,9 +353,9 @@ cast! { /// A kind of decorative line. #[derive(Debug, Clone, Eq, PartialEq, Hash)] enum DecoLine { - Underline { stroke: Stroke, offset: Smart, evade: bool }, - Strikethrough { stroke: Stroke, offset: Smart }, - Overline { stroke: Stroke, offset: Smart, evade: bool }, + Underline { stroke: Stroke, offset: Smart, evade: bool, background: bool }, + Strikethrough { stroke: Stroke, offset: Smart, background: bool }, + Overline { stroke: Stroke, offset: Smart, evade: bool, background: bool }, Highlight { fill: Paint, top_edge: TopEdge, bottom_edge: BottomEdge }, } @@ -346,15 +379,15 @@ pub(super) fn decorate( return; } - let (stroke, metrics, offset, evade) = match &deco.line { - DecoLine::Strikethrough { stroke, offset } => { - (stroke, font_metrics.strikethrough, offset, false) + let (stroke, metrics, offset, evade, background) = match &deco.line { + DecoLine::Strikethrough { stroke, offset, background } => { + (stroke, font_metrics.strikethrough, offset, false, *background) } - DecoLine::Overline { stroke, offset, evade } => { - (stroke, font_metrics.overline, offset, *evade) + DecoLine::Overline { stroke, offset, evade, background } => { + (stroke, font_metrics.overline, offset, *evade, *background) } - DecoLine::Underline { stroke, offset, evade } => { - (stroke, font_metrics.underline, offset, *evade) + DecoLine::Underline { stroke, offset, evade, background } => { + (stroke, font_metrics.underline, offset, *evade, *background) } _ => return, }; @@ -372,18 +405,23 @@ pub(super) fn decorate( let start = pos.x - deco.extent; let end = pos.x + (width + 2.0 * deco.extent); - let mut push_segment = |from: Abs, to: Abs| { + let mut push_segment = |from: Abs, to: Abs, prepend: bool| { let origin = Point::new(from, pos.y + offset); let target = Point::new(to - from, Abs::zero()); if target.x >= min_width || !evade { let shape = Geometry::Line(target).stroked(stroke.clone()); - frame.push(origin, FrameItem::Shape(shape, Span::detached())); + + if prepend { + frame.prepend(origin, FrameItem::Shape(shape, Span::detached())); + } else { + frame.push(origin, FrameItem::Shape(shape, Span::detached())); + } } }; if !evade { - push_segment(start, end); + push_segment(start, end, background); return; } @@ -438,7 +476,7 @@ pub(super) fn decorate( if r - l < gap_padding { continue; } else { - push_segment(l + gap_padding, r - gap_padding); + push_segment(l + gap_padding, r - gap_padding, background); } } } diff --git a/tests/ref/text/deco.png b/tests/ref/text/deco.png index 006c969fb..104c5a12a 100644 Binary files a/tests/ref/text/deco.png and b/tests/ref/text/deco.png differ diff --git a/tests/typ/text/deco.typ b/tests/typ/text/deco.typ index b79b80b22..dbf86a864 100644 --- a/tests/typ/text/deco.typ +++ b/tests/typ/text/deco.typ @@ -57,3 +57,19 @@ We can also specify a customized value #set highlight(top-edge: "bounds", bottom-edge: "bounds") #highlight[abc] #highlight[abc #sym.integral] + +--- +// Test underline background +#set underline(background: true, stroke: (thickness: 0.5em, paint: red, cap: "round")) +#underline[This is in the background] + +--- +// Test overline background +#set overline(background: true, stroke: (thickness: 0.5em, paint: red, cap: "round")) +#overline[This is in the background] + + +--- +// Test strike background +#set strike(background: true, stroke: 5pt + red) +#strike[This is in the background]