Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added MacOS menu #1583

Merged
merged 9 commits into from
Apr 24, 2021
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- On Windows, added `WindowExtWindows::set_enable` to allow creating modal popup windows.
- On macOS, emit `RedrawRequested` events immediately while the window is being resized.
- Implement `Default`, `Hash`, and `Eq` for `LogicalPosition`, `PhysicalPosition`, `LogicalSize`, and `PhysicalSize`.
- On macOS, initialize the Menu Bar with minimal defaults.

# 0.24.0 (2020-12-09)

Expand Down
4 changes: 4 additions & 0 deletions src/platform_impl/macos/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::{
platform_impl::platform::{
event::{EventProxy, EventWrapper},
event_loop::{post_dummy_event, PanicInfo},
menu,
observer::{CFRunLoopGetMain, CFRunLoopWakeUp, EventLoopWaker},
util::{IdRef, Never},
window::get_window_id,
Expand Down Expand Up @@ -274,6 +275,9 @@ impl AppState {
pub fn launched() {
HANDLER.set_ready();
HANDLER.waker().start();
// The menubar initialization should be before the `NewEvents` event, to allow overriding
// of the default menu in the event
menu::initialize();
HANDLER.set_in_callback(true);
HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::NewEvents(
StartCause::Init,
Expand Down
117 changes: 117 additions & 0 deletions src/platform_impl/macos/menu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use cocoa::appkit::{
NSApp, NSApplication, NSApplicationActivationPolicyRegular, NSEventModifierFlags, NSMenu,
NSMenuItem,
};
use cocoa::base::{nil, selector};
use cocoa::foundation::{NSAutoreleasePool, NSProcessInfo, NSString};
use objc::{
rc::autoreleasepool,
runtime::{Object, Sel},
};

struct KeyEquivalent<'a> {
key: &'a str,
masks: Option<NSEventModifierFlags>,
}

pub fn initialize() {
autoreleasepool(|| unsafe {
let menubar = NSMenu::new(nil).autorelease();
let app_menu_item = NSMenuItem::new(nil).autorelease();
menubar.addItem_(app_menu_item);
let app = NSApp();
app.setMainMenu_(menubar);

let app_menu = NSMenu::new(nil);
let process_name = NSProcessInfo::processInfo(nil).processName();

// About menu item
let about_item_prefix = NSString::alloc(nil).init_str("About ");
let about_item_title = about_item_prefix.stringByAppendingString_(process_name);
let about_item = menu_item(
about_item_title,
selector("orderFrontStandardAboutPanel:"),
None,
);

// Seperator menu item
let sep_first = NSMenuItem::separatorItem(nil);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add the Services menu here (can be done by creating a menu item with an empty menu, and registering that as the servicesMenu)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, not sure it make sense if we don't have any services to add?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into how the Services menu is used. Winit uses a custom NSView subclass for the entire window surface. Applications and custom NSView types have to implement a certain protocol (see "Using Services" in the Apple Developer Documentation Archive). More specifically the NSView has to provide an implementation for validRequestorForSendType through which it must specify whether the currently selected thing is a valid target for a service that operates on the provided types of data. However winit has no idea what is selected currently. Given that winit doesn't provide APIs to handle validRequestorForSendType messages, it's currently not possible to use the Services menu through winit (if I understand correctly). This is somewhat related to #1759 in that it's a macOS specific "event" that a higher level code may want to be able to handle.

Anyhow I don't think we should be concerned about that for this PR. So with all that, just like @casperstorm wrote, it seems there's no point in adding the Services menu.

// Hide application menu item
let hide_item_prefix = NSString::alloc(nil).init_str("Hide ");
let hide_item_title = hide_item_prefix.stringByAppendingString_(process_name);
let hide_item = menu_item(
hide_item_title,
selector("hide:"),
Some(KeyEquivalent {
key: "h",
masks: None,
}),
);

// Hide other applications menu item
let hide_others_item_title = NSString::alloc(nil).init_str("Hide Others");
let hide_others_item = menu_item(
hide_others_item_title,
selector("hideOtherApplications:"),
Some(KeyEquivalent {
key: "h",
masks: Some(
NSEventModifierFlags::NSAlternateKeyMask
| NSEventModifierFlags::NSCommandKeyMask,
),
}),
);

// Show applications menu item
let show_all_item_title = NSString::alloc(nil).init_str("Show All");
let show_all_item = menu_item(
show_all_item_title,
selector("unhideAllApplications:"),
None,
);

// Seperator menu item
let sep = NSMenuItem::separatorItem(nil);

// Quit application menu item
let quit_item_prefix = NSString::alloc(nil).init_str("Quit ");
let quit_item_title = quit_item_prefix.stringByAppendingString_(process_name);
let quit_item = menu_item(
quit_item_title,
selector("terminate:"),
Some(KeyEquivalent {
key: "q",
masks: None,
}),
);

app_menu.addItem_(about_item);
app_menu.addItem_(sep_first);
app_menu.addItem_(hide_item);
app_menu.addItem_(hide_others_item);
app_menu.addItem_(show_all_item);
app_menu.addItem_(sep);
app_menu.addItem_(quit_item);
app_menu_item.setSubmenu_(app_menu);
});
}

fn menu_item(
title: *mut Object,
selector: Sel,
key_equivalent: Option<KeyEquivalent<'_>>,
) -> *mut Object {
unsafe {
let (key, masks) = match key_equivalent {
Some(ke) => (NSString::alloc(nil).init_str(ke.key), ke.masks),
None => (NSString::alloc(nil).init_str(""), None),
};
let item = NSMenuItem::alloc(nil).initWithTitle_action_keyEquivalent_(title, selector, key);
if let Some(masks) = masks {
item.setKeyEquivalentModifierMask_(masks)
}

item
}
}
1 change: 1 addition & 0 deletions src/platform_impl/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod app_state;
mod event;
mod event_loop;
mod ffi;
mod menu;
mod monitor;
mod observer;
mod util;
Expand Down