Implement early-exit optimization to avoid expensive width calculations when we can determine that all remaining content will definitely fit on the current line.
23 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.
✅ Fractions (NEWLY SUPPORTED!)
"a + \\frac{1}{2} + b + \\frac{3}{4} + c"
Now works perfectly: Fractions stay inline when they fit within width constraint. No longer forces line breaks!
Implementation: Lines 701-721 in MTTypesetter.swift
- Creates fraction display first
- Checks if adding it would exceed maxWidth
- Only breaks to new line if necessary
- Otherwise adds inline with proper spacing
Impact: ⭐⭐⭐⭐⭐ HUGE improvement for mathematical expressions!
✅ Radicals (NEWLY SUPPORTED!)
"x + \\sqrt{2} + y + \\sqrt{3} + z"
Now works perfectly: Radicals stay inline when they fit. Handles both simple radicals and those with degrees (cube roots, etc.).
Implementation: Lines 677-705 in MTTypesetter.swift
- Creates radical display first (including degree if present)
- Checks if adding it would exceed maxWidth
- Only breaks to new line if necessary
- Otherwise adds inline with proper spacing
Impact: ⭐⭐⭐⭐⭐ HUGE improvement for mathematical expressions!
✅ Mixed Complex Expressions (NEWLY SUPPORTED!)
"a + \\frac{1}{2} + \\sqrt{3} + b"
Now works perfectly: Intelligently mixes fractions, radicals, and simple atoms. Each element stays inline if it fits.
✅ Large Operators (NEWLY SUPPORTED!)
"a + \\sum x_i + \\int f(x)dx + b"
Now works perfectly: Large operators (∑, ∫, ∏, lim) stay inline when they fit within width constraints. Includes intelligent height checking for operators with limits.
Implementation: Lines 729-748 in MTTypesetter.swift
- Creates operator display first (including limits if present)
- Checks both width AND height (breaks if height > fontSize * 2.5)
- Only breaks to new line if necessary
- Otherwise adds inline with proper spacing
Impact: ⭐⭐⭐⭐⭐ HUGE improvement for mathematical expressions!
✅ Delimited Expressions (NEWLY SUPPORTED!)
"(a+b) + \\left(\\frac{c}{d}\\right) + e"
Now works perfectly: Delimiters stay inline when they fit. Inner content respects width constraints and can wrap naturally.
Implementation: Lines 750-776 in MTTypesetter.swift
- Creates delimited display first with maxWidth propagation
- Checks if adding it would exceed maxWidth
- Only breaks to new line if necessary
- Passes maxWidth to inner content for proper wrapping
Impact: ⭐⭐⭐⭐⭐ HUGE improvement for complex equations!
✅ Colored Expressions (NEWLY SUPPORTED!)
"a + \\color{red}{b + c + d} + e"
Now works perfectly: Colored sections stay inline when they fit. Inner content respects width constraints and wraps properly.
Implementation: Lines 622-685 in MTTypesetter.swift (all three color types: .color, .textcolor, .colorBox)
- Creates colored display first with maxWidth propagation
- Checks if adding it would exceed maxWidth
- Only breaks to new line if necessary
- Passes maxWidth to inner content for proper wrapping
Impact: ⭐⭐⭐⭐ VERY GOOD improvement for emphasized content!
✅ Matrices/Tables (NEWLY SUPPORTED!)
"A = \\begin{pmatrix} 1 & 2 \\end{pmatrix} + B"
Now works perfectly: Small matrices stay inline when they fit within width constraints.
Implementation: Lines 899-916 in MTTypesetter.swift
- Creates matrix display first
- Checks if adding it would exceed maxWidth
- Only breaks to new line if necessary
- Otherwise adds inline with proper spacing
Impact: ⭐⭐⭐ GOOD improvement for small matrices and vectors!
✅ Atoms with Scripts (NEWLY IMPROVED!)
"a^{2} + b^{2} + c^{2} + d^{2}"
Now works much better: Atoms with superscripts and subscripts now participate in intelligent width-based breaking!
Implementation: Lines 1123-1137 in MTTypesetter.swift
- Estimates total width of atom including scripts before adding
- Checks if adding scripted atom would exceed maxWidth
- Only breaks to new line if necessary
- Otherwise adds inline with proper spacing
Impact: ⭐⭐⭐⭐ SIGNIFICANT improvement for mathematical expressions with exponents!
Limited Support Cases
⚠️ 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.
Remaining Unsupported Cases
GREAT NEWS: As of the latest update, ALL major complex atom types now support intelligent inline layout! 🎉
✅ Previously Unsupported - NOW FIXED!
The following cases that previously forced line breaks now work perfectly:
- ✅ Large operators (∑, ∫, ∏) - Now stay inline with height/width checking
- ✅ Delimiters (\left...\right) - Now stay inline with width propagation
- ✅ Colored expressions - Now stay inline with width propagation
- ✅ Matrices/tables - Now stay inline when they fit
ℹ️ Special Note: Accents
Code location: MTTypesetter.swift:751-824
"\\hat{x} + \\tilde{y}"
Status: Already partially supported when maxWidth > 0. Simple accents work well; complex accents may need minor polish but are generally functional.
Recent Improvements (Implemented!)
✅ FIXED: Over-Breaking with Fractions and Radicals
Previous Problem: Expressions mixing simple atoms with fractions/radicals had too many breaks.
Previous Example:
"a + \\frac{1}{2} + b + \\sqrt{3} + c"
// Previously became 5 lines
Solution Implemented: Check if complex atom + current line width fits within constraint before flushing.
Current Behavior: Now stays on 1-2 lines as expected! ✅
Implementation Details:
- Added
shouldBreakBeforeDisplay()helper function (line 552-573) - Added
performLineBreak()helper function (line 575-582) - Modified fraction handling (lines 701-721) to check width before breaking
- Modified radical handling (lines 677-705) to check width before breaking
- Added 8 comprehensive tests (MTTypesetterTests.swift:1712-1869)
- All 43 tests pass on both iOS and macOS
Remaining Issues and Edge Cases
1. 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.
2. 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.
3. ✅ FIXED: Scripts Disable Interatom Breaking
Previous Problem: Atoms with superscripts/subscripts fell back to universal breaking.
Solution Implemented: Now checks width before flushing for scripted atoms!
- Added
estimateAtomWidthWithScripts()helper function - Checks if atom with scripts would exceed width constraint BEFORE flushing
- Only breaks line if necessary
- Scripted atoms now participate in intelligent width-based breaking
Result: ✅ Much better breaking behavior for expressions with exponents!
4. 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
5. 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.
6. ✅ FIXED: Inconsistent Behavior with Recursion
Previous Problem: Nested math lists (inner, color, etc.) created their own displays recursively without width constraints.
Solution: Now propagates maxWidth to all recursive createLineForMathList() calls in:
.coloratoms (line 625).textcoloratoms (line 637).colorBoxatoms (line 667).inneratoms (lines 755, 762)makeLeftRight()helper (line 1867)
Result: ✅ Inner content now wraps properly!
Future Enhancement Opportunities
✅ COMPLETED: Priority 1 - Fix ALL Complex Atom Line Flushing
Status: ✅ 100% IMPLEMENTED AND TESTED
What was done:
- Added
shouldBreakBeforeDisplay()helper to check width before flushing - Modified
.fractioncase to check width before breaking ✅ - Modified
.radicalcase to check width before breaking ✅ - Modified
.largeOperatorcase with height+width checking ✅ - Modified
.innercase with maxWidth propagation ✅ - Modified all 3 color cases (.color, .textcolor, .colorBox) with maxWidth propagation ✅
- Modified
.tablecase to check width before breaking ✅ - Added 20 comprehensive tests covering all newly fixed scenarios ✅
- Fixed 6 old tests that checked exact pixel values ✅
- All 76 tests pass on both iOS and macOS ✅
Impact: ⭐⭐⭐⭐⭐ TRANSFORMATIONAL! ALL complex atom types now intelligently stay inline!
Progress: 100% complete! 🎉
✅ COMPLETED: Priority 1 - Improve Script Handling
Status: ✅ IMPLEMENTED AND TESTED
What was done:
- Added
estimateAtomWidthWithScripts()helper function to calculate atom width including scripts - Check width constraint BEFORE flushing for scripted atoms (lines 1123-1137)
- Only break line if adding scripted atom would exceed maxWidth
- Otherwise add inline with proper spacing
- Added 8 comprehensive tests covering all scenarios
- All 232 tests pass on iOS ✅
Impact: ⭐⭐⭐⭐ SIGNIFICANT improvement! Expressions with exponents now break intelligently based on width!
Progress: Scripted atoms now participate in interatom breaking decisions while preserving correct script positioning!
✅ Break Quality Scoring (NEWLY COMPLETED!)
Goal: Prefer better break points aesthetically (e.g., after operators rather than in the middle of expressions).
Implementation: Lines 517-607 in MTTypesetter.swift
- Added
calculateBreakPenalty()function that assigns penalty scores:- Penalty 0 (best): After binary operators (+, -, ×, ÷), relations (=, <, >), punctuation
- Penalty 10 (good): After ordinary atoms (variables, numbers)
- Penalty 100 (bad): After open brackets or before close brackets
- Penalty 150 (worse): After unary/large operators
- Modified
checkAndPerformInteratomLineBreak()with look-ahead logic:- When width is slightly exceeded (100%-120% of maxWidth), looks ahead up to 3 atoms
- Calculates penalties for each potential break point in window
- Chooses break point with lowest penalty
- Defers breaking if better point found within look-ahead window
- Updated to handle special atom types (Space, Style) that don't participate in width calculations
Impact: ⭐⭐⭐⭐ SIGNIFICANT aesthetic improvement! Expressions now break at natural, readable points!
Progress: COMPLETED with 8 comprehensive tests!
✅ Dynamic Line Height (NEWLY COMPLETED!)
Goal: Adjust vertical spacing based on actual line content height rather than fixed fontSize × 1.5.
Implementation: Lines 366-367, 421-423, 654-689 in MTTypesetter.swift
- Added
currentLineStartIndex: Intto track where each line's displays begin in displayAtoms array - Added
minimumLineSpacing: CGFloatset to 20% of fontSize for breathing room between lines - Created
calculateCurrentLineHeight()function that:- Iterates through all displays added for the current line (from currentLineStartIndex to displayAtoms.count)
- Finds maximum ascent and maximum descent across all displays
- Returns total height = maxAscent + maxDescent + minimumLineSpacing
- Ensures at least fontSize × 1.2 spacing for readability
- Modified all line breaking functions to use dynamic height:
performInteratomLineBreak()- for interatom breaks (lines 606-624)performLineBreak()- for complex display breaks (lines 649-664)- Universal line breaking - two locations (lines 1237-1241, 1260-1264)
- Scripted atom breaking (lines 1290-1293)
checkAndBreakLine()helper - two locations (lines 1486-1490, 1510-1514)
- All break locations now:
- Calculate line height based on actual content
- Update currentPosition.y using dynamic height
- Update currentLineStartIndex for next line
Impact: ⭐⭐⭐⭐ SIGNIFICANT improvement! Lines with tall content (fractions, large operators) get appropriate spacing, while regular lines don't have excessive gaps!
Progress: COMPLETED with 8 comprehensive tests!
Testing Strategy
Current Test Coverage
✅ Simple equations (6 tests in MTTypesetterTests.swift:1577-1711)
✅ Text and math mixing
✅ Atoms at boundaries
✅ Superscripts (limited)
✅ No breaking when not needed
✅ Breaking after operators
✅ Fractions inline (8 tests in MTTypesetterTests.swift:1712-1869)
✅ Radicals inline (included in above)
✅ Mixed fractions and radicals (included in above)
✅ Fractions with complex content (included in above)
✅ Radicals with degrees (included in above)
✅ No breaking without width constraint (included in above)
✅ Very narrow widths (edge cases) (NEW - line 1873)
✅ Very wide atoms (overflow handling) (NEW - line 1895)
✅ Mixed scripts and non-scripts (NEW - line 1913)
✅ Multiple line breaks (4+ lines) (NEW - line 1930)
✅ Unicode text wrapping (NEW - line 1962)
✅ Number protection (NEW - line 1983)
✅ Large operators inline (NEW - 3 tests in lines 2111-2165)
✅ Delimiters inline (NEW - 4 tests in lines 2167-2246)
✅ Colored expressions inline (NEW - 3 tests in lines 2248-2304)
✅ Matrices inline (NEW - 3 tests in lines 2306-2362)
✅ Integration tests (NEW - 2 tests in lines 2364-2415)
✅ Real-world examples (NEW - 3 tests in lines 2417-2492)
✅ Edge cases (NEW - 2 tests in lines 2494-2534)
✅ Scripted atoms inline (NEW - 8 tests in lines 2609-2780)
✅ Break quality scoring (NEW - 8 tests in lines 2797-3006)
✅ Dynamic line height (NEW - 8 tests in lines 3007-3218)
Total: 97 tests in MTTypesetterTests.swift, all passing on iOS Overall: 248 tests across entire test suite, all passing
Coverage Summary by Category
Complex Atoms - Inline Layout: (20 tests)
- Large operators: 3 tests (inline when fit, break when too wide, multiple operators)
- Delimiters: 4 tests (inline when fit, break when too wide, nested delimiters, multiple delimiters)
- Colored expressions: 3 tests (inline when fit, break when too wide, multiple colored sections)
- Matrices: 3 tests (small inline, break when too wide, with surrounding content)
- Integration: 2 tests (mixed complex elements, no breaking without constraints)
- Real-world: 3 tests (quadratic formula with color, complex fractions, mixed operations)
- Edge cases: 2 tests (very narrow width, very wide atom)
Improved Script Handling: (8 tests)
- Scripted atoms inline when fit
- Scripted atoms break when too wide
- Mixed scripted and non-scripted atoms
- Both subscripts and superscripts
- Real-world: Quadratic expansion with exponents
- Real-world: Polynomial with multiple exponent terms
- No breaking without width constraint
- Complex expressions mixing fractions and scripts
Break Quality Scoring: (8 tests)
- Prefer breaking after binary operators (+, -, ×, ÷)
- Prefer breaking after relation operators (=, <, >)
- Avoid breaking after open brackets
- Look-ahead finds better break points
- Multiple operators break at best available points
- Complex expressions with various atom types
- No unnecessary breaks when content fits
- Penalty ordering validates break preferences
Dynamic Line Height: (8 NEW tests)
- Tall content (fractions) gets more spacing
- Regular content has reasonable spacing (not excessive)
- Mixed content varies spacing appropriately per line
- Large operators with limits get adequate vertical space
- Similar content gets consistent spacing
- No regression on single-line expressions
- Deep/nested fractions get extra space
- Radicals with indices (cube roots) get adequate spacing
Edge Cases & Stress Tests: (4 tests)
- Very narrow widths (30pt)
- Very wide atoms (overflow)
- Mixed scripts and non-scripts
- Multiple line breaks (4+ lines)
Internationalization: (2 tests)
- Unicode text wrapping (CJK, Arabic, etc.)
- Number protection across locales
Real-World Examples: (3 tests)
- Quadratic formula
- Complex nested fractions (continued fractions)
- Multiple fractions in sequence
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
- ✅ NEW: Early exit optimization when remaining content fits (IMPLEMENTED!)
✅ COMPLETED: Early Exit Optimization
Goal: Skip expensive line breaking checks when we know all remaining content will fit.
Implementation: Lines 376, 549-606 in MTTypesetter.swift
- Added
remainingContentFitsflag to track when optimization applies - In
checkAndPerformInteratomLineBreak():- After confirming current atom fits, estimates remaining content width
- If current usage < 60% of maxWidth with ≤5 atoms remaining: sets flag (conservative)
- If current usage < 75%: estimates remaining width via
estimateRemainingAtomsWidth() - Sets flag if projected total width ≤ maxWidth
- Once flag is set, all subsequent breaking checks return immediately (fast path)
- Flag is reset when line break actually occurs
estimateRemainingAtomsWidth()uses character count × average char width heuristic with 1.5× safety margin
Impact: ⭐⭐⭐ GOOD performance improvement for short expressions! Avoids width calculations for atoms that definitely fit.
Benefit: Most mathematical expressions fit on one line - this optimization makes them render faster by skipping unnecessary width checks after determining the line has plenty of space.
Progress: COMPLETED and tested!
Remaining Potential Optimizations
- Width caching: Cache calculated atom widths
- Batch processing: Calculate multiple atom widths together
Conclusion
🎉 COMPLETE: Major Transformation Achieved!
The multiline line breaking implementation now provides comprehensive support for ALL complex atom types!
✅ What's Now Excellent (All Major Features Complete!)
The implementation now provides excellent support for:
- ✅ Simple equations with operators
- ✅ Text and math mixing
- ✅ Long sequences of variables/numbers
- ✅ Fractions inline (COMPLETED!)
- ✅ Radicals/square roots inline (COMPLETED!)
- ✅ Large operators inline (COMPLETED!)
- ✅ Delimited expressions inline (COMPLETED!)
- ✅ Colored expressions inline (COMPLETED!)
- ✅ Matrices/tables inline (COMPLETED!)
- ✅ Scripted atoms (superscripts/subscripts) (COMPLETED!)
- ✅ Mixed complex expressions (COMPLETED!)
- ✅ Width constraint propagation to nested content (COMPLETED!)
Transformational achievements:
- ✅ Expressions like
a + \frac{1}{2} + \sqrt{3} + bnow stay on 1-2 lines instead of 5! - ✅ Equations like
a + \sum x_i + \int f(x)dx + bnow flow naturally instead of forcing breaks! - ✅ Delimited content like
(a+b) + \left(\frac{c}{d}\right) + estays inline with proper wrapping! - ✅ Colored sections respect width constraints with proper nested wrapping!
- ✅ Small matrices and tables can stay inline with surrounding content!
- ✅ NEW: Scripted atoms like
a^{2} + b^{2} + c^{2}break intelligently based on width!
⚠️ Remaining Limitations (Minor Cases Only)
Still need work for:
- ⚠️ Very long text atoms - break within atom rather than between atoms (already implemented via universal line breaking but could be further optimized)
Note: This is a minor edge case rather than a fundamental limitation!
🎯 Future Enhancements (All Core Features Complete!)
All major priorities have been completed! Possible future enhancements:
- Further optimize very long text atom breaking - fine-tune Unicode-aware breaking for edge cases
- Configurable line spacing multiplier - allow users to adjust minimum spacing
- Alignment options - left/center/right alignment for multiline expressions
Progress: 🎉 100% COMPLETE for all major features! All atom types (simple, complex, and scripted) now support:
- ✅ Intelligent inline layout with width-based breaking
- ✅ Aesthetically-pleasing break point selection (after operators, avoiding bad breaks)
- ✅ Dynamic line height based on actual content (tall fractions get more space, regular content stays compact)