summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/math/underover.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/math/underover.rs')
-rw-r--r--crates/typst-library/src/math/underover.rs339
1 files changed, 339 insertions, 0 deletions
diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst-library/src/math/underover.rs
new file mode 100644
index 00000000..796c9ebc
--- /dev/null
+++ b/crates/typst-library/src/math/underover.rs
@@ -0,0 +1,339 @@
+use super::*;
+
+const BRACE_GAP: Em = Em::new(0.25);
+const BRACKET_GAP: Em = Em::new(0.25);
+
+/// A marker to distinguish under- vs. overlines.
+enum LineKind {
+ Over,
+ Under,
+}
+
+/// A horizontal line under content.
+///
+/// ## Example { #example }
+/// ```example
+/// $ underline(1 + 2 + ... + 5) $
+/// ```
+///
+/// Display: Underline
+/// Category: math
+#[element(LayoutMath)]
+pub struct UnderlineElem {
+ /// The content above the line.
+ #[required]
+ pub body: Content,
+}
+
+impl LayoutMath for UnderlineElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout_underoverline(ctx, &self.body(), self.span(), LineKind::Under)
+ }
+}
+
+/// A horizontal line over content.
+///
+/// ## Example { #example }
+/// ```example
+/// $ overline(1 + 2 + ... + 5) $
+/// ```
+///
+/// Display: Overline
+/// Category: math
+#[element(LayoutMath)]
+pub struct OverlineElem {
+ /// The content below the line.
+ #[required]
+ pub body: Content,
+}
+
+impl LayoutMath for OverlineElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout_underoverline(ctx, &self.body(), self.span(), LineKind::Over)
+ }
+}
+
+/// layout under- or overlined content
+fn layout_underoverline(
+ ctx: &mut MathContext,
+ body: &Content,
+ span: Span,
+ line: LineKind,
+) -> SourceResult<()> {
+ let (extra_height, content, line_pos, content_pos, baseline, bar_height);
+ match line {
+ LineKind::Under => {
+ let sep = scaled!(ctx, underbar_extra_descender);
+ bar_height = scaled!(ctx, underbar_rule_thickness);
+ let gap = scaled!(ctx, underbar_vertical_gap);
+ extra_height = sep + bar_height + gap;
+
+ content = ctx.layout_fragment(body)?;
+
+ line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
+ content_pos = Point::zero();
+ baseline = content.ascent()
+ }
+ LineKind::Over => {
+ let sep = scaled!(ctx, overbar_extra_ascender);
+ bar_height = scaled!(ctx, overbar_rule_thickness);
+ let gap = scaled!(ctx, overbar_vertical_gap);
+ extra_height = sep + bar_height + gap;
+
+ ctx.style(ctx.style.with_cramped(true));
+ content = ctx.layout_fragment(body)?;
+ ctx.unstyle();
+
+ line_pos = Point::with_y(sep + bar_height / 2.0);
+ content_pos = Point::with_y(extra_height);
+ baseline = content.ascent() + extra_height;
+ }
+ }
+
+ let width = content.width();
+ let height = content.height() + extra_height;
+ let size = Size::new(width, height);
+
+ let content_class = content.class().unwrap_or(MathClass::Normal);
+ let mut frame = Frame::new(size);
+ frame.set_baseline(baseline);
+ frame.push_frame(content_pos, content.into_frame());
+ frame.push(
+ line_pos,
+ FrameItem::Shape(
+ Geometry::Line(Point::with_x(width)).stroked(Stroke {
+ paint: TextElem::fill_in(ctx.styles()),
+ thickness: bar_height,
+ ..Stroke::default()
+ }),
+ span,
+ ),
+ );
+
+ ctx.push(FrameFragment::new(ctx, frame).with_class(content_class));
+
+ Ok(())
+}
+
+/// A horizontal brace under content, with an optional annotation below.
+///
+/// ## Example { #example }
+/// ```example
+/// $ underbrace(1 + 2 + ... + 5, "numbers") $
+/// ```
+///
+/// Display: Underbrace
+/// Category: math
+#[element(LayoutMath)]
+pub struct UnderbraceElem {
+ /// The content above the brace.
+ #[required]
+ pub body: Content,
+
+ /// The optional content below the brace.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+impl LayoutMath for UnderbraceElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout_underoverspreader(
+ ctx,
+ &self.body(),
+ &self.annotation(ctx.styles()),
+ '⏟',
+ BRACE_GAP,
+ false,
+ self.span(),
+ )
+ }
+}
+
+/// A horizontal brace over content, with an optional annotation above.
+///
+/// ## Example { #example }
+/// ```example
+/// $ overbrace(1 + 2 + ... + 5, "numbers") $
+/// ```
+///
+/// Display: Overbrace
+/// Category: math
+#[element(LayoutMath)]
+pub struct OverbraceElem {
+ /// The content below the brace.
+ #[required]
+ pub body: Content,
+
+ /// The optional content above the brace.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+impl LayoutMath for OverbraceElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout_underoverspreader(
+ ctx,
+ &self.body(),
+ &self.annotation(ctx.styles()),
+ '⏞',
+ BRACE_GAP,
+ true,
+ self.span(),
+ )
+ }
+}
+
+/// A horizontal bracket under content, with an optional annotation below.
+///
+/// ## Example { #example }
+/// ```example
+/// $ underbracket(1 + 2 + ... + 5, "numbers") $
+/// ```
+///
+/// Display: Underbracket
+/// Category: math
+#[element(LayoutMath)]
+pub struct UnderbracketElem {
+ /// The content above the bracket.
+ #[required]
+ pub body: Content,
+
+ /// The optional content below the bracket.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+impl LayoutMath for UnderbracketElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout_underoverspreader(
+ ctx,
+ &self.body(),
+ &self.annotation(ctx.styles()),
+ '⎵',
+ BRACKET_GAP,
+ false,
+ self.span(),
+ )
+ }
+}
+
+/// A horizontal bracket over content, with an optional annotation above.
+///
+/// ## Example { #example }
+/// ```example
+/// $ overbracket(1 + 2 + ... + 5, "numbers") $
+/// ```
+///
+/// Display: Overbracket
+/// Category: math
+#[element(LayoutMath)]
+pub struct OverbracketElem {
+ /// The content below the bracket.
+ #[required]
+ pub body: Content,
+
+ /// The optional content above the bracket.
+ #[positional]
+ pub annotation: Option<Content>,
+}
+
+impl LayoutMath for OverbracketElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ layout_underoverspreader(
+ ctx,
+ &self.body(),
+ &self.annotation(ctx.styles()),
+ '⎴',
+ BRACKET_GAP,
+ true,
+ self.span(),
+ )
+ }
+}
+
+/// Layout an over- or underbrace-like object.
+fn layout_underoverspreader(
+ ctx: &mut MathContext,
+ body: &Content,
+ annotation: &Option<Content>,
+ c: char,
+ gap: Em,
+ reverse: bool,
+ span: Span,
+) -> SourceResult<()> {
+ let gap = gap.scaled(ctx);
+ let body = ctx.layout_row(body)?;
+ let body_class = body.class();
+ let body = body.into_fragment(ctx);
+ let glyph = GlyphFragment::new(ctx, c, span);
+ let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
+
+ let mut rows = vec![MathRow::new(vec![body]), stretched.into()];
+ ctx.style(if reverse {
+ ctx.style.for_subscript()
+ } else {
+ ctx.style.for_superscript()
+ });
+ rows.extend(
+ annotation
+ .as_ref()
+ .map(|annotation| ctx.layout_row(annotation))
+ .transpose()?,
+ );
+ ctx.unstyle();
+
+ let mut baseline = 0;
+ if reverse {
+ rows.reverse();
+ baseline = rows.len() - 1;
+ }
+
+ let frame = stack(ctx, rows, Align::Center, gap, baseline);
+ ctx.push(FrameFragment::new(ctx, frame).with_class(body_class));
+
+ Ok(())
+}
+
+/// Stack rows on top of each other.
+///
+/// Add a `gap` between each row and uses the baseline of the `baseline`th
+/// row for the whole frame.
+pub(super) fn stack(
+ ctx: &MathContext,
+ rows: Vec<MathRow>,
+ align: Align,
+ gap: Abs,
+ baseline: usize,
+) -> Frame {
+ let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect();
+ let AlignmentResult { points, width } = alignments(&rows);
+ let rows: Vec<_> = rows
+ .into_iter()
+ .map(|row| row.into_aligned_frame(ctx, &points, align))
+ .collect();
+
+ let mut y = Abs::zero();
+ let mut frame = Frame::new(Size::new(
+ width,
+ rows.iter().map(|row| row.height()).sum::<Abs>()
+ + rows.len().saturating_sub(1) as f64 * gap,
+ ));
+
+ for (i, row) in rows.into_iter().enumerate() {
+ let x = align.position(width - row.width());
+ let pos = Point::new(x, y);
+ if i == baseline {
+ frame.set_baseline(y + row.baseline());
+ }
+ y += row.height() + gap;
+ frame.push_frame(pos, row);
+ }
+
+ frame
+}