-
Notifications
You must be signed in to change notification settings - Fork 8
/
SwiftUI.swift
134 lines (118 loc) · 5.04 KB
/
SwiftUI.swift
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import Cocoa
import SwiftUI
extension AttributedStringConvertible {
/// Render a SwiftUI view as the background of this attributed string.
public func background<Content: View>(@ViewBuilder content: () -> Content) -> some AttributedStringConvertible {
let c = content()
return EnvironmentReader(\.modifyEnv) { modifyEnv in
self.modify(perform: {
$0.backgroundView = AnyView(c
.transformEnvironment(\.self, transform: modifyEnv)
.font(Font($0.computedFont))
)
})
}
}
}
extension View {
func snapshot(proposal: ProposedViewSize) -> NSImage? {
let controller = NSHostingController(rootView: self.frame(width: proposal.width, height: proposal.height))
let targetSize = controller.view.intrinsicContentSize
let contentRect = NSRect(origin: .zero, size: targetSize)
let window = NSWindow(
contentRect: contentRect,
styleMask: [.borderless],
backing: .buffered,
defer: false
)
window.contentView = controller.view
guard
let bitmapRep = controller.view.bitmapImageRepForCachingDisplay(in: contentRect)
else { return nil }
controller.view.cacheDisplay(in: contentRect, to: bitmapRep)
let image = NSImage(size: bitmapRep.size)
image.addRepresentation(bitmapRep)
return image
}
}
struct ModifySwiftUIEnvironment: EnvironmentKey {
static var defaultValue: (inout SwiftUI.EnvironmentValues) -> () = { _ in () }
}
extension EnvironmentValues {
var modifyEnv: (inout SwiftUI.EnvironmentValues) -> () {
get { self[ModifySwiftUIEnvironment.self] }
set { self[ModifySwiftUIEnvironment.self] = newValue }
}
}
extension AttributedStringConvertible {
public func transformSwiftUIEnvironment(_ transform: @escaping (inout SwiftUI.EnvironmentValues) -> ()) -> some AttributedStringConvertible {
environment(\.modifyEnv, value: transform)
}
}
struct DefaultEmbedProposal: EnvironmentKey {
static let defaultValue: ProposedViewSize = .unspecified
}
extension EnvironmentValues {
/// The default proposal that's used for ``Embed``
public var defaultProposal: ProposedViewSize {
get { self[DefaultEmbedProposal.self] }
set { self[DefaultEmbedProposal.self] = newValue }
}
}
/// This takes a SwiftUI view and renders it to an image that's embedded into the resulting attributed string.
///
/// You can customize the default proposal through the ``defaultProposal`` property in the environment.
public struct Embed<V: View>: AttributedStringConvertible {
/// Embed a SwiftUI view into an attributed string
/// - Parameters:
/// - proposal: The size that's proposed to the view or `nil` if you want to have the default proposal (from the environment).
/// - scale: The scale at which the view should be rendered
/// - bitmap: Whether or not to embed the rendered image as a bitmap
/// - view: The view
public init(proposal: ProposedViewSize? = nil, scale: CGFloat = 1, bitmap: Bool = false, @ViewBuilder view: () -> V) {
self.proposal = proposal
self.view = view()
self.scale = scale
self.bitmap = bitmap
}
var scale: CGFloat
var proposal: ProposedViewSize?
var bitmap: Bool
var view: V
@MainActor
public func attributedString(context: inout Context) -> [NSAttributedString] {
let proposal = self.proposal ?? context.environment.defaultProposal
let theView = view
.transformEnvironment(\.self, transform: context.environment.modifyEnv)
.font(SwiftUI.Font(context.environment.attributes.computedFont))
if bitmap {
let i = theView.snapshot(proposal: proposal)!
i.size.width *= scale
i.size.height *= scale
return i.attributedString(context: &context)
} else {
let renderer = ImageRenderer(content: theView)
renderer.proposedSize = proposal
let _ = renderer.nsImage! // this is necessary to get the correct size in the .render closure, even for pdf
let data = NSMutableData()
renderer.render { size, renderer in
var mediaBox = CGRect(origin: .zero, size: size)
guard let consumer = CGDataConsumer(data: data),
let pdfContext = CGContext(consumer: consumer, mediaBox: &mediaBox, nil)
else {
return
}
pdfContext.beginPDFPage(nil)
pdfContext.translateBy(x: mediaBox.size.width / 2 - size.width / 2,
y: mediaBox.size.height / 2 - size.height / 2)
renderer(pdfContext)
pdfContext.endPDFPage()
pdfContext.closePDF()
}
let i = NSImage(data: data as Data)!
i.size.width *= scale
i.size.height *= scale
return i.attributedString(context: &context)
}
}
}