Fix large operator positioning causing incorrect atom ordering
When rendering large operators (e.g., sum, integral) with scripts in text
mode, the operator glyph was incorrectly positioned after its subscripts
and superscripts instead of before them. This caused expressions like
\sum_{i=1}^{n} i = \frac{n(n+1)}{2} to render with the equals sign
appearing visually misplaced.
Root cause:
The line-breaking refactoring introduced double-positioning of large
operators. makeLargeOp() internally sets the operator position, advances
currentPosition.x, and adds script displays. However, the calling code
then overwrote the position and advanced currentPosition.x again, causing:
- Double-advancement leading to incorrect width calculations
- Scripts positioned before the operator instead of after
Solution:
Save and restore typesetter state before/after line break dimension checks,
then call makeLargeOp() once at the correct position after handling line
breaks and inter-element spacing.
This commit is contained in:
@@ -258,9 +258,24 @@ public class MTMathUILabel : MTView {
|
||||
let effectiveWidth = _preferredMaxLayoutWidth > 0 ? _preferredMaxLayoutWidth : bounds.size.width
|
||||
let availableWidth = effectiveWidth - contentInsets.left - contentInsets.right
|
||||
|
||||
print("🔧 MTMathUILabel _layoutSubviews:")
|
||||
print(" preferredMaxLayoutWidth: \(_preferredMaxLayoutWidth)")
|
||||
print(" bounds.size.width: \(bounds.size.width)")
|
||||
print(" effectiveWidth: \(effectiveWidth)")
|
||||
print(" availableWidth: \(availableWidth)")
|
||||
print(" LaTeX: \(_latex.prefix(60))...")
|
||||
|
||||
// print("Pre list = \(_mathList!)")
|
||||
_displayList = MTTypesetter.createLineForMathList(_mathList, font: font, style: currentStyle, maxWidth: availableWidth)
|
||||
_displayList!.textColor = textColor
|
||||
|
||||
print(" Display subDisplays count: \(_displayList!.subDisplays.count)")
|
||||
for (index, subDisplay) in _displayList!.subDisplays.enumerated() {
|
||||
print(" Display \(index): type=\(type(of: subDisplay)), x=\(subDisplay.position.x), width=\(subDisplay.width)")
|
||||
if let lineDisplay = subDisplay as? MTCTLineDisplay {
|
||||
print(" Content: '\(lineDisplay.attributedString?.string ?? "")'")
|
||||
}
|
||||
}
|
||||
// print("Post list = \(_mathList!)")
|
||||
var textX = CGFloat(0)
|
||||
switch self.textAlignment {
|
||||
|
||||
@@ -626,7 +626,15 @@ class MTTypesetter {
|
||||
display!.localTextColor = MTColor(fromHexString: colorAtom.colorString)
|
||||
|
||||
// Check if we need to break before adding this colored content
|
||||
if shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: .ordinary) {
|
||||
let shouldBreak = shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: .ordinary)
|
||||
|
||||
// Flush current line to convert accumulated text to displays
|
||||
if currentLine.length > 0 {
|
||||
self.addDisplayLine()
|
||||
}
|
||||
|
||||
// Perform line break if needed
|
||||
if shouldBreak {
|
||||
performLineBreak()
|
||||
} else {
|
||||
self.addInterElementSpace(prevNode, currentType:.ordinary)
|
||||
@@ -643,7 +651,15 @@ class MTTypesetter {
|
||||
display!.localTextColor = MTColor(fromHexString: colorAtom.colorString)
|
||||
|
||||
// Check if we need to break before adding this colored content
|
||||
if shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: .ordinary) {
|
||||
let shouldBreak = shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: .ordinary)
|
||||
|
||||
// Flush current line to convert accumulated text to displays
|
||||
if currentLine.length > 0 {
|
||||
self.addDisplayLine()
|
||||
}
|
||||
|
||||
// Perform line break if needed
|
||||
if shouldBreak {
|
||||
performLineBreak()
|
||||
} else if prevNode != nil && display!.subDisplays.count > 0 {
|
||||
// Handle inter-element spacing if not breaking
|
||||
@@ -652,17 +668,8 @@ class MTTypesetter {
|
||||
!ctLineDisplay.atoms.isEmpty {
|
||||
let subDisplayAtom = ctLineDisplay.atoms[0]
|
||||
let interElementSpace = self.getInterElementSpace(prevNode!.type, right:subDisplayAtom.type)
|
||||
if currentLine.length > 0 {
|
||||
if interElementSpace > 0 {
|
||||
// add a kerning of that space to the previous character
|
||||
currentLine.addAttribute(kCTKernAttributeName as NSAttributedString.Key,
|
||||
value:NSNumber(floatLiteral: interElementSpace),
|
||||
range:currentLine.mutableString.rangeOfComposedCharacterSequence(at: currentLine.length-1))
|
||||
}
|
||||
} else {
|
||||
// increase the space
|
||||
currentPosition.x += interElementSpace
|
||||
}
|
||||
// Since we already flushed currentLine, it's empty now, so use x positioning
|
||||
currentPosition.x += interElementSpace
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,7 +685,15 @@ class MTTypesetter {
|
||||
display!.localBackgroundColor = MTColor(fromHexString: colorboxAtom.colorString)
|
||||
|
||||
// Check if we need to break before adding this colorbox
|
||||
if shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: .ordinary) {
|
||||
let shouldBreak = shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: .ordinary)
|
||||
|
||||
// Flush current line to convert accumulated text to displays
|
||||
if currentLine.length > 0 {
|
||||
self.addDisplayLine()
|
||||
}
|
||||
|
||||
// Perform line break if needed
|
||||
if shouldBreak {
|
||||
performLineBreak()
|
||||
} else {
|
||||
self.addInterElementSpace(prevNode, currentType:.ordinary)
|
||||
@@ -700,7 +715,15 @@ class MTTypesetter {
|
||||
|
||||
// Check if we need to break before adding this radical
|
||||
// Radicals are considered as Ord in rule 16.
|
||||
if shouldBreakBeforeDisplay(displayRad!, prevNode: prevNode, displayType: .ordinary) {
|
||||
let shouldBreak = shouldBreakBeforeDisplay(displayRad!, prevNode: prevNode, displayType: .ordinary)
|
||||
|
||||
// Flush current line to convert accumulated text to displays
|
||||
if currentLine.length > 0 {
|
||||
self.addDisplayLine()
|
||||
}
|
||||
|
||||
// Perform line break if needed
|
||||
if shouldBreak {
|
||||
performLineBreak()
|
||||
} else {
|
||||
self.addInterElementSpace(prevNode, currentType:.ordinary)
|
||||
@@ -724,7 +747,15 @@ class MTTypesetter {
|
||||
let display = self.makeFraction(frac)
|
||||
|
||||
// Check if we need to break before adding this fraction
|
||||
if shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: atom.type) {
|
||||
let shouldBreak = shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: atom.type)
|
||||
|
||||
// Flush current line to convert accumulated text to displays
|
||||
if currentLine.length > 0 {
|
||||
self.addDisplayLine()
|
||||
}
|
||||
|
||||
// Perform line break if needed
|
||||
if shouldBreak {
|
||||
performLineBreak()
|
||||
} else {
|
||||
self.addInterElementSpace(prevNode, currentType:atom.type)
|
||||
@@ -741,25 +772,37 @@ class MTTypesetter {
|
||||
}
|
||||
|
||||
case .largeOperator:
|
||||
// Create the large operator display first
|
||||
// Flush current line to convert accumulated text to displays
|
||||
if currentLine.length > 0 {
|
||||
self.addDisplayLine()
|
||||
}
|
||||
|
||||
// Create the large operator display to check if we need line breaking
|
||||
let op = atom as! MTLargeOperator?
|
||||
let display = self.makeLargeOp(op)
|
||||
|
||||
// Check if we need to break before adding this operator
|
||||
// Large operators can be tall (with limits), so check both width and height
|
||||
let isTooTall = (display!.ascent + display!.descent) > styleFont.fontSize * 2.5
|
||||
let isTooWide = shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: atom.type)
|
||||
// Save state before creating display (makeLargeOp may add scripts to displayAtoms)
|
||||
let savedDisplayAtomsCount = displayAtoms.count
|
||||
let savedPosition = currentPosition
|
||||
let tempDisplay = self.makeLargeOp(op)
|
||||
let tempIsTooTall = (tempDisplay!.ascent + tempDisplay!.descent) > styleFont.fontSize * 2.5
|
||||
let tempIsTooWide = shouldBreakBeforeDisplay(tempDisplay!, prevNode: prevNode, displayType: atom.type)
|
||||
let shouldBreak = tempIsTooTall || tempIsTooWide
|
||||
|
||||
if isTooTall || isTooWide {
|
||||
// Restore state (remove any scripts that were added)
|
||||
displayAtoms.removeLast(displayAtoms.count - savedDisplayAtomsCount)
|
||||
currentPosition = savedPosition
|
||||
|
||||
// Perform line break if needed
|
||||
if shouldBreak {
|
||||
performLineBreak()
|
||||
} else {
|
||||
self.addInterElementSpace(prevNode, currentType:atom.type)
|
||||
}
|
||||
|
||||
// Position and add the operator display
|
||||
display!.position = currentPosition
|
||||
// Now create the display at the correct position (after spacing/line break)
|
||||
// makeLargeOp sets position, advances currentPosition.x, and adds scripts
|
||||
let display = self.makeLargeOp(op)
|
||||
displayAtoms.append(display!)
|
||||
currentPosition.x += display!.width
|
||||
|
||||
case .inner:
|
||||
// Create the inner display first
|
||||
@@ -774,7 +817,15 @@ class MTTypesetter {
|
||||
}
|
||||
|
||||
// Check if we need to break before adding this inner content
|
||||
if shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: .inner) {
|
||||
let shouldBreak = shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: .inner)
|
||||
|
||||
// Flush current line to convert accumulated text to displays
|
||||
if currentLine.length > 0 {
|
||||
self.addDisplayLine()
|
||||
}
|
||||
|
||||
// Perform line break if needed
|
||||
if shouldBreak {
|
||||
performLineBreak()
|
||||
} else {
|
||||
self.addInterElementSpace(prevNode, currentType:atom.type)
|
||||
@@ -908,7 +959,15 @@ class MTTypesetter {
|
||||
|
||||
// Check if we need to break before adding this table
|
||||
// We will consider tables as inner
|
||||
if shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: .inner) {
|
||||
let shouldBreak = shouldBreakBeforeDisplay(display!, prevNode: prevNode, displayType: .inner)
|
||||
|
||||
// Flush current line to convert accumulated text to displays
|
||||
if currentLine.length > 0 {
|
||||
self.addDisplayLine()
|
||||
}
|
||||
|
||||
// Perform line break if needed
|
||||
if shouldBreak {
|
||||
performLineBreak()
|
||||
} else {
|
||||
self.addInterElementSpace(prevNode, currentType:.inner)
|
||||
|
||||
Reference in New Issue
Block a user