aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgUI/Math/SwiftMathRenderer.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/NorgUI/Math/SwiftMathRenderer.swift')
-rw-r--r--Sources/NorgUI/Math/SwiftMathRenderer.swift60
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