mirror of
https://github.com/typst/typst
synced 2025-08-19 17:38:32 +08:00
Implement fraction styles: vertical, skewed, and horizontal. (#6672)
This commit is contained in:
parent
5ef94692ca
commit
f985d15a94
@ -123,9 +123,20 @@ impl Eval for ast::MathFrac<'_> {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
||||
let num = self.num().eval_display(vm)?;
|
||||
let denom = self.denom().eval_display(vm)?;
|
||||
Ok(FracElem::new(num, denom).pack())
|
||||
let num_expr = self.num();
|
||||
let num = num_expr.eval_display(vm)?;
|
||||
let denom_expr = self.denom();
|
||||
let denom = denom_expr.eval_display(vm)?;
|
||||
|
||||
let num_depar =
|
||||
matches!(num_expr, ast::Expr::Math(math) if math.was_deparenthesized());
|
||||
let denom_depar =
|
||||
matches!(denom_expr, ast::Expr::Math(math) if math.was_deparenthesized());
|
||||
|
||||
Ok(FracElem::new(num, denom)
|
||||
.with_num_deparenthesized(num_depar)
|
||||
.with_denom_deparenthesized(denom_depar)
|
||||
.pack())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,11 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
|
||||
use typst_library::layout::{Em, Frame, FrameItem, Point, Size};
|
||||
use typst_library::math::{BinomElem, EquationElem, FracElem, MathSize};
|
||||
use typst_library::foundations::{
|
||||
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
|
||||
};
|
||||
use typst_library::layout::{Abs, Em, Frame, FrameItem, Point, Size};
|
||||
use typst_library::math::{
|
||||
BinomElem, EquationElem, FracElem, FracStyle, LrElem, MathSize,
|
||||
};
|
||||
use typst_library::text::TextElem;
|
||||
use typst_library::visualize::{FixedStroke, Geometry};
|
||||
use typst_syntax::Span;
|
||||
@ -20,14 +24,28 @@ pub fn layout_frac(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
layout_frac_like(
|
||||
ctx,
|
||||
styles,
|
||||
&elem.num,
|
||||
std::slice::from_ref(&elem.denom),
|
||||
false,
|
||||
elem.span(),
|
||||
)
|
||||
match elem.style.get(styles) {
|
||||
FracStyle::Skewed => {
|
||||
layout_skewed_frac(ctx, styles, &elem.num, &elem.denom, elem.span())
|
||||
}
|
||||
FracStyle::Horizontal => layout_horizontal_frac(
|
||||
ctx,
|
||||
styles,
|
||||
&elem.num,
|
||||
&elem.denom,
|
||||
elem.span(),
|
||||
elem.num_deparenthesized.get(styles),
|
||||
elem.denom_deparenthesized.get(styles),
|
||||
),
|
||||
FracStyle::Vertical => layout_vertical_frac_like(
|
||||
ctx,
|
||||
styles,
|
||||
&elem.num,
|
||||
std::slice::from_ref(&elem.denom),
|
||||
false,
|
||||
elem.span(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lays out a [`BinomElem`].
|
||||
@ -37,11 +55,11 @@ pub fn layout_binom(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
layout_frac_like(ctx, styles, &elem.upper, &elem.lower, true, elem.span())
|
||||
layout_vertical_frac_like(ctx, styles, &elem.upper, &elem.lower, true, elem.span())
|
||||
}
|
||||
|
||||
/// Layout a fraction or binomial.
|
||||
fn layout_frac_like(
|
||||
/// Layout a vertical fraction or binomial.
|
||||
fn layout_vertical_frac_like(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
num: &Content,
|
||||
@ -143,3 +161,119 @@ fn layout_frac_like(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Lays out a horizontal fraction
|
||||
fn layout_horizontal_frac(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
num: &Content,
|
||||
denom: &Content,
|
||||
span: Span,
|
||||
num_deparen: bool,
|
||||
denom_deparen: bool,
|
||||
) -> SourceResult<()> {
|
||||
let num = if num_deparen {
|
||||
&LrElem::new(Content::sequence(vec![
|
||||
SymbolElem::packed('('),
|
||||
num.clone(),
|
||||
SymbolElem::packed(')'),
|
||||
]))
|
||||
.pack()
|
||||
} else {
|
||||
num
|
||||
};
|
||||
let num_frame = ctx.layout_into_fragment(num, styles)?;
|
||||
ctx.push(num_frame);
|
||||
|
||||
let mut slash =
|
||||
ctx.layout_into_fragment(&SymbolElem::packed('/').spanned(span), styles)?;
|
||||
slash.center_on_axis();
|
||||
ctx.push(slash);
|
||||
|
||||
let denom = if denom_deparen {
|
||||
&LrElem::new(Content::sequence(vec![
|
||||
SymbolElem::packed('('),
|
||||
denom.clone(),
|
||||
SymbolElem::packed(')'),
|
||||
]))
|
||||
.pack()
|
||||
} else {
|
||||
denom
|
||||
};
|
||||
let denom_frame = ctx.layout_into_fragment(denom, styles)?;
|
||||
ctx.push(denom_frame);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lay out a skewed fraction.
|
||||
fn layout_skewed_frac(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
num: &Content,
|
||||
denom: &Content,
|
||||
span: Span,
|
||||
) -> SourceResult<()> {
|
||||
// Font-derived constants
|
||||
let constants = ctx.font().math();
|
||||
let vgap = constants.skewed_fraction_vertical_gap.resolve(styles);
|
||||
let hgap = constants.skewed_fraction_horizontal_gap.resolve(styles);
|
||||
let axis = constants.axis_height.resolve(styles);
|
||||
|
||||
let num_style = style_for_numerator(styles);
|
||||
let num_frame = ctx.layout_into_frame(num, styles.chain(&num_style))?;
|
||||
let num_size = num_frame.size();
|
||||
let denom_style = style_for_denominator(styles);
|
||||
let denom_frame = ctx.layout_into_frame(denom, styles.chain(&denom_style))?;
|
||||
let denom_size = denom_frame.size();
|
||||
|
||||
let short_fall = DELIM_SHORT_FALL.resolve(styles);
|
||||
|
||||
// Height of the fraction frame
|
||||
// We recalculate this value below if the slash glyph overflows
|
||||
let mut fraction_height = num_size.y + denom_size.y + vgap;
|
||||
|
||||
// Build the slash glyph to calculate its size
|
||||
let mut slash_frag =
|
||||
ctx.layout_into_fragment(&SymbolElem::packed('\u{2044}').spanned(span), styles)?;
|
||||
slash_frag.stretch_vertical(ctx, fraction_height - short_fall);
|
||||
slash_frag.center_on_axis();
|
||||
let slash_frame = slash_frag.into_frame();
|
||||
|
||||
// Adjust the fraction height if the slash overflows
|
||||
let slash_size = slash_frame.size();
|
||||
let vertical_offset = Abs::zero().max(slash_size.y - fraction_height) / 2.0;
|
||||
fraction_height.set_max(slash_size.y);
|
||||
|
||||
// Reference points for all three objects, used to place them in the frame.
|
||||
let mut slash_up_left = Point::new(num_size.x + hgap / 2.0, fraction_height / 2.0)
|
||||
- slash_size.to_point() / 2.0;
|
||||
let mut num_up_left = Point::with_y(vertical_offset);
|
||||
let mut denom_up_left = num_up_left + num_size.to_point() + Point::new(hgap, vgap);
|
||||
|
||||
// Fraction width
|
||||
let fraction_width = (denom_up_left.x + denom_size.x)
|
||||
.max(slash_up_left.x + slash_size.x)
|
||||
+ Abs::zero().max(-slash_up_left.x);
|
||||
// We have to shift everything right to avoid going in the negatives for
|
||||
// the x coordinate
|
||||
let horizontal_offset = Point::with_x(Abs::zero().max(-slash_up_left.x));
|
||||
slash_up_left += horizontal_offset;
|
||||
num_up_left += horizontal_offset;
|
||||
denom_up_left += horizontal_offset;
|
||||
|
||||
// Build the final frame
|
||||
let mut fraction_frame = Frame::soft(Size::new(fraction_width, fraction_height));
|
||||
|
||||
// Baseline (use axis height to center slash on the axis)
|
||||
fraction_frame.set_baseline(fraction_height / 2.0 + axis);
|
||||
|
||||
// Numerator, Denominator, Slash
|
||||
fraction_frame.push_frame(num_up_left, num_frame);
|
||||
fraction_frame.push_frame(denom_up_left, denom_frame);
|
||||
fraction_frame.push_frame(slash_up_left, slash_frame);
|
||||
|
||||
ctx.push(FrameFragment::new(styles, fraction_frame));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use typst_syntax::Spanned;
|
||||
|
||||
use crate::diag::bail;
|
||||
use crate::foundations::{Content, Value, elem};
|
||||
use crate::foundations::{Cast, Content, Value, elem};
|
||||
use crate::math::Mathy;
|
||||
|
||||
/// A mathematical fraction.
|
||||
@ -26,6 +26,43 @@ pub struct FracElem {
|
||||
/// The fraction's denominator.
|
||||
#[required]
|
||||
pub denom: Content,
|
||||
|
||||
/// How the fraction shoud be laid out.
|
||||
///
|
||||
/// ```example
|
||||
/// #set math.frac(style: "skewed")
|
||||
/// $ a / b $
|
||||
/// $ frac(x, y, style: "vertical") $
|
||||
/// ```
|
||||
#[default(FracStyle::Vertical)]
|
||||
pub style: FracStyle,
|
||||
|
||||
/// Whether the numerator was originally surrounded by parentheses
|
||||
/// that were stripped by the parser.
|
||||
#[internal]
|
||||
#[parse(None)]
|
||||
#[default(false)]
|
||||
pub num_deparenthesized: bool,
|
||||
|
||||
/// Whether the denominator was originally surrounded by parentheses
|
||||
/// that were stripped by the parser.
|
||||
#[internal]
|
||||
#[parse(None)]
|
||||
#[default(false)]
|
||||
pub denom_deparenthesized: bool,
|
||||
}
|
||||
|
||||
/// Fraction style
|
||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||
pub enum FracStyle {
|
||||
/// Stacked numerator and denominator with a bar.
|
||||
#[default]
|
||||
Vertical,
|
||||
/// Numerator and denominator separated by a slash.
|
||||
Skewed,
|
||||
/// Numerator and denominator placed inline and parentheses are not
|
||||
/// absorbed.
|
||||
Horizontal,
|
||||
}
|
||||
|
||||
/// A binomial expression.
|
||||
|
@ -379,6 +379,10 @@ impl FontMetrics {
|
||||
.to_em(constants.fraction_denominator_gap_min().value),
|
||||
fraction_denom_display_style_gap_min: font
|
||||
.to_em(constants.fraction_denom_display_style_gap_min().value),
|
||||
skewed_fraction_vertical_gap: font
|
||||
.to_em(constants.skewed_fraction_vertical_gap().value),
|
||||
skewed_fraction_horizontal_gap: font
|
||||
.to_em(constants.skewed_fraction_horizontal_gap().value),
|
||||
overbar_vertical_gap: font
|
||||
.to_em(constants.overbar_vertical_gap().value),
|
||||
overbar_rule_thickness: font
|
||||
@ -413,6 +417,8 @@ impl FontMetrics {
|
||||
// - `flattened_accent_base_height` from Building Math Fonts
|
||||
// - `overbar_rule_thickness` and `underbar_rule_thickness`
|
||||
// from our best guess
|
||||
// - `skewed_fraction_vertical_gap` and `skewed_fraction_horizontal_gap`
|
||||
// from our best guess
|
||||
// - `script_percent_scale_down` and
|
||||
// `script_script_percent_scale_down` from Building Math
|
||||
// Fonts as the defaults given in MathML Core have more
|
||||
@ -458,6 +464,8 @@ impl FontMetrics {
|
||||
fraction_denominator_gap_min: metrics.underline.thickness,
|
||||
fraction_denom_display_style_gap_min: 3.0
|
||||
* metrics.underline.thickness,
|
||||
skewed_fraction_vertical_gap: Em::zero(),
|
||||
skewed_fraction_horizontal_gap: Em::new(0.5),
|
||||
overbar_vertical_gap: 3.0 * metrics.underline.thickness,
|
||||
overbar_rule_thickness: metrics.underline.thickness,
|
||||
overbar_extra_ascender: metrics.underline.thickness,
|
||||
@ -553,6 +561,8 @@ pub struct MathConstants {
|
||||
pub fraction_rule_thickness: Em,
|
||||
pub fraction_denominator_gap_min: Em,
|
||||
pub fraction_denom_display_style_gap_min: Em,
|
||||
pub skewed_fraction_vertical_gap: Em,
|
||||
pub skewed_fraction_horizontal_gap: Em,
|
||||
pub overbar_vertical_gap: Em,
|
||||
pub overbar_rule_thickness: Em,
|
||||
pub overbar_extra_ascender: Em,
|
||||
|
@ -840,6 +840,16 @@ impl<'a> Math<'a> {
|
||||
pub fn exprs(self) -> impl DoubleEndedIterator<Item = Expr<'a>> {
|
||||
self.0.children().filter_map(Expr::cast_with_space)
|
||||
}
|
||||
|
||||
/// Whether this `Math` node was originally parenthesized.
|
||||
pub fn was_deparenthesized(self) -> bool {
|
||||
let mut iter = self.0.children();
|
||||
matches!(iter.next().map(SyntaxNode::kind), Some(SyntaxKind::LeftParen))
|
||||
&& matches!(
|
||||
iter.next_back().map(SyntaxNode::kind),
|
||||
Some(SyntaxKind::RightParen)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
node! {
|
||||
|
BIN
tests/ref/math-frac-horizontal-explicit.png
Normal file
BIN
tests/ref/math-frac-horizontal-explicit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 458 B |
BIN
tests/ref/math-frac-horizontal-lr-paren.png
Normal file
BIN
tests/ref/math-frac-horizontal-lr-paren.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 320 B |
BIN
tests/ref/math-frac-horizontal-nonparen-brackets.png
Normal file
BIN
tests/ref/math-frac-horizontal-nonparen-brackets.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 346 B |
BIN
tests/ref/math-frac-horizontal.png
Normal file
BIN
tests/ref/math-frac-horizontal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 515 B |
BIN
tests/ref/math-frac-skewed.png
Normal file
BIN
tests/ref/math-frac-skewed.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 413 B |
BIN
tests/ref/math-frac-styles-inline.png
Normal file
BIN
tests/ref/math-frac-styles-inline.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 539 B |
@ -45,3 +45,34 @@ $ a_1/b_2, 1/f(x), zeta(x)/2, "foo"[|x|]/2 \
|
||||
--- math-frac-gap ---
|
||||
// Test that the gap above and below the fraction rule is correct.
|
||||
$ sqrt(n^(2/3)) $
|
||||
|
||||
--- math-frac-horizontal ---
|
||||
// Test that horizontal fractions look identical to inline math with `slash`
|
||||
#set math.frac(style: "horizontal")
|
||||
$ (a / b) / (c / (d / e)) $
|
||||
$ (a slash b) slash (c slash (d slash e)) $
|
||||
|
||||
--- math-frac-horizontal-lr-paren ---
|
||||
// Test that parentheses are in a left-right pair even when rebuilt by a horizontal fraction
|
||||
#set math.frac(style: "horizontal")
|
||||
$ (#v(2em)) / n $
|
||||
|
||||
--- math-frac-skewed ---
|
||||
// Test skewed fractions
|
||||
#set math.frac(style: "skewed")
|
||||
$ a / b, a / (b / c) $
|
||||
|
||||
--- math-frac-horizontal-explicit ---
|
||||
// Test that explicit fractions don't change parentheses
|
||||
#set math.frac(style: "horizontal")
|
||||
$ frac(a, (b + c)), frac(a, b + c) $
|
||||
|
||||
--- math-frac-horizontal-nonparen-brackets ---
|
||||
// Test that non-parentheses left-right pairs remain untouched
|
||||
#set math.frac(style: "horizontal")
|
||||
$ [x+y] / {z} $
|
||||
|
||||
--- math-frac-styles-inline ---
|
||||
// Test inline layout of styled fractions
|
||||
#set math.frac(style: "horizontal")
|
||||
$a/(b+c), frac(a, b+c, style: "skewed"), frac(a, b+c, style: "vertical")$
|
||||
|
Loading…
x
Reference in New Issue
Block a user