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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
//! The bridge that puts a SwiftUI view inside a native view.
//!
//! AppKit has `NSHostingView`, a view you can use directly. UIKit has no
//! equivalent: the only bridge is `UIHostingController`, and the view comes out
//! of its `.view` property. So on iOS this type holds both — the controller,
//! because that is what owns `rootView` and answers `sizeThatFits`, and the
//! view, because that is what everything else wants.
use crate::abi;
use crate::objc::{self, NSRect};
use crate::resolve;
use crate::views::ViewHandle;
use core::ffi::c_void;
/// A hosting view whose root view can be replaced at any time.
pub struct HostingView {
/// The `NSView`/`UIView` SwiftUI draws into.
ptr: *mut c_void,
/// On UIKit the SwiftUI side lives on the controller, not the view.
#[cfg(not(target_os = "macos"))]
controller: *mut c_void,
/// The offscreen window this view is captured in, made on first use.
///
/// One per hosting view, not one per capture. Per capture leaks a window
/// every frame — an application holds on to every window it makes — and
/// closing them instead detaches the view between frames, which stops an
/// animation dead. One window that lives exactly as long as the view it
/// hosts does neither.
window: core::cell::Cell<*mut c_void>,
}
impl HostingView {
/// Wrap a view in a fresh hosting view.
#[cfg(target_os = "macos")]
pub fn new(view: &ViewHandle) -> Self {
let _pool = objc::AutoreleasePool::new();
Self {
ptr: unsafe { make_hosting_view(view) },
window: core::cell::Cell::new(core::ptr::null_mut()),
}
}
/// Wrap a view in a fresh hosting controller, and take its view.
#[cfg(not(target_os = "macos"))]
pub fn new(view: &ViewHandle) -> Self {
let _pool = objc::AutoreleasePool::new();
unsafe {
let controller = make_hosting_controller(view);
// Asking for `.view` is what makes the controller load it.
let ptr = objc::send(controller, objc::sel(c"view"));
assert!(!ptr.is_null(), "UIHostingController produced no view");
Self { ptr, controller, window: core::cell::Cell::new(core::ptr::null_mut()) }
}
}
/// The native view SwiftUI draws into.
pub fn as_ptr(&self) -> *mut c_void {
self.ptr
}
/// The object that owns `rootView`: the view on AppKit, the controller on UIKit.
fn root_owner(&self) -> *mut c_void {
#[cfg(target_os = "macos")]
{
self.ptr
}
#[cfg(not(target_os = "macos"))]
{
self.controller
}
}
/// The view controller, on platforms that have one.
#[cfg(not(target_os = "macos"))]
pub fn controller(&self) -> *mut c_void {
self.controller
}
/// Swap the root view. SwiftUI diffs the new tree against the old one and
/// updates the rendered output in place — this is how a Rust-driven UI
/// changes after it is on screen.
pub fn set_root(&self, view: &ViewHandle) {
let _pool = objc::AutoreleasePool::new();
unsafe {
// `rootView` is a stored property on a generic class, so the setter
// takes the new value indirectly (x0) with `self` in the Swift self
// register (x20). It is reached through its dispatch thunk.
// The setter consumes the value, and `to_anyview_word` already
// mints a `+1` — it boxes a copy of the handle's own value rather
// than lending the handle's reference, as it did when a handle *was*
// an `AnyView`. The retain that used to balance this is now one too
// many.
let mut incoming: u64 = view.to_anyview_word();
core::arch::asm!("blr {f}", f = in(reg) resolve::hosting_root_setter(),
in("x0") &mut incoming as *mut u64,
in("x20") self.root_owner(),
lateout("x1") _, lateout("x2") _, lateout("x3") _,
lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
lateout("x8") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
clobber_abi("C"),
);
}
}
/// Lay the view out at a fixed size.
pub fn set_frame(&self, width: f64, height: f64) {
let _pool = objc::AutoreleasePool::new();
unsafe {
objc::send_rect(
self.ptr,
objc::sel(c"setFrame:"),
NSRect::new(0.0, 0.0, width, height),
);
#[cfg(target_os = "macos")]
objc::send(self.ptr, objc::sel(c"layoutSubtreeIfNeeded"));
#[cfg(not(target_os = "macos"))]
{
objc::send(self.ptr, objc::sel(c"setNeedsLayout"));
objc::send(self.ptr, objc::sel(c"layoutIfNeeded"));
}
}
}
/// Whether SwiftUI should inset its content for the window's safe area.
///
/// A hosting controller reads the safe area of whatever window it ends up
/// in, so on a device with a notch a view asked for at 240x160 comes back
/// with a band across the top that has nothing to do with the view. For
/// rendering that is noise; for an app it is the point. `safeAreaRegions`
/// is the supported way to say which you want.
#[cfg(not(target_os = "macos"))]
pub fn set_safe_area(&self, enabled: bool) {
unsafe {
// `SafeAreaRegions` is an OptionSet over a single word; the empty
// set is zero and `.all` is every bit.
let regions: u64 = if enabled { u64::MAX } else { 0 };
core::arch::asm!("blr {f}", f = in(reg) resolve::hosting_ctrl_safe_area(),
in("x0") regions,
in("x20") self.controller,
lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
);
}
}
/// The size SwiftUI would like this view to be.
#[cfg(target_os = "macos")]
pub fn fitting_size(&self) -> (f64, f64) {
let s = unsafe { objc::send_ret_size(self.ptr, objc::sel(c"fittingSize")) };
(s.width, s.height)
}
/// The size SwiftUI would like this view to be.
///
/// `UIHostingController.sizeThatFits(in:)` rather than the view's own: only
/// the controller asks SwiftUI. A zero target is UIKit's
/// `layoutFittingCompressedSize`, i.e. "as small as you can be".
#[cfg(not(target_os = "macos"))]
pub fn fitting_size(&self) -> (f64, f64) {
unsafe {
let (w, h): (f64, f64);
core::arch::asm!("blr {f}", f = in(reg) resolve::hosting_ctrl_size(),
in("d0") 0.0f64, in("d1") 0.0f64,
in("x20") self.controller,
lateout("d0") w, lateout("d1") h,
lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
lateout("x8") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
clobber_abi("C"),
);
(w, h)
}
}
}
impl HostingView {
/// The offscreen window to capture this view in, created on first use and
/// resized to `width` x `height`.
///
/// # Safety
/// Must be called on the main thread.
pub(crate) unsafe fn offscreen(&self, width: f64, height: f64) -> *mut c_void {
unsafe { offscreen_window(self, width, height) }
}
}
impl Drop for HostingView {
fn drop(&mut self) {
let _pool = objc::AutoreleasePool::new();
unsafe {
let window = self.window.get();
if !window.is_null() {
#[cfg(target_os = "macos")]
{
// Closing takes it out of the application's window list,
// which is the only thing keeping it alive. `release` alone
// frees nothing.
objc::send(window, objc::sel(c"close"));
}
#[cfg(not(target_os = "macos"))]
{
// A `UIWindow` has no `close` — sending it one raises. It
// is hidden and released instead.
objc::send_bool(window, objc::sel(c"setHidden:"), true);
objc::send(window, objc::sel(c"release"));
}
}
}
// `init(rootView:)` hands back an owned reference. Anything this was
// installed into — a window's content view, a window's root view
// controller — took its own, so letting go of ours is safe and is the
// difference between a render costing nothing and costing a hosting
// view every time.
unsafe {
#[cfg(target_os = "macos")]
objc::send(self.ptr, objc::sel(c"release"));
#[cfg(not(target_os = "macos"))]
objc::send(self.controller, objc::sel(c"release"));
}
}
}
/// Build `NSHostingView<AnyView>` and call `init(rootView:)` under the Swift CC.
#[cfg(target_os = "macos")]
unsafe fn make_hosting_view(view: &ViewHandle) -> *mut c_void {
// `NSHostingView<V: View>` takes TWO key arguments: `V.Type` and the
// `V: View` witness table. The class loads both straight back out of its own
// metadata (`ldp x1, x2, [x20, #0x50]` in its init), so passing only
// `V.Type` leaves the conformance slot holding register garbage — which
// faults deep inside the Swift metadata cache rather than here.
unsafe {
let meta = abi::generic_metadata2(
resolve::hosting_view_ma(),
resolve::anyview_meta(),
resolve::anyview_wt(),
);
init_root_view(resolve::hosting_view_init(), meta, view, "NSHostingView")
}
}
/// Build `UIHostingController<AnyView>` and call `init(rootView:)`.
#[cfg(not(target_os = "macos"))]
unsafe fn make_hosting_controller(view: &ViewHandle) -> *mut c_void {
unsafe {
let meta = abi::generic_metadata2(
resolve::hosting_ctrl_ma(),
resolve::anyview_meta(),
resolve::anyview_wt(),
);
init_root_view(resolve::hosting_ctrl_init(), meta, view, "UIHostingController")
}
}
/// `init(rootView:)` — x0 = @in AnyView, x20 = the class's metadata.
unsafe fn init_root_view(
init: *const c_void,
meta: *const c_void,
view: &ViewHandle,
what: &str,
) -> *mut c_void {
unsafe {
let mut storage: u64 = view.to_anyview_word();
swift_runtime_sys::RuntimeRaw::swift_retain(storage as *mut c_void);
let out: *mut c_void;
core::arch::asm!("blr {f}", f = in(reg) init,
in("x0") &mut storage as *mut u64,
in("x20") meta,
lateout("x0") out,
lateout("x1") _, lateout("x2") _, lateout("x3") _,
lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
lateout("x8") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
clobber_abi("C"),
);
assert!(!out.is_null(), "{what} creation failed");
out
}
}
/// Make (once) and size the offscreen window a hosting view is captured in.
///
/// Borderless and buffered, so the content rect is the whole window, and pinned
/// to the light appearance so a capture does not depend on the host's system
/// setting.
#[cfg(target_os = "macos")]
unsafe fn offscreen_window(hv: &HostingView, width: f64, height: f64) -> *mut c_void {
unsafe {
let bounds = NSRect::new(0.0, 0.0, width, height);
let existing = hv.window.get();
if !existing.is_null() {
type SetFrame = unsafe extern "C" fn(*mut c_void, *const c_void, NSRect, bool);
let set_frame: SetFrame = core::mem::transmute(crate::objc::msg_send_addr());
set_frame(existing, objc::sel(c"setFrame:display:"), bounds, false);
return existing;
}
let _ = objc::shared_app();
let win = objc::send(objc::class(c"OBJC_CLASS_$_NSWindow"), objc::sel(c"alloc"));
type InitWin =
unsafe extern "C" fn(*mut c_void, *const c_void, NSRect, u64, u64, bool) -> *mut c_void;
let init: InitWin = core::mem::transmute(crate::objc::msg_send_addr());
// borderless | buffered
let win = init(
win,
objc::sel(c"initWithContentRect:styleMask:backing:defer:"),
bounds,
0,
2,
false,
);
let name = objc::nsstring("NSAppearanceNameAqua");
let appearance = objc::send_id(
objc::class(c"OBJC_CLASS_$_NSAppearance"),
objc::sel(c"appearanceNamed:"),
name,
);
if !appearance.is_null() {
objc::send_id(win, objc::sel(c"setAppearance:"), appearance);
}
objc::send_id(win, objc::sel(c"setContentView:"), hv.as_ptr());
hv.window.set(win);
win
}
}
/// UIKit's equivalent: a window purely so the view gets laid out.
#[cfg(not(target_os = "macos"))]
unsafe fn offscreen_window(hv: &HostingView, width: f64, height: f64) -> *mut c_void {
unsafe {
let bounds = NSRect::new(0.0, 0.0, width, height);
let existing = hv.window.get();
if !existing.is_null() {
objc::send_rect(existing, objc::sel(c"setFrame:"), bounds);
return existing;
}
let window = objc::send(objc::class(c"OBJC_CLASS_$_UIWindow"), objc::sel(c"alloc"));
type InitFrame = unsafe extern "C" fn(*mut c_void, *const c_void, NSRect) -> *mut c_void;
let init: InitFrame = core::mem::transmute(crate::objc::msg_send_addr());
let window = init(window, objc::sel(c"initWithFrame:"), bounds);
// The capture is of the view at exactly the size asked for, so the
// window's safe area must not push the content around.
hv.set_safe_area(false);
// As a subview, not as `rootViewController`: a root view controller is
// laid out inside the window's safe area, which on a device with a
// notch inserts a band at the top that has nothing to do with the view
// under test.
objc::send_id(window, objc::sel(c"addSubview:"), hv.as_ptr());
// Not `makeKeyAndVisible`: the window only has to exist for layout, and
// a process spawned without an application should not try to present.
objc::send_bool(window, objc::sel(c"setHidden:"), false);
hv.window.set(window);
window
}
}