mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-25 10:57:14 -06:00
- Math straight in the body or a cell (m:oMathPara/m:oMath without w:p, allowed by EG_BlockLevelElts) gets its own paragraph instead of vanishing. - mc:AlternateContent Requires="m" picks the equation, not its fallback. - Inside an equation, runs in w:hyperlink, w:fldSimple, w:dir, w:bdo and mc:AlternateContent are kept (EG_PContentMath); depth stays bounded. - w:sym, w:noBreakHyphen and Symbol-font text in math runs become Unicode. - Layout: n-ary operators, delimiters, accents and group characters the math font lacks use a fallback font instead of a missing-glyph box. - Coverage test reads every ECMA-376 §22.1 element and checks no argument's text is dropped; tests for body-level math, Requires="m", symbols, hostile nesting and the font fallback. Parity docs and progress log updated. Claude-Session: https://claude.ai/code/session_01P8veLveuUsJJm6UEcQPEuW Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
2283 lines
94 KiB
Rust
2283 lines
94 KiB
Rust
//! Equation layout: the [`wordcraft_doc::math`] tree → positioned glyphs and rules.
|
||
//!
|
||
//! Follows the OpenType `MATH` model (the one Word's own math engine and Cambria Math were built
|
||
//! around): font constants for script shifts, fraction and radical gaps, limits and rules; size
|
||
//! variants and glyph assemblies for stretchy delimiters, radicals, big operators and wide
|
||
//! accents; atom classes with TeX/Word inter-atom spacing. Without a math font the layout uses
|
||
//! fixed proportions of the em.
|
||
//!
|
||
//! Coordinates: x right from the equation's left edge, y *up* from its baseline.
|
||
|
||
use std::sync::Arc;
|
||
|
||
use wordcraft_doc::math::{
|
||
Arg, BrkBin, BrkBinSub, ColJc, FracKind, LimLoc, MAX_DEPTH, MClass, MNode, MRun, MScr, MSty, MathProps, ScriptKind, is_integral, math_alnum,
|
||
parse_linear,
|
||
};
|
||
use wordcraft_doc::props::{Rgb, TextColor};
|
||
use wordcraft_doc::resolve::ResolvedChar;
|
||
use wordcraft_fonts::math::{C, Construction, MathTable, glyph_bounds};
|
||
use wordcraft_fonts::{FaceRef, FontDb};
|
||
|
||
/// Something to draw.
|
||
#[derive(Clone, Debug)]
|
||
pub enum MItem {
|
||
Glyphs {
|
||
face: FaceRef,
|
||
size: f32,
|
||
color: Rgb,
|
||
synth_bold: bool,
|
||
synth_italic: bool,
|
||
/// Glyph id, x, y (baseline, up).
|
||
glyphs: Vec<(u32, f32, f32)>,
|
||
text: String,
|
||
},
|
||
/// A filled rectangle; `y` is its bottom edge.
|
||
Rect {
|
||
x: f32,
|
||
y: f32,
|
||
w: f32,
|
||
h: f32,
|
||
color: Rgb,
|
||
},
|
||
Line {
|
||
x0: f32,
|
||
y0: f32,
|
||
x1: f32,
|
||
y1: f32,
|
||
width: f32,
|
||
color: Rgb,
|
||
dotted: bool,
|
||
},
|
||
/// Drawn on screen only (placeholders for empty slots, the empty-equation hint).
|
||
ScreenOnly(Box<MItem>),
|
||
}
|
||
|
||
impl MItem {
|
||
fn shift(&mut self, dx: f32, dy: f32) {
|
||
match self {
|
||
MItem::Glyphs { glyphs, .. } => {
|
||
for g in glyphs {
|
||
g.1 += dx;
|
||
g.2 += dy;
|
||
}
|
||
}
|
||
MItem::Rect { x, y, .. } => {
|
||
*x += dx;
|
||
*y += dy;
|
||
}
|
||
MItem::Line { x0, y0, x1, y1, .. } => {
|
||
*x0 += dx;
|
||
*x1 += dx;
|
||
*y0 += dy;
|
||
*y1 += dy;
|
||
}
|
||
MItem::ScreenOnly(i) => i.shift(dx, dy),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A caret position inside an equation, where it is drawn: `x`, the baseline `y` (up from the
|
||
/// equation's baseline) and the caret's extent above (`a`) and below (`d`) it.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct Slot {
|
||
pub path: Vec<(usize, usize)>,
|
||
pub off: usize,
|
||
pub x: f32,
|
||
pub y: f32,
|
||
pub a: f32,
|
||
pub d: f32,
|
||
}
|
||
|
||
/// A laid-out equation.
|
||
#[derive(Clone, Debug, Default)]
|
||
pub struct MathLayout {
|
||
pub width: f32,
|
||
/// Extent above and below the baseline, points.
|
||
pub ascent: f32,
|
||
pub descent: f32,
|
||
pub items: Vec<MItem>,
|
||
/// Every caret position, for drawing the caret and hit testing.
|
||
pub slots: Vec<Slot>,
|
||
}
|
||
|
||
impl MathLayout {
|
||
/// Where the caret at (`path`, `off`) is drawn.
|
||
pub fn slot(&self, path: &[(usize, usize)], off: usize) -> Option<&Slot> {
|
||
self.slots
|
||
.iter()
|
||
.find(|s| s.path == path && s.off == off)
|
||
.or_else(|| self.slots.iter().filter(|s| s.path == path).min_by_key(|s| s.off.abs_diff(off)))
|
||
}
|
||
/// The caret position nearest to (`x`, `y`) (y up from the baseline).
|
||
pub fn hit(&self, x: f32, y: f32) -> Option<&Slot> {
|
||
self.slots.iter().min_by(|a, b| slot_dist(a, x, y).total_cmp(&slot_dist(b, x, y)))
|
||
}
|
||
}
|
||
|
||
fn slot_dist(s: &Slot, x: f32, y: f32) -> f32 {
|
||
let (top, bottom) = (s.y + s.a, s.y - s.d);
|
||
let dy = if y > top {
|
||
y - top
|
||
} else if y < bottom {
|
||
bottom - y
|
||
} else {
|
||
0.0
|
||
};
|
||
// Inside a slot's height, prefer the innermost (shortest) argument.
|
||
let h = (s.a + s.d).max(0.1);
|
||
(s.x - x).abs() + dy * 3.0 + if dy == 0.0 { h * 0.05 } else { 0.0 }
|
||
}
|
||
|
||
/// Numbered rows (`#`) in an equation: they advance the automatic numbering.
|
||
pub fn auto_numbers(math: &wordcraft_doc::math::Math, display: bool) -> u32 {
|
||
if !display {
|
||
return 0;
|
||
}
|
||
match math.nodes.as_slice() {
|
||
[MNode::EqArr { rows }] => rows.iter().filter(|r| split_number(r).is_some()).count() as u32,
|
||
_ => 0,
|
||
}
|
||
}
|
||
|
||
/// A row's equation and number parts around its top-level `#`, and the units before the `#`.
|
||
fn split_number(row: &[MNode]) -> Option<(Arg, Arg, usize)> {
|
||
let mut eq = Vec::new();
|
||
let mut units = 0usize;
|
||
for (i, n) in row.iter().enumerate() {
|
||
if let MNode::Run(r) = n
|
||
&& !r.lit
|
||
&& let Some(b) = r.text.find('#')
|
||
{
|
||
let mut left = r.clone();
|
||
left.text = r.text.get(..b).unwrap_or("").to_string();
|
||
let mut right = r.clone();
|
||
right.text = r.text.get(b + 1..).unwrap_or("").to_string();
|
||
units += left.text.chars().count();
|
||
if !left.text.is_empty() {
|
||
eq.push(MNode::Run(left));
|
||
}
|
||
let mut num = Vec::new();
|
||
if !right.text.is_empty() {
|
||
num.push(MNode::Run(right));
|
||
}
|
||
num.extend(row.get(i + 1..).unwrap_or(&[]).iter().cloned());
|
||
return Some((eq, num, units));
|
||
}
|
||
units += match n {
|
||
MNode::Run(r) => r.text.chars().count(),
|
||
_ => 1,
|
||
};
|
||
eq.push(n.clone());
|
||
}
|
||
None
|
||
}
|
||
|
||
/// Lay out an equation set at the size and colour of `rc`. `display`: an equation on a line of
|
||
/// its own (bigger operators, limits above and below, full-size fractions).
|
||
/// `avail` is the width of the line (numbered display equations fill it, wider display equations
|
||
/// wrap to it as `props` says); `counter` counts the document's automatic equation numbers so far
|
||
/// and advances past this equation's.
|
||
pub fn layout_equation(
|
||
math: &wordcraft_doc::math::Math,
|
||
linear: &str,
|
||
rc: &ResolvedChar,
|
||
display: bool,
|
||
avail: f32,
|
||
props: &MathProps,
|
||
counter: &mut u32,
|
||
) -> MathLayout {
|
||
let parsed;
|
||
let nodes: &Arg = if math.nodes.is_empty() && !math.linear {
|
||
parsed = parse_linear(linear);
|
||
&parsed
|
||
} else {
|
||
&math.nodes
|
||
};
|
||
let (face, table) = wordcraft_fonts::math::resolve_math("Cambria Math");
|
||
let color = match rc.color {
|
||
TextColor::Rgb(c) => c,
|
||
TextColor::Auto => Rgb::BLACK,
|
||
};
|
||
let base = rc.draw_size().clamp(1.0, 1638.0);
|
||
let mut cx = Ctx {
|
||
face,
|
||
t: table,
|
||
upem: face.upem.max(1.0) as f32,
|
||
base,
|
||
color,
|
||
text_font: rc.font.clone(),
|
||
nodes: 0,
|
||
path: Vec::new(),
|
||
node_stack: Vec::new(),
|
||
};
|
||
let st = St { level: 0, display, cramped: false, upright: false };
|
||
let avail = if avail.is_finite() { avail.clamp(0.0, 1e5) } else { 0.0 };
|
||
let b = match nodes.as_slice() {
|
||
[] => cx.hint(st),
|
||
[MNode::EqArr { rows }] if display && rows.iter().any(|r| split_number(r).is_some()) => cx.numbered(rows, st, avail, counter),
|
||
_ if display => cx.wrapped(nodes, st, avail, props),
|
||
_ => cx.arg(nodes, st, 0),
|
||
};
|
||
let mut slots = b.slots;
|
||
let em = base;
|
||
for s in &mut slots {
|
||
if s.a <= 0.0 && s.d <= 0.0 {
|
||
s.a = em * 0.75;
|
||
s.d = em * 0.25;
|
||
}
|
||
}
|
||
MathLayout { width: b.w.max(0.0), ascent: b.a.max(0.0), descent: b.d.max(0.0), items: b.items, slots }
|
||
}
|
||
|
||
/// Layout style: script level (0 = text size, 1 = script, 2 = script-script), display or inline,
|
||
/// cramped (superscripts lowered: inside radicals, denominators, subscripts).
|
||
#[derive(Clone, Copy, Debug)]
|
||
struct St {
|
||
level: u8,
|
||
display: bool,
|
||
cramped: bool,
|
||
/// Letters default to upright (function names).
|
||
upright: bool,
|
||
}
|
||
|
||
impl St {
|
||
fn script(self) -> St {
|
||
St { level: (self.level + 1).min(2), display: false, ..self }
|
||
}
|
||
fn cramp(self) -> St {
|
||
St { cramped: true, ..self }
|
||
}
|
||
/// Numerator / denominator style.
|
||
fn frac(self) -> St {
|
||
if self.display { St { display: false, ..self } } else { self.script() }
|
||
}
|
||
}
|
||
|
||
/// TeX atom classes (spacing).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||
enum Cls {
|
||
#[default]
|
||
Ord,
|
||
Op,
|
||
Bin,
|
||
Rel,
|
||
Open,
|
||
Close,
|
||
Punct,
|
||
Inner,
|
||
}
|
||
|
||
impl Cls {
|
||
fn idx(self) -> usize {
|
||
self as usize
|
||
}
|
||
}
|
||
|
||
/// Inter-atom space in mu (1/18 em): TeX's table; negative = only outside scripts.
|
||
const SPACING: [[i8; 8]; 8] = [
|
||
// Ord Op Bin Rel Open Close Punct Inner
|
||
[0, 3, -4, -5, 0, 0, 0, -3], // Ord
|
||
[3, 3, 0, -5, 0, 0, 0, -3], // Op
|
||
[-4, -4, 0, 0, -4, 0, 0, -4], // Bin
|
||
[-5, -5, 0, 0, -5, 0, 0, -5], // Rel
|
||
[0, 0, 0, 0, 0, 0, 0, 0], // Open
|
||
[0, 3, -4, -5, 0, 0, 0, -3], // Close
|
||
[-3, -3, 0, -3, -3, -3, -3, -3], // Punct
|
||
[-3, 3, -4, -5, -3, 0, -3, -3], // Inner
|
||
];
|
||
|
||
fn spacing_mu(l: Cls, r: Cls, st: St) -> f32 {
|
||
let v = SPACING.get(l.idx()).and_then(|row| row.get(r.idx())).copied().unwrap_or(0);
|
||
if v < 0 && st.level > 0 { 0.0 } else { v.unsigned_abs() as f32 }
|
||
}
|
||
|
||
/// A box under construction.
|
||
#[derive(Clone, Debug, Default)]
|
||
struct Bx {
|
||
w: f32,
|
||
a: f32,
|
||
d: f32,
|
||
items: Vec<MItem>,
|
||
/// Italic correction after the box (superscripts move right by it).
|
||
ic: f32,
|
||
first: Cls,
|
||
last: Cls,
|
||
/// The box is one glyph: (face, glyph, top-accent attachment x).
|
||
glyph: Option<(FaceRef, u32, f32)>,
|
||
/// Takes no part in inter-atom spacing (spaces).
|
||
transparent: bool,
|
||
/// Caret positions inside, relative to the box.
|
||
slots: Vec<Slot>,
|
||
/// Text atoms: x of each character boundary (one more than the characters).
|
||
cuts: Vec<f32>,
|
||
/// A manual line break before this atom (`m:brk`; the value is its `m:alnAt`).
|
||
brk: Option<u8>,
|
||
}
|
||
|
||
impl Bx {
|
||
fn cls(mut self, c: Cls) -> Bx {
|
||
self.first = c;
|
||
self.last = c;
|
||
self
|
||
}
|
||
/// Draw `o` at (dx, dy) into this box (extents grow; width is the caller's business).
|
||
fn put(&mut self, o: Bx, dx: f32, dy: f32) {
|
||
self.a = self.a.max(o.a + dy);
|
||
self.d = self.d.max(o.d - dy);
|
||
for mut it in o.items {
|
||
it.shift(dx, dy);
|
||
self.items.push(it);
|
||
}
|
||
for mut sl in o.slots {
|
||
sl.x += dx;
|
||
sl.y += dy;
|
||
self.slots.push(sl);
|
||
}
|
||
}
|
||
fn slot(&mut self, path: &[(usize, usize)], off: usize, x: f32) {
|
||
self.slots.push(Slot { path: path.to_vec(), off, x, y: 0.0, a: 0.0, d: 0.0 });
|
||
}
|
||
fn rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Rgb) {
|
||
if w > 0.0 && h > 0.0 {
|
||
self.items.push(MItem::Rect { x, y, w, h, color });
|
||
self.a = self.a.max(y + h);
|
||
self.d = self.d.max(-y);
|
||
}
|
||
}
|
||
}
|
||
|
||
struct Ctx {
|
||
face: FaceRef,
|
||
t: Option<Arc<MathTable>>,
|
||
upem: f32,
|
||
base: f32,
|
||
color: Rgb,
|
||
text_font: String,
|
||
/// Nodes laid out so far (caps work on hostile input).
|
||
nodes: usize,
|
||
/// The argument being laid out (for caret slots).
|
||
path: Vec<(usize, usize)>,
|
||
/// Index of the node being laid out in its argument, innermost last.
|
||
node_stack: Vec<usize>,
|
||
}
|
||
|
||
const MAX_NODES: usize = 200_000;
|
||
|
||
/// Constants as fractions of the em when there's no `MATH` table.
|
||
fn fallback(c: C) -> f32 {
|
||
match c {
|
||
C::MathLeading => 0.15,
|
||
C::AxisHeight => 0.25,
|
||
C::AccentBaseHeight => 0.45,
|
||
C::FlattenedAccentBaseHeight => 0.66,
|
||
C::SubscriptShiftDown => 0.2,
|
||
C::SubscriptTopMax => 0.36,
|
||
C::SubscriptBaselineDropMin => 0.05,
|
||
C::SuperscriptShiftUp => 0.36,
|
||
C::SuperscriptShiftUpCramped => 0.29,
|
||
C::SuperscriptBottomMin => 0.11,
|
||
C::SuperscriptBaselineDropMax => 0.25,
|
||
C::SubSuperscriptGapMin => 0.2,
|
||
C::SuperscriptBottomMaxWithSubscript => 0.36,
|
||
C::SpaceAfterScript => 0.05,
|
||
C::UpperLimitGapMin => 0.1,
|
||
C::UpperLimitBaselineRiseMin => 0.2,
|
||
C::LowerLimitGapMin => 0.1,
|
||
C::LowerLimitBaselineDropMin => 0.6,
|
||
C::StackTopShiftUp => 0.44,
|
||
C::StackTopDisplayStyleShiftUp => 0.68,
|
||
C::StackBottomShiftDown => 0.34,
|
||
C::StackBottomDisplayStyleShiftDown => 0.69,
|
||
C::StackGapMin => 0.12,
|
||
C::StackDisplayStyleGapMin => 0.28,
|
||
C::StretchStackTopShiftUp => 0.2,
|
||
C::StretchStackBottomShiftDown => 0.6,
|
||
C::StretchStackGapAboveMin => 0.1,
|
||
C::StretchStackGapBelowMin => 0.1,
|
||
C::FractionNumeratorShiftUp => 0.39,
|
||
C::FractionNumeratorDisplayStyleShiftUp => 0.68,
|
||
C::FractionDenominatorShiftDown => 0.34,
|
||
C::FractionDenominatorDisplayStyleShiftDown => 0.69,
|
||
C::FractionNumeratorGapMin => 0.04,
|
||
C::FractionNumDisplayStyleGapMin => 0.12,
|
||
C::FractionRuleThickness => 0.05,
|
||
C::FractionDenominatorGapMin => 0.04,
|
||
C::FractionDenomDisplayStyleGapMin => 0.12,
|
||
C::SkewedFractionHorizontalGap => 0.35,
|
||
C::SkewedFractionVerticalGap => 0.1,
|
||
C::OverbarVerticalGap => 0.15,
|
||
C::OverbarRuleThickness => 0.05,
|
||
C::OverbarExtraAscender => 0.05,
|
||
C::UnderbarVerticalGap => 0.15,
|
||
C::UnderbarRuleThickness => 0.05,
|
||
C::UnderbarExtraDescender => 0.05,
|
||
C::RadicalVerticalGap => 0.05,
|
||
C::RadicalDisplayStyleVerticalGap => 0.15,
|
||
C::RadicalRuleThickness => 0.05,
|
||
C::RadicalExtraAscender => 0.05,
|
||
C::RadicalKernBeforeDegree => 0.28,
|
||
C::RadicalKernAfterDegree => -0.55,
|
||
}
|
||
}
|
||
|
||
/// Characters drawn as operators with their own spacing class.
|
||
fn class_of(c: char) -> Cls {
|
||
match wordcraft_doc::math::math_class(c) {
|
||
MClass::Ord => Cls::Ord,
|
||
MClass::Op => Cls::Op,
|
||
MClass::Bin => Cls::Bin,
|
||
MClass::Rel => Cls::Rel,
|
||
MClass::Open => Cls::Open,
|
||
MClass::Close => Cls::Close,
|
||
MClass::Punct => Cls::Punct,
|
||
MClass::Inner => Cls::Inner,
|
||
}
|
||
}
|
||
|
||
/// Accents drawn across the whole base (stretched horizontally).
|
||
fn wide_accent(c: char) -> bool {
|
||
matches!(
|
||
c,
|
||
'\u{20D6}'
|
||
| '\u{20D7}'
|
||
| '\u{20E1}'
|
||
| '\u{20D0}'
|
||
| '\u{20D1}'
|
||
| '\u{2190}'
|
||
| '\u{2192}'
|
||
| '\u{2194}'
|
||
| '\u{23DC}'
|
||
| '\u{23DD}'
|
||
| '\u{23DE}'
|
||
| '\u{23DF}'
|
||
| '\u{23B4}'
|
||
| '\u{23B5}'
|
||
)
|
||
}
|
||
|
||
impl Ctx {
|
||
fn scale(&self, level: u8) -> f32 {
|
||
let (s1, s2) = match &self.t {
|
||
Some(t) => (t.script_percent, t.script_script_percent),
|
||
None => (70.0, 50.0),
|
||
};
|
||
let s1 = if (30.0..=100.0).contains(&s1) { s1 } else { 70.0 };
|
||
let s2 = if (20.0..=100.0).contains(&s2) { s2 } else { 50.0 };
|
||
match level {
|
||
0 => 1.0,
|
||
1 => s1 / 100.0,
|
||
_ => s2 / 100.0,
|
||
}
|
||
}
|
||
fn size(&self, st: St) -> f32 {
|
||
self.base * self.scale(st.level)
|
||
}
|
||
/// A constant in points at the style's size.
|
||
fn k(&self, c: C, st: St) -> f32 {
|
||
let size = self.size(st);
|
||
match &self.t {
|
||
Some(t) => t.get(c) * size / self.upem,
|
||
None => fallback(c) * size,
|
||
}
|
||
}
|
||
fn axis(&self, st: St) -> f32 {
|
||
self.k(C::AxisHeight, st)
|
||
}
|
||
fn em(&self, st: St) -> f32 {
|
||
self.size(st)
|
||
}
|
||
|
||
fn arg(&mut self, a: &[MNode], st: St, depth: usize) -> Bx {
|
||
let atoms = self.row(a, st, depth, 0);
|
||
let em = self.em(st);
|
||
let mut b = compose(atoms.into_iter().map(|(b, _)| b).collect(), st, em);
|
||
finalize(&mut b, &self.path, em);
|
||
b
|
||
}
|
||
|
||
/// Child argument `c` of the node being laid out; an empty one shows a placeholder.
|
||
fn sub(&mut self, c: usize, a: &[MNode], st: St, depth: usize) -> Bx {
|
||
let me = self.node_stack.last().copied().unwrap_or(0);
|
||
self.path.push((me, c));
|
||
let b = if a.is_empty() { self.placeholder(st) } else { self.arg(a, st, depth) };
|
||
self.path.pop();
|
||
b
|
||
}
|
||
|
||
/// The dotted box an empty argument shows on screen.
|
||
fn placeholder(&mut self, st: St) -> Bx {
|
||
let em = self.em(st);
|
||
let (w, a, d, pad) = (em * 0.56, em * 0.7, em * 0.06, em * 0.05);
|
||
let mut b = Bx { w: w + 2.0 * pad, a, d, ..Default::default() };
|
||
let (x0, x1, y0, y1) = (pad, pad + w, -d, a);
|
||
let width = (em * 0.035).max(0.4);
|
||
for ((ax, ay), (bx, by)) in [((x0, y0), (x1, y0)), ((x1, y0), (x1, y1)), ((x1, y1), (x0, y1)), ((x0, y1), (x0, y0))] {
|
||
let line = MItem::Line { x0: ax, y0: ay, x1: bx, y1: by, width, color: Rgb(0x60, 0x60, 0x60), dotted: true };
|
||
b.items.push(MItem::ScreenOnly(Box::new(line)));
|
||
}
|
||
let path = self.path.clone();
|
||
b.slot(&path, 0, pad);
|
||
finalize(&mut b, &path, em);
|
||
b
|
||
}
|
||
|
||
/// An empty equation: a grey prompt (screen only).
|
||
fn hint(&mut self, st: St) -> Bx {
|
||
let size = self.size(st);
|
||
let rs = wordcraft_fonts::word::resolve(&self.text_font, false, false);
|
||
let mut g = self.glyphs(rs.face, "Type an equation", size, 0, Rgb(0x80, 0x80, 0x80), (rs.synth_bold, rs.synth_italic));
|
||
g.items = std::mem::take(&mut g.items).into_iter().map(|i| MItem::ScreenOnly(Box::new(i))).collect();
|
||
g.glyph = None;
|
||
g.cuts.clear();
|
||
g.slots.clear();
|
||
g.slot(&[], 0, 0.0);
|
||
finalize(&mut g, &[], size);
|
||
g
|
||
}
|
||
|
||
/// The atoms of a row with their alignment segment (`&` starts a new segment). `u0`: the
|
||
/// unit offset of the row's first node (for caret slots).
|
||
fn row(&mut self, a: &[MNode], st: St, depth: usize, u0: usize) -> Vec<(Bx, usize)> {
|
||
let mut out = Vec::new();
|
||
if depth > MAX_DEPTH {
|
||
return out;
|
||
}
|
||
let mut seg = 0usize;
|
||
let mut u = u0;
|
||
for (i, n) in a.iter().enumerate() {
|
||
self.nodes += 1;
|
||
if self.nodes > MAX_NODES {
|
||
break;
|
||
}
|
||
let path = self.path.clone();
|
||
match n {
|
||
MNode::Run(r) => {
|
||
let first = out.len();
|
||
for part in self.run_atoms(r, st, u, &path) {
|
||
match part {
|
||
RunPart::Atom(b) => out.push((b, seg)),
|
||
RunPart::Align(b) => {
|
||
seg += 1;
|
||
out.push((b, seg));
|
||
}
|
||
}
|
||
}
|
||
if let Some((b, _)) = out.get_mut(first) {
|
||
b.brk = r.brk;
|
||
}
|
||
u += r.text.chars().count();
|
||
}
|
||
MNode::Nary { .. } | MNode::Func { .. } => {
|
||
self.node_stack.push(i);
|
||
let mut atoms = self.op_atoms(n, st, depth + 1);
|
||
self.node_stack.pop();
|
||
if let Some(f) = atoms.first_mut() {
|
||
f.slot(&path, u, 0.0);
|
||
}
|
||
if let Some(l) = atoms.last_mut() {
|
||
let w = l.w;
|
||
l.slot(&path, u + 1, w);
|
||
}
|
||
for b in atoms {
|
||
out.push((b, seg));
|
||
}
|
||
u += 1;
|
||
}
|
||
_ => {
|
||
self.node_stack.push(i);
|
||
let mut b = self.node(n, st, depth + 1);
|
||
self.node_stack.pop();
|
||
let w = b.w;
|
||
b.slot(&path, u, 0.0);
|
||
b.slot(&path, u + 1, w);
|
||
out.push((b, seg));
|
||
u += 1;
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Glyph run for `text` in `face` at `size`: a box with ink extents.
|
||
fn glyphs(&self, face: FaceRef, text: &str, size: f32, level: u8, color: Rgb, synth: (bool, bool)) -> Bx {
|
||
let feats = if level > 0 { vec![wordcraft_fonts::feature_value(b"ssty", level as u32)] } else { Vec::new() };
|
||
let shaped = wordcraft_fonts::shape(&face, text, &feats, |c| c);
|
||
let k = size / face.upem.max(1.0) as f32;
|
||
let mut x = 0.0f32;
|
||
let mut b = Bx::default();
|
||
let mut glyphs = Vec::with_capacity(shaped.len());
|
||
let mut last_gid = None;
|
||
for g in &shaped {
|
||
let gx = x + g.x_offset as f32 * k;
|
||
let gy = g.y_offset as f32 * k;
|
||
if let Some((x0, y0, x1, y1)) = glyph_bounds(&face, g.gid) {
|
||
b.a = b.a.max(y1 * k + gy);
|
||
b.d = b.d.max(-y0 * k - gy);
|
||
let _ = (x0, x1);
|
||
}
|
||
glyphs.push((g.gid, gx, gy));
|
||
x += g.x_advance as f32 * k;
|
||
last_gid = Some(g.gid);
|
||
}
|
||
b.w = x;
|
||
// Character boundaries: where the first glyph of each character starts.
|
||
let mut cuts = Vec::with_capacity(text.len() + 1);
|
||
for (cb, _) in text.char_indices() {
|
||
let mut gx = 0.0f32;
|
||
let mut found = None;
|
||
for g in &shaped {
|
||
if g.cluster >= cb {
|
||
found = Some(gx);
|
||
break;
|
||
}
|
||
gx += g.x_advance as f32 * k;
|
||
}
|
||
cuts.push(found.unwrap_or(x));
|
||
}
|
||
cuts.push(x);
|
||
b.cuts = cuts;
|
||
if let (Some(t), Some(g)) = (&self.t, last_gid)
|
||
&& face.id() == self.face.id()
|
||
{
|
||
b.ic = t.italics.get(&g).copied().unwrap_or(0.0) * k;
|
||
}
|
||
if shaped.len() == 1
|
||
&& let Some(g) = last_gid
|
||
{
|
||
let attach = match (&self.t, glyph_bounds(&face, g)) {
|
||
(Some(t), _) if face.id() == self.face.id() && t.top_accent.contains_key(&g) => t.top_accent.get(&g).copied().unwrap_or(0.0) * k,
|
||
(_, Some((x0, _, x1, _))) => (x0 + x1) / 2.0 * k,
|
||
_ => b.w / 2.0,
|
||
};
|
||
b.glyph = Some((face, g, attach));
|
||
}
|
||
if !glyphs.is_empty() {
|
||
b.items.push(MItem::Glyphs { face, size, color, synth_bold: synth.0, synth_italic: synth.1, glyphs, text: text.to_string() });
|
||
}
|
||
b
|
||
}
|
||
|
||
/// `c` in the math font, or in a fallback font when the math font lacks it (an n-ary
|
||
/// operator, delimiter or accent the font doesn't cover would otherwise draw as a missing
|
||
/// glyph box, #1273).
|
||
fn char_glyphs(&self, c: char, size: f32, level: u8) -> Bx {
|
||
let face = if self.face.covers(c) {
|
||
self.face
|
||
} else {
|
||
FontDb::global().fallback_for(c, self.face.id()).map(|f| FaceRef::of(&f)).unwrap_or(self.face)
|
||
};
|
||
self.glyphs(face, &c.to_string(), size, level, self.color, (false, false))
|
||
}
|
||
|
||
/// A single glyph by id (size variants, assembly parts).
|
||
fn glyph_id(&self, gid: u32, size: f32, color: Rgb) -> Bx {
|
||
let face = self.face;
|
||
let k = size / face.upem.max(1.0) as f32;
|
||
let mut b = Bx { w: face.advance(gid) as f32 * k, ..Default::default() };
|
||
if let Some((_, y0, _, y1)) = glyph_bounds(&face, gid) {
|
||
b.a = y1 * k;
|
||
b.d = -y0 * k;
|
||
}
|
||
if let Some(t) = &self.t {
|
||
b.ic = t.italics.get(&gid).copied().unwrap_or(0.0) * k;
|
||
}
|
||
b.items.push(MItem::Glyphs { face, size, color, synth_bold: false, synth_italic: false, glyphs: vec![(gid, 0.0, 0.0)], text: String::new() });
|
||
b
|
||
}
|
||
|
||
/// A text atom with caret slots at its character boundaries (`k0`: the unit offset of its
|
||
/// first character).
|
||
fn text_atom(&self, face: FaceRef, text: &str, size: f32, level: u8, color: Rgb, synth: (bool, bool), path: &[(usize, usize)], k0: usize) -> Bx {
|
||
let mut b = self.glyphs(face, text, size, level, color, synth);
|
||
let cuts = std::mem::take(&mut b.cuts);
|
||
for (j, x) in cuts.iter().enumerate() {
|
||
b.slot(path, k0 + j, *x);
|
||
}
|
||
b
|
||
}
|
||
|
||
fn run_atoms(&self, r: &MRun, st: St, u0: usize, path: &[(usize, usize)]) -> Vec<RunPart> {
|
||
let mut out = Vec::new();
|
||
let size = r.size.filter(|s| s.is_finite()).unwrap_or(self.base).clamp(1.0, 1638.0) * self.scale(st.level);
|
||
let color = r.color.unwrap_or(self.color);
|
||
if r.nor {
|
||
// Normal text: the text font, upright unless asked.
|
||
let fam = r.font.clone().filter(|f| !f.eq_ignore_ascii_case("Cambria Math")).unwrap_or_else(|| self.text_font.clone());
|
||
let (bold, italic) = match r.sty {
|
||
Some(MSty::Bold) => (true, false),
|
||
Some(MSty::Italic) => (false, true),
|
||
Some(MSty::BoldItalic) => (true, true),
|
||
_ => (false, false),
|
||
};
|
||
let rs = wordcraft_fonts::word::resolve(&fam, bold, italic);
|
||
let mut b = self.text_atom(rs.face, &r.text, size, 0, color, (rs.synth_bold, rs.synth_italic), path, u0);
|
||
b.glyph = None;
|
||
out.push(RunPart::Atom(b.cls(Cls::Ord)));
|
||
return out;
|
||
}
|
||
// Map characters (math alphabets, operators) and split into atoms by class.
|
||
let mut cur = String::new();
|
||
let mut cur_k0 = 0usize;
|
||
let mut cur_face: Option<FaceRef> = None;
|
||
let mut synth_italic = false;
|
||
let flush = |cur: &mut String, k0: usize, face: Option<FaceRef>, synth_italic: bool, out: &mut Vec<RunPart>| {
|
||
if !cur.is_empty() {
|
||
let f = face.unwrap_or(self.face);
|
||
let b = self.text_atom(f, cur, size, st.level, color, (false, synth_italic), path, u0 + k0);
|
||
out.push(RunPart::Atom(b.cls(Cls::Ord)));
|
||
cur.clear();
|
||
}
|
||
};
|
||
// A zero-width (or space) atom that takes no part in spacing, with its two slots.
|
||
let blank = |w: f32, k: usize| {
|
||
let mut b = Bx { w, transparent: true, ..Default::default() };
|
||
b.slot(path, u0 + k, 0.0);
|
||
b.slot(path, u0 + k + 1, w);
|
||
b
|
||
};
|
||
for (k, c) in r.text.chars().enumerate() {
|
||
if c == '&' && !r.lit {
|
||
flush(&mut cur, cur_k0, cur_face, synth_italic, &mut out);
|
||
out.push(RunPart::Align(blank(0.0, k)));
|
||
continue;
|
||
}
|
||
let c = if r.lit {
|
||
c
|
||
} else {
|
||
match c {
|
||
'-' => '−',
|
||
'*' => '∗',
|
||
'\'' => '′',
|
||
_ => c,
|
||
}
|
||
};
|
||
let is_letter = c.is_alphabetic();
|
||
let greek_cap = ('\u{391}'..='\u{3A9}').contains(&c);
|
||
let (bold, italic) = match r.sty {
|
||
Some(MSty::Plain) => (false, false),
|
||
Some(MSty::Bold) => (true, false),
|
||
Some(MSty::Italic) => (false, true),
|
||
Some(MSty::BoldItalic) => (true, true),
|
||
None => (false, is_letter && !greek_cap && !st.upright && r.scr == MScr::Roman && !r.lit),
|
||
};
|
||
let mapped = math_alnum(c, bold, italic, r.scr);
|
||
// Fall back to the plain letter (slanted) when the face lacks the math alphabet.
|
||
let (ch, slant) = if mapped != c && !self.face.covers(mapped) { (c, italic) } else { (mapped, false) };
|
||
let face = if self.face.covers(ch) || ch.is_whitespace() {
|
||
None
|
||
} else {
|
||
FontDb::global().fallback_for(ch, self.face.id()).map(|f| FaceRef::of(&f))
|
||
};
|
||
let cls = if r.lit { Cls::Ord } else { class_of(ch) };
|
||
if ch == ' ' || ch == '\u{2061}' || ch == '\u{2062}' || ch == '\u{2063}' || ch == '\u{200B}' {
|
||
flush(&mut cur, cur_k0, cur_face, synth_italic, &mut out);
|
||
// Word sets a typed space in an equation about 0.4 em wide.
|
||
out.push(RunPart::Atom(blank(if ch == ' ' { size * 0.4 } else { 0.0 }, k)));
|
||
continue;
|
||
}
|
||
let same_face = cur_face.map(|f| f.id()) == face.map(|f| f.id());
|
||
if cls != Cls::Ord || !same_face || slant != synth_italic {
|
||
flush(&mut cur, cur_k0, cur_face, synth_italic, &mut out);
|
||
}
|
||
if cls != Cls::Ord {
|
||
let f = face.unwrap_or(self.face);
|
||
let b = self.text_atom(f, &ch.to_string(), size, st.level, color, (false, slant), path, u0 + k);
|
||
out.push(RunPart::Atom(b.cls(cls)));
|
||
continue;
|
||
}
|
||
if cur.is_empty() {
|
||
cur_k0 = k;
|
||
}
|
||
cur_face = face;
|
||
synth_italic = slant;
|
||
cur.push(ch);
|
||
}
|
||
flush(&mut cur, cur_k0, cur_face, synth_italic, &mut out);
|
||
out
|
||
}
|
||
|
||
/// N-ary operators and functions: the operator atom (class Op) then the operand.
|
||
fn op_atoms(&mut self, n: &MNode, st: St, depth: usize) -> Vec<Bx> {
|
||
match n {
|
||
MNode::Nary { chr, lim_loc, grow, sub_hide, sup_hide, sub, sup, e } => {
|
||
let e = self.sub(2, e, st, depth);
|
||
let loc = lim_loc.unwrap_or(if is_integral(*chr) { LimLoc::SubSup } else { LimLoc::UndOvr });
|
||
let loc = if st.display { loc } else { LimLoc::SubSup };
|
||
let size = self.size(st);
|
||
let base_gid = self.face.glyph_for(*chr);
|
||
let mut target = 0.0f32;
|
||
if st.display {
|
||
target = match &self.t {
|
||
Some(t) => t.display_operator_min_height * size / self.upem,
|
||
None => size * 1.5,
|
||
};
|
||
}
|
||
if *grow {
|
||
target = target.max(e.a + e.d);
|
||
}
|
||
let op = if base_gid == 0 {
|
||
// The math font lacks the operator: a fallback font's glyph, display-sized.
|
||
let s = if st.display { size * 1.4 } else { size };
|
||
self.char_glyphs(*chr, s, 0)
|
||
} else if *grow && target > 0.0 && self.t.is_some() {
|
||
self.stretch_v(base_gid, target, size).unwrap_or_else(|| self.glyph_id(base_gid, size, self.color))
|
||
} else if target > 0.0 && self.t.is_some() {
|
||
// Word sets display operators about 1.2 em tall: the size variant nearest that.
|
||
let want = target.min(size * 1.2) * self.upem / size;
|
||
let (g, adv) = self
|
||
.t
|
||
.as_ref()
|
||
.and_then(|t| t.vert.get(&base_gid))
|
||
.and_then(|c| c.variants.iter().min_by(|a, b| (a.1 - want).abs().total_cmp(&(b.1 - want).abs())))
|
||
.copied()
|
||
.unwrap_or((base_gid, want));
|
||
// A font without a variant near that size: scale the nearest one.
|
||
let f = if adv > 0.0 { (want / adv).clamp(0.6, 1.5) } else { 1.0 };
|
||
let f = if (0.9..=1.1).contains(&f) { 1.0 } else { f };
|
||
self.glyph_id(g, size * f, self.color)
|
||
} else {
|
||
self.glyph_id(base_gid, size, self.color)
|
||
};
|
||
// Centre the operator on the math axis.
|
||
let dy = self.axis(st) - (op.a - op.d) / 2.0;
|
||
let mut ob = Bx { w: op.w, ..Default::default() };
|
||
let ic = op.ic;
|
||
ob.put(op, 0.0, dy);
|
||
let sub = if *sub_hide { None } else { Some(self.sub(0, sub, st.script().cramp(), depth)) };
|
||
let sup = if *sup_hide { None } else { Some(self.sub(1, sup, st.script(), depth)) };
|
||
let opb = match loc {
|
||
LimLoc::UndOvr => self.limits(ob, sub, sup, ic, st, true),
|
||
LimLoc::SubSup => {
|
||
ob.ic = ic;
|
||
self.attach_op(ob, sub, sup, st)
|
||
}
|
||
};
|
||
vec![opb.cls(Cls::Op), e]
|
||
}
|
||
MNode::Func { name, e } => {
|
||
let name = self.sub(0, name, St { upright: true, ..st }, depth);
|
||
let e = self.sub(1, e, St { upright: false, ..st }, depth);
|
||
vec![name.cls(Cls::Op), e]
|
||
}
|
||
_ => Vec::new(),
|
||
}
|
||
}
|
||
|
||
/// Limits under and over `base` (centred), spaced as for big operators (`big`) or as
|
||
/// accents-like limits.
|
||
fn limits(&self, base: Bx, under: Option<Bx>, over: Option<Bx>, ic: f32, st: St, big: bool) -> Bx {
|
||
let w = base.w.max(under.as_ref().map_or(0.0, |b| b.w)).max(over.as_ref().map_or(0.0, |b| b.w));
|
||
let mut out = Bx { w, ..Default::default() };
|
||
let (ba, bd) = (base.a, base.d);
|
||
let bw = base.w;
|
||
out.put(base, (w - bw) / 2.0, 0.0);
|
||
if let Some(o) = over {
|
||
let rise = if big {
|
||
self.k(C::UpperLimitBaselineRiseMin, st).max(self.k(C::UpperLimitGapMin, st) + o.d)
|
||
} else {
|
||
self.k(C::UpperLimitGapMin, st) + o.d
|
||
};
|
||
let x = (w - o.w) / 2.0 + ic / 2.0;
|
||
out.put(o, x, ba + rise);
|
||
}
|
||
if let Some(u) = under {
|
||
let drop = if big {
|
||
self.k(C::LowerLimitBaselineDropMin, st).max(self.k(C::LowerLimitGapMin, st) + u.a)
|
||
} else {
|
||
self.k(C::LowerLimitGapMin, st) + u.a
|
||
};
|
||
let x = (w - u.w) / 2.0 - ic / 2.0;
|
||
out.put(u, x, -bd - drop);
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Limits beside a big operator: the superscript at its advance, the subscript tucked left
|
||
/// under the slant by the italic correction.
|
||
fn attach_op(&self, base: Bx, sub: Option<Bx>, sup: Option<Bx>, st: St) -> Bx {
|
||
let ic = base.ic;
|
||
let mut b = base;
|
||
b.ic = 0.0;
|
||
let (bw, bd) = (b.w, b.d);
|
||
let mut out = self.attach(b, None, sup, st, false);
|
||
if let Some(s) = sub {
|
||
let v = self.k(C::SubscriptShiftDown, st).max(s.a - self.k(C::SubscriptTopMax, st)).max(bd + self.k(C::SubscriptBaselineDropMin, st));
|
||
let x = (bw - ic).max(0.0);
|
||
let sw = s.w;
|
||
out.put(s, x, -v);
|
||
out.w = out.w.max(x + sw + self.k(C::SpaceAfterScript, st));
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Sub/superscripts after `base`. `char_base`: the base is a single character (no baseline drop).
|
||
fn attach(&self, base: Bx, sub: Option<Bx>, sup: Option<Bx>, st: St, char_base: bool) -> Bx {
|
||
let (bw, ba, bd, ic) = (base.w, base.a, base.d, base.ic);
|
||
let (first, last) = (base.first, base.last);
|
||
let mut out = Bx { w: bw, ..Default::default() };
|
||
out.put(base, 0.0, 0.0);
|
||
let mut u = 0.0f32;
|
||
let mut v = 0.0f32;
|
||
if let Some(p) = &sup {
|
||
let shift = if st.cramped { self.k(C::SuperscriptShiftUpCramped, st) } else { self.k(C::SuperscriptShiftUp, st) };
|
||
u = shift.max(p.d + self.k(C::SuperscriptBottomMin, st));
|
||
if !char_base {
|
||
u = u.max(ba - self.k(C::SuperscriptBaselineDropMax, st));
|
||
}
|
||
}
|
||
if let Some(b) = &sub {
|
||
v = self.k(C::SubscriptShiftDown, st).max(b.a - self.k(C::SubscriptTopMax, st));
|
||
if !char_base {
|
||
v = v.max(bd + self.k(C::SubscriptBaselineDropMin, st));
|
||
}
|
||
}
|
||
if let (Some(p), Some(b)) = (&sup, &sub) {
|
||
let gap = (u - p.d) - (b.a - v);
|
||
let min = self.k(C::SubSuperscriptGapMin, st);
|
||
if gap < min {
|
||
v += min - gap;
|
||
let lift = self.k(C::SuperscriptBottomMaxWithSubscript, st) - (u - p.d);
|
||
if lift > 0.0 {
|
||
u += lift;
|
||
v -= lift;
|
||
}
|
||
}
|
||
}
|
||
let space = self.k(C::SpaceAfterScript, st);
|
||
let mut w = bw;
|
||
if let Some(p) = sup {
|
||
let x = bw + ic;
|
||
w = w.max(x + p.w);
|
||
out.put(p, x, u);
|
||
}
|
||
if let Some(b) = sub {
|
||
let x = bw;
|
||
w = w.max(x + b.w);
|
||
out.put(b, x, -v);
|
||
}
|
||
out.w = w + if w > bw { space } else { 0.0 };
|
||
out.first = first;
|
||
out.last = last;
|
||
out
|
||
}
|
||
|
||
/// A vertical size variant or assembly of `gid` at least `target` tall (`None` without a
|
||
/// `MATH` table or construction for the glyph).
|
||
fn stretch_v(&self, gid: u32, target: f32, size: f32) -> Option<Bx> {
|
||
let t = self.t.as_ref()?;
|
||
let con = t.vert.get(&gid)?;
|
||
self.stretch(con, t, target, size, true)
|
||
}
|
||
|
||
fn stretch_h(&self, gid: u32, target: f32, size: f32) -> Option<Bx> {
|
||
let t = self.t.as_ref()?;
|
||
let con = t.horiz.get(&gid)?;
|
||
self.stretch(con, t, target, size, false)
|
||
}
|
||
|
||
fn stretch(&self, con: &Construction, t: &MathTable, target: f32, size: f32, vertical: bool) -> Option<Bx> {
|
||
let k = size / self.upem;
|
||
let target_fu = target / k;
|
||
// The first variant big enough.
|
||
if let Some((g, _)) = con.variants.iter().find(|(_, adv)| *adv >= target_fu - 1.0) {
|
||
return Some(self.glyph_id(*g, size, self.color));
|
||
}
|
||
if con.parts.is_empty() {
|
||
return con.variants.last().map(|(g, _)| self.glyph_id(*g, size, self.color));
|
||
}
|
||
// Assembly: repeat extenders until the parts span the target.
|
||
let o = t.min_connector_overlap;
|
||
let fixed: f32 = con.parts.iter().filter(|p| !p.extender).map(|p| p.full_advance).sum();
|
||
let ext: f32 = con.parts.iter().filter(|p| p.extender).map(|p| p.full_advance).sum();
|
||
let n_fixed = con.parts.iter().filter(|p| !p.extender).count();
|
||
let n_ext = con.parts.len() - n_fixed;
|
||
let mut reps = 0usize;
|
||
if n_ext > 0 {
|
||
while reps < 100 {
|
||
let n = n_fixed + n_ext * reps;
|
||
let len = fixed + ext * reps as f32 - o * n.saturating_sub(1) as f32;
|
||
if len >= target_fu {
|
||
break;
|
||
}
|
||
reps += 1;
|
||
}
|
||
}
|
||
let seq: Vec<_> = con.parts.iter().flat_map(|p| std::iter::repeat_n(*p, if p.extender { reps } else { 1 })).collect();
|
||
if seq.is_empty() {
|
||
return None;
|
||
}
|
||
let joints = seq.len().saturating_sub(1);
|
||
let max_len: f32 = seq.iter().map(|p| p.full_advance).sum::<f32>() - o * joints as f32;
|
||
// Spread the excess over the joints as extra overlap (within each connector's length).
|
||
let excess = (max_len - target_fu).max(0.0);
|
||
let per = if joints > 0 { excess / joints as f32 } else { 0.0 };
|
||
let mut b = Bx::default();
|
||
let mut pos = 0.0f32;
|
||
let mut glyphs = Vec::with_capacity(seq.len());
|
||
for (i, p) in seq.iter().enumerate() {
|
||
if vertical {
|
||
let y0 = glyph_bounds(&self.face, p.gid).map(|bb| bb.1).unwrap_or(0.0);
|
||
glyphs.push((p.gid, 0.0, (pos - y0) * k));
|
||
b.w = b.w.max(self.face.advance(p.gid) as f32 * k);
|
||
} else {
|
||
glyphs.push((p.gid, pos * k, 0.0));
|
||
if let Some((_, y0, _, y1)) = glyph_bounds(&self.face, p.gid) {
|
||
b.a = b.a.max(y1 * k);
|
||
b.d = b.d.max(-y0 * k);
|
||
}
|
||
}
|
||
if let Some(next) = seq.get(i + 1) {
|
||
let room = p.end_connector.min(next.start_connector).max(o);
|
||
let ov = (o + per).min(room);
|
||
pos += p.full_advance - ov;
|
||
} else {
|
||
pos += p.full_advance;
|
||
}
|
||
}
|
||
if vertical {
|
||
b.a = pos * k;
|
||
b.d = 0.0;
|
||
} else {
|
||
b.w = pos * k;
|
||
}
|
||
b.items.push(MItem::Glyphs { face: self.face, size, color: self.color, synth_bold: false, synth_italic: false, glyphs, text: String::new() });
|
||
Some(b)
|
||
}
|
||
|
||
/// A delimiter (or separator) glyph covering `a`/`d` around the axis (or the content exactly
|
||
/// with `exact`).
|
||
fn delimiter(&self, c: char, a: f32, d: f32, st: St, grow: bool, exact: bool) -> Bx {
|
||
let size = self.size(st);
|
||
let axis = self.axis(st);
|
||
let mut glyph = self.char_glyphs(c, size, 0);
|
||
glyph.glyph = None;
|
||
if !grow {
|
||
return glyph;
|
||
}
|
||
let target = if exact { a + d } else { 2.0 * (a - axis).max(d + axis) };
|
||
// Small content keeps the plain glyph where it sits.
|
||
if glyph.a + glyph.d >= target - size * 0.02 {
|
||
return glyph;
|
||
}
|
||
let gid = self.face.glyph_for(c);
|
||
let Some(big) = (if gid != 0 { self.stretch_v(gid, target, size) } else { None }) else { return glyph };
|
||
let dy = if exact { (a - d) / 2.0 - (big.a - big.d) / 2.0 } else { axis - (big.a - big.d) / 2.0 };
|
||
let mut out = Bx { w: big.w, ..Default::default() };
|
||
out.put(big, 0.0, dy);
|
||
out
|
||
}
|
||
|
||
fn node(&mut self, n: &MNode, st: St, depth: usize) -> Bx {
|
||
if depth > MAX_DEPTH {
|
||
return Bx::default();
|
||
}
|
||
let d = depth + 1;
|
||
let em = self.em(st);
|
||
match n {
|
||
MNode::Run(r) => {
|
||
let atoms: Vec<Bx> = self
|
||
.run_atoms(r, st, 0, &self.path.clone())
|
||
.into_iter()
|
||
.map(|p| match p {
|
||
RunPart::Atom(b) | RunPart::Align(b) => b,
|
||
})
|
||
.collect();
|
||
compose(atoms, st, em)
|
||
}
|
||
MNode::Frac { kind, num, den } => self.frac(*kind, num, den, st, d),
|
||
MNode::Script { kind, base, sub, sup } => {
|
||
let b = self.sub(0, base, st, d);
|
||
let char_base = b.glyph.is_some();
|
||
let sub_b = if matches!(kind, ScriptKind::Sub | ScriptKind::SubSup | ScriptKind::Pre) {
|
||
Some(self.sub(1, sub, st.script().cramp(), d))
|
||
} else {
|
||
None
|
||
};
|
||
let sup_b = if matches!(kind, ScriptKind::Sup | ScriptKind::SubSup | ScriptKind::Pre) {
|
||
Some(self.sub(2, sup, st.script(), d))
|
||
} else {
|
||
None
|
||
};
|
||
if *kind == ScriptKind::Pre {
|
||
// Pre-scripts: attach to an empty base, then put the base after them.
|
||
let empty = Bx::default();
|
||
let mut pre = self.attach(empty, sub_b, sup_b, st, false);
|
||
let pw = pre.w;
|
||
let (f, l) = (b.first, b.last);
|
||
pre.w = pw + b.w;
|
||
pre.put(b, pw, 0.0);
|
||
pre.first = f;
|
||
pre.last = l;
|
||
return pre;
|
||
}
|
||
self.attach(b, sub_b, sup_b, st, char_base)
|
||
}
|
||
MNode::Rad { deg, deg_hide, e } => self.radical(deg, *deg_hide, e, st, d),
|
||
MNode::Nary { .. } | MNode::Func { .. } => {
|
||
let atoms = self.op_atoms(n, st, d);
|
||
compose(atoms, st, em)
|
||
}
|
||
MNode::Delim { beg, end, sep, grow, shp_match, elems } => {
|
||
let parts: Vec<Bx> = elems.iter().enumerate().map(|(k, e)| self.sub(k, e, st, d)).collect();
|
||
let a = parts.iter().map(|b| b.a).fold(0.0f32, f32::max);
|
||
let dd = parts.iter().map(|b| b.d).fold(0.0f32, f32::max);
|
||
let mut row: Vec<Bx> = Vec::new();
|
||
if let Some(c) = beg {
|
||
row.push(self.delimiter(*c, a, dd, st, *grow, *shp_match).cls(Cls::Open));
|
||
}
|
||
let n = parts.len();
|
||
for (i, p) in parts.into_iter().enumerate() {
|
||
row.push(p.cls(Cls::Inner));
|
||
if i + 1 < n
|
||
&& let Some(c) = sep
|
||
{
|
||
row.push(self.delimiter(*c, a, dd, st, *grow, *shp_match).cls(Cls::Ord));
|
||
}
|
||
}
|
||
if let Some(c) = end {
|
||
row.push(self.delimiter(*c, a, dd, st, *grow, *shp_match).cls(Cls::Close));
|
||
}
|
||
// No spacing inside the brackets.
|
||
let mut out = hcat(row);
|
||
out.first = if beg.is_some() { Cls::Open } else { Cls::Inner };
|
||
out.last = if end.is_some() { Cls::Close } else { Cls::Inner };
|
||
out
|
||
}
|
||
MNode::Lim { upper, e, lim } => {
|
||
let base = self.sub(0, e, st, d);
|
||
let ls = St { upright: false, ..st.script() };
|
||
let l = self.sub(1, lim, if *upper { ls } else { ls.cramp() }, d);
|
||
let (f, la) = (base.first, base.last);
|
||
let mut out =
|
||
if *upper { self.limits(base, None, Some(l), 0.0, st, false) } else { self.limits(base, Some(l), None, 0.0, st, false) };
|
||
out.first = f;
|
||
out.last = la;
|
||
out
|
||
}
|
||
MNode::Acc { chr, e } => self.accent(*chr, e, st, d),
|
||
MNode::Bar { top, e } => {
|
||
let b = self.sub(0, e, st, d);
|
||
let (w, a, dd) = (b.w, b.a, b.d);
|
||
let mut out = Bx { w, ..Default::default() };
|
||
out.put(b, 0.0, 0.0);
|
||
let color = self.color;
|
||
if *top {
|
||
let t = self.k(C::OverbarRuleThickness, st).max(0.4);
|
||
let y = a + self.k(C::OverbarVerticalGap, st);
|
||
out.rect(0.0, y, w, t, color);
|
||
out.a = out.a.max(y + t + self.k(C::OverbarExtraAscender, st));
|
||
} else {
|
||
let t = self.k(C::UnderbarRuleThickness, st).max(0.4);
|
||
let y = -dd - self.k(C::UnderbarVerticalGap, st) - t;
|
||
out.rect(0.0, y, w, t, color);
|
||
out.d = out.d.max(-y + self.k(C::UnderbarExtraDescender, st));
|
||
}
|
||
out
|
||
}
|
||
MNode::BorderBox { hide, strike, e } => {
|
||
let b = self.sub(0, e, st, d);
|
||
let t = self.k(C::FractionRuleThickness, st).max(0.4);
|
||
let pad = em * 0.12;
|
||
let (w, a, dd) = (b.w + 2.0 * pad, b.a + pad, b.d + pad);
|
||
let mut out = Bx { w: w + 2.0 * t, ..Default::default() };
|
||
out.put(b, pad + t, 0.0);
|
||
let color = self.color;
|
||
let [ht, hb, hl, hr] = *hide;
|
||
let (x0, x1, y0, y1) = (0.0, w + 2.0 * t, -dd - t, a + t);
|
||
if !ht {
|
||
out.rect(x0, a, x1 - x0, t, color);
|
||
}
|
||
if !hb {
|
||
out.rect(x0, y0, x1 - x0, t, color);
|
||
}
|
||
if !hl {
|
||
out.rect(x0, y0, t, y1 - y0, color);
|
||
}
|
||
if !hr {
|
||
out.rect(x1 - t, y0, t, y1 - y0, color);
|
||
}
|
||
let [sh, sv, bltr, tlbr] = *strike;
|
||
let mid = (a - dd) / 2.0;
|
||
let line = |out: &mut Bx, p: (f32, f32, f32, f32)| {
|
||
out.items.push(MItem::Line { x0: p.0, y0: p.1, x1: p.2, y1: p.3, width: t, color, dotted: false })
|
||
};
|
||
if sh {
|
||
line(&mut out, (x0, mid, x1, mid));
|
||
}
|
||
if sv {
|
||
line(&mut out, ((x0 + x1) / 2.0, y0, (x0 + x1) / 2.0, y1));
|
||
}
|
||
if bltr {
|
||
line(&mut out, (x0, y0, x1, y1));
|
||
}
|
||
if tlbr {
|
||
line(&mut out, (x0, y1, x1, y0));
|
||
}
|
||
out.a = out.a.max(y1);
|
||
out.d = out.d.max(-y0);
|
||
out
|
||
}
|
||
MNode::Boxed { e } => self.sub(0, e, st, d),
|
||
MNode::GroupChr { chr, top, e } => {
|
||
let b = self.sub(0, e, st, d);
|
||
let size = self.size(st);
|
||
let gid = self.face.glyph_for(*chr);
|
||
let g = self.stretch_h(gid, b.w, size).unwrap_or_else(|| self.char_glyphs(*chr, size, 0));
|
||
let w = b.w.max(g.w);
|
||
let (ba, bd, bw) = (b.a, b.d, b.w);
|
||
let (ga, gd, gw) = (g.a, g.d, g.w);
|
||
let mut out = Bx { w, ..Default::default() };
|
||
out.put(b, (w - bw) / 2.0, 0.0);
|
||
if *top {
|
||
let y = ba + self.k(C::StretchStackGapAboveMin, st) + gd;
|
||
out.put(g, (w - gw) / 2.0, y);
|
||
} else {
|
||
let y = -bd - self.k(C::StretchStackGapBelowMin, st) - ga;
|
||
out.put(g, (w - gw) / 2.0, y);
|
||
}
|
||
out
|
||
}
|
||
MNode::EqArr { rows } => self.eq_arr(rows, st, d),
|
||
MNode::Matrix { rows, col_jc } => self.matrix(rows, col_jc, st, d),
|
||
MNode::Phant { show, zero_wid, zero_asc, zero_desc, e } => {
|
||
let mut b = self.sub(0, e, st, d);
|
||
if !show {
|
||
b.items.clear();
|
||
}
|
||
if *zero_wid {
|
||
b.w = 0.0;
|
||
}
|
||
if *zero_asc {
|
||
b.a = 0.0;
|
||
}
|
||
if *zero_desc {
|
||
b.d = 0.0;
|
||
}
|
||
b.glyph = None;
|
||
b
|
||
}
|
||
}
|
||
}
|
||
|
||
fn frac(&mut self, kind: FracKind, num: &[MNode], den: &[MNode], st: St, d: usize) -> Bx {
|
||
let em = self.em(st);
|
||
match kind {
|
||
FracKind::Linear => {
|
||
let n = self.sub(0, num, st, d);
|
||
let dn = self.sub(1, den, st, d);
|
||
let slash = self.glyphs(self.face, "/", self.size(st), st.level, self.color, (false, false)).cls(Cls::Ord);
|
||
hcat(vec![n, slash, dn])
|
||
}
|
||
FracKind::Skewed => {
|
||
let n = self.sub(0, num, st.script(), d);
|
||
let dn = self.sub(1, den, st.script().cramp(), d);
|
||
let size = self.size(st);
|
||
let slash = self.glyphs(self.face, "\u{2044}", size, 0, self.color, (false, false));
|
||
let gap = self.k(C::SkewedFractionHorizontalGap, st) / 2.0;
|
||
let (nw, sw) = (n.w, slash.w);
|
||
let mut out = Bx { w: n.w + gap + slash.w + gap + dn.w, ..Default::default() };
|
||
let up = self.axis(st) + self.k(C::SkewedFractionVerticalGap, st) / 2.0;
|
||
out.put(n, 0.0, up.max(0.0));
|
||
out.put(slash, nw + gap, 0.0);
|
||
out.put(dn, nw + gap + sw + gap, -self.k(C::SkewedFractionVerticalGap, st) / 2.0);
|
||
out
|
||
}
|
||
FracKind::Bar | FracKind::NoBar => {
|
||
let cs = st.frac();
|
||
let n = self.sub(0, num, St { cramped: st.cramped, ..cs }, d);
|
||
let dn = self.sub(1, den, cs.cramp(), d);
|
||
let axis = self.axis(st);
|
||
let (u, v) = if kind == FracKind::Bar {
|
||
let t = self.k(C::FractionRuleThickness, st);
|
||
let (su, sd, gn, gd) = if st.display {
|
||
(
|
||
self.k(C::FractionNumeratorDisplayStyleShiftUp, st),
|
||
self.k(C::FractionDenominatorDisplayStyleShiftDown, st),
|
||
self.k(C::FractionNumDisplayStyleGapMin, st),
|
||
self.k(C::FractionDenomDisplayStyleGapMin, st),
|
||
)
|
||
} else {
|
||
(
|
||
self.k(C::FractionNumeratorShiftUp, st),
|
||
self.k(C::FractionDenominatorShiftDown, st),
|
||
self.k(C::FractionNumeratorGapMin, st),
|
||
self.k(C::FractionDenominatorGapMin, st),
|
||
)
|
||
};
|
||
(su.max(gn + t / 2.0 + axis + n.d), sd.max(gd + t / 2.0 - axis + dn.a))
|
||
} else {
|
||
let (mut su, mut sd, gap) = if st.display {
|
||
(
|
||
self.k(C::StackTopDisplayStyleShiftUp, st),
|
||
self.k(C::StackBottomDisplayStyleShiftDown, st),
|
||
self.k(C::StackDisplayStyleGapMin, st),
|
||
)
|
||
} else {
|
||
(self.k(C::StackTopShiftUp, st), self.k(C::StackBottomShiftDown, st), self.k(C::StackGapMin, st))
|
||
};
|
||
let g = (su - n.d) - (dn.a - sd);
|
||
if g < gap {
|
||
su += (gap - g) / 2.0;
|
||
sd += (gap - g) / 2.0;
|
||
}
|
||
(su, sd)
|
||
};
|
||
let pad = em * 0.08;
|
||
let w = n.w.max(dn.w) + 2.0 * pad;
|
||
let (nw, dw) = (n.w, dn.w);
|
||
let mut out = Bx { w, ..Default::default() };
|
||
out.put(n, (w - nw) / 2.0, u);
|
||
out.put(dn, (w - dw) / 2.0, -v);
|
||
if kind == FracKind::Bar {
|
||
let t = self.k(C::FractionRuleThickness, st).max(0.4);
|
||
out.rect(pad * 0.5, axis - t / 2.0, w - pad, t, self.color);
|
||
}
|
||
out
|
||
}
|
||
}
|
||
}
|
||
|
||
fn radical(&mut self, deg: &[MNode], deg_hide: bool, e: &[MNode], st: St, d: usize) -> Bx {
|
||
let b = self.sub(1, e, st.cramp(), d);
|
||
let size = self.size(st);
|
||
let t = self.k(C::RadicalRuleThickness, st).max(0.4);
|
||
let mut gap = if st.display { self.k(C::RadicalDisplayStyleVerticalGap, st) } else { self.k(C::RadicalVerticalGap, st) };
|
||
let target = b.a + b.d + gap + t;
|
||
let gid = self.face.glyph_for('√');
|
||
let g = self.stretch_v(gid, target, size).unwrap_or_else(|| {
|
||
let mut g = self.glyphs(self.face, "√", size, 0, self.color, (false, false));
|
||
g.glyph = None;
|
||
g
|
||
});
|
||
let extra = (g.a + g.d) - target;
|
||
if extra > 0.0 {
|
||
gap += extra / 2.0;
|
||
}
|
||
// The sign's top meets the rule over the base.
|
||
let top = b.a + gap + t;
|
||
let dy = top - g.a;
|
||
let (ga, gd, gw) = (g.a, g.d, g.w);
|
||
let mut sign = Bx { w: gw, ..Default::default() };
|
||
sign.put(g, 0.0, dy);
|
||
// Degree.
|
||
let mut x0 = 0.0f32;
|
||
let mut out = Bx::default();
|
||
if !deg_hide {
|
||
let db = self.sub(0, deg, St { level: (st.level + 2).min(2), display: false, cramped: true, upright: st.upright }, d);
|
||
let kb = self.k(C::RadicalKernBeforeDegree, st);
|
||
let ka = self.k(C::RadicalKernAfterDegree, st);
|
||
let raise = self.t.as_ref().map_or(60.0, |t| t.radical_degree_bottom_raise_percent) / 100.0 * (ga + gd);
|
||
let y = (dy - gd) + raise;
|
||
let dw = db.w;
|
||
out.put(db, kb, y);
|
||
x0 = (kb + dw + ka).max(0.0);
|
||
}
|
||
let bw = b.w;
|
||
out.put(sign, x0, 0.0);
|
||
out.rect(x0 + gw, top - t, bw, t, self.color);
|
||
out.put(b, x0 + gw, 0.0);
|
||
out.w = x0 + gw + bw;
|
||
out.a = out.a.max(top + self.k(C::RadicalExtraAscender, st));
|
||
out
|
||
}
|
||
|
||
fn accent(&mut self, chr: char, e: &[MNode], st: St, d: usize) -> Bx {
|
||
let b = self.sub(0, e, st.cramp(), d);
|
||
let size = self.size(st);
|
||
let color = self.color;
|
||
// Overline-style accents are rules.
|
||
if matches!(chr, '\u{305}' | '\u{AF}' | '\u{304}' | '\u{203E}') {
|
||
let (w, a) = (b.w, b.a);
|
||
let mut out = Bx { w, ..Default::default() };
|
||
let (f, l) = (b.first, b.last);
|
||
out.put(b, 0.0, 0.0);
|
||
let t = self.k(C::OverbarRuleThickness, st).max(0.4);
|
||
let y = a + self.k(C::OverbarVerticalGap, st);
|
||
out.rect(0.0, y, w, t, color);
|
||
out.first = f;
|
||
out.last = l;
|
||
return out;
|
||
}
|
||
let gid = self.face.glyph_for(chr);
|
||
// Combining marks have no advance: compare the base with the accent's ink.
|
||
let ink = glyph_bounds(&self.face, gid).map(|(x0, _, x1, _)| (x1 - x0) * size / self.upem).unwrap_or(0.0);
|
||
let narrow = gid != 0 && b.w <= ink * 1.2;
|
||
let mut acc = if wide_accent(chr) && gid != 0 && !narrow {
|
||
self.stretch_h(gid, b.w, size).unwrap_or_else(|| self.glyph_id(gid, size, color))
|
||
} else if gid != 0 {
|
||
self.glyph_id(gid, size, color)
|
||
} else {
|
||
self.char_glyphs(chr, size, 0)
|
||
};
|
||
acc.glyph = None;
|
||
// Horizontal: the base's attachment point under the accent's.
|
||
let base_x = b.glyph.map(|(_, _, ax)| ax).unwrap_or(b.w / 2.0);
|
||
let acc_x = {
|
||
let single = acc.items.first().and_then(|it| match it {
|
||
MItem::Glyphs { glyphs, .. } if glyphs.len() == 1 => glyphs.first().map(|g| g.0),
|
||
_ => None,
|
||
});
|
||
let k = size / self.upem;
|
||
match single {
|
||
Some(g) => match self.t.as_ref().and_then(|t| t.top_accent.get(&g).copied()) {
|
||
Some(v) => v * k,
|
||
None => glyph_bounds(&self.face, g).map(|(x0, _, x1, _)| (x0 + x1) / 2.0 * k).unwrap_or(acc.w / 2.0),
|
||
},
|
||
None => acc.w / 2.0,
|
||
}
|
||
};
|
||
// Vertical: accents sit on an x-height base; taller bases push them up.
|
||
let abh = self.k(C::AccentBaseHeight, st);
|
||
let raise = (b.a - abh).max(0.0);
|
||
let (f, l, w, ic) = (b.first, b.last, b.w, b.ic);
|
||
let glyph = b.glyph;
|
||
let mut out = Bx { w, ..Default::default() };
|
||
out.put(b, 0.0, 0.0);
|
||
let mut x = base_x - acc_x;
|
||
if x < 0.0 && acc.w > w {
|
||
x = (w - acc.w) / 2.0;
|
||
}
|
||
out.put(acc, x, raise);
|
||
out.first = f;
|
||
out.last = l;
|
||
out.ic = ic;
|
||
out.glyph = glyph.filter(|_| false);
|
||
out
|
||
}
|
||
|
||
/// Rows stacked and centred on the math axis; `gap`: least space between rows, `min_skip`:
|
||
/// least baseline-to-baseline distance.
|
||
fn stack_rows(&self, rows: Vec<(Bx, f32)>, st: St, gap: f32, min_skip: f32) -> (Bx, Vec<f32>) {
|
||
// (row box, x) → baselines going down.
|
||
let mut ys = Vec::with_capacity(rows.len());
|
||
let mut y = 0.0f32;
|
||
let mut prev_d: Option<f32> = None;
|
||
for (b, _) in &rows {
|
||
if let Some(pd) = prev_d {
|
||
y -= (pd + gap + b.a).max(min_skip);
|
||
}
|
||
ys.push(y);
|
||
prev_d = Some(b.d);
|
||
}
|
||
let top = rows.first().map_or(0.0, |(b, _)| b.a);
|
||
let bottom = rows.last().map_or(0.0, |(b, _)| b.d) - ys.last().copied().unwrap_or(0.0);
|
||
// Centre on the axis.
|
||
let shift = self.axis(st) + (bottom - top) / 2.0;
|
||
let mut out = Bx::default();
|
||
let mut bases = Vec::with_capacity(ys.len());
|
||
for ((b, x), yy) in rows.into_iter().zip(ys) {
|
||
out.w = out.w.max(x + b.w);
|
||
out.put(b, x, yy + shift);
|
||
bases.push(yy + shift);
|
||
}
|
||
(out, bases)
|
||
}
|
||
|
||
fn eq_arr(&mut self, rows: &[Arg], st: St, d: usize) -> Bx {
|
||
self.eq_rows(rows, st, d).0
|
||
}
|
||
|
||
/// Equation-array rows aligned at `&`, and each row's baseline.
|
||
fn eq_rows(&mut self, rows: &[Arg], st: St, d: usize) -> (Bx, Vec<f32>) {
|
||
let em = self.em(st);
|
||
let me = self.node_stack.last().copied().unwrap_or(0);
|
||
// Each row: segments split at `&`.
|
||
let mut segs_rows: Vec<Vec<Bx>> = Vec::new();
|
||
let mut row_paths = Vec::new();
|
||
for (ri, r) in rows.iter().take(1000).enumerate() {
|
||
self.path.push((me, ri));
|
||
row_paths.push(self.path.clone());
|
||
let atoms = if r.is_empty() { vec![(self.placeholder(st), 0)] } else { self.row(r, st, d, 0) };
|
||
self.path.pop();
|
||
let n = atoms.iter().map(|(_, s)| *s + 1).max().unwrap_or(1).min(64);
|
||
let all: Vec<Bx> = atoms.iter().map(|(b, _)| b.clone()).collect();
|
||
let gaps = gaps_between(&all, st, em);
|
||
let mut segs: Vec<Vec<Bx>> = vec![Vec::new(); n];
|
||
for (i, (mut b, s)) in atoms.into_iter().enumerate() {
|
||
// Keep the spacing before an atom with the atom.
|
||
let g = gaps.get(i).copied().unwrap_or(0.0);
|
||
if g > 0.0 {
|
||
let mut wrapped = Bx { w: g + b.w, a: b.a, d: b.d, first: b.first, last: b.last, ..Default::default() };
|
||
wrapped.put(b, g, 0.0);
|
||
b = wrapped;
|
||
}
|
||
if let Some(seg) = segs.get_mut(s.min(n - 1)) {
|
||
seg.push(b);
|
||
}
|
||
}
|
||
segs_rows.push(segs.into_iter().map(hcat).collect());
|
||
}
|
||
let ncols = segs_rows.iter().map(|r| r.len()).max().unwrap_or(1);
|
||
let mut col_w = vec![0.0f32; ncols];
|
||
for r in &segs_rows {
|
||
for (i, b) in r.iter().enumerate() {
|
||
if let Some(c) = col_w.get_mut(i) {
|
||
*c = c.max(b.w);
|
||
}
|
||
}
|
||
}
|
||
let total: f32 = col_w.iter().sum();
|
||
let mut placed = Vec::new();
|
||
for (r, path) in segs_rows.into_iter().zip(row_paths) {
|
||
let mut row = Bx::default();
|
||
let mut x = 0.0f32;
|
||
let single = r.len() == 1 && ncols > 1;
|
||
for (i, b) in r.into_iter().enumerate() {
|
||
let cw = col_w.get(i).copied().unwrap_or(0.0);
|
||
let bw = b.w;
|
||
let dx = if ncols == 1 {
|
||
0.0
|
||
} else if single {
|
||
// A row without `&` aligns with the first column's right edge.
|
||
cw - bw
|
||
} else if i % 2 == 0 {
|
||
cw - bw
|
||
} else {
|
||
0.0
|
||
};
|
||
row.put(b, x + dx, 0.0);
|
||
x += cw;
|
||
row.w = row.w.max(x);
|
||
}
|
||
let rx = if ncols == 1 { (total - row.w) / 2.0 } else { 0.0 };
|
||
finalize(&mut row, &path, em);
|
||
placed.push((row, rx.max(0.0)));
|
||
}
|
||
let (mut out, bases) = self.stack_rows(placed, st, em * 0.1, em * 1.1);
|
||
out.w = out.w.max(total);
|
||
(out, bases)
|
||
}
|
||
|
||
/// A display equation with numbers (`#`): the equation centred on the line, each row's
|
||
/// number at the right margin. Automatic numbers (`#` alone) count up from `counter`.
|
||
fn numbered(&mut self, rows: &[Arg], st: St, avail: f32, counter: &mut u32) -> Bx {
|
||
let em = self.em(st);
|
||
let parts: Vec<(Arg, Arg, usize)> =
|
||
rows.iter().take(1000).map(|r| split_number(r).unwrap_or_else(|| (r.clone(), Vec::new(), usize::MAX))).collect();
|
||
self.node_stack.push(0);
|
||
let eq_rows: Vec<Arg> = parts.iter().map(|(e, _, _)| e.clone()).collect();
|
||
let (eq, bases) = self.eq_rows(&eq_rows, st, 1);
|
||
// Numbers.
|
||
let mut nums = Vec::new();
|
||
for (ri, (_, num, hash)) in parts.iter().enumerate() {
|
||
if *hash == usize::MAX {
|
||
nums.push(None);
|
||
continue;
|
||
}
|
||
self.path.push((0, ri));
|
||
let path = self.path.clone();
|
||
// Every numbered equation counts; `#` alone shows the count.
|
||
*counter = counter.saturating_add(1);
|
||
let b = if num.is_empty() {
|
||
let r = MRun { text: format!("({counter})"), sty: Some(MSty::Plain), ..Default::default() };
|
||
let mut b = compose(
|
||
self.run_atoms(&r, St { upright: true, ..st }, 0, &path)
|
||
.into_iter()
|
||
.map(|p| match p {
|
||
RunPart::Atom(b) | RunPart::Align(b) => b,
|
||
})
|
||
.collect(),
|
||
st,
|
||
em,
|
||
);
|
||
b.slots.clear();
|
||
b.slot(&path, hash + 1, 0.0);
|
||
b
|
||
} else {
|
||
let atoms = self.row(num, st, 2, hash + 1);
|
||
let mut b = compose(atoms.into_iter().map(|(b, _)| b).collect(), st, em);
|
||
b.slot(&path, hash + 1, 0.0);
|
||
b
|
||
};
|
||
let mut b = b;
|
||
finalize(&mut b, &path, em);
|
||
self.path.pop();
|
||
nums.push(Some(b));
|
||
}
|
||
self.node_stack.pop();
|
||
let num_w = nums.iter().flatten().map(|b| b.w).fold(0.0f32, f32::max);
|
||
let gap = em;
|
||
let mut x_eq = ((avail - eq.w) / 2.0).max(0.0);
|
||
if x_eq + eq.w + gap + num_w > avail {
|
||
x_eq = (avail - num_w - gap - eq.w).max(0.0);
|
||
}
|
||
let width = avail.max(x_eq + eq.w + gap + num_w);
|
||
let mut out = Bx { w: width, ..Default::default() };
|
||
out.put(eq, x_eq, 0.0);
|
||
for (b, y) in nums.into_iter().zip(bases) {
|
||
if let Some(b) = b {
|
||
let bw = b.w;
|
||
out.put(b, width - bw, y);
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// A display equation, broken into lines where it is wider than `avail` or a run asks for a
|
||
/// manual break (`m:brk`). Breaks fall only at top-level binary operators and relations (never
|
||
/// inside a fraction, radical, script or matrix), before or after the operator as `m:brkBin`
|
||
/// says; lines are filled greedily and a term wider than the line overflows it. The first line
|
||
/// starts at the left; the rest are indented by `m:wrapIndent`, set flush right with
|
||
/// `m:wrapRight`, or aligned at an operator of the first line (`m:alnAt`).
|
||
fn wrapped(&mut self, a: &[MNode], st: St, avail: f32, mp: &MathProps) -> Bx {
|
||
let em = self.em(st);
|
||
let atoms: Vec<Bx> = self.row(a, st, 0, 0).into_iter().map(|(b, _)| b).collect();
|
||
let gaps = gaps_between(&atoms, st, em);
|
||
let total: f32 = atoms.iter().map(|b| b.w).sum::<f32>() + gaps.iter().sum::<f32>();
|
||
let manual = atoms.iter().skip(1).any(|b| b.brk.is_some());
|
||
if atoms.len() < 2 || (!manual && total <= avail + 0.01) {
|
||
let mut b = compose(atoms, st, em);
|
||
finalize(&mut b, &self.path, em);
|
||
return b;
|
||
}
|
||
let cls = classes(&atoms);
|
||
let n = atoms.len();
|
||
let is_op = |i: usize| -> bool {
|
||
atoms.get(i).is_some_and(|b| b.glyph.is_some() && !b.transparent)
|
||
&& cls.get(i).is_some_and(|c| matches!(c.0, Cls::Bin | Cls::Rel) && c.0 == c.1)
|
||
};
|
||
// Chunk starts (after the first): the atom index → forced (`Some(alnAt)`) and the
|
||
// operator repeated at the start of the line it begins.
|
||
let mut starts: std::collections::BTreeMap<usize, (Option<u8>, Option<usize>)> = std::collections::BTreeMap::new();
|
||
for i in 1..n {
|
||
let op = is_op(i);
|
||
let at = if op && mp.brk_bin != BrkBin::Before { i + 1 } else { i };
|
||
if at == 0 || at >= n {
|
||
continue;
|
||
}
|
||
let repeat = (op && mp.brk_bin == BrkBin::Repeat).then_some(i);
|
||
let forced = atoms.get(i).and_then(|b| b.brk);
|
||
if op || forced.is_some() {
|
||
let e = starts.entry(at).or_insert((None, None));
|
||
if forced.is_some() {
|
||
e.0 = forced;
|
||
}
|
||
if repeat.is_some() {
|
||
e.1 = repeat;
|
||
}
|
||
}
|
||
}
|
||
// Chunks: [s, e) with their width inside and the gap before them.
|
||
let mut bounds: Vec<usize> = vec![0];
|
||
bounds.extend(starts.keys().copied());
|
||
bounds.push(n);
|
||
let width_of = |s: usize, e: usize| -> f32 {
|
||
(s..e).map(|i| atoms.get(i).map_or(0.0, |b| b.w) + if i > s { gaps.get(i).copied().unwrap_or(0.0) } else { 0.0 }).sum()
|
||
};
|
||
// The operator glyphs a repeated operator shows at the end of one line and the start of
|
||
// the next.
|
||
let repeats: std::collections::HashMap<usize, (Option<Bx>, Bx)> = starts
|
||
.values()
|
||
.filter_map(|v| v.1)
|
||
.filter_map(|i| {
|
||
let b = atoms.get(i)?;
|
||
let (end, start) = match op_char(b) {
|
||
Some('−' | '-') => match mp.brk_bin_sub {
|
||
BrkBinSub::MinusMinus => ('−', '−'),
|
||
BrkBinSub::MinusPlus => ('−', '+'),
|
||
BrkBinSub::PlusMinus => ('+', '−'),
|
||
},
|
||
Some(c) => (c, c),
|
||
None => return None,
|
||
};
|
||
let end = (Some(end) != op_char(b)).then(|| self.op_glyph(b, end, st)).flatten();
|
||
Some((i, (end, self.op_glyph(b, start, st)?)))
|
||
})
|
||
.collect();
|
||
let indent = if mp.wrap_indent.is_finite() { mp.wrap_indent.clamp(0.0, 1584.0) } else { 0.0 }.min(avail * 0.5);
|
||
// Greedy fill: lines as lists of chunks (index into `bounds`).
|
||
let mut lines: Vec<(usize, usize)> = Vec::new();
|
||
let mut cur_w = 0.0f32;
|
||
let mut auto = false;
|
||
for c in 0..bounds.len().saturating_sub(1) {
|
||
let (s, e) = (bounds.get(c).copied().unwrap_or(0), bounds.get(c + 1).copied().unwrap_or(n));
|
||
let w = width_of(s, e);
|
||
let lead = gaps.get(s).copied().unwrap_or(0.0);
|
||
let info = starts.get(&s).copied();
|
||
let copy_w = info.and_then(|v| v.1).and_then(|i| repeats.get(&i)).map_or(0.0, |r| r.1.w + lead);
|
||
let line_avail = if lines.len() <= 1 || mp.wrap_right { avail } else { avail - indent };
|
||
match lines.last_mut() {
|
||
Some(l) if info.is_some_and(|v| v.0.is_none()) && cur_w + lead + w <= line_avail + 0.01 => {
|
||
l.1 = c + 1;
|
||
cur_w += lead + w;
|
||
}
|
||
Some(_) if info.is_some_and(|v| v.0.is_none()) => {
|
||
auto = true;
|
||
lines.push((c, c + 1));
|
||
cur_w = copy_w + w;
|
||
}
|
||
_ => {
|
||
lines.push((c, c + 1));
|
||
cur_w = copy_w + w;
|
||
}
|
||
}
|
||
}
|
||
// Build each line's box.
|
||
let ops: Vec<bool> = (0..n).map(is_op).collect();
|
||
let path = self.path.clone();
|
||
let mut atoms: Vec<Option<Bx>> = atoms.into_iter().map(Some).collect();
|
||
let mut boxes: Vec<(Bx, Option<u8>)> = Vec::with_capacity(lines.len());
|
||
let mut ops_x: Vec<f32> = Vec::new();
|
||
for (li, &(c0, c1)) in lines.iter().enumerate() {
|
||
let (s, e) = (bounds.get(c0).copied().unwrap_or(0), bounds.get(c1).copied().unwrap_or(n));
|
||
let info = starts.get(&s).copied();
|
||
let next = lines.get(li + 1).and_then(|l| bounds.get(l.0)).and_then(|s| starts.get(s)).and_then(|v| v.1);
|
||
let mut row = Bx::default();
|
||
let mut x = 0.0f32;
|
||
let copy = info.and_then(|v| v.1).and_then(|i| repeats.get(&i)).map(|r| r.1.clone());
|
||
let has_copy = copy.is_some();
|
||
if let Some(cb) = copy {
|
||
let w = cb.w;
|
||
row.put(cb, 0.0, 0.0);
|
||
x = w;
|
||
}
|
||
for i in s..e {
|
||
let Some(mut b) = atoms.get_mut(i).and_then(Option::take) else { continue };
|
||
if i > s || has_copy {
|
||
x += gaps.get(i).copied().unwrap_or(0.0);
|
||
}
|
||
if next == Some(i)
|
||
&& let Some((Some(end), _)) = repeats.get(&i)
|
||
{
|
||
let mut r = end.clone();
|
||
let k = if b.w > 0.0 { r.w / b.w } else { 1.0 };
|
||
r.slots = std::mem::take(&mut b.slots)
|
||
.into_iter()
|
||
.map(|mut sl| {
|
||
sl.x *= k;
|
||
sl
|
||
})
|
||
.collect();
|
||
b = r;
|
||
}
|
||
if li == 0 && ops.get(i).copied().unwrap_or(false) {
|
||
ops_x.push(x);
|
||
}
|
||
let w = b.w;
|
||
row.put(b, x, 0.0);
|
||
x += w;
|
||
}
|
||
row.w = x;
|
||
finalize(&mut row, &path, em);
|
||
boxes.push((row, info.and_then(|v| v.0)));
|
||
}
|
||
let widest = boxes.iter().map(|(b, _)| b.w).fold(0.0f32, f32::max);
|
||
let right = if auto { avail.max(widest) } else { widest };
|
||
let mut placed = Vec::with_capacity(boxes.len());
|
||
for (li, (b, aln)) in boxes.into_iter().enumerate() {
|
||
let x = if li == 0 {
|
||
0.0
|
||
} else if let Some(ax) = aln.filter(|a| *a > 0).and_then(|a| ops_x.get(a as usize - 1)) {
|
||
*ax
|
||
} else if mp.wrap_right {
|
||
(right - b.w).max(0.0)
|
||
} else {
|
||
indent
|
||
};
|
||
placed.push((b, x));
|
||
}
|
||
// Stack the lines down from the first one's baseline.
|
||
let (gap, skip) = (em * 0.2, em * 1.25);
|
||
let mut out = Bx::default();
|
||
let mut y = 0.0f32;
|
||
let mut prev_d: Option<f32> = None;
|
||
for (b, x) in placed {
|
||
if let Some(pd) = prev_d {
|
||
y -= (pd + gap + b.a).max(skip);
|
||
}
|
||
prev_d = Some(b.d);
|
||
out.w = out.w.max(x + b.w);
|
||
out.put(b, x, y);
|
||
}
|
||
out.w = out.w.max(right);
|
||
out
|
||
}
|
||
|
||
/// Operator `c` drawn like the operator atom `like` (its face, size and colour), without
|
||
/// caret slots.
|
||
fn op_glyph(&self, like: &Bx, c: char, st: St) -> Option<Bx> {
|
||
let MItem::Glyphs { face, size, color, synth_bold, synth_italic, .. } = like.items.iter().find(|i| matches!(i, MItem::Glyphs { .. }))? else {
|
||
return None;
|
||
};
|
||
let face = if face.covers(c) { *face } else { self.face };
|
||
let mut b = self.glyphs(face, &c.to_string(), *size, st.level, *color, (*synth_bold, *synth_italic));
|
||
b.cuts.clear();
|
||
Some(b.cls(like.first))
|
||
}
|
||
|
||
fn matrix(&mut self, rows: &[Vec<Arg>], col_jc: &[ColJc], st: St, d: usize) -> Bx {
|
||
let em = self.em(st);
|
||
let mut cells: Vec<Vec<Bx>> = Vec::new();
|
||
let mut flat = 0usize;
|
||
for r in rows.iter().take(1000) {
|
||
let mut row = Vec::new();
|
||
for (ci, c) in r.iter().enumerate() {
|
||
if ci < 64 {
|
||
row.push(self.sub(flat, c, st, d));
|
||
}
|
||
flat += 1;
|
||
}
|
||
cells.push(row);
|
||
}
|
||
let ncols = cells.iter().map(|r| r.len()).max().unwrap_or(0);
|
||
let mut col_w = vec![0.0f32; ncols];
|
||
for r in &cells {
|
||
for (i, b) in r.iter().enumerate() {
|
||
if let Some(c) = col_w.get_mut(i) {
|
||
*c = c.max(b.w);
|
||
}
|
||
}
|
||
}
|
||
let gap = em * 0.8;
|
||
let mut placed = Vec::new();
|
||
for r in cells {
|
||
let mut row = Bx::default();
|
||
let mut x = 0.0f32;
|
||
for (i, b) in r.into_iter().enumerate() {
|
||
let cw = col_w.get(i).copied().unwrap_or(0.0);
|
||
let bw = b.w;
|
||
let dx = match col_jc.get(i).copied().unwrap_or_default() {
|
||
ColJc::Center => (cw - bw) / 2.0,
|
||
ColJc::Left => 0.0,
|
||
ColJc::Right => cw - bw,
|
||
};
|
||
row.put(b, x + dx, 0.0);
|
||
x += cw + gap;
|
||
}
|
||
row.w = (x - gap).max(0.0);
|
||
placed.push((row, 0.0));
|
||
}
|
||
let (mut out, _) = self.stack_rows(placed, st, em * 0.25, em * 1.2);
|
||
out.w = col_w.iter().sum::<f32>() + gap * ncols.saturating_sub(1) as f32;
|
||
out
|
||
}
|
||
}
|
||
|
||
/// The character an operator atom draws.
|
||
fn op_char(b: &Bx) -> Option<char> {
|
||
b.items.iter().find_map(|i| match i {
|
||
MItem::Glyphs { text, .. } => {
|
||
let mut cs = text.chars();
|
||
let c = cs.next()?;
|
||
cs.next().is_none().then_some(c)
|
||
}
|
||
_ => None,
|
||
})
|
||
}
|
||
|
||
enum RunPart {
|
||
Atom(Bx),
|
||
/// An `&` alignment mark (a zero-width atom holding its caret slots).
|
||
Align(Bx),
|
||
}
|
||
|
||
/// Give the caret slots of argument `path` in `b` the argument's height.
|
||
fn finalize(b: &mut Bx, path: &[(usize, usize)], em: f32) {
|
||
let (a, d) = (b.a.max(em * 0.72), b.d.max(em * 0.22));
|
||
for s in &mut b.slots {
|
||
if s.path == path && s.a <= 0.0 && s.d <= 0.0 {
|
||
s.a = a - s.y;
|
||
s.d = d + s.y;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Spacing before each atom (TeX rules: a binary operator after nothing, an operator, a
|
||
/// relation, an opening bracket or punctuation is ordinary, as is one before a relation,
|
||
/// closing bracket or punctuation).
|
||
fn gaps_between(atoms: &[Bx], st: St, em: f32) -> Vec<f32> {
|
||
let cls = classes(atoms);
|
||
let solid: Vec<usize> = (0..atoms.len()).filter(|i| atoms.get(*i).is_some_and(|b| !b.transparent)).collect();
|
||
let mut gaps = vec![0.0f32; atoms.len()];
|
||
for k in 1..solid.len() {
|
||
let (Some(&p), Some(&i)) = (solid.get(k - 1), solid.get(k)) else { continue };
|
||
let (Some(l), Some(r)) = (cls.get(p), cls.get(i)) else { continue };
|
||
if let Some(g) = gaps.get_mut(i) {
|
||
*g = spacing_mu(l.1, r.0, st) * em / 18.0;
|
||
}
|
||
}
|
||
gaps
|
||
}
|
||
|
||
/// Each atom's classes (first, last) in context: a binary operator where TeX makes it ordinary
|
||
/// (a unary minus) becomes ordinary.
|
||
fn classes(atoms: &[Bx]) -> Vec<(Cls, Cls)> {
|
||
let mut cls: Vec<(Cls, Cls)> = atoms.iter().map(|b| (b.first, b.last)).collect();
|
||
let solid: Vec<usize> = (0..atoms.len()).filter(|i| atoms.get(*i).is_some_and(|b| !b.transparent)).collect();
|
||
for (k, &i) in solid.iter().enumerate() {
|
||
let prev = k.checked_sub(1).and_then(|p| solid.get(p)).and_then(|p| cls.get(*p)).map(|c| c.1);
|
||
let next = solid.get(k + 1).and_then(|n| cls.get(*n)).map(|c| c.0);
|
||
if let Some(c) = cls.get_mut(i) {
|
||
if c.0 == Cls::Bin && matches!(prev, None | Some(Cls::Bin | Cls::Op | Cls::Rel | Cls::Open | Cls::Punct)) {
|
||
c.0 = Cls::Ord;
|
||
if c.1 == Cls::Bin {
|
||
c.1 = Cls::Ord;
|
||
}
|
||
}
|
||
if c.1 == Cls::Bin && matches!(next, None | Some(Cls::Rel | Cls::Close | Cls::Punct)) {
|
||
c.1 = Cls::Ord;
|
||
if c.0 == Cls::Bin {
|
||
c.0 = Cls::Ord;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
cls
|
||
}
|
||
|
||
/// Atoms side by side with inter-atom spacing.
|
||
fn compose(mut atoms: Vec<Bx>, st: St, em: f32) -> Bx {
|
||
if atoms.len() == 1
|
||
&& let Some(b) = atoms.pop()
|
||
{
|
||
return b;
|
||
}
|
||
let gaps = gaps_between(&atoms, st, em);
|
||
let first = atoms.iter().find(|b| !b.transparent).map(|b| b.first).unwrap_or_default();
|
||
let last = atoms.iter().rev().find(|b| !b.transparent).map(|b| b.last).unwrap_or_default();
|
||
let ic = atoms.last().map(|b| b.ic).unwrap_or(0.0);
|
||
let mut out = Bx::default();
|
||
let mut x = 0.0f32;
|
||
for (b, g) in atoms.into_iter().zip(gaps) {
|
||
x += g;
|
||
let w = b.w;
|
||
out.put(b, x, 0.0);
|
||
x += w;
|
||
}
|
||
out.w = x;
|
||
out.first = first;
|
||
out.last = last;
|
||
out.ic = ic;
|
||
out
|
||
}
|
||
|
||
/// Boxes side by side, no spacing.
|
||
fn hcat(parts: Vec<Bx>) -> Bx {
|
||
let first = parts.first().map(|b| b.first).unwrap_or_default();
|
||
let last = parts.last().map(|b| b.last).unwrap_or_default();
|
||
let mut out = Bx::default();
|
||
let mut x = 0.0f32;
|
||
for b in parts {
|
||
let w = b.w;
|
||
out.put(b, x, 0.0);
|
||
x += w;
|
||
}
|
||
out.w = x;
|
||
out.first = first;
|
||
out.last = last;
|
||
out
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use wordcraft_doc::math::Math;
|
||
|
||
fn rc() -> ResolvedChar {
|
||
wordcraft_doc::styles::StyleSheet::default().resolve_char(None, &Default::default())
|
||
}
|
||
|
||
fn lay(lin: &str, display: bool) -> MathLayout {
|
||
layout_equation(&Math::default(), lin, &rc(), display, 400.0, &MathProps::default(), &mut 0)
|
||
}
|
||
|
||
/// The glyphs of a laid-out equation by line: (baseline, [(x, text)]), top line first.
|
||
fn lines(m: &MathLayout) -> Vec<(f32, Vec<(f32, String)>)> {
|
||
let mut out: Vec<(f32, Vec<(f32, String)>)> = Vec::new();
|
||
for it in &m.items {
|
||
if let MItem::Glyphs { glyphs, text, .. } = it
|
||
&& let Some(&(_, x, y)) = glyphs.first()
|
||
{
|
||
match out.iter_mut().find(|l| (l.0 - y).abs() < 0.5) {
|
||
Some(l) => l.1.push((x, text.clone())),
|
||
None => out.push((y, vec![(x, text.clone())])),
|
||
}
|
||
}
|
||
}
|
||
out.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||
for l in &mut out {
|
||
l.1.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||
}
|
||
out
|
||
}
|
||
|
||
fn lay_in(math: &Math, lin: &str, display: bool, avail: f32, mp: &MathProps) -> MathLayout {
|
||
layout_equation(math, lin, &rc(), display, avail, mp, &mut 0)
|
||
}
|
||
|
||
#[test]
|
||
fn long_display_equations_wrap_before_operators_and_indent() {
|
||
let sum = "y=a+b+c+d+e+f+g+h+i+j+k+l+m+n+o+p+q+r+s+t+u+v+w";
|
||
let mp = MathProps::default();
|
||
let full = lay_in(&Math::default(), sum, true, 1e4, &mp);
|
||
assert_eq!(lines(&full).len(), 1, "fits on one line when there is room");
|
||
let avail = full.width * 0.45;
|
||
assert!(avail > 2.0 * mp.wrap_indent, "room for the indent: {avail}");
|
||
let m = lay_in(&Math::default(), sum, true, avail, &mp);
|
||
let ls = lines(&m);
|
||
assert!(ls.len() >= 2, "{ls:?}");
|
||
assert!((m.width - avail).abs() < 0.01, "the wrapped equation fills the line: {} vs {avail}", m.width);
|
||
for (k, (_, gs)) in ls.iter().enumerate() {
|
||
let (x0, first) = gs.first().cloned().unwrap();
|
||
if k == 0 {
|
||
// Math italic 𝑦 when a font has it; plain y where none does (FreeBSD CI has no fonts).
|
||
assert!(x0.abs() < 0.01 && (first == "𝑦" || first == "y"), "{gs:?}");
|
||
} else {
|
||
// Continuation lines start with the operator, one wrap indent in.
|
||
assert!((x0 - mp.wrap_indent).abs() < 0.01 && first == "+", "line {k}: {gs:?}");
|
||
}
|
||
for (x, t) in gs {
|
||
assert!(*x < avail, "line {k}: {t} at {x} past {avail}");
|
||
}
|
||
}
|
||
// Breaks after the operator, and the operator repeated, as the settings say.
|
||
let after = MathProps { brk_bin: BrkBin::After, ..Default::default() };
|
||
let ls = lines(&lay_in(&Math::default(), sum, true, avail, &after));
|
||
assert!(ls.len() >= 2 && ls[0].1.last().is_some_and(|g| g.1 == "+") && ls[1].1[0].1 != "+", "{ls:?}");
|
||
let repeat = MathProps { brk_bin: BrkBin::Repeat, wrap_right: true, ..Default::default() };
|
||
let m = lay_in(&Math::default(), sum, true, avail, &repeat);
|
||
let ls = lines(&m);
|
||
assert!(ls.len() >= 2 && ls[0].1.last().is_some_and(|g| g.1 == "+") && ls[1].1[0].1 == "+", "{ls:?}");
|
||
// Inline equations never wrap.
|
||
assert_eq!(lines(&lay_in(&Math::default(), sum, false, avail, &mp)).len(), 1);
|
||
// A term wider than the line overflows it rather than breaking inside.
|
||
let frac = lay_in(&Math::default(), "y=(a+b+c+d+e+f+g+h+i+j+k+l+m+n+o+p+q+r+s+t+u+v+w)/2", true, avail, &mp);
|
||
assert!(frac.width > avail && frac.items.iter().any(|i| matches!(i, MItem::Rect { .. })), "{}", frac.width);
|
||
}
|
||
|
||
#[test]
|
||
fn manual_breaks_start_new_lines() {
|
||
let mp = MathProps::default();
|
||
let nodes = vec![MNode::Run(MRun::new("a=b")), MNode::Run(MRun { brk: Some(0), ..MRun::new("+c") })];
|
||
let m = lay_in(&Math { nodes, ..Default::default() }, "", true, 400.0, &mp);
|
||
let ls = lines(&m);
|
||
assert_eq!(ls.len(), 2, "{ls:?}");
|
||
assert!((ls[1].1[0].0 - mp.wrap_indent).abs() < 0.01 && ls[1].1[0].1 == "+", "{ls:?}");
|
||
assert!(m.width < 400.0, "a manually broken equation is as wide as its lines");
|
||
// `m:alnAt`: the new line's operator sits under the first line's first operator.
|
||
let nodes = vec![MNode::Run(MRun::new("a=b")), MNode::Run(MRun { brk: Some(1), ..MRun::new("+c") })];
|
||
let ls = lines(&lay_in(&Math { nodes, ..Default::default() }, "", true, 400.0, &mp));
|
||
let eq_x = ls[0].1.iter().find(|g| g.1 == "=").unwrap().0;
|
||
assert!((ls[1].1[0].0 - eq_x).abs() < 0.01, "{ls:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn short_display_equations_stay_on_one_line() {
|
||
let mp = MathProps::default();
|
||
let one = lay_in(&Math::default(), "a+b=c", true, 400.0, &mp);
|
||
assert_eq!(lines(&one).len(), 1);
|
||
let plain = lay_in(&Math::default(), "a+b=c", false, 400.0, &mp);
|
||
assert!(one.width < 400.0 && (one.width - plain.width).abs() < 0.01, "{} vs {}", one.width, plain.width);
|
||
}
|
||
|
||
#[test]
|
||
fn fraction_is_stacked_with_a_rule() {
|
||
let flat = lay("ab", true);
|
||
let f = lay("a/b", true);
|
||
assert!(f.ascent > flat.ascent + 2.0 && f.descent > flat.descent + 2.0, "{f:?}");
|
||
assert!(f.items.iter().any(|i| matches!(i, MItem::Rect { .. })), "fraction rule");
|
||
assert!(f.width < flat.width * 1.2, "stacked, not side by side");
|
||
}
|
||
|
||
#[test]
|
||
fn superscript_is_raised_and_smaller() {
|
||
let m = lay("x^2", false);
|
||
let sizes: Vec<(f32, f32)> = m
|
||
.items
|
||
.iter()
|
||
.filter_map(|i| match i {
|
||
MItem::Glyphs { size, glyphs, .. } => glyphs.first().map(|g| (*size, g.2)),
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
assert_eq!(sizes.len(), 2, "{m:?}");
|
||
assert!(sizes[1].0 < sizes[0].0 && sizes[1].1 > 2.0, "{sizes:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn operators_get_spaces_and_unary_minus_does_not() {
|
||
let tight = lay("ab", false);
|
||
let spaced = lay("a+b", false);
|
||
let unary = lay("−b", false);
|
||
let plus_w = lay("+", false).width;
|
||
assert!(spaced.width > tight.width + plus_w + 2.0, "medium spaces around +");
|
||
assert!(unary.width < lay("b", false).width + plus_w + 1.0, "no space after a unary minus");
|
||
}
|
||
|
||
#[test]
|
||
fn display_operators_are_big_with_limits_over_and_under() {
|
||
let d = lay("∑_(i=1)^n▒i", true);
|
||
let t = lay("∑_(i=1)^n▒i", false);
|
||
assert!(d.ascent + d.descent > t.ascent + t.descent, "display sum taller");
|
||
assert!(d.width < t.width + 4.0, "limits stacked in display");
|
||
}
|
||
|
||
#[test]
|
||
fn delimiters_grow_around_tall_content() {
|
||
let small = lay("(x)", true);
|
||
let big = lay("((a)/(b))", true);
|
||
let h = |m: &MathLayout| m.ascent + m.descent;
|
||
assert!(h(&big) > h(&small) * 1.4, "{} vs {}", h(&big), h(&small));
|
||
}
|
||
|
||
#[test]
|
||
fn every_structure_lays_out() {
|
||
use wordcraft_doc::math::*;
|
||
let x = || vec![MNode::Run(MRun::new("x"))];
|
||
let nodes = vec![
|
||
MNode::Acc { chr: '\u{302}', e: x() },
|
||
MNode::Acc { chr: '\u{305}', e: x() },
|
||
MNode::Bar { top: false, e: x() },
|
||
MNode::BorderBox { hide: [false; 4], strike: [true; 4], e: x() },
|
||
MNode::Boxed { e: x() },
|
||
MNode::GroupChr { chr: '⏟', top: false, e: x() },
|
||
MNode::EqArr { rows: vec![vec![MNode::Run(MRun::new("x&=1"))], vec![MNode::Run(MRun::new("y&=22"))]] },
|
||
MNode::Matrix { rows: vec![vec![x(), x()], vec![x(), Vec::new()]], col_jc: vec![] },
|
||
MNode::Phant { show: false, zero_wid: false, zero_asc: false, zero_desc: true, e: x() },
|
||
MNode::Lim { upper: false, e: vec![MNode::Run(MRun::plain("lim"))], lim: x() },
|
||
MNode::Rad { deg: x(), deg_hide: false, e: x() },
|
||
MNode::Script { kind: ScriptKind::Pre, base: x(), sub: x(), sup: x() },
|
||
MNode::Frac { kind: FracKind::Skewed, num: x(), den: x() },
|
||
MNode::Frac { kind: FracKind::Linear, num: x(), den: x() },
|
||
MNode::Frac { kind: FracKind::NoBar, num: x(), den: x() },
|
||
MNode::Delim { beg: None, end: Some('|'), sep: Some('|'), grow: true, shp_match: true, elems: vec![x(), x()] },
|
||
MNode::Run(MRun { text: "text".into(), nor: true, ..Default::default() }),
|
||
MNode::Run(MRun { text: "R".into(), scr: MScr::DoubleStruck, ..Default::default() }),
|
||
];
|
||
for n in nodes {
|
||
let m = layout_equation(&Math { nodes: vec![n.clone()], ..Default::default() }, "", &rc(), true, 400.0, &MathProps::default(), &mut 0);
|
||
assert!(m.width > 0.0 && m.ascent + m.descent > 0.0, "{n:?} → {m:?}");
|
||
for it in &m.items {
|
||
if let MItem::Glyphs { glyphs, .. } = it {
|
||
assert!(glyphs.iter().all(|g| g.1.is_finite() && g.2.is_finite()));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// #1273: an n-ary operator, delimiter, accent or group character the math font lacks is
|
||
/// drawn from a fallback font, not as a missing-glyph box.
|
||
#[test]
|
||
fn characters_the_math_font_lacks_fall_back() {
|
||
let (face, _) = wordcraft_fonts::math::resolve_math("Cambria Math");
|
||
let Some(c) =
|
||
['⨌', '⫿', '⟅', 'ア', '中', '가'].into_iter().find(|c| !face.covers(*c) && FontDb::global().fallback_for(*c, face.id()).is_some())
|
||
else {
|
||
return; // Every candidate is in the math font, or no fallback font is installed.
|
||
};
|
||
let x = || vec![MNode::Run(MRun::new("x"))];
|
||
let nodes = [
|
||
MNode::Nary { chr: c, lim_loc: None, grow: false, sub_hide: true, sup_hide: true, sub: vec![], sup: vec![], e: x() },
|
||
MNode::Delim { beg: Some(c), end: Some(c), sep: None, grow: true, shp_match: false, elems: vec![x()] },
|
||
MNode::Acc { chr: c, e: x() },
|
||
MNode::GroupChr { chr: c, top: true, e: x() },
|
||
];
|
||
for display in [false, true] {
|
||
for n in &nodes {
|
||
let m =
|
||
layout_equation(&Math { nodes: vec![n.clone()], ..Default::default() }, "", &rc(), display, 400.0, &MathProps::default(), &mut 0);
|
||
for it in &m.items {
|
||
if let MItem::Glyphs { glyphs, .. } = it {
|
||
assert!(glyphs.iter().all(|g| g.0 != 0), "{c:?} drew a missing glyph in {n:?}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn caret_slots_cover_every_position() {
|
||
use wordcraft_doc::math_edit::{MathPos, move_right, units};
|
||
let math = Math { nodes: wordcraft_doc::math::parse_linear("x=(-b±√(b^2-4ac))/2a+■(a&b@c&d)"), ..Default::default() };
|
||
let m = layout_equation(&math, "", &rc(), true, 400.0, &MathProps::default(), &mut 0);
|
||
// Walking the caret through the equation, every position has a slot.
|
||
let mut p = MathPos::default();
|
||
let mut seen = 0;
|
||
loop {
|
||
let s = m.slots.iter().find(|s| s.path == p.path && s.off == p.off);
|
||
assert!(s.is_some(), "no slot for {p:?}");
|
||
seen += 1;
|
||
match move_right(&math.nodes, &p) {
|
||
Some(n) => p = n,
|
||
None => break,
|
||
}
|
||
assert!(seen < 500);
|
||
}
|
||
assert_eq!(p.off, units(&math.nodes));
|
||
// Hit testing finds the denominator below the bar.
|
||
let den = m.slots.iter().find(|s| s.path == vec![(1, 1)]).unwrap();
|
||
let hit = m.hit(den.x + 1.0, den.y).unwrap();
|
||
assert_eq!(hit.path, vec![(1, 1)]);
|
||
}
|
||
|
||
#[test]
|
||
fn empty_slots_show_placeholders_on_screen_only() {
|
||
let math = Math { nodes: wordcraft_doc::math::parse_linear("⬚/⬚"), ..Default::default() };
|
||
let m = layout_equation(&math, "", &rc(), true, 400.0, &MathProps::default(), &mut 0);
|
||
assert!(m.items.iter().all(|i| matches!(i, MItem::ScreenOnly(_) | MItem::Rect { .. })), "{:?}", m.items);
|
||
assert!(m.slots.iter().any(|s| s.path == vec![(0, 0)] && s.off == 0));
|
||
let empty = layout_equation(&Math::default(), "", &rc(), true, 400.0, &MathProps::default(), &mut 0);
|
||
assert!(empty.width > 10.0 && empty.items.iter().all(|i| matches!(i, MItem::ScreenOnly(_))));
|
||
}
|
||
|
||
#[test]
|
||
fn numbered_equations_fill_the_line_with_the_number_at_the_right() {
|
||
let math = Math { nodes: wordcraft_doc::math::parse_linear("E=mc^2#(1)"), ..Default::default() };
|
||
let m = layout_equation(&math, "", &rc(), true, 400.0, &MathProps::default(), &mut 0);
|
||
assert!((m.width - 400.0).abs() < 0.01, "{}", m.width);
|
||
let xs: Vec<f32> =
|
||
m.items.iter().filter_map(|i| if let MItem::Glyphs { glyphs, .. } = i { glyphs.first().map(|g| g.1) } else { None }).collect();
|
||
let max = xs.iter().cloned().fold(0.0f32, f32::max);
|
||
assert!(max > 350.0, "number at the right margin: {xs:?}");
|
||
let min = xs.iter().cloned().fold(f32::MAX, f32::min);
|
||
assert!(min > 120.0 && min < 220.0, "equation centred: {xs:?}");
|
||
// Automatic numbers count.
|
||
let auto = Math { nodes: wordcraft_doc::math::parse_linear("a=b#"), ..Default::default() };
|
||
let mut n = 4;
|
||
let _ = layout_equation(&auto, "", &rc(), true, 400.0, &MathProps::default(), &mut n);
|
||
assert_eq!(n, 5);
|
||
assert_eq!(auto_numbers(&auto, true), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn math_alphabets() {
|
||
assert_eq!(math_alnum('x', false, true, MScr::Roman), '𝑥');
|
||
assert_eq!(math_alnum('h', false, true, MScr::Roman), 'ℎ');
|
||
assert_eq!(math_alnum('R', false, false, MScr::DoubleStruck), 'ℝ');
|
||
assert_eq!(math_alnum('α', false, true, MScr::Roman), '𝛼');
|
||
assert_eq!(math_alnum('2', false, true, MScr::Roman), '2');
|
||
assert_eq!(math_alnum('A', true, false, MScr::Roman), '𝐀');
|
||
}
|
||
|
||
#[test]
|
||
fn hostile_trees_never_panic() {
|
||
use wordcraft_doc::math::*;
|
||
// Deep nesting.
|
||
let mut n = vec![MNode::Run(MRun::new("x"))];
|
||
for _ in 0..500 {
|
||
n = vec![MNode::Frac { kind: FracKind::Bar, num: n.clone(), den: Vec::new() }];
|
||
}
|
||
let _ = layout_equation(&Math { nodes: n, ..Default::default() }, "", &rc(), true, 400.0, &MathProps::default(), &mut 0);
|
||
// Empty everything.
|
||
let empty = vec![
|
||
MNode::Nary { chr: '\0', lim_loc: None, grow: true, sub_hide: false, sup_hide: false, sub: vec![], sup: vec![], e: vec![] },
|
||
MNode::Delim { beg: Some('\u{10FFFF}'), end: None, sep: None, grow: true, shp_match: false, elems: vec![] },
|
||
MNode::Matrix { rows: vec![vec![]], col_jc: vec![] },
|
||
MNode::EqArr { rows: vec![] },
|
||
MNode::Run(MRun { text: String::new(), size: Some(f32::NAN), ..Default::default() }),
|
||
];
|
||
let m = layout_equation(&Math { nodes: empty, ..Default::default() }, "", &rc(), true, 400.0, &MathProps::default(), &mut 0);
|
||
assert!(m.width.is_finite() && m.ascent.is_finite());
|
||
// Breaks everywhere, odd wrap settings, no room at all.
|
||
let runs: Vec<MNode> = (0..2000).map(|i| MNode::Run(MRun { brk: (i % 3 == 0).then_some((i % 256) as u8), ..MRun::new("−+=") })).collect();
|
||
for avail in [0.0, f32::NAN, 1e9] {
|
||
for brk_bin in [BrkBin::Before, BrkBin::After, BrkBin::Repeat] {
|
||
let mp = MathProps { brk_bin, brk_bin_sub: BrkBinSub::PlusMinus, wrap_indent: f32::INFINITY, wrap_right: avail == 0.0 };
|
||
let m = layout_equation(&Math { nodes: runs.clone(), ..Default::default() }, "", &rc(), true, avail, &mp, &mut 0);
|
||
assert!(m.width.is_finite() && m.descent.is_finite());
|
||
}
|
||
}
|
||
}
|
||
}
|