rust_widgets 2.0.0

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 60+ widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
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
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! `impl Platform for MacOSPlatform` — the main trait implementation.

#![allow(deprecated)] // Cocoa 0.24 fallback; remove when objc2 backend fully replaces cocoa

use crate::core::{ObjectId, PlatformFamily};
use crate::platform::accessibility::AccessibilityBridge;
use crate::platform::clipboard::RichClipboardBackend;
use crate::platform::ime::ImeBridge;
use crate::platform::macos::types::*;
use crate::platform::Platform;
use cocoa::appkit::{
    NSApp, NSApplication, NSApplicationActivationOptions, NSApplicationActivationPolicyRegular,
    NSBackingStoreBuffered, NSRunningApplication, NSView, NSWindow,
};
use cocoa::base::{id, nil, BOOL, NO};
use cocoa::foundation::{NSAutoreleasePool, NSPoint, NSString};
use objc::{class, msg_send, sel, sel_impl};
use std::ffi::CStr;
use std::os::raw::c_char;

impl Platform for MacOSPlatform {
    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
    fn backend_name(&self) -> &'static str {
        "cocoa"
    }

    /// A library-painted widget gets an `NSView` subclass whose `drawRect:` blits a
    /// frame out of `widget::runtime`. See `macos/canvas.rs`.
    ///
    /// Gated on the same profile conditions as `canvas.rs`: `widget::runtime` is
    /// absent from `mini`/`embedded`, so the fallback defaults below apply there
    /// and `supports_surfaces()` honestly reports `false`.
    #[cfg(widgets_unstripped)]
    fn mount_surface(&self, parent: ObjectId, id: ObjectId, rect: crate::core::Rect) -> bool {
        self.mount_surface_impl(parent, id, rect)
    }

    #[cfg(widgets_unstripped)]
    fn resize_surface(&self, id: ObjectId, rect: crate::core::Rect) -> bool {
        self.resize_surface_impl(id, rect)
    }

    #[cfg(widgets_unstripped)]
    fn unmount_surface(&self, id: ObjectId) -> bool {
        self.unmount_surface_impl(id)
    }

    /// `true` only when the surface actually exists for this profile.
    ///
    /// Reporting `true` in a build where `canvas.rs` is compiled out would be a
    /// lie: a host would mount a widget and get an empty window with no error.
    #[cfg(widgets_unstripped)]
    fn supports_surfaces(&self) -> bool {
        true
    }

    /// Mark the canvas view as needing display, which schedules `drawRect:`.
    #[cfg(widgets_unstripped)]
    fn invalidate_surface(&self, id: ObjectId) -> bool {
        self.invalidate_surface_impl(id)
    }
    fn family(&self) -> PlatformFamily {
        PlatformFamily::Desktop
    }

    /// Reads `MemTotal` from `/proc/meminfo` via [`os_probes`].
    fn total_memory_mb(&self) -> Option<u64> {
        crate::platform::os_probes::total_memory_mb()
    }

    /// Reports whether any battery in `/sys/class/power_supply` is discharging.
    fn is_on_battery(&self) -> bool {
        crate::platform::os_probes::is_on_battery()
    }

    /// Samples RSS over VmSize for this process from `/proc/self/status`.
    fn process_memory_utilization(&self) -> Option<f32> {
        crate::platform::os_probes::process_memory_utilization()
    }

    /// Estimates CPU load as thread count over twice the available cores.
    fn process_cpu_utilization(&self) -> Option<f32> {
        crate::platform::os_probes::process_cpu_utilization()
    }

    /// Submits the job to the unix print spooler via [`os_probes`].
    fn spawn_print_job(&self, job_file: &std::path::Path) -> Result<(), String> {
        crate::platform::os_probes::spawn_print_job(job_file)
    }

    /// macOS ships CUPS, so `lp`/`lpr` are present on every normal install.
    fn has_print_support(&self) -> bool {
        crate::platform::types::unix_print_clients_available()
    }

    /// Renders menu accelerators with AppKit symbols (`⌘⇧Z`).
    fn shortcut_style(&self) -> crate::shortcut::PlatformShortcutStyle {
        crate::shortcut::PlatformShortcutStyle::Mac
    }
    fn init(&self) {
        // AppKit's `NSApplication` singleton may only be created/activated on the
        // main thread. Off-main we skip the native bootstrap entirely and leave
        // the backend in state mode (mirrors the `macos_objc2` preview backend).
        if !super::types::is_main_thread() {
            log::debug!("[macos] init skipped: not on the AppKit main thread (state-only mode)");
            return;
        }
        // SAFETY: NSAutoreleasePool::new(nil) is safe per Apple's documentation
        // (nil argument is allowed). NSApplication sharedApplication and messaging
        // are called on the main thread, which is required by Cocoa. All Objective-C
        // message sends use valid selectors from the cocoa/objc crates.
        unsafe {
            let pool = NSAutoreleasePool::new(nil);
            let app = NSApplication::sharedApplication(nil);
            app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
            let _: () = msg_send![app, finishLaunching];
            let current_app = NSRunningApplication::currentApplication(nil);
            current_app.activateWithOptions_(
                NSApplicationActivationOptions::NSApplicationActivateIgnoringOtherApps,
            );
            pool.drain();
        }
    }
    fn run(&self) {
        // `-[NSApplication run]` must only be entered from the main thread; from
        // any other thread it would raise a foreign exception. Off-main callers
        // get a deterministic state-mode polling loop instead.
        if !super::types::is_main_thread() {
            log::debug!("[macos] run skipped: not on the AppKit main thread (state-only loop)");
            return;
        }
        // SAFETY: NSApp() returns the shared application instance initialized in init().
        // run() must be called on the main thread, which is guaranteed by the platform
        // contract (init is called before run on the same thread).
        unsafe {
            NSApp().run();
        }
    }
    fn quit(&self) {
        // Stopping the shared application is also main-thread-only.
        if !super::types::is_main_thread() {
            log::debug!("[macos] quit skipped: not on the AppKit main thread");
            return;
        }
        // SAFETY: NSApp().stop_(nil) is safe to call on the main thread after the
        // application has been initialized. The nil argument tells the app to stop
        // without a specific sender.
        unsafe {
            NSApp().stop_(nil);
        }
    }
    fn destroy_widget(&self, widget_id: ObjectId) -> bool {
        // Teardown is safe on any thread: nothing here messages AppKit. The
        // retained native objects (NSWindow/NSView instances) stay referenced by
        // the AppKit view hierarchy, which releases them when the window closes.
        // Off-main the backend never constructed a native object at all (it
        // registered a state-only handle), so the state record and side tables
        // are the only per-widget resources in either case.
        //
        // Each lock guard is released at the end of its own statement so that no
        // two of the backend's mutexes are ever held at the same time.
        self.handles.lock().expect("macos handle lock poisoned").remove(&widget_id);
        // Drop the per-widget accessibility registration that `register_handle` added.
        self.a11y_bridge.unregister_handle(widget_id);
        // The state record is the authority for whether the widget existed.
        self.state.destroy_widget(widget_id)
    }
    fn create_window(&self, title: &str, x: i32, y: i32, width: u32, height: u32) -> u64 {
        // Off-main (e.g. the C ABI called from a worker thread or unit tests),
        // never construct `NSWindow`: AppKit raises a foreign exception that
        // aborts the process. Register a state-only handle instead so every
        // caller still receives a valid, text/geometry-consistent widget id.
        if !super::types::is_main_thread() {
            let id =
                self.register_state_only_handle(HandleKind::Window, title, x, y, width, height);
            // `window_style()` is titled + closable + resizable + miniaturizable,
            // so a fresh macOS window starts resizable and decorated.
            self.state
                .init_window_state(id, crate::platform::state::WindowStateRecord::new_window());
            return id;
        }
        // SAFETY: Cocoa APIs require the main thread, guaranteed by the platform contract.
        // NSAutoreleasePool::new(nil) is safe with nil argument. All Objective-C messages
        // use valid selectors from the cocoa crate. Self::register_handle() stores the
        // raw pointer cast as usize without aliasing issues. Nil returns from alloc are
        // checked and logged before proceeding.
        unsafe {
            let pool = NSAutoreleasePool::new(nil);

            // Check for nil after NSWindow::alloc — Cocoa returns nil on allocation failure.
            let raw_window = NSWindow::alloc(nil);
            if raw_window == nil {
                log::error!("[macos] create_window: NSWindow::alloc returned nil (out of memory?)");
                pool.drain();
                return 0;
            }
            let window = raw_window.initWithContentRect_styleMask_backing_defer_(
                Self::make_rect(x, y, width, height),
                Self::window_style(),
                NSBackingStoreBuffered,
                NO,
            );

            // Check for nil after NSView::alloc
            let raw_content = NSView::alloc(nil);
            let content_view = if raw_content == nil {
                log::error!("[macos] create_window: NSView::alloc returned nil (out of memory?)");
                pool.drain();
                return 0;
            } else {
                NSView::initWithFrame_(raw_content, Self::make_rect(0, 0, width, height))
            };

            // Check for nil after NSString::alloc for the title
            let raw_title = NSString::alloc(nil);
            if raw_title == nil {
                log::error!("[macos] create_window: NSString::alloc returned nil (out of memory?)");
                pool.drain();
                return 0;
            }

            let _: () = msg_send![window, setContentView: content_view];
            window.cascadeTopLeftFromPoint_(NSPoint::new(20.0, 20.0));
            NSWindow::setTitle_(window, raw_title.init_str(title));
            window.makeKeyAndOrderFront_(nil);
            let _: () = msg_send![window, display];
            let id = self.register_handle(
                HandleKind::Window,
                title,
                x,
                y,
                width,
                height,
                window as usize,
            );
            self.state
                .init_window_state(id, crate::platform::state::WindowStateRecord::new_window());
            pool.drain();
            id
        }
    }
    fn menu_item_shortcut(&self, menu_item: ObjectId) -> Option<String> {
        let shortcuts = self.menu_item_shortcuts.lock().ok()?;
        shortcuts.get(&menu_item).cloned().filter(|text| !text.is_empty())
    }
    fn get_native_handle(&self, widget: ObjectId) -> Option<usize> {
        // A handle with a null pointer means the widget exists only as logical
        // state, so there is no native object to hand out.
        let handle = self.get_handle(widget)?;
        if handle.ptr == 0 {
            return None;
        }
        Some(handle.ptr)
    }
    fn poll_menu_triggered(&self) -> Option<u64> {
        let mut events = menu_events().lock().expect("menu event lock poisoned");
        if events.is_empty() {
            None
        } else {
            Some(events.remove(0))
        }
    }
    fn set_clipboard_text(&self, text: &str) -> bool {
        // `NSPasteboard` is a window-server singleton and may only be touched on
        // the AppKit main thread; off-main we go straight to state.
        if !super::types::is_main_thread() {
            return self.state.set_clipboard_text(text);
        }
        // Try real NSPasteboard integration first
        let result = std::panic::catch_unwind(|| unsafe {
            let pb: id = msg_send![class!(NSPasteboard), generalPasteboard];
            if pb == nil {
                return false;
            }
            let _: () = msg_send![pb, clearContents];
            let ns_str = NSString::alloc(nil).init_str(text);
            let type_str = NSString::alloc(nil).init_str("public.utf8-plain-text");
            let success: BOOL = msg_send![pb, setString:ns_str forType:type_str];
            success != NO
        });
        // Fall back to state on ObjC failure (including panics)
        result.unwrap_or_else(|_| self.state.set_clipboard_text(text))
    }
    fn get_clipboard_text(&self) -> String {
        // `NSPasteboard` is main-thread-only; state is the off-main source.
        if !super::types::is_main_thread() {
            return self.state.clipboard_text();
        }
        // Try real NSPasteboard integration first
        let result = std::panic::catch_unwind(|| unsafe {
            let pb: id = msg_send![class!(NSPasteboard), generalPasteboard];
            if pb == nil {
                return None;
            }
            let type_str = NSString::alloc(nil).init_str("public.utf8-plain-text");
            let text_obj: id = msg_send![pb, stringForType:type_str];
            if text_obj == nil {
                return None;
            }
            let c_str: *const c_char = msg_send![text_obj, UTF8String];
            if c_str.is_null() {
                return None;
            }
            Some(CStr::from_ptr(c_str).to_string_lossy().into_owned())
        });
        // Fall back to state on ObjC failure
        result.unwrap_or(None).unwrap_or_else(|| self.state.clipboard_text())
    }
    fn ime_bridge(&self) -> Option<&dyn ImeBridge> {
        Some(&self.ime_bridge)
    }

    /// Reads the record the backend keeps for every id it allocated.
    ///
    /// Both allocation paths — a real `NSWindow` on the main thread and a
    /// state-only handle off it — insert a [`crate::platform::state::WidgetRecord`],
    /// so text/geometry/enabled/visible are answered from that one record. Without
    /// these overrides the trait defaults would report empty text for a window the
    /// backend demonstrably created.
    fn get_widget_text(&self, widget_id: ObjectId) -> String {
        self.state.text(widget_id)
    }

    fn set_widget_text(&self, widget_id: ObjectId, text: &str) {
        self.state.set_text(widget_id, text);
        // Keep the live AppKit title in step when there is a native window. A
        // state-only handle has `ptr == 0`, so the message is skipped entirely.
        if let Some(handle) = self.get_handle(widget_id) {
            if handle.ptr != 0 && handle.kind == HandleKind::Window {
                // SAFETY: the handle was created in this process for an NSWindow
                // and `setTitle:` is declared by NSWindow.
                unsafe {
                    let window = Self::as_id(handle);
                    let title = NSString::alloc(nil).init_str(text);
                    NSWindow::setTitle_(window, title);
                }
            }
        }
    }

    fn set_widget_enabled(&self, widget_id: ObjectId, enabled: bool) {
        self.state.set_enabled(widget_id, enabled);
    }

    fn is_widget_enabled(&self, widget_id: ObjectId) -> bool {
        self.state.enabled(widget_id)
    }

    fn set_widget_visible(&self, widget_id: ObjectId, visible: bool) {
        self.state.set_visible(widget_id, visible);
    }

    fn is_widget_visible(&self, widget_id: ObjectId) -> bool {
        self.state.visible(widget_id)
    }

    fn set_widget_geometry(&self, widget_id: ObjectId, x: i32, y: i32, width: u32, height: u32) {
        self.state.set_geometry(widget_id, x, y, width, height);
    }

    /// Window state (maximised/minimised/fullscreen/resizable) round-trips
    /// through the record, so it works for the off-main state-only path too.
    fn set_window_state(
        &self,
        widget_id: ObjectId,
        flag: crate::platform::WindowStateFlag,
        on: bool,
    ) -> bool {
        self.state.set_window_state(widget_id, flag, on)
    }

    fn is_window_in_state(
        &self,
        widget_id: ObjectId,
        flag: crate::platform::WindowStateFlag,
    ) -> Option<bool> {
        self.state.window_state(widget_id, flag)
    }

    fn set_window_min_size(&self, widget_id: ObjectId, width: u32, height: u32) -> bool {
        self.state.set_window_min_size(widget_id, width, height)
    }

    fn window_min_size(&self, widget_id: ObjectId) -> Option<(u32, u32)> {
        self.state.window_min_size(widget_id)
    }

    fn set_widget_ime_enabled(&self, widget_id: ObjectId, enabled: bool) -> bool {
        self.state.set_ime_enabled(widget_id, enabled)
    }

    fn is_widget_ime_enabled(&self, widget_id: ObjectId) -> bool {
        self.state.ime_enabled(widget_id)
    }

    fn set_widget_accessibility_name(&self, widget_id: ObjectId, name: &str) -> bool {
        self.state.set_accessibility_name(widget_id, name)
    }

    fn get_widget_accessibility_name(&self, widget_id: ObjectId) -> String {
        self.state.accessibility_name(widget_id)
    }

    fn clipboard_backend(&self) -> Option<&dyn RichClipboardBackend> {
        Some(&self.clipboard)
    }

    fn accessibility_bridge(&self) -> Option<&dyn AccessibilityBridge> {
        Some(&self.a11y_bridge)
    }
}