summaryrefslogtreecommitdiff
path: root/library/src/math/attach.rs
diff options
context:
space:
mode:
Diffstat (limited to 'library/src/math/attach.rs')
-rw-r--r--library/src/math/attach.rs251
1 files changed, 251 insertions, 0 deletions
diff --git a/library/src/math/attach.rs b/library/src/math/attach.rs
new file mode 100644
index 00000000..2205e556
--- /dev/null
+++ b/library/src/math/attach.rs
@@ -0,0 +1,251 @@
+use super::*;
+
+/// # Attachment
+/// A base with optional attachments.
+///
+/// ## Syntax
+/// This function also has dedicated syntax: Use the underscore (`_`) to
+/// indicate a bottom attachment and the circumflex (`^`) to indicate a top
+/// attachment.
+///
+/// ## Example
+/// ```
+/// $ sum_(i=0)^n a_i = 2^(1+i) $
+/// ```
+///
+/// ## Parameters
+/// - base: Content (positional, required)
+/// The base to which things are attached.
+///
+/// - top: Content (named)
+/// The top attachment.
+///
+/// - bottom: Content (named)
+/// The bottom attachment.
+///
+/// ## Category
+/// math
+#[func]
+#[capable(LayoutMath)]
+#[derive(Debug, Hash)]
+pub struct AttachNode {
+ /// The base.
+ pub base: Content,
+ /// The top attachment.
+ pub top: Option<Content>,
+ /// The bottom attachment.
+ pub bottom: Option<Content>,
+}
+
+#[node]
+impl AttachNode {
+ fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
+ let base = args.expect("base")?;
+ let top = args.named("top")?;
+ let bottom = args.named("bottom")?;
+ Ok(Self { base, top, bottom }.pack())
+ }
+}
+
+impl LayoutMath for AttachNode {
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ let base = ctx.layout_fragment(&self.base)?;
+
+ let mut sub = Frame::new(Size::zero());
+ if let Some(node) = &self.top {
+ ctx.style(ctx.style.for_subscript());
+ sub = ctx.layout_frame(node)?;
+ ctx.unstyle();
+ }
+
+ let mut sup = Frame::new(Size::zero());
+ if let Some(node) = &self.bottom {
+ ctx.style(ctx.style.for_superscript());
+ sup = ctx.layout_frame(node)?;
+ ctx.unstyle();
+ }
+
+ let render_limits = self.base.is::<LimitsNode>()
+ || (!self.base.is::<ScriptsNode>()
+ && ctx.style.size == MathSize::Display
+ && base.class() == Some(MathClass::Large)
+ && match &base {
+ MathFragment::Variant(variant) => LIMITS.contains(&variant.c),
+ MathFragment::Frame(fragment) => fragment.limits,
+ _ => false,
+ });
+
+ if render_limits {
+ limits(ctx, base, sub, sup)
+ } else {
+ scripts(ctx, base, sub, sup, self.top.is_some() && self.bottom.is_some())
+ }
+ }
+}
+
+/// # Scripts
+/// Force a base to display attachments as scripts.
+///
+/// ## Example
+/// ```
+/// $ scripts(sum)_1^2 != sum_1^2 $
+/// ```
+///
+/// ## Parameters
+/// - base: Content (positional, required)
+/// The base to attach the scripts to.
+///
+/// ## Category
+/// math
+#[func]
+#[capable(LayoutMath)]
+#[derive(Debug, Hash)]
+pub struct ScriptsNode(Content);
+
+#[node]
+impl ScriptsNode {
+ fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
+ Ok(Self(args.expect("base")?).pack())
+ }
+}
+
+impl LayoutMath for ScriptsNode {
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ self.0.layout_math(ctx)
+ }
+}
+
+/// # Limits
+/// Force a base to display attachments as limits.
+///
+/// ## Example
+/// ```
+/// $ limits(A)_1^2 != A_1^2 $
+/// ```
+///
+/// ## Parameters
+/// - base: Content (positional, required)
+/// The base to attach the limits to.
+///
+/// ## Category
+/// math
+#[func]
+#[capable(LayoutMath)]
+#[derive(Debug, Hash)]
+pub struct LimitsNode(Content);
+
+#[node]
+impl LimitsNode {
+ fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
+ Ok(Self(args.expect("base")?).pack())
+ }
+}
+
+impl LayoutMath for LimitsNode {
+ fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
+ self.0.layout_math(ctx)
+ }
+}
+
+/// Layout sub- and superscripts.
+fn scripts(
+ ctx: &mut MathContext,
+ base: MathFragment,
+ sub: Frame,
+ sup: Frame,
+ both: bool,
+) -> SourceResult<()> {
+ 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 space_after = scaled!(ctx, space_after_script);
+
+ let mut shift_up = sup_shift_up
+ .max(base.ascent() - sup_drop_max)
+ .max(sup_bottom_min + sup.descent());
+
+ let mut shift_down = sub_shift_down
+ .max(base.descent() + sub_drop_min)
+ .max(sub.ascent() - sub_top_max);
+
+ if both {
+ let sup_bottom = shift_up - sup.descent();
+ let sub_top = sub.ascent() - shift_down;
+ let gap = sup_bottom - sub_top;
+ if gap < gap_min {
+ 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;
+ }
+ }
+
+ let delta = base.italics_correction();
+ let ascent = shift_up + sup.ascent();
+ let descent = shift_down + sub.descent();
+ let height = ascent + descent;
+ let width = base.width() + sup.width().max(sub.width() - delta) + space_after;
+ let base_pos = Point::with_y(ascent - base.ascent());
+ let sup_pos = Point::with_x(base.width());
+ let sub_pos = Point::new(base.width() - delta, height - sub.height());
+ let class = base.class().unwrap_or(MathClass::Normal);
+
+ let mut frame = Frame::new(Size::new(width, height));
+ frame.set_baseline(ascent);
+ frame.push_frame(base_pos, base.to_frame(ctx));
+ frame.push_frame(sub_pos, sub);
+ frame.push_frame(sup_pos, sup);
+ ctx.push(FrameFragment::new(frame).with_class(class));
+
+ Ok(())
+}
+
+/// Layout limits.
+fn limits(
+ ctx: &mut MathContext,
+ base: MathFragment,
+ sub: Frame,
+ sup: Frame,
+) -> SourceResult<()> {
+ 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 sup_gap = upper_gap_min.max(upper_rise_min - sup.descent());
+ let sub_gap = lower_gap_min.max(lower_drop_min - sub.ascent());
+
+ let delta = base.italics_correction() / 2.0;
+ let width = base.width().max(sup.width()).max(sub.width());
+ let height = sup.height() + sup_gap + base.height() + sub_gap + sub.height();
+ let base_pos = Point::new((width - base.width()) / 2.0, sup.height() + sup_gap);
+ let sup_pos = Point::with_x((width - sup.width()) / 2.0 + delta);
+ let sub_pos = Point::new((width - sub.width()) / 2.0 - delta, height - sub.height());
+ let class = base.class().unwrap_or(MathClass::Normal);
+
+ let mut frame = Frame::new(Size::new(width, height));
+ frame.set_baseline(base_pos.y + base.ascent());
+ frame.push_frame(base_pos, base.to_frame(ctx));
+ frame.push_frame(sub_pos, sub);
+ frame.push_frame(sup_pos, sup);
+ ctx.push(FrameFragment::new(frame).with_class(class));
+
+ Ok(())
+}
+
+/// Codepoints that should have sub- and superscripts attached as limits.
+const LIMITS: &[char] = &[
+ '\u{2210}', '\u{22C1}', '\u{22C0}', '\u{2A04}', '\u{22C2}', '\u{22C3}', '\u{220F}',
+ '\u{2211}', '\u{2A02}', '\u{2A01}', '\u{2A00}', '\u{2A06}',
+];