More robust automatic math spacing

This commit is contained in:
Laurenz 2023-02-02 16:47:10 +01:00
parent e6400861ab
commit e9ff2d6463
21 changed files with 220 additions and 154 deletions

View File

@ -118,7 +118,7 @@ impl LayoutMath for AccentNode {
frame.set_baseline(baseline);
frame.push_frame(accent_pos, accent);
frame.push_frame(base_pos, base.to_frame(ctx));
ctx.push(frame);
ctx.push(FrameFragment::new(ctx, frame));
Ok(())
}

View File

@ -29,8 +29,7 @@ pub(super) fn alignments(rows: &[MathRow]) -> Vec<Abs> {
let count = rows
.iter()
.map(|row| {
row.0
.iter()
row.iter()
.filter(|fragment| matches!(fragment, MathFragment::Align))
.count()
})
@ -42,7 +41,7 @@ pub(super) fn alignments(rows: &[MathRow]) -> Vec<Abs> {
for row in rows {
let mut x = Abs::zero();
let mut i = 0;
for fragment in &row.0 {
for fragment in row.iter() {
if matches!(fragment, MathFragment::Align) {
if i < current {
x = points[i];

View File

@ -231,7 +231,7 @@ fn scripts(
frame.push_frame(sub_pos, sub);
}
ctx.push(FrameFragment::new(frame).with_class(class));
ctx.push(FrameFragment::new(ctx, frame).with_class(class));
Ok(())
}
@ -284,7 +284,7 @@ fn limits(
frame.push_frame(bottom_pos, bottom);
}
ctx.push(FrameFragment::new(frame).with_class(class));
ctx.push(FrameFragment::new(ctx, frame).with_class(class));
Ok(())
}

View File

@ -31,7 +31,7 @@ pub struct MathContext<'a, 'b, 'v> {
pub table: ttf_parser::math::Table<'a>,
pub constants: ttf_parser::math::Constants<'a>,
pub space_width: Em,
pub row: MathRow,
pub fragments: Vec<MathFragment>,
pub map: StyleMap,
pub style: MathStyle,
pub size: Abs,
@ -69,7 +69,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
table,
constants,
space_width,
row: MathRow::new(),
fragments: vec![],
map: StyleMap::new(),
style: MathStyle {
variant: MathVariant::Serif,
@ -88,45 +88,45 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
}
pub fn push(&mut self, fragment: impl Into<MathFragment>) {
self.row.push(self.size, self.space_width, self.style, fragment);
self.fragments.push(fragment.into());
}
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> {
Ok(content
.layout(&mut self.vt, self.outer.chain(&self.map), self.regions)?
.into_frame())
pub fn extend(&mut self, fragments: Vec<MathFragment>) {
self.fragments.extend(fragments);
}
pub fn layout_fragment(
&mut self,
node: &dyn LayoutMath,
) -> SourceResult<MathFragment> {
let row = self.layout_row(node)?;
Ok(if row.0.len() == 1 {
row.0.into_iter().next().unwrap()
} else {
row.to_frame(self).into()
})
let row = self.layout_fragments(node)?;
Ok(MathRow::new(row).to_fragment(self))
}
pub fn layout_fragments(
&mut self,
node: &dyn LayoutMath,
) -> SourceResult<Vec<MathFragment>> {
let prev = std::mem::take(&mut self.fragments);
node.layout_math(self)?;
Ok(std::mem::replace(&mut self.fragments, prev))
}
pub fn layout_row(&mut self, node: &dyn LayoutMath) -> SourceResult<MathRow> {
let prev = std::mem::take(&mut self.row);
node.layout_math(self)?;
Ok(std::mem::replace(&mut self.row, prev))
let fragments = self.layout_fragments(node)?;
Ok(MathRow::new(fragments))
}
pub fn layout_frame(&mut self, node: &dyn LayoutMath) -> SourceResult<Frame> {
Ok(self.layout_fragment(node)?.to_frame(self))
}
pub fn layout_content(&mut self, content: &Content) -> SourceResult<Frame> {
Ok(content
.layout(&mut self.vt, self.outer.chain(&self.map), self.regions)?
.into_frame())
}
pub fn layout_text(&mut self, text: &str) -> SourceResult<()> {
let mut chars = text.chars();
if let Some(glyph) = chars
@ -146,13 +146,13 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
}
} else if text.chars().all(|c| c.is_ascii_digit()) {
// Numbers aren't that difficult.
let mut vec = vec![];
let mut fragments = vec![];
for c in text.chars() {
let c = self.style.styled_char(c);
vec.push(GlyphFragment::new(self, c).into());
fragments.push(GlyphFragment::new(self, c).into());
}
let frame = MathRow(vec).to_frame(self);
self.push(frame);
let frame = MathRow::new(fragments).to_frame(self);
self.push(FrameFragment::new(self, frame));
} else {
// Anything else is handled by Typst's standard text layout.
let spaced = text.graphemes(true).count() > 1;
@ -161,9 +161,9 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
style = style.with_italic(false);
}
let text: EcoString = text.chars().map(|c| style.styled_char(c)).collect();
let frame = self.layout_non_math(&TextNode::packed(text))?;
let frame = self.layout_content(&TextNode::packed(text))?;
self.push(
FrameFragment::new(frame)
FrameFragment::new(self, frame)
.with_class(MathClass::Alphabetic)
.with_spaced(spaced),
);

View File

@ -60,10 +60,9 @@ impl LayoutMath for LrNode {
}
}
let mut row = ctx.layout_row(body)?;
let mut fragments = ctx.layout_fragments(body)?;
let axis = scaled!(ctx, axis_height);
let max_extent = row
.0
let max_extent = fragments
.iter()
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
.max()
@ -75,7 +74,7 @@ impl LayoutMath for LrNode {
.resolve(ctx.styles())
.relative_to(2.0 * max_extent);
match row.0.as_mut_slice() {
match fragments.as_mut_slice() {
[one] => scale(ctx, one, height, None),
[first, .., last] => {
scale(ctx, first, height, Some(MathClass::Opening));
@ -84,7 +83,7 @@ impl LayoutMath for LrNode {
_ => {}
}
ctx.extend(row);
ctx.extend(fragments);
Ok(())
}

View File

@ -153,7 +153,7 @@ fn layout(
if binom {
ctx.push(GlyphFragment::new(ctx, '(').stretch_vertical(ctx, height, short_fall));
ctx.push(frame);
ctx.push(FrameFragment::new(ctx, frame));
ctx.push(GlyphFragment::new(ctx, ')').stretch_vertical(ctx, height, short_fall));
} else {
frame.push(
@ -165,7 +165,7 @@ fn layout(
}),
),
);
ctx.push(frame);
ctx.push(FrameFragment::new(ctx, frame));
}
Ok(())

View File

@ -6,7 +6,7 @@ pub enum MathFragment {
Variant(VariantFragment),
Frame(FrameFragment),
Spacing(Abs),
Space,
Space(Abs),
Linebreak,
Align,
}
@ -22,6 +22,7 @@ impl MathFragment {
Self::Variant(variant) => variant.frame.width(),
Self::Frame(fragment) => fragment.frame.width(),
Self::Spacing(amount) => *amount,
Self::Space(amount) => *amount,
_ => Abs::zero(),
}
}
@ -62,6 +63,24 @@ impl MathFragment {
}
}
pub fn style(&self) -> Option<MathStyle> {
match self {
Self::Glyph(glyph) => Some(glyph.style),
Self::Variant(variant) => Some(variant.style),
Self::Frame(fragment) => Some(fragment.style),
_ => None,
}
}
pub fn font_size(&self) -> Option<Abs> {
match self {
Self::Glyph(glyph) => Some(glyph.font_size),
Self::Variant(variant) => Some(variant.font_size),
Self::Frame(fragment) => Some(fragment.font_size),
_ => None,
}
}
pub fn set_class(&mut self, class: MathClass) {
match self {
Self::Glyph(glyph) => glyph.class = Some(class),
@ -71,8 +90,11 @@ impl MathFragment {
}
}
pub fn participating(&self) -> bool {
!matches!(self, Self::Space | Self::Spacing(_) | Self::Align)
pub fn is_spaced(&self) -> bool {
match self {
MathFragment::Frame(frame) => frame.spaced,
_ => self.class() == Some(MathClass::Fence),
}
}
pub fn italics_correction(&self) -> Abs {
@ -111,23 +133,18 @@ impl From<FrameFragment> for MathFragment {
}
}
impl From<Frame> for MathFragment {
fn from(frame: Frame) -> Self {
Self::Frame(FrameFragment::new(frame))
}
}
#[derive(Debug, Clone, Copy)]
#[derive(Clone, Copy)]
pub struct GlyphFragment {
pub id: GlyphId,
pub c: char,
pub lang: Lang,
pub fill: Paint,
pub font_size: Abs,
pub width: Abs,
pub ascent: Abs,
pub descent: Abs,
pub italics_correction: Abs,
pub style: MathStyle,
pub font_size: Abs,
pub class: Option<MathClass>,
}
@ -163,6 +180,7 @@ impl GlyphFragment {
c,
lang: ctx.styles().get(TextNode::LANG),
fill: ctx.styles().get(TextNode::FILL),
style: ctx.style,
font_size: ctx.size,
width,
ascent: bbox.y_max.scaled(ctx),
@ -184,6 +202,8 @@ impl GlyphFragment {
c: self.c,
id: Some(self.id),
frame: self.to_frame(ctx),
style: self.style,
font_size: self.font_size,
italics_correction: self.italics_correction,
class: self.class,
}
@ -210,30 +230,48 @@ impl GlyphFragment {
}
}
#[derive(Debug, Clone)]
impl Debug for GlyphFragment {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "GlyphFragment({:?})", self.c)
}
}
#[derive(Clone)]
pub struct VariantFragment {
pub c: char,
pub id: Option<GlyphId>,
pub frame: Frame,
pub italics_correction: Abs,
pub frame: Frame,
pub style: MathStyle,
pub font_size: Abs,
pub class: Option<MathClass>,
}
impl Debug for VariantFragment {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "VariantFragment({:?})", self.c)
}
}
#[derive(Debug, Clone)]
pub struct FrameFragment {
pub frame: Frame,
pub class: MathClass,
pub limits: bool,
pub spaced: bool,
pub style: MathStyle,
pub font_size: Abs,
pub class: MathClass,
}
impl FrameFragment {
pub fn new(frame: Frame) -> Self {
pub fn new(ctx: &MathContext, frame: Frame) -> Self {
Self {
frame,
class: MathClass::Normal,
limits: false,
spaced: false,
font_size: ctx.size,
style: ctx.style,
class: MathClass::Normal,
}
}

View File

@ -307,7 +307,7 @@ fn layout_delimiters(
ctx.push(GlyphFragment::new(ctx, left).stretch_vertical(ctx, target, short_fall));
}
ctx.push(frame);
ctx.push(FrameFragment::new(ctx, frame));
if let Some(right) = right {
ctx.push(

View File

@ -261,7 +261,7 @@ impl LayoutMath for Content {
}
if self.is::<SpaceNode>() {
ctx.push(MathFragment::Space);
ctx.push(MathFragment::Space(ctx.space_width.scaled(ctx)));
return Ok(());
}
@ -288,8 +288,8 @@ impl LayoutMath for Content {
return node.layout_math(ctx);
}
let frame = ctx.layout_non_math(self)?;
ctx.push(FrameFragment::new(frame).with_spaced(true));
let frame = ctx.layout_content(self)?;
ctx.push(FrameFragment::new(ctx, frame).with_spaced(true));
Ok(())
}

View File

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

View File

@ -158,7 +158,7 @@ fn layout(
);
frame.push_frame(radicand_pos, radicand);
ctx.push(frame);
ctx.push(FrameFragment::new(ctx, frame));
Ok(())
}

View File

@ -5,55 +5,55 @@ use super::*;
pub const TIGHT_LEADING: Em = Em::new(0.25);
#[derive(Debug, Default, Clone)]
pub struct MathRow(pub Vec<MathFragment>);
pub struct MathRow(Vec<MathFragment>);
impl MathRow {
pub fn new() -> Self {
Self(vec![])
}
pub fn new(fragments: Vec<MathFragment>) -> Self {
let mut iter = fragments.into_iter().peekable();
let mut last: Option<usize> = None;
let mut space: Option<MathFragment> = None;
let mut resolved: Vec<MathFragment> = vec![];
pub fn width(&self) -> Abs {
self.0.iter().map(|fragment| fragment.width()).sum()
}
pub fn height(&self) -> Abs {
self.ascent() + self.descent()
}
pub fn ascent(&self) -> Abs {
self.0.iter().map(MathFragment::ascent).max().unwrap_or_default()
}
pub fn descent(&self) -> Abs {
self.0.iter().map(MathFragment::descent).max().unwrap_or_default()
}
pub fn push(
&mut self,
font_size: Abs,
space_width: Em,
style: MathStyle,
fragment: impl Into<MathFragment>,
) {
let mut fragment = fragment.into();
if !fragment.participating() {
self.0.push(fragment);
return;
}
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;
while let Some(mut fragment) = iter.next() {
match fragment {
// Keep space only if supported by spaced fragments.
MathFragment::Space(_) => {
if last.is_some() {
space = Some(fragment);
}
continue;
}
if fragment.class() == Some(MathClass::Vary) {
if matches!(
prev.class(),
// Explicit spacing disables automatic spacing.
MathFragment::Spacing(_) => {
last = None;
space = None;
resolved.push(fragment);
continue;
}
// Alignment points are resolved later.
MathFragment::Align => {
resolved.push(fragment);
continue;
}
// New line, new things.
MathFragment::Linebreak => {
resolved.push(fragment);
space = None;
last = None;
continue;
}
_ => {}
}
// Convert variable operators into binary operators if something
// precedes them.
if fragment.class() == Some(MathClass::Vary)
&& matches!(
last.and_then(|i| resolved[i].class()),
Some(
MathClass::Normal
| MathClass::Alphabetic
@ -62,22 +62,43 @@ impl MathRow {
| MathClass::Fence
| MathClass::Relation
)
) {
)
{
fragment.set_class(MathClass::Binary);
}
// Insert spacing between the last and this item.
if let Some(i) = last {
if let Some(s) = spacing(&resolved[i], space.take(), &fragment) {
resolved.insert(i + 1, s);
}
}
let mut amount = Abs::zero();
amount += spacing(prev, &fragment, style, space, space_width).at(font_size);
if !amount.is_zero() {
self.0.insert(i + 1, MathFragment::Spacing(amount));
last = Some(resolved.len());
resolved.push(fragment);
}
break;
Self(resolved)
}
self.0.push(fragment);
pub fn iter(&self) -> std::slice::Iter<'_, MathFragment> {
self.0.iter()
}
pub fn width(&self) -> Abs {
self.iter().map(MathFragment::width).sum()
}
pub fn height(&self) -> Abs {
self.ascent() + self.descent()
}
pub fn ascent(&self) -> Abs {
self.iter().map(MathFragment::ascent).max().unwrap_or_default()
}
pub fn descent(&self) -> Abs {
self.iter().map(MathFragment::descent).max().unwrap_or_default()
}
pub fn to_frame(self, ctx: &MathContext) -> Frame {
@ -86,14 +107,22 @@ impl MathRow {
self.to_aligned_frame(ctx, &[], align)
}
pub fn to_fragment(self, ctx: &MathContext) -> MathFragment {
if self.0.len() == 1 {
self.0.into_iter().next().unwrap()
} else {
FrameFragment::new(ctx, self.to_frame(ctx)).into()
}
}
pub fn to_aligned_frame(
mut self,
ctx: &MathContext,
points: &[Abs],
align: Align,
) -> Frame {
if self.0.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) {
let fragments = std::mem::take(&mut self.0);
if self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) {
let fragments: Vec<_> = std::mem::take(&mut self.0);
let leading = if ctx.style.size >= MathSize::Text {
ctx.styles().get(ParNode::LEADING)
} else {
@ -140,7 +169,7 @@ impl MathRow {
if let (Some(&first), Align::Center) = (points.first(), align) {
let mut offset = first;
for fragment in &self.0 {
for fragment in self.iter() {
offset -= fragment.width();
if matches!(fragment, MathFragment::Align) {
x = offset;

View File

@ -1,6 +1,5 @@
use super::*;
pub(super) const ZERO: Em = Em::zero();
pub(super) const THIN: Em = Em::new(1.0 / 6.0);
pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0);
pub(super) const THICK: Em = Em::new(5.0 / 18.0);
@ -14,49 +13,48 @@ pub(super) fn define(math: &mut Scope) {
math.define("quad", HNode::strong(QUAD).pack());
}
/// Determine the spacing between two fragments in a given style.
/// Create the spacing between two fragments in a given style.
pub(super) fn spacing(
left: &MathFragment,
right: &MathFragment,
style: MathStyle,
space: bool,
space_width: Em,
) -> Em {
l: &MathFragment,
space: Option<MathFragment>,
r: &MathFragment,
) -> Option<MathFragment> {
use MathClass::*;
let script = style.size <= MathSize::Script;
let class = |frag: &MathFragment| frag.class().unwrap_or(Special);
match (class(left), class(right)) {
let class = |f: &MathFragment| f.class().unwrap_or(Special);
let resolve = |v: Em, f: &MathFragment| {
Some(MathFragment::Spacing(f.font_size().map_or(Abs::zero(), |size| v.at(size))))
};
let script =
|f: &MathFragment| f.style().map_or(false, |s| s.size <= MathSize::Script);
match (class(l), class(r)) {
// No spacing before punctuation; thin spacing after punctuation, unless
// in script size.
(_, Punctuation) => ZERO,
(Punctuation, _) if !script => THIN,
(_, Punctuation) => None,
(Punctuation, _) if !script(l) => resolve(THIN, l),
// No spacing after opening delimiters and before closing delimiters.
(Opening, _) | (_, Closing) => ZERO,
(Opening, _) | (_, Closing) => None,
// Thick spacing around relations, unless followed by a another relation
// or in script size.
(Relation, Relation) => ZERO,
(Relation, _) | (_, Relation) if !script => THICK,
(Relation, Relation) => None,
(Relation, _) if !script(l) => resolve(THICK, l),
(_, Relation) if !script(r) => resolve(THICK, r),
// Medium spacing around binary operators, unless in script size.
(Binary, _) | (_, Binary) if !script => MEDIUM,
(Binary, _) if !script(l) => resolve(MEDIUM, l),
(_, Binary) if !script(r) => resolve(MEDIUM, r),
// Thin spacing around large operators, unless next to a delimiter.
(Large, Opening | Fence) | (Closing | Fence, Large) => ZERO,
(Large, _) | (_, Large) => THIN,
(Large, Opening | Fence) | (Closing | Fence, Large) => None,
(Large, _) => resolve(THIN, l),
(_, Large) => resolve(THIN, r),
// Spacing around spaced frames.
_ if space && (is_spaced(left) || is_spaced(right)) => space_width,
_ if (l.is_spaced() || r.is_spaced()) => space,
_ => 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),
_ => None,
}
}

View File

@ -177,6 +177,8 @@ fn assemble(
c: base.c,
id: None,
frame,
style: base.style,
font_size: base.font_size,
italics_correction: Abs::zero(),
class: base.class,
}

View File

@ -313,7 +313,7 @@ impl LayoutMath for BbNode {
}
}
/// The style in a formula.
/// Text properties in a formula.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct MathStyle {
/// The style variant to select.

View File

@ -270,7 +270,8 @@ fn layout(
baseline = rows.len() - 1;
}
ctx.push(stack(ctx, rows, Align::Center, gap, baseline));
let frame = stack(ctx, rows, Align::Center, gap, baseline);
ctx.push(FrameFragment::new(ctx, frame));
Ok(())
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB