summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst/src/layout/inline/mod.rs49
-rw-r--r--crates/typst/src/math/ctx.rs5
-rw-r--r--crates/typst/src/math/equation.rs175
-rw-r--r--crates/typst/src/math/row.rs96
-rw-r--r--tests/ref/math/delimited.pngbin29020 -> 29090 bytes
-rw-r--r--tests/ref/math/linebreak.pngbin0 -> 7385 bytes
-rw-r--r--tests/ref/text/linebreak-obj.pngbin45919 -> 46208 bytes
-rw-r--r--tests/typ/math/delimited.typ1
-rw-r--r--tests/typ/math/linebreak.typ50
-rw-r--r--tests/typ/math/spacing.typ2
10 files changed, 298 insertions, 80 deletions
diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs
index 39af8ef1..20e6dcbc 100644
--- a/crates/typst/src/layout/inline/mod.rs
+++ b/crates/typst/src/layout/inline/mod.rs
@@ -19,7 +19,7 @@ use crate::layout::{
Abs, AlignElem, Axes, BoxElem, Dir, Em, FixedAlign, Fr, Fragment, Frame, HElem,
Layout, Point, Regions, Size, Sizing, Spacing,
};
-use crate::math::EquationElem;
+use crate::math::{EquationElem, MathParItem};
use crate::model::{Linebreaks, ParElem};
use crate::syntax::Span;
use crate::text::{
@@ -61,7 +61,8 @@ pub(crate) fn layout_inline(
};
// 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
// 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.
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Clone)]
enum Segment<'a> {
/// One or multiple collapsed text or text-equivalent children. Stores how
/// long the segment is (in bytes of the full text string).
@@ -188,7 +189,7 @@ enum Segment<'a> {
/// Horizontal spacing between other segments.
Spacing(Spacing),
/// A mathematical equation.
- Equation(&'a EquationElem),
+ Equation(&'a EquationElem, Vec<MathParItem>),
/// A box with arbitrary content.
Box(&'a BoxElem, bool),
/// Metadata.
@@ -201,8 +202,12 @@ impl Segment<'_> {
match *self {
Self::Text(len) => len,
Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
- Self::Box(_, true) => SPACING_REPLACE.len_utf8(),
- Self::Equation(_) | Self::Box(_, _) => OBJ_REPLACE.len_utf8(),
+ Self::Box(_, frac) => {
+ (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,
}
}
@@ -395,12 +400,14 @@ impl<'a> Line<'a> {
}
}
-/// Collect all text of the paragraph into one string. This also performs
-/// string-level preprocessing like case transformations.
+/// Collect all text of the paragraph into one string and layout equations. This
+/// also performs string-level preprocessing like case transformations.
#[allow(clippy::type_complexity)]
fn collect<'a>(
children: &'a [Prehashed<Content>],
+ engine: &mut Engine<'_>,
styles: &'a StyleChain<'a>,
+ region: Size,
consecutive: bool,
) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>, SpanMapper)> {
let mut full = String::new();
@@ -493,8 +500,10 @@ fn collect<'a>(
}
Segment::Text(full.len() - prev)
} else if let Some(elem) = child.to::<EquationElem>() {
- full.push(OBJ_REPLACE);
- Segment::Equation(elem)
+ let pod = Regions::one(region, Axes::splat(false));
+ 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>() {
let frac = elem.width(styles).is_fractional();
full.push(if frac { SPACING_REPLACE } else { OBJ_REPLACE });
@@ -512,7 +521,7 @@ fn collect<'a>(
spans.push(segment.len(), child.span());
if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) =
- (segments.last_mut(), segment)
+ (segments.last_mut(), &segment)
{
if *last_styles == styles {
*last_len += len;
@@ -526,8 +535,7 @@ fn collect<'a>(
Ok((full, segments, spans))
}
-/// Prepare paragraph layout by shaping the whole paragraph and layouting all
-/// contained inline-level content.
+/// Prepare paragraph layout by shaping the whole paragraph.
fn prepare<'a>(
engine: &mut Engine,
children: &'a [Prehashed<Content>],
@@ -566,11 +574,16 @@ fn prepare<'a>(
items.push(Item::Fractional(v, None));
}
},
- Segment::Equation(equation) => {
- let pod = Regions::one(region, Axes::splat(false));
- let mut frame = equation.layout(engine, styles, pod)?.into_frame();
- frame.translate(Point::with_y(TextElem::baseline_in(styles)));
- items.push(Item::Frame(frame));
+ Segment::Equation(_, par_items) => {
+ for item in par_items {
+ match item {
+ MathParItem::Space(s) => items.push(Item::Absolute(s)),
+ MathParItem::Frame(mut frame) => {
+ frame.translate(Point::with_y(TextElem::baseline_in(styles)));
+ items.push(Item::Frame(frame));
+ }
+ }
+ }
}
Segment::Box(elem, _) => {
if let Sizing::Fr(v) = elem.width(styles) {
diff --git a/crates/typst/src/math/ctx.rs b/crates/typst/src/math/ctx.rs
index 9352f08b..73cb8707 100644
--- a/crates/typst/src/math/ctx.rs
+++ b/crates/typst/src/math/ctx.rs
@@ -140,6 +140,11 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
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(
&mut self,
elem: &dyn LayoutMath,
diff --git a/crates/typst/src/math/equation.rs b/crates/typst/src/math/equation.rs
index dee7fef3..04e40804 100644
--- a/crates/typst/src/math/equation.rs
+++ b/crates/typst/src/math/equation.rs
@@ -8,13 +8,14 @@ use crate::foundations::{
};
use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
use crate::layout::{
- Abs, Align, AlignElem, Axes, Dir, Em, FixedAlign, Fragment, Layout, Point, Regions,
- Size,
+ Abs, Align, AlignElem, Axes, Dir, Em, FixedAlign, Fragment, Frame, Layout, Point,
+ Regions, Size,
};
use crate::math::{LayoutMath, MathContext};
use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement};
+use crate::syntax::Span;
use crate::text::{
- families, variant, FontFamily, FontList, FontWeight, Lang, LocalName, Region,
+ families, variant, Font, FontFamily, FontList, FontWeight, Lang, LocalName, Region,
TextElem,
};
use crate::util::{option_eq, NonZeroExt, Numeric};
@@ -136,70 +137,47 @@ impl Finalize for EquationElem {
}
}
-impl Layout for EquationElem {
- #[typst_macros::time(name = "math.equation", span = self.span())]
- fn layout(
+/// Layouted items suitable for placing in a paragraph.
+#[derive(Debug, Clone)]
+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,
- engine: &mut Engine,
+ engine: &mut Engine<'_>,
styles: StyleChain,
regions: Regions,
- ) -> SourceResult<Fragment> {
- const NUMBER_GUTTER: Em = Em::new(0.5);
-
- let block = self.block(styles);
+ ) -> SourceResult<Vec<MathParItem>> {
+ assert!(!self.block(styles));
// Find a math 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!(self.span(), "current font does not support math");
- };
+ let font = find_math_font(engine, styles, self.span())?;
- let mut ctx = MathContext::new(engine, styles, regions, &font, block);
- let mut frame = ctx.layout_frame(self)?;
+ let mut ctx = MathContext::new(engine, styles, regions, &font, false);
+ let rows = ctx.layout_root(self)?;
- 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)
- }
+ let mut items = if rows.row_count() == 1 {
+ rows.into_par_items()
} else {
+ vec![MathParItem::Frame(rows.into_fragment(&ctx).into_frame())]
+ };
+
+ for item in &mut items {
+ let MathParItem::Frame(frame) = item else { continue };
+
let font_size = TextElem::size_in(styles);
let slack = ParElem::leading_in(styles) * 0.7;
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);
frame.translate(Point::with_y(ascent - frame.baseline()));
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.
@@ -316,3 +355,21 @@ impl LayoutMath for EquationElem {
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)
+}
diff --git a/crates/typst/src/math/row.rs b/crates/typst/src/math/row.rs
index cd75e7c3..48b33934 100644
--- a/crates/typst/src/math/row.rs
+++ b/crates/typst/src/math/row.rs
@@ -3,10 +3,10 @@ use std::iter::once;
use unicode_math_class::MathClass;
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::{
alignments, spacing, AlignmentResult, FrameFragment, MathContext, MathFragment,
- MathSize, Scaled,
+ MathParItem, MathSize, Scaled,
};
use crate::model::ParElem;
@@ -103,6 +103,19 @@ impl MathRow {
.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 {
self.iter().map(MathFragment::ascent).max().unwrap_or_default()
}
@@ -239,6 +252,85 @@ impl MathRow {
frame.size_mut().x = x;
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 {
diff --git a/tests/ref/math/delimited.png b/tests/ref/math/delimited.png
index 727f2d13..5c827c48 100644
--- a/tests/ref/math/delimited.png
+++ b/tests/ref/math/delimited.png
Binary files differ
diff --git a/tests/ref/math/linebreak.png b/tests/ref/math/linebreak.png
new file mode 100644
index 00000000..f3212a4a
--- /dev/null
+++ b/tests/ref/math/linebreak.png
Binary files differ
diff --git a/tests/ref/text/linebreak-obj.png b/tests/ref/text/linebreak-obj.png
index 3c21377c..b13ced1a 100644
--- a/tests/ref/text/linebreak-obj.png
+++ b/tests/ref/text/linebreak-obj.png
Binary files differ
diff --git a/tests/typ/math/delimited.typ b/tests/typ/math/delimited.typ
index 6607c302..ba623b34 100644
--- a/tests/typ/math/delimited.typ
+++ b/tests/typ/math/delimited.typ
@@ -2,6 +2,7 @@
---
// Test automatic matching.
+#set page(width:122pt)
$ (a) + {b/2} + abs(a)/2 + (b) $
$f(x/2) < zeta(c^2 + abs(a + b/2))$
diff --git a/tests/typ/math/linebreak.typ b/tests/typ/math/linebreak.typ
new file mode 100644
index 00000000..88ce69d2
--- /dev/null
+++ b/tests/typ/math/linebreak.typ
@@ -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.
diff --git a/tests/typ/math/spacing.typ b/tests/typ/math/spacing.typ
index e62d2eb3..63a60ae1 100644
--- a/tests/typ/math/spacing.typ
+++ b/tests/typ/math/spacing.typ
@@ -3,7 +3,7 @@
---
// Test spacing cases.
$ä, +, c, (, )$ \
-$=), (+), {times}$
+$=), (+), {times}$ \
$⟧<⟦, abs(-), [=$ \
$a=b, a==b$ \
$-a, +a$ \