diff --git a/Package.swift b/Package.swift index ceeb038..ea00947 100755 --- a/Package.swift +++ b/Package.swift @@ -5,10 +5,10 @@ import PackageDescription let package = Package( name: "swiftui-math", platforms: [ - .macOS(.v12), - .iOS(.v15), - .tvOS(.v15), - .watchOS(.v8), + .macOS(.v14), + .iOS(.v17), + .tvOS(.v17), + .watchOS(.v10), .visionOS(.v1), ], products: [ diff --git a/Sources/SwiftUIMath/Internal/Display/CGContext+DisplayNode.swift b/Sources/SwiftUIMath/Internal/Display/CGContext+DisplayNode.swift new file mode 100644 index 0000000..5d0279a --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/CGContext+DisplayNode.swift @@ -0,0 +1,227 @@ +import CoreGraphics +import CoreText +import Foundation + +extension CGContext { + func draw(_ displayNode: Math.DisplayNode, foregroundColor: CGColor) { + let foregroundColor = + displayNode.localTextColor?.cgColor + ?? displayNode.textColor?.cgColor + ?? foregroundColor + + switch displayNode { + case let list as Math.DisplayList: + draw(list, foregroundColor: foregroundColor) + case let textRun as Math.DisplayTextRun: + draw(textRun, foregroundColor: foregroundColor) + case let glyph as Math.DisplayGlyph: + draw(glyph, foregroundColor: foregroundColor) + case let glyphRun as Math.DisplayGlyphRun: + draw(glyphRun, foregroundColor: foregroundColor) + case let fraction as Math.DisplayFraction: + draw(fraction, foregroundColor: foregroundColor) + case let radical as Math.DisplayRadical: + draw(radical, foregroundColor: foregroundColor) + case let line as Math.DisplayLine: + draw(line, foregroundColor: foregroundColor) + case let largeOperator as Math.DisplayLargeOperator: + draw(largeOperator, foregroundColor: foregroundColor) + case let accent as Math.DisplayAccent: + draw(accent, foregroundColor: foregroundColor) + default: + break + } + } +} + +extension CGContext { + private func draw(_ list: Math.DisplayList, foregroundColor: CGColor) { + saveGState() + + translateBy(x: list.position.x, y: list.position.y) + textPosition = .zero + + for child in list.children { + draw(child, foregroundColor: foregroundColor) + } + + restoreGState() + } + + private func draw(_ textRun: Math.DisplayTextRun, foregroundColor: CGColor) { + guard let platformFont = Math.PlatformFont(font: textRun.font) else { + return + } + + let attributes: [NSAttributedString.Key: Any] = [ + NSAttributedString.Key(kCTFontAttributeName as String): platformFont.ctFont, + NSAttributedString.Key(kCTForegroundColorAttributeName as String): foregroundColor, + ] + let attributedString = NSAttributedString(string: textRun.text, attributes: attributes) + let line = CTLineCreateWithAttributedString(attributedString) + + saveGState() + textPosition = textRun.position + CTLineDraw(line, self) + restoreGState() + } + + private func draw(_ glyph: Math.DisplayGlyph, foregroundColor: CGColor) { + guard let platformFont = Math.PlatformFont(font: glyph.font) else { + return + } + + saveGState() + + translateBy(x: glyph.position.x, y: glyph.position.y - glyph.shiftDown) + textPosition = .zero + + setFillColor(foregroundColor) + + var cgGlyph = CGGlyph(glyph.glyph) + var pos = CGPoint.zero + CTFontDrawGlyphs(platformFont.ctFont, &cgGlyph, &pos, 1, self) + + restoreGState() + } + + private func draw(_ glyphRun: Math.DisplayGlyphRun, foregroundColor: CGColor) { + guard let platformFont = Math.PlatformFont(font: glyphRun.font) else { + return + } + + saveGState() + + translateBy(x: glyphRun.position.x, y: glyphRun.position.y - glyphRun.shiftDown) + textPosition = .zero + + setFillColor(foregroundColor) + + var glyphs = glyphRun.glyphs.map { CGGlyph($0) } + var positions = glyphRun.offsets.map { CGPoint(x: 0, y: $0) } + CTFontDrawGlyphs(platformFont.ctFont, &glyphs, &positions, glyphs.count, self) + + restoreGState() + } + + private func draw(_ fraction: Math.DisplayFraction, foregroundColor: CGColor) { + if let numerator = fraction.numerator { + draw(numerator, foregroundColor: foregroundColor) + } + if let denominator = fraction.denominator { + draw(denominator, foregroundColor: foregroundColor) + } + + guard fraction.lineThickness > 0 else { + return + } + + saveGState() + + setStrokeColor(foregroundColor) + setLineWidth(fraction.lineThickness) + + let lineStart = CGPoint( + x: fraction.position.x, + y: fraction.position.y + fraction.linePosition + ) + let lineEnd = CGPoint( + x: fraction.position.x + fraction.width, + y: lineStart.y + ) + + move(to: lineStart) + addLine(to: lineEnd) + strokePath() + + restoreGState() + } + + private func draw(_ radical: Math.DisplayRadical, foregroundColor: CGColor) { + if let radicand = radical.radicand { + draw(radicand, foregroundColor: foregroundColor) + } + if let degree = radical.degree { + draw(degree, foregroundColor: foregroundColor) + } + + saveGState() + + setStrokeColor(foregroundColor) + setFillColor(foregroundColor) + + translateBy(x: radical.position.x + radical.radicalShift, y: radical.position.y) + textPosition = .zero + + if let radicalGlyph = radical.radicalGlyph { + draw(radicalGlyph, foregroundColor: foregroundColor) + } + + let heightFromTop = radical.topKern + let glyphWidth = radical.radicalGlyph?.width ?? 0 + let radicandWidth = radical.radicand?.width ?? 0 + let lineStart = CGPoint( + x: glyphWidth, + y: radical.ascent - heightFromTop - radical.lineThickness / 2 + ) + let lineEnd = CGPoint(x: lineStart.x + radicandWidth, y: lineStart.y) + + setLineWidth(radical.lineThickness) + setLineCap(.round) + move(to: lineStart) + addLine(to: lineEnd) + strokePath() + + restoreGState() + } + + private func draw(_ line: Math.DisplayLine, foregroundColor: CGColor) { + if let inner = line.inner { + draw(inner, foregroundColor: foregroundColor) + } + + saveGState() + + setStrokeColor(foregroundColor) + setLineWidth(line.lineThickness) + + let lineStart = CGPoint(x: line.position.x, y: line.position.y + line.lineShiftUp) + let lineEnd = CGPoint(x: lineStart.x + (line.inner?.width ?? 0), y: lineStart.y) + + move(to: lineStart) + addLine(to: lineEnd) + strokePath() + + restoreGState() + } + + private func draw(_ largeOperator: Math.DisplayLargeOperator, foregroundColor: CGColor) { + if let upperLimit = largeOperator.upperLimit { + draw(upperLimit, foregroundColor: foregroundColor) + } + if let lowerLimit = largeOperator.lowerLimit { + draw(lowerLimit, foregroundColor: foregroundColor) + } + if let nucleus = largeOperator.nucleus { + draw(nucleus, foregroundColor: foregroundColor) + } + } + + private func draw(_ accent: Math.DisplayAccent, foregroundColor: CGColor) { + if let accentee = accent.accentee { + draw(accentee, foregroundColor: foregroundColor) + } + + guard let accentGlyph = accent.accent else { + return + } + + saveGState() + + translateBy(x: accent.position.x, y: accent.position.y) + textPosition = .zero + draw(accentGlyph, foregroundColor: foregroundColor) + + restoreGState() + } +} diff --git a/Sources/SwiftUIMath/Internal/Display/GraphicsContext+DisplayNode.swift b/Sources/SwiftUIMath/Internal/Display/GraphicsContext+DisplayNode.swift new file mode 100644 index 0000000..9f03534 --- /dev/null +++ b/Sources/SwiftUIMath/Internal/Display/GraphicsContext+DisplayNode.swift @@ -0,0 +1,27 @@ +import SwiftUI + +extension GraphicsContext { + func draw(_ displayNode: Math.DisplayNode, size: CGSize, foregroundColor: Color) { + var context = self + + context.translateBy(x: 0, y: size.height) + context.scaleBy(x: 1, y: -1) + + let foregroundColor = foregroundColor.resolve(in: environment).cgColor + + context.withCGContext { cgContext in + cgContext.draw(displayNode, foregroundColor: foregroundColor) + } + } + + func draw(_ displayNode: Math.DisplayNode, size: CGSize, with shading: GraphicsContext.Shading) { + var context = self + + context.fill(Path(CGRect(origin: .zero, size: size)), with: shading) + context.blendMode = .destinationIn + + context.drawLayer { + $0.draw(displayNode, size: size, foregroundColor: .black) + } + } +} diff --git a/Tests/SwiftUIMathTests/Internal/Display/TypesetterTests.swift b/Tests/SwiftUIMathTests/Internal/Display/TypesetterTests.swift index 8bfec16..b6af879 100755 --- a/Tests/SwiftUIMathTests/Internal/Display/TypesetterTests.swift +++ b/Tests/SwiftUIMathTests/Internal/Display/TypesetterTests.swift @@ -1567,10 +1567,10 @@ struct TypesetterTests { } } - @Test - func variables() throws { - // Test all variables - let allSymbols = Math.AtomFactory.supportedLatexSymbolNames + @Test + func variables() throws { + // Test all variables + let allSymbols = Math.AtomFactory.supportedLatexSymbolNames for symName in allSymbols { let atom = Math.AtomFactory.atom(forLatexSymbol: symName)! if atom.type != .variable { @@ -2701,9 +2701,9 @@ struct TypesetterTests { } @Test - func sumEquationWithFraction_WithWidthConstraint() throws { - // Test case for: \(\sum_{i=1}^{n} i = \frac{n(n+1)}{2}\) with width constraint - // This reproduces the issue where = appears at the end instead of in the middle + func sumEquationWithFraction_WithWidthConstraint() throws { + // Test case for: \(\sum_{i=1}^{n} i = \frac{n(n+1)}{2}\) with width constraint + // This reproduces the issue where = appears at the end instead of in the middle let latex = "\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}" let mathList = Math.Parser.build(fromString: latex) #expect(mathList != nil)