Inline equations linebreak at appropriate places (#2938)

Co-authored-by: David Maxwell <damaxwell@alaska.edu>
Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Myriad-Dreamin 2024-01-03 20:04:36 +08:00 committed by GitHub
parent 3aeb150c95
commit 34e3bd52aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 298 additions and 80 deletions

View File

@ -19,7 +19,7 @@ use crate::layout::{
Abs, AlignElem, Axes, BoxElem, Dir, Em, FixedAlign, Fr, Fragment, Frame, HElem, Abs, AlignElem, Axes, BoxElem, Dir, Em, FixedAlign, Fr, Fragment, Frame, HElem,
Layout, Point, Regions, Size, Sizing, Spacing, Layout, Point, Regions, Size, Sizing, Spacing,
}; };
use crate::math::EquationElem; use crate::math::{EquationElem, MathParItem};
use crate::model::{Linebreaks, ParElem}; use crate::model::{Linebreaks, ParElem};
use crate::syntax::Span; use crate::syntax::Span;
use crate::text::{ use crate::text::{
@ -61,7 +61,8 @@ pub(crate) fn layout_inline(
}; };
// Collect all text into one string for BiDi analysis. // Collect all text into one string for BiDi analysis.
let (text, segments, spans) = collect(children, &styles, consecutive)?; let (text, segments, spans) =
collect(children, &mut engine, &styles, region, consecutive)?;
// Perform BiDi analysis and then prepare paragraph layout by building a // Perform BiDi analysis and then prepare paragraph layout by building a
// representation on which we can do line breaking without layouting // representation on which we can do line breaking without layouting
@ -180,7 +181,7 @@ impl<'a> Preparation<'a> {
} }
/// A segment of one or multiple collapsed children. /// A segment of one or multiple collapsed children.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Clone)]
enum Segment<'a> { enum Segment<'a> {
/// One or multiple collapsed text or text-equivalent children. Stores how /// One or multiple collapsed text or text-equivalent children. Stores how
/// long the segment is (in bytes of the full text string). /// long the segment is (in bytes of the full text string).
@ -188,7 +189,7 @@ enum Segment<'a> {
/// Horizontal spacing between other segments. /// Horizontal spacing between other segments.
Spacing(Spacing), Spacing(Spacing),
/// A mathematical equation. /// A mathematical equation.
Equation(&'a EquationElem), Equation(&'a EquationElem, Vec<MathParItem>),
/// A box with arbitrary content. /// A box with arbitrary content.
Box(&'a BoxElem, bool), Box(&'a BoxElem, bool),
/// Metadata. /// Metadata.
@ -201,8 +202,12 @@ impl Segment<'_> {
match *self { match *self {
Self::Text(len) => len, Self::Text(len) => len,
Self::Spacing(_) => SPACING_REPLACE.len_utf8(), Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
Self::Box(_, true) => SPACING_REPLACE.len_utf8(), Self::Box(_, frac) => {
Self::Equation(_) | Self::Box(_, _) => OBJ_REPLACE.len_utf8(), (if frac { SPACING_REPLACE } else { OBJ_REPLACE }).len_utf8()
}
Self::Equation(_, ref par_items) => {
par_items.iter().map(MathParItem::text).map(char::len_utf8).sum()
}
Self::Meta => 0, Self::Meta => 0,
} }
} }
@ -395,12 +400,14 @@ impl<'a> Line<'a> {
} }
} }
/// Collect all text of the paragraph into one string. This also performs /// Collect all text of the paragraph into one string and layout equations. This
/// string-level preprocessing like case transformations. /// also performs string-level preprocessing like case transformations.
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn collect<'a>( fn collect<'a>(
children: &'a [Prehashed<Content>], children: &'a [Prehashed<Content>],
engine: &mut Engine<'_>,
styles: &'a StyleChain<'a>, styles: &'a StyleChain<'a>,
region: Size,
consecutive: bool, consecutive: bool,
) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>, SpanMapper)> { ) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>, SpanMapper)> {
let mut full = String::new(); let mut full = String::new();
@ -493,8 +500,10 @@ fn collect<'a>(
} }
Segment::Text(full.len() - prev) Segment::Text(full.len() - prev)
} else if let Some(elem) = child.to::<EquationElem>() { } else if let Some(elem) = child.to::<EquationElem>() {
full.push(OBJ_REPLACE); let pod = Regions::one(region, Axes::splat(false));
Segment::Equation(elem) let items = elem.layout_inline(engine, styles, pod)?;
full.extend(items.iter().map(MathParItem::text));
Segment::Equation(elem, items)
} else if let Some(elem) = child.to::<BoxElem>() { } else if let Some(elem) = child.to::<BoxElem>() {
let frac = elem.width(styles).is_fractional(); let frac = elem.width(styles).is_fractional();
full.push(if frac { SPACING_REPLACE } else { OBJ_REPLACE }); full.push(if frac { SPACING_REPLACE } else { OBJ_REPLACE });
@ -512,7 +521,7 @@ fn collect<'a>(
spans.push(segment.len(), child.span()); spans.push(segment.len(), child.span());
if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) = if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) =
(segments.last_mut(), segment) (segments.last_mut(), &segment)
{ {
if *last_styles == styles { if *last_styles == styles {
*last_len += len; *last_len += len;
@ -526,8 +535,7 @@ fn collect<'a>(
Ok((full, segments, spans)) Ok((full, segments, spans))
} }
/// Prepare paragraph layout by shaping the whole paragraph and layouting all /// Prepare paragraph layout by shaping the whole paragraph.
/// contained inline-level content.
fn prepare<'a>( fn prepare<'a>(
engine: &mut Engine, engine: &mut Engine,
children: &'a [Prehashed<Content>], children: &'a [Prehashed<Content>],
@ -566,11 +574,16 @@ fn prepare<'a>(
items.push(Item::Fractional(v, None)); items.push(Item::Fractional(v, None));
} }
}, },
Segment::Equation(equation) => { Segment::Equation(_, par_items) => {
let pod = Regions::one(region, Axes::splat(false)); for item in par_items {
let mut frame = equation.layout(engine, styles, pod)?.into_frame(); match item {
frame.translate(Point::with_y(TextElem::baseline_in(styles))); MathParItem::Space(s) => items.push(Item::Absolute(s)),
items.push(Item::Frame(frame)); MathParItem::Frame(mut frame) => {
frame.translate(Point::with_y(TextElem::baseline_in(styles)));
items.push(Item::Frame(frame));
}
}
}
} }
Segment::Box(elem, _) => { Segment::Box(elem, _) => {
if let Sizing::Fr(v) = elem.width(styles) { if let Sizing::Fr(v) = elem.width(styles) {

View File

@ -140,6 +140,11 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
self.fragments.extend(fragments); self.fragments.extend(fragments);
} }
pub fn layout_root(&mut self, elem: &dyn LayoutMath) -> SourceResult<MathRow> {
let row = self.layout_fragments(elem)?;
Ok(MathRow::new(row))
}
pub fn layout_fragment( pub fn layout_fragment(
&mut self, &mut self,
elem: &dyn LayoutMath, elem: &dyn LayoutMath,

View File

@ -8,13 +8,14 @@ use crate::foundations::{
}; };
use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
use crate::layout::{ use crate::layout::{
Abs, Align, AlignElem, Axes, Dir, Em, FixedAlign, Fragment, Layout, Point, Regions, Abs, Align, AlignElem, Axes, Dir, Em, FixedAlign, Fragment, Frame, Layout, Point,
Size, Regions, Size,
}; };
use crate::math::{LayoutMath, MathContext}; use crate::math::{LayoutMath, MathContext};
use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement}; use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement};
use crate::syntax::Span;
use crate::text::{ use crate::text::{
families, variant, FontFamily, FontList, FontWeight, Lang, LocalName, Region, families, variant, Font, FontFamily, FontList, FontWeight, Lang, LocalName, Region,
TextElem, TextElem,
}; };
use crate::util::{option_eq, NonZeroExt, Numeric}; use crate::util::{option_eq, NonZeroExt, Numeric};
@ -136,70 +137,47 @@ impl Finalize for EquationElem {
} }
} }
impl Layout for EquationElem { /// Layouted items suitable for placing in a paragraph.
#[typst_macros::time(name = "math.equation", span = self.span())] #[derive(Debug, Clone)]
fn layout( pub enum MathParItem {
Space(Abs),
Frame(Frame),
}
impl MathParItem {
/// The text representation of this item.
pub fn text(&self) -> char {
match self {
MathParItem::Space(_) => ' ', // Space
MathParItem::Frame(_) => '\u{FFFC}', // Object Replacement Character
}
}
}
impl EquationElem {
pub fn layout_inline(
&self, &self,
engine: &mut Engine, engine: &mut Engine<'_>,
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Vec<MathParItem>> {
const NUMBER_GUTTER: Em = Em::new(0.5); assert!(!self.block(styles));
let block = self.block(styles);
// Find a math font. // Find a math font.
let variant = variant(styles); let font = find_math_font(engine, styles, self.span())?;
let world = engine.world;
let Some(font) = families(styles).find_map(|family| { let mut ctx = MathContext::new(engine, styles, regions, &font, false);
let id = world.book().select(family, variant)?; let rows = ctx.layout_root(self)?;
let font = world.font(id)?;
let _ = font.ttf().tables().math?.constants?; let mut items = if rows.row_count() == 1 {
Some(font) rows.into_par_items()
}) else { } else {
bail!(self.span(), "current font does not support math"); vec![MathParItem::Frame(rows.into_fragment(&ctx).into_frame())]
}; };
let mut ctx = MathContext::new(engine, styles, regions, &font, block); for item in &mut items {
let mut frame = ctx.layout_frame(self)?; let MathParItem::Frame(frame) = item else { continue };
if block {
if let Some(numbering) = self.numbering(styles) {
let pod = Regions::one(regions.base(), Axes::splat(false));
let counter = Counter::of(Self::elem())
.display(self.span(), Some(numbering), false)
.layout(engine, styles, pod)?
.into_frame();
let full_counter_width = counter.width() + NUMBER_GUTTER.resolve(styles);
let width = if regions.size.x.is_finite() {
regions.size.x
} else {
frame.width() + 2.0 * full_counter_width
};
let height = frame.height().max(counter.height());
let align = AlignElem::alignment_in(styles).resolve(styles).x;
frame.resize(Size::new(width, height), Axes::splat(align));
let dir = TextElem::dir_in(styles);
let offset = match (align, dir) {
(FixedAlign::Start, Dir::RTL) => full_counter_width,
(FixedAlign::End, Dir::LTR) => -full_counter_width,
_ => Abs::zero(),
};
frame.translate(Point::with_x(offset));
let x = if dir.is_positive() {
frame.width() - counter.width()
} else {
Abs::zero()
};
let y = (frame.height() - counter.height()) / 2.0;
frame.push_frame(Point::new(x, y), counter)
}
} else {
let font_size = TextElem::size_in(styles); let font_size = TextElem::size_in(styles);
let slack = ParElem::leading_in(styles) * 0.7; let slack = ParElem::leading_in(styles) * 0.7;
let top_edge = TextElem::top_edge_in(styles).resolve(font_size, &font, None); let top_edge = TextElem::top_edge_in(styles).resolve(font_size, &font, None);
@ -210,6 +188,67 @@ impl Layout for EquationElem {
let descent = bottom_edge.max(frame.descent() - slack); let descent = bottom_edge.max(frame.descent() - slack);
frame.translate(Point::with_y(ascent - frame.baseline())); frame.translate(Point::with_y(ascent - frame.baseline()));
frame.size_mut().y = ascent + descent; frame.size_mut().y = ascent + descent;
// Apply metadata.
frame.meta(styles, false);
}
Ok(items)
}
}
impl Layout for EquationElem {
#[typst_macros::time(name = "math.equation", span = self.span())]
fn layout(
&self,
engine: &mut Engine,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
const NUMBER_GUTTER: Em = Em::new(0.5);
assert!(self.block(styles));
// Find a math font.
let font = find_math_font(engine, styles, self.span())?;
let mut ctx = MathContext::new(engine, styles, regions, &font, true);
let mut frame = ctx.layout_frame(self)?;
if let Some(numbering) = self.numbering(styles) {
let pod = Regions::one(regions.base(), Axes::splat(false));
let counter = Counter::of(Self::elem())
.display(self.span(), Some(numbering), false)
.layout(engine, styles, pod)?
.into_frame();
let full_counter_width = counter.width() + NUMBER_GUTTER.resolve(styles);
let width = if regions.size.x.is_finite() {
regions.size.x
} else {
frame.width() + 2.0 * full_counter_width
};
let height = frame.height().max(counter.height());
let align = AlignElem::alignment_in(styles).resolve(styles).x;
frame.resize(Size::new(width, height), Axes::splat(align));
let dir = TextElem::dir_in(styles);
let offset = match (align, dir) {
(FixedAlign::Start, Dir::RTL) => full_counter_width,
(FixedAlign::End, Dir::LTR) => -full_counter_width,
_ => Abs::zero(),
};
frame.translate(Point::with_x(offset));
let x = if dir.is_positive() {
frame.width() - counter.width()
} else {
Abs::zero()
};
let y = (frame.height() - counter.height()) / 2.0;
frame.push_frame(Point::new(x, y), counter)
} }
// Apply metadata. // Apply metadata.
@ -316,3 +355,21 @@ impl LayoutMath for EquationElem {
self.body().layout_math(ctx) self.body().layout_math(ctx)
} }
} }
fn find_math_font(
engine: &mut Engine<'_>,
styles: StyleChain,
span: Span,
) -> SourceResult<Font> {
let variant = variant(styles);
let world = engine.world;
let Some(font) = families(styles).find_map(|family| {
let id = world.book().select(family, variant)?;
let font = world.font(id)?;
let _ = font.ttf().tables().math?.constants?;
Some(font)
}) else {
bail!(span, "current font does not support math");
};
Ok(font)
}

View File

@ -3,10 +3,10 @@ use std::iter::once;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use crate::foundations::Resolve; use crate::foundations::Resolve;
use crate::layout::{Abs, AlignElem, Em, FixedAlign, Frame, Point, Size}; use crate::layout::{Abs, AlignElem, Em, FixedAlign, Frame, FrameKind, Point, Size};
use crate::math::{ use crate::math::{
alignments, spacing, AlignmentResult, FrameFragment, MathContext, MathFragment, alignments, spacing, AlignmentResult, FrameFragment, MathContext, MathFragment,
MathSize, Scaled, MathParItem, MathSize, Scaled,
}; };
use crate::model::ParElem; use crate::model::ParElem;
@ -103,6 +103,19 @@ impl MathRow {
.collect() .collect()
} }
pub fn row_count(&self) -> usize {
let mut count =
1 + self.0.iter().filter(|f| matches!(f, MathFragment::Linebreak)).count();
// A linebreak at the very end does not introduce an extra row.
if let Some(f) = self.0.last() {
if matches!(f, MathFragment::Linebreak) {
count -= 1
}
}
count
}
pub fn ascent(&self) -> Abs { pub fn ascent(&self) -> Abs {
self.iter().map(MathFragment::ascent).max().unwrap_or_default() self.iter().map(MathFragment::ascent).max().unwrap_or_default()
} }
@ -239,6 +252,85 @@ impl MathRow {
frame.size_mut().x = x; frame.size_mut().x = x;
frame frame
} }
pub fn into_par_items(self) -> Vec<MathParItem> {
let mut items = vec![];
let mut x = Abs::zero();
let mut ascent = Abs::zero();
let mut descent = Abs::zero();
let mut frame = Frame::new(Size::zero(), FrameKind::Soft);
let finalize_frame = |frame: &mut Frame, x, ascent, descent| {
frame.set_size(Size::new(x, ascent + descent));
frame.set_baseline(Abs::zero());
frame.translate(Point::with_y(ascent));
};
let mut space_is_visible = false;
let is_relation =
|f: &MathFragment| matches!(f.class(), Some(MathClass::Relation));
let is_space = |f: &MathFragment| {
matches!(f, MathFragment::Space(_) | MathFragment::Spacing(_))
};
let mut iter = self.0.into_iter().peekable();
while let Some(fragment) = iter.next() {
if space_is_visible {
match fragment {
MathFragment::Space(s) | MathFragment::Spacing(s) => {
items.push(MathParItem::Space(s));
continue;
}
_ => {}
}
}
let class = fragment.class();
let y = fragment.ascent();
ascent.set_max(y);
descent.set_max(fragment.descent());
let pos = Point::new(x, -y);
x += fragment.width();
frame.push_frame(pos, fragment.into_frame());
if class == Some(MathClass::Binary)
|| (class == Some(MathClass::Relation)
&& !iter.peek().map(is_relation).unwrap_or_default())
{
let mut frame_prev = std::mem::replace(
&mut frame,
Frame::new(Size::zero(), FrameKind::Soft),
);
finalize_frame(&mut frame_prev, x, ascent, descent);
items.push(MathParItem::Frame(frame_prev));
x = Abs::zero();
ascent = Abs::zero();
descent = Abs::zero();
space_is_visible = true;
if let Some(f_next) = iter.peek() {
if !is_space(f_next) {
items.push(MathParItem::Space(Abs::zero()));
}
}
} else {
space_is_visible = false;
}
}
if !frame.is_empty() {
finalize_frame(&mut frame, x, ascent, descent);
items.push(MathParItem::Frame(frame));
}
items
}
} }
impl<T: Into<MathFragment>> From<T> for MathRow { impl<T: Into<MathFragment>> From<T> for MathRow {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -2,6 +2,7 @@
--- ---
// Test automatic matching. // Test automatic matching.
#set page(width:122pt)
$ (a) + {b/2} + abs(a)/2 + (b) $ $ (a) + {b/2} + abs(a)/2 + (b) $
$f(x/2) < zeta(c^2 + abs(a + b/2))$ $f(x/2) < zeta(c^2 + abs(a + b/2))$

View File

@ -0,0 +1,50 @@
// Test inline equation line breaking.
---
// Basic breaking after binop, rel
#let hrule(x) = box(line(length: x))
#hrule(45pt)$e^(pi i)+1 = 0$\
#hrule(55pt)$e^(pi i)+1 = 0$\
#hrule(70pt)$e^(pi i)+1 = 0$
---
// LR groups prevent linbreaking.
#let hrule(x) = box(line(length: x))
#hrule(76pt)$a+b$\
#hrule(74pt)$(a+b)$\
#hrule(74pt)$paren.l a+b paren.r$
---
// Multiline yet inline does not linebreak
#let hrule(x) = box(line(length: x))
#hrule(80pt)$a + b \ c + d$\
---
// A single linebreak at the end still counts as one line.
#let hrule(x) = box(line(length: x))
#hrule(60pt)$e^(pi i)+1 = 0\ $
---
// Inline, in a box, doesn't linebreak.
#let hrule(x) = box(line(length: x))
#hrule(80pt)#box($a+b$)
---
// A relation followed by a relation doesn't linebreak
#let hrule(x) = box(line(length: x))
#hrule(70pt)$a < = b$\
#hrule(74pt)$a < = b$
---
// Page breaks can happen after a relation even if there is no
// explicit space.
#let hrule(x) = box(line(length: x))
#hrule(90pt)$<;$\
#hrule(95pt)$<;$\
#hrule(90pt)$<)$\
#hrule(95pt)$<)$
---
// Verify empty rows are handled ok.
$ $\
Nothing: $ $, just empty.

View File

@ -3,7 +3,7 @@
--- ---
// Test spacing cases. // Test spacing cases.
$ä, +, c, (, )$ \ $ä, +, c, (, )$ \
$=), (+), {times}$ $=), (+), {times}$ \
$<, abs(-), [=$ \ $<, abs(-), [=$ \
$a=b, a==b$ \ $a=b, a==b$ \
$-a, +a$ \ $-a, +a$ \