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}