use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Abs, Frame, FrameItem, Point, Size}; use typst_library::math::{EquationElem, MathSize, RootElem}; use typst_library::text::TextElem; use typst_library::visualize::{FixedStroke, Geometry}; use super::{style_cramped, FrameFragment, MathContext}; /// Lays out a [`RootElem`]. /// /// TeXbook page 443, page 360 /// See also: #[typst_macros::time(name = "math.root", span = elem.span())] pub fn layout_root( elem: &Packed, ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { let span = elem.span(); // Layout radicand. let radicand = { let cramped = style_cramped(); let styles = styles.chain(&cramped); let run = ctx.layout_into_run(&elem.radicand, styles)?; let multiline = run.is_multiline(); let radicand = run.into_fragment(styles); if multiline { // Align the frame center line with the math axis. let (font, size) = radicand.font(ctx, styles, elem.radicand.span())?; let axis = value!(font, axis_height).at(size); let mut radicand = radicand.into_frame(); radicand.set_baseline(radicand.height() / 2.0 + axis); radicand } else { radicand.into_frame() } }; // Layout root symbol. let mut sqrt = ctx.layout_into_fragment(&SymbolElem::packed('√').spanned(span), styles)?; let (font, size) = sqrt.font(ctx, styles, span)?; let thickness = value!(font, radical_rule_thickness).at(size); let extra_ascender = value!(font, radical_extra_ascender).at(size); let kern_before = value!(font, radical_kern_before_degree).at(size); let kern_after = value!(font, radical_kern_after_degree).at(size); let raise_factor = percent!(font, radical_degree_bottom_raise_percent); let gap = value!( font, styles, text: radical_vertical_gap, display: radical_display_style_vertical_gap, ) .at(size); let line = FrameItem::Shape( Geometry::Line(Point::with_x(radicand.width())).stroked(FixedStroke::from_pair( sqrt.fill() .unwrap_or_else(|| styles.get_ref(TextElem::fill).as_decoration()), thickness, )), span, ); let target = radicand.height() + thickness + gap; sqrt.stretch_vertical(ctx, target); let sqrt = sqrt.into_frame(); // Layout the index. let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap(); let index = elem .index .get_ref(styles) .as_ref() .map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript))) .transpose()?; // TeXbook, page 443, item 11 // Keep original gap, and then distribute any remaining free space // equally above and below. let gap = gap.max((sqrt.height() - thickness - radicand.height() + gap) / 2.0); let sqrt_ascent = radicand.ascent() + gap + thickness; let descent = sqrt.height() - sqrt_ascent; let inner_ascent = sqrt_ascent + extra_ascender; let mut sqrt_offset = Abs::zero(); let mut shift_up = Abs::zero(); let mut ascent = inner_ascent; if let Some(index) = &index { sqrt_offset = kern_before + index.width() + kern_after; // The formula below for how much raise the index by comes from // the TeXbook, page 360, in the definition of `\root`. // However, the `+ index.descent()` part is different from TeX. // Without it, descenders can collide with the surd, a rarity // in practice, but possible. MS Word also adjusts index positions // for descenders. shift_up = raise_factor * (inner_ascent - descent) + index.descent(); ascent.set_max(shift_up + index.ascent()); } let sqrt_x = sqrt_offset.max(Abs::zero()); let radicand_x = sqrt_x + sqrt.width(); let radicand_y = ascent - radicand.ascent(); let width = radicand_x + radicand.width(); let size = Size::new(width, ascent + descent); // The extra "- thickness" comes from the fact that the sqrt is placed // in `push_frame` with respect to its top, not its baseline. let sqrt_pos = Point::new(sqrt_x, radicand_y - gap - thickness); let line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0)); let radicand_pos = Point::new(radicand_x, radicand_y); let mut frame = Frame::soft(size); frame.set_baseline(ascent); if let Some(index) = index { let index_x = -sqrt_offset.min(Abs::zero()) + kern_before; let index_pos = Point::new(index_x, ascent - index.ascent() - shift_up); frame.push_frame(index_pos, index); } frame.push_frame(sqrt_pos, sqrt); frame.push(line_pos, line); frame.push_frame(radicand_pos, radicand); ctx.push(FrameFragment::new(styles, frame)); Ok(()) }