diff --git a/CHANGELOG.md b/CHANGELOG.md index e96831c3..bf65199d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ And please only add new entries to the top of this list, right below the `# Unre # Unreleased +- On macOS, add tabbing APIs on `WindowExtMacOS`. - **Breaking:** Rename `Window::set_inner_size` to `Window::request_inner_size` and indicate if the size was applied immediately. - On X11, fix false positive flagging of key repeats when pressing different keys with no release between presses. - Implement `PartialOrd` and `Ord` for `KeyCode` and `NativeKeyCode`. diff --git a/examples/window_tabbing.rs b/examples/window_tabbing.rs new file mode 100644 index 00000000..b8aadf6a --- /dev/null +++ b/examples/window_tabbing.rs @@ -0,0 +1,105 @@ +#![allow(clippy::single_match)] + +#[cfg(target_os = "macos")] +use std::{collections::HashMap, num::NonZeroUsize}; + +#[cfg(target_os = "macos")] +use simple_logger::SimpleLogger; +#[cfg(target_os = "macos")] +use winit::{ + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::EventLoop, + keyboard::Key, + platform::macos::{WindowBuilderExtMacOS, WindowExtMacOS}, + window::{Window, WindowBuilder}, +}; + +#[cfg(target_os = "macos")] +#[path = "util/fill.rs"] +mod fill; + +#[cfg(target_os = "macos")] +fn main() { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + let mut windows = HashMap::new(); + let window = Window::new(&event_loop).unwrap(); + println!("Opened a new window: {:?}", window.id()); + windows.insert(window.id(), window); + + println!("Press N to open a new window."); + + event_loop.run(move |event, event_loop, control_flow| { + control_flow.set_wait(); + + match event { + Event::WindowEvent { event, window_id } => { + match event { + WindowEvent::CloseRequested => { + println!("Window {window_id:?} has received the signal to close"); + + // This drops the window, causing it to close. + windows.remove(&window_id); + + if windows.is_empty() { + control_flow.set_exit(); + } + } + WindowEvent::Resized(_) => { + if let Some(window) = windows.get(&window_id) { + window.request_redraw(); + } + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Pressed, + logical_key, + .. + }, + is_synthetic: false, + .. + } => match logical_key.as_ref() { + Key::Character("t") => { + let tabbing_id = windows.get(&window_id).unwrap().tabbing_identifier(); + let window = WindowBuilder::new() + .with_tabbing_identifier(&tabbing_id) + .build(event_loop) + .unwrap(); + println!("Added a new tab: {:?}", window.id()); + windows.insert(window.id(), window); + } + Key::Character("w") => { + let _ = windows.remove(&window_id); + } + Key::ArrowRight => { + windows.get(&window_id).unwrap().select_next_tab(); + } + Key::ArrowLeft => { + windows.get(&window_id).unwrap().select_previous_tab(); + } + Key::Character(ch) => { + if let Ok(index) = ch.parse::() { + windows.get(&window_id).unwrap().select_tab_at_index(index); + } + } + _ => (), + }, + _ => (), + } + } + Event::RedrawRequested(window_id) => { + if let Some(window) = windows.get(&window_id) { + fill::fill_window(window); + } + } + _ => (), + } + }) +} + +#[cfg(not(target_os = "macos"))] +fn main() { + println!("This example is only supported on MacOS"); +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 18d4745d..4691b4d5 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,4 +1,4 @@ -use std::os::raw::c_void; +use std::{num::NonZeroUsize, os::raw::c_void}; use objc2::rc::Id; @@ -38,6 +38,33 @@ pub trait WindowExtMacOS { /// Sets whether or not the window has shadow. fn set_has_shadow(&self, has_shadow: bool); + /// Sets whether the system can automatically organize windows into tabs. + /// + /// + fn set_allows_automatic_window_tabbing(&self, enabled: bool); + + /// Returns whether the system can automatically organize windows into tabs. + fn allows_automatic_window_tabbing(&self) -> bool; + + /// Group windows together by using the same tabbing identifier. + /// + /// + fn set_tabbing_identifier(&self, identifier: &str); + + /// Returns the window's tabbing identifier. + fn tabbing_identifier(&self) -> String; + + /// Select next tab. + fn select_next_tab(&self); + + /// Select previous tab. + fn select_previous_tab(&self); + + /// Select the tab with the given index. + /// + /// Will no-op when the index is out of bounds. + fn select_tab_at_index(&self, index: NonZeroUsize); + /// Get the window's edit state. /// /// # Examples @@ -100,6 +127,41 @@ impl WindowExtMacOS for Window { self.window.set_has_shadow(has_shadow) } + #[inline] + fn set_allows_automatic_window_tabbing(&self, enabled: bool) { + self.window.set_allows_automatic_window_tabbing(enabled) + } + + #[inline] + fn allows_automatic_window_tabbing(&self) -> bool { + self.window.allows_automatic_window_tabbing() + } + + #[inline] + fn set_tabbing_identifier(&self, identifier: &str) { + self.window.set_tabbing_identifier(identifier); + } + + #[inline] + fn tabbing_identifier(&self) -> String { + self.window.tabbing_identifier() + } + + #[inline] + fn select_next_tab(&self) { + self.window.select_next_tab(); + } + + #[inline] + fn select_previous_tab(&self) { + self.window.select_previous_tab(); + } + + #[inline] + fn select_tab_at_index(&self, index: NonZeroUsize) { + self.window.select_tab_at_index(index); + } + #[inline] fn is_document_edited(&self) -> bool { self.window.is_document_edited() @@ -161,6 +223,14 @@ pub trait WindowBuilderExtMacOS { fn with_has_shadow(self, has_shadow: bool) -> WindowBuilder; /// Window accepts click-through mouse events. fn with_accepts_first_mouse(self, accepts_first_mouse: bool) -> WindowBuilder; + /// Whether the window could do automatic window tabbing. + /// + /// The default is `true`. + fn with_automatic_window_tabbing(self, automatic_tabbing: bool) -> WindowBuilder; + /// Defines the window tabbing identifier. + /// + /// + fn with_tabbing_identifier(self, identifier: &str) -> WindowBuilder; /// Set how the Option keys are interpreted. /// /// See [`WindowExtMacOS::set_option_as_alt`] for details on what this means if set. @@ -225,6 +295,20 @@ impl WindowBuilderExtMacOS for WindowBuilder { self } + #[inline] + fn with_automatic_window_tabbing(mut self, automatic_tabbing: bool) -> WindowBuilder { + self.platform_specific.allows_automatic_window_tabbing = automatic_tabbing; + self + } + + #[inline] + fn with_tabbing_identifier(mut self, tabbing_identifier: &str) -> WindowBuilder { + self.platform_specific + .tabbing_identifier + .replace(tabbing_identifier.to_string()); + self + } + #[inline] fn with_option_as_alt(mut self, option_as_alt: OptionAsAlt) -> WindowBuilder { self.platform_specific.option_as_alt = option_as_alt; diff --git a/src/platform_impl/macos/appkit/mod.rs b/src/platform_impl/macos/appkit/mod.rs index 7f96e35d..cb7363c8 100644 --- a/src/platform_impl/macos/appkit/mod.rs +++ b/src/platform_impl/macos/appkit/mod.rs @@ -24,6 +24,7 @@ mod menu_item; mod pasteboard; mod responder; mod screen; +mod tab_group; mod text_input_context; mod version; mod view; @@ -49,6 +50,7 @@ pub(crate) use self::pasteboard::{NSFilenamesPboardType, NSPasteboard, NSPastebo pub(crate) use self::responder::NSResponder; #[allow(unused_imports)] pub(crate) use self::screen::{NSDeviceDescriptionKey, NSScreen}; +pub(crate) use self::tab_group::NSWindowTabGroup; pub(crate) use self::text_input_context::NSTextInputContext; pub(crate) use self::version::NSAppKitVersion; pub(crate) use self::view::{NSTrackingRectTag, NSView}; diff --git a/src/platform_impl/macos/appkit/tab_group.rs b/src/platform_impl/macos/appkit/tab_group.rs new file mode 100644 index 00000000..d45d2dec --- /dev/null +++ b/src/platform_impl/macos/appkit/tab_group.rs @@ -0,0 +1,28 @@ +use objc2::foundation::{NSArray, NSObject}; +use objc2::rc::{Id, Shared}; +use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; + +use super::NSWindow; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSWindowTabGroup; + + unsafe impl ClassType for NSWindowTabGroup { + type Super = NSObject; + } +); + +extern_methods!( + unsafe impl NSWindowTabGroup { + #[sel(selectNextTab)] + pub fn selectNextTab(&self); + #[sel(selectPreviousTab)] + pub fn selectPreviousTab(&self); + pub fn tabbedWindows(&self) -> Id, Shared> { + unsafe { msg_send_id![self, windows] } + } + #[sel(setSelectedWindow:)] + pub fn setSelectedWindow(&self, window: &NSWindow); + } +); diff --git a/src/platform_impl/macos/appkit/window.rs b/src/platform_impl/macos/appkit/window.rs index c6f078eb..4c4fed02 100644 --- a/src/platform_impl/macos/appkit/window.rs +++ b/src/platform_impl/macos/appkit/window.rs @@ -6,7 +6,9 @@ use objc2::rc::{Id, Shared}; use objc2::runtime::Object; use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; -use super::{NSButton, NSColor, NSEvent, NSPasteboardType, NSResponder, NSScreen, NSView}; +use super::{ + NSButton, NSColor, NSEvent, NSPasteboardType, NSResponder, NSScreen, NSView, NSWindowTabGroup, +}; extern_class!( /// Main-Thread-Only! @@ -171,6 +173,12 @@ extern_methods!( #[sel(setLevel:)] pub fn setLevel(&self, level: NSWindowLevel); + #[sel(setAllowsAutomaticWindowTabbing:)] + pub fn setAllowsAutomaticWindowTabbing(val: bool); + + #[sel(setTabbingIdentifier:)] + pub fn setTabbingIdentifier(&self, identifier: &NSString); + #[sel(setDocumentEdited:)] pub fn setDocumentEdited(&self, val: bool); @@ -201,6 +209,20 @@ extern_methods!( #[sel(isZoomed)] pub fn isZoomed(&self) -> bool; + #[sel(allowsAutomaticWindowTabbing)] + pub fn allowsAutomaticWindowTabbing() -> bool; + + #[sel(selectNextTab)] + pub fn selectNextTab(&self); + + pub fn tabbingIdentifier(&self) -> Id { + unsafe { msg_send_id![self, tabbingIdentifier] } + } + + pub fn tabGroup(&self) -> Id { + unsafe { msg_send_id![self, tabGroup] } + } + #[sel(isDocumentEdited)] pub fn isDocumentEdited(&self) -> bool; diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 1413b05f..20675f75 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -2,6 +2,7 @@ use std::collections::VecDeque; use std::f64; +use std::num::NonZeroUsize; use std::ops; use std::os::raw::c_void; use std::ptr::NonNull; @@ -82,6 +83,8 @@ pub struct PlatformSpecificWindowBuilderAttributes { pub disallow_hidpi: bool, pub has_shadow: bool, pub accepts_first_mouse: bool, + pub allows_automatic_window_tabbing: bool, + pub tabbing_identifier: Option, pub option_as_alt: OptionAsAlt, } @@ -98,6 +101,8 @@ impl Default for PlatformSpecificWindowBuilderAttributes { disallow_hidpi: false, has_shadow: true, accepts_first_mouse: true, + allows_automatic_window_tabbing: true, + tabbing_identifier: None, option_as_alt: Default::default(), } } @@ -357,6 +362,12 @@ impl WinitWindow { this.setTitle(&NSString::from_str(&attrs.title)); this.setAcceptsMouseMovedEvents(true); + if let Some(identifier) = pl_attrs.tabbing_identifier { + this.setTabbingIdentifier(&NSString::from_str(&identifier)); + } + + NSWindow::setAllowsAutomaticWindowTabbing(pl_attrs.allows_automatic_window_tabbing); + if attrs.content_protected { this.setSharingType(NSWindowSharingType::NSWindowSharingNone); } @@ -1394,6 +1405,46 @@ impl WindowExtMacOS for WinitWindow { self.setHasShadow(has_shadow) } + #[inline] + fn set_allows_automatic_window_tabbing(&self, enabled: bool) { + NSWindow::setAllowsAutomaticWindowTabbing(enabled); + } + + #[inline] + fn allows_automatic_window_tabbing(&self) -> bool { + NSWindow::allowsAutomaticWindowTabbing() + } + + #[inline] + fn set_tabbing_identifier(&self, identifier: &str) { + self.setTabbingIdentifier(&NSString::from_str(identifier)) + } + + #[inline] + fn tabbing_identifier(&self) -> String { + self.tabbingIdentifier().to_string() + } + + #[inline] + fn select_next_tab(&self) { + self.tabGroup().selectNextTab(); + } + + #[inline] + fn select_previous_tab(&self) { + self.tabGroup().selectPreviousTab(); + } + + #[inline] + fn select_tab_at_index(&self, index: NonZeroUsize) { + let tab_group = self.tabGroup(); + let windows = tab_group.tabbedWindows(); + let index = index.get() - 1; + if index < windows.len() { + tab_group.setSelectedWindow(&windows[index]); + } + } + fn is_document_edited(&self) -> bool { self.isDocumentEdited() }