From cb890fb787cd7961487233ccc7b5e9be96924438 Mon Sep 17 00:00:00 2001 From: Nicolas Guillot Date: Mon, 17 Nov 2025 09:37:59 +0100 Subject: [PATCH] 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. --- .../SwiftMath/MathRender/MTMathUILabel.swift | 110 +++++++++++------- .../SwiftMath/MathRender/MTTypesetter.swift | 42 +++---- 2 files changed, 84 insertions(+), 68 deletions(-) diff --git a/Sources/SwiftMath/MathRender/MTMathUILabel.swift b/Sources/SwiftMath/MathRender/MTMathUILabel.swift index 65a560b..cb3a276 100644 --- a/Sources/SwiftMath/MathRender/MTMathUILabel.swift +++ b/Sources/SwiftMath/MathRender/MTMathUILabel.swift @@ -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 diff --git a/Sources/SwiftMath/MathRender/MTTypesetter.swift b/Sources/SwiftMath/MathRender/MTTypesetter.swift index 8bc4e5e..8df9b8b 100644 --- a/Sources/SwiftMath/MathRender/MTTypesetter.swift +++ b/Sources/SwiftMath/MathRender/MTTypesetter.swift @@ -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!)