Add math.stretch element function (#5030)

This commit is contained in:
Max 2024-09-26 14:46:26 +00:00 committed by GitHub
parent a40e068590
commit e00e3e4fbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 312 additions and 80 deletions

View File

@ -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<AttachElem> {
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<AttachElem> {
};
}
// 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<AttachElem>) -> Option<Packed<AttachElem>> {
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<AttachElem>,
) -> Option<Smart<Rel<Length>>> {
// Extract from an EquationElem.
let mut base = elem.base();
if let Some(equation) = base.to_packed::<EquationElem>() {
base = equation.body();
}
base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles))
}
/// Layout the attachments.

View File

@ -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<LrElem> {
.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<LrElem> {
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<Rel<Length>>,
apply: Option<MathClass>,
) {
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);
}

View File

@ -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);

View File

@ -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::<LrElem>();
math.define_elem::<MidElem>();
math.define_elem::<AttachElem>();
math.define_elem::<StretchElem>();
math.define_elem::<ScriptsElem>();
math.define_elem::<LimitsElem>();
math.define_elem::<AccentElem>();
@ -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,
}
}

View File

@ -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<Rel<Length>>,
}
impl LayoutMath for Packed<StretchElem> {
#[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<Axis>,
relative_to: Option<Abs>,
stretch: Smart<Rel<Length>>,
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<Axis> {
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,

View File

@ -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) $

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

View File

@ -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" $