aboutsummaryrefslogtreecommitdiff
path: root/Sources/NorgUI/Math/SwiftMathRenderer.swift
blob: 6012a4485a729c286b40aa1c9791da426ecaea51 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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