winit_appkit/
lib.rs

1//! # macOS / AppKit
2//!
3//! Winit has [the same macOS version requirements as `rustc`][rustc-macos-version], and is tested
4//! once in a while on as low as macOS 10.14.
5//!
6//! [rustc-macos-version]: https://doc.rust-lang.org/rustc/platform-support/apple-darwin.html#os-version
7//!
8//! ## Custom `NSApplicationDelegate`
9//!
10//! Winit usually handles everything related to the lifecycle events of the application. Sometimes,
11//! though, you might want to do more niche stuff, such as [handle when the user re-activates the
12//! application][reopen]. Such functionality is not exposed directly in Winit, since it would
13//! increase the API surface by quite a lot.
14//!
15//! [reopen]: https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428638-applicationshouldhandlereopen?language=objc
16//!
17//! Instead, Winit guarantees that it will not register an application delegate, so the solution is
18//! to register your own application delegate, as outlined in the following example (see
19//! `objc2-app-kit` for more detailed information).
20//! ```
21//! use objc2::rc::Retained;
22//! use objc2::runtime::ProtocolObject;
23//! use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send};
24//! use objc2_app_kit::{NSApplication, NSApplicationDelegate};
25//! use objc2_foundation::{NSArray, NSObject, NSObjectProtocol, NSURL};
26//! use winit::event_loop::EventLoop;
27//!
28//! define_class!(
29//!     #[unsafe(super(NSObject))]
30//!     #[thread_kind = MainThreadOnly]
31//!     #[name = "AppDelegate"]
32//!     struct AppDelegate;
33//!
34//!     unsafe impl NSObjectProtocol for AppDelegate {}
35//!
36//!     unsafe impl NSApplicationDelegate for AppDelegate {
37//!         #[unsafe(method(application:openURLs:))]
38//!         fn application_openURLs(&self, application: &NSApplication, urls: &NSArray<NSURL>) {
39//!             // Note: To specifically get `application:openURLs:` to work, you _might_
40//!             // have to bundle your application. This is not done in this example.
41//!             println!("open urls: {application:?}, {urls:?}");
42//!         }
43//!     }
44//! );
45//!
46//! impl AppDelegate {
47//!     fn new(mtm: MainThreadMarker) -> Retained<Self> {
48//!         unsafe { msg_send![super(Self::alloc(mtm).set_ivars(())), init] }
49//!     }
50//! }
51//!
52//! fn main() -> Result<(), Box<dyn std::error::Error>> {
53//!     let event_loop = EventLoop::new()?;
54//!
55//!     let mtm = MainThreadMarker::new().unwrap();
56//!     let delegate = AppDelegate::new(mtm);
57//!     // Important: Call `sharedApplication` after `EventLoop::new`,
58//!     // doing it before is not yet supported.
59//!     let app = NSApplication::sharedApplication(mtm);
60//!     app.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
61//!
62//!     // event_loop.run_app(&mut my_app);
63//!     Ok(())
64//! }
65//! ```
66#![cfg(target_vendor = "apple")] // TODO: Remove once `objc2` allows compiling on all platforms
67
68#[macro_use]
69mod util;
70
71mod app;
72mod app_state;
73mod cursor;
74mod event;
75mod event_loop;
76mod ffi;
77mod menu;
78mod monitor;
79mod notification_center;
80mod observer;
81mod view;
82mod window;
83mod window_delegate;
84
85use std::os::raw::c_void;
86
87#[cfg(feature = "serde")]
88use serde::{Deserialize, Serialize};
89#[doc(inline)]
90pub use winit_core::application::macos::ApplicationHandlerExtMacOS;
91use winit_core::event_loop::ActiveEventLoop;
92use winit_core::monitor::MonitorHandle;
93use winit_core::window::{PlatformWindowAttributes, Window};
94
95pub use self::event::{physicalkey_to_scancode, scancode_to_physicalkey};
96use self::event_loop::ActiveEventLoop as AppKitActiveEventLoop;
97pub use self::event_loop::{EventLoop, PlatformSpecificEventLoopAttributes};
98use self::monitor::MonitorHandle as AppKitMonitorHandle;
99use self::window::Window as AppKitWindow;
100
101/// Additional methods on [`Window`] that are specific to MacOS.
102pub trait WindowExtMacOS {
103    /// Returns whether or not the window is in simple fullscreen mode.
104    fn simple_fullscreen(&self) -> bool;
105
106    /// Toggles a fullscreen mode that doesn't require a new macOS space.
107    /// Returns a boolean indicating whether the transition was successful (this
108    /// won't work if the window was already in the native fullscreen).
109    ///
110    /// This is how fullscreen used to work on macOS in versions before Lion.
111    /// And allows the user to have a fullscreen window without using another
112    /// space or taking control over the entire monitor.
113    ///
114    /// Make sure you only draw your important content inside the safe area so that it does not
115    /// overlap with the notch on newer devices, see [`Window::safe_area`] for details.
116    fn set_simple_fullscreen(&self, fullscreen: bool) -> bool;
117
118    /// Returns whether or not the window has shadow.
119    fn has_shadow(&self) -> bool;
120
121    /// Sets whether or not the window has shadow.
122    fn set_has_shadow(&self, has_shadow: bool);
123
124    /// Group windows together by using the same tabbing identifier.
125    ///
126    /// <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
127    fn set_tabbing_identifier(&self, identifier: &str);
128
129    /// Returns the window's tabbing identifier.
130    fn tabbing_identifier(&self) -> String;
131
132    /// Select next tab.
133    fn select_next_tab(&self);
134
135    /// Select previous tab.
136    fn select_previous_tab(&self);
137
138    /// Select the tab with the given index.
139    ///
140    /// Will no-op when the index is out of bounds.
141    fn select_tab_at_index(&self, index: usize);
142
143    /// Get the number of tabs in the window tab group.
144    fn num_tabs(&self) -> usize;
145
146    /// Get the window's edit state.
147    ///
148    /// # Examples
149    ///
150    /// ```ignore
151    /// WindowEvent::CloseRequested => {
152    ///     if window.is_document_edited() {
153    ///         // Show the user a save pop-up or similar
154    ///     } else {
155    ///         // Close the window
156    ///         drop(window);
157    ///     }
158    /// }
159    /// ```
160    fn is_document_edited(&self) -> bool;
161
162    /// Put the window in a state which indicates a file save is required.
163    fn set_document_edited(&self, edited: bool);
164
165    /// Set option as alt behavior as described in [`OptionAsAlt`].
166    ///
167    /// This will ignore diacritical marks and accent characters from
168    /// being processed as received characters. Instead, the input
169    /// device's raw character will be placed in event queues with the
170    /// Alt modifier set.
171    fn set_option_as_alt(&self, option_as_alt: OptionAsAlt);
172
173    /// Getter for the [`WindowExtMacOS::set_option_as_alt`].
174    fn option_as_alt(&self) -> OptionAsAlt;
175
176    /// Disable the Menu Bar and Dock in Simple or Borderless Fullscreen mode. Useful for games.
177    /// The effect is applied when [`WindowExtMacOS::set_simple_fullscreen`] or
178    /// [`Window::set_fullscreen`] is called.
179    fn set_borderless_game(&self, borderless_game: bool);
180
181    /// Getter for the [`WindowExtMacOS::set_borderless_game`].
182    fn is_borderless_game(&self) -> bool;
183
184    /// Makes the titlebar bigger, effectively adding more space around the
185    /// window controls if the titlebar is invisible.
186    fn set_unified_titlebar(&self, unified_titlebar: bool);
187
188    /// Getter for the [`WindowExtMacOS::set_unified_titlebar`].
189    fn unified_titlebar(&self) -> bool;
190}
191
192impl WindowExtMacOS for dyn Window + '_ {
193    #[inline]
194    fn simple_fullscreen(&self) -> bool {
195        let window = self.cast_ref::<AppKitWindow>().unwrap();
196        window.maybe_wait_on_main(|w| w.simple_fullscreen())
197    }
198
199    #[inline]
200    fn set_simple_fullscreen(&self, fullscreen: bool) -> bool {
201        let window = self.cast_ref::<AppKitWindow>().unwrap();
202        window.maybe_wait_on_main(move |w| w.set_simple_fullscreen(fullscreen))
203    }
204
205    #[inline]
206    fn has_shadow(&self) -> bool {
207        let window = self.cast_ref::<AppKitWindow>().unwrap();
208        window.maybe_wait_on_main(|w| w.has_shadow())
209    }
210
211    #[inline]
212    fn set_has_shadow(&self, has_shadow: bool) {
213        let window = self.cast_ref::<AppKitWindow>().unwrap();
214        window.maybe_wait_on_main(move |w| w.set_has_shadow(has_shadow));
215    }
216
217    #[inline]
218    fn set_tabbing_identifier(&self, identifier: &str) {
219        let window = self.cast_ref::<AppKitWindow>().unwrap();
220        window.maybe_wait_on_main(|w| w.set_tabbing_identifier(identifier))
221    }
222
223    #[inline]
224    fn tabbing_identifier(&self) -> String {
225        let window = self.cast_ref::<AppKitWindow>().unwrap();
226        window.maybe_wait_on_main(|w| w.tabbing_identifier())
227    }
228
229    #[inline]
230    fn select_next_tab(&self) {
231        let window = self.cast_ref::<AppKitWindow>().unwrap();
232        window.maybe_wait_on_main(|w| w.select_next_tab());
233    }
234
235    #[inline]
236    fn select_previous_tab(&self) {
237        let window = self.cast_ref::<AppKitWindow>().unwrap();
238        window.maybe_wait_on_main(|w| w.select_previous_tab());
239    }
240
241    #[inline]
242    fn select_tab_at_index(&self, index: usize) {
243        let window = self.cast_ref::<AppKitWindow>().unwrap();
244        window.maybe_wait_on_main(move |w| w.select_tab_at_index(index));
245    }
246
247    #[inline]
248    fn num_tabs(&self) -> usize {
249        let window = self.cast_ref::<AppKitWindow>().unwrap();
250        window.maybe_wait_on_main(|w| w.num_tabs())
251    }
252
253    #[inline]
254    fn is_document_edited(&self) -> bool {
255        let window = self.cast_ref::<AppKitWindow>().unwrap();
256        window.maybe_wait_on_main(|w| w.is_document_edited())
257    }
258
259    #[inline]
260    fn set_document_edited(&self, edited: bool) {
261        let window = self.cast_ref::<AppKitWindow>().unwrap();
262        window.maybe_wait_on_main(move |w| w.set_document_edited(edited));
263    }
264
265    #[inline]
266    fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) {
267        let window = self.cast_ref::<AppKitWindow>().unwrap();
268        window.maybe_wait_on_main(move |w| w.set_option_as_alt(option_as_alt));
269    }
270
271    #[inline]
272    fn option_as_alt(&self) -> OptionAsAlt {
273        let window = self.cast_ref::<AppKitWindow>().unwrap();
274        window.maybe_wait_on_main(|w| w.option_as_alt())
275    }
276
277    #[inline]
278    fn set_borderless_game(&self, borderless_game: bool) {
279        let window = self.cast_ref::<AppKitWindow>().unwrap();
280        window.maybe_wait_on_main(|w| w.set_borderless_game(borderless_game))
281    }
282
283    #[inline]
284    fn is_borderless_game(&self) -> bool {
285        let window = self.cast_ref::<AppKitWindow>().unwrap();
286        window.maybe_wait_on_main(|w| w.is_borderless_game())
287    }
288
289    #[inline]
290    fn set_unified_titlebar(&self, unified_titlebar: bool) {
291        let window = self.cast_ref::<AppKitWindow>().unwrap();
292        window.maybe_wait_on_main(|w| w.set_unified_titlebar(unified_titlebar))
293    }
294
295    #[inline]
296    fn unified_titlebar(&self) -> bool {
297        let window = self.cast_ref::<AppKitWindow>().unwrap();
298        window.maybe_wait_on_main(|w| w.unified_titlebar())
299    }
300}
301
302/// Corresponds to `NSApplicationActivationPolicy`.
303#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
304#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
305pub enum ActivationPolicy {
306    /// Corresponds to `NSApplicationActivationPolicyRegular`.
307    #[default]
308    Regular,
309
310    /// Corresponds to `NSApplicationActivationPolicyAccessory`.
311    Accessory,
312
313    /// Corresponds to `NSApplicationActivationPolicyProhibited`.
314    Prohibited,
315}
316
317/// Window attributes that are specific to MacOS.
318///
319/// **Note:** Properties dealing with the titlebar will be overwritten by the
320/// [`WindowAttributes::with_decorations`] method:
321/// - `with_titlebar_transparent`
322/// - `with_title_hidden`
323/// - `with_titlebar_hidden`
324/// - `with_titlebar_buttons_hidden`
325/// - `with_fullsize_content_view`
326///
327/// [`WindowAttributes::with_decorations`]: crate::window::WindowAttributes::with_decorations
328#[derive(Clone, Debug, PartialEq)]
329pub struct WindowAttributesMacOS {
330    pub(crate) movable_by_window_background: bool,
331    pub(crate) titlebar_transparent: bool,
332    pub(crate) title_hidden: bool,
333    pub(crate) titlebar_hidden: bool,
334    pub(crate) titlebar_buttons_hidden: bool,
335    pub(crate) fullsize_content_view: bool,
336    pub(crate) disallow_hidpi: bool,
337    pub(crate) has_shadow: bool,
338    pub(crate) accepts_first_mouse: bool,
339    pub(crate) tabbing_identifier: Option<String>,
340    pub(crate) option_as_alt: OptionAsAlt,
341    pub(crate) borderless_game: bool,
342    pub(crate) unified_titlebar: bool,
343    pub(crate) panel: bool,
344}
345
346impl WindowAttributesMacOS {
347    /// Enables click-and-drag behavior for the entire window, not just the titlebar.
348    #[inline]
349    pub fn with_movable_by_window_background(mut self, movable_by_window_background: bool) -> Self {
350        self.movable_by_window_background = movable_by_window_background;
351        self
352    }
353
354    /// Makes the titlebar transparent and allows the content to appear behind it.
355    #[inline]
356    pub fn with_titlebar_transparent(mut self, titlebar_transparent: bool) -> Self {
357        self.titlebar_transparent = titlebar_transparent;
358        self
359    }
360
361    /// Hides the window titlebar.
362    #[inline]
363    pub fn with_titlebar_hidden(mut self, titlebar_hidden: bool) -> Self {
364        self.titlebar_hidden = titlebar_hidden;
365        self
366    }
367
368    /// Hides the window titlebar buttons.
369    #[inline]
370    pub fn with_titlebar_buttons_hidden(mut self, titlebar_buttons_hidden: bool) -> Self {
371        self.titlebar_buttons_hidden = titlebar_buttons_hidden;
372        self
373    }
374
375    /// Hides the window title.
376    #[inline]
377    pub fn with_title_hidden(mut self, title_hidden: bool) -> Self {
378        self.title_hidden = title_hidden;
379        self
380    }
381
382    /// Makes the window content appear behind the titlebar.
383    #[inline]
384    pub fn with_fullsize_content_view(mut self, fullsize_content_view: bool) -> Self {
385        self.fullsize_content_view = fullsize_content_view;
386        self
387    }
388
389    #[inline]
390    pub fn with_disallow_hidpi(mut self, disallow_hidpi: bool) -> Self {
391        self.disallow_hidpi = disallow_hidpi;
392        self
393    }
394
395    #[inline]
396    pub fn with_has_shadow(mut self, has_shadow: bool) -> Self {
397        self.has_shadow = has_shadow;
398        self
399    }
400
401    /// Window accepts click-through mouse events.
402    #[inline]
403    pub fn with_accepts_first_mouse(mut self, accepts_first_mouse: bool) -> Self {
404        self.accepts_first_mouse = accepts_first_mouse;
405        self
406    }
407
408    /// Defines the window tabbing identifier.
409    ///
410    /// <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
411    #[inline]
412    pub fn with_tabbing_identifier(mut self, tabbing_identifier: &str) -> Self {
413        self.tabbing_identifier.replace(tabbing_identifier.to_string());
414        self
415    }
416
417    /// Set how the <kbd>Option</kbd> keys are interpreted.
418    ///
419    /// See [`WindowExtMacOS::set_option_as_alt`] for details on what this means if set.
420    #[inline]
421    pub fn with_option_as_alt(mut self, option_as_alt: OptionAsAlt) -> Self {
422        self.option_as_alt = option_as_alt;
423        self
424    }
425
426    /// See [`WindowExtMacOS::set_borderless_game`] for details on what this means if set.
427    #[inline]
428    pub fn with_borderless_game(mut self, borderless_game: bool) -> Self {
429        self.borderless_game = borderless_game;
430        self
431    }
432
433    /// See [`WindowExtMacOS::set_unified_titlebar`] for details on what this means if set.
434    #[inline]
435    pub fn with_unified_titlebar(mut self, unified_titlebar: bool) -> Self {
436        self.unified_titlebar = unified_titlebar;
437        self
438    }
439
440    /// Use [`NSPanel`] window with [`NonactivatingPanel`] window style mask instead of
441    /// [`NSWindow`].
442    ///
443    /// [`NSWindow`]: https://developer.apple.com/documentation/appkit/NSWindow?language=objc
444    /// [`NSPanel`]: https://developer.apple.com/documentation/appkit/NSPanel?language=objc
445    /// [`NonactivatingPanel`]: https://developer.apple.com/documentation/appkit/nswindow/stylemask-swift.struct/nonactivatingpanel?language=objc
446    #[inline]
447    pub fn with_panel(mut self, panel: bool) -> Self {
448        self.panel = panel;
449        self
450    }
451}
452
453impl Default for WindowAttributesMacOS {
454    #[inline]
455    fn default() -> Self {
456        Self {
457            movable_by_window_background: false,
458            titlebar_transparent: false,
459            title_hidden: false,
460            titlebar_hidden: false,
461            titlebar_buttons_hidden: false,
462            fullsize_content_view: false,
463            disallow_hidpi: false,
464            has_shadow: true,
465            accepts_first_mouse: true,
466            tabbing_identifier: None,
467            option_as_alt: Default::default(),
468            borderless_game: false,
469            unified_titlebar: false,
470            panel: false,
471        }
472    }
473}
474
475impl PlatformWindowAttributes for WindowAttributesMacOS {
476    fn box_clone(&self) -> Box<dyn PlatformWindowAttributes> {
477        Box::from(self.clone())
478    }
479}
480
481pub trait EventLoopBuilderExtMacOS {
482    /// Sets the activation policy for the application. If used, this will override
483    /// any relevant settings provided in the package manifest.
484    /// For instance, `with_activation_policy(ActivationPolicy::Regular)` will prevent
485    /// the application from running as an "agent", even if LSUIElement is set to true.
486    ///
487    /// If unused, the Winit will honor the package manifest.
488    ///
489    /// # Example
490    ///
491    /// Set the activation policy to "accessory".
492    ///
493    /// ```
494    /// use winit::event_loop::EventLoop;
495    /// #[cfg(target_os = "macos")]
496    /// use winit::platform::macos::{ActivationPolicy, EventLoopBuilderExtMacOS};
497    ///
498    /// let mut builder = EventLoop::builder();
499    /// #[cfg(target_os = "macos")]
500    /// builder.with_activation_policy(ActivationPolicy::Accessory);
501    /// # if false { // We can't test this part
502    /// let event_loop = builder.build();
503    /// # }
504    /// ```
505    fn with_activation_policy(&mut self, activation_policy: ActivationPolicy) -> &mut Self;
506
507    /// Used to control whether a default menubar menu is created.
508    ///
509    /// Menu creation is enabled by default.
510    ///
511    /// # Example
512    ///
513    /// Disable creating a default menubar.
514    ///
515    /// ```
516    /// use winit::event_loop::EventLoop;
517    /// #[cfg(target_os = "macos")]
518    /// use winit::platform::macos::EventLoopBuilderExtMacOS;
519    ///
520    /// let mut builder = EventLoop::builder();
521    /// #[cfg(target_os = "macos")]
522    /// builder.with_default_menu(false);
523    /// # if false { // We can't test this part
524    /// let event_loop = builder.build();
525    /// # }
526    /// ```
527    fn with_default_menu(&mut self, enable: bool) -> &mut Self;
528
529    /// Used to prevent the application from automatically activating when launched if
530    /// another application is already active.
531    ///
532    /// The default behavior is to ignore other applications and activate when launched.
533    fn with_activate_ignoring_other_apps(&mut self, ignore: bool) -> &mut Self;
534}
535
536/// Additional methods on [`MonitorHandle`] that are specific to MacOS.
537pub trait MonitorHandleExtMacOS {
538    /// Returns a pointer to the NSScreen representing this monitor.
539    fn ns_screen(&self) -> Option<*mut c_void>;
540}
541
542impl MonitorHandleExtMacOS for MonitorHandle {
543    fn ns_screen(&self) -> Option<*mut c_void> {
544        let monitor = self.cast_ref::<AppKitMonitorHandle>().unwrap();
545        // SAFETY: We only use the marker to get a pointer
546        let mtm = unsafe { objc2::MainThreadMarker::new_unchecked() };
547        monitor.ns_screen(mtm).map(|s| objc2::rc::Retained::as_ptr(&s) as _)
548    }
549}
550
551/// Additional methods on [`ActiveEventLoop`] that are specific to macOS.
552pub trait ActiveEventLoopExtMacOS {
553    /// Hide the entire application. In most applications this is typically triggered with
554    /// Command-H.
555    fn hide_application(&self);
556    /// Hide the other applications. In most applications this is typically triggered with
557    /// Command+Option-H.
558    fn hide_other_applications(&self);
559    /// Set whether the system can automatically organize windows into tabs.
560    ///
561    /// <https://developer.apple.com/documentation/appkit/nswindow/1646657-allowsautomaticwindowtabbing>
562    fn set_allows_automatic_window_tabbing(&self, enabled: bool);
563    /// Returns whether the system can automatically organize windows into tabs.
564    fn allows_automatic_window_tabbing(&self) -> bool;
565}
566
567impl ActiveEventLoopExtMacOS for dyn ActiveEventLoop + '_ {
568    fn hide_application(&self) {
569        let event_loop =
570            self.cast_ref::<AppKitActiveEventLoop>().expect("non macOS event loop on macOS");
571        event_loop.hide_application()
572    }
573
574    fn hide_other_applications(&self) {
575        let event_loop =
576            self.cast_ref::<AppKitActiveEventLoop>().expect("non macOS event loop on macOS");
577        event_loop.hide_other_applications()
578    }
579
580    fn set_allows_automatic_window_tabbing(&self, enabled: bool) {
581        let event_loop =
582            self.cast_ref::<AppKitActiveEventLoop>().expect("non macOS event loop on macOS");
583        event_loop.set_allows_automatic_window_tabbing(enabled);
584    }
585
586    fn allows_automatic_window_tabbing(&self) -> bool {
587        let event_loop =
588            self.cast_ref::<AppKitActiveEventLoop>().expect("non macOS event loop on macOS");
589        event_loop.allows_automatic_window_tabbing()
590    }
591}
592
593/// Option as alt behavior.
594///
595/// The default is `None`.
596#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
597#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
598pub enum OptionAsAlt {
599    /// The left `Option` key is treated as `Alt`.
600    OnlyLeft,
601
602    /// The right `Option` key is treated as `Alt`.
603    OnlyRight,
604
605    /// Both `Option` keys are treated as `Alt`.
606    Both,
607
608    /// No special handling is applied for `Option` key.
609    #[default]
610    None,
611}