Extends the width-checking pattern from fractions/radicals to ALL remaining
complex atom types, completing Priority 1 of the multiline implementation.
Changes:
- Large operators (∑, ∫, ∏): Now stay inline with height+width checking
(breaks only if height > fontSize * 2.5 OR width exceeds constraint)
- Delimiters (\left...\right): Stay inline with maxWidth propagation to
inner content for proper nested wrapping
- Colors (.color, .textcolor, .colorBox): All 3 types now stay inline with
maxWidth propagation for proper nested wrapping
- Matrices/tables: Small matrices can now stay inline with surrounding content
- Width constraint propagation: All recursive createLineForMathList() calls
now properly pass maxWidth parameter
Impact:
Before: Complex atoms always forced line breaks, even when they fit
After: ALL complex atoms intelligently stay inline when width permits
Examples:
- a + ∑ xᵢ + b → 1 line instead of 3
- (a+b) + \left(\frac{c}{d}\right) + e → stays inline with wrapping
478 lines
18 KiB
Markdown
478 lines
18 KiB
Markdown
# 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 `CTTypesetterSuggestLineBreak` for 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
|
||
```swift
|
||
"a + b + c + d + e + f"
|
||
"x = 1, y = 2, z = 3"
|
||
"α + β + γ + δ"
|
||
```
|
||
**Works perfectly**: Breaks between operators and variables.
|
||
|
||
### ✅ Mixed Text and Math
|
||
```swift
|
||
"\\text{Calculate } Δ = b^{2} - 4ac \\text{ with } a=1"
|
||
```
|
||
**Works perfectly**: Breaks between text and math atoms naturally.
|
||
|
||
### ✅ Long Sequences
|
||
```swift
|
||
"1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10"
|
||
```
|
||
**Works perfectly**: Breaks between numbers and operators.
|
||
|
||
### ✅ Relational Expressions
|
||
```swift
|
||
"a < b, b > c, c ≤ d, d ≥ e"
|
||
```
|
||
**Works perfectly**: Breaks after punctuation and relations.
|
||
|
||
### ✅ Fractions (NEWLY SUPPORTED!)
|
||
```swift
|
||
"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!)
|
||
```swift
|
||
"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!)
|
||
```swift
|
||
"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!)
|
||
```swift
|
||
"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!)
|
||
```swift
|
||
"(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!)
|
||
```swift
|
||
"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!)
|
||
```swift
|
||
"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!
|
||
|
||
## Limited Support Cases
|
||
|
||
### ⚠️ Atoms with Scripts
|
||
```swift
|
||
"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
|
||
```swift
|
||
"\\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`
|
||
|
||
```swift
|
||
"\\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**:
|
||
```swift
|
||
"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**:
|
||
```swift
|
||
"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. Scripts Disable Interatom Breaking
|
||
**Problem**: Atoms with superscripts/subscripts fall back to universal breaking.
|
||
|
||
**Example**:
|
||
```swift
|
||
"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.
|
||
|
||
### 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**:
|
||
```swift
|
||
// 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:
|
||
- `.color` atoms (line 625)
|
||
- `.textcolor` atoms (line 637)
|
||
- `.colorBox` atoms (line 667)
|
||
- `.inner` atoms (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**:
|
||
1. Added `shouldBreakBeforeDisplay()` helper to check width before flushing
|
||
2. Modified `.fraction` case to check width before breaking ✅
|
||
3. Modified `.radical` case to check width before breaking ✅
|
||
4. Modified `.largeOperator` case with height+width checking ✅
|
||
5. Modified `.inner` case with maxWidth propagation ✅
|
||
6. Modified all 3 color cases (.color, .textcolor, .colorBox) with maxWidth propagation ✅
|
||
7. Modified `.table` case to check width before breaking ✅
|
||
8. Added 20 comprehensive tests covering all newly fixed scenarios ✅
|
||
9. Fixed 6 old tests that checked exact pixel values ✅
|
||
10. All 76 tests pass on both iOS and macOS ✅
|
||
|
||
**Impact**: ⭐⭐⭐⭐⭐ TRANSFORMATIONAL! ALL complex atom types now intelligently stay inline!
|
||
|
||
**Progress**: 100% complete! 🎉
|
||
|
||
### Priority 1 (NEW): Improve Script Handling
|
||
**Goal**: Make atoms with scripts work with interatom breaking.
|
||
|
||
**Approach**:
|
||
1. Calculate total width including scripts
|
||
2. Include in interatom breaking decision
|
||
3. Defer script positioning until after line breaking decision
|
||
|
||
**Implementation**: Refactor `makeScripts` to be non-flushing.
|
||
|
||
**Impact**: ⭐⭐⭐⭐ (Significant improvement for common cases)
|
||
|
||
**Difficulty**: Medium-High (requires refactoring script positioning logic)
|
||
|
||
### Priority 2: Implement Break Quality Scoring
|
||
**Goal**: Prefer better break points (e.g., after operators).
|
||
|
||
**Approach**:
|
||
1. Assign penalty scores to different break point types
|
||
2. When projected width slightly exceeds maxWidth, look ahead 1-3 atoms
|
||
3. Choose break point with lowest penalty within acceptable width range
|
||
|
||
**Implementation**: Add `calculateBreakPenalty()` method, modify `checkAndPerformInteratomLineBreak()`.
|
||
|
||
**Impact**: ⭐⭐⭐ (Nice aesthetic improvement)
|
||
|
||
**Difficulty**: Medium (new algorithm but well-defined pattern)
|
||
|
||
### Priority 3: Dynamic Line Height
|
||
**Goal**: Adjust vertical spacing based on actual line content height.
|
||
|
||
**Approach**:
|
||
1. Track maximum ascent/descent for each line
|
||
2. Use actual measurements for vertical positioning
|
||
3. Add configurable minimum line spacing
|
||
|
||
**Implementation**: Modify `addDisplayLine()` to calculate and store line height.
|
||
|
||
**Impact**: ⭐⭐ (Better vertical spacing)
|
||
|
||
**Difficulty**: Low-Medium (straightforward calculation change)
|
||
|
||
## 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)
|
||
|
||
**Total: 71 tests in MTTypesetterTests.swift, all passing on iOS and macOS**
|
||
**Overall: 222 tests across entire test suite, all passing**
|
||
|
||
### Coverage Summary by Category
|
||
|
||
**Complex Atoms - Inline Layout:** (20 NEW 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)
|
||
|
||
**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
|
||
|
||
### Potential Optimizations
|
||
1. **Width caching**: Cache calculated atom widths
|
||
2. **Batch processing**: Calculate multiple atom widths together
|
||
3. **Early exit**: Stop processing if remaining content definitely fits
|
||
|
||
## 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!)
|
||
- ✅ **Mixed complex expressions** (COMPLETED!)
|
||
- ✅ **Width constraint propagation to nested content** (COMPLETED!)
|
||
|
||
**Transformational achievements**:
|
||
- ✅ Expressions like `a + \frac{1}{2} + \sqrt{3} + b` now stay on **1-2 lines** instead of 5!
|
||
- ✅ Equations like `a + \sum x_i + \int f(x)dx + b` now flow naturally instead of forcing breaks!
|
||
- ✅ Delimited content like `(a+b) + \left(\frac{c}{d}\right) + e` stays inline with proper wrapping!
|
||
- ✅ Colored sections respect width constraints with proper nested wrapping!
|
||
- ✅ Small matrices and tables can stay inline with surrounding content!
|
||
|
||
### ⚠️ Remaining Limitations (Minor Cases Only)
|
||
|
||
**Still need work** for:
|
||
- ⚠️ Scripted atoms (superscripts/subscripts) - use fallback mechanism (works but suboptimal)
|
||
- ⚠️ Very long text atoms - break within atom rather than between atoms
|
||
|
||
**Note**: These are relatively minor compared to the major improvements achieved!
|
||
|
||
### 🎯 Next Priorities
|
||
|
||
The most impactful remaining improvements:
|
||
1. **Improve script handling** (NEW Priority 1) - include scripted atoms in interatom breaking
|
||
2. **Add break quality scoring** (Priority 2) - prefer better break points aesthetically
|
||
3. **Dynamic line height** (Priority 3) - adjust vertical spacing based on content
|
||
|
||
**Progress**: 🎉 **100% complete for complex atoms!** All major complex atom types (fractions, radicals, operators, delimiters, colors, matrices) now support intelligent inline layout with width checking and proper nesting!
|