mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Text decorations
This commit is contained in:
parent
5611c26577
commit
c28708aa19
@ -125,13 +125,19 @@ pub struct FontState {
|
|||||||
/// The bottom end of the text bounding box.
|
/// The bottom end of the text bounding box.
|
||||||
pub bottom_edge: VerticalFontMetric,
|
pub bottom_edge: VerticalFontMetric,
|
||||||
/// The glyph fill color / texture.
|
/// The glyph fill color / texture.
|
||||||
pub color: Fill,
|
pub fill: Fill,
|
||||||
/// Whether the strong toggle is active or inactive. This determines
|
/// Whether the strong toggle is active or inactive. This determines
|
||||||
/// whether the next `*` adds or removes font weight.
|
/// whether the next `*` adds or removes font weight.
|
||||||
pub strong: bool,
|
pub strong: bool,
|
||||||
/// Whether the emphasis toggle is active or inactive. This determines
|
/// Whether the emphasis toggle is active or inactive. This determines
|
||||||
/// whether the next `_` makes italic or non-italic.
|
/// whether the next `_` makes italic or non-italic.
|
||||||
pub emph: bool,
|
pub emph: bool,
|
||||||
|
/// The specifications for a strikethrough line, if any.
|
||||||
|
pub strikethrough: Option<LineState>,
|
||||||
|
/// The specifications for a underline, if any.
|
||||||
|
pub underline: Option<LineState>,
|
||||||
|
/// The specifications for a overline line, if any.
|
||||||
|
pub overline: Option<LineState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FontState {
|
impl FontState {
|
||||||
@ -156,13 +162,17 @@ impl FontState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let size = self.resolve_size();
|
||||||
FontProps {
|
FontProps {
|
||||||
families: Rc::clone(&self.families),
|
families: Rc::clone(&self.families),
|
||||||
variant,
|
variant,
|
||||||
size: self.resolve_size(),
|
size,
|
||||||
top_edge: self.top_edge,
|
top_edge: self.top_edge,
|
||||||
bottom_edge: self.bottom_edge,
|
bottom_edge: self.bottom_edge,
|
||||||
fill: self.color,
|
strikethrough: self.strikethrough.map(|s| s.resolve_props(size, &self.fill)),
|
||||||
|
underline: self.underline.map(|s| s.resolve_props(size, &self.fill)),
|
||||||
|
overline: self.overline.map(|s| s.resolve_props(size, &self.fill)),
|
||||||
|
fill: self.fill,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,9 +195,39 @@ impl Default for FontState {
|
|||||||
top_edge: VerticalFontMetric::CapHeight,
|
top_edge: VerticalFontMetric::CapHeight,
|
||||||
bottom_edge: VerticalFontMetric::Baseline,
|
bottom_edge: VerticalFontMetric::Baseline,
|
||||||
scale: Linear::one(),
|
scale: Linear::one(),
|
||||||
color: Fill::Color(Color::Rgba(RgbaColor::BLACK)),
|
fill: Fill::Color(Color::Rgba(RgbaColor::BLACK)),
|
||||||
strong: false,
|
strong: false,
|
||||||
emph: false,
|
emph: false,
|
||||||
|
strikethrough: None,
|
||||||
|
underline: None,
|
||||||
|
overline: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a line that could be positioned over or under text.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Hash)]
|
||||||
|
pub struct LineState {
|
||||||
|
/// Color of the line. Will default to text color if `None`.
|
||||||
|
pub fill: Option<Fill>,
|
||||||
|
/// Thickness of the line's stroke. Calling functions should attempt to
|
||||||
|
/// read this value from the appropriate font tables if this is `None`.
|
||||||
|
pub strength: Option<Linear>,
|
||||||
|
/// Position of the line relative to the baseline. Calling functions should
|
||||||
|
/// attempt to read this value from the appropriate font tables if this is
|
||||||
|
/// `None`.
|
||||||
|
pub position: Option<Linear>,
|
||||||
|
/// Amount that the line will be longer or shorter than its associated text.
|
||||||
|
pub extent: Linear,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineState {
|
||||||
|
pub fn resolve_props(&self, font_size: Length, fill: &Fill) -> LineProps {
|
||||||
|
LineProps {
|
||||||
|
fill: self.fill.unwrap_or_else(|| fill.clone()),
|
||||||
|
strength: self.strength.map(|s| s.resolve(font_size)),
|
||||||
|
position: self.position.map(|p| p.resolve(font_size)),
|
||||||
|
extent: self.extent.resolve(font_size),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -207,6 +247,12 @@ pub struct FontProps {
|
|||||||
pub bottom_edge: VerticalFontMetric,
|
pub bottom_edge: VerticalFontMetric,
|
||||||
/// The fill color of the text.
|
/// The fill color of the text.
|
||||||
pub fill: Fill,
|
pub fill: Fill,
|
||||||
|
/// The specifications for a strikethrough line, if any.
|
||||||
|
pub strikethrough: Option<LineProps>,
|
||||||
|
/// The specifications for a underline, if any.
|
||||||
|
pub underline: Option<LineProps>,
|
||||||
|
/// The specifications for a overline line, if any.
|
||||||
|
pub overline: Option<LineProps>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Font family definitions.
|
/// Font family definitions.
|
||||||
@ -273,3 +319,19 @@ impl Display for FontFamily {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Describes a line that could be positioned over or under text.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Hash)]
|
||||||
|
pub struct LineProps {
|
||||||
|
/// Color of the line.
|
||||||
|
pub fill: Fill,
|
||||||
|
/// Thickness of the line's stroke. Calling functions should attempt to
|
||||||
|
/// read this value from the appropriate font tables if this is `None`.
|
||||||
|
pub strength: Option<Length>,
|
||||||
|
/// Position of the line relative to the baseline. Calling functions should
|
||||||
|
/// attempt to read this value from the appropriate font tables if this is
|
||||||
|
/// `None`.
|
||||||
|
pub position: Option<Length>,
|
||||||
|
/// Amount that the line will be longer or shorter than its associated text.
|
||||||
|
pub extent: Length,
|
||||||
|
}
|
||||||
|
@ -183,12 +183,17 @@ impl<'a> PdfExporter<'a> {
|
|||||||
content.rect(x, y - h, w, h, false, true);
|
content.rect(x, y - h, w, h, false, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Shape::Ellipse(size) => {
|
Shape::Ellipse(size) => {
|
||||||
let path = geom::Path::ellipse(size);
|
let path = geom::Path::ellipse(size);
|
||||||
write_path(&mut content, x, y, &path, false, true);
|
write_path(&mut content, x, y, &path, false, true);
|
||||||
}
|
}
|
||||||
|
Shape::Line(target, stroke) => {
|
||||||
|
write_stroke(&mut content, fill, stroke.to_pt() as f32);
|
||||||
|
content.path(true, false).move_to(x, y).line_to(
|
||||||
|
x + target.x.to_pt() as f32,
|
||||||
|
y - target.y.to_pt() as f32,
|
||||||
|
);
|
||||||
|
}
|
||||||
Shape::Path(ref path) => {
|
Shape::Path(ref path) => {
|
||||||
write_path(&mut content, x, y, path, false, true)
|
write_path(&mut content, x, y, path, false, true)
|
||||||
}
|
}
|
||||||
@ -371,6 +376,20 @@ fn write_fill(content: &mut Content, fill: Fill) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write a stroke change into a content stream.
|
||||||
|
fn write_stroke(content: &mut Content, fill: Fill, thickness: f32) {
|
||||||
|
match fill {
|
||||||
|
Fill::Color(Color::Rgba(c)) => {
|
||||||
|
content.stroke_rgb(
|
||||||
|
c.r as f32 / 255.0,
|
||||||
|
c.g as f32 / 255.0,
|
||||||
|
c.b as f32 / 255.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content.line_width(thickness);
|
||||||
|
}
|
||||||
|
|
||||||
/// Write a path into a content stream.
|
/// Write a path into a content stream.
|
||||||
fn write_path(
|
fn write_path(
|
||||||
content: &mut Content,
|
content: &mut Content,
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{self, Debug, Display, Formatter};
|
use std::fmt::{self, Debug, Display, Formatter};
|
||||||
|
use std::ops::Add;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -156,6 +157,14 @@ impl Em {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Add for Em {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, other: Self) -> Self {
|
||||||
|
Self(self.0 + other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Caches parsed font faces.
|
/// Caches parsed font faces.
|
||||||
pub struct FontCache {
|
pub struct FontCache {
|
||||||
faces: Vec<Option<Face>>,
|
faces: Vec<Option<Face>>,
|
||||||
|
@ -92,12 +92,14 @@ pub enum Shape {
|
|||||||
Rect(Size),
|
Rect(Size),
|
||||||
/// An ellipse with its origin in the center.
|
/// An ellipse with its origin in the center.
|
||||||
Ellipse(Size),
|
Ellipse(Size),
|
||||||
|
/// A line to a `Point` (relative to its position) with a stroke width.
|
||||||
|
Line(Point, Length),
|
||||||
/// A bezier path.
|
/// A bezier path.
|
||||||
Path(Path),
|
Path(Path),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How text and shapes are filled.
|
/// How text and shapes are filled.
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||||
pub enum Fill {
|
pub enum Fill {
|
||||||
/// A solid color.
|
/// A solid color.
|
||||||
Color(Color),
|
Color(Color),
|
||||||
|
@ -190,7 +190,7 @@ impl<'a> ParLayout<'a> {
|
|||||||
while !stack.regions.current.height.fits(line.size.height)
|
while !stack.regions.current.height.fits(line.size.height)
|
||||||
&& !stack.regions.in_full_last()
|
&& !stack.regions.in_full_last()
|
||||||
{
|
{
|
||||||
stack.finish_region();
|
stack.finish_region(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the line does not fit horizontally or we have a mandatory
|
// If the line does not fit horizontally or we have a mandatory
|
||||||
@ -217,7 +217,7 @@ impl<'a> ParLayout<'a> {
|
|||||||
stack.push(line);
|
stack.push(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.finish()
|
stack.finish(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the index of the item whose range contains the `text_offset`.
|
/// Find the index of the item whose range contains the `text_offset`.
|
||||||
@ -302,7 +302,7 @@ impl<'a> LineStack<'a> {
|
|||||||
self.lines.push(line);
|
self.lines.push(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_region(&mut self) {
|
fn finish_region(&mut self, ctx: &LayoutContext) {
|
||||||
if self.regions.fixed.horizontal {
|
if self.regions.fixed.horizontal {
|
||||||
self.size.width = self.regions.current.width;
|
self.size.width = self.regions.current.width;
|
||||||
}
|
}
|
||||||
@ -312,7 +312,7 @@ impl<'a> LineStack<'a> {
|
|||||||
let mut first = true;
|
let mut first = true;
|
||||||
|
|
||||||
for line in std::mem::take(&mut self.lines) {
|
for line in std::mem::take(&mut self.lines) {
|
||||||
let frame = line.build(self.size.width);
|
let frame = line.build(ctx, self.size.width);
|
||||||
|
|
||||||
let pos = Point::new(Length::zero(), offset);
|
let pos = Point::new(Length::zero(), offset);
|
||||||
if first {
|
if first {
|
||||||
@ -329,8 +329,8 @@ impl<'a> LineStack<'a> {
|
|||||||
self.size = Size::zero();
|
self.size = Size::zero();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(mut self) -> Vec<Frame> {
|
fn finish(mut self, ctx: &LayoutContext) -> Vec<Frame> {
|
||||||
self.finish_region();
|
self.finish_region(ctx);
|
||||||
self.finished
|
self.finished
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -447,7 +447,7 @@ impl<'a> LineLayout<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build the line's frame.
|
/// Build the line's frame.
|
||||||
fn build(&self, width: Length) -> Frame {
|
fn build(&self, ctx: &LayoutContext, width: Length) -> Frame {
|
||||||
let size = Size::new(self.size.width.max(width), self.size.height);
|
let size = Size::new(self.size.width.max(width), self.size.height);
|
||||||
let free = size.width - self.size.width;
|
let free = size.width - self.size.width;
|
||||||
|
|
||||||
@ -463,7 +463,7 @@ impl<'a> LineLayout<'a> {
|
|||||||
}
|
}
|
||||||
ParItem::Text(ref shaped, align) => {
|
ParItem::Text(ref shaped, align) => {
|
||||||
ruler = ruler.max(align);
|
ruler = ruler.max(align);
|
||||||
shaped.build()
|
shaped.build(ctx)
|
||||||
}
|
}
|
||||||
ParItem::Frame(ref frame, align) => {
|
ParItem::Frame(ref frame, align) => {
|
||||||
ruler = ruler.max(align);
|
ruler = ruler.max(align);
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::ops::Range;
|
use std::ops::{Add, Range};
|
||||||
|
|
||||||
use rustybuzz::UnicodeBuffer;
|
use rustybuzz::UnicodeBuffer;
|
||||||
|
|
||||||
use super::{Element, Frame, Glyph, LayoutContext, Text};
|
use super::{Element, Frame, Glyph, LayoutContext, Text};
|
||||||
use crate::exec::FontProps;
|
use crate::exec::FontProps;
|
||||||
use crate::font::{Face, FaceId};
|
use crate::font::{Em, Face, FaceId, VerticalFontMetric};
|
||||||
use crate::geom::{Dir, Length, Point, Size};
|
use crate::geom::{Dir, Length, Point, Size};
|
||||||
|
use crate::layout::Shape;
|
||||||
use crate::util::SliceExt;
|
use crate::util::SliceExt;
|
||||||
|
|
||||||
/// The result of shaping text.
|
/// The result of shaping text.
|
||||||
@ -59,12 +60,13 @@ enum Side {
|
|||||||
|
|
||||||
impl<'a> ShapedText<'a> {
|
impl<'a> ShapedText<'a> {
|
||||||
/// Build the shaped text's frame.
|
/// Build the shaped text's frame.
|
||||||
pub fn build(&self) -> Frame {
|
pub fn build(&self, ctx: &LayoutContext) -> Frame {
|
||||||
let mut frame = Frame::new(self.size, self.baseline);
|
let mut frame = Frame::new(self.size, self.baseline);
|
||||||
let mut offset = Length::zero();
|
let mut offset = Length::zero();
|
||||||
|
|
||||||
for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
|
for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
|
||||||
let pos = Point::new(offset, self.baseline);
|
let pos = Point::new(offset, self.baseline);
|
||||||
|
|
||||||
let mut text = Text {
|
let mut text = Text {
|
||||||
face_id,
|
face_id,
|
||||||
size: self.props.size,
|
size: self.props.size,
|
||||||
@ -72,16 +74,20 @@ impl<'a> ShapedText<'a> {
|
|||||||
glyphs: vec![],
|
glyphs: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut width = Length::zero();
|
||||||
for glyph in group {
|
for glyph in group {
|
||||||
text.glyphs.push(Glyph {
|
text.glyphs.push(Glyph {
|
||||||
id: glyph.glyph_id,
|
id: glyph.glyph_id,
|
||||||
x_advance: glyph.x_advance,
|
x_advance: glyph.x_advance,
|
||||||
x_offset: glyph.x_offset,
|
x_offset: glyph.x_offset,
|
||||||
});
|
});
|
||||||
offset += glyph.x_advance;
|
width += glyph.x_advance;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.push(pos, Element::Text(text));
|
frame.push(pos, Element::Text(text));
|
||||||
|
decorate(ctx, &mut frame, &self.props, face_id, pos, width);
|
||||||
|
|
||||||
|
offset += width;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame
|
frame
|
||||||
@ -364,3 +370,81 @@ fn measure(
|
|||||||
|
|
||||||
(Size::new(width, top + bottom), top)
|
(Size::new(width, top + bottom), top)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add underline, strikthrough and overline decorations.
|
||||||
|
fn decorate(
|
||||||
|
ctx: &LayoutContext,
|
||||||
|
frame: &mut Frame,
|
||||||
|
props: &FontProps,
|
||||||
|
face_id: FaceId,
|
||||||
|
pos: Point,
|
||||||
|
width: Length,
|
||||||
|
) {
|
||||||
|
let mut apply = |strength, position, extent, fill| {
|
||||||
|
let pos = Point::new(pos.x - extent, pos.y - position);
|
||||||
|
let target = Point::new(width + 2.0 * extent, Length::zero());
|
||||||
|
frame.push(pos, Element::Geometry(Shape::Line(target, strength), fill));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(strikethrough) = props.strikethrough {
|
||||||
|
let face = ctx.cache.font.get(face_id);
|
||||||
|
|
||||||
|
let strength = strikethrough.strength.unwrap_or_else(|| {
|
||||||
|
face.ttf()
|
||||||
|
.strikeout_metrics()
|
||||||
|
.or_else(|| face.ttf().underline_metrics())
|
||||||
|
.map_or(Em::new(0.06), |m| face.to_em(m.thickness))
|
||||||
|
.to_length(props.size)
|
||||||
|
});
|
||||||
|
|
||||||
|
let position = strikethrough.position.unwrap_or_else(|| {
|
||||||
|
face.ttf()
|
||||||
|
.strikeout_metrics()
|
||||||
|
.map_or(Em::new(0.25), |m| face.to_em(m.position))
|
||||||
|
.to_length(props.size)
|
||||||
|
});
|
||||||
|
|
||||||
|
apply(strength, position, strikethrough.extent, strikethrough.fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(underline) = props.underline {
|
||||||
|
let face = ctx.cache.font.get(face_id);
|
||||||
|
|
||||||
|
let strength = underline.strength.unwrap_or_else(|| {
|
||||||
|
face.ttf()
|
||||||
|
.underline_metrics()
|
||||||
|
.or_else(|| face.ttf().strikeout_metrics())
|
||||||
|
.map_or(Em::new(0.06), |m| face.to_em(m.thickness))
|
||||||
|
.to_length(props.size)
|
||||||
|
});
|
||||||
|
|
||||||
|
let position = underline.position.unwrap_or_else(|| {
|
||||||
|
face.ttf()
|
||||||
|
.underline_metrics()
|
||||||
|
.map_or(Em::new(-0.2), |m| face.to_em(m.position))
|
||||||
|
.to_length(props.size)
|
||||||
|
});
|
||||||
|
|
||||||
|
apply(strength, position, underline.extent, underline.fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(overline) = props.overline {
|
||||||
|
let face = ctx.cache.font.get(face_id);
|
||||||
|
|
||||||
|
let strength = overline.strength.unwrap_or_else(|| {
|
||||||
|
face.ttf()
|
||||||
|
.underline_metrics()
|
||||||
|
.or_else(|| face.ttf().strikeout_metrics())
|
||||||
|
.map_or(Em::new(0.06), |m| face.to_em(m.thickness))
|
||||||
|
.to_length(props.size)
|
||||||
|
});
|
||||||
|
|
||||||
|
let position = overline.position.unwrap_or_else(|| {
|
||||||
|
face.vertical_metric(VerticalFontMetric::CapHeight)
|
||||||
|
.add(Em::new(0.1))
|
||||||
|
.to_length(props.size)
|
||||||
|
});
|
||||||
|
|
||||||
|
apply(strength, position, overline.extent, overline.fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
84
src/library/decorations.rs
Normal file
84
src/library/decorations.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
use crate::exec::{FontState, LineState};
|
||||||
|
use crate::layout::Fill;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// `strike`: Enable striken-through text.
|
||||||
|
///
|
||||||
|
/// # Named parameters
|
||||||
|
/// - Color: `color`, of type `color`.
|
||||||
|
/// - Baseline offset: `position`, of type `linear`.
|
||||||
|
/// - Strength: `strength`, of type `linear`.
|
||||||
|
/// - Extent that is applied on either end of the line: `extent`, of type
|
||||||
|
/// `linear`.
|
||||||
|
///
|
||||||
|
/// # Return value
|
||||||
|
/// A template that enables striken-through text. The effect is scoped to the
|
||||||
|
/// body if present.
|
||||||
|
pub fn strike(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||||
|
line_impl("strike", ctx, args, |font| &mut font.strikethrough)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `underline`: Enable underlined text.
|
||||||
|
///
|
||||||
|
/// # Named parameters
|
||||||
|
/// - Color: `color`, of type `color`.
|
||||||
|
/// - Baseline offset: `position`, of type `linear`.
|
||||||
|
/// - Strength: `strength`, of type `linear`.
|
||||||
|
/// - Extent that is applied on either end of the line: `extent`, of type
|
||||||
|
/// `linear`.
|
||||||
|
///
|
||||||
|
/// # Return value
|
||||||
|
/// A template that enables underlined text. The effect is scoped to the body if
|
||||||
|
/// present.
|
||||||
|
pub fn underline(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||||
|
line_impl("underline", ctx, args, |font| &mut font.underline)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `overline`: Add an overline above text.
|
||||||
|
///
|
||||||
|
/// # Named parameters
|
||||||
|
/// - Color: `color`, of type `color`.
|
||||||
|
/// - Baseline offset: `position`, of type `linear`.
|
||||||
|
/// - Strength: `strength`, of type `linear`.
|
||||||
|
/// - Extent that is applied on either end of the line: `extent`, of type
|
||||||
|
/// `linear`.
|
||||||
|
///
|
||||||
|
/// # Return value
|
||||||
|
/// A template that adds an overline above text. The effect is scoped to the
|
||||||
|
/// body if present.
|
||||||
|
pub fn overline(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
||||||
|
line_impl("overline", ctx, args, |font| &mut font.overline)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_impl(
|
||||||
|
name: &str,
|
||||||
|
ctx: &mut EvalContext,
|
||||||
|
args: &mut FuncArgs,
|
||||||
|
substate: impl Fn(&mut FontState) -> &mut Option<LineState> + 'static,
|
||||||
|
) -> Value {
|
||||||
|
let color = args.eat_named(ctx, "color");
|
||||||
|
let position = args.eat_named(ctx, "position");
|
||||||
|
let strength = args.eat_named::<Linear>(ctx, "strength");
|
||||||
|
let extent = args.eat_named(ctx, "extent").unwrap_or_default();
|
||||||
|
let body = args.eat::<TemplateValue>(ctx);
|
||||||
|
|
||||||
|
// Suppress any existing strikethrough if strength is explicitly zero.
|
||||||
|
let state = strength.map_or(true, |s| !s.is_zero()).then(|| LineState {
|
||||||
|
fill: color.map(Fill::Color),
|
||||||
|
strength,
|
||||||
|
position,
|
||||||
|
extent,
|
||||||
|
});
|
||||||
|
|
||||||
|
Value::template(name, move |ctx| {
|
||||||
|
let snapshot = ctx.state.clone();
|
||||||
|
|
||||||
|
*substate(&mut ctx.state.font) = state;
|
||||||
|
|
||||||
|
if let Some(body) = &body {
|
||||||
|
body.exec(ctx);
|
||||||
|
ctx.state = snapshot;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -99,7 +99,7 @@ pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(color) = color {
|
if let Some(color) = color {
|
||||||
ctx.state.font.color = Fill::Color(color);
|
ctx.state.font.fill = Fill::Color(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(FontFamilies(serif)) = &serif {
|
if let Some(FontFamilies(serif)) = &serif {
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
mod align;
|
mod align;
|
||||||
mod basic;
|
mod basic;
|
||||||
|
mod decorations;
|
||||||
mod font;
|
mod font;
|
||||||
mod grid;
|
mod grid;
|
||||||
mod image;
|
mod image;
|
||||||
@ -20,6 +21,7 @@ mod stack;
|
|||||||
pub use self::image::*;
|
pub use self::image::*;
|
||||||
pub use align::*;
|
pub use align::*;
|
||||||
pub use basic::*;
|
pub use basic::*;
|
||||||
|
pub use decorations::*;
|
||||||
pub use font::*;
|
pub use font::*;
|
||||||
pub use grid::*;
|
pub use grid::*;
|
||||||
pub use lang::*;
|
pub use lang::*;
|
||||||
@ -55,6 +57,7 @@ pub fn new() -> Scope {
|
|||||||
std.def_func("lang", lang);
|
std.def_func("lang", lang);
|
||||||
std.def_func("max", max);
|
std.def_func("max", max);
|
||||||
std.def_func("min", min);
|
std.def_func("min", min);
|
||||||
|
std.def_func("overline", overline);
|
||||||
std.def_func("pad", pad);
|
std.def_func("pad", pad);
|
||||||
std.def_func("page", page);
|
std.def_func("page", page);
|
||||||
std.def_func("pagebreak", pagebreak);
|
std.def_func("pagebreak", pagebreak);
|
||||||
@ -64,7 +67,9 @@ pub fn new() -> Scope {
|
|||||||
std.def_func("rgb", rgb);
|
std.def_func("rgb", rgb);
|
||||||
std.def_func("square", square);
|
std.def_func("square", square);
|
||||||
std.def_func("stack", stack);
|
std.def_func("stack", stack);
|
||||||
|
std.def_func("strike", strike);
|
||||||
std.def_func("type", type_);
|
std.def_func("type", type_);
|
||||||
|
std.def_func("underline", underline);
|
||||||
std.def_func("v", v);
|
std.def_func("v", v);
|
||||||
|
|
||||||
// Colors.
|
// Colors.
|
||||||
|
BIN
tests/ref/text/decorations.png
Normal file
BIN
tests/ref/text/decorations.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
19
tests/typ/text/decorations.typ
Normal file
19
tests/typ/text/decorations.typ
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// Test text decorations.
|
||||||
|
|
||||||
|
---
|
||||||
|
#strike[Statements dreamt up by the utterly deranged.]
|
||||||
|
|
||||||
|
Sometimes, we work #strike(extent: 5%, strength: 10pt)[in secret].
|
||||||
|
There might be #strike(extent: 5%, strength: 10pt, color: #abcdef88)[redacted]
|
||||||
|
things.
|
||||||
|
|
||||||
|
---
|
||||||
|
#underline(color: #fc0030)[Critical information is conveyed here.]
|
||||||
|
#underline[
|
||||||
|
Still important, but not #underline(strength: 0pt)[mission ]critical.
|
||||||
|
]
|
||||||
|
|
||||||
|
#font(color: #fc0030, underline[Change with the wind.])
|
||||||
|
|
||||||
|
---
|
||||||
|
#overline(underline[Running amongst the wolves.])
|
@ -8,7 +8,7 @@ use std::rc::Rc;
|
|||||||
use image::{GenericImageView, Rgba};
|
use image::{GenericImageView, Rgba};
|
||||||
use tiny_skia::{
|
use tiny_skia::{
|
||||||
Color, ColorU8, FillRule, FilterQuality, Paint, Pattern, Pixmap, Rect, SpreadMode,
|
Color, ColorU8, FillRule, FilterQuality, Paint, Pattern, Pixmap, Rect, SpreadMode,
|
||||||
Transform,
|
Stroke, Transform,
|
||||||
};
|
};
|
||||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
@ -474,6 +474,17 @@ fn draw_geometry(canvas: &mut Pixmap, ts: Transform, shape: &Shape, fill: Fill)
|
|||||||
let path = convert_typst_path(&geom::Path::ellipse(size));
|
let path = convert_typst_path(&geom::Path::ellipse(size));
|
||||||
canvas.fill_path(&path, &paint, rule, ts, None);
|
canvas.fill_path(&path, &paint, rule, ts, None);
|
||||||
}
|
}
|
||||||
|
Shape::Line(target, thickness) => {
|
||||||
|
let path = {
|
||||||
|
let mut builder = tiny_skia::PathBuilder::new();
|
||||||
|
builder.line_to(target.x.to_pt() as f32, target.y.to_pt() as f32);
|
||||||
|
builder.finish().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stroke = Stroke::default();
|
||||||
|
stroke.width = thickness.to_pt() as f32;
|
||||||
|
canvas.stroke_path(&path, &paint, &stroke, ts, None);
|
||||||
|
}
|
||||||
Shape::Path(ref path) => {
|
Shape::Path(ref path) => {
|
||||||
let path = convert_typst_path(path);
|
let path = convert_typst_path(path);
|
||||||
canvas.fill_path(&path, &paint, rule, ts, None);
|
canvas.fill_path(&path, &paint, rule, ts, None);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user