Skip to content

Commit

Permalink
On macOS, add tabbing APIs
Browse files Browse the repository at this point in the history
This should let the users control macOS tabbing and allow to create
windows in tab.

Co-authored-by: Amr Bashir <[email protected]>
  • Loading branch information
kchibisov and amrbashir authored Jul 13, 2023
1 parent b631646 commit c5941d1
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
105 changes: 105 additions & 0 deletions examples/window_tabbing.rs
Original file line number Diff line number Diff line change
@@ -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::<NonZeroUsize>() {
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");
}
86 changes: 85 additions & 1 deletion src/platform/macos.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::os::raw::c_void;
use std::{num::NonZeroUsize, os::raw::c_void};

use objc2::rc::Id;

Expand Down Expand Up @@ -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.
///
/// <https://developer.apple.com/documentation/appkit/nswindow/1646657-allowsautomaticwindowtabbing>
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.
///
/// <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
///
/// <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
fn with_tabbing_identifier(self, identifier: &str) -> WindowBuilder;
/// Set how the <kbd>Option</kbd> keys are interpreted.
///
/// See [`WindowExtMacOS::set_option_as_alt`] for details on what this means if set.
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/platform_impl/macos/appkit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ mod menu_item;
mod pasteboard;
mod responder;
mod screen;
mod tab_group;
mod text_input_context;
mod version;
mod view;
Expand All @@ -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};
Expand Down
28 changes: 28 additions & 0 deletions src/platform_impl/macos/appkit/tab_group.rs
Original file line number Diff line number Diff line change
@@ -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<NSArray<NSWindow, Shared>, Shared> {
unsafe { msg_send_id![self, windows] }
}
#[sel(setSelectedWindow:)]
pub fn setSelectedWindow(&self, window: &NSWindow);
}
);
24 changes: 23 additions & 1 deletion src/platform_impl/macos/appkit/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<NSString, Shared> {
unsafe { msg_send_id![self, tabbingIdentifier] }
}

pub fn tabGroup(&self) -> Id<NSWindowTabGroup, Shared> {
unsafe { msg_send_id![self, tabGroup] }
}

#[sel(isDocumentEdited)]
pub fn isDocumentEdited(&self) -> bool;

Expand Down
Loading

0 comments on commit c5941d1

Please sign in to comment.