swiftui_native/window.rs
1//! Window hosting — pure Rust.
2//!
3//! `NSWindow` on the Mac, `UIWindow` on iOS. The shapes differ: an `NSWindow`
4//! takes a content *view*, a `UIWindow` takes a root view *controller*, and
5//! only AppKit has a title bar to put a title in.
6//!
7//! - `NSHostingView.init(rootView:)` via **Swift CC** (see [`crate::hosting`])
8//! - `NSWindow` / `NSApplication` via the **ObjC runtime** (see [`crate::objc`])
9//!
10//! Both paths use the same dlsym + asm approach — no Swift source code.
11
12use crate::hosting::HostingView;
13use crate::objc::{self, NSRect};
14use crate::views::ViewHandle;
15use core::ffi::c_void;
16
17/// A native window hosting a SwiftUI view built from Rust.
18///
19/// The root view can be replaced at any time with [`Window::set_root`], which is
20/// how a Rust-driven interface updates after it is already on screen.
21pub struct Window {
22 window: *mut c_void,
23 hosting: HostingView,
24}
25
26#[cfg(target_os = "macos")]
27impl Window {
28 /// Create and show a window. Does not start the event loop.
29 pub fn new(view: &ViewHandle, title: &str, width: f64, height: f64) -> Self {
30 unsafe {
31 let hosting = HostingView::new(view);
32 let app = objc::shared_app();
33
34 let win = objc::send(objc::class(c"OBJC_CLASS_$_NSWindow"), objc::sel(c"alloc"));
35 type InitWin =
36 unsafe extern "C" fn(*mut c_void, *const c_void, NSRect, u64, u64, bool) -> *mut c_void;
37 let init: InitWin = core::mem::transmute(objc::msg_send_addr());
38 let win = init(
39 win,
40 objc::sel(c"initWithContentRect:styleMask:backing:defer:"),
41 NSRect::new(0.0, 0.0, width, height),
42 1 | 2 | 4 | 8, // titled | closable | miniaturizable | resizable
43 2, // buffered
44 false,
45 );
46
47 objc::send_id(win, objc::sel(c"setTitle:"), objc::nsstring(title));
48 objc::send_id(win, objc::sel(c"setContentView:"), hosting.as_ptr());
49 objc::send(win, objc::sel(c"center"));
50 objc::send_id(win, objc::sel(c"makeKeyAndOrderFront:"), core::ptr::null_mut());
51
52 objc::send_i64(app, objc::sel(c"setActivationPolicy:"), 0);
53 objc::send_bool(app, objc::sel(c"activateIgnoringOtherApps:"), true);
54
55 Self { window: win, hosting }
56 }
57 }
58
59 /// Replace the displayed view. SwiftUI diffs it against the previous tree.
60 pub fn set_root(&self, view: &ViewHandle) {
61 self.hosting.set_root(view);
62 }
63
64 /// The hosting view, for rendering or further configuration.
65 pub fn hosting(&self) -> &HostingView {
66 &self.hosting
67 }
68
69 pub fn as_ptr(&self) -> *mut c_void {
70 self.window
71 }
72
73 /// Enter the AppKit event loop. Never returns.
74 pub fn run(&self) -> ! {
75 unsafe {
76 let app = objc::shared_app();
77 objc::send(app, objc::sel(c"run"));
78 }
79 std::process::exit(0)
80 }
81}
82
83#[cfg(not(target_os = "macos"))]
84impl Window {
85 /// Create and show a window. Does not start the run loop.
86 ///
87 /// `title` is unused: a `UIWindow` has no title bar to put it in. It stays
88 /// in the signature so a caller can be written once for both platforms.
89 pub fn new(view: &ViewHandle, title: &str, width: f64, height: f64) -> Self {
90 let _ = title;
91 unsafe {
92 let hosting = HostingView::new(view);
93 let window = objc::send(objc::class(c"OBJC_CLASS_$_UIWindow"), objc::sel(c"alloc"));
94 type InitFrame =
95 unsafe extern "C" fn(*mut c_void, *const c_void, NSRect) -> *mut c_void;
96 let init: InitFrame = core::mem::transmute(objc::msg_send_addr());
97 let window = init(
98 window,
99 objc::sel(c"initWithFrame:"),
100 NSRect::new(0.0, 0.0, width, height),
101 );
102 objc::send_id(
103 window,
104 objc::sel(c"setRootViewController:"),
105 hosting.controller(),
106 );
107 objc::send(window, objc::sel(c"makeKeyAndVisible"));
108 Self { window, hosting }
109 }
110 }
111
112 /// Replace the displayed view. SwiftUI diffs it against the previous tree.
113 pub fn set_root(&self, view: &ViewHandle) {
114 self.hosting.set_root(view);
115 }
116
117 /// The hosting view, for rendering or further configuration.
118 pub fn hosting(&self) -> &HostingView {
119 &self.hosting
120 }
121
122 pub fn as_ptr(&self) -> *mut c_void {
123 self.window
124 }
125
126 /// Enter the UIKit event loop. Never returns.
127 ///
128 /// `UIApplicationMain` will not start without a delegate *class* to name,
129 /// and it has to be a real Objective-C class — so one is built at runtime
130 /// and its `application:didFinishLaunchingWithOptions:` puts this window on
131 /// screen. See [`run_app`].
132 ///
133 /// Already inside a running application? Then there is no loop to enter:
134 /// the window went on screen when it was created, and this just turns the
135 /// run loop.
136 pub fn run(&self) -> ! {
137 unsafe {
138 #[cfg(feature = "uikit-entry")]
139 if objc::shared_app().is_null() {
140 run_app(self.hosting.controller());
141 }
142 // An application is already running; keep this thread alive.
143 unsafe extern "C" {
144 fn CFRunLoopRun();
145 }
146 CFRunLoopRun();
147 }
148 std::process::exit(0)
149 }
150}
151
152/// The root view controller the synthesised delegate installs on launch.
153///
154/// Gated on `uikit-entry` like the two functions that touch it: without the
155/// feature there is no UIKit entry point to hand a root to, and the plain
156/// `not(macos)` gate left it dead in the default iOS build.
157#[cfg(all(not(target_os = "macos"), feature = "uikit-entry"))]
158static PENDING_ROOT: std::sync::atomic::AtomicPtr<c_void> =
159 std::sync::atomic::AtomicPtr::new(core::ptr::null_mut());
160
161/// Start a UIKit application whose root view controller is `controller`.
162///
163/// UIKit's entry point takes the *name* of a delegate class and instantiates it
164/// itself, so there has to be a class to name. Rather than ship an Objective-C
165/// source file for it, the class is defined through the runtime — the same move
166/// as synthesising a Swift type in [`crate::state`], one runtime down.
167///
168/// Never returns.
169///
170/// # Safety
171/// `controller` must be a live `UIViewController`, and this must be called from
172/// the main thread of a process that is not already running an application.
173#[cfg(all(not(target_os = "macos"), feature = "uikit-entry"))]
174pub unsafe fn run_app(controller: *mut c_void) -> ! {
175 use std::sync::atomic::Ordering;
176 unsafe extern "C" {
177 fn UIApplicationMain(
178 argc: i32,
179 argv: *const *const core::ffi::c_char,
180 principal: *mut c_void,
181 delegate: *mut c_void,
182 ) -> i32;
183 }
184
185 /// `-[RustAppDelegate application:didFinishLaunchingWithOptions:]`
186 unsafe extern "C" fn did_finish_launching(
187 this: *mut c_void,
188 _cmd: *const c_void,
189 _app: *mut c_void,
190 _options: *mut c_void,
191 ) -> bool {
192 unsafe {
193 let screen = objc::send(
194 objc::class(c"OBJC_CLASS_$_UIScreen"),
195 objc::sel(c"mainScreen"),
196 );
197 let bounds = objc::send_ret_rect(screen, objc::sel(c"bounds"));
198
199 let window = objc::send(objc::class(c"OBJC_CLASS_$_UIWindow"), objc::sel(c"alloc"));
200 type InitFrame =
201 unsafe extern "C" fn(*mut c_void, *const c_void, objc::NSRect) -> *mut c_void;
202 let init: InitFrame = core::mem::transmute(objc::msg_send_addr());
203 let window = init(window, objc::sel(c"initWithFrame:"), bounds);
204
205 let root = PENDING_ROOT.load(Ordering::Acquire);
206 if !root.is_null() {
207 objc::send_id(window, objc::sel(c"setRootViewController:"), root);
208 }
209 objc::send(window, objc::sel(c"makeKeyAndVisible"));
210 // Nothing here runs under ARC, so the window is still at the +1 it
211 // came out of `alloc` with. Parking it in a static keeps that
212 // reference owned by something, rather than merely leaked.
213 let _ = this;
214 WINDOW.store(window, Ordering::Release);
215 true
216 }
217 }
218
219 static WINDOW: std::sync::atomic::AtomicPtr<c_void> =
220 std::sync::atomic::AtomicPtr::new(core::ptr::null_mut());
221
222 unsafe {
223 PENDING_ROOT.store(controller, Ordering::Release);
224
225 const DELEGATE: &core::ffi::CStr = c"RustAppDelegate";
226 if objc::class_named(DELEGATE).is_null() {
227 objc::ClassBuilder::new(c"UIResponder", DELEGATE)
228 .expect("RustAppDelegate")
229 // `B@:@@` — returns BOOL, takes the application and a
230 // launch-options dictionary after the implicit receiver and
231 // selector.
232 .method(
233 c"application:didFinishLaunchingWithOptions:",
234 did_finish_launching as *const c_void,
235 c"B@:@@",
236 )
237 .conforms(c"UIApplicationDelegate")
238 .register();
239 }
240
241 let name = objc::nsstring("RustAppDelegate");
242 let args: Vec<*const core::ffi::c_char> = std::env::args()
243 .map(|a| {
244 std::ffi::CString::new(a).unwrap_or_default().into_raw() as *const core::ffi::c_char
245 })
246 .collect();
247 UIApplicationMain(
248 args.len() as i32,
249 args.as_ptr(),
250 core::ptr::null_mut(),
251 name,
252 );
253 }
254 std::process::exit(0)
255}
256
257/// Show a view in a native window and run the event loop. Never returns.
258#[cfg(target_os = "macos")]
259pub fn show_window(view: &ViewHandle, title: &str, width: f64, height: f64) {
260 Window::new(view, title, width, height).run()
261}
262
263/// Show a view in a native window and run the event loop. Never returns.
264///
265/// # One window, made by the delegate
266///
267/// This deliberately does *not* go through [`Window::new`]. That would build a
268/// `UIWindow` and send it `makeKeyAndVisible` here — before `UIApplicationMain`
269/// has run, so before UIKit is initialised — and the synthesised delegate then
270/// builds a second window and gives it the same root view controller on launch.
271///
272/// A controller can only be the root of one window: UIKit moves its view to the
273/// second, which is what gets drawn. The first stays visible and key, and since
274/// hit-testing starts at the key window, every touch lands in a window with
275/// nothing in it. The application renders correctly and ignores input — no
276/// button, no toggle, no slider responds.
277///
278/// So only the controller is built here. The delegate makes the one window.
279#[cfg(all(not(target_os = "macos"), feature = "uikit-entry"))]
280pub fn show_window(view: &ViewHandle, title: &str, width: f64, height: f64) {
281 use std::sync::atomic::Ordering;
282 let _ = (title, width, height);
283 let hosting = HostingView::new(view);
284 let controller = hosting.controller();
285 // Kept, not forgotten: [`set_root`] needs it to push a new tree later.
286 let hosting: &'static HostingView = Box::leak(Box::new(hosting));
287 HOSTING.store(hosting as *const HostingView as *mut c_void, Ordering::Release);
288 unsafe { run_app(controller) }
289}
290
291/// The hosting view of the running application, for [`set_root`].
292#[cfg(not(target_os = "macos"))]
293static HOSTING: std::sync::atomic::AtomicPtr<c_void> =
294 std::sync::atomic::AtomicPtr::new(core::ptr::null_mut());
295
296/// Replace the running application's view tree.
297///
298/// SwiftUI cannot observe a Rust value: a `Binding` reads its getter only when
299/// something else causes the body to be evaluated, so a state change made in
300/// Rust is invisible until the tree is handed over again. This is that handover
301/// — rebuild the view from the new state and give it to the hosting controller,
302/// which diffs it as SwiftUI would diff any other update.
303///
304/// Must run on the main thread; see [`on_main`].
305#[cfg(not(target_os = "macos"))]
306pub fn set_root(view: &ViewHandle) {
307 use std::sync::atomic::Ordering;
308 let p = HOSTING.load(Ordering::Acquire);
309 if p.is_null() {
310 return;
311 }
312 unsafe { (*(p as *const HostingView)).set_root(view) }
313}
314
315/// Run `f` on the main thread.
316///
317/// UIKit and SwiftUI are main-thread only, and Rust state can be written from
318/// anywhere. `dispatch_async_f` is the function-pointer form of `dispatch_async`
319/// — no block, so nothing here has to synthesise one.
320#[cfg(not(target_os = "macos"))]
321pub fn on_main(f: unsafe extern "C" fn(*mut c_void), ctx: *mut c_void) {
322 unsafe extern "C" {
323 fn dispatch_async_f(queue: *mut c_void, ctx: *mut c_void, work: unsafe extern "C" fn(*mut c_void));
324 /// `dispatch_get_main_queue()` is a macro over the address of this.
325 static _dispatch_main_q: c_void;
326 }
327 unsafe {
328 dispatch_async_f(
329 &raw const _dispatch_main_q as *mut c_void,
330 ctx,
331 f,
332 );
333 }
334}
335