summaryrefslogtreecommitdiff
path: root/crates/typst-layout/src/math/accent.rs
blob: e7f051aced199221bf61a0548219a26c1f252fcc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Em, Frame, Point, Size};
use typst_library::math::AccentElem;

use super::{
    style_cramped, style_dtls, style_flac, FrameFragment, GlyphFragment, MathContext,
    MathFragment,
};

/// 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 = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?;

    // 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 = scaled!(ctx, styles, flattened_accent_base_height);
    let flac = style_flac();
    let accent_styles = if top_accent && base.ascent() > flattened_base_height {
        styles.chain(&flac)
    } else {
        styles
    };

    let mut glyph =
        GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?;

    // 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(glyph.item.size);
    glyph.stretch_horizontal(ctx, width - short_fall);
    let accent_attach = glyph.accent_attach.0;
    let accent = glyph.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 = scaled!(ctx, styles, accent_base_height);
        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(())
}