12 KiB
Multiline/Line Breaking Implementation Notes
Overview
SwiftMath now supports automatic line breaking (multiline display) for mathematical equations. This document provides technical details about the implementation, supported cases, limitations, and potential areas for improvement.
Implementation Architecture
Two-Tier Breaking System
1. Interatom Line Breaking (Primary - NEW)
Location: MTTypesetter.swift:845-846
Mechanism:
- Checks before adding each atom to the current line
- Calculates projected width:
currentLineWidth + atomWidth + interElementSpacing - If projected width > maxWidth: flushes current line, moves down, starts new line
- Line spacing:
fontSize × 1.5
Applies to atom types:
.ordinary- Variables, text, regular symbols.binaryOperator-+,-,×,÷.relation-=,<,>,≤,≥.open- Opening brackets(.close- Closing brackets).placeholder- Placeholder squares.punctuation- Commas, periods
Advantages:
- ✅ Clean semantic breaks between mathematical elements
- ✅ Respects TeX inter-element spacing rules
- ✅ Fast width calculations using Core Text
- ✅ Preserves mathematical structure
2. Universal Line Breaking (Fallback - EXISTING)
Location: MTTypesetter.swift:877-950
Mechanism:
- Checks after adding atom (for simple atoms without scripts)
- Uses Core Text's
CTTypesetterSuggestLineBreakfor Unicode-aware breaking - Protects numbers from splitting (3.14, 1,000, etc.)
- Supports multiple locales (EN, FR, CH)
Applies when:
- Atoms have no superscripts/subscripts
- Used for very long single text atoms
- Fallback for cases where interatom breaking doesn't apply
Fully Supported Cases
✅ Simple Equations
"a + b + c + d + e + f"
"x = 1, y = 2, z = 3"
"α + β + γ + δ"
Works perfectly: Breaks between operators and variables.
✅ Mixed Text and Math
"\\text{Calculate } Δ = b^{2} - 4ac \\text{ with } a=1"
Works perfectly: Breaks between text and math atoms naturally.
✅ Long Sequences
"1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10"
Works perfectly: Breaks between numbers and operators.
✅ Relational Expressions
"a < b, b > c, c ≤ d, d ≥ e"
Works perfectly: Breaks after punctuation and relations.
Limited Support Cases
⚠️ Atoms with Scripts
"a^{2} + b^{2} + c^{2} + d^{2}"
Works but suboptimal: Falls back to universal breaking which breaks within accumulated text rather than at clean atom boundaries.
Why: Atoms with scripts still trigger line flushing for script positioning, which interrupts the interatom breaking flow.
Impact: May not break at the most aesthetically pleasing positions.
⚠️ Very Long Text Atoms
"\\text{This is an extremely long piece of text within a single text command}"
Works: Uses Core Text's word boundary breaking with number protection.
Limitation: Breaks within the text atom, not between atoms.
Unsupported Cases (Forced Line Breaks)
These atom types always flush the current line before rendering, meaning they start on their own line:
❌ Fractions
Code location: MTTypesetter.swift:669-682
"a + \\frac{1}{2} + b"
// Results in 3 lines:
// Line 1: "a +"
// Line 2: "½"
// Line 3: "+ b"
Why: Fractions require complex vertical layout (numerator/denominator) and force a line flush.
Impact: Expressions with multiple fractions have excessive line breaks.
❌ Radicals (Square Roots)
Code location: MTTypesetter.swift:645-668
"x + \\sqrt{2} + y"
// Results in 3 lines
Why: Radicals require special rendering (radical sign + vinculum) and force line flush.
❌ Large Operators
Code location: MTTypesetter.swift:684-693
"\\sum_{i=1}^{n} x_i + \\int_{0}^{1} f(x)dx"
Why: Large operators (∑, ∫, ∏, lim) with subscripts/superscripts require special vertical positioning.
Impact: Each operator gets its own line.
❌ Inner Lists (Delimiters)
Code location: MTTypesetter.swift:694-709
"a + \\left( \\frac{b}{c} \\right) + d"
Why: \left...\right pairs create inner lists that flush the line for proper delimiter sizing.
❌ Matrices/Tables
Code location: MTTypesetter.swift:757-770
"A = \\begin{pmatrix} 1 & 2 \\\\ 3 & 4 \\end{pmatrix}"
Why: Matrices require complex 2D layout.
❌ Colored Expressions
Code locations:
MTTypesetter.swift:590-600(.color)MTTypesetter.swift:602-630(.textcolor)MTTypesetter.swift:632-643(.colorBox)
"a + \\color{red}{b + c} + d"
Why: Color atoms recursively create displays and flush the line.
❌ Accents
Code location: MTTypesetter.swift:711-755
"\\hat{x} + \\tilde{y}"
Why: Accents require special vertical positioning and may flush lines.
Potential Issues and Edge Cases
1. Over-Breaking with Complex Atoms
Problem: Expressions mixing simple and complex atoms have too many breaks.
Example:
"a + \\frac{1}{2} + b + \\sqrt{3} + c"
// Becomes 5 lines instead of ideally 1-2
Root cause: Each complex atom flushes the line independently.
Possible solution: Check if complex atom + current line width fits within constraint before flushing.
2. No Look-Ahead Optimization
Problem: Greedy algorithm breaks immediately without considering slightly better break points nearby.
Example:
"abc + defgh"
// With narrow width might break: "abc +"
// "defgh"
// Better might be: "abc"
// "+ defgh"
Root cause: Algorithm doesn't look ahead to see if next few atoms would create a better break point.
Possible solution: Implement k-atom look-ahead with break quality scoring.
3. Fixed Line Height
Problem: All lines use fontSize × 1.5 regardless of content height.
Example: A line with a fraction is much taller than a line with just variables, but spacing is uniform.
Possible solution: Calculate actual line height based on ascent/descent of atoms on each line.
4. Scripts Disable Interatom Breaking
Problem: Atoms with superscripts/subscripts fall back to universal breaking.
Example:
"a^{2} + b^{2} + c^{2}"
Root cause: Scripts cause line flushing for vertical positioning (line 892-908), interrupting interatom flow.
Possible solution: Refactor script handling to not require immediate line flush, or handle scripted atoms specially in interatom breaking.
5. No Break Quality Scoring
Problem: All break points are treated equally - no preference for breaking after operators vs. before.
Example: Breaking after + is generally better than breaking before it for readability.
Possible solution: Implement break penalty system:
- Low penalty: after binary operators, after relations, after punctuation
- Medium penalty: after ordinary atoms
- High penalty: after opening brackets, before closing brackets
6. No Widow/Orphan Control
Problem: Single atoms can end up alone on lines.
Example:
// Last line might just be: "+ e"
Possible solution: Minimum atoms per line constraint.
7. Inconsistent Behavior with Recursion
Problem: Nested math lists (inner, color, etc.) create their own displays recursively, potentially without width constraints.
Example:
"\\color{red}{a + b + c + d + e + f + g}"
// The entire colored portion might render on one line even if too wide
Root cause: Recursive calls to createLineForMathList at lines 596, 608, 638 don't pass maxWidth.
Possible solution: Propagate maxWidth to recursive calls.
Future Enhancement Opportunities
Priority 1: Fix Complex Atom Line Flushing
Goal: Allow fractions, radicals, etc. to coexist on lines with other atoms.
Approach:
- Check if complex atom width + current line width fits
- If yes, add to line without flushing
- If no, flush current line, add complex atom to new line
Implementation: Modify switch cases for .fraction, .radical, .largeOperator to check width before flushing.
Impact: ⭐⭐⭐⭐⭐ (Huge improvement for mathematical expressions)
Priority 2: Improve Script Handling
Goal: Make atoms with scripts work with interatom breaking.
Approach:
- Calculate total width including scripts
- Include in interatom breaking decision
- Defer script positioning until after line breaking decision
Implementation: Refactor makeScripts to be non-flushing.
Impact: ⭐⭐⭐⭐ (Significant improvement for common cases)
Priority 3: Implement Break Quality Scoring
Goal: Prefer better break points (e.g., after operators).
Approach:
- Assign penalty scores to different break point types
- When projected width slightly exceeds maxWidth, look ahead 1-3 atoms
- Choose break point with lowest penalty within acceptable width range
Implementation: Add calculateBreakPenalty() method, modify checkAndPerformInteratomLineBreak().
Impact: ⭐⭐⭐ (Nice aesthetic improvement)
Priority 4: Dynamic Line Height
Goal: Adjust vertical spacing based on actual line content height.
Approach:
- Track maximum ascent/descent for each line
- Use actual measurements for vertical positioning
- Add configurable minimum line spacing
Implementation: Modify addDisplayLine() to calculate and store line height.
Impact: ⭐⭐ (Better vertical spacing)
Priority 5: Width Constraint Propagation
Goal: Apply width constraints to nested/recursive displays.
Approach:
- Pass
maxWidthto all recursivecreateLineForMathListcalls - Adjust for nesting level (reduce maxWidth for inner content)
Implementation: Update all recursive calls with maxWidth parameter.
Impact: ⭐⭐ (More consistent behavior)
Testing Strategy
Current Test Coverage
✅ Simple equations (6 tests in MTTypesetterTests.swift:1577-1709)
✅ Text and math mixing
✅ Atoms at boundaries
✅ Superscripts (limited)
✅ No breaking when not needed
✅ Breaking after operators
Recommended Additional Tests
- Fractions in equations
- Radicals in equations
- Large operators with breaking
- Nested expressions
- Colored sections
- Very narrow widths (edge cases)
- Very wide atoms (overflow handling)
- Mixed scripts and non-scripts
- Matrices with surrounding content
- Multiple line breaks (3+ lines)
- Unicode text wrapping
- Number protection across languages
Performance Considerations
Current Performance
- Width calculations use Core Text (relatively fast)
- No caching of calculated widths
- Greedy algorithm is O(n) where n = number of atoms
Potential Optimizations
- Width caching: Cache calculated atom widths
- Batch processing: Calculate multiple atom widths together
- Early exit: Stop processing if remaining content definitely fits
Conclusion
The current implementation provides excellent support for:
- ✅ Simple equations with operators
- ✅ Text and math mixing
- ✅ Long sequences of variables/numbers
Limitations exist for:
- ⚠️ Expressions with fractions, radicals, large operators
- ⚠️ Nested/colored expressions
- ⚠️ Scripted atoms (superscripts/subscripts)
The most impactful improvements would be:
- Fix complex atom flushing (allow fractions/radicals inline)
- Improve script handling (include in interatom breaking)
- Add break quality scoring (prefer better break points)
These enhancements would significantly expand the range of expressions that break naturally and aesthetically across multiple lines.