summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/math/root.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/math/root.rs')
-rw-r--r--crates/typst-library/src/math/root.rs156
1 files changed, 156 insertions, 0 deletions
diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs
new file mode 100644
index 00000000..d1c5f46a
--- /dev/null
+++ b/crates/typst-library/src/math/root.rs
@@ -0,0 +1,156 @@
+use super::*;
+
+/// A square root.
+///
+/// ## Example { #example }
+/// ```example
+/// $ sqrt(x^2) = x = sqrt(x)^2 $
+/// ```
+///
+/// Display: Square Root
+/// Category: math
+#[func]
+pub fn sqrt(
+ /// The expression to take the square root of.
+ radicand: Content,
+) -> Content {
+ RootElem::new(radicand).pack()
+}
+
+/// A general root.
+///
+/// ## Example { #example }
+/// ```example
+/// $ root(3, x) $
+/// ```
+///
+/// Display: Root
+/// Category: math
+#[element(LayoutMath)]
+pub struct RootElem {
+ /// Which root of the radicand to take.
+ #[positional]
+ pub index: Option<Content>,
+
+ /// The expression to take the root of.
+ #[required]
+ pub radicand: Content,
+}
+
+impl LayoutMath for RootElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout(ctx, self.index(ctx.styles()).as_ref(), &self.radicand(), self.span())
+ }
+}
+
+/// Layout a root.
+///
+/// https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot
+fn layout(
+ ctx: &mut MathContext,
+ mut index: Option<&Content>,
+ radicand: &Content,
+ span: Span,
+) -> SourceResult<()> {
+ let gap = scaled!(
+ ctx,
+ text: radical_vertical_gap,
+ display: radical_display_style_vertical_gap,
+ );
+ let thickness = scaled!(ctx, radical_rule_thickness);
+ let extra_ascender = scaled!(ctx, radical_extra_ascender);
+ let kern_before = scaled!(ctx, radical_kern_before_degree);
+ let kern_after = scaled!(ctx, radical_kern_after_degree);
+ let raise_factor = percent!(ctx, radical_degree_bottom_raise_percent);
+
+ // Layout radicand.
+ ctx.style(ctx.style.with_cramped(true));
+ let radicand = ctx.layout_frame(radicand)?;
+ ctx.unstyle();
+
+ // Layout root symbol.
+ let target = radicand.height() + thickness + gap;
+ let sqrt = precomposed(ctx, index, target)
+ .map(|frame| {
+ index = None;
+ frame
+ })
+ .unwrap_or_else(|| {
+ let glyph = GlyphFragment::new(ctx, '√', span);
+ glyph.stretch_vertical(ctx, target, Abs::zero()).frame
+ });
+
+ // Layout the index.
+ // Script-script style looks too small, we use Script style instead.
+ ctx.style(ctx.style.with_size(MathSize::Script));
+ let index = index.map(|elem| ctx.layout_frame(elem)).transpose()?;
+ ctx.unstyle();
+
+ let gap = gap.max((sqrt.height() - radicand.height() - thickness) / 2.0);
+ let descent = radicand.descent() + gap;
+ let inner_ascent = extra_ascender + thickness + gap + radicand.ascent();
+
+ 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;
+ shift_up = raise_factor * sqrt.height() - descent + index.descent();
+ ascent.set_max(shift_up + index.ascent());
+ }
+
+ let radicant_offset = sqrt_offset + sqrt.width();
+ let width = radicant_offset + radicand.width();
+ let size = Size::new(width, ascent + descent);
+
+ let sqrt_pos = Point::new(sqrt_offset, ascent - inner_ascent);
+ let line_pos = Point::new(radicant_offset, ascent - inner_ascent + thickness / 2.0);
+ let radicand_pos = Point::new(radicant_offset, ascent - radicand.ascent());
+
+ let mut frame = Frame::new(size);
+ frame.set_baseline(ascent);
+
+ if let Some(index) = index {
+ let index_pos = Point::new(kern_before, ascent - shift_up - index.ascent());
+ frame.push_frame(index_pos, index);
+ }
+
+ frame.push_frame(sqrt_pos, sqrt);
+ frame.push(
+ line_pos,
+ FrameItem::Shape(
+ Geometry::Line(Point::with_x(radicand.width())).stroked(Stroke {
+ paint: TextElem::fill_in(ctx.styles()),
+ thickness,
+ ..Stroke::default()
+ }),
+ span,
+ ),
+ );
+
+ frame.push_frame(radicand_pos, radicand);
+ ctx.push(FrameFragment::new(ctx, frame));
+
+ Ok(())
+}
+
+/// Select a precomposed radical, if the font has it.
+fn precomposed(ctx: &MathContext, index: Option<&Content>, target: Abs) -> Option<Frame> {
+ let elem = index?.to::<TextElem>()?;
+ let c = match elem.text().as_str() {
+ "3" => '∛',
+ "4" => '∜',
+ _ => return None,
+ };
+
+ ctx.ttf.glyph_index(c)?;
+ let glyph = GlyphFragment::new(ctx, c, elem.span());
+ let variant = glyph.stretch_vertical(ctx, target, Abs::zero()).frame;
+ if variant.height() < target {
+ return None;
+ }
+
+ Some(variant)
+}