2025-07-23 17:32:23 +10:00

107 lines
3.9 KiB
Rust

use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Em, Frame, Point, Size};
use typst_library::math::AccentElem;
use super::{
FrameFragment, MathContext, MathFragment, style_cramped, style_dtls, style_flac,
};
/// How much the accent can be shorter than the base.
const ACCENT_SHORT_FALL: Em = Em::new(0.5);
/// Lays out an [`AccentElem`].
#[typst_macros::time(name = "math.accent", span = elem.span())]
pub fn layout_accent(
elem: &Packed<AccentElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let accent = elem.accent;
let top_accent = !accent.is_bottom();
// Try to replace the base glyph with its dotless variant.
let dtls = style_dtls();
let base_styles =
if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles };
let cramped = style_cramped();
let base_styles = base_styles.chain(&cramped);
let base = ctx.layout_into_fragment(&elem.base, base_styles)?;
let (font, size) = base.font(ctx, base_styles, elem.base.span())?;
// Preserve class to preserve automatic spacing.
let base_class = base.class();
let base_attach = base.accent_attach();
// Try to replace the accent glyph with its flattened variant.
let flattened_base_height = value!(font, flattened_accent_base_height).at(size);
let flac = style_flac();
let accent_styles = if top_accent && base.ascent() > flattened_base_height {
styles.chain(&flac)
} else {
styles
};
let mut accent = ctx.layout_into_fragment(
&SymbolElem::packed(accent.0).spanned(elem.span()),
accent_styles,
)?;
// Forcing the accent to be at least as large as the base makes it too wide
// in many cases.
let width = elem.size.resolve(styles).relative_to(base.width());
let short_fall = ACCENT_SHORT_FALL.at(size);
accent.stretch_horizontal(ctx, width - short_fall);
let accent_attach = accent.accent_attach().0;
let accent = accent.into_frame();
let (gap, accent_pos, base_pos) = if top_accent {
// 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 = value!(font, accent_base_height).at(size);
let gap = -accent.descent() - base.ascent().min(accent_base_height);
let accent_pos = Point::with_x(base_attach.0 - accent_attach);
let base_pos = Point::with_y(accent.height() + gap);
(gap, accent_pos, base_pos)
} else {
let gap = -accent.ascent();
let accent_pos = Point::new(base_attach.1 - accent_attach, base.height() + gap);
let base_pos = Point::zero();
(gap, accent_pos, base_pos)
};
let size = Size::new(base.width(), accent.height() + gap + base.height());
let baseline = base_pos.y + base.ascent();
let base_italics_correction = base.italics_correction();
let base_text_like = base.is_text_like();
let base_ascent = match &base {
MathFragment::Frame(frame) => frame.base_ascent,
_ => base.ascent(),
};
let base_descent = match &base {
MathFragment::Frame(frame) => frame.base_descent,
_ => base.descent(),
};
let mut frame = Frame::soft(size);
frame.set_baseline(baseline);
frame.push_frame(accent_pos, accent);
frame.push_frame(base_pos, base.into_frame());
ctx.push(
FrameFragment::new(styles, frame)
.with_class(base_class)
.with_base_ascent(base_ascent)
.with_base_descent(base_descent)
.with_italics_correction(base_italics_correction)
.with_accent_attach(base_attach)
.with_text_like(base_text_like),
);
Ok(())
}