diff --git a/library/src/lib.rs b/library/src/lib.rs index 98ec121a2..e7a23cd7c 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -227,13 +227,25 @@ fn items() -> LangItems { equation: |body, block| math::EquationElem::new(body).with_block(block).pack(), math_align_point: || math::AlignPointElem::new().pack(), math_delimited: |open, body, close| math::LrElem::new(open + body + close).pack(), - math_attach: |base, bottom, top| { + math_attach: |base, top, bottom, topleft, bottomleft, topright, bottomright| { let mut elem = math::AttachElem::new(base); - if let Some(bottom) = bottom { - elem.push_bottom(Some(bottom)); - } if let Some(top) = top { - elem.push_top(Some(top)); + elem.push_t(Some(top)); + } + if let Some(bottom) = bottom { + elem.push_b(Some(bottom)); + } + if let Some(topleft) = topleft { + elem.push_tl(Some(topleft)); + } + if let Some(bottomleft) = bottomleft { + elem.push_bl(Some(bottomleft)); + } + if let Some(topright) = topright { + elem.push_tr(Some(topright)); + } + if let Some(bottomright) = bottomright { + elem.push_br(Some(bottomright)); } elem.pack() }, diff --git a/library/src/math/attach.rs b/library/src/math/attach.rs index dcf7fb78d..34c24e17f 100644 --- a/library/src/math/attach.rs +++ b/library/src/math/attach.rs @@ -3,8 +3,9 @@ use super::*; /// A base with optional attachments. /// /// ## Syntax -/// This function also has dedicated syntax: Use the underscore (`_`) to -/// indicate a bottom attachment and the hat (`^`) to indicate a top attachment. +/// 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. /// /// ## Example /// ```example @@ -19,38 +20,56 @@ pub struct AttachElem { #[required] pub base: Content, - /// The top attachment. - pub top: Option, + /// The top attachment, smartly positioned at top-right or above the base. + /// Use limits() or scripts() on the base to override the smart positioning. + pub t: Option, - /// The bottom attachment. - pub bottom: Option, + /// The bottom attachment, smartly positioned at the bottom-right or below the base. + /// Use limits() or scripts() on the base to override the smart positioning. + pub b: Option, + + /// The top-left attachment before the base. + pub tl: Option, + + /// The bottom-left attachment before base. + pub bl: Option, + + /// The top-right attachment after the base. + pub tr: Option, + + /// The bottom-right attachment after the base. + pub br: Option, } +type GetAttachmentContent = + fn(&AttachElem, styles: ::typst::model::StyleChain) -> Option; + impl LayoutMath for AttachElem { #[tracing::instrument(skip(ctx))] fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let base = self.base(); - let display_limits = base.is::(); - let display_scripts = base.is::(); + let base = ctx.layout_fragment(&self.base())?; - let base = ctx.layout_fragment(&base)?; + let getarg = |ctx: &mut MathContext, getter: GetAttachmentContent| { + getter(self, ctx.styles()) + .map(|elem| ctx.layout_fragment(&elem)) + .transpose() + .unwrap() + }; ctx.style(ctx.style.for_superscript()); - let top = self - .top(ctx.styles()) - .map(|elem| ctx.layout_fragment(&elem)) - .transpose()?; + let arg_tl = getarg(ctx, Self::tl); + let arg_tr = getarg(ctx, Self::tr); + let arg_t = getarg(ctx, Self::t); ctx.unstyle(); ctx.style(ctx.style.for_subscript()); - let bottom = self - .bottom(ctx.styles()) - .map(|elem| ctx.layout_fragment(&elem)) - .transpose()?; + let arg_bl = getarg(ctx, Self::bl); + let arg_br = getarg(ctx, Self::br); + let arg_b = getarg(ctx, Self::b); ctx.unstyle(); - let display_limits = display_limits - || (!display_scripts + let as_limits = self.base().is::() + || (!self.base().is::() && ctx.style.size == MathSize::Display && base.class() == Some(MathClass::Large) && match &base { @@ -59,11 +78,12 @@ impl LayoutMath for AttachElem { _ => false, }); - if display_limits { - limits(ctx, base, top, bottom) - } else { - scripts(ctx, base, top, bottom) - } + let (t, tr) = + if as_limits || arg_tr.is_some() { (arg_t, arg_tr) } else { (None, arg_t) }; + let (b, br) = + if as_limits || arg_br.is_some() { (arg_b, arg_br) } else { (None, arg_b) }; + + layout_attachments(ctx, base, [arg_tl, t, tr, arg_bl, b, br]) } } @@ -113,113 +133,98 @@ impl LayoutMath for LimitsElem { } } -/// Layout sub- and superscripts. -fn scripts( +/// Layout the attachments. +fn layout_attachments( ctx: &mut MathContext, base: MathFragment, - sup: Option, - sub: Option, + [tl, t, tr, bl, b, br]: [Option; 6], ) -> 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 (shift_up, shift_down) = + compute_shifts_up_and_down(ctx, &base, [&tl, &tr, &bl, &br]); - let mut shift_up = Abs::zero(); - let mut shift_down = Abs::zero(); - - if let Some(sup) = &sup { - let ascent = match &base { - MathFragment::Frame(frame) => frame.base_ascent, - _ => base.ascent(), - }; - - shift_up = sup_shift_up - .max(ascent - sup_drop_max) - .max(sup_bottom_min + sup.descent()); - } - - if let Some(sub) = &sub { - shift_down = sub_shift_down - .max(base.descent() + sub_drop_min) - .max(sub.ascent() - sub_top_max); - } - - 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 { - 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 italics = base.italics_correction(); let sup_delta = Abs::zero(); - let sub_delta = -italics; + 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 mut width = Abs::zero(); - let mut ascent = base.ascent(); - let mut descent = base.descent(); - - if let Some(sup) = &sup { - ascent.set_max(shift_up + sup.ascent()); - width.set_max(sup_delta + sup.width()); + macro_rules! measure { + ($e: ident, $attr: ident) => { + $e.as_ref().map(|e| e.$attr()).unwrap_or_default() + }; } - if let Some(sub) = &sub { - descent.set_max(shift_down + sub.descent()); - width.set_max(sub_delta + sub.width()); + 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(()); } - width += base.width() + space_after; - - let base_pos = Point::with_y(ascent - base.ascent()); - let base_width = base.width(); - let class = base.class().unwrap_or(MathClass::Normal); - - let mut frame = Frame::new(Size::new(width, ascent + descent)); + 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, base.into_frame()); + frame.push_frame(base_pos, center_frame); - if let Some(sup) = sup { - let sup_pos = - Point::new(sup_delta + base_width, ascent - shift_up - sup.ascent()); - frame.push_frame(sup_pos, sup.into_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(sub) = sub { - let sub_pos = - Point::new(sub_delta + base_width, ascent + shift_down - sub.ascent()); - frame.push_frame(sub_pos, sub.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()); } - ctx.push(FrameFragment::new(ctx, frame).with_class(class)); + 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(()) } -/// Layout limits. -fn limits( +fn attach_top_and_bottom( ctx: &mut MathContext, base: MathFragment, - top: Option, - bottom: Option, -) -> SourceResult<()> { + t: Option, + b: Option, +) -> (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); @@ -229,41 +234,99 @@ fn limits( let mut width = base.width(); let mut height = base.height(); - if let Some(top) = &top { - let top_gap = upper_gap_min.max(upper_rise_min - top.descent()); - width.set_max(top.width()); - height += top.height() + top_gap; - base_offset = top_gap + top.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(bottom) = &bottom { - let bottom_gap = lower_gap_min.max(lower_drop_min - bottom.ascent()); - width.set_max(bottom.width()); - height += bottom.height() + bottom_gap; + 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 class = base.class().unwrap_or(MathClass::Normal); 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(top) = top { - let top_pos = Point::with_x((width - top.width()) / 2.0 + delta); - frame.push_frame(top_pos, top.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(bottom) = bottom { + if let Some(b) = b { let bottom_pos = - Point::new((width - bottom.width()) / 2.0 - delta, height - bottom.height()); - frame.push_frame(bottom_pos, bottom.into_frame()); + Point::new((width - b.width()) / 2.0 - delta, height - b.height()); + frame.push_frame(bottom_pos, b.into_frame()); } - ctx.push(FrameFragment::new(ctx, frame).with_class(class)); + (frame, base_offset) +} - Ok(()) +fn compute_shifts_up_and_down( + ctx: &MathContext, + base: &MathFragment, + [tl, tr, bl, br]: [&Option; 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(); + + for e in [tl, tr].into_iter().flatten() { + let ascent = match &base { + MathFragment::Frame(frame) => frame.base_ascent, + _ => base.ascent(), + }; + + shift_up = shift_up + .max(sup_shift_up) + .max(ascent - sup_drop_max) + .max(sup_bottom_min + e.descent()); + } + for e in [bl, br].into_iter().flatten() { + shift_down = shift_down + .max(sub_shift_down) + .max(base.descent() + sub_drop_min) + .max(e.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) } /// Codepoints that should have sub- and superscripts attached as limits. diff --git a/src/eval/library.rs b/src/eval/library.rs index 0c635864a..a92d8bd19 100644 --- a/src/eval/library.rs +++ b/src/eval/library.rs @@ -87,8 +87,18 @@ pub struct LangItems { /// Matched delimiters in math: `[x + y]`. pub math_delimited: fn(open: Content, body: Content, close: Content) -> Content, /// A base with optional attachments in math: `a_1^2`. - pub math_attach: - fn(base: Content, bottom: Option, top: Option) -> Content, + #[allow(clippy::type_complexity)] + pub math_attach: fn( + base: Content, + // Positioned smartly. + top: Option, + bottom: Option, + // Fixed positions. + topleft: Option, + bottomleft: Option, + topright: Option, + bottomright: Option, + ) -> Content, /// A base with an accent: `arrow(x)`. pub math_accent: fn(base: Content, accent: char) -> Content, /// A fraction in math: `x/2`. diff --git a/src/eval/mod.rs b/src/eval/mod.rs index d8f49d661..d2ca0e74e 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -708,9 +708,9 @@ impl Eval for ast::MathAttach { #[tracing::instrument(name = "MathAttach::eval", skip_all)] fn eval(&self, vm: &mut Vm) -> SourceResult { let base = self.base().eval_display(vm)?; - let bottom = self.bottom().map(|expr| expr.eval_display(vm)).transpose()?; let top = self.top().map(|expr| expr.eval_display(vm)).transpose()?; - Ok((vm.items.math_attach)(base, bottom, top)) + let bottom = self.bottom().map(|expr| expr.eval_display(vm)).transpose()?; + Ok((vm.items.math_attach)(base, top, bottom, None, None, None, None)) } } diff --git a/tests/ref/math/attach.png b/tests/ref/math/attach.png index 956199ca2..e65a16910 100644 Binary files a/tests/ref/math/attach.png and b/tests/ref/math/attach.png differ diff --git a/tests/typ/compiler/content-field.typ b/tests/typ/compiler/content-field.typ index 031a65b89..1d3dceb77 100644 --- a/tests/typ/compiler/content-field.typ +++ b/tests/typ/compiler/content-field.typ @@ -15,8 +15,8 @@ } } else if func == math.attach { let value = f(elem.base) - if elem.has("top") { - value = calc.pow(value, f(elem.top)) + if elem.has("t") { + value = calc.pow(value, f(elem.t)) } value } else if elem.has("children") { diff --git a/tests/typ/math/attach.typ b/tests/typ/math/attach.typ index 3963c9c87..070b9eca0 100644 --- a/tests/typ/math/attach.typ +++ b/tests/typ/math/attach.typ @@ -1,9 +1,45 @@ -// Test top and bottom attachments. +// Test t and b attachments. --- -// Test basics. -$f_x + t^b + V_1^2 - + attach(A, top: alpha, bottom: beta)$ +// Test basics, postscripts. +$f_x + t^b + V_1^2 + attach(A, t: alpha, b: beta)$ + +--- +// Test basics, prescripts. Notably, the upper and lower prescripts' content need to be +// aligned on the right edge of their bounding boxes, not on the left as in postscripts. +$ +attach(upright(O), bl: 8, tl: 16, br: 2, tr: 2-), +attach("Pb", bl: 82, tl: 207) + attach(upright(e), bl: -1, tl: 0) + macron(v)_e \ +$ + +--- +// A mixture of attachment positioning schemes. +$ +attach(a, tl: u), attach(a, tr: v), attach(a, bl: x), +attach(a, br: y), limits(a)^t, limits(a)_b \ + +attach(a, tr: v, t: t), +attach(a, tr: v, br: y), +attach(a, br: y, b: b), +attach(limits(a), b: b, bl: x), +attach(a, tl: u, bl: x), +attach(limits(a), t: t, tl: u) \ + +attach(a, tl: u, tr: v), +attach(limits(a), t: t, br: y), +attach(limits(a), b: b, tr: v), +attach(a, bl: x, br: y), +attach(limits(a), b: b, tl: u), +attach(limits(a), t: t, bl: u), +limits(a)^t_b \ + +attach(a, tl: u, tr: v, bl: x, br: y), +attach(limits(a), t: t, bl: x, br: y, b: b), +attach(limits(a), t: t, tl: u, tr: v, b: b), +attach(limits(a), tl: u, bl: x, t: t, b: b), +attach(limits(a), t: t, b: b, tr: v, br: y), +attach(a, tl: u, t: t, tr: v, bl: x, b: b, br: y) +$ --- // Test function call after subscript. @@ -12,13 +48,21 @@ $pi_1(Y), a_f(x), a^zeta(x) \ --- // Test associativity and scaling. -$ 1/(V^2^3^4^5) $ +$ 1/(V^2^3^4^5), + 1/attach(V, tl: attach(2, tl: attach(3, tl: attach(4, tl: 5)))), + attach(Omega, + tl: attach(2, tl: attach(3, tl: attach(4, tl: 5))), + tr: attach(2, tr: attach(3, tr: attach(4, tr: 5))), + bl: attach(2, bl: attach(3, bl: attach(4, bl: 5))), + br: attach(2, br: attach(3, br: attach(4, br: 5))), + ) +$ --- // Test high subscript and superscript. -$sqrt(a_(1/2)^zeta)$ -$sqrt(a_alpha^(1/2))$ -$sqrt(a_(1/2)^(3/4))$ +$ sqrt(a_(1/2)^zeta), sqrt(a_alpha^(1/2)), sqrt(a_(1/2)^(3/4)) \ + sqrt(attach(a, tl: 1/2, bl: 3/4)), + sqrt(attach(a, tl: 1/2, bl: 3/4, tr: 1/2, br: 3/4)) $ --- // Test frame base. diff --git a/tests/typ/math/content.typ b/tests/typ/math/content.typ index a36960ff2..765e0ec32 100644 --- a/tests/typ/math/content.typ +++ b/tests/typ/math/content.typ @@ -11,7 +11,7 @@ $ x := #table(columns: 2)[x][y]/mat(1, 2, 3) = #table[A][B][C] $ --- // Test non-equation math directly in content. -#math.attach($a$, top: [b]) +#math.attach($a$, t: [b]) --- // Test font switch.