Skip to content

Commit

Permalink
feat: add window menu on mac (neovide#2323)
Browse files Browse the repository at this point in the history
* feat: add window menu on mac

* refactor: use icrate instead of cocoa

* feat: reimplement mac app menu

* feat: add ctrl+command+f binding for fullscreen

* fix: disable native tabbing mode on mac
  • Loading branch information
polachok authored Feb 29, 2024
1 parent 69ebd1f commit de59b9e
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ winapi = { version = "0.3.9", features = ["winuser", "wincon", "winerror", "dwma
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.24.0"
objc = "0.2.7"
icrate = { version = "0.0.4", features = [ "apple", "Foundation", "Foundation_NSThread", "AppKit", "AppKit_NSColor", "AppKit_NSEvent", "AppKit_NSView", "AppKit_NSWindow" ] }
icrate = { version = "0.0.4", features = [ "apple", "Foundation", "Foundation_NSThread", "AppKit", "AppKit_NSColor", "AppKit_NSEvent", "AppKit_NSView", "AppKit_NSWindow", "AppKit_NSViewController", "AppKit_NSMenu", "AppKit_NSMenuItem", "AppKit_NSOpenPanel", "Foundation_NSArray" ] }
objc2 = "0.4.1"

[target.'cfg(target_os = "linux")'.dependencies]
Expand Down
169 changes: 164 additions & 5 deletions src/window/macos.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
use icrate::{
AppKit::{
NSColor, NSEvent, NSView, NSViewMinYMargin, NSViewWidthSizable, NSWindow,
NSWindowStyleMaskFullScreen, NSWindowStyleMaskTitled,
NSApplication, NSColor, NSEvent, NSEventModifierFlagCommand, NSEventModifierFlagControl,
NSEventModifierFlagOption, NSMenu, NSMenuItem, NSView, NSViewMinYMargin,
NSViewWidthSizable, NSWindow, NSWindowStyleMaskFullScreen, NSWindowStyleMaskTitled,
NSWindowTabbingModeDisallowed,
},
Foundation::{MainThreadMarker, NSPoint, NSRect, NSSize},
Foundation::{MainThreadMarker, NSObject, NSPoint, NSProcessInfo, NSRect, NSSize, NSString},
};
use objc2::{declare_class, msg_send_id, mutability::InteriorMutable, rc::Id, ClassType};
use objc2::{declare_class, msg_send_id, mutability::InteriorMutable, rc::Id, sel, ClassType};

use csscolorparser::Color;
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
use winit::event::{Event, WindowEvent};
use winit::window::Window;

use crate::bridge::{send_ui, ParallelCommand};
use crate::{
cmd_line::CmdLineSettings, error_msg, frame::Frame, renderer::WindowedContext,
settings::SETTINGS,
settings::SETTINGS, window::UserEvent,
};

use super::{WindowSettings, WindowSettingsChanged};
Expand Down Expand Up @@ -65,6 +69,10 @@ impl MacosWindowFeature {
},
_ => panic!("Not an appkit window."),
};
// Disallow tabbing mode to prevent the window from being tabbed.
unsafe {
ns_window.setTabbingMode(NSWindowTabbingModeDisallowed);
}

let mut extra_titlebar_height_in_pixel: u32 = 0;

Expand Down Expand Up @@ -257,3 +265,154 @@ impl MacosWindowFeature {
}
}
}

declare_class!(
struct QuitHandler;

unsafe impl ClassType for QuitHandler {
type Super = NSObject;
type Mutability = InteriorMutable;
const NAME: &'static str = "QuitHandler";
}

unsafe impl QuitHandler {
#[method(quit:)]
unsafe fn quit(&self, _event: &NSEvent) {
send_ui(ParallelCommand::Quit);
}
}
);

impl QuitHandler {
pub fn new(_mtm: MainThreadMarker) -> Id<QuitHandler> {
unsafe { msg_send_id![Self::alloc(), init] }
}
}

pub struct Menu {
menu_added: bool,
quit_handler: Id<QuitHandler>,
}

impl Menu {
pub fn new(mtm: MainThreadMarker) -> Self {
Menu {
menu_added: false,
quit_handler: QuitHandler::new(mtm),
}
}
pub fn ensure_menu_added(&mut self, ev: &Event<UserEvent>) {
if let Event::WindowEvent {
event: WindowEvent::Focused(_),
..
} = ev
{
if !self.menu_added {
self.add_menus();
self.menu_added = true;
}
}
}

fn add_app_menu(&self) -> Id<NSMenu> {
unsafe {
let app_menu = NSMenu::new();
let process_name = NSProcessInfo::processInfo().processName();
let about_item = NSMenuItem::new();
about_item
.setTitle(&NSString::from_str("About ").stringByAppendingString(&process_name));
about_item.setAction(Some(sel!(orderFrontStandardAboutPanel:)));
app_menu.addItem(&about_item);

let services_item = NSMenuItem::new();
let services_menu = NSMenu::new();
services_item.setTitle(&NSString::from_str("Services"));
services_item.setSubmenu(Some(&services_menu));
app_menu.addItem(&services_item);

let sep = NSMenuItem::separatorItem();
app_menu.addItem(&sep);

// application window operations
let hide_item = NSMenuItem::new();
hide_item.setTitle(&NSString::from_str("Hide ").stringByAppendingString(&process_name));
hide_item.setKeyEquivalent(&NSString::from_str("h"));
hide_item.setAction(Some(sel!(hide:)));
app_menu.addItem(&hide_item);

let hide_others_item = NSMenuItem::new();
hide_others_item.setTitle(&NSString::from_str("Hide Others"));
hide_others_item.setKeyEquivalent(&NSString::from_str("h"));
hide_others_item.setKeyEquivalentModifierMask(
NSEventModifierFlagOption | NSEventModifierFlagCommand,
);
hide_others_item.setAction(Some(sel!(hideOtherApplications:)));
app_menu.addItem(&hide_others_item);

let show_all_item = NSMenuItem::new();
show_all_item.setTitle(&NSString::from_str("Show All"));
show_all_item.setAction(Some(sel!(unhideAllApplications:)));

// quit
let sep = NSMenuItem::separatorItem();
app_menu.addItem(&sep);

let quit_item = NSMenuItem::new();
quit_item.setTitle(&NSString::from_str("Quit ").stringByAppendingString(&process_name));
quit_item.setKeyEquivalent(&NSString::from_str("q"));
quit_item.setAction(Some(sel!(quit:)));
quit_item.setTarget(Some(&self.quit_handler));
app_menu.addItem(&quit_item);

app_menu
}
}

fn add_menus(&self) {
let app = unsafe { NSApplication::sharedApplication() };

let main_menu = unsafe { NSMenu::new() };

unsafe {
let app_menu = self.add_app_menu();
let app_menu_item = NSMenuItem::new();
app_menu_item.setSubmenu(Some(&app_menu));
if let Some(services_menu) = app_menu.itemWithTitle(&NSString::from_str("Services")) {
app.setServicesMenu(services_menu.submenu().as_deref());
}
main_menu.addItem(&app_menu_item);

let win_menu = self.add_window_menu();
let win_menu_item = NSMenuItem::new();
win_menu_item.setSubmenu(Some(&win_menu));
main_menu.addItem(&win_menu_item);
app.setWindowsMenu(Some(&win_menu));
}

unsafe { app.setMainMenu(Some(&main_menu)) };
}

fn add_window_menu(&self) -> Id<NSMenu> {
let menu_title = NSString::from_str("Window");
unsafe {
let menu = NSMenu::new();
menu.setTitle(&menu_title);

let full_screen_item = NSMenuItem::new();
full_screen_item.setTitle(&NSString::from_str("Enter Full Screen"));
full_screen_item.setKeyEquivalent(&NSString::from_str("f"));
full_screen_item.setAction(Some(sel!(toggleFullScreen:)));
full_screen_item.setKeyEquivalentModifierMask(
NSEventModifierFlagControl | NSEventModifierFlagCommand,
);
menu.addItem(&full_screen_item);

let min_item = NSMenuItem::new();
min_item.setTitle(&NSString::from_str("Minimize"));
min_item.setKeyEquivalent(&NSString::from_str("m"));
min_item.setAction(Some(sel!(performMiniaturize:)));
menu.addItem(&min_item);
menu
}
}
}
20 changes: 17 additions & 3 deletions src/window/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ mod macos;
#[cfg(target_os = "linux")]
use std::env;

#[cfg(target_os = "macos")]
use icrate::Foundation::MainThreadMarker;

use winit::{
dpi::{PhysicalSize, Size},
error::EventLoopError,
Expand All @@ -28,6 +31,9 @@ use winit::platform::wayland::WindowBuilderExtWayland;
#[cfg(target_os = "linux")]
use winit::platform::x11::WindowBuilderExtX11;

#[cfg(target_os = "macos")]
use winit::platform::macos::EventLoopBuilderExtMacOS;

use image::{load_from_memory, GenericImageView, Pixel};
use keyboard_manager::KeyboardManager;
use mouse_manager::MouseManager;
Expand Down Expand Up @@ -116,9 +122,10 @@ impl From<HotReloadConfigs> for UserEvent {
}

pub fn create_event_loop() -> EventLoop<UserEvent> {
EventLoopBuilder::<UserEvent>::with_user_event()
.build()
.expect("Failed to create winit event loop")
let mut builder = EventLoopBuilder::<UserEvent>::with_user_event();
#[cfg(target_os = "macos")]
builder.with_default_menu(false);
builder.build().expect("Failed to create winit event loop")
}

pub fn create_window(
Expand Down Expand Up @@ -299,7 +306,14 @@ pub fn main_loop(

let mut update_loop = UpdateLoop::new(cmd_line_settings.idle);

#[cfg(target_os = "macos")]
let mut menu = {
let mtm = MainThreadMarker::new().expect("must be on the main thread");
macos::Menu::new(mtm)
};
event_loop.run(move |e, window_target| {
#[cfg(target_os = "macos")]
menu.ensure_menu_added(&e);
if e == Event::LoopExiting {
return;
}
Expand Down

0 comments on commit de59b9e

Please sign in to comment.