use super::*;
/// How much the accent can be shorter than the base.
const ACCENT_SHORT_FALL: Em = Em::new(0.5);
/// Attach 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` | `
|
/// | Acute | `acute` | `´` |
/// | Circumflex | `hat` | `^` |
/// | Tilde | `tilde` | `~` |
/// | Macron | `macron` | `¯` |
/// | Breve | `breve` | `˘` |
/// | Dot | `dot` | `.` |
/// | Double dot | `dot.double` | `¨` |
/// | Triple dot | `dot.triple` | ⃛
|
/// | Quadruple dot | `dot.quad` | ⃜
|
/// | 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::() {
Some(elem) => Value::Str(elem.text().into()).cast()?,
None => Err("expected text")?,
},
}