Skip to main content

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