1#![allow(unstable_name_collisions)]
3
4#[allow(unused_imports)]
6use typst_utils::OptionExt;
7
8use typst_library::diag::SourceResult;
9use typst_library::foundations::{Packed, StyleChain, SymbolElem};
10use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
11use typst_library::math::{
12 AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
13};
14use typst_library::text::Font;
15
16use super::{
17 FrameFragment, Limits, MathContext, MathFragment, stretch_fragment,
18 style_for_subscript, style_for_superscript,
19};
20
21macro_rules! measure {
22 ($e: ident, $attr: ident) => {
23 $e.as_ref().map(|e| e.$attr()).unwrap_or_default()
24 };
25}
26
27#[typst_macros::time(name = "math.attach", span = elem.span())]
29pub fn layout_attach(
30 elem: &Packed<AttachElem>,
31 ctx: &mut MathContext,
32 styles: StyleChain,
33) -> SourceResult<()> {
34 let merged = elem.merge_base();
35 let elem = merged.as_ref().unwrap_or(elem);
36 let stretch = stretch_size(styles, elem);
37
38 let mut base = ctx.layout_into_fragment(&elem.base, styles)?;
39 let sup_style = style_for_superscript(styles);
40 let sup_style_chain = styles.chain(&sup_style);
41 let tl = elem.tl.get_cloned(sup_style_chain);
42 let tr = elem.tr.get_cloned(sup_style_chain);
43 let primed = tr.as_ref().is_some_and(|content| content.is::<PrimesElem>());
44 let t = elem.t.get_cloned(sup_style_chain);
45
46 let sub_style = style_for_subscript(styles);
47 let sub_style_chain = styles.chain(&sub_style);
48 let bl = elem.bl.get_cloned(sub_style_chain);
49 let br = elem.br.get_cloned(sub_style_chain);
50 let b = elem.b.get_cloned(sub_style_chain);
51
52 let limits = base.limits().active(styles);
53 let (t, tr) = match (t, tr) {
54 (Some(t), Some(tr)) if primed && !limits => (None, Some(tr + t)),
55 (Some(t), None) if !limits => (None, Some(t)),
56 (t, tr) => (t, tr),
57 };
58 let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) };
59
60 macro_rules! layout {
61 ($content:ident, $style_chain:ident) => {
62 $content
63 .map(|elem| ctx.layout_into_fragment(&elem, $style_chain))
64 .transpose()
65 };
66 }
67
68 let t = layout!(t, sup_style_chain)?;
71 let b = layout!(b, sub_style_chain)?;
72 if let Some(stretch) = stretch {
73 let relative_to_width = measure!(t, width).max(measure!(b, width));
74 stretch_fragment(
75 ctx,
76 &mut base,
77 Some(Axis::X),
78 Some(relative_to_width),
79 stretch,
80 Abs::zero(),
81 );
82 }
83
84 let fragments = [
85 layout!(tl, sup_style_chain)?,
86 t,
87 layout!(tr, sup_style_chain)?,
88 layout!(bl, sub_style_chain)?,
89 b,
90 layout!(br, sub_style_chain)?,
91 ];
92
93 layout_attachments(ctx, styles, base, fragments)
94}
95
96#[typst_macros::time(name = "math.primes", span = elem.span())]
98pub fn layout_primes(
99 elem: &Packed<PrimesElem>,
100 ctx: &mut MathContext,
101 styles: StyleChain,
102) -> SourceResult<()> {
103 match elem.count {
104 count @ 1..=4 => {
105 let c = match count {
106 1 => '′',
107 2 => '″',
108 3 => '‴',
109 4 => '⁗',
110 _ => unreachable!(),
111 };
112 let f = ctx.layout_into_fragment(
113 &SymbolElem::packed(c).spanned(elem.span()),
114 styles,
115 )?;
116 ctx.push(f);
117 }
118 count => {
119 let prime = ctx
121 .layout_into_fragment(
122 &SymbolElem::packed('′').spanned(elem.span()),
123 styles,
124 )?
125 .into_frame();
126 let width = prime.width() * (count + 1) as f64 / 2.0;
127 let mut frame = Frame::soft(Size::new(width, prime.height()));
128 frame.set_baseline(prime.ascent());
129
130 for i in 0..count {
131 frame.push_frame(
132 Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()),
133 prime.clone(),
134 )
135 }
136 ctx.push(FrameFragment::new(styles, frame).with_text_like(true));
137 }
138 }
139 Ok(())
140}
141
142#[typst_macros::time(name = "math.scripts", span = elem.span())]
144pub fn layout_scripts(
145 elem: &Packed<ScriptsElem>,
146 ctx: &mut MathContext,
147 styles: StyleChain,
148) -> SourceResult<()> {
149 let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
150 fragment.set_limits(Limits::Never);
151 ctx.push(fragment);
152 Ok(())
153}
154
155#[typst_macros::time(name = "math.limits", span = elem.span())]
157pub fn layout_limits(
158 elem: &Packed<LimitsElem>,
159 ctx: &mut MathContext,
160 styles: StyleChain,
161) -> SourceResult<()> {
162 let limits = if elem.inline.get(styles) { Limits::Always } else { Limits::Display };
163 let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
164 fragment.set_limits(limits);
165 ctx.push(fragment);
166 Ok(())
167}
168
169fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs>> {
171 let mut base = &elem.base;
173 while let Some(equation) = base.to_packed::<EquationElem>() {
174 base = &equation.body;
175 }
176
177 base.to_packed::<StretchElem>()
178 .map(|stretch| stretch.size.resolve(styles))
179}
180
181fn layout_attachments(
183 ctx: &mut MathContext,
184 styles: StyleChain,
185 base: MathFragment,
186 [tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
187) -> SourceResult<()> {
188 let class = base.class();
189 let (font, size) = base.font(ctx, styles);
190 let cramped = styles.get(EquationElem::cramped);
191
192 let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
195 (Abs::zero(), Abs::zero())
196 } else {
197 compute_script_shifts(&font, size, cramped, &base, [&tl, &tr, &bl, &br])
198 };
199
200 let (t_shift, b_shift) =
203 compute_limit_shifts(&font, size, &base, [t.as_ref(), b.as_ref()]);
204
205 let ascent = base
207 .ascent()
208 .max(tx_shift + measure!(tr, ascent))
209 .max(tx_shift + measure!(tl, ascent))
210 .max(t_shift + measure!(t, ascent));
211 let descent = base
212 .descent()
213 .max(bx_shift + measure!(br, descent))
214 .max(bx_shift + measure!(bl, descent))
215 .max(b_shift + measure!(b, descent));
216 let height = ascent + descent;
217
218 let base_y = ascent - base.ascent();
220 let tx_y = |tx: &MathFragment| ascent - tx_shift - tx.ascent();
221 let bx_y = |bx: &MathFragment| ascent + bx_shift - bx.ascent();
222 let t_y = |t: &MathFragment| ascent - t_shift - t.ascent();
223 let b_y = |b: &MathFragment| ascent + b_shift - b.ascent();
224
225 let ((t_pre_width, t_post_width), (b_pre_width, b_post_width)) =
228 compute_limit_widths(&base, [t.as_ref(), b.as_ref()]);
229
230 let space_after_script = font.math().space_after_script.at(size);
234
235 let (tl_pre_width, bl_pre_width) = compute_pre_script_widths(
238 &base,
239 [tl.as_ref(), bl.as_ref()],
240 (tx_shift, bx_shift),
241 space_after_script,
242 );
243
244 let ((tr_post_width, tr_kern), (br_post_width, br_kern)) = compute_post_script_widths(
248 &base,
249 [tr.as_ref(), br.as_ref()],
250 (tx_shift, bx_shift),
251 space_after_script,
252 );
253
254 let pre_width = t_pre_width.max(b_pre_width).max(tl_pre_width).max(bl_pre_width);
256 let base_width = base.width();
257 let post_width = t_post_width.max(b_post_width).max(tr_post_width).max(br_post_width);
258 let width = pre_width + base_width + post_width;
259
260 let base_x = pre_width;
262 let tl_x = pre_width - tl_pre_width + space_after_script;
263 let bl_x = pre_width - bl_pre_width + space_after_script;
264 let tr_x = pre_width + base_width + tr_kern;
265 let br_x = pre_width + base_width + br_kern;
266 let t_x = pre_width - t_pre_width;
267 let b_x = pre_width - b_pre_width;
268
269 let mut frame = Frame::soft(Size::new(width, height));
271 frame.set_baseline(ascent);
272 frame.push_frame(Point::new(base_x, base_y), base.into_frame());
273
274 macro_rules! layout {
275 ($e: ident, $x: ident, $y: ident) => {
276 if let Some($e) = $e {
277 frame.push_frame(Point::new($x, $y(&$e)), $e.into_frame());
278 }
279 };
280 }
281
282 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(styles, frame).with_class(class));
291
292 Ok(())
293}
294
295fn compute_post_script_widths(
303 base: &MathFragment,
304 [tr, br]: [Option<&MathFragment>; 2],
305 (tr_shift, br_shift): (Abs, Abs),
306 space_after_post_script: Abs,
307) -> ((Abs, Abs), (Abs, Abs)) {
308 let tr_values = tr.map_or_default(|tr| {
309 let kern = math_kern(base, tr, tr_shift, Corner::TopRight);
310 (space_after_post_script + tr.width() + kern, kern)
311 });
312
313 let br_values = br.map_or_default(|br| {
317 let kern = math_kern(base, br, br_shift, Corner::BottomRight)
318 - base.italics_correction();
319 (space_after_post_script + br.width() + kern, kern)
320 });
321
322 (tr_values, br_values)
323}
324
325fn compute_pre_script_widths(
332 base: &MathFragment,
333 [tl, bl]: [Option<&MathFragment>; 2],
334 (tl_shift, bl_shift): (Abs, Abs),
335 space_before_pre_script: Abs,
336) -> (Abs, Abs) {
337 let tl_pre_width = tl.map_or_default(|tl| {
338 let kern = math_kern(base, tl, tl_shift, Corner::TopLeft);
339 space_before_pre_script + tl.width() + kern
340 });
341
342 let bl_pre_width = bl.map_or_default(|bl| {
343 let kern = math_kern(base, bl, bl_shift, Corner::BottomLeft);
344 space_before_pre_script + bl.width() + kern
345 });
346
347 (tl_pre_width, bl_pre_width)
348}
349
350fn compute_limit_widths(
358 base: &MathFragment,
359 [t, b]: [Option<&MathFragment>; 2],
360) -> ((Abs, Abs), (Abs, Abs)) {
361 let delta = base.italics_correction() / 2.0;
364
365 let t_widths = t.map_or_default(|t| {
366 let half = (t.width() - base.width()) / 2.0;
367 (half - delta, half + delta)
368 });
369
370 let b_widths = b.map_or_default(|b| {
371 let half = (b.width() - base.width()) / 2.0;
372 (half + delta, half - delta)
373 });
374
375 (t_widths, b_widths)
376}
377
378fn compute_limit_shifts(
382 font: &Font,
383 font_size: Abs,
384 base: &MathFragment,
385 [t, b]: [Option<&MathFragment>; 2],
386) -> (Abs, Abs) {
387 let t_shift = t.map_or_default(|t| {
392 let upper_gap_min = font.math().upper_limit_gap_min.at(font_size);
393 let upper_rise_min = font.math().upper_limit_baseline_rise_min.at(font_size);
394 base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
395 });
396
397 let b_shift = b.map_or_default(|b| {
398 let lower_gap_min = font.math().lower_limit_gap_min.at(font_size);
399 let lower_drop_min = font.math().lower_limit_baseline_drop_min.at(font_size);
400 base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
401 });
402
403 (t_shift, b_shift)
404}
405
406fn compute_script_shifts(
410 font: &Font,
411 font_size: Abs,
412 cramped: bool,
413 base: &MathFragment,
414 [tl, tr, bl, br]: [&Option<MathFragment>; 4],
415) -> (Abs, Abs) {
416 let sup_shift_up = (if cramped {
417 font.math().superscript_shift_up_cramped
418 } else {
419 font.math().superscript_shift_up
420 })
421 .at(font_size);
422
423 let sup_bottom_min = font.math().superscript_bottom_min.at(font_size);
424 let sup_bottom_max_with_sub =
425 font.math().superscript_bottom_max_with_subscript.at(font_size);
426 let sup_drop_max = font.math().superscript_baseline_drop_max.at(font_size);
427 let gap_min = font.math().sub_superscript_gap_min.at(font_size);
428 let sub_shift_down = font.math().subscript_shift_down.at(font_size);
429 let sub_top_max = font.math().subscript_top_max.at(font_size);
430 let sub_drop_min = font.math().subscript_baseline_drop_min.at(font_size);
431
432 let mut shift_up = Abs::zero();
433 let mut shift_down = Abs::zero();
434 let is_text_like = base.is_text_like();
435
436 if tl.is_some() || tr.is_some() {
437 let ascent = match &base {
438 MathFragment::Frame(frame) => frame.base_ascent,
439 _ => base.ascent(),
440 };
441 shift_up = shift_up
442 .max(sup_shift_up)
443 .max(if is_text_like { Abs::zero() } else { ascent - sup_drop_max })
444 .max(sup_bottom_min + measure!(tl, descent))
445 .max(sup_bottom_min + measure!(tr, descent));
446 }
447
448 if bl.is_some() || br.is_some() {
449 let descent = match &base {
450 MathFragment::Frame(frame) => frame.base_descent,
451 _ => base.descent(),
452 };
453 shift_down = shift_down
454 .max(sub_shift_down)
455 .max(if is_text_like { Abs::zero() } else { descent + sub_drop_min })
456 .max(measure!(bl, ascent) - sub_top_max)
457 .max(measure!(br, ascent) - sub_top_max);
458 }
459
460 for (sup, sub) in [(tl, bl), (tr, br)] {
461 if let (Some(sup), Some(sub)) = (&sup, &sub) {
462 let sup_bottom = shift_up - sup.descent();
463 let sub_top = sub.ascent() - shift_down;
464 let gap = sup_bottom - sub_top;
465 if gap >= gap_min {
466 continue;
467 }
468
469 let increase = gap_min - gap;
470 let sup_only =
471 (sup_bottom_max_with_sub - sup_bottom).clamp(Abs::zero(), increase);
472 let rest = (increase - sup_only) / 2.0;
473 shift_up += sup_only + rest;
474 shift_down += rest;
475 }
476 }
477
478 (shift_up, shift_down)
479}
480
481fn math_kern(base: &MathFragment, script: &MathFragment, shift: Abs, pos: Corner) -> Abs {
487 let (corr_height_top, corr_height_bot) = match pos {
491 Corner::TopLeft | Corner::TopRight => {
497 (base.ascent() - shift, shift - script.descent())
498 }
499 Corner::BottomLeft | Corner::BottomRight => {
505 (script.ascent() - shift, shift - base.descent())
506 }
507 };
508
509 let summed_kern = |height| {
511 let base_kern = base.kern_at_height(pos, height);
512 let attach_kern = script.kern_at_height(pos.inv(), height);
513 base_kern + attach_kern
514 };
515
516 summed_kern(corr_height_top).max(summed_kern(corr_height_bot))
522}