summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/math/accent.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/math/accent.rs')
-rw-r--r--crates/typst-library/src/math/accent.rs139
1 files changed, 139 insertions, 0 deletions
diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs
new file mode 100644
index 00000000..d1bee198
--- /dev/null
+++ b/crates/typst-library/src/math/accent.rs
@@ -0,0 +1,139 @@
+use super::*;
+
+/// How much the accent can be shorter than the base.
+const ACCENT_SHORT_FALL: Em = Em::new(0.5);
+
+/// Attaches an accent to a base.
+///
+/// ## Example { #example }
+/// ```example
+/// $grave(a) = accent(a, `)$ \
+/// $arrow(a) = accent(a, arrow)$ \
+/// $tilde(a) = accent(a, \u{0303})$
+/// ```
+///
+/// Display: Accent
+/// Category: math
+#[element(LayoutMath)]
+pub struct AccentElem {
+ /// The base to which the accent is applied.
+ /// May consist of multiple letters.
+ ///
+ /// ```example
+ /// $arrow(A B C)$
+ /// ```
+ #[required]
+ pub base: Content,
+
+ /// The accent to apply to the base.
+ ///
+ /// Supported accents include:
+ ///
+ /// | Accent | Name | Codepoint |
+ /// | ------------- | --------------- | --------- |
+ /// | Grave | `grave` | <code>&DiacriticalGrave;</code> |
+ /// | Acute | `acute` | `´` |
+ /// | Circumflex | `hat` | `^` |
+ /// | Tilde | `tilde` | `~` |
+ /// | Macron | `macron` | `¯` |
+ /// | Breve | `breve` | `˘` |
+ /// | Dot | `dot` | `.` |
+ /// | Double dot | `dot.double` | `¨` |
+ /// | Triple dot | `dot.triple` | <code>&tdot;</code> |
+ /// | Quadruple dot | `dot.quad` | <code>&DotDot;</code> |
+ /// | Diaeresis | `diaer` | `¨` |
+ /// | Circle | `circle` | `∘` |
+ /// | Double acute | `acute.double` | `˝` |
+ /// | Caron | `caron` | `ˇ` |
+ /// | Right arrow | `arrow`, `->` | `→` |
+ /// | Left arrow | `arrow.l`, `<-` | `←` |
+ #[required]
+ pub accent: Accent,
+}
+
+impl LayoutMath for AccentElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ ctx.style(ctx.style.with_cramped(true));
+ let base = ctx.layout_fragment(&self.base())?;
+ ctx.unstyle();
+
+ // Preserve class to preserve automatic spacing.
+ let base_class = base.class().unwrap_or(MathClass::Normal);
+ let base_attach = match &base {
+ MathFragment::Glyph(base) => {
+ attachment(ctx, base.id, base.italics_correction)
+ }
+ _ => (base.width() + base.italics_correction()) / 2.0,
+ };
+
+ // Forcing the accent to be at least as large as the base makes it too
+ // wide in many case.
+ let Accent(c) = self.accent();
+ let glyph = GlyphFragment::new(ctx, c, self.span());
+ let short_fall = ACCENT_SHORT_FALL.scaled(ctx);
+ let variant = glyph.stretch_horizontal(ctx, base.width(), short_fall);
+ let accent = variant.frame;
+ let accent_attach = match variant.id {
+ Some(id) => attachment(ctx, id, variant.italics_correction),
+ None => accent.width() / 2.0,
+ };
+
+ // Descent is negative because the accent's ink bottom is above the
+ // baseline. Therefore, the default gap is the accent's negated descent
+ // minus the accent base height. Only if the base is very small, we need
+ // a larger gap so that the accent doesn't move too low.
+ let accent_base_height = scaled!(ctx, accent_base_height);
+ let gap = -accent.descent() - base.height().min(accent_base_height);
+ let size = Size::new(base.width(), accent.height() + gap + base.height());
+ let accent_pos = Point::with_x(base_attach - accent_attach);
+ let base_pos = Point::with_y(accent.height() + gap);
+ let base_ascent = base.ascent();
+ let baseline = base_pos.y + base.ascent();
+
+ let mut frame = Frame::new(size);
+ frame.set_baseline(baseline);
+ frame.push_frame(accent_pos, accent);
+ frame.push_frame(base_pos, base.into_frame());
+ ctx.push(
+ FrameFragment::new(ctx, frame)
+ .with_class(base_class)
+ .with_base_ascent(base_ascent),
+ );
+
+ Ok(())
+ }
+}
+
+/// The horizontal attachment position for the given glyph.
+fn attachment(ctx: &MathContext, id: GlyphId, italics_correction: Abs) -> Abs {
+ ctx.table
+ .glyph_info
+ .and_then(|info| info.top_accent_attachments)
+ .and_then(|attachments| attachments.get(id))
+ .map(|record| record.value.scaled(ctx))
+ .unwrap_or_else(|| {
+ let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default();
+ (advance.scaled(ctx) + italics_correction) / 2.0
+ })
+}
+
+/// An accent character.
+pub struct Accent(char);
+
+impl Accent {
+ /// Normalize a character into an accent.
+ pub fn new(c: char) -> Self {
+ Self(Symbol::combining_accent(c).unwrap_or(c))
+ }
+}
+
+cast! {
+ Accent,
+ self => self.0.into_value(),
+ v: char => Self::new(v),
+ v: Content => match v.to::<TextElem>() {
+ Some(elem) => Value::Str(elem.text().into()).cast()?,
+ None => bail!("expected text"),
+ },
+}