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