Sources/OrgSwiftUI/OrgMathText.swift
64 lines · 2382 bytes
1import OrgSwift
2import SwiftUI
3
4/// Draws one TeX expression for ``OrgView``: a `Text` holding it, typically an image set on
5/// the baseline, or nil to show the source as written.
6public typealias OrgMathRenderer = @MainActor (OrgMath) -> Text?
7
8private struct OrgMathKey: EnvironmentKey {
9 static let defaultValue: OrgMathRenderer? = nil
10}
11
12extension EnvironmentValues {
13 var orgMath: OrgMathRenderer? {
14 get { self[OrgMathKey.self] }
15 set { self[OrgMathKey.self] = newValue }
16 }
17}
18
19/// A styled run as `Text`, each expression drawn by `math` where it can be. Runs are taken
20/// by their math attribute alone, so an expression a `textTransform` split is still one;
21/// two identical expressions side by side share a range, so one holding its source k times
22/// draws k.
23@MainActor
24func orgText(_ string: AttributedString, math: OrgMathRenderer?) -> Text {
25 guard let math, string.runs.contains(where: { $0[OrgMathAttribute.self] != nil }) else {
26 return Text(string)
27 }
28 var result = Text(verbatim: "")
29 for (expression, range) in string.runs[OrgMathAttribute.self] {
30 let piece = AttributedString(string[range])
31 guard let expression, let drawn = math(expression) else {
32 result = Text("\(result)\(Text(piece))")
33 continue
34 }
35 let count = max(1, piece.characters.count / max(1, expression.source.count))
36 for _ in 0..<count { result = Text("\(result)\(drawn)") }
37 }
38 return result
39}
40
41/// Display math on its own: centred, scrolling sideways when wider than the column, or the
42/// source as a code block where nothing draws it.
43struct OrgMathBlockView: View {
44 let math: OrgMath
45 @Environment(\.orgMath) private var renderer
46
47 var body: some View {
48 if let drawn = renderer?(math) {
49 ViewThatFits(in: .horizontal) {
50 drawn.frame(maxWidth: .infinity)
51 ScrollView(.horizontal, showsIndicators: false) { drawn }
52 }
53 .padding(.vertical, 4)
54 } else {
55 ScrollView(.horizontal) {
56 Text(math.source)
57 .font(.system(.footnote, design: .monospaced))
58 .textSelection(.enabled)
59 .padding(10)
60 }
61 .background(Color.secondary.opacity(0.12), in: RoundedRectangle(cornerRadius: 4))
62 }
63 }
64}