From e00e3e4fbfb9432061d176fb0a3e6b4bf2430fc6 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 26 Sep 2024 14:46:26 +0000 Subject: [PATCH] Add `math.stretch` element function (#5030) --- crates/typst/src/math/attach.rs | 54 +++-- crates/typst/src/math/lr.rs | 59 +++--- crates/typst/src/math/matrix.rs | 8 +- crates/typst/src/math/mod.rs | 11 +- crates/typst/src/math/stretch.rs | 184 +++++++++++++++--- docs/reference/groups.yml | 4 + .../math-stretch-attach-nested-equation.png | Bin 0 -> 180 bytes tests/ref/math-stretch-attach.png | Bin 0 -> 2245 bytes tests/ref/math-stretch-basic.png | Bin 0 -> 787 bytes tests/ref/math-stretch-complex.png | Bin 0 -> 1457 bytes tests/ref/math-stretch-horizontal.png | Bin 0 -> 591 bytes tests/ref/math-stretch-nested.png | Bin 0 -> 253 bytes tests/ref/math-stretch-shorthand.png | Bin 0 -> 375 bytes tests/ref/math-stretch-vertical.png | Bin 0 -> 481 bytes tests/suite/math/stretch.typ | 72 +++++++ 15 files changed, 312 insertions(+), 80 deletions(-) create mode 100644 tests/ref/math-stretch-attach-nested-equation.png create mode 100644 tests/ref/math-stretch-attach.png create mode 100644 tests/ref/math-stretch-basic.png create mode 100644 tests/ref/math-stretch-complex.png create mode 100644 tests/ref/math-stretch-horizontal.png create mode 100644 tests/ref/math-stretch-nested.png create mode 100644 tests/ref/math-stretch-shorthand.png create mode 100644 tests/ref/math-stretch-vertical.png create mode 100644 tests/suite/math/stretch.typ diff --git a/crates/typst/src/math/attach.rs b/crates/typst/src/math/attach.rs index 2d491dfd3..9eb0c824c 100644 --- a/crates/typst/src/math/attach.rs +++ b/crates/typst/src/math/attach.rs @@ -1,15 +1,21 @@ use unicode_math_class::MathClass; use crate::diag::SourceResult; -use crate::foundations::{elem, Content, Packed, StyleChain}; -use crate::layout::{Abs, Corner, Frame, Point, Size}; +use crate::foundations::{elem, Content, Packed, Smart, StyleChain}; +use crate::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size}; use crate::math::{ - style_for_subscript, style_for_superscript, EquationElem, FrameFragment, LayoutMath, - MathContext, MathFragment, MathSize, Scaled, + stretch_fragment, style_for_subscript, style_for_superscript, EquationElem, + FrameFragment, LayoutMath, MathContext, MathFragment, MathSize, Scaled, StretchElem, }; use crate::text::TextElem; use crate::utils::OptionExt; +macro_rules! measure { + ($e: ident, $attr: ident) => { + $e.as_ref().map(|e| e.$attr()).unwrap_or_default() + }; +} + /// A base with optional attachments. /// /// ```example @@ -55,8 +61,9 @@ impl LayoutMath for Packed { fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { let new_elem = merge_base(self); let elem = new_elem.as_ref().unwrap_or(self); + let stretch = stretch_size(styles, elem); - let base = ctx.layout_into_fragment(elem.base(), styles)?; + let mut base = ctx.layout_into_fragment(elem.base(), styles)?; let sup_style = style_for_superscript(styles); let sup_style_chain = styles.chain(&sup_style); let tl = elem.tl(sup_style_chain); @@ -86,12 +93,29 @@ impl LayoutMath for Packed { }; } + // Layout the top and bottom attachments early so we can measure their + // widths, in order to calculate what the stretch size is relative to. + let t = layout!(t, sup_style_chain)?; + let b = layout!(b, sub_style_chain)?; + if let Some(stretch) = stretch { + let relative_to_width = measure!(t, width).max(measure!(b, width)); + stretch_fragment( + ctx, + styles, + &mut base, + Some(Axis::X), + Some(relative_to_width), + stretch, + Abs::zero(), + ); + } + let fragments = [ layout!(tl, sup_style_chain)?, - layout!(t, sup_style_chain)?, + t, layout!(tr, sup_style_chain)?, layout!(bl, sub_style_chain)?, - layout!(b, sub_style_chain)?, + b, layout!(br, sub_style_chain)?, ]; @@ -288,10 +312,18 @@ fn merge_base(elem: &Packed) -> Option> { None } -macro_rules! measure { - ($e: ident, $attr: ident) => { - $e.as_ref().map(|e| e.$attr()).unwrap_or_default() - }; +/// Get the size to stretch the base to, if the attach argument is true. +fn stretch_size( + styles: StyleChain, + elem: &Packed, +) -> Option>> { + // Extract from an EquationElem. + let mut base = elem.base(); + if let Some(equation) = base.to_packed::() { + base = equation.body(); + } + + base.to_packed::().map(|stretch| stretch.size(styles)) } /// Layout the attachments. diff --git a/crates/typst/src/math/lr.rs b/crates/typst/src/math/lr.rs index 70fab12df..80ce55eb3 100644 --- a/crates/typst/src/math/lr.rs +++ b/crates/typst/src/math/lr.rs @@ -1,15 +1,11 @@ use unicode_math_class::MathClass; use crate::diag::SourceResult; -use crate::foundations::{ - elem, func, Content, NativeElement, Packed, Resolve, Smart, StyleChain, -}; -use crate::layout::{Abs, Em, Length, Rel}; -use crate::math::{GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled}; +use crate::foundations::{elem, func, Content, NativeElement, Packed, Smart, StyleChain}; +use crate::layout::{Abs, Axis, Em, Length, Rel}; +use crate::math::{stretch_fragment, LayoutMath, MathContext, MathFragment, Scaled}; use crate::text::TextElem; -use super::delimiter_alignment; - /// How much less high scaled delimiters can be than what they wrap. pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1); @@ -55,18 +51,15 @@ impl LayoutMath for Packed { .max() .unwrap_or_default(); - let height = self - .size(styles) - .unwrap_or(Rel::one()) - .resolve(styles) - .relative_to(2.0 * max_extent); + let relative_to = 2.0 * max_extent; + let height = self.size(styles); // Scale up fragments at both ends. match fragments.as_mut_slice() { - [one] => scale(ctx, styles, one, height, None), + [one] => scale(ctx, styles, one, relative_to, height, None), [first, .., last] => { - scale(ctx, styles, first, height, Some(MathClass::Opening)); - scale(ctx, styles, last, height, Some(MathClass::Closing)); + scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening)); + scale(ctx, styles, last, relative_to, height, Some(MathClass::Closing)); } _ => {} } @@ -76,7 +69,14 @@ impl LayoutMath for Packed { if let MathFragment::Variant(ref mut variant) = fragment { if variant.mid_stretched == Some(false) { variant.mid_stretched = Some(true); - scale(ctx, styles, fragment, height, Some(MathClass::Large)); + scale( + ctx, + styles, + fragment, + relative_to, + height, + Some(MathClass::Large), + ); } } } @@ -140,26 +140,27 @@ fn scale( ctx: &mut MathContext, styles: StyleChain, fragment: &mut MathFragment, - height: Abs, + relative_to: Abs, + height: Smart>, apply: Option, ) { if matches!( fragment.class(), MathClass::Opening | MathClass::Closing | MathClass::Fence ) { - let glyph = match fragment { - MathFragment::Glyph(glyph) => glyph.clone(), - MathFragment::Variant(variant) => { - GlyphFragment::new(ctx, styles, variant.c, variant.span) - } - _ => return, - }; + // This unwrap doesn't really matter. If it is None, then the fragment + // won't be stretchable anyways. + let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default()); + stretch_fragment( + ctx, + styles, + fragment, + Some(Axis::Y), + Some(relative_to), + height, + short_fall, + ); - let short_fall = DELIM_SHORT_FALL.at(glyph.font_size); - let mut stretched = glyph.stretch_vertical(ctx, height, short_fall); - stretched.align_on_axis(ctx, delimiter_alignment(stretched.c)); - - *fragment = MathFragment::Variant(stretched); if let Some(class) = apply { fragment.set_class(class); } diff --git a/crates/typst/src/math/matrix.rs b/crates/typst/src/math/matrix.rs index 5b9b17a7d..345c66050 100644 --- a/crates/typst/src/math/matrix.rs +++ b/crates/typst/src/math/matrix.rs @@ -11,9 +11,9 @@ use crate::layout::{ Rel, Size, }; use crate::math::{ - alignments, scaled_font_size, stack, style_for_denominator, AlignmentResult, - FrameFragment, GlyphFragment, LayoutMath, LeftRightAlternator, MathContext, Scaled, - DELIM_SHORT_FALL, + alignments, delimiter_alignment, scaled_font_size, stack, style_for_denominator, + AlignmentResult, FrameFragment, GlyphFragment, LayoutMath, LeftRightAlternator, + MathContext, Scaled, DELIM_SHORT_FALL, }; use crate::symbols::Symbol; use crate::syntax::{Span, Spanned}; @@ -21,8 +21,6 @@ use crate::text::TextElem; use crate::utils::Numeric; use crate::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape, Stroke}; -use super::delimiter_alignment; - const DEFAULT_ROW_GAP: Em = Em::new(0.2); const DEFAULT_COL_GAP: Em = Em::new(0.5); const VERTICAL_PADDING: Ratio = Ratio::new(0.1); diff --git a/crates/typst/src/math/mod.rs b/crates/typst/src/math/mod.rs index ab9989758..2c5c32cbf 100644 --- a/crates/typst/src/math/mod.rs +++ b/crates/typst/src/math/mod.rs @@ -34,6 +34,7 @@ pub use self::lr::*; pub use self::matrix::*; pub use self::op::*; pub use self::root::*; +pub use self::stretch::*; pub use self::style::*; pub use self::underover::*; @@ -44,7 +45,6 @@ use self::spacing::*; use crate::diag::SourceResult; use crate::foundations::{category, Category, Module, Scope, StyleChain}; -use crate::layout::VAlignment; use crate::text::TextElem; /// Typst has special [syntax]($syntax/#math) and library functions to typeset @@ -161,6 +161,7 @@ pub fn module() -> Module { math.define_elem::(); math.define_elem::(); math.define_elem::(); + math.define_elem::(); math.define_elem::(); math.define_elem::(); math.define_elem::(); @@ -217,11 +218,3 @@ pub trait LayoutMath { /// Layout the element, producing fragment in the context. fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()>; } - -fn delimiter_alignment(delimiter: char) -> VAlignment { - match delimiter { - '\u{231c}' | '\u{231d}' => VAlignment::Top, - '\u{231e}' | '\u{231f}' => VAlignment::Bottom, - _ => VAlignment::Horizon, - } -} diff --git a/crates/typst/src/math/stretch.rs b/crates/typst/src/math/stretch.rs index 749773e3c..d316fa9a2 100644 --- a/crates/typst/src/math/stretch.rs +++ b/crates/typst/src/math/stretch.rs @@ -1,12 +1,137 @@ use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; use ttf_parser::LazyArray16; -use crate::layout::{Abs, Frame, Point, Size}; -use crate::math::{GlyphFragment, MathContext, Scaled, VariantFragment}; +use crate::diag::SourceResult; +use crate::foundations::{elem, Content, Packed, Resolve, Smart, StyleChain}; +use crate::layout::{Abs, Axis, Frame, Length, Point, Rel, Size, VAlignment}; +use crate::math::{ + GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled, VariantFragment, +}; +use crate::utils::Get; /// Maximum number of times extenders can be repeated. const MAX_REPEATS: usize = 1024; +/// Stretches a glyph. +/// +/// This function can also be used to automatically stretch the base of an +/// attachment, so that it fits the top and bottom attachments. +/// +/// Note that only some glyphs can be stretched, and which ones can depend on +/// the math font being used. However, most math fonts are the same in this +/// regard. +/// +/// ```example +/// $ H stretch(=)^"define" U + p V $ +/// $ f : X stretch(->>, size: #150%)_"surjective" Y $ +/// $ x stretch(harpoons.ltrb, size: #3em) y +/// stretch(\[, size: #150%) z $ +/// ``` +#[elem(LayoutMath)] +pub struct StretchElem { + /// The glyph to stretch. + #[required] + pub body: Content, + + /// The size to stretch to, relative to the glyph's current size. + pub size: Smart>, +} + +impl LayoutMath for Packed { + #[typst_macros::time(name = "math.stretch", span = self.span())] + fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { + let mut fragment = ctx.layout_into_fragment(self.body(), styles)?; + stretch_fragment( + ctx, + styles, + &mut fragment, + None, + None, + self.size(styles), + Abs::zero(), + ); + ctx.push(fragment); + Ok(()) + } +} + +/// Attempts to stretch the given fragment by/to the amount given in stretch. +pub(super) fn stretch_fragment( + ctx: &mut MathContext, + styles: StyleChain, + fragment: &mut MathFragment, + axis: Option, + relative_to: Option, + stretch: Smart>, + short_fall: Abs, +) { + let glyph = match fragment { + MathFragment::Glyph(glyph) => glyph.clone(), + MathFragment::Variant(variant) => { + GlyphFragment::new(ctx, styles, variant.c, variant.span) + } + _ => return, + }; + + let Some(axis) = axis.or_else(|| stretch_axis(ctx, &glyph)) else { + return; + }; + + let relative_to_size = relative_to.unwrap_or_else(|| fragment.size().get(axis)); + + let mut variant = stretch_glyph( + ctx, + glyph, + stretch + .unwrap_or(Rel::one()) + .resolve(styles) + .relative_to(relative_to_size), + short_fall, + axis, + ); + + if axis == Axis::Y { + variant.align_on_axis(ctx, delimiter_alignment(variant.c)); + } + + *fragment = MathFragment::Variant(variant); +} + +pub(super) fn delimiter_alignment(delimiter: char) -> VAlignment { + match delimiter { + '\u{231c}' | '\u{231d}' => VAlignment::Top, + '\u{231e}' | '\u{231f}' => VAlignment::Bottom, + _ => VAlignment::Horizon, + } +} + +/// Return whether the glyph is stretchable and if it is, along which axis it +/// can be stretched. +fn stretch_axis(ctx: &MathContext, base: &GlyphFragment) -> Option { + let base_id = base.id; + let vertical = ctx + .table + .variants + .and_then(|variants| variants.vertical_constructions.get(base_id)) + .map(|_| Axis::Y); + let horizontal = ctx + .table + .variants + .and_then(|variants| variants.horizontal_constructions.get(base_id)) + .map(|_| Axis::X); + + match (vertical, horizontal) { + (vertical, None) => vertical, + (None, horizontal) => horizontal, + _ => { + // As far as we know, there aren't any glyphs that have both + // vertical and horizontal constructions. So for the time being, we + // will assume that a glyph cannot have both. + panic!("glyph {:?} has both vertical and horizontal constructions", base.c); + } + } +} + impl GlyphFragment { /// Try to stretch a glyph to a desired height. pub fn stretch_vertical( @@ -15,7 +140,7 @@ impl GlyphFragment { height: Abs, short_fall: Abs, ) -> VariantFragment { - stretch_glyph(ctx, self, height, short_fall, false) + stretch_glyph(ctx, self, height, short_fall, Axis::Y) } /// Try to stretch a glyph to a desired width. @@ -25,7 +150,7 @@ impl GlyphFragment { width: Abs, short_fall: Abs, ) -> VariantFragment { - stretch_glyph(ctx, self, width, short_fall, true) + stretch_glyph(ctx, self, width, short_fall, Axis::X) } } @@ -37,10 +162,13 @@ fn stretch_glyph( mut base: GlyphFragment, target: Abs, short_fall: Abs, - horizontal: bool, + axis: Axis, ) -> VariantFragment { // If the base glyph is good enough, use it. - let advance = if horizontal { base.width } else { base.height() }; + let advance = match axis { + Axis::X => base.width, + Axis::Y => base.height(), + }; let short_target = target - short_fall; if short_target <= advance { return base.into_variant(); @@ -52,10 +180,9 @@ fn stretch_glyph( .variants .and_then(|variants| { min_overlap = variants.min_connector_overlap.scaled(ctx, base.font_size); - if horizontal { - variants.horizontal_constructions - } else { - variants.vertical_constructions + match axis { + Axis::X => variants.horizontal_constructions, + Axis::Y => variants.vertical_constructions, } .get(base.id) }) @@ -80,7 +207,7 @@ fn stretch_glyph( // Assemble from parts. let assembly = construction.assembly.unwrap(); - assemble(ctx, base, assembly, min_overlap, target, horizontal) + assemble(ctx, base, assembly, min_overlap, target, axis) } /// Assemble a glyph from parts. @@ -90,7 +217,7 @@ fn assemble( assembly: GlyphAssembly, min_overlap: Abs, target: Abs, - horizontal: bool, + axis: Axis, ) -> VariantFragment { // Determine the number of times the extenders need to be repeated as well // as a ratio specifying how much to spread the parts apart @@ -153,15 +280,18 @@ fn assemble( let size; let baseline; - if horizontal { - let height = base.ascent + base.descent; - size = Size::new(full, height); - baseline = base.ascent; - } else { - let axis = ctx.constants.axis_height().scaled(ctx, base.font_size); - let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default(); - size = Size::new(width, full); - baseline = full / 2.0 + axis; + match axis { + Axis::X => { + let height = base.ascent + base.descent; + size = Size::new(full, height); + baseline = base.ascent; + } + Axis::Y => { + let axis = ctx.constants.axis_height().scaled(ctx, base.font_size); + let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default(); + size = Size::new(width, full); + baseline = full / 2.0 + axis; + } } let mut frame = Frame::soft(size); @@ -170,16 +300,18 @@ fn assemble( frame.post_process_raw(base.dests, base.hidden); for (fragment, advance) in selected { - let pos = if horizontal { - Point::new(offset, frame.baseline() - fragment.ascent) - } else { - Point::with_y(full - offset - fragment.height()) + let pos = match axis { + Axis::X => Point::new(offset, frame.baseline() - fragment.ascent), + Axis::Y => Point::with_y(full - offset - fragment.height()), }; frame.push_frame(pos, fragment.into_frame()); offset += advance; } - let accent_attach = if horizontal { frame.width() / 2.0 } else { base.accent_attach }; + let accent_attach = match axis { + Axis::X => frame.width() / 2.0, + Axis::Y => base.accent_attach, + }; VariantFragment { c: base.c, diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index 0be0fd5c3..3f2bef23e 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -83,6 +83,10 @@ automatically decides which is more suitable depending on the base, but you can also control this manually with the `scripts` and `limits` functions. + If you want the base to stretch to fit long top and bottom attachments (for + example, an arrow with text above it), use the [`stretch`]($math.stretch) + function. + # Example ```example $ sum_(i=0)^n a_i = 2^(1+i) $ diff --git a/tests/ref/math-stretch-attach-nested-equation.png b/tests/ref/math-stretch-attach-nested-equation.png new file mode 100644 index 0000000000000000000000000000000000000000..5ab4fbb513e92a82dc7da92e39c063021f64319f GIT binary patch literal 180 zcmeAS@N?(olHy`uVBq!ia0vp^6+kS<0VEg>Eo$`yQq`U=jv*Ddl7HAcG$dYm6xi*q zE4OvVIkEYbJjrX9uRkhzx<22kVDrbSt4lY$F4*I=} fi^ck*d}0|G^zOV)7iA1%2RX;n)z4*}Q$iB}v>s2W literal 0 HcmV?d00001 diff --git a/tests/ref/math-stretch-attach.png b/tests/ref/math-stretch-attach.png new file mode 100644 index 0000000000000000000000000000000000000000..179281a5afbdb2f3843c024fd101c1908c68003e GIT binary patch literal 2245 zcmV;$2s-zPP)e4`rmdZS#08)jJ?Cx9=wQzJ1^XJ6^|<9o%3C#2s-D zE8ZQpZ}=qAU*nN94LqJ^f=6+k{_1=Dy!2G+35Ew?ET3@lKDP4MAiY!BAPBnzVW;%E>;5`c5Vi}#jx-@YNl1wkgfGN5 zf{-W(yMH_lRpK>Cf)FRZjTeLjK}ZpVR6$4<1mD|KX(vfKn{xkyDT&f%vh=|e>7w{F zVb4x!Kk+BlNB%8?mVS-!OS%!>|M9zW!q0q)fR#gCPr;MO`&s5wP+8r-^}Am=mT~^2 z?|kJu=U(|qg2_SLp0#2EElOfAylGzkC_)N ztg`s=74H3g5je+5BN9zrF96P;J56gZA~9TRRU?_KDd)aT7NYBcbIyV(B-Q7~fV$#a z8`QG}v0yEj8lQibs9R9KfEW-u#__Q#@JKa-royb9G%qm9(sDu>qdvUdmYliN3{W zm}TR?2STY_c((H52mpGWj~O`m5x~zz!LHNj>UEc-Y0%9Byr{nV`V8O_Pu2RSwO*r@ z7P`wiJAhU3ry`JC*ZGXFtT;NvO7%M|t5o`Q0>ufqsn`t6Dvv3%#mNzmT%`qA`X#Ja z0}JA*1ImNSgQpH>pUCnp#81rvwV+M_NsZE{jb)oFLah96F3WCPd=`M|S1;A&ML7VE z#cVNJOui9UOMqq_{84&_iW?)AQOoE}qiJH)w-7($h2b^0J}`mgj>X3c68A6egjU&d zJkQQU4C#t3OIncF!R2PzMBedHniXTPvWpE?boXRsjIgr1vUwx-c>Y{?pX$&4WdbBh zD0ia8zn(!{XXshLFC{4Ow6P+*l^tU&Yc4obK*tCH>@82V&Z_b6oKmTx;Z_ipAuDMy z&lW2PpmITp{sa75tf;UV02_pMA-L=FCR8Yn~^7-qWyMLjXpz zblOV2l$zu1$r6nY#BnpQBc4gDi z1u!g2l_kqkmM8Twvs=~+G_Ip6s)S;KtbA?`3IMn_Z=DM`^Yhku>moqj7fbm7c*HZ} zhBRMiq{Q-}ako8WlA9`}N+BNNtX}y&TcR=oBtTpB_y605Ew$=o zZt^gzBp))pX1baQ+|^Z`#SeE{WpUQg7FIbDKl#srp(Mbm(dC4*GT{o&f|9Nom||kq zF+S@6OrP`8XwL#O+`VN`;787RINDTafhneD?dE$6fOQl9ZZ(Z_+dV(Im=Vs(7i~aE zZjw#7S)6dZ^R3P}fHkJF3@t@N+1*F|S8f^snsy%;-#?j`lhGR06a%nfy5e&!HEl;M zN#U*BejT(Wy$5GD_UKQTi6w6EcrUX3tZC)L9_>tk%B*_OHmS?>n|eBH+eviAnUA+q z^aX3QY|}N89Nx8Yt!(<8DHOM%s-lb zwz8=^yTG&g>Lw#F7mLiu^)uQVA9Q~b&Ps6MhZk>%mqmW}L@Fbdk;+Kr54`e!7TjW( T>#<@C00000NkvXXu0mjf;u%sz literal 0 HcmV?d00001 diff --git a/tests/ref/math-stretch-basic.png b/tests/ref/math-stretch-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..3144b11f238e361be3c29225eb33f8e9f0e4eb19 GIT binary patch literal 787 zcmV+u1MK{XP)q}E%0LO9v0{?)r3q?WrqE|*QB0{n-kd%Um6e1`usHM0KFB|6N#=o`=Kv@^CoM3Bf-S!iBjo z7v{pj37b-qnkrx5FRL}uvKDx;d^4-6c-H?g(&WGi1KenDn1ML2&pofhhlo9dPa5(4 zu*Eekf}Jg55kQ6*9+XnBBaZ^rg#Tf~erc;n4;NLfV~heLvKt;um&0!Kj4VM)`yYOu z3@5ULa5!BJpdkyu=8+A+NHCI#?0DjO9y4U*5yV*njN(Sf`0ziR{uC_Q0t&#J#EpJy zZaX|#6j{Nzpu?ji|HH4s>cJwSz@Sqz_17d6JSil>=m>p__LNq?!xnNIEF_#hj>nI8 zfrWL4nIwT(b1)8&bFn0MWO89H9JsIvZp>o)YVgCOa3=(y3}X28#9X0LvMt!o9c~(u zqED%R{IFLrDWS6m+DyeJW&3oYqO)r$;9)93(^j`S)|n5UC6&&ZZw7y`7ebqCIMbGc z{I5w;q!~mDEddWhKrhzmI@BWYkp;f7e5+_c38ArenFl_EsFxR)wDr#g9!uNW!TqB#4&rL1OE2g?9`}_!7NfMrHXv`)!RE&X*%AeW$=Vaxb z5!ubPZ0CG{*Yj&DL+Qh(R5&q0Q%Tv=%+L z;L^ARhvN+^n^t(UUIvyQn2ZK~`Wm(!>f!Nr?&F?w_v+ZtE_f$?2DXZlUA$xURp21( z8VHArX<#e>+Boc_Cm6Sh#N_*ziTEty4nYWxt%D`z-7dmqb@;LmM%P?d20NzST?OxW zZe1$O>U((PN`Z}xlB~HFnk`@~q*qoHQ}Al#C0JC%r6)A^Kjy+*m<#_W;Xm?9KC|f- Rv>yNf002ovPDHLkV1iZfZ$|(C literal 0 HcmV?d00001 diff --git a/tests/ref/math-stretch-complex.png b/tests/ref/math-stretch-complex.png new file mode 100644 index 0000000000000000000000000000000000000000..be694b71bb9c3088e241dde91cf14c6560118c56 GIT binary patch literal 1457 zcmV;i1y1^jP)G7U;eF_CM_d&P1hUEn9Xz{rB|bpElp5Ip_aB zE3iyfaA7XYg}HF-!#n)|jN<08IENkCK3GuP_hiO0V+Snp3YXnY@a38+#VvI$B?bAG zLUUP6!;inlp>Qjzjfn+Cw-`oj+7WIP4Sj|KMiKe;jfg*NJNEg#4(!V7(D z1j>U5$-^^Fih#=&L>=BIXPC8GyvWn{0hA$(LdZals(J(siLf>x`tT~nrJ6TE1b!9e zWaS*s&CJgzNH5GN%q(Pk&4~ccf$%$>$DGbt0Lp}Z%T5e@iDE<_p8Npl{0=PhW0TQj zoERrB!-Sr^Z2$x$svZQvBXN)*eWoYTuLV#f!|POJ%dqh0Av-lmL>vaa*gk z2-z{VJe$NwavQfTa$zp~4{6$5v^?ROE^y)hY}l|&VgKRyhUY6!T@*P1_W=#VSQ+LC zdUhm>Dw291#TEWi_a9hr&9ZTD9bG@3gr5ngX@0I$SR_r6 zN>kZ6LJ!>RKZvgo3J(QE%u>U|W-Ewk9nr7Tb?Js~=`;hn+jq$+8Dx%_1CZTr+Y%1X z%NCoJ{uWbN$o+>&VP95s)U2?dpR-w@2 z<&wQ1x*OI!3Wc>J!r$U@ANxB&JjgTimt_I*Dz5mrCG-Cg-cz+aVb{2wd)VW`|Jtyz zd*R$aGWv9WWYmucjE-3W{b(@KuqScb!f)mZR!+J;F9~4r=Br+N2yDC%aaesE&j_v@ zlTU&$77XH59w20O2UJ5qgkj6pWD5up!V1>u!=HGyGbXSkyLvkjaTwdpVAUb4=4}^P z(jJCkY>dOo*zAmjVI~)b-Nw$@7{;9V0EFWWC87wg!?UgpB*+lbO4GE6KAA;l7t(^W zbQ(IDDAwmQo z&Tbn+Gp`hjR1enuBZ7&@!;;fu%d>zKU&KqP7mkF-3w4~9OdA68>=Ayz2c-!%umZ6T z2Y9XxH!{5&ajLhqtg|k;+PTBJVfv7!z9hzB2yp~y5IPezy?cRTv0GVs*DvAb%v_ihmFBL4SHJ5TkqUc`@3#=WxZXo4D*{^DM-cc zX=FA?vu?|WoC5fAifyg!hnUnDhUa@uK1>H6LO@v8=jkq;w;$8_a|#{mt7%A!Ww@$6 z(9uTS6Hh_tzoNnQZeNFHh@Mql^N2NKQyeZXpI8~ot6IM4CLl<4A!66RPs&Qqi8}0l z3&0rnXY1YrcWF}UzjTF4$qcN@q#w>i1r^G);V?Y-xA?Hd$b2u3i15lj{QX+!YO6PRTf#_VFQG8PL-E;Ga2Fe&csz=;>2V|(12 zSLVMTJk)W;?IRXd$ww=+R$Nuf`m)9^V;>_J!Dox*kTKw$o^4~mz4rmE{a|`(F~b$; zw*mmK6PO2pdoD16rJJP3R)9X6F?-y!|5tsMX2o_#r@$Wz?c7>V-I#Wo{Dl_?f)U&% znx)9g0jJaXgnR$JMGagFIBWy{54n6kpPur$Y<-~y6>!#5(NumuFVX%S@{;5`~mDRtit zYxXT03@dJVNnVl{3#CG_SSZPhGLdDS!1BB}FV3ZNnOrU-rRPKovWvn)?b*Mpxr?0t^^1U?QRyL+PM$s29?-00000NkvXXu0mjf DXOU>4 literal 0 HcmV?d00001 diff --git a/tests/ref/math-stretch-shorthand.png b/tests/ref/math-stretch-shorthand.png new file mode 100644 index 0000000000000000000000000000000000000000..59db832adf3b347d7e30a9afbd204778de4209f8 GIT binary patch literal 375 zcmV--0f_#IP)H7F^uy>LPA#799LTp)M+v4kA`77^hSam%>35l7i7dp&m#uleAR9 zMWdoaqvkvUc8Crk*c{T_@bKO<9lxLOT6gvP z``23>@9wl)Y6Sa%n+*JsqhMxfH(9uHAp`qal7X#)5WG|ug2$hQ;1P-jTdy4ETe#L@ z!^08bR*9os4A+Y&a9$^%qYP|8qNKve+_$DF@~9Yi#m zc#MY5aj7HDZF9Z1iFkg7hK8DsrirLc-u3xcI~C&C3D*lA#e_#A!r0|#{$MPM3r{=> zU8ll{5RA)0FlD9S>ZuS+^PfWS?FPq_J!R*UfZNpSoo_s%Mv><}SRc}NbqfC<_zOk$ VjR3hwme&9P002ovPDHLkV1mrLr;Gpq literal 0 HcmV?d00001 diff --git a/tests/ref/math-stretch-vertical.png b/tests/ref/math-stretch-vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..1f60506d75b85e37b02b91986427114d90cba7b0 GIT binary patch literal 481 zcmV<70UrK|P)|2Cf)GMVxJrW| zVk(hhzwUJPoQ518JYdh~PG^v>djFj+2k?jX_=~59+`oK6?W(G`pKt*^i3&#IM#)DNb zrCtT$>Ik^ESu$=1!F=~92;YPN&hckh>=%ojW1*}1bwKDozHo^>xx}8;Qg0`r_p!Gq zw1FH+?Ob}j)#3jet{9dmY!Pc*0eAa=hQ~OVqG4q0p}b_ITL|YS%^~vxwt_VdgPHYL z(NJ5P0%gvbb=WeYSS#TLI6*kAgE|`pHFnCveli}b3j5YUZF<`j4uA@JbhyL9)BB(z zlWhstx+|bQyQ^&oKMy9r-47=)WxOmV;vTHEy^tdVylnXvZwcI9oAtT?tt(Q X3u|Uj;~%Tx00000NkvXXu0mjfsn*k? literal 0 HcmV?d00001 diff --git a/tests/suite/math/stretch.typ b/tests/suite/math/stretch.typ new file mode 100644 index 000000000..e6817ee5e --- /dev/null +++ b/tests/suite/math/stretch.typ @@ -0,0 +1,72 @@ +// Test math stretch. + +--- math-stretch-basic --- +// Test basic stretch. +$ P -> Q stretch(->, size: #200%) R \ + R stretch(->) S stretch(->, size: #50%)^"epimorphism" T $ + +--- math-stretch-complex --- +// Test complex stretch. +$ H stretch(=)^"define" U + p V \ + x stretch(harpoons.ltrb, size: #3em) y + stretch(\[, size: #150%) z \ + f : X stretch(arrow.hook, size: #150%)_"injective" Y \ + V stretch(->, size: #(100% + 1.5em))^("surjection") ZZ $ + +--- math-stretch-attach --- +// Test stretch interactions with attachments. +#set page(width: auto) + +$stretch(stretch(=, size: #4em))_A$ +$stretch(arrow.hook, size: #5em)^"injective map"$ +$stretch(arrow.hook, size: #200%)^"injective map"$ + +$ P = Q + stretch(=)^(k = 0)_(forall i) R + stretch(=, size: #150%)^(k = 0)_(forall i) S + stretch(=, size: #2mm)^(k = 0)_(forall i) T \ + U stretch(equiv)^(forall i)_"Chern-Weil" V + stretch(equiv, size: #(120% + 2mm))^(forall i)_"Chern-Weil" W $ + +--- math-stretch-horizontal --- +// Test stretching along horizontal axis. +#let ext(sym) = math.stretch(sym, size: 2em) +$ ext(arrow.r) quad ext(arrow.l.double.bar) \ + ext(harpoon.rb) quad ext(harpoons.ltrb) \ + ext(paren.t) quad ext(shell.b) \ + ext(eq) quad ext(equiv) $ + +--- math-stretch-vertical --- +// Test stretching along vertical axis. +#let ext(sym) = math.stretch(sym, size: 2em) +$ ext(bar.v) quad ext(bar.v.double) quad + ext(angle.l) quad ext(angle.r) quad + ext(paren.l) quad ext(paren.r) \ + ext(bracket.l.double) quad ext(bracket.r.double) quad + ext(brace.l) quad ext(brace.r) quad + ext(bracket.l) quad ext(bracket.r) $ + +--- math-stretch-shorthand --- +// Test stretch when base is given with shorthand. +$stretch(||, size: #2em)$ +$stretch(\(, size: #2em)$ +$stretch("⟧", size: #2em)$ +$stretch("|", size: #2em)$ +$stretch(->, size: #2em)$ +$stretch(↣, size: #2em)$ + +--- math-stretch-nested --- +// Test nested stretch calls. +$ stretch(=, size: #2em) \ + stretch(stretch(=, size: #4em), size: #50%) $ + +#let base = math.stretch($=$, size: 4em) +$ stretch(base, size: #50%) $ + +#let base = $stretch(=, size: #4em) $ +$ stretch(base, size: #50%) $ + +--- math-stretch-attach-nested-equation --- +// Test stretching with attachments when nested in an equation. +#let body = $stretch(=)$ +$ body^"text" $