Add math.stretch
element function (#5030)
@ -1,15 +1,21 @@
|
|||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
use crate::diag::SourceResult;
|
use crate::diag::SourceResult;
|
||||||
use crate::foundations::{elem, Content, Packed, StyleChain};
|
use crate::foundations::{elem, Content, Packed, Smart, StyleChain};
|
||||||
use crate::layout::{Abs, Corner, Frame, Point, Size};
|
use crate::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size};
|
||||||
use crate::math::{
|
use crate::math::{
|
||||||
style_for_subscript, style_for_superscript, EquationElem, FrameFragment, LayoutMath,
|
stretch_fragment, style_for_subscript, style_for_superscript, EquationElem,
|
||||||
MathContext, MathFragment, MathSize, Scaled,
|
FrameFragment, LayoutMath, MathContext, MathFragment, MathSize, Scaled, StretchElem,
|
||||||
};
|
};
|
||||||
use crate::text::TextElem;
|
use crate::text::TextElem;
|
||||||
use crate::utils::OptionExt;
|
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.
|
/// A base with optional attachments.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
@ -55,8 +61,9 @@ impl LayoutMath for Packed<AttachElem> {
|
|||||||
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
|
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
|
||||||
let new_elem = merge_base(self);
|
let new_elem = merge_base(self);
|
||||||
let elem = new_elem.as_ref().unwrap_or(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 = style_for_superscript(styles);
|
||||||
let sup_style_chain = styles.chain(&sup_style);
|
let sup_style_chain = styles.chain(&sup_style);
|
||||||
let tl = elem.tl(sup_style_chain);
|
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 = [
|
let fragments = [
|
||||||
layout!(tl, sup_style_chain)?,
|
layout!(tl, sup_style_chain)?,
|
||||||
layout!(t, sup_style_chain)?,
|
t,
|
||||||
layout!(tr, sup_style_chain)?,
|
layout!(tr, sup_style_chain)?,
|
||||||
layout!(bl, sub_style_chain)?,
|
layout!(bl, sub_style_chain)?,
|
||||||
layout!(b, sub_style_chain)?,
|
b,
|
||||||
layout!(br, sub_style_chain)?,
|
layout!(br, sub_style_chain)?,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -288,10 +312,18 @@ fn merge_base(elem: &Packed<AttachElem>) -> Option<Packed<AttachElem>> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! measure {
|
/// Get the size to stretch the base to, if the attach argument is true.
|
||||||
($e: ident, $attr: ident) => {
|
fn stretch_size(
|
||||||
$e.as_ref().map(|e| e.$attr()).unwrap_or_default()
|
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.
|
/// Layout the attachments.
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
use crate::diag::SourceResult;
|
use crate::diag::SourceResult;
|
||||||
use crate::foundations::{
|
use crate::foundations::{elem, func, Content, NativeElement, Packed, Smart, StyleChain};
|
||||||
elem, func, Content, NativeElement, Packed, Resolve, Smart, StyleChain,
|
use crate::layout::{Abs, Axis, Em, Length, Rel};
|
||||||
};
|
use crate::math::{stretch_fragment, LayoutMath, MathContext, MathFragment, Scaled};
|
||||||
use crate::layout::{Abs, Em, Length, Rel};
|
|
||||||
use crate::math::{GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled};
|
|
||||||
use crate::text::TextElem;
|
use crate::text::TextElem;
|
||||||
|
|
||||||
use super::delimiter_alignment;
|
|
||||||
|
|
||||||
/// How much less high scaled delimiters can be than what they wrap.
|
/// How much less high scaled delimiters can be than what they wrap.
|
||||||
pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1);
|
pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1);
|
||||||
|
|
||||||
@ -55,18 +51,15 @@ impl LayoutMath for Packed<LrElem> {
|
|||||||
.max()
|
.max()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let height = self
|
let relative_to = 2.0 * max_extent;
|
||||||
.size(styles)
|
let height = self.size(styles);
|
||||||
.unwrap_or(Rel::one())
|
|
||||||
.resolve(styles)
|
|
||||||
.relative_to(2.0 * max_extent);
|
|
||||||
|
|
||||||
// Scale up fragments at both ends.
|
// Scale up fragments at both ends.
|
||||||
match fragments.as_mut_slice() {
|
match fragments.as_mut_slice() {
|
||||||
[one] => scale(ctx, styles, one, height, None),
|
[one] => scale(ctx, styles, one, relative_to, height, None),
|
||||||
[first, .., last] => {
|
[first, .., last] => {
|
||||||
scale(ctx, styles, first, height, Some(MathClass::Opening));
|
scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening));
|
||||||
scale(ctx, styles, last, height, Some(MathClass::Closing));
|
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 let MathFragment::Variant(ref mut variant) = fragment {
|
||||||
if variant.mid_stretched == Some(false) {
|
if variant.mid_stretched == Some(false) {
|
||||||
variant.mid_stretched = Some(true);
|
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,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
fragment: &mut MathFragment,
|
fragment: &mut MathFragment,
|
||||||
height: Abs,
|
relative_to: Abs,
|
||||||
|
height: Smart<Rel<Length>>,
|
||||||
apply: Option<MathClass>,
|
apply: Option<MathClass>,
|
||||||
) {
|
) {
|
||||||
if matches!(
|
if matches!(
|
||||||
fragment.class(),
|
fragment.class(),
|
||||||
MathClass::Opening | MathClass::Closing | MathClass::Fence
|
MathClass::Opening | MathClass::Closing | MathClass::Fence
|
||||||
) {
|
) {
|
||||||
let glyph = match fragment {
|
// This unwrap doesn't really matter. If it is None, then the fragment
|
||||||
MathFragment::Glyph(glyph) => glyph.clone(),
|
// won't be stretchable anyways.
|
||||||
MathFragment::Variant(variant) => {
|
let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default());
|
||||||
GlyphFragment::new(ctx, styles, variant.c, variant.span)
|
stretch_fragment(
|
||||||
}
|
ctx,
|
||||||
_ => return,
|
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 {
|
if let Some(class) = apply {
|
||||||
fragment.set_class(class);
|
fragment.set_class(class);
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,9 @@ use crate::layout::{
|
|||||||
Rel, Size,
|
Rel, Size,
|
||||||
};
|
};
|
||||||
use crate::math::{
|
use crate::math::{
|
||||||
alignments, scaled_font_size, stack, style_for_denominator, AlignmentResult,
|
alignments, delimiter_alignment, scaled_font_size, stack, style_for_denominator,
|
||||||
FrameFragment, GlyphFragment, LayoutMath, LeftRightAlternator, MathContext, Scaled,
|
AlignmentResult, FrameFragment, GlyphFragment, LayoutMath, LeftRightAlternator,
|
||||||
DELIM_SHORT_FALL,
|
MathContext, Scaled, DELIM_SHORT_FALL,
|
||||||
};
|
};
|
||||||
use crate::symbols::Symbol;
|
use crate::symbols::Symbol;
|
||||||
use crate::syntax::{Span, Spanned};
|
use crate::syntax::{Span, Spanned};
|
||||||
@ -21,8 +21,6 @@ use crate::text::TextElem;
|
|||||||
use crate::utils::Numeric;
|
use crate::utils::Numeric;
|
||||||
use crate::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape, Stroke};
|
use crate::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape, Stroke};
|
||||||
|
|
||||||
use super::delimiter_alignment;
|
|
||||||
|
|
||||||
const DEFAULT_ROW_GAP: Em = Em::new(0.2);
|
const DEFAULT_ROW_GAP: Em = Em::new(0.2);
|
||||||
const DEFAULT_COL_GAP: Em = Em::new(0.5);
|
const DEFAULT_COL_GAP: Em = Em::new(0.5);
|
||||||
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
|
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
|
||||||
|
@ -34,6 +34,7 @@ pub use self::lr::*;
|
|||||||
pub use self::matrix::*;
|
pub use self::matrix::*;
|
||||||
pub use self::op::*;
|
pub use self::op::*;
|
||||||
pub use self::root::*;
|
pub use self::root::*;
|
||||||
|
pub use self::stretch::*;
|
||||||
pub use self::style::*;
|
pub use self::style::*;
|
||||||
pub use self::underover::*;
|
pub use self::underover::*;
|
||||||
|
|
||||||
@ -44,7 +45,6 @@ use self::spacing::*;
|
|||||||
|
|
||||||
use crate::diag::SourceResult;
|
use crate::diag::SourceResult;
|
||||||
use crate::foundations::{category, Category, Module, Scope, StyleChain};
|
use crate::foundations::{category, Category, Module, Scope, StyleChain};
|
||||||
use crate::layout::VAlignment;
|
|
||||||
use crate::text::TextElem;
|
use crate::text::TextElem;
|
||||||
|
|
||||||
/// Typst has special [syntax]($syntax/#math) and library functions to typeset
|
/// 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::<LrElem>();
|
||||||
math.define_elem::<MidElem>();
|
math.define_elem::<MidElem>();
|
||||||
math.define_elem::<AttachElem>();
|
math.define_elem::<AttachElem>();
|
||||||
|
math.define_elem::<StretchElem>();
|
||||||
math.define_elem::<ScriptsElem>();
|
math.define_elem::<ScriptsElem>();
|
||||||
math.define_elem::<LimitsElem>();
|
math.define_elem::<LimitsElem>();
|
||||||
math.define_elem::<AccentElem>();
|
math.define_elem::<AccentElem>();
|
||||||
@ -217,11 +218,3 @@ pub trait LayoutMath {
|
|||||||
/// Layout the element, producing fragment in the context.
|
/// Layout the element, producing fragment in the context.
|
||||||
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()>;
|
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,12 +1,137 @@
|
|||||||
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
|
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
|
||||||
use ttf_parser::LazyArray16;
|
use ttf_parser::LazyArray16;
|
||||||
|
|
||||||
use crate::layout::{Abs, Frame, Point, Size};
|
use crate::diag::SourceResult;
|
||||||
use crate::math::{GlyphFragment, MathContext, Scaled, VariantFragment};
|
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.
|
/// Maximum number of times extenders can be repeated.
|
||||||
const MAX_REPEATS: usize = 1024;
|
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 {
|
impl GlyphFragment {
|
||||||
/// Try to stretch a glyph to a desired height.
|
/// Try to stretch a glyph to a desired height.
|
||||||
pub fn stretch_vertical(
|
pub fn stretch_vertical(
|
||||||
@ -15,7 +140,7 @@ impl GlyphFragment {
|
|||||||
height: Abs,
|
height: Abs,
|
||||||
short_fall: Abs,
|
short_fall: Abs,
|
||||||
) -> VariantFragment {
|
) -> 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.
|
/// Try to stretch a glyph to a desired width.
|
||||||
@ -25,7 +150,7 @@ impl GlyphFragment {
|
|||||||
width: Abs,
|
width: Abs,
|
||||||
short_fall: Abs,
|
short_fall: Abs,
|
||||||
) -> VariantFragment {
|
) -> 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,
|
mut base: GlyphFragment,
|
||||||
target: Abs,
|
target: Abs,
|
||||||
short_fall: Abs,
|
short_fall: Abs,
|
||||||
horizontal: bool,
|
axis: Axis,
|
||||||
) -> VariantFragment {
|
) -> VariantFragment {
|
||||||
// If the base glyph is good enough, use it.
|
// 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;
|
let short_target = target - short_fall;
|
||||||
if short_target <= advance {
|
if short_target <= advance {
|
||||||
return base.into_variant();
|
return base.into_variant();
|
||||||
@ -52,10 +180,9 @@ fn stretch_glyph(
|
|||||||
.variants
|
.variants
|
||||||
.and_then(|variants| {
|
.and_then(|variants| {
|
||||||
min_overlap = variants.min_connector_overlap.scaled(ctx, base.font_size);
|
min_overlap = variants.min_connector_overlap.scaled(ctx, base.font_size);
|
||||||
if horizontal {
|
match axis {
|
||||||
variants.horizontal_constructions
|
Axis::X => variants.horizontal_constructions,
|
||||||
} else {
|
Axis::Y => variants.vertical_constructions,
|
||||||
variants.vertical_constructions
|
|
||||||
}
|
}
|
||||||
.get(base.id)
|
.get(base.id)
|
||||||
})
|
})
|
||||||
@ -80,7 +207,7 @@ fn stretch_glyph(
|
|||||||
|
|
||||||
// Assemble from parts.
|
// Assemble from parts.
|
||||||
let assembly = construction.assembly.unwrap();
|
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.
|
/// Assemble a glyph from parts.
|
||||||
@ -90,7 +217,7 @@ fn assemble(
|
|||||||
assembly: GlyphAssembly,
|
assembly: GlyphAssembly,
|
||||||
min_overlap: Abs,
|
min_overlap: Abs,
|
||||||
target: Abs,
|
target: Abs,
|
||||||
horizontal: bool,
|
axis: Axis,
|
||||||
) -> VariantFragment {
|
) -> VariantFragment {
|
||||||
// Determine the number of times the extenders need to be repeated as well
|
// Determine the number of times the extenders need to be repeated as well
|
||||||
// as a ratio specifying how much to spread the parts apart
|
// as a ratio specifying how much to spread the parts apart
|
||||||
@ -153,15 +280,18 @@ fn assemble(
|
|||||||
|
|
||||||
let size;
|
let size;
|
||||||
let baseline;
|
let baseline;
|
||||||
if horizontal {
|
match axis {
|
||||||
let height = base.ascent + base.descent;
|
Axis::X => {
|
||||||
size = Size::new(full, height);
|
let height = base.ascent + base.descent;
|
||||||
baseline = base.ascent;
|
size = Size::new(full, height);
|
||||||
} else {
|
baseline = base.ascent;
|
||||||
let axis = ctx.constants.axis_height().scaled(ctx, base.font_size);
|
}
|
||||||
let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default();
|
Axis::Y => {
|
||||||
size = Size::new(width, full);
|
let axis = ctx.constants.axis_height().scaled(ctx, base.font_size);
|
||||||
baseline = full / 2.0 + axis;
|
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);
|
let mut frame = Frame::soft(size);
|
||||||
@ -170,16 +300,18 @@ fn assemble(
|
|||||||
frame.post_process_raw(base.dests, base.hidden);
|
frame.post_process_raw(base.dests, base.hidden);
|
||||||
|
|
||||||
for (fragment, advance) in selected {
|
for (fragment, advance) in selected {
|
||||||
let pos = if horizontal {
|
let pos = match axis {
|
||||||
Point::new(offset, frame.baseline() - fragment.ascent)
|
Axis::X => Point::new(offset, frame.baseline() - fragment.ascent),
|
||||||
} else {
|
Axis::Y => Point::with_y(full - offset - fragment.height()),
|
||||||
Point::with_y(full - offset - fragment.height())
|
|
||||||
};
|
};
|
||||||
frame.push_frame(pos, fragment.into_frame());
|
frame.push_frame(pos, fragment.into_frame());
|
||||||
offset += advance;
|
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 {
|
VariantFragment {
|
||||||
c: base.c,
|
c: base.c,
|
||||||
|
@ -83,6 +83,10 @@
|
|||||||
automatically decides which is more suitable depending on the base, but you
|
automatically decides which is more suitable depending on the base, but you
|
||||||
can also control this manually with the `scripts` and `limits` functions.
|
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
|
||||||
```example
|
```example
|
||||||
$ sum_(i=0)^n a_i = 2^(1+i) $
|
$ sum_(i=0)^n a_i = 2^(1+i) $
|
||||||
|
BIN
tests/ref/math-stretch-attach-nested-equation.png
Normal file
After Width: | Height: | Size: 180 B |
BIN
tests/ref/math-stretch-attach.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
tests/ref/math-stretch-basic.png
Normal file
After Width: | Height: | Size: 787 B |
BIN
tests/ref/math-stretch-complex.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
tests/ref/math-stretch-horizontal.png
Normal file
After Width: | Height: | Size: 591 B |
BIN
tests/ref/math-stretch-nested.png
Normal file
After Width: | Height: | Size: 253 B |
BIN
tests/ref/math-stretch-shorthand.png
Normal file
After Width: | Height: | Size: 375 B |
BIN
tests/ref/math-stretch-vertical.png
Normal file
After Width: | Height: | Size: 481 B |
72
tests/suite/math/stretch.typ
Normal 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" $
|