Fix assert failures for unhandled atom types in inter-element spacing

Replace fatal asserts with explicit handling for all MTMathAtomType cases
  in getInterElementSpaceArrayIndexForType(). Previously, unhandled types
  (accent, number, variable, unaryOperator, underline, overline, boundary,
  space, style, table) would trigger assert failures and return Int.max,
  causing array out-of-bounds crashes.
This commit is contained in:
Nicolas Guillot
2025-11-17 09:37:59 +01:00
parent b014be12b4
commit cb890fb787
2 changed files with 84 additions and 68 deletions

View File

@@ -244,6 +244,7 @@ public class MTMathUILabel : MTView {
override public func draw(_ dirtyRect: MTRect) { override public func draw(_ dirtyRect: MTRect) {
super.draw(dirtyRect) super.draw(dirtyRect)
if self.mathList == nil { return } if self.mathList == nil { return }
if self.font == nil { return }
// drawing code // drawing code
let context = MTGraphicsGetCurrentContext()! let context = MTGraphicsGetCurrentContext()!
@@ -253,48 +254,64 @@ public class MTMathUILabel : MTView {
} }
func _layoutSubviews() { func _layoutSubviews() {
if _mathList != nil { guard _mathList != nil && self.font != nil else {
// Use the effective width for layout
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 {
case .left: textX = contentInsets.left
case .center: textX = (bounds.size.width - contentInsets.left - contentInsets.right - _displayList!.width) / 2 + contentInsets.left
case .right: textX = bounds.size.width - _displayList!.width - contentInsets.right
}
let availableHeight = bounds.size.height - contentInsets.bottom - contentInsets.top
// center things vertically
var height = _displayList!.ascent + _displayList!.descent
if height < fontSize/2 {
height = fontSize/2 // set height to half the font size
}
let textY = (availableHeight - height) / 2 + _displayList!.descent + contentInsets.bottom
_displayList!.position = CGPointMake(textX, textY)
} else {
_displayList = nil _displayList = nil
errorLabel?.frame = self.bounds
self.setNeedsDisplay()
return
} }
// Ensure we have a valid font before attempting to typeset
if self.font == nil {
// No valid font - try to get default font
if let defaultFont = MTFontManager.fontManager.defaultFont {
self._font = defaultFont
} else {
// Cannot typeset without a font, clear display list
_displayList = nil
errorLabel?.frame = self.bounds
self.setNeedsDisplay()
return
}
}
// Use the effective width for layout
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: self.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 {
case .left: textX = contentInsets.left
case .center: textX = (bounds.size.width - contentInsets.left - contentInsets.right - _displayList!.width) / 2 + contentInsets.left
case .right: textX = bounds.size.width - _displayList!.width - contentInsets.right
}
let availableHeight = bounds.size.height - contentInsets.bottom - contentInsets.top
// center things vertically
var height = _displayList!.ascent + _displayList!.descent
if height < fontSize/2 {
height = fontSize/2 // set height to half the font size
}
let textY = (availableHeight - height) / 2 + _displayList!.descent + contentInsets.bottom
_displayList!.position = CGPointMake(textX, textY)
errorLabel?.frame = self.bounds errorLabel?.frame = self.bounds
self.setNeedsDisplay() self.setNeedsDisplay()
} }
@@ -305,6 +322,17 @@ public class MTMathUILabel : MTView {
return CGSize(width: -1, height: -1) return CGSize(width: -1, height: -1)
} }
// Ensure we have a valid font before attempting to typeset
if self.font == nil {
// No valid font - try to get default font
if let defaultFont = MTFontManager.fontManager.defaultFont {
self._font = defaultFont
} else {
// Cannot typeset without a font
return CGSize(width: -1, height: -1)
}
}
// Determine the maximum width to use // Determine the maximum width to use
var maxWidth: CGFloat = 0 var maxWidth: CGFloat = 0
if _preferredMaxLayoutWidth > 0 { if _preferredMaxLayoutWidth > 0 {
@@ -314,7 +342,7 @@ public class MTMathUILabel : MTView {
} }
var displayList:MTMathListDisplay? = nil var displayList:MTMathListDisplay? = nil
displayList = MTTypesetter.createLineForMathList(_mathList, font: font, style: currentStyle, maxWidth: maxWidth) displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: maxWidth)
guard displayList != nil else { guard displayList != nil else {
// Failed to create display list // Failed to create display list

View File

@@ -73,12 +73,18 @@ func getInterElementSpaceArrayIndexForType(_ type:MTMathAtomType, row:Bool) -> I
// They have the same spacing as ordinary except with ordinary. // They have the same spacing as ordinary except with ordinary.
return 8; return 8;
} else { } else {
assert(false, "Interelement space undefined for radical on the right. Treat radical as ordinary.") // Treat radical as ordinary on the right side
return Int.max return 0
} }
default: // Numbers, variables, and unary operators are treated as ordinary
assert(false, "Interelement space undefined for type \(type)") case .number, .variable, .unaryOperator:
return Int.max return 0
// Decorative types (accent, underline, overline) are treated as ordinary
case .accent, .underline, .overline:
return 0
// Special types that don't typically participate in spacing are treated as ordinary
case .boundary, .space, .style, .table:
return 0
} }
} }
@@ -958,30 +964,12 @@ class MTTypesetter {
self.addDisplayLine() self.addDisplayLine()
} }
// Create the large operator display to check if we need line breaking // Add inter-element spacing before operator
let op = atom as! MTLargeOperator? self.addInterElementSpace(prevNode, currentType:atom.type)
// Save state before creating display (makeLargeOp may add scripts to displayAtoms) // Create and position the large operator display
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
// 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)
}
// Now create the display at the correct position (after spacing/line break)
// makeLargeOp sets position, advances currentPosition.x, and adds scripts // makeLargeOp sets position, advances currentPosition.x, and adds scripts
let op = atom as! MTLargeOperator?
let display = self.makeLargeOp(op) let display = self.makeLargeOp(op)
displayAtoms.append(display!) displayAtoms.append(display!)