From cd9c3f7a3708da6f18f20fd2194e06eb2a610a59 Mon Sep 17 00:00:00 2001 From: Nicolas Guillot Date: Thu, 13 Nov 2025 15:08:55 +0100 Subject: [PATCH] add documentation --- MULTILINE_IMPLEMENTATION_NOTES.md | 373 ++++++++++++++++++++++++++++++ README.md | 218 +++++++++++++---- 2 files changed, 552 insertions(+), 39 deletions(-) create mode 100644 MULTILINE_IMPLEMENTATION_NOTES.md diff --git a/MULTILINE_IMPLEMENTATION_NOTES.md b/MULTILINE_IMPLEMENTATION_NOTES.md new file mode 100644 index 0000000..65c1cbd --- /dev/null +++ b/MULTILINE_IMPLEMENTATION_NOTES.md @@ -0,0 +1,373 @@ +# 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. + +## 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. + +## 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` + +```swift +"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` + +```swift +"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` + +```swift +"\\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` + +```swift +"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` + +```swift +"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`) + +```swift +"a + \\color{red}{b + c} + d" +``` + +**Why**: Color atoms recursively create displays and flush the line. + +### ❌ Accents +**Code location**: `MTTypesetter.swift:711-755` + +```swift +"\\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**: +```swift +"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**: +```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. + +### 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**: +```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. + +### 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**: +```swift +// 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**: +```swift +"\\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**: +1. Check if complex atom width + current line width fits +2. If yes, add to line without flushing +3. 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**: +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) + +### Priority 3: 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) + +### Priority 4: 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) + +### Priority 5: Width Constraint Propagation +**Goal**: Apply width constraints to nested/recursive displays. + +**Approach**: +1. Pass `maxWidth` to all recursive `createLineForMathList` calls +2. 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 +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 + +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: +1. **Fix complex atom flushing** (allow fractions/radicals inline) +2. **Improve script handling** (include in interatom breaking) +3. **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. diff --git a/README.md b/README.md index 827144b..df4c50d 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ struct MathView: NSViewRepresentable { ### Automatic Line Wrapping -`SwiftMath` supports automatic line wrapping for text and simple math expressions. When the content exceeds the available width, it will wrap at word boundaries to fit within the constrained space. +`SwiftMath` supports automatic line wrapping (multiline display) for mathematical content. The implementation uses **interatom line breaking** which breaks equations at atom boundaries (between mathematical elements) rather than within them, preserving the semantic structure of the mathematics. #### Using Line Wrapping with UIKit/AppKit @@ -201,18 +201,17 @@ For direct `MTMathUILabel` usage, set the `preferredMaxLayoutWidth` property: ```swift let label = MTMathUILabel() -label.latex = "\\(\\text{Remember the conversion: 1 km equals 1000 meters.}\\)" +label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5" label.font = MTFontManager.fontManager.defaultFont -label.labelMode = .text // Enable line wrapping by setting a maximum width -label.preferredMaxLayoutWidth = 300 +label.preferredMaxLayoutWidth = 235 ``` You can also use `sizeThatFits` to calculate the size with a width constraint: ```swift -let constrainedSize = label.sizeThatFits(CGSize(width: 300, height: .greatestFiniteMagnitude)) +let constrainedSize = label.sizeThatFits(CGSize(width: 235, height: .greatestFiniteMagnitude)) ``` #### Using Line Wrapping with SwiftUI @@ -222,64 +221,205 @@ The `MathView` examples above include `sizeThatFits()` which automatically enabl ```swift VStack(alignment: .leading, spacing: 8) { MathView( - equation: "\\(\\text{Remember the conversion: 1 km equals 1000 meters.}\\)", + equation: "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5", fontSize: 17, labelMode: .text ) } -.frame(maxWidth: 300) // The text will wrap to fit within 300pt +.frame(maxWidth: 235) // The equation will break across multiple lines ``` -#### Line Wrapping Behavior +#### Line Wrapping Behavior and Capabilities -- **Works for**: Text content (`\text{...}`), mixed text with simple math, and simple equations -- **Breaks at**: Word boundaries (spaces) -- **Preserves**: Complex math layout (fractions, superscripts, matrices remain on single lines) -- **Respects**: Unicode text including CJK characters with proper word boundaries +SwiftMath implements **two complementary line breaking mechanisms**: + +##### 1. Interatom Line Breaking (Primary) +Breaks equations **between atoms** (mathematical elements) when content exceeds the width constraint. This is the preferred method as it maintains semantic integrity. + +##### 2. Universal Line Breaking (Fallback) +For very long text within single atoms, breaks at Unicode word boundaries using Core Text with number protection (prevents splitting numbers like "3.14"). + +#### Fully Supported Cases + +These atom types work perfectly with interatom line breaking: + +**✅ Variables and ordinary text:** +```swift +label.latex = "a b c d e f g h i j k l m n o p" +label.preferredMaxLayoutWidth = 150 +// Breaks between individual variables at natural boundaries +``` + +**✅ Binary operators (+, -, ×, ÷):** +```swift +label.latex = "a+b+c+d+e+f+g+h" +label.preferredMaxLayoutWidth = 100 +// Breaks cleanly: "a+b+c+d+" +// "e+f+g+h" +``` + +**✅ Relations (=, <, >, ≤, ≥, etc.):** +```swift +label.latex = "a=1, b=2, c=3, d=4, e=5" +label.preferredMaxLayoutWidth = 120 +// Breaks after commas and operators +``` + +**✅ Mixed text and simple math:** +```swift +label.latex = "\\text{Calculer }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1" +label.preferredMaxLayoutWidth = 200 +// Breaks between text and math atoms naturally +``` + +**✅ Punctuation (commas, periods):** +```swift +label.latex = "\\text{First, second, third, fourth, fifth}" +label.preferredMaxLayoutWidth = 150 +// Breaks at commas and spaces +``` + +**✅ Brackets and parentheses (simple):** +```swift +label.latex = "(a+b)+(c+d)+(e+f)" +label.preferredMaxLayoutWidth = 120 +// Breaks between parenthesized groups +``` + +**✅ Greek letters and symbols:** +```swift +label.latex = "\\alpha+\\beta+\\gamma+\\delta+\\epsilon+\\zeta" +label.preferredMaxLayoutWidth = 150 +// Breaks between Greek letters +``` + +#### Limited Support Cases + +These cases work but with some constraints: + +**⚠️ Atoms with superscripts/subscripts:** +```swift +label.latex = "a^{2}+b^{2}+c^{2}+d^{2}+e^{2}" +label.preferredMaxLayoutWidth = 150 +// Works, but uses fallback breaking mechanism +// May not break at the most optimal positions +``` +**Note**: Scripted atoms (with superscripts/subscripts) trigger the universal breaking mechanism which breaks within accumulated text rather than at atom boundaries. This still works but may not be as clean as pure interatom breaking. + +**⚠️ Very long single text atoms:** +```swift +label.latex = "\\text{This is an extremely long piece of text within a single text command}" +label.preferredMaxLayoutWidth = 200 +// Uses Unicode word boundary breaking with Core Text +// Protects numbers from being split (e.g., "3.14" stays together) +``` + +#### Unsupported/Forced Line Break Cases + +These atom types **always start on a new line** because they flush the current line before rendering. This can lead to excessive line breaks: + +**❌ Fractions:** +```swift +label.latex = "a + \\frac{1}{2} + b" +// Results in: +// Line 1: "a +" +// Line 2: "½" (fraction on own line) +// Line 3: "+ b" +``` + +**❌ Radicals (square roots):** +```swift +label.latex = "x + \\sqrt{2} + y" +// Results in: +// Line 1: "x +" +// Line 2: "√2" (radical on own line) +// Line 3: "+ y" +``` + +**❌ Large operators (∑, ∫, ∏, lim):** +```swift +label.latex = "\\sum_{i=1}^{n} x_i + \\int_{0}^{1} f(x)dx" +// Each operator forces a new line +``` + +**❌ Matrices and tables:** +```swift +label.latex = "A = \\begin{pmatrix} 1 & 2 \\\\ 3 & 4 \\end{pmatrix}" +// Matrix always on own line +``` + +**❌ Delimited expressions (\left...\right):** +```swift +label.latex = "\\left(\\frac{a}{b}\\right) + c" +// The parenthesized group forces line breaks +``` + +**❌ Colored expressions:** +```swift +label.latex = "a + \\color{red}{b} + c" +// Colored portion causes line break +``` + +**❌ Math accents:** +```swift +label.latex = "\\hat{x} + \\tilde{y} + \\bar{z}" +// Accents may cause line breaks +``` + +#### Best Practices + +**DO:** +- Use interatom breaking for simple equations with operators and relations +- Use for mixed text and math where you want natural breaks +- Use for long sequences of variables, numbers, and operators +- Set appropriate `preferredMaxLayoutWidth` based on your layout needs + +**DON'T:** +- Expect natural breaking in expressions with many fractions +- Expect natural breaking in expressions with many radicals +- Expect natural breaking in expressions with large operators +- Use extremely narrow widths (less than ~80pt) which may cause poor breaks #### Examples -**Simple text wrapping:** +**Excellent use case (discriminant formula):** ```swift -// Long text will wrap to multiple lines -label.latex = "\\(\\text{The quadratic formula is used to solve equations of the form } ax^2 + bx + c = 0\\)" -label.preferredMaxLayoutWidth = 250 +label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5" +label.preferredMaxLayoutWidth = 235 +// ✅ Breaks naturally at good points between atoms ``` -**Simple equation with operators:** +**Good use case (simple arithmetic):** ```swift -// Long equations can break between operators if too long -label.latex = "\\(5 + 10 + 15 + 20 + 25 + 30\\)" +label.latex = "5+10+15+20+25+30+35+40+45+50" label.preferredMaxLayoutWidth = 150 -// Will wrap: "5 + 10 + 15 + 20 +" -// "25 + 30" +// ✅ Breaks between operators cleanly ``` -**Mixed text and math:** +**Problematic use case (many fractions):** ```swift -// Text wraps but math expressions stay intact -label.latex = "\\(\\text{Result: } 5 \\times 1000 = 5000 \\text{ meters}\\)" +label.latex = "\\frac{1}{2}+\\frac{3}{4}+\\frac{5}{6}+\\frac{7}{8}" label.preferredMaxLayoutWidth = 200 -// Will wrap at spaces between text and operators +// ⚠️ Each fraction on separate line, not ideal +// Better to avoid line breaking for such expressions ``` -**Multiple lines in SwiftUI:** +**Alternative for complex expressions:** ```swift -ScrollView { - VStack(alignment: .leading, spacing: 12) { - ForEach(steps) { step in - MathView( - equation: step.description, - fontSize: 17, - labelMode: .text - ) - } - } - .padding() -} -// Each MathView will automatically wrap based on available width +// Instead of trying to break this: +label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" +// Consider it as a single display equation without width constraint +label.preferredMaxLayoutWidth = 0 // No breaking ``` +#### Technical Details + +- **Line spacing**: New lines are positioned at `fontSize × 1.5` below the previous line +- **Breaking algorithm**: Greedy - breaks immediately when projected width exceeds constraint +- **Width calculation**: Includes inter-element spacing according to TeX spacing rules +- **Number protection**: Numbers in patterns like "3.14", "1,000", etc. are kept intact +- **Supports locales**: English, French, Swiss number formats + ### Included Features This is a list of formula types that the library currently supports: