diff options
Diffstat (limited to 'Sources/NorgUI/Math/SwiftMathRenderer.swift')
| -rw-r--r-- | Sources/NorgUI/Math/SwiftMathRenderer.swift | 60 |
1 files changed, 60 insertions, 0 deletions
diff --git a/Sources/NorgUI/Math/SwiftMathRenderer.swift b/Sources/NorgUI/Math/SwiftMathRenderer.swift new file mode 100644 index 0000000..6012a44 --- /dev/null +++ b/Sources/NorgUI/Math/SwiftMathRenderer.swift @@ -0,0 +1,60 @@ +#if SwiftMathSupport + import SwiftMath + import SwiftUI + + /// ``NorgMathRenderer`` backed by SwiftMath. Enabled by the + /// `SwiftMathSupport` package trait. + struct SwiftMathRenderer: NorgMathRenderer { + @MainActor + func render( + latex: String, mode: NorgMathMode, fontSize: CGFloat, color: Color? + ) async -> NorgMathImage? { + let label = MTMathUILabel() + label.latex = latex + label.labelMode = mode == .block ? .display : .text + label.fontSize = mode == .block ? fontSize * 1.4 : fontSize + if let color { + #if canImport(UIKit) + label.textColor = UIColor(color) + #elseif canImport(AppKit) + label.textColor = NSColor(color) + #endif + } + + let size = label.intrinsicContentSize + guard size.width > 0, size.height > 0, !latex.isEmpty else { return nil } + guard let image = rasterize(label, size: size) else { return nil } + + // SwiftMath does not expose the layout descent publicly, so approximate + // it by centring the formula on the text line. Tune if inline math sits + // too high or low against surrounding text. + let descent = max(0, (size.height - fontSize) / 2) + return NorgMathImage(image: image, size: size, descent: descent) + } + + @MainActor + private func rasterize(_ label: MTMathUILabel, size: CGSize) -> PlatformImage? { + label.frame = CGRect(origin: .zero, size: size) + #if canImport(UIKit) + label.layoutIfNeeded() + let format = UIGraphicsImageRendererFormat() + format.scale = UIScreen.main.scale + format.opaque = false + let renderer = UIGraphicsImageRenderer(size: size, format: format) + return renderer.image { context in + let cg = context.cgContext + cg.translateBy(x: 0, y: size.height) + cg.scaleBy(x: 1, y: -1) + label.layer.render(in: cg) + } + #elseif canImport(AppKit) + label.layoutSubtreeIfNeeded() + guard let rep = label.bitmapImageRepForCachingDisplay(in: label.bounds) else { return nil } + label.cacheDisplay(in: label.bounds, to: rep) + let image = NSImage(size: size) + image.addRepresentation(rep) + return image + #endif + } + } +#endif |