summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/math/attach.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/math/attach.rs')
-rw-r--r--crates/typst-library/src/math/attach.rs411
1 files changed, 411 insertions, 0 deletions
diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs
new file mode 100644
index 00000000..fedeb908
--- /dev/null
+++ b/crates/typst-library/src/math/attach.rs
@@ -0,0 +1,411 @@
+use super::*;
+
+/// A base with optional attachments.
+///
+/// ## Example { #example }
+/// ```example
+/// // With syntax.
+/// $ sum_(i=0)^n a_i = 2^(1+i) $
+///
+/// // With function call.
+/// $ attach(
+/// Pi, t: alpha, b: beta,
+/// tl: 1, tr: 2+3, bl: 4+5, br: 6,
+/// ) $
+/// ```
+///
+/// ## Syntax { #syntax }
+/// This function also has dedicated syntax for attachments after the base: Use
+/// the underscore (`_`) to indicate a subscript i.e. bottom attachment and the
+/// hat (`^`) to indicate a superscript i.e. top attachment.
+///
+/// Display: Attachment
+/// Category: math
+#[element(LayoutMath)]
+pub struct AttachElem {
+ /// The base to which things are attached.
+ #[required]
+ pub base: Content,
+
+ /// The top attachment, smartly positioned at top-right or above the base.
+ ///
+ /// You can wrap the base in `{limits()}` or `{scripts()}` to override the
+ /// smart positioning.
+ pub t: Option<Content>,
+
+ /// The bottom attachment, smartly positioned at the bottom-right or below
+ /// the base.
+ ///
+ /// You can wrap the base in `{limits()}` or `{scripts()}` to override the
+ /// smart positioning.
+ pub b: Option<Content>,
+
+ /// The top-left attachment (before the base).
+ pub tl: Option<Content>,
+
+ /// The bottom-left attachment (before base).
+ pub bl: Option<Content>,
+
+ /// The top-right attachment (after the base).
+ pub tr: Option<Content>,
+
+ /// The bottom-right attachment (after the base).
+ pub br: Option<Content>,
+}
+
+impl LayoutMath for AttachElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ type GetAttachment = fn(&AttachElem, styles: StyleChain) -> Option<Content>;
+ let layout_attachment = |ctx: &mut MathContext, getter: GetAttachment| {
+ getter(self, ctx.styles())
+ .map(|elem| ctx.layout_fragment(&elem))
+ .transpose()
+ };
+
+ let base = ctx.layout_fragment(&self.base())?;
+
+ ctx.style(ctx.style.for_superscript());
+ let tl = layout_attachment(ctx, Self::tl)?;
+ let tr = layout_attachment(ctx, Self::tr)?;
+ let t = layout_attachment(ctx, Self::t)?;
+ ctx.unstyle();
+
+ ctx.style(ctx.style.for_subscript());
+ let bl = layout_attachment(ctx, Self::bl)?;
+ let br = layout_attachment(ctx, Self::br)?;
+ let b = layout_attachment(ctx, Self::b)?;
+ ctx.unstyle();
+
+ let limits = base.limits().active(ctx);
+ let (t, tr) = if limits || tr.is_some() { (t, tr) } else { (None, t) };
+ let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) };
+ layout_attachments(ctx, base, [tl, t, tr, bl, b, br])
+ }
+}
+
+/// Forces a base to display attachments as scripts.
+///
+/// ## Example { #example }
+/// ```example
+/// $ scripts(sum)_1^2 != sum_1^2 $
+/// ```
+///
+/// Display: Scripts
+/// Category: math
+#[element(LayoutMath)]
+pub struct ScriptsElem {
+ /// The base to attach the scripts to.
+ #[required]
+ pub body: Content,
+}
+
+impl LayoutMath for ScriptsElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ let mut fragment = ctx.layout_fragment(&self.body())?;
+ fragment.set_limits(Limits::Never);
+ ctx.push(fragment);
+ Ok(())
+ }
+}
+
+/// Forces a base to display attachments as limits.
+///
+/// ## Example { #example }
+/// ```example
+/// $ limits(A)_1^2 != A_1^2 $
+/// ```
+///
+/// Display: Limits
+/// Category: math
+#[element(LayoutMath)]
+pub struct LimitsElem {
+ /// The base to attach the limits to.
+ #[required]
+ pub body: Content,
+
+ /// Whether to also force limits in inline equations.
+ ///
+ /// When applying limits globally (e.g., through a show rule), it is
+ /// typically a good idea to disable this.
+ #[default(true)]
+ pub inline: bool,
+}
+
+impl LayoutMath for LimitsElem {
+ #[tracing::instrument(skip(ctx))]
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ let mut fragment = ctx.layout_fragment(&self.body())?;
+ fragment.set_limits(if self.inline(ctx.styles()) {
+ Limits::Always
+ } else {
+ Limits::Display
+ });
+ ctx.push(fragment);
+ Ok(())
+ }
+}
+
+/// Describes in which situation a frame should use limits for attachments.
+#[derive(Debug, Copy, Clone)]
+pub enum Limits {
+ /// Always scripts.
+ Never,
+ /// Display limits only in `display` math.
+ Display,
+ /// Always limits.
+ Always,
+}
+
+impl Limits {
+ /// The default limit configuration if the given character is the base.
+ pub fn for_char(c: char) -> Self {
+ if Self::DEFAULT_TO_LIMITS.contains(&c) {
+ Limits::Display
+ } else {
+ Limits::Never
+ }
+ }
+
+ /// Whether limits should be displayed in this context
+ pub fn active(&self, ctx: &MathContext) -> bool {
+ match self {
+ Self::Always => true,
+ Self::Display => ctx.style.size == MathSize::Display,
+ Self::Never => false,
+ }
+ }
+
+ /// Unicode codepoints that should show attachments as limits in display
+ /// mode.
+ #[rustfmt::skip]
+ const DEFAULT_TO_LIMITS: &[char] = &[
+ /* ∏ */ '\u{220F}', /* ∐ */ '\u{2210}', /* ∑ */ '\u{2211}',
+ /* ⋀ */ '\u{22C0}', /* ⋁ */ '\u{22C1}',
+ /* ⋂ */ '\u{22C2}', /* ⋃ */ '\u{22C3}',
+ /* ⨀ */ '\u{2A00}', /* ⨁ */ '\u{2A01}', /* ⨂ */ '\u{2A02}',
+ /* ⨃ */ '\u{2A03}', /* ⨄ */ '\u{2A04}',
+ /* ⨅ */ '\u{2A05}', /* ⨆ */ '\u{2A06}',
+ ];
+}
+
+macro_rules! measure {
+ ($e: ident, $attr: ident) => {
+ $e.as_ref().map(|e| e.$attr()).unwrap_or_default()
+ };
+}
+
+/// Layout the attachments.
+fn layout_attachments(
+ ctx: &mut MathContext,
+ base: MathFragment,
+ [tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
+) -> SourceResult<()> {
+ let (shift_up, shift_down) =
+ compute_shifts_up_and_down(ctx, &base, [&tl, &tr, &bl, &br]);
+
+ let sup_delta = Abs::zero();
+ let sub_delta = -base.italics_correction();
+ let (base_width, base_ascent, base_descent) =
+ (base.width(), base.ascent(), base.descent());
+ let base_class = base.class().unwrap_or(MathClass::Normal);
+
+ let ascent = base_ascent
+ .max(shift_up + measure!(tr, ascent))
+ .max(shift_up + measure!(tl, ascent))
+ .max(shift_up + measure!(t, height));
+
+ let descent = base_descent
+ .max(shift_down + measure!(br, descent))
+ .max(shift_down + measure!(bl, descent))
+ .max(shift_down + measure!(b, height));
+
+ let pre_sup_width = measure!(tl, width);
+ let pre_sub_width = measure!(bl, width);
+ let pre_width_dif = pre_sup_width - pre_sub_width; // Could be negative.
+ let pre_width_max = pre_sup_width.max(pre_sub_width);
+ let post_max_width =
+ (sup_delta + measure!(tr, width)).max(sub_delta + measure!(br, width));
+
+ let (center_frame, base_offset) = attach_top_and_bottom(ctx, base, t, b);
+ let base_pos =
+ Point::new(sup_delta + pre_width_max, ascent - base_ascent - base_offset);
+ if [&tl, &bl, &tr, &br].iter().all(|&e| e.is_none()) {
+ ctx.push(FrameFragment::new(ctx, center_frame).with_class(base_class));
+ return Ok(());
+ }
+
+ let mut frame = Frame::new(Size::new(
+ pre_width_max + base_width + post_max_width + scaled!(ctx, space_after_script),
+ ascent + descent,
+ ));
+ frame.set_baseline(ascent);
+ frame.push_frame(base_pos, center_frame);
+
+ if let Some(tl) = tl {
+ let pos =
+ Point::new(-pre_width_dif.min(Abs::zero()), ascent - shift_up - tl.ascent());
+ frame.push_frame(pos, tl.into_frame());
+ }
+
+ if let Some(bl) = bl {
+ let pos =
+ Point::new(pre_width_dif.max(Abs::zero()), ascent + shift_down - bl.ascent());
+ frame.push_frame(pos, bl.into_frame());
+ }
+
+ if let Some(tr) = tr {
+ let pos = Point::new(
+ sup_delta + pre_width_max + base_width,
+ ascent - shift_up - tr.ascent(),
+ );
+ frame.push_frame(pos, tr.into_frame());
+ }
+
+ if let Some(br) = br {
+ let pos = Point::new(
+ sub_delta + pre_width_max + base_width,
+ ascent + shift_down - br.ascent(),
+ );
+ frame.push_frame(pos, br.into_frame());
+ }
+
+ ctx.push(FrameFragment::new(ctx, frame).with_class(base_class));
+
+ Ok(())
+}
+
+fn attach_top_and_bottom(
+ ctx: &mut MathContext,
+ base: MathFragment,
+ t: Option<MathFragment>,
+ b: Option<MathFragment>,
+) -> (Frame, Abs) {
+ let upper_gap_min = scaled!(ctx, upper_limit_gap_min);
+ let upper_rise_min = scaled!(ctx, upper_limit_baseline_rise_min);
+ let lower_gap_min = scaled!(ctx, lower_limit_gap_min);
+ let lower_drop_min = scaled!(ctx, lower_limit_baseline_drop_min);
+
+ let mut base_offset = Abs::zero();
+ let mut width = base.width();
+ let mut height = base.height();
+
+ if let Some(t) = &t {
+ let top_gap = upper_gap_min.max(upper_rise_min - t.descent());
+ width.set_max(t.width());
+ height += t.height() + top_gap;
+ base_offset = top_gap + t.height();
+ }
+
+ if let Some(b) = &b {
+ let bottom_gap = lower_gap_min.max(lower_drop_min - b.ascent());
+ width.set_max(b.width());
+ height += b.height() + bottom_gap;
+ }
+
+ let base_pos = Point::new((width - base.width()) / 2.0, base_offset);
+ let delta = base.italics_correction() / 2.0;
+
+ let mut frame = Frame::new(Size::new(width, height));
+ frame.set_baseline(base_pos.y + base.ascent());
+ frame.push_frame(base_pos, base.into_frame());
+
+ if let Some(t) = t {
+ let top_pos = Point::with_x((width - t.width()) / 2.0 + delta);
+ frame.push_frame(top_pos, t.into_frame());
+ }
+
+ if let Some(b) = b {
+ let bottom_pos =
+ Point::new((width - b.width()) / 2.0 - delta, height - b.height());
+ frame.push_frame(bottom_pos, b.into_frame());
+ }
+
+ (frame, base_offset)
+}
+
+fn compute_shifts_up_and_down(
+ ctx: &MathContext,
+ base: &MathFragment,
+ [tl, tr, bl, br]: [&Option<MathFragment>; 4],
+) -> (Abs, Abs) {
+ let sup_shift_up = if ctx.style.cramped {
+ scaled!(ctx, superscript_shift_up_cramped)
+ } else {
+ scaled!(ctx, superscript_shift_up)
+ };
+
+ let sup_bottom_min = scaled!(ctx, superscript_bottom_min);
+ let sup_bottom_max_with_sub = scaled!(ctx, superscript_bottom_max_with_subscript);
+ let sup_drop_max = scaled!(ctx, superscript_baseline_drop_max);
+ let gap_min = scaled!(ctx, sub_superscript_gap_min);
+ let sub_shift_down = scaled!(ctx, subscript_shift_down);
+ let sub_top_max = scaled!(ctx, subscript_top_max);
+ let sub_drop_min = scaled!(ctx, subscript_baseline_drop_min);
+
+ let mut shift_up = Abs::zero();
+ let mut shift_down = Abs::zero();
+ let is_char_box = is_character_box(base);
+
+ 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_char_box { 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_char_box { 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)
+}
+
+/// Whether the fragment consists of a single character or atomic piece of text.
+fn is_character_box(fragment: &MathFragment) -> bool {
+ match fragment {
+ MathFragment::Glyph(_) | MathFragment::Variant(_) => {
+ fragment.class() != Some(MathClass::Large)
+ }
+ MathFragment::Frame(fragment) => is_atomic_text_frame(&fragment.frame),
+ _ => false,
+ }
+}
+
+/// Handles e.g. "sin", "log", "exp", "CustomOperator".
+fn is_atomic_text_frame(frame: &Frame) -> bool {
+ // Meta information isn't visible or renderable, so we exclude it.
+ let mut iter = frame
+ .items()
+ .map(|(_, item)| item)
+ .filter(|item| !matches!(item, FrameItem::Meta(_, _)));
+ matches!(iter.next(), Some(FrameItem::Text(_))) && iter.next().is_none()
+}