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) {
super.draw(dirtyRect)
if self.mathList == nil { return }
if self.font == nil { return }
// drawing code
let context = MTGraphicsGetCurrentContext()!
@@ -253,48 +254,64 @@ public class MTMathUILabel : MTView {
}
func _layoutSubviews() {
if _mathList != nil {
// 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 {
guard _mathList != nil && self.font != nil else {
_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
self.setNeedsDisplay()
}
@@ -305,6 +322,17 @@ public class MTMathUILabel : MTView {
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
var maxWidth: CGFloat = 0
if _preferredMaxLayoutWidth > 0 {
@@ -314,7 +342,7 @@ public class MTMathUILabel : MTView {
}
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 {
// 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.
return 8;
} else {
assert(false, "Interelement space undefined for radical on the right. Treat radical as ordinary.")
return Int.max
// Treat radical as ordinary on the right side
return 0
}
default:
assert(false, "Interelement space undefined for type \(type)")
return Int.max
// Numbers, variables, and unary operators are treated as ordinary
case .number, .variable, .unaryOperator:
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()
}
// Create the large operator display to check if we need line breaking
let op = atom as! MTLargeOperator?
// Add inter-element spacing before operator
self.addInterElementSpace(prevNode, currentType: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
// 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)
// Create and position the large operator display
// makeLargeOp sets position, advances currentPosition.x, and adds scripts
let op = atom as! MTLargeOperator?
let display = self.makeLargeOp(op)
displayAtoms.append(display!)