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;
|
type Output = Content;
|
||||||
|
|
||||||
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
||||||
let num = self.num().eval_display(vm)?;
|
let num_expr = self.num();
|
||||||
let denom = self.denom().eval_display(vm)?;
|
let num = num_expr.eval_display(vm)?;
|
||||||
Ok(FracElem::new(num, denom).pack())
|
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::diag::SourceResult;
|
||||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
|
use typst_library::foundations::{
|
||||||
use typst_library::layout::{Em, Frame, FrameItem, Point, Size};
|
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
|
||||||
use typst_library::math::{BinomElem, EquationElem, FracElem, MathSize};
|
};
|
||||||
|
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::text::TextElem;
|
||||||
use typst_library::visualize::{FixedStroke, Geometry};
|
use typst_library::visualize::{FixedStroke, Geometry};
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
@ -20,14 +24,28 @@ pub fn layout_frac(
|
|||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
layout_frac_like(
|
match elem.style.get(styles) {
|
||||||
ctx,
|
FracStyle::Skewed => {
|
||||||
styles,
|
layout_skewed_frac(ctx, styles, &elem.num, &elem.denom, elem.span())
|
||||||
&elem.num,
|
}
|
||||||
std::slice::from_ref(&elem.denom),
|
FracStyle::Horizontal => layout_horizontal_frac(
|
||||||
false,
|
ctx,
|
||||||
elem.span(),
|
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`].
|
/// Lays out a [`BinomElem`].
|
||||||
@ -37,11 +55,11 @@ pub fn layout_binom(
|
|||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<()> {
|
) -> 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.
|
/// Layout a vertical fraction or binomial.
|
||||||
fn layout_frac_like(
|
fn layout_vertical_frac_like(
|
||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
num: &Content,
|
num: &Content,
|
||||||
@ -143,3 +161,119 @@ fn layout_frac_like(
|
|||||||
|
|
||||||
Ok(())
|
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 typst_syntax::Spanned;
|
||||||
|
|
||||||
use crate::diag::bail;
|
use crate::diag::bail;
|
||||||
use crate::foundations::{Content, Value, elem};
|
use crate::foundations::{Cast, Content, Value, elem};
|
||||||
use crate::math::Mathy;
|
use crate::math::Mathy;
|
||||||
|
|
||||||
/// A mathematical fraction.
|
/// A mathematical fraction.
|
||||||
@ -26,6 +26,43 @@ pub struct FracElem {
|
|||||||
/// The fraction's denominator.
|
/// The fraction's denominator.
|
||||||
#[required]
|
#[required]
|
||||||
pub denom: Content,
|
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.
|
/// A binomial expression.
|
||||||
|
@ -379,6 +379,10 @@ impl FontMetrics {
|
|||||||
.to_em(constants.fraction_denominator_gap_min().value),
|
.to_em(constants.fraction_denominator_gap_min().value),
|
||||||
fraction_denom_display_style_gap_min: font
|
fraction_denom_display_style_gap_min: font
|
||||||
.to_em(constants.fraction_denom_display_style_gap_min().value),
|
.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
|
overbar_vertical_gap: font
|
||||||
.to_em(constants.overbar_vertical_gap().value),
|
.to_em(constants.overbar_vertical_gap().value),
|
||||||
overbar_rule_thickness: font
|
overbar_rule_thickness: font
|
||||||
@ -413,6 +417,8 @@ impl FontMetrics {
|
|||||||
// - `flattened_accent_base_height` from Building Math Fonts
|
// - `flattened_accent_base_height` from Building Math Fonts
|
||||||
// - `overbar_rule_thickness` and `underbar_rule_thickness`
|
// - `overbar_rule_thickness` and `underbar_rule_thickness`
|
||||||
// from our best guess
|
// from our best guess
|
||||||
|
// - `skewed_fraction_vertical_gap` and `skewed_fraction_horizontal_gap`
|
||||||
|
// from our best guess
|
||||||
// - `script_percent_scale_down` and
|
// - `script_percent_scale_down` and
|
||||||
// `script_script_percent_scale_down` from Building Math
|
// `script_script_percent_scale_down` from Building Math
|
||||||
// Fonts as the defaults given in MathML Core have more
|
// Fonts as the defaults given in MathML Core have more
|
||||||
@ -458,6 +464,8 @@ impl FontMetrics {
|
|||||||
fraction_denominator_gap_min: metrics.underline.thickness,
|
fraction_denominator_gap_min: metrics.underline.thickness,
|
||||||
fraction_denom_display_style_gap_min: 3.0
|
fraction_denom_display_style_gap_min: 3.0
|
||||||
* metrics.underline.thickness,
|
* 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_vertical_gap: 3.0 * metrics.underline.thickness,
|
||||||
overbar_rule_thickness: metrics.underline.thickness,
|
overbar_rule_thickness: metrics.underline.thickness,
|
||||||
overbar_extra_ascender: metrics.underline.thickness,
|
overbar_extra_ascender: metrics.underline.thickness,
|
||||||
@ -553,6 +561,8 @@ pub struct MathConstants {
|
|||||||
pub fraction_rule_thickness: Em,
|
pub fraction_rule_thickness: Em,
|
||||||
pub fraction_denominator_gap_min: Em,
|
pub fraction_denominator_gap_min: Em,
|
||||||
pub fraction_denom_display_style_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_vertical_gap: Em,
|
||||||
pub overbar_rule_thickness: Em,
|
pub overbar_rule_thickness: Em,
|
||||||
pub overbar_extra_ascender: Em,
|
pub overbar_extra_ascender: Em,
|
||||||
|
@ -840,6 +840,16 @@ impl<'a> Math<'a> {
|
|||||||
pub fn exprs(self) -> impl DoubleEndedIterator<Item = Expr<'a>> {
|
pub fn exprs(self) -> impl DoubleEndedIterator<Item = Expr<'a>> {
|
||||||
self.0.children().filter_map(Expr::cast_with_space)
|
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! {
|
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 ---
|
--- math-frac-gap ---
|
||||||
// Test that the gap above and below the fraction rule is correct.
|
// Test that the gap above and below the fraction rule is correct.
|
||||||
$ sqrt(n^(2/3)) $
|
$ 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