Fix math spacing bugs

This commit is contained in:
Laurenz 2023-01-27 15:09:05 +01:00
parent a59b9fff93
commit 2e039cb052
19 changed files with 191 additions and 74 deletions

View File

@ -31,7 +31,7 @@ impl LayoutMath for AtomNode {
{ {
// A single letter that is available in the math font. // A single letter that is available in the math font.
if ctx.style.size == MathSize::Display if ctx.style.size == MathSize::Display
&& glyph.class() == Some(MathClass::Large) && glyph.class == Some(MathClass::Large)
{ {
let height = scaled!(ctx, display_operator_min_height); let height = scaled!(ctx, display_operator_min_height);
ctx.push(glyph.stretch_vertical(ctx, height, Abs::zero())); ctx.push(glyph.stretch_vertical(ctx, height, Abs::zero()));
@ -49,7 +49,8 @@ impl LayoutMath for AtomNode {
ctx.push(frame); ctx.push(frame);
} else { } else {
// Anything else is handled by Typst's standard text layout. // Anything else is handled by Typst's standard text layout.
TextNode(self.0.clone()).pack().layout_math(ctx)?; let frame = ctx.layout_non_math(&TextNode(self.0.clone()).pack())?;
ctx.push(FrameFragment::new(frame).with_class(MathClass::Alphabetic));
} }
Ok(()) Ok(())

View File

@ -30,6 +30,7 @@ pub(super) struct MathContext<'a, 'b, 'v> {
pub ttf: &'a ttf_parser::Face<'a>, pub ttf: &'a ttf_parser::Face<'a>,
pub table: ttf_parser::math::Table<'a>, pub table: ttf_parser::math::Table<'a>,
pub constants: ttf_parser::math::Constants<'a>, pub constants: ttf_parser::math::Constants<'a>,
pub space_width: Em,
pub fill: Paint, pub fill: Paint,
pub lang: Lang, pub lang: Lang,
pub row: MathRow, pub row: MathRow,
@ -50,6 +51,14 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
let table = font.ttf().tables().math.unwrap(); let table = font.ttf().tables().math.unwrap();
let constants = table.constants.unwrap(); let constants = table.constants.unwrap();
let size = styles.get(TextNode::SIZE); let size = styles.get(TextNode::SIZE);
let ttf = font.ttf();
let space_width = ttf
.glyph_index(' ')
.and_then(|id| ttf.glyph_hor_advance(id))
.map(|advance| font.to_em(advance))
.unwrap_or(THICK);
Self { Self {
vt, vt,
outer: styles, outer: styles,
@ -71,6 +80,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
ttf: font.ttf(), ttf: font.ttf(),
table, table,
constants, constants,
space_width,
row: MathRow::new(), row: MathRow::new(),
base_size: size, base_size: size,
scaled_size: size, scaled_size: size,
@ -79,7 +89,16 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
} }
pub fn push(&mut self, fragment: impl Into<MathFragment>) { pub fn push(&mut self, fragment: impl Into<MathFragment>) {
self.row.push(self.scaled_size, self.style, fragment); self.row
.push(self.scaled_size, self.space_width, self.style, fragment);
}
pub fn extend(&mut self, row: MathRow) {
let mut iter = row.0.into_iter();
if let Some(first) = iter.next() {
self.push(first);
}
self.row.0.extend(iter);
} }
pub fn layout_non_math(&mut self, content: &Content) -> SourceResult<Frame> { pub fn layout_non_math(&mut self, content: &Content) -> SourceResult<Frame> {

View File

@ -6,8 +6,9 @@ pub(super) enum MathFragment {
Variant(VariantFragment), Variant(VariantFragment),
Frame(FrameFragment), Frame(FrameFragment),
Spacing(Abs), Spacing(Abs),
Align, Space,
Linebreak, Linebreak,
Align,
} }
impl MathFragment { impl MathFragment {
@ -54,13 +55,26 @@ impl MathFragment {
pub fn class(&self) -> Option<MathClass> { pub fn class(&self) -> Option<MathClass> {
match self { match self {
Self::Glyph(glyph) => glyph.class(), Self::Glyph(glyph) => glyph.class,
Self::Variant(variant) => variant.class(), Self::Variant(variant) => variant.class,
Self::Frame(fragment) => Some(fragment.class), Self::Frame(fragment) => Some(fragment.class),
_ => None, _ => None,
} }
} }
pub fn set_class(&mut self, class: MathClass) {
match self {
Self::Glyph(glyph) => glyph.class = Some(class),
Self::Variant(variant) => variant.class = Some(class),
Self::Frame(fragment) => fragment.class = class,
_ => {}
}
}
pub fn participating(&self) -> bool {
!matches!(self, Self::Space | Self::Spacing(_) | Self::Align)
}
pub fn italics_correction(&self) -> Abs { pub fn italics_correction(&self) -> Abs {
match self { match self {
Self::Glyph(glyph) => glyph.italics_correction, Self::Glyph(glyph) => glyph.italics_correction,
@ -99,7 +113,7 @@ impl From<FrameFragment> for MathFragment {
impl From<Frame> for MathFragment { impl From<Frame> for MathFragment {
fn from(frame: Frame) -> Self { fn from(frame: Frame) -> Self {
Self::Frame(FrameFragment { frame, class: MathClass::Normal, limits: false }) Self::Frame(FrameFragment::new(frame))
} }
} }
@ -112,6 +126,7 @@ pub(super) struct GlyphFragment {
pub ascent: Abs, pub ascent: Abs,
pub descent: Abs, pub descent: Abs,
pub italics_correction: Abs, pub italics_correction: Abs,
pub class: Option<MathClass>,
} }
impl GlyphFragment { impl GlyphFragment {
@ -144,6 +159,10 @@ impl GlyphFragment {
ascent: bbox.y_max.scaled(ctx), ascent: bbox.y_max.scaled(ctx),
descent: -bbox.y_min.scaled(ctx), descent: -bbox.y_min.scaled(ctx),
italics_correction: italics, italics_correction: italics,
class: match c {
':' => Some(MathClass::Relation),
_ => unicode_math_class::class(c),
},
} }
} }
@ -151,16 +170,13 @@ impl GlyphFragment {
self.ascent + self.descent self.ascent + self.descent
} }
pub fn class(&self) -> Option<MathClass> {
unicode_math_class::class(self.c)
}
pub fn to_variant(&self, ctx: &MathContext) -> VariantFragment { pub fn to_variant(&self, ctx: &MathContext) -> VariantFragment {
VariantFragment { VariantFragment {
c: self.c, c: self.c,
id: Some(self.id), id: Some(self.id),
frame: self.to_frame(ctx), frame: self.to_frame(ctx),
italics_correction: self.italics_correction, italics_correction: self.italics_correction,
class: self.class,
} }
} }
@ -191,12 +207,7 @@ pub struct VariantFragment {
pub id: Option<GlyphId>, pub id: Option<GlyphId>,
pub frame: Frame, pub frame: Frame,
pub italics_correction: Abs, pub italics_correction: Abs,
} pub class: Option<MathClass>,
impl VariantFragment {
pub fn class(&self) -> Option<MathClass> {
unicode_math_class::class(self.c)
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -204,6 +215,30 @@ pub struct FrameFragment {
pub frame: Frame, pub frame: Frame,
pub class: MathClass, pub class: MathClass,
pub limits: bool, pub limits: bool,
pub spaced: bool,
}
impl FrameFragment {
pub fn new(frame: Frame) -> Self {
Self {
frame,
class: MathClass::Normal,
limits: false,
spaced: false,
}
}
pub fn with_class(self, class: MathClass) -> Self {
Self { class, ..self }
}
pub fn with_limits(self, limits: bool) -> Self {
Self { limits, ..self }
}
pub fn with_spaced(self, spaced: bool) -> Self {
Self { spaced, ..self }
}
} }
/// Look up the italics correction for a glyph. /// Look up the italics correction for a glyph.

View File

@ -69,9 +69,7 @@ impl LayoutMath for LrNode {
} }
} }
for fragment in row.0 { ctx.extend(row);
ctx.push(fragment);
}
Ok(()) Ok(())
} }

View File

@ -43,6 +43,7 @@ use self::row::*;
use self::spacing::*; use self::spacing::*;
use crate::layout::HNode; use crate::layout::HNode;
use crate::layout::ParNode; use crate::layout::ParNode;
use crate::layout::Spacing;
use crate::prelude::*; use crate::prelude::*;
use crate::text::LinebreakNode; use crate::text::LinebreakNode;
use crate::text::TextNode; use crate::text::TextNode;
@ -222,6 +223,7 @@ impl LayoutMath for FormulaNode {
impl LayoutMath for Content { impl LayoutMath for Content {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
if self.is::<SpaceNode>() { if self.is::<SpaceNode>() {
ctx.push(MathFragment::Space);
return Ok(()); return Ok(());
} }
@ -230,6 +232,17 @@ impl LayoutMath for Content {
return Ok(()); return Ok(());
} }
if let Some(node) = self.to::<HNode>() {
if let Spacing::Relative(rel) = node.amount {
if rel.rel.is_zero() {
ctx.push(MathFragment::Spacing(
rel.abs.resolve(ctx.outer.chain(&ctx.map)),
));
}
}
return Ok(());
}
if let Some(node) = self.to::<SequenceNode>() { if let Some(node) = self.to::<SequenceNode>() {
for child in &node.0 { for child in &node.0 {
child.layout_math(ctx)?; child.layout_math(ctx)?;
@ -242,7 +255,7 @@ impl LayoutMath for Content {
} }
let frame = ctx.layout_non_math(self)?; let frame = ctx.layout_non_math(self)?;
ctx.push(frame); ctx.push(FrameFragment::new(frame).with_spaced(true));
Ok(()) Ok(())
} }

View File

@ -39,11 +39,11 @@ impl OpNode {
impl LayoutMath for OpNode { impl LayoutMath for OpNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let frame = ctx.layout_non_math(&TextNode(self.text.clone()).pack())?; let frame = ctx.layout_non_math(&TextNode(self.text.clone()).pack())?;
ctx.push(FrameFragment { ctx.push(
frame, FrameFragment::new(frame)
class: MathClass::Large, .with_class(MathClass::Large)
limits: self.limits, .with_limits(self.limits),
}); );
Ok(()) Ok(())
} }
} }

View File

@ -15,36 +15,60 @@ impl MathRow {
pub fn push( pub fn push(
&mut self, &mut self,
font_size: Abs, font_size: Abs,
space_width: Em,
style: MathStyle, style: MathStyle,
fragment: impl Into<MathFragment>, fragment: impl Into<MathFragment>,
) { ) {
let fragment = fragment.into(); let mut fragment = fragment.into();
if let Some(fragment_class) = fragment.class() { if !fragment.participating() {
for (i, prev) in self.0.iter().enumerate().rev() { self.0.push(fragment);
if matches!(prev, MathFragment::Align) { return;
continue;
}
let mut amount = Abs::zero();
if let MathFragment::Glyph(glyph) = *prev {
if !glyph.italics_correction.is_zero()
&& fragment_class != MathClass::Alphabetic
{
amount += glyph.italics_correction;
}
}
if let Some(prev_class) = prev.class() {
amount += spacing(prev_class, fragment_class, style).at(font_size);
}
if !amount.is_zero() {
self.0.insert(i + 1, MathFragment::Spacing(amount));
}
break;
}
} }
let mut space = false;
for (i, prev) in self.0.iter().enumerate().rev() {
if !prev.participating() {
space |= matches!(prev, MathFragment::Space);
if matches!(prev, MathFragment::Spacing(_)) {
break;
}
continue;
}
if fragment.class() == Some(MathClass::Vary) {
if matches!(
prev.class(),
Some(
MathClass::Normal
| MathClass::Alphabetic
| MathClass::Binary
| MathClass::Closing
| MathClass::Fence
| MathClass::Relation
)
) {
fragment.set_class(MathClass::Binary);
}
}
let mut amount = Abs::zero();
if let MathFragment::Glyph(glyph) = *prev {
if !glyph.italics_correction.is_zero()
&& fragment.class() != Some(MathClass::Alphabetic)
{
amount += glyph.italics_correction;
}
}
amount += spacing(prev, &fragment, style, space, space_width).at(font_size);
if !amount.is_zero() {
self.0.insert(i + 1, MathFragment::Spacing(amount));
}
break;
}
self.0.push(fragment); self.0.push(fragment);
} }

View File

@ -139,7 +139,7 @@ fn scripts(
frame.push_frame(base_pos, base.to_frame(ctx)); frame.push_frame(base_pos, base.to_frame(ctx));
frame.push_frame(sub_pos, sub); frame.push_frame(sub_pos, sub);
frame.push_frame(sup_pos, sup); frame.push_frame(sup_pos, sup);
ctx.push(FrameFragment { frame, class, limits: false }); ctx.push(FrameFragment::new(frame).with_class(class));
Ok(()) Ok(())
} }
@ -172,7 +172,7 @@ fn limits(
frame.push_frame(base_pos, base.to_frame(ctx)); frame.push_frame(base_pos, base.to_frame(ctx));
frame.push_frame(sub_pos, sub); frame.push_frame(sub_pos, sub);
frame.push_frame(sup_pos, sup); frame.push_frame(sup_pos, sup);
ctx.push(FrameFragment { frame, class, limits: false }); ctx.push(FrameFragment::new(frame).with_class(class));
Ok(()) Ok(())
} }

View File

@ -1,10 +1,10 @@
use super::*; use super::*;
const ZERO: Em = Em::zero(); pub(super) const ZERO: Em = Em::zero();
const THIN: Em = Em::new(1.0 / 6.0); pub(super) const THIN: Em = Em::new(1.0 / 6.0);
const MEDIUM: Em = Em::new(2.0 / 9.0); pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0);
const THICK: Em = Em::new(5.0 / 18.0); pub(super) const THICK: Em = Em::new(5.0 / 18.0);
const QUAD: Em = Em::new(1.0); pub(super) const QUAD: Em = Em::new(1.0);
/// Hook up all spacings. /// Hook up all spacings.
pub(super) fn define(math: &mut Scope) { pub(super) fn define(math: &mut Scope) {
@ -15,10 +15,20 @@ pub(super) fn define(math: &mut Scope) {
} }
/// Determine the spacing between two fragments in a given style. /// Determine the spacing between two fragments in a given style.
pub(super) fn spacing(left: MathClass, right: MathClass, style: MathStyle) -> Em { pub(super) fn spacing(
left: &MathFragment,
right: &MathFragment,
style: MathStyle,
space: bool,
space_width: Em,
) -> Em {
use MathClass::*; use MathClass::*;
let script = style.size <= MathSize::Script; let script = style.size <= MathSize::Script;
match (left, right) { let (Some(l), Some(r)) = (left.class(), right.class()) else {
return ZERO;
};
match (l, r) {
// No spacing before punctuation; thin spacing after punctuation, unless // No spacing before punctuation; thin spacing after punctuation, unless
// in script size. // in script size.
(_, Punctuation) => ZERO, (_, Punctuation) => ZERO,
@ -33,12 +43,23 @@ pub(super) fn spacing(left: MathClass, right: MathClass, style: MathStyle) -> Em
(Relation, _) | (_, Relation) if !script => THICK, (Relation, _) | (_, Relation) if !script => THICK,
// Medium spacing around binary operators, unless in script size. // Medium spacing around binary operators, unless in script size.
(Vary | Binary, _) | (_, Vary | Binary) if !script => MEDIUM, (Binary, _) | (_, Binary) if !script => MEDIUM,
// Thin spacing around large operators, unless next to a delimiter. // Thin spacing around large operators, unless next to a delimiter.
(Large, Opening | Fence) | (Closing | Fence, Large) => ZERO, (Large, Opening | Fence) | (Closing | Fence, Large) => ZERO,
(Large, _) | (_, Large) => THIN, (Large, _) | (_, Large) => THIN,
// Spacing around spaced frames.
_ if space && (is_spaced(left) || is_spaced(right)) => space_width,
_ => ZERO, _ => ZERO,
} }
} }
/// Whether this fragment should react to adjacent spaces.
fn is_spaced(fragment: &MathFragment) -> bool {
match fragment {
MathFragment::Frame(frame) => frame.spaced,
_ => fragment.class() == Some(MathClass::Fence),
}
}

View File

@ -178,6 +178,7 @@ fn assemble(
id: None, id: None,
frame, frame,
italics_correction: Abs::zero(), italics_correction: Abs::zero(),
class: base.class,
} }
} }

View File

@ -209,7 +209,12 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
SyntaxKind::Unary => None, SyntaxKind::Unary => None,
SyntaxKind::Binary => None, SyntaxKind::Binary => None,
SyntaxKind::FieldAccess => match node.parent_kind() { SyntaxKind::FieldAccess => match node.parent_kind() {
Some(SyntaxKind::Markup | SyntaxKind::Math) => Some(Category::Interpolated), Some(
SyntaxKind::Markup
| SyntaxKind::Math
| SyntaxKind::MathFrac
| SyntaxKind::MathScript,
) => Some(Category::Interpolated),
Some(SyntaxKind::FieldAccess) => node.parent().and_then(highlight), Some(SyntaxKind::FieldAccess) => node.parent().and_then(highlight),
_ => None, _ => None,
}, },

View File

@ -9,7 +9,7 @@ use unicode_segmentation::UnicodeSegmentation;
use super::{ use super::{
methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Func, Label, methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Func, Label,
LangItems, Module, Recipe, Scopes, Selector, StyleMap, Transform, Value, LangItems, Module, Recipe, Scopes, Selector, StyleMap, Symbol, Transform, Value,
}; };
use crate::diag::{ use crate::diag::{
bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint, bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint,
@ -421,9 +421,7 @@ impl Eval for ast::Escape {
type Output = Value; type Output = Value;
fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
// This can be in markup and math, going through a string ensure Ok(Value::Symbol(Symbol::new(self.get())))
// that either text or atom is picked.
Ok(Value::Str(self.get().into()))
} }
} }
@ -431,9 +429,7 @@ impl Eval for ast::Shorthand {
type Output = Value; type Output = Value;
fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
// This can be in markup and math, going through a string ensure Ok(Value::Symbol(Symbol::new(self.get())))
// that either text or atom is picked.
Ok(Value::Str(self.get().into()))
} }
} }

View File

@ -151,7 +151,6 @@ impl Value {
Self::Int(v) => item!(math_atom)(format_eco!("{}", v)), Self::Int(v) => item!(math_atom)(format_eco!("{}", v)),
Self::Float(v) => item!(math_atom)(format_eco!("{}", v)), Self::Float(v) => item!(math_atom)(format_eco!("{}", v)),
Self::Symbol(v) => item!(math_atom)(v.get().into()), Self::Symbol(v) => item!(math_atom)(v.get().into()),
Self::Str(v) => item!(math_atom)(v.into()),
_ => self.display(), _ => self.display(),
} }
} }

View File

@ -308,7 +308,12 @@ fn math_delimited(p: &mut Parser, stop: MathClass) {
p.eat(); p.eat();
let m2 = p.marker(); let m2 = p.marker();
while !p.eof() && !p.at(SyntaxKind::Dollar) { while !p.eof() && !p.at(SyntaxKind::Dollar) {
if math_class(p.current_text()) == Some(stop) { let class = math_class(p.current_text());
if stop == MathClass::Fence && class == Some(MathClass::Closing) {
break;
}
if class == Some(stop) {
p.wrap(m2, SyntaxKind::Math); p.wrap(m2, SyntaxKind::Math);
p.eat(); p.eat();
p.wrap(m, SyntaxKind::MathDelimited); p.wrap(m, SyntaxKind::MathDelimited);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -12,8 +12,8 @@
``` ```
Let $x in NN$ be ... Let $x in NN$ be ...
$ (1 + x/2)^2 $ $ (1 + x/2)^2 $
$ x arrow:l y $ $ x arrow.l y $
$ sum_(n=1)^mu 1 + (2pi (5 + n)) / k $ $ sum_(n=1)^mu 1 + (2pi(5 + n)) / k $
$ { x in RR | x "is natural" and x < 10 } $ $ { x in RR | x "is natural" and x < 10 } $
$ sqrt(x^2) = frac(x, 1) $ $ sqrt(x^2) = frac(x, 1) $
$ "profit" = "income" - "expenses" $ $ "profit" = "income" - "expenses" $