From 742696268a717bf109cf71e2858c683c1a63b255 Mon Sep 17 00:00:00 2001 From: David Lemarier Date: Thu, 29 Apr 2021 11:32:19 -0400 Subject: [PATCH 1/3] feat(macOS): WIP Custom menu builder --- examples/custom_menu.rs | 88 +++++++ examples/custom_menu_multiwindow.rs | 64 +++++ src/lib.rs | 1 + src/menu.rs | 102 ++++++++ src/platform_impl/macos/app_state.rs | 4 - src/platform_impl/macos/menu.rs | 341 +++++++++++++++++++------- src/platform_impl/macos/util/async.rs | 6 + src/platform_impl/macos/window.rs | 14 +- src/window.rs | 29 +++ 9 files changed, 561 insertions(+), 88 deletions(-) create mode 100644 examples/custom_menu.rs create mode 100644 examples/custom_menu_multiwindow.rs create mode 100644 src/menu.rs diff --git a/examples/custom_menu.rs b/examples/custom_menu.rs new file mode 100644 index 0000000000..51021b97b6 --- /dev/null +++ b/examples/custom_menu.rs @@ -0,0 +1,88 @@ +use simple_logger::SimpleLogger; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + menu::{Menu, MenuItem}, + window::WindowBuilder, +}; + +pub enum Message { + MenuClickAddNewItem, +} + +fn main() { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .with_menu(vec![ + Menu::new( + "", + vec![ + MenuItem::About("Todos".to_string()), + MenuItem::Separator, + MenuItem::new("Preferences".to_string()).key(","), + MenuItem::Separator, + MenuItem::Services, + MenuItem::Separator, + MenuItem::Hide, + MenuItem::HideOthers, + MenuItem::ShowAll, + MenuItem::Separator, + MenuItem::Quit, + ], + ), + Menu::new( + "File", + vec![ + MenuItem::new("Open/Show Window".to_string()).key("n"), + MenuItem::Separator, + MenuItem::new("Add Todo".to_string()).key("+"), + MenuItem::Separator, + MenuItem::CloseWindow, + ], + ), + Menu::new( + "Edit", + vec![ + MenuItem::Undo, + MenuItem::Redo, + MenuItem::Separator, + MenuItem::Cut, + MenuItem::Copy, + MenuItem::Paste, + MenuItem::Separator, + MenuItem::SelectAll, + ], + ), + Menu::new("View", vec![MenuItem::EnterFullScreen]), + Menu::new( + "Window", + vec![ + MenuItem::Minimize, + MenuItem::Zoom, + MenuItem::Separator, + MenuItem::new("Bring All to Front".to_string()), + ], + ), + Menu::new("Help", vec![]), + ]) + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + } if window_id == window.id() => *control_flow = ControlFlow::Exit, + Event::MainEventsCleared => { + window.request_redraw(); + } + _ => (), + } + }); +} diff --git a/examples/custom_menu_multiwindow.rs b/examples/custom_menu_multiwindow.rs new file mode 100644 index 0000000000..4706b3c871 --- /dev/null +++ b/examples/custom_menu_multiwindow.rs @@ -0,0 +1,64 @@ +use std::collections::HashMap; + +use simple_logger::SimpleLogger; +use winit::{ + event::{ElementState, Event, KeyboardInput, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + menu::{Menu, MenuItem}, + window::{Window, WindowBuilder}, +}; + +fn main() { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + let mut windows = HashMap::new(); + for _ in 0..3 { + let window = WindowBuilder::new() + .with_menu(vec![Menu::new( + "asf", + vec![ + MenuItem::About("My app!".to_string()), + MenuItem::Separator, + MenuItem::ShowAll, + ], + )]) + .build(&event_loop) + .unwrap(); + windows.insert(window.id(), window); + } + + event_loop.run(move |event, event_loop, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, window_id } => { + match event { + WindowEvent::CloseRequested => { + println!("Window {:?} has received the signal to close", window_id); + + // This drops the window, causing it to close. + windows.remove(&window_id); + + if windows.is_empty() { + *control_flow = ControlFlow::Exit; + } + } + WindowEvent::KeyboardInput { + input: + KeyboardInput { + state: ElementState::Pressed, + .. + }, + .. + } => { + let window = Window::new(&event_loop).unwrap(); + windows.insert(window.id(), window); + } + _ => (), + } + } + _ => (), + } + }) +} diff --git a/src/lib.rs b/src/lib.rs index 51f4a8634a..f5360d0519 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -155,6 +155,7 @@ pub mod error; pub mod event; pub mod event_loop; mod icon; +pub mod menu; pub mod monitor; mod platform_impl; pub mod window; diff --git a/src/menu.rs b/src/menu.rs new file mode 100644 index 0000000000..6197743386 --- /dev/null +++ b/src/menu.rs @@ -0,0 +1,102 @@ +#[derive(Debug, Clone)] +pub struct Menu { + pub title: String, + pub items: Vec, +} + +impl Menu { + pub fn new(title: &str, items: Vec) -> Self { + Self { + title: String::from(title), + items, + } + } +} + +#[derive(Debug, Clone)] +pub struct CustomMenu { + pub name: String, + pub key: Option, +} + +#[derive(Debug, Clone)] +pub enum MenuItem { + /// A custom MenuItem. This type functions as a builder, so you can customize it easier. + /// You can (and should) create this variant via the `new(title)` method, but if you need to do + /// something crazier, then wrap it in this and you can hook into the Cacao menu system + /// accordingly. + Custom(CustomMenu), + + /// Shows a standard "About" item, which will bring up the necessary window when clicked + /// (include a `credits.html` in your App to make use of here). The argument baked in here + /// should be your app name. + About(String), + + /// A standard "hide the app" menu item. + Hide, + + /// A standard "Services" menu item. + Services, + + /// A "hide all other windows" menu item. + HideOthers, + + /// A menu item to show all the windows for this app. + ShowAll, + + /// Close the current window. + CloseWindow, + + /// A "quit this app" menu icon. + Quit, + + /// A menu item for enabling copying (often text) from responders. + Copy, + + /// A menu item for enabling cutting (often text) from responders. + Cut, + + /// An "undo" menu item; particularly useful for supporting the cut/copy/paste/undo lifecycle + /// of events. + Undo, + + /// An "redo" menu item; particularly useful for supporting the cut/copy/paste/undo lifecycle + /// of events. + Redo, + + /// A menu item for selecting all (often text) from responders. + SelectAll, + + /// A menu item for pasting (often text) into responders. + Paste, + + /// A standard "enter full screen" item. + EnterFullScreen, + + /// An item for minimizing the window with the standard system controls. + Minimize, + + /// An item for instructing the app to zoom. Your app must react to this with necessary window + /// lifecycle events. + Zoom, + + /// Represents a Separator. It's useful nonetheless for + /// separating out pieces of the `NSMenu` structure. + Separator, +} + +impl MenuItem { + pub fn new(title: String) -> Self { + MenuItem::Custom(CustomMenu { + key: None, + name: title, + }) + } + + pub fn key(mut self, key: &str) -> Self { + if let MenuItem::Custom(ref mut custom_menu) = self { + custom_menu.key = Some(key.to_string()); + } + self + } +} diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index 4f20e21694..59cf0f2aac 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -25,7 +25,6 @@ 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, @@ -275,9 +274,6 @@ 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, diff --git a/src/platform_impl/macos/menu.rs b/src/platform_impl/macos/menu.rs index a0d61edd74..de104602fd 100644 --- a/src/platform_impl/macos/menu.rs +++ b/src/platform_impl/macos/menu.rs @@ -1,105 +1,225 @@ -use cocoa::appkit::{ - NSApp, NSApplication, NSApplicationActivationPolicyRegular, NSEventModifierFlags, NSMenu, - NSMenuItem, -}; -use cocoa::base::{nil, selector}; -use cocoa::foundation::{NSAutoreleasePool, NSProcessInfo, NSString}; +use cocoa::appkit::{NSApp, NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem}; +use cocoa::base::{id, nil, selector}; +use cocoa::foundation::{NSAutoreleasePool, NSString}; use objc::{ + declare::ClassDecl, rc::autoreleasepool, - runtime::{Object, Sel}, + runtime::{Class, Object, Sel}, }; +use std::sync::Once; + +use crate::menu::{Menu, MenuItem}; + +static BLOCK_PTR: &'static str = "winitMenuItemBlockPtr"; struct KeyEquivalent<'a> { key: &'a str, masks: Option, } -pub fn initialize() { +struct Action(Box); + +pub fn initialize(menu: Vec) { 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(); + for menu in menu { + // create our menu + let menu_item = NSMenuItem::new(nil).autorelease(); + menubar.addItem_(menu_item); + // prepare our submenu tree + let menu_title = NSString::alloc(nil).init_str(&menu.title); + let menu_object = NSMenu::alloc(nil).initWithTitle_(menu_title).autorelease(); - // 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, - ); + // create menu + for item in &menu.items { + let item_obj: *mut Object = match item { + MenuItem::Custom(custom_menu) => { + let title = NSString::alloc(nil).init_str(custom_menu.name.as_str()); + make_menu_item(title, None, None) + } + MenuItem::Separator => NSMenuItem::separatorItem(nil), + MenuItem::About(app_name) => { + let title = format!("About {}", app_name); + let title_alloc = NSString::alloc(nil).init_str(title.as_str()); + make_menu_item( + title_alloc, + Some(selector("orderFrontStandardAboutPanel:")), + None, + ) + } + MenuItem::CloseWindow => { + let title = NSString::alloc(nil).init_str("Close Window"); + make_menu_item( + title, + Some(selector("performClose:")), + Some(KeyEquivalent { + key: "w", + masks: None, + }), + ) + } + MenuItem::Quit => { + let title = NSString::alloc(nil).init_str("Quit"); + make_menu_item( + title, + Some(selector("terminate:")), + Some(KeyEquivalent { + key: "q", + masks: None, + }), + ) + } + MenuItem::Hide => { + let title = NSString::alloc(nil).init_str("Hide"); + make_menu_item( + title, + Some(selector("hide:")), + Some(KeyEquivalent { + key: "h", + masks: None, + }), + ) + } + MenuItem::HideOthers => { + let title = NSString::alloc(nil).init_str("Hide Others"); + make_menu_item( + title, + Some(selector("hideOtherApplications:")), + Some(KeyEquivalent { + key: "h", + masks: Some( + NSEventModifierFlags::NSAlternateKeyMask + | NSEventModifierFlags::NSCommandKeyMask, + ), + }), + ) + } + MenuItem::ShowAll => { + let title = NSString::alloc(nil).init_str("Show All"); + make_menu_item(title, Some(selector("unhideAllApplications:")), None) + } + MenuItem::EnterFullScreen => { + let title = NSString::alloc(nil).init_str("Enter Full Screen"); + make_menu_item( + title, + Some(selector("toggleFullScreen:")), + Some(KeyEquivalent { + key: "h", + masks: Some( + NSEventModifierFlags::NSCommandKeyMask + | NSEventModifierFlags::NSControlKeyMask, + ), + }), + ) + } + MenuItem::Minimize => { + let title = NSString::alloc(nil).init_str("Minimize"); + make_menu_item( + title, + Some(selector("performMiniaturize:")), + Some(KeyEquivalent { + key: "m", + masks: None, + }), + ) + } + MenuItem::Zoom => { + let title = NSString::alloc(nil).init_str("Zoom"); + make_menu_item(title, Some(selector("performZoom:")), None) + } + MenuItem::Copy => { + let title = NSString::alloc(nil).init_str("Copy"); + make_menu_item( + title, + Some(selector("copy:")), + Some(KeyEquivalent { + key: "c", + masks: None, + }), + ) + } + MenuItem::Cut => { + let title = NSString::alloc(nil).init_str("Cut"); + make_menu_item( + title, + Some(selector("cut:")), + Some(KeyEquivalent { + key: "x", + masks: None, + }), + ) + } + MenuItem::Paste => { + let title = NSString::alloc(nil).init_str("Paste"); + make_menu_item( + title, + Some(selector("paste:")), + Some(KeyEquivalent { + key: "v", + masks: None, + }), + ) + } + MenuItem::Undo => { + let title = NSString::alloc(nil).init_str("Undo"); + make_menu_item( + title, + Some(selector("undo:")), + Some(KeyEquivalent { + key: "z", + masks: None, + }), + ) + } + MenuItem::Redo => { + let title = NSString::alloc(nil).init_str("Redo"); + make_menu_item( + title, + Some(selector("redo:")), + Some(KeyEquivalent { + key: "Z", + masks: None, + }), + ) + } + MenuItem::SelectAll => { + let title = NSString::alloc(nil).init_str("Select All"); + make_menu_item( + title, + Some(selector("selectAll:")), + Some(KeyEquivalent { + key: "a", + masks: None, + }), + ) + } + MenuItem::Services => { + let title = NSString::alloc(nil).init_str("Services"); + let item = make_menu_item(title, None, None); + let app_class = class!(NSApplication); + let app: id = msg_send![app_class, sharedApplication]; + let services: id = msg_send![app, servicesMenu]; + let _: () = msg_send![&*item, setSubmenu: services]; + item + } + }; - // Seperator menu item - let sep_first = NSMenuItem::separatorItem(nil); - - // 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, - ), - }), - ); + menu_object.addItem_(item_obj); + } - // 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, - }), - ); + menu_item.setSubmenu_(menu_object); + } - 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); + // Set the menu as main menu for the app + let app = NSApp(); + app.setMainMenu_(menubar); }); } -fn menu_item( +fn make_menu_item( title: *mut Object, - selector: Sel, + selector: Option, key_equivalent: Option>, ) -> *mut Object { unsafe { @@ -107,7 +227,18 @@ fn menu_item( 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 no selector defined, that mean it's a custom + // menu so fire our handler + let selector = match selector { + Some(selector) => selector, + None => sel!(fireBlockAction:), + }; + + // allocate our item to our class + let alloc: id = msg_send![make_menu_item_class(), alloc]; + let item = + NSMenuItem::alloc(alloc).initWithTitle_action_keyEquivalent_(title, selector, key); + if let Some(masks) = masks { item.setKeyEquivalentModifierMask_(masks) } @@ -115,3 +246,47 @@ fn menu_item( item } } + +fn make_menu_item_class() -> *const Class { + static mut APP_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = class!(NSMenuItem); + let mut decl = ClassDecl::new("WinitMenuItem", superclass).unwrap(); + decl.add_ivar::(BLOCK_PTR); + + decl.add_method( + sel!(dealloc), + dealloc_custom_menuitem as extern "C" fn(&Object, _), + ); + decl.add_method( + sel!(fireBlockAction:), + fire_custom_menu_click as extern "C" fn(&Object, _, id), + ); + + APP_CLASS = decl.register(); + }); + + unsafe { APP_CLASS } +} + +extern "C" fn fire_custom_menu_click(this: &Object, _: Sel, _item: id) { + println!("CLICK"); +} + +extern "C" fn dealloc_custom_menuitem(this: &Object, _: Sel) { + println!("DEALOC"); + unsafe { + let ptr: usize = *this.get_ivar(BLOCK_PTR); + let obj = ptr as *mut Action; + println!("Action {:?}", obj); + + if !obj.is_null() { + let _handler = Box::from_raw(obj); + } + + //let _: () = msg_send![this, setTarget:nil]; + let _: () = msg_send![super(this, class!(NSMenuItem)), dealloc]; + } +} diff --git a/src/platform_impl/macos/util/async.rs b/src/platform_impl/macos/util/async.rs index 7f8e7b8714..2005c79bb2 100644 --- a/src/platform_impl/macos/util/async.rs +++ b/src/platform_impl/macos/util/async.rs @@ -14,6 +14,7 @@ use objc::runtime::NO; use crate::{ dpi::LogicalSize, + menu::Menu, platform_impl::platform::{ffi, util::IdRef, window::SharedState}, }; @@ -205,6 +206,11 @@ pub unsafe fn set_title_async(ns_window: id, title: String) { }); } +// `setMenu:` isn't thread-safe. +pub unsafe fn set_menu_async(_ns_window: id, menu: Option>) { + dbg!(menu); +} + // `close:` is thread-safe, but we want the event to be triggered from the main // thread. Though, it's a good idea to look into that more... // diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 7d16f2fce3..9f0c4ed74b 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -15,12 +15,13 @@ use crate::{ }, error::{ExternalError, NotSupportedError, OsError as RootOsError}, icon::Icon, + menu::Menu, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, platform::macos::{ActivationPolicy, WindowExtMacOS}, platform_impl::platform::{ app_state::AppState, app_state::INTERRUPT_EVENT_LOOP_EXIT, - ffi, + ffi, menu, monitor::{self, MonitorHandle, VideoMode}, util::{self, IdRef}, view::CursorState, @@ -260,6 +261,11 @@ fn create_window( if attrs.position.is_none() { ns_window.center(); } + + if let Some(window_menu) = attrs.window_menu.clone() { + menu::initialize(window_menu); + } + ns_window }); pool.drain(); @@ -476,6 +482,12 @@ impl UnownedWindow { } } + pub fn set_menu(&self, menu: Option>) { + unsafe { + util::set_menu_async(*self.ns_window, menu); + } + } + pub fn set_visible(&self, visible: bool) { match visible { true => unsafe { util::make_key_and_order_front_async(*self.ns_window) }, diff --git a/src/window.rs b/src/window.rs index 079c711240..fe1ea83701 100644 --- a/src/window.rs +++ b/src/window.rs @@ -5,6 +5,7 @@ use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError}, event_loop::EventLoopWindowTarget, + menu::Menu, monitor::{MonitorHandle, VideoMode}, platform_impl, }; @@ -186,6 +187,11 @@ pub struct WindowAttributes { /// /// The default is `None`. pub window_icon: Option, + + /// The window menu. + /// + /// The default is `None`. + pub window_menu: Option>, } impl Default for WindowAttributes { @@ -205,6 +211,7 @@ impl Default for WindowAttributes { decorations: true, always_on_top: false, window_icon: None, + window_menu: None, } } } @@ -355,6 +362,17 @@ impl WindowBuilder { self } + /// Requests a specific title for the window. + /// + /// See [`Window::set_menu`] for details. + /// + /// [`Window::set_menu`]: crate::window::Window::set_menu + #[inline] + pub fn with_menu>>(mut self, menu: M) -> Self { + self.window.window_menu = Some(menu.into()); + self + } + /// Builds the window. /// /// Possible causes of error include denied permission, incompatible system, and lack of memory. @@ -747,6 +765,17 @@ impl Window { pub fn request_user_attention(&self, request_type: Option) { self.window.request_user_attention(request_type) } + + /// Modifies the menu of the window. + /// + /// ## Platform-specific + /// + /// - Only supported on macOS. + + #[inline] + pub fn set_menu(&self, menu: Option>) { + self.window.set_menu(menu) + } } /// Cursor functions. From a115ebcb98994825d8cdfca19da72a6d6efc7718 Mon Sep 17 00:00:00 2001 From: David Lemarier Date: Thu, 29 Apr 2021 12:30:50 -0400 Subject: [PATCH 2/3] Cleanup --- examples/custom_menu.rs | 19 +- src/menu.rs | 4 +- src/platform_impl/macos/menu.rs | 307 ++++++++++++++++---------------- 3 files changed, 157 insertions(+), 173 deletions(-) diff --git a/examples/custom_menu.rs b/examples/custom_menu.rs index 51021b97b6..e70b51e3ec 100644 --- a/examples/custom_menu.rs +++ b/examples/custom_menu.rs @@ -22,7 +22,7 @@ fn main() { vec![ MenuItem::About("Todos".to_string()), MenuItem::Separator, - MenuItem::new("Preferences".to_string()).key(","), + MenuItem::new("preferences".to_string(), "Preferences".to_string()).key(","), MenuItem::Separator, MenuItem::Services, MenuItem::Separator, @@ -34,11 +34,9 @@ fn main() { ], ), Menu::new( - "File", + "Custom menu", vec![ - MenuItem::new("Open/Show Window".to_string()).key("n"), - MenuItem::Separator, - MenuItem::new("Add Todo".to_string()).key("+"), + MenuItem::new("add_todo".to_string(), "Add Todo".to_string()).key("+"), MenuItem::Separator, MenuItem::CloseWindow, ], @@ -57,20 +55,11 @@ fn main() { ], ), Menu::new("View", vec![MenuItem::EnterFullScreen]), - Menu::new( - "Window", - vec![ - MenuItem::Minimize, - MenuItem::Zoom, - MenuItem::Separator, - MenuItem::new("Bring All to Front".to_string()), - ], - ), + Menu::new("Window", vec![MenuItem::Minimize, MenuItem::Zoom]), Menu::new("Help", vec![]), ]) .build(&event_loop) .unwrap(); - event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; diff --git a/src/menu.rs b/src/menu.rs index 6197743386..6239668131 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -15,6 +15,7 @@ impl Menu { #[derive(Debug, Clone)] pub struct CustomMenu { + pub id: String, pub name: String, pub key: Option, } @@ -86,8 +87,9 @@ pub enum MenuItem { } impl MenuItem { - pub fn new(title: String) -> Self { + pub fn new(unique_menu_id: String, title: String) -> Self { MenuItem::Custom(CustomMenu { + id: unique_menu_id, key: None, name: title, }) diff --git a/src/platform_impl/macos/menu.rs b/src/platform_impl/macos/menu.rs index de104602fd..1478c0f6aa 100644 --- a/src/platform_impl/macos/menu.rs +++ b/src/platform_impl/macos/menu.rs @@ -17,7 +17,7 @@ struct KeyEquivalent<'a> { masks: Option, } -struct Action(Box); +struct Action(Box); pub fn initialize(menu: Vec) { autoreleasepool(|| unsafe { @@ -34,169 +34,129 @@ pub fn initialize(menu: Vec) { // create menu for item in &menu.items { let item_obj: *mut Object = match item { - MenuItem::Custom(custom_menu) => { - let title = NSString::alloc(nil).init_str(custom_menu.name.as_str()); - make_menu_item(title, None, None) - } + MenuItem::Custom(custom_menu) => make_custom_menu_item( + custom_menu.id.clone(), + custom_menu.name.as_str(), + None, + None, + ), MenuItem::Separator => NSMenuItem::separatorItem(nil), MenuItem::About(app_name) => { let title = format!("About {}", app_name); - let title_alloc = NSString::alloc(nil).init_str(title.as_str()); make_menu_item( - title_alloc, + title.as_str(), Some(selector("orderFrontStandardAboutPanel:")), None, ) } - MenuItem::CloseWindow => { - let title = NSString::alloc(nil).init_str("Close Window"); - make_menu_item( - title, - Some(selector("performClose:")), - Some(KeyEquivalent { - key: "w", - masks: None, - }), - ) - } - MenuItem::Quit => { - let title = NSString::alloc(nil).init_str("Quit"); - make_menu_item( - title, - Some(selector("terminate:")), - Some(KeyEquivalent { - key: "q", - masks: None, - }), - ) - } - MenuItem::Hide => { - let title = NSString::alloc(nil).init_str("Hide"); - make_menu_item( - title, - Some(selector("hide:")), - Some(KeyEquivalent { - key: "h", - masks: None, - }), - ) - } - MenuItem::HideOthers => { - let title = NSString::alloc(nil).init_str("Hide Others"); - make_menu_item( - title, - Some(selector("hideOtherApplications:")), - Some(KeyEquivalent { - key: "h", - masks: Some( - NSEventModifierFlags::NSAlternateKeyMask - | NSEventModifierFlags::NSCommandKeyMask, - ), - }), - ) - } + MenuItem::CloseWindow => make_menu_item( + "Close Window", + Some(selector("performClose:")), + Some(KeyEquivalent { + key: "w", + masks: None, + }), + ), + MenuItem::Quit => make_menu_item( + "Quit", + Some(selector("terminate:")), + Some(KeyEquivalent { + key: "q", + masks: None, + }), + ), + MenuItem::Hide => make_menu_item( + "Hide", + Some(selector("hide:")), + Some(KeyEquivalent { + key: "h", + masks: None, + }), + ), + MenuItem::HideOthers => make_menu_item( + "Hide Others", + Some(selector("hideOtherApplications:")), + Some(KeyEquivalent { + key: "h", + masks: Some( + NSEventModifierFlags::NSAlternateKeyMask + | NSEventModifierFlags::NSCommandKeyMask, + ), + }), + ), MenuItem::ShowAll => { - let title = NSString::alloc(nil).init_str("Show All"); - make_menu_item(title, Some(selector("unhideAllApplications:")), None) - } - MenuItem::EnterFullScreen => { - let title = NSString::alloc(nil).init_str("Enter Full Screen"); - make_menu_item( - title, - Some(selector("toggleFullScreen:")), - Some(KeyEquivalent { - key: "h", - masks: Some( - NSEventModifierFlags::NSCommandKeyMask - | NSEventModifierFlags::NSControlKeyMask, - ), - }), - ) - } - MenuItem::Minimize => { - let title = NSString::alloc(nil).init_str("Minimize"); - make_menu_item( - title, - Some(selector("performMiniaturize:")), - Some(KeyEquivalent { - key: "m", - masks: None, - }), - ) - } - MenuItem::Zoom => { - let title = NSString::alloc(nil).init_str("Zoom"); - make_menu_item(title, Some(selector("performZoom:")), None) - } - MenuItem::Copy => { - let title = NSString::alloc(nil).init_str("Copy"); - make_menu_item( - title, - Some(selector("copy:")), - Some(KeyEquivalent { - key: "c", - masks: None, - }), - ) - } - MenuItem::Cut => { - let title = NSString::alloc(nil).init_str("Cut"); - make_menu_item( - title, - Some(selector("cut:")), - Some(KeyEquivalent { - key: "x", - masks: None, - }), - ) - } - MenuItem::Paste => { - let title = NSString::alloc(nil).init_str("Paste"); - make_menu_item( - title, - Some(selector("paste:")), - Some(KeyEquivalent { - key: "v", - masks: None, - }), - ) - } - MenuItem::Undo => { - let title = NSString::alloc(nil).init_str("Undo"); - make_menu_item( - title, - Some(selector("undo:")), - Some(KeyEquivalent { - key: "z", - masks: None, - }), - ) - } - MenuItem::Redo => { - let title = NSString::alloc(nil).init_str("Redo"); - make_menu_item( - title, - Some(selector("redo:")), - Some(KeyEquivalent { - key: "Z", - masks: None, - }), - ) - } - MenuItem::SelectAll => { - let title = NSString::alloc(nil).init_str("Select All"); - make_menu_item( - title, - Some(selector("selectAll:")), - Some(KeyEquivalent { - key: "a", - masks: None, - }), - ) + make_menu_item("Show All", Some(selector("unhideAllApplications:")), None) } + MenuItem::EnterFullScreen => make_menu_item( + "Enter Full Screen", + Some(selector("toggleFullScreen:")), + Some(KeyEquivalent { + key: "h", + masks: Some( + NSEventModifierFlags::NSCommandKeyMask + | NSEventModifierFlags::NSControlKeyMask, + ), + }), + ), + MenuItem::Minimize => make_menu_item( + "Minimize", + Some(selector("performMiniaturize:")), + Some(KeyEquivalent { + key: "m", + masks: None, + }), + ), + MenuItem::Zoom => make_menu_item("Zoom", Some(selector("performZoom:")), None), + MenuItem::Copy => make_menu_item( + "Copy", + Some(selector("copy:")), + Some(KeyEquivalent { + key: "c", + masks: None, + }), + ), + MenuItem::Cut => make_menu_item( + "Cut", + Some(selector("cut:")), + Some(KeyEquivalent { + key: "x", + masks: None, + }), + ), + MenuItem::Paste => make_menu_item( + "Paste", + Some(selector("paste:")), + Some(KeyEquivalent { + key: "v", + masks: None, + }), + ), + MenuItem::Undo => make_menu_item( + "Undo", + Some(selector("undo:")), + Some(KeyEquivalent { + key: "z", + masks: None, + }), + ), + MenuItem::Redo => make_menu_item( + "Redo", + Some(selector("redo:")), + Some(KeyEquivalent { + key: "Z", + masks: None, + }), + ), + MenuItem::SelectAll => make_menu_item( + "Select All", + Some(selector("selectAll:")), + Some(KeyEquivalent { + key: "a", + masks: None, + }), + ), MenuItem::Services => { - let title = NSString::alloc(nil).init_str("Services"); - let item = make_menu_item(title, None, None); + let item = make_menu_item("Services", None, None); let app_class = class!(NSApplication); let app: id = msg_send![app_class, sharedApplication]; let services: id = msg_send![app, servicesMenu]; @@ -217,7 +177,42 @@ pub fn initialize(menu: Vec) { }); } +fn make_menu_alloc() -> *mut Object { + unsafe { msg_send![make_menu_item_class(), alloc] } +} + +fn make_custom_menu_item( + id: String, + title: &str, + selector: Option, + key_equivalent: Option>, +) -> *mut Object { + let alloc = make_menu_alloc(); + let menu_id = Box::new(Action(Box::new(id))); + let ptr = Box::into_raw(menu_id); + + unsafe { + (&mut *alloc).set_ivar(BLOCK_PTR, ptr as usize); + let _: () = msg_send![&*alloc, setTarget:&*alloc]; + let title = NSString::alloc(nil).init_str(title); + make_menu_item_from_alloc(alloc, title, selector, key_equivalent) + } +} + fn make_menu_item( + title: &str, + selector: Option, + key_equivalent: Option>, +) -> *mut Object { + let alloc = make_menu_alloc(); + unsafe { + let title = NSString::alloc(nil).init_str(title); + make_menu_item_from_alloc(alloc, title, selector, key_equivalent) + } +} + +fn make_menu_item_from_alloc( + alloc: *mut Object, title: *mut Object, selector: Option, key_equivalent: Option>, @@ -231,11 +226,10 @@ fn make_menu_item( // menu so fire our handler let selector = match selector { Some(selector) => selector, - None => sel!(fireBlockAction:), + None => sel!(fireCustomMenuAction:), }; // allocate our item to our class - let alloc: id = msg_send![make_menu_item_class(), alloc]; let item = NSMenuItem::alloc(alloc).initWithTitle_action_keyEquivalent_(title, selector, key); @@ -261,7 +255,7 @@ fn make_menu_item_class() -> *const Class { dealloc_custom_menuitem as extern "C" fn(&Object, _), ); decl.add_method( - sel!(fireBlockAction:), + sel!(fireCustomMenuAction:), fire_custom_menu_click as extern "C" fn(&Object, _, id), ); @@ -276,7 +270,6 @@ extern "C" fn fire_custom_menu_click(this: &Object, _: Sel, _item: id) { } extern "C" fn dealloc_custom_menuitem(this: &Object, _: Sel) { - println!("DEALOC"); unsafe { let ptr: usize = *this.get_ivar(BLOCK_PTR); let obj = ptr as *mut Action; From d6a7fd8499ac66327f4c0cd55333b77f388ec0d3 Mon Sep 17 00:00:00 2001 From: David Lemarier Date: Thu, 29 Apr 2021 13:14:39 -0400 Subject: [PATCH 3/3] Fix custom menu and implement basic event loop handler --- examples/custom_menu.rs | 5 +++++ src/event.rs | 6 ++++++ src/platform_impl/macos/menu.rs | 21 ++++++++++++--------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/examples/custom_menu.rs b/examples/custom_menu.rs index e70b51e3ec..ba515988a3 100644 --- a/examples/custom_menu.rs +++ b/examples/custom_menu.rs @@ -60,6 +60,7 @@ fn main() { ]) .build(&event_loop) .unwrap(); + event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; @@ -71,6 +72,10 @@ fn main() { Event::MainEventsCleared => { window.request_redraw(); } + Event::MenuEvent(menu_id) => { + println!("Clicked on {}", menu_id); + window.set_title("New window title!"); + } _ => (), } }); diff --git a/src/event.rs b/src/event.rs index f39ddaac50..63df286f3c 100644 --- a/src/event.rs +++ b/src/event.rs @@ -70,6 +70,9 @@ pub enum Event<'a, T: 'static> { /// Emitted when an event is sent from [`EventLoopProxy::send_event`](crate::event_loop::EventLoopProxy::send_event) UserEvent(T), + /// Emitted when a menu has been clicked. + MenuEvent(String), + /// Emitted when the application has been suspended. Suspended, @@ -138,6 +141,7 @@ impl Clone for Event<'static, T> { LoopDestroyed => LoopDestroyed, Suspended => Suspended, Resumed => Resumed, + MenuEvent(event) => MenuEvent(event.clone()), } } } @@ -156,6 +160,7 @@ impl<'a, T> Event<'a, T> { LoopDestroyed => Ok(LoopDestroyed), Suspended => Ok(Suspended), Resumed => Ok(Resumed), + MenuEvent(event) => Ok(MenuEvent(event)), } } @@ -176,6 +181,7 @@ impl<'a, T> Event<'a, T> { LoopDestroyed => Some(LoopDestroyed), Suspended => Some(Suspended), Resumed => Some(Resumed), + MenuEvent(event) => Some(MenuEvent(event)), } } } diff --git a/src/platform_impl/macos/menu.rs b/src/platform_impl/macos/menu.rs index 1478c0f6aa..8b13bc62ff 100644 --- a/src/platform_impl/macos/menu.rs +++ b/src/platform_impl/macos/menu.rs @@ -8,15 +8,18 @@ use objc::{ }; use std::sync::Once; +use crate::event::Event; use crate::menu::{Menu, MenuItem}; +use super::{app_state::AppState, event::EventWrapper}; + static BLOCK_PTR: &'static str = "winitMenuItemBlockPtr"; struct KeyEquivalent<'a> { key: &'a str, masks: Option, } - +#[derive(Debug)] struct Action(Box); pub fn initialize(menu: Vec) { @@ -230,9 +233,7 @@ fn make_menu_item_from_alloc( }; // allocate our item to our class - let item = - NSMenuItem::alloc(alloc).initWithTitle_action_keyEquivalent_(title, selector, key); - + let item: id = msg_send![alloc, initWithTitle:&*title action:selector keyEquivalent:&*key]; if let Some(masks) = masks { item.setKeyEquivalentModifierMask_(masks) } @@ -266,20 +267,22 @@ fn make_menu_item_class() -> *const Class { } extern "C" fn fire_custom_menu_click(this: &Object, _: Sel, _item: id) { - println!("CLICK"); + let menu_id = unsafe { + let ptr: usize = *this.get_ivar(BLOCK_PTR); + let obj = ptr as *const Action; + &*obj + }; + let event = Event::MenuEvent(menu_id.0.to_string()); + AppState::queue_event(EventWrapper::StaticEvent(event)); } extern "C" fn dealloc_custom_menuitem(this: &Object, _: Sel) { unsafe { let ptr: usize = *this.get_ivar(BLOCK_PTR); let obj = ptr as *mut Action; - println!("Action {:?}", obj); - if !obj.is_null() { let _handler = Box::from_raw(obj); } - - //let _: () = msg_send![this, setTarget:nil]; let _: () = msg_send![super(this, class!(NSMenuItem)), dealloc]; } }