use unicode_math_class::MathClass;
use crate::diag::SourceResult;
use crate::foundations::{elem, Content, Packed, Smart, StyleChain};
use crate::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size};
use crate::math::{
stretch_fragment, style_for_subscript, style_for_superscript, EquationElem,
FrameFragment, LayoutMath, MathContext, MathFragment, MathSize, Scaled, StretchElem,
};
use crate::text::TextElem;
use crate::utils::OptionExt;
macro_rules! measure {
($e: ident, $attr: ident) => {
$e.as_ref().map(|e| e.$attr()).unwrap_or_default()
};
}
#[elem(LayoutMath)]
pub struct AttachElem {
#[required]
pub base: Content,
pub t: Option<Content>,
pub b: Option<Content>,
pub tl: Option<Content>,
pub bl: Option<Content>,
pub tr: Option<Content>,
pub br: Option<Content>,
}
impl LayoutMath for Packed<AttachElem> {
#[typst_macros::time(name = "math.attach", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
let new_elem = merge_base(self);
let elem = new_elem.as_ref().unwrap_or(self);
let stretch = stretch_size(styles, elem);
let mut base = ctx.layout_into_fragment(elem.base(), styles)?;
let sup_style = style_for_superscript(styles);
let sup_style_chain = styles.chain(&sup_style);
let tl = elem.tl(sup_style_chain);
let tr = elem.tr(sup_style_chain);
let primed = tr.as_ref().is_some_and(|content| content.is::<PrimesElem>());
let t = elem.t(sup_style_chain);
let sub_style = style_for_subscript(styles);
let sub_style_chain = styles.chain(&sub_style);
let bl = elem.bl(sub_style_chain);
let br = elem.br(sub_style_chain);
let b = elem.b(sub_style_chain);
let limits = base.limits().active(styles);
let (t, tr) = match (t, tr) {
(Some(t), Some(tr)) if primed && !limits => (None, Some(tr + t)),
(Some(t), None) if !limits => (None, Some(t)),
(t, tr) => (t, tr),
};
let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) };
macro_rules! layout {
($content:ident, $style_chain:ident) => {
$content
.map(|elem| ctx.layout_into_fragment(&elem, $style_chain))
.transpose()
};
}
let t = layout!(t, sup_style_chain)?;
let b = layout!(b, sub_style_chain)?;
if let Some(stretch) = stretch {
let relative_to_width = measure!(t, width).max(measure!(b, width));
stretch_fragment(
ctx,
styles,
&mut base,
Some(Axis::X),
Some(relative_to_width),
stretch,
Abs::zero(),
);
}
let fragments = [
layout!(tl, sup_style_chain)?,
t,
layout!(tr, sup_style_chain)?,
layout!(bl, sub_style_chain)?,
b,
layout!(br, sub_style_chain)?,
];
layout_attachments(ctx, styles, base, fragments)
}
}
#[elem(LayoutMath)]
pub struct PrimesElem {
#[required]
pub count: usize,
}
impl LayoutMath for Packed<PrimesElem> {
#[typst_macros::time(name = "math.primes", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
match *self.count() {
count @ 1..=4 => {
let c = match count {
1 => '′',
2 => '″',
3 => '‴',
4 => '⁗',
_ => unreachable!(),
};
let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?;
ctx.push(f);
}
count => {
let prime = ctx
.layout_into_fragment(&TextElem::packed('′'), styles)?
.into_frame();
let width = prime.width() * (count + 1) as f64 / 2.0;
let mut frame = Frame::soft(Size::new(width, prime.height()));
frame.set_baseline(prime.ascent());
for i in 0..count {
frame.push_frame(
Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()),
prime.clone(),
)
}
ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true));
}
}
Ok(())
}
}
#[elem(LayoutMath)]
pub struct ScriptsElem {
#[required]
pub body: Content,
}
impl LayoutMath for Packed<ScriptsElem> {
#[typst_macros::time(name = "math.scripts", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(self.body(), styles)?;
fragment.set_limits(Limits::Never);
ctx.push(fragment);
Ok(())
}
}
#[elem(LayoutMath)]
pub struct LimitsElem {
#[required]
pub body: Content,
#[default(true)]
pub inline: bool,
}
impl LayoutMath for Packed<LimitsElem> {
#[typst_macros::time(name = "math.limits", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
let limits = if self.inline(styles) { Limits::Always } else { Limits::Display };
let mut fragment = ctx.layout_into_fragment(self.body(), styles)?;
fragment.set_limits(limits);
ctx.push(fragment);
Ok(())
}
}
#[derive(Debug, Copy, Clone)]
pub enum Limits {
Never,
Display,
Always,
}
impl Limits {
pub fn for_char(c: char) -> Self {
match unicode_math_class::class(c) {
Some(MathClass::Large) => {
if is_integral_char(c) {
Limits::Never
} else {
Limits::Display
}
}
Some(MathClass::Relation) => Limits::Always,
_ => Limits::Never,
}
}
pub fn for_class(class: MathClass) -> Self {
match class {
MathClass::Large => Self::Display,
MathClass::Relation => Self::Always,
_ => Self::Never,
}
}
pub fn active(&self, styles: StyleChain) -> bool {
match self {
Self::Always => true,
Self::Display => EquationElem::size_in(styles) == MathSize::Display,
Self::Never => false,
}
}
}
fn merge_base(elem: &Packed<AttachElem>) -> Option<Packed<AttachElem>> {
let mut base = elem.base();
if let Some(equation) = base.to_packed::<EquationElem>() {
base = equation.body();
}
if let Some(base) = base.to_packed::<AttachElem>() {
let mut elem = elem.clone();
let mut base = base.clone();
macro_rules! merge {
($content:ident) => {
if base.$content.is_none() && elem.$content.is_some() {
base.$content = elem.$content.clone();
elem.$content = None;
}
};
}
merge!(t);
merge!(b);
merge!(tl);
merge!(tr);
merge!(bl);
merge!(br);
elem.base = base.pack();
return Some(elem);
}
None
}
fn stretch_size(
styles: StyleChain,
elem: &Packed<AttachElem>,
) -> Option<Smart<Rel<Length>>> {
let mut base = elem.base();
if let Some(equation) = base.to_packed::<EquationElem>() {
base = equation.body();
}
base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles))
}
fn layout_attachments(
ctx: &mut MathContext,
styles: StyleChain,
base: MathFragment,
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
) -> SourceResult<()> {
let base_class = base.class();
let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
(Abs::zero(), Abs::zero())
} else {
compute_script_shifts(ctx, styles, &base, [&tl, &tr, &bl, &br])
};
let (t_shift, b_shift) =
compute_limit_shifts(ctx, styles, &base, [t.as_ref(), b.as_ref()]);
let ascent = base
.ascent()
.max(tx_shift + measure!(tr, ascent))
.max(tx_shift + measure!(tl, ascent))
.max(t_shift + measure!(t, ascent));
let descent = base
.descent()
.max(bx_shift + measure!(br, descent))
.max(bx_shift + measure!(bl, descent))
.max(b_shift + measure!(b, descent));
let height = ascent + descent;
let base_y = ascent - base.ascent();
let tx_y = |tx: &MathFragment| ascent - tx_shift - tx.ascent();
let bx_y = |bx: &MathFragment| ascent + bx_shift - bx.ascent();
let t_y = |t: &MathFragment| ascent - t_shift - t.ascent();
let b_y = |b: &MathFragment| ascent + b_shift - b.ascent();
let ((t_pre_width, t_post_width), (b_pre_width, b_post_width)) =
compute_limit_widths(&base, [t.as_ref(), b.as_ref()]);
let space_after_script = scaled!(ctx, styles, space_after_script);
let (tl_pre_width, bl_pre_width) = compute_pre_script_widths(
ctx,
&base,
[tl.as_ref(), bl.as_ref()],
(tx_shift, bx_shift),
space_after_script,
);
let ((tr_post_width, tr_kern), (br_post_width, br_kern)) = compute_post_script_widths(
ctx,
&base,
[tr.as_ref(), br.as_ref()],
(tx_shift, bx_shift),
space_after_script,
);
let pre_width = t_pre_width.max(b_pre_width).max(tl_pre_width).max(bl_pre_width);
let base_width = base.width();
let post_width = t_post_width.max(b_post_width).max(tr_post_width).max(br_post_width);
let width = pre_width + base_width + post_width;
let base_x = pre_width;
let tl_x = pre_width - tl_pre_width + space_after_script;
let bl_x = pre_width - bl_pre_width + space_after_script;
let tr_x = pre_width + base_width + tr_kern;
let br_x = pre_width + base_width + br_kern;
let t_x = pre_width - t_pre_width;
let b_x = pre_width - b_pre_width;
let mut frame = Frame::soft(Size::new(width, height));
frame.set_baseline(ascent);
frame.push_frame(Point::new(base_x, base_y), base.into_frame());
macro_rules! layout {
($e: ident, $x: ident, $y: ident) => {
if let Some($e) = $e {
frame.push_frame(Point::new($x, $y(&$e)), $e.into_frame());
}
};
}
layout!(tl, tl_x, tx_y); layout!(bl, bl_x, bx_y); layout!(tr, tr_x, tx_y); layout!(br, br_x, bx_y); layout!(t, t_x, t_y); layout!(b, b_x, b_y);
ctx.push(FrameFragment::new(ctx, styles, frame).with_class(base_class));
Ok(())
}
fn compute_post_script_widths(
ctx: &MathContext,
base: &MathFragment,
[tr, br]: [Option<&MathFragment>; 2],
(tr_shift, br_shift): (Abs, Abs),
space_after_post_script: Abs,
) -> ((Abs, Abs), (Abs, Abs)) {
let tr_values = tr.map_or_default(|tr| {
let kern = math_kern(ctx, base, tr, tr_shift, Corner::TopRight);
(space_after_post_script + tr.width() + kern, kern)
});
let br_values = br.map_or_default(|br| {
let kern = math_kern(ctx, base, br, br_shift, Corner::BottomRight)
- base.italics_correction();
(space_after_post_script + br.width() + kern, kern)
});
(tr_values, br_values)
}
fn compute_pre_script_widths(
ctx: &MathContext,
base: &MathFragment,
[tl, bl]: [Option<&MathFragment>; 2],
(tl_shift, bl_shift): (Abs, Abs),
space_before_pre_script: Abs,
) -> (Abs, Abs) {
let tl_pre_width = tl.map_or_default(|tl| {
let kern = math_kern(ctx, base, tl, tl_shift, Corner::TopLeft);
space_before_pre_script + tl.width() + kern
});
let bl_pre_width = bl.map_or_default(|bl| {
let kern = math_kern(ctx, base, bl, bl_shift, Corner::BottomLeft);
space_before_pre_script + bl.width() + kern
});
(tl_pre_width, bl_pre_width)
}
fn compute_limit_widths(
base: &MathFragment,
[t, b]: [Option<&MathFragment>; 2],
) -> ((Abs, Abs), (Abs, Abs)) {
let delta = base.italics_correction() / 2.0;
let t_widths = t.map_or_default(|t| {
let half = (t.width() - base.width()) / 2.0;
(half - delta, half + delta)
});
let b_widths = b.map_or_default(|b| {
let half = (b.width() - base.width()) / 2.0;
(half + delta, half - delta)
});
(t_widths, b_widths)
}
fn compute_limit_shifts(
ctx: &MathContext,
styles: StyleChain,
base: &MathFragment,
[t, b]: [Option<&MathFragment>; 2],
) -> (Abs, Abs) {
let t_shift = t.map_or_default(|t| {
let upper_gap_min = scaled!(ctx, styles, upper_limit_gap_min);
let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min);
base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
});
let b_shift = b.map_or_default(|b| {
let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min);
let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min);
base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
});
(t_shift, b_shift)
}
fn compute_script_shifts(
ctx: &MathContext,
styles: StyleChain,
base: &MathFragment,
[tl, tr, bl, br]: [&Option<MathFragment>; 4],
) -> (Abs, Abs) {
let sup_shift_up = if EquationElem::cramped_in(styles) {
scaled!(ctx, styles, superscript_shift_up_cramped)
} else {
scaled!(ctx, styles, superscript_shift_up)
};
let sup_bottom_min = scaled!(ctx, styles, superscript_bottom_min);
let sup_bottom_max_with_sub =
scaled!(ctx, styles, superscript_bottom_max_with_subscript);
let sup_drop_max = scaled!(ctx, styles, superscript_baseline_drop_max);
let gap_min = scaled!(ctx, styles, sub_superscript_gap_min);
let sub_shift_down = scaled!(ctx, styles, subscript_shift_down);
let sub_top_max = scaled!(ctx, styles, subscript_top_max);
let sub_drop_min = scaled!(ctx, styles, subscript_baseline_drop_min);
let mut shift_up = Abs::zero();
let mut shift_down = Abs::zero();
let is_text_like = base.is_text_like();
if tl.is_some() || tr.is_some() {
let ascent = match &base {
MathFragment::Frame(frame) => frame.base_ascent,
_ => base.ascent(),
};
shift_up = shift_up
.max(sup_shift_up)
.max(if is_text_like { Abs::zero() } else { ascent - sup_drop_max })
.max(sup_bottom_min + measure!(tl, descent))
.max(sup_bottom_min + measure!(tr, descent));
}
if bl.is_some() || br.is_some() {
shift_down = shift_down
.max(sub_shift_down)
.max(if is_text_like { Abs::zero() } else { base.descent() + sub_drop_min })
.max(measure!(bl, ascent) - sub_top_max)
.max(measure!(br, ascent) - sub_top_max);
}
for (sup, sub) in [(tl, bl), (tr, br)] {
if let (Some(sup), Some(sub)) = (&sup, &sub) {
let sup_bottom = shift_up - sup.descent();
let sub_top = sub.ascent() - shift_down;
let gap = sup_bottom - sub_top;
if gap >= gap_min {
continue;
}
let increase = gap_min - gap;
let sup_only =
(sup_bottom_max_with_sub - sup_bottom).clamp(Abs::zero(), increase);
let rest = (increase - sup_only) / 2.0;
shift_up += sup_only + rest;
shift_down += rest;
}
}
(shift_up, shift_down)
}
fn math_kern(
ctx: &MathContext,
base: &MathFragment,
script: &MathFragment,
shift: Abs,
pos: Corner,
) -> Abs {
let (corr_height_top, corr_height_bot) = match pos {
Corner::TopLeft | Corner::TopRight => {
(base.ascent() - shift, shift - script.descent())
}
Corner::BottomLeft | Corner::BottomRight => {
(script.ascent() - shift, shift - base.descent())
}
};
let summed_kern = |height| {
let base_kern = base.kern_at_height(ctx, pos, height);
let attach_kern = script.kern_at_height(ctx, pos.inv(), height);
base_kern + attach_kern
};
summed_kern(corr_height_top).max(summed_kern(corr_height_bot))
}
fn is_integral_char(c: char) -> bool {
('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
}