diff --git a/Cargo.toml b/Cargo.toml index 640059e..b1f25da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,5 +32,5 @@ cocoa = "0.24" objc = "0.2" [dev-dependencies] -winit = "0.26" +winit = { git = "https://github.com/rust-windowing/winit" } tao = { git = "https://github.com/tauri-apps/tao", branch = "muda/disable-gtk-menu-creation" } diff --git a/examples/tao.rs b/examples/tao.rs index d6f4104..0509b48 100644 --- a/examples/tao.rs +++ b/examples/tao.rs @@ -16,16 +16,16 @@ fn main() { let window2 = WindowBuilder::new().build(&event_loop).unwrap(); let mut menu_bar = Menu::new(); - let mut file_menu = menu_bar.add_submenu("File", true); - let mut edit_menu = menu_bar.add_submenu("Edit", true); + let mut file_menu = menu_bar.add_submenu("&File", true); + let mut edit_menu = menu_bar.add_submenu("&Edit", true); - let mut open_item = file_menu.add_text_item("Open", true); + let mut open_item = file_menu.add_text_item("&Open", true, None); - let mut save_item = file_menu.add_text_item("Save", true); - let _quit_item = file_menu.add_text_item("Quit", true); + let mut save_item = file_menu.add_text_item("&Save", true, Some("CommandOrCtrl+S")); + let _quit_item = file_menu.add_text_item("&Quit", true, None); - let _copy_item = edit_menu.add_text_item("Copy", true); - let _cut_item = edit_menu.add_text_item("Cut", true); + let _copy_item = edit_menu.add_text_item("&Copy", true, None); + let _cut_item = edit_menu.add_text_item("C&ut", true, None); #[cfg(target_os = "windows")] { @@ -55,7 +55,7 @@ fn main() { _ if event.id == save_item.id() => { println!("Save menu item activated!"); counter += 1; - save_item.set_label(format!("Save activated {counter} times")); + save_item.set_label(format!("&Save activated {counter} times")); if !open_item_disabled { println!("Open item disabled!"); diff --git a/examples/winit.rs b/examples/winit.rs index 2f7e321..20b254a 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -1,29 +1,49 @@ use muda::{menu_event_receiver, Menu}; +#[cfg(target_os = "macos")] +use winit::platform::macOS::EventLoopExtMacOS; #[cfg(target_os = "windows")] -use winit::platform::windows::WindowExtWindows; +use winit::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows}; use winit::{ event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, + event_loop::{ControlFlow, EventLoopBuilder}, window::WindowBuilder, }; fn main() { - let event_loop = EventLoop::new(); + let mut event_loop_builder = EventLoopBuilder::new(); + + let mut menu_bar = Menu::new(); + let menu_bar_c = menu_bar.clone(); + + #[cfg(target_os = "windows")] + { + event_loop_builder.with_msg_hook(move |msg| { + use windows_sys::Win32::UI::WindowsAndMessaging::{TranslateAcceleratorW, MSG}; + unsafe { + let msg = msg as *mut MSG; + let translated = TranslateAcceleratorW((*msg).hwnd, menu_bar_c.haccel(), msg); + translated == 1 + } + }); + } + #[allow(unused_mut)] + let mut event_loop = event_loop_builder.build(); + #[cfg(target_os = "macos")] + event_loop.enable_default_menu_creation(false); let window = WindowBuilder::new().build(&event_loop).unwrap(); let _window2 = WindowBuilder::new().build(&event_loop).unwrap(); - let mut menu_bar = Menu::new(); - let mut file_menu = menu_bar.add_submenu("File", true); - let mut edit_menu = menu_bar.add_submenu("Edit", true); + let mut file_menu = menu_bar.add_submenu("&File", true); + let mut edit_menu = menu_bar.add_submenu("&Edit", true); - let mut open_item = file_menu.add_text_item("Open", true); + let mut open_item = file_menu.add_text_item("&Open", true, Some("Ctrl+O")); - let mut save_item = file_menu.add_text_item("Save", true); - let _quit_item = file_menu.add_text_item("Quit", true); + let mut save_item = file_menu.add_text_item("&Save", true, Some("CommandOrCtrl+S")); + let _quit_item = file_menu.add_text_item("&Quit", true, None); - let _copy_item = edit_menu.add_text_item("Copy", true); - let _cut_item = edit_menu.add_text_item("Cut", true); + let _copy_item = edit_menu.add_text_item("&Copy", true, Some("Ctrl+C")); + let _cut_item = edit_menu.add_text_item("C&ut", true, None); #[cfg(target_os = "windows")] { @@ -43,7 +63,7 @@ fn main() { _ if event.id == save_item.id() => { println!("Save menu item activated!"); counter += 1; - save_item.set_label(format!("Save activated {counter} times")); + save_item.set_label(format!("&Save activated {counter} times")); if !open_item_disabled { println!("Open item disabled!"); @@ -59,7 +79,7 @@ fn main() { #[cfg(target_os = "macos")] Event::NewEvents(winit::event::StartCause::Init) => { menu_bar.init_for_nsapp(); - }, + } Event::WindowEvent { event: WindowEvent::CloseRequested, .. diff --git a/src/counter.rs b/src/counter.rs new file mode 100644 index 0000000..8408a9d --- /dev/null +++ b/src/counter.rs @@ -0,0 +1,23 @@ +#![allow(unused)] + +use std::sync::atomic::{AtomicU64, Ordering}; + +pub struct Counter(AtomicU64); + +impl Counter { + pub const fn new() -> Self { + Self(AtomicU64::new(1)) + } + + pub const fn new_with_start(start: u64) -> Self { + Self(AtomicU64::new(start)) + } + + pub fn next(&self) -> u64 { + self.0.fetch_add(1, Ordering::Relaxed) + } + + pub fn current(&self) -> u64 { + self.0.load(Ordering::Relaxed) + } +} diff --git a/src/lib.rs b/src/lib.rs index 6e476c4..9252ea2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,8 +62,8 @@ use crossbeam_channel::{unbounded, Receiver, Sender}; use once_cell::sync::Lazy; +mod counter; mod platform_impl; -mod util; static MENU_CHANNEL: Lazy<(Sender, Receiver)> = Lazy::new(|| unbounded()); @@ -93,6 +93,7 @@ pub struct MenuEvent { /// let file_menu = menu.add_submenu("File", true); /// let edit_menu = menu.add_submenu("Edit", true); /// ``` +#[derive(Clone)] pub struct Menu(platform_impl::Menu); impl Menu { @@ -102,6 +103,13 @@ impl Menu { } /// Creates a new [`Submenu`] whithin this menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: The menu label can containt `&` to indicate which letter should get a generated accelerator. + /// For example, using `&File` for the File menu would result in the label gets an underline under the `F`, + /// and the `&` character is not displayed on menu label. + /// Then the menu can be activated by press `Alt+F`. pub fn add_submenu(&mut self, label: impl AsRef, enabled: bool) -> Submenu { Submenu(self.0.add_submenu(label, enabled)) } @@ -119,16 +127,47 @@ impl Menu { pub fn init_for_gtk_window(&self, w: &W) -> std::rc::Rc where W: gtk::prelude::IsA, + W: gtk::prelude::IsA, { self.0.init_for_gtk_window(w) } /// Adds this menu to a win32 window. + /// + /// ## Note about accelerators: + /// + /// For accelerators to work, the event loop needs to call + /// [`TranslateAcceleratorW`](windows_sys::Win32::UI::WindowsAndMessaging::TranslateAcceleratorW) + /// with the [`HACCEL`](windows_sys::Win32::UI::WindowsAndMessaging::HACCEL) returned from [`Menu::haccel`] + /// + /// #### Example: + /// ``` + /// # use windows_sys::Win32::UI::WindowsAndMessaging::{MSG, GetMessageW, TranslateMessage, DispatchMessageW }; + /// let menu = Menu::new(); + /// unsafe { + /// let msg: MSG = std::mem::zeroed(); + /// while GetMessageW(&mut msg, 0, 0, 0) == 1 { + /// let translated = TranslateAcceleratorW(msg.hwnd, menu.haccel(), msg); + /// if !translated { + /// TranslateMessage(&msg); + /// DispatchMessageW(&msg); + /// } + /// } + /// } + /// ``` #[cfg(target_os = "windows")] pub fn init_for_hwnd(&self, hwnd: isize) { self.0.init_for_hwnd(hwnd) } + /// Returns The [`HACCEL`](windows_sys::Win32::UI::WindowsAndMessaging::HACCEL) associated with this menu + /// It can be used with [`TranslateAcceleratorW`](windows_sys::Win32::UI::WindowsAndMessaging::TranslateAcceleratorW) + /// in the event loop to enable accelerators + #[cfg(target_os = "windows")] + pub fn haccel(&self) -> windows_sys::Win32::UI::WindowsAndMessaging::HACCEL { + self.0.haccel() + } + /// Adds this menu to NSApp. #[cfg(target_os = "macos")] pub fn init_for_nsapp(&self) { @@ -136,8 +175,8 @@ impl Menu { } } -#[derive(Clone)] /// This is a submenu within another [`Submenu`] or [`Menu`]. +#[derive(Clone)] pub struct Submenu(platform_impl::Submenu); impl Submenu { @@ -162,13 +201,32 @@ impl Submenu { } /// Creates a new [`Submenu`] whithin this submenu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: The menu label can containt `&` to indicate which letter should get a generated accelerator. + /// For example, using `&File` for the File menu would result in the label gets an underline under the `F`, + /// and the `&` character is not displayed on menu label. + /// Then the menu can be activated by press `F` when its parent menu is active. pub fn add_submenu(&mut self, label: impl AsRef, enabled: bool) -> Submenu { Submenu(self.0.add_submenu(label, enabled)) } /// Creates a new [`TextMenuItem`] whithin this submenu. - pub fn add_text_item(&mut self, label: impl AsRef, enabled: bool) -> TextMenuItem { - TextMenuItem(self.0.add_text_item(label, enabled)) + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: The menu item label can containt `&` to indicate which letter should get a generated accelerator. + /// For example, using `&Save` for the save menu item would result in the label gets an underline under the `S`, + /// and the `&` character is not displayed on menu item label. + /// Then the menu item can be activated by press `S` when its parent menu is active. + pub fn add_text_item( + &mut self, + label: impl AsRef, + enabled: bool, + accelerator: Option<&str>, + ) -> TextMenuItem { + TextMenuItem(self.0.add_text_item(label, enabled, accelerator)) } } diff --git a/src/platform_impl/linux/accelerator.rs b/src/platform_impl/linux/accelerator.rs new file mode 100644 index 0000000..4890a47 --- /dev/null +++ b/src/platform_impl/linux/accelerator.rs @@ -0,0 +1,44 @@ +pub fn to_gtk_menemenoic(string: impl AsRef) -> String { + string + .as_ref() + .replace("&&", "[~~]") + .replace("&", "_") + .replace("[~~]", "&&") +} + +pub fn to_gtk_accelerator(accelerator: impl AsRef) -> String { + let accelerator = accelerator.as_ref(); + let mut s = accelerator.split("+"); + let count = s.clone().count(); + let (mod1, mod2, key) = { + if count == 2 { + (s.next().unwrap(), None, s.next().unwrap()) + } else if count == 3 { + ( + s.next().unwrap(), + Some(s.next().unwrap()), + s.next().unwrap(), + ) + } else { + panic!("Unsupported accelerator format: {}", accelerator) + } + }; + + let mut gtk_accelerator = parse_mod(mod1).to_string(); + if let Some(mod2) = mod2 { + gtk_accelerator.push_str(parse_mod(mod2)); + } + gtk_accelerator.push_str(key); + + gtk_accelerator +} + +fn parse_mod(modifier: &str) -> &str { + match modifier.to_uppercase().as_str() { + "SHIFT" => "", + "CONTROL" | "CTRL" | "COMMAND" | "COMMANDORCONTROL" | "COMMANDORCTRL" => "", + "ALT" => "", + "SUPER" | "META" | "WIN" => "", + _ => panic!("Unsupported modifier: {}", modifier), + } +} diff --git a/src/platform_impl/linux.rs b/src/platform_impl/linux/mod.rs similarity index 81% rename from src/platform_impl/linux.rs rename to src/platform_impl/linux/mod.rs index 0d5dbcb..a16f33b 100644 --- a/src/platform_impl/linux.rs +++ b/src/platform_impl/linux/mod.rs @@ -1,11 +1,14 @@ -#![cfg(target_os = "linux")] +mod accelerator; -use crate::util::Counter; +use crate::counter::Counter; use gtk::{prelude::*, Orientation}; use std::{cell::RefCell, rc::Rc}; +use self::accelerator::{to_gtk_accelerator, to_gtk_menemenoic}; + static COUNTER: Counter = Counter::new(); +#[derive(PartialEq, Eq)] enum MenuEntryType { Submenu, Text, @@ -17,6 +20,7 @@ struct MenuEntry { enabled: bool, r#type: MenuEntryType, item_id: Option, + accelerator: Option, // NOTE(amrbashir): because gtk doesn't allow using the same `gtk::MenuItem` // multiple times, and thus can't be used in multiple windows, each entry // keeps a vector of a `gtk::MenuItem` or a tuple of `gtk::MenuItem` and `gtk::Menu` @@ -33,8 +37,10 @@ struct InnerMenu { // keeps a vector of a tuple of `gtk::MenuBar` and `gtk::Box` // and push to it every time `Menu::init_for_gtk_window` is called. gtk_items: Vec<(gtk::MenuBar, Rc)>, + accel_group: gtk::AccelGroup, } +#[derive(Clone)] pub struct Menu(Rc>); impl Menu { @@ -42,6 +48,7 @@ impl Menu { Self(Rc::new(RefCell::new(InnerMenu { entries: Vec::new(), gtk_items: Vec::new(), + accel_group: gtk::AccelGroup::new(), }))) } @@ -53,6 +60,7 @@ impl Menu { entries: Some(Vec::new()), r#type: MenuEntryType::Submenu, item_id: None, + accelerator: None, menu_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))), item_gtk_items: None, })); @@ -63,9 +71,12 @@ impl Menu { pub fn init_for_gtk_window(&self, w: &W) -> Rc where W: IsA, + W: IsA, { + let mut inner = self.0.borrow_mut(); let menu_bar = gtk::MenuBar::new(); - add_entries_to_menu(&menu_bar, &self.0.borrow().entries); + add_entries_to_menu(&menu_bar, &inner.entries, &inner.accel_group); + w.add_accel_group(&inner.accel_group); let vbox = gtk::Box::new(Orientation::Vertical, 0); vbox.pack_start(&menu_bar, false, false, 0); @@ -75,7 +86,7 @@ impl Menu { let vbox = Rc::new(vbox); let vbox_c = Rc::clone(&vbox); - self.0.borrow_mut().gtk_items.push((menu_bar, vbox)); + inner.gtk_items.push((menu_bar, vbox)); vbox_c } @@ -84,16 +95,17 @@ impl Menu { fn add_entries_to_menu>( gtk_menu: &M, entries: &Vec>>, + accel_group: >k::AccelGroup, ) { for entry in entries { let mut entry = entry.borrow_mut(); - let gtk_item = gtk::MenuItem::with_label(&entry.label); + let gtk_item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(&entry.label)); gtk_menu.append(>k_item); gtk_item.set_sensitive(entry.enabled); - if let MenuEntryType::Submenu = entry.r#type { + if entry.r#type == MenuEntryType::Submenu { let gtk_menu = gtk::Menu::new(); gtk_item.set_submenu(Some(>k_menu)); - add_entries_to_menu(>k_menu, entry.entries.as_ref().unwrap()); + add_entries_to_menu(>k_menu, entry.entries.as_ref().unwrap(), accel_group); entry .menu_gtk_items .as_mut() @@ -101,6 +113,17 @@ fn add_entries_to_menu>( .borrow_mut() .push((gtk_item, gtk_menu)); } else { + if let Some(accelerator) = &entry.accelerator { + let (key, modifiers) = gtk::accelerator_parse(&to_gtk_accelerator(accelerator)); + gtk_item.add_accelerator( + "activate", + accel_group, + key, + modifiers, + gtk::AccelFlags::VISIBLE, + ); + } + let id = entry.item_id.unwrap_or_default(); gtk_item.connect_activate(move |_| { let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); @@ -151,6 +174,7 @@ impl Submenu { entries: Some(Vec::new()), r#type: MenuEntryType::Submenu, item_id: None, + accelerator: None, menu_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))), item_gtk_items: None, })); @@ -163,13 +187,19 @@ impl Submenu { Submenu(entry) } - pub fn add_text_item(&mut self, label: impl AsRef, enabled: bool) -> TextMenuItem { + pub fn add_text_item( + &mut self, + label: impl AsRef, + enabled: bool, + accelerator: Option<&str>, + ) -> TextMenuItem { let entry = Rc::new(RefCell::new(MenuEntry { - label: label.as_ref().to_string(), + label: to_gtk_menemenoic(label), enabled, entries: None, r#type: MenuEntryType::Text, item_id: Some(COUNTER.next()), + accelerator: accelerator.map(|s| s.to_string()), menu_gtk_items: None, item_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))), })); diff --git a/src/platform_impl/macos/menu_item.rs b/src/platform_impl/macos/menu_item.rs index 942bc92..f8db622 100644 --- a/src/platform_impl/macos/menu_item.rs +++ b/src/platform_impl/macos/menu_item.rs @@ -1,3 +1,4 @@ +use crate::counter::Counter; use cocoa::{ appkit::NSButton, base::{id, nil, BOOL, NO, YES}, @@ -17,43 +18,11 @@ use std::{ hash::{Hash, Hasher}, }; -/// Identifier of a custom menu item. -/// -/// Whenever you receive an event arising from a particular menu, this event contains a `MenuId` which -/// identifies its origin. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct MenuId(pub u64); - -impl From for u64 { - fn from(s: MenuId) -> u64 { - s.0 - } -} - -impl MenuId { - /// Return an empty `MenuId`. - pub const EMPTY: MenuId = MenuId(0); - - /// Create new `MenuId` from a String. - pub fn new(unique_string: &str) -> MenuId { - MenuId(hash_string_to_u64(unique_string)) - } - - /// Whenever this menu is empty. - pub fn is_empty(self) -> bool { - Self::EMPTY == self - } -} - -fn hash_string_to_u64(title: &str) -> u64 { - let mut s = DefaultHasher::new(); - title.to_uppercase().hash(&mut s); - s.finish() as u64 -} +static COUNTER: Counter = Counter::new(); #[derive(Debug, Clone)] pub struct TextMenuItem { - pub(crate) id: MenuId, + pub(crate) id: u64, pub(crate) ns_menu_item: id, } @@ -62,7 +31,7 @@ impl TextMenuItem { let (id, ns_menu_item) = make_menu_item(label.as_ref(), selector); unsafe { - (&mut *ns_menu_item).set_ivar(MENU_IDENTITY, id.0); + (&mut *ns_menu_item).set_ivar(MENU_IDENTITY, id); let () = msg_send![&*ns_menu_item, setTarget:&*ns_menu_item]; if !enabled { @@ -108,7 +77,7 @@ impl TextMenuItem { } pub fn id(&self) -> u64 { - self.id.0 + self.id } } @@ -119,7 +88,7 @@ pub fn make_menu_item( //menu_type: MenuType, ) -> (MenuId, *mut Object) { let alloc = make_menu_item_alloc(); - let menu_id = MenuId::new(title); + let menu_id = COUNTER.next(); unsafe { let title = NSString::alloc(nil).init_str(title); diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index b96806d..58e6900 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -23,11 +23,7 @@ impl Menu { pub fn add_submenu(&mut self, label: impl AsRef, enabled: bool) -> Submenu { let menu = Menu::new(); - let menu_item = TextMenuItem::new( - "", - enabled, - sel!(fireMenubarAction:), - ); + let menu_item = TextMenuItem::new("", enabled, sel!(fireMenubarAction:)); unsafe { menu_item.ns_menu_item.setSubmenu_(menu.0); diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index 2280d10..baf3dd8 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -1,10 +1,10 @@ pub use self::platform_impl::*; #[cfg(target_os = "windows")] -#[path = "windows.rs"] +#[path = "windows/mod.rs"] mod platform_impl; #[cfg(target_os = "linux")] -#[path = "linux.rs"] +#[path = "linux/mod.rs"] mod platform_impl; #[cfg(target_os = "macos")] #[path = "macos/mod.rs"] diff --git a/src/platform_impl/windows/accelerator.rs b/src/platform_impl/windows/accelerator.rs new file mode 100644 index 0000000..65d29f5 --- /dev/null +++ b/src/platform_impl/windows/accelerator.rs @@ -0,0 +1,115 @@ +use windows_sys::Win32::UI::WindowsAndMessaging::{FALT, FCONTROL, FSHIFT, FVIRTKEY}; + +/// Returns a tuple of (Key, Modifier, a string representation to be used in menu items) +pub fn parse_accelerator(accelerator: impl AsRef) -> (u16, u32, String) { + let accelerator = accelerator.as_ref(); + let mut s = accelerator.split("+"); + let count = s.clone().count(); + let (mod1, mod2, key) = { + if count == 2 { + (s.next().unwrap(), None, s.next().unwrap()) + } else if count == 3 { + ( + s.next().unwrap(), + Some(s.next().unwrap()), + s.next().unwrap(), + ) + } else { + panic!("Unsupported accelerator format: {}", accelerator) + } + }; + + let mut accel_str = String::new(); + let mut mods_vk = FVIRTKEY; + + let (mod1_vk, mod1_str) = parse_mod(mod1); + accel_str.push_str(mod1_str); + accel_str.push_str("+"); + mods_vk |= mod1_vk; + if let Some(mod2) = mod2 { + let (mod2_vk, mod2_str) = parse_mod(mod2); + accel_str.push_str(mod2_str); + accel_str.push_str("+"); + mods_vk |= mod2_vk; + } + let (key_vk, key_str) = parse_key(key); + accel_str.push_str(key_str); + + (key_vk, mods_vk, accel_str) +} + +fn parse_mod(modifier: &str) -> (u32, &str) { + match modifier.to_uppercase().as_str() { + "SHIFT" => (FSHIFT, "Shift"), + "CONTROL" | "CTRL" | "COMMAND" | "COMMANDORCONTROL" | "COMMANDORCTRL" => (FCONTROL, "Ctrl"), + "ALT" => (FALT, "Alt"), + _ => panic!("Unsupported modifier: {}", modifier), + } +} + +fn parse_key(key: &str) -> (u16, &str) { + match key.to_uppercase().as_str() { + "SPACE" => (0x20, "Space"), + "BACKSPACE" => (0x08, "Backspace"), + "TAB" => (0x09, "Tab"), + "ENTER" | "RETURN" => (0x0D, "Enter"), + "CAPSLOCK" => (0x14, "Caps Lock"), + "ESC" | "ESCAPE" => (0x1B, "Esc"), + "PAGEUP" => (0x21, "Page Up"), + "PAGEDOWN" => (0x22, "Page Down"), + "END" => (0x23, "End"), + "HOME" => (0x24, "Home"), + "LEFTARROW" => (0x25, "Left Arrow"), + "UPARROW" => (0x26, "Up Arrow"), + "RIGHTARROW" => (0x27, "Right Arrow"), + "DOWNARROW" => (0x28, "Down Arrow"), + "DELETE" => (0x2E, "Del"), + "0" => (0x30, "0"), + "1" => (0x31, "1"), + "2" => (0x32, "2"), + "3" => (0x33, "3"), + "4" => (0x34, "4"), + "5" => (0x35, "5"), + "6" => (0x36, "6"), + "7" => (0x37, "7"), + "8" => (0x38, "8"), + "9" => (0x39, "9"), + "A" => (0x41, "A"), + "B" => (0x42, "B"), + "C" => (0x43, "C"), + "D" => (0x44, "D"), + "E" => (0x45, "E"), + "F" => (0x46, "F"), + "G" => (0x47, "G"), + "H" => (0x48, "H"), + "I" => (0x49, "I"), + "J" => (0x4A, "J"), + "K" => (0x4B, "K"), + "L" => (0x4C, "L"), + "M" => (0x4D, "M"), + "N" => (0x4E, "N"), + "O" => (0x4F, "O"), + "P" => (0x50, "P"), + "Q" => (0x51, "Q"), + "R" => (0x52, "R"), + "S" => (0x53, "S"), + "T" => (0x54, "T"), + "U" => (0x55, "U"), + "V" => (0x56, "V"), + "W" => (0x57, "W"), + "X" => (0x58, "X"), + "Y" => (0x59, "Y"), + "Z" => (0x5A, "Z"), + "NUM0" | "NUMPAD0" => (0x60, "Num 0"), + "NUM1" | "NUMPAD1" => (0x61, "Num 1"), + "NUM2" | "NUMPAD2" => (0x62, "Num 2"), + "NUM3" | "NUMPAD3" => (0x63, "Num 3"), + "NUM4" | "NUMPAD4" => (0x64, "Num 4"), + "NUM5" | "NUMPAD5" => (0x65, "Num 5"), + "NUM6" | "NUMPAD6" => (0x66, "Num 6"), + "NUM7" | "NUMPAD7" => (0x67, "Num 7"), + "NUM8" | "NUMPAD8" => (0x68, "Num 8"), + "NUM9" | "NUMPAD9" => (0x69, "Num 9"), + _ => panic!("Unsupported modifier: {}", key), + } +} diff --git a/src/platform_impl/windows.rs b/src/platform_impl/windows/mod.rs similarity index 66% rename from src/platform_impl/windows.rs rename to src/platform_impl/windows/mod.rs index 731c8c1..2fbf86b 100644 --- a/src/platform_impl/windows.rs +++ b/src/platform_impl/windows/mod.rs @@ -1,25 +1,43 @@ #![cfg(target_os = "windows")] -use crate::util::{decode_wide, encode_wide, Counter, LOWORD}; +mod accelerator; +mod util; + +use crate::counter::Counter; +use std::{cell::RefCell, rc::Rc}; +use util::{decode_wide, encode_wide, LOWORD}; use windows_sys::Win32::{ Foundation::{HWND, LPARAM, LRESULT, WPARAM}, UI::{ Shell::{DefSubclassProc, SetWindowSubclass}, WindowsAndMessaging::{ - AppendMenuW, CreateMenu, EnableMenuItem, GetMenuItemInfoW, SetMenu, SetMenuItemInfoW, - MENUITEMINFOW, MFS_DISABLED, MF_DISABLED, MF_ENABLED, MF_GRAYED, MF_POPUP, MIIM_STATE, - MIIM_STRING, WM_COMMAND, + AppendMenuW, CreateAcceleratorTableW, CreateMenu, EnableMenuItem, GetMenuItemInfoW, + SetMenu, SetMenuItemInfoW, ACCEL, HACCEL, HMENU, MENUITEMINFOW, MFS_DISABLED, + MF_DISABLED, MF_ENABLED, MF_GRAYED, MF_POPUP, MIIM_STATE, MIIM_STRING, WM_COMMAND, }, }, }; -static COUNTER: Counter = Counter::new(); +use self::accelerator::parse_accelerator; -pub struct Menu(isize); +static COUNTER: Counter = Counter::new_with_start(563); + +struct InnerMenu { + hmenu: HMENU, + accelerators: Vec, + haccel: HACCEL, +} + +#[derive(Clone)] +pub struct Menu(Rc>); impl Menu { pub fn new() -> Self { - Self(unsafe { CreateMenu() }) + Self(Rc::new(RefCell::new(InnerMenu { + hmenu: unsafe { CreateMenu() }, + accelerators: Vec::new(), + haccel: 0, + }))) } pub fn add_submenu(&mut self, label: impl AsRef, enabled: bool) -> Submenu { @@ -30,7 +48,7 @@ impl Menu { } unsafe { AppendMenuW( - self.0, + self.0.borrow().hmenu, flags, hmenu as _, encode_wide(label.as_ref()).as_ptr(), @@ -38,22 +56,35 @@ impl Menu { }; Submenu { hmenu, - parent_hmenu: self.0, + parent_hmenu: self.0.borrow().hmenu, + parent_menu: self.clone(), } } pub fn init_for_hwnd(&self, hwnd: isize) { unsafe { - SetMenu(hwnd, self.0); + SetMenu(hwnd, self.0.borrow().hmenu); SetWindowSubclass(hwnd, Some(menu_subclass_proc), 22, 0); }; } + + pub fn haccel(&self) -> HACCEL { + self.0.borrow().haccel + } + + fn update_haccel(&mut self) { + let mut inner = self.0.borrow_mut(); + inner.haccel = unsafe { + CreateAcceleratorTableW(inner.accelerators.as_ptr(), inner.accelerators.len() as _) + }; + } } #[derive(Clone)] pub struct Submenu { - hmenu: isize, - parent_hmenu: isize, + hmenu: HMENU, + parent_hmenu: HMENU, + parent_menu: Menu, } impl Submenu { @@ -72,6 +103,7 @@ impl Submenu { unsafe { GetMenuItemInfoW(self.parent_hmenu, self.hmenu as _, false.into(), &mut info) }; + // TOOD: check if it returns the label containing an ambersand and make gtk comply to that decode_wide(info.dwTypeData) } @@ -121,23 +153,41 @@ impl Submenu { Submenu { hmenu, parent_hmenu: self.hmenu, + parent_menu: self.parent_menu.clone(), } } - pub fn add_text_item(&mut self, label: impl AsRef, enabled: bool) -> TextMenuItem { + pub fn add_text_item( + &mut self, + label: impl AsRef, + enabled: bool, + accelerator: Option<&str>, + ) -> TextMenuItem { let id = COUNTER.next(); let mut flags = MF_POPUP; if !enabled { flags |= MF_GRAYED; } - unsafe { - AppendMenuW( - self.hmenu, - flags, - id as _, - encode_wide(label.as_ref()).as_ptr(), - ) - }; + + let mut label = label.as_ref().to_string(); + if let Some(accelerator) = accelerator { + let (key, mods, accel_str) = parse_accelerator(accelerator); + let accel = ACCEL { + key, + fVirt: mods as _, + cmd: id as _, + }; + + label.push_str("\t"); + label.push_str(&accel_str); + { + let mut parent_inner = self.parent_menu.0.borrow_mut(); + parent_inner.accelerators.push(accel); + } + self.parent_menu.update_haccel(); + } + + unsafe { AppendMenuW(self.hmenu, flags, id as _, encode_wide(label).as_ptr()) }; TextMenuItem { id, parent_hmenu: self.hmenu, @@ -148,11 +198,19 @@ impl Submenu { #[derive(Clone)] pub struct TextMenuItem { id: u64, - parent_hmenu: isize, + parent_hmenu: HMENU, } impl TextMenuItem { pub fn label(&self) -> String { + self.label_with_accel() + .split("\t") + .next() + .unwrap_or_default() + .to_string() + } + + fn label_with_accel(&self) -> String { let mut label = Vec::::new(); let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; @@ -168,17 +226,20 @@ impl TextMenuItem { unsafe { GetMenuItemInfoW(self.parent_hmenu, self.id as _, false.into(), &mut info) }; decode_wide(info.dwTypeData) - .split("\t") - .next() - .unwrap_or_default() - .to_string() } pub fn set_label(&mut self, label: impl AsRef) { + let mut label = label.as_ref().to_string(); + let prev_label = self.label_with_accel(); + if let Some(accel_str) = prev_label.split("\t").nth(1) { + label.push_str("\t"); + label.push_str(accel_str); + } + let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; info.cbSize = std::mem::size_of::() as _; info.fMask = MIIM_STRING; - info.dwTypeData = encode_wide(label.as_ref()).as_mut_ptr(); + info.dwTypeData = encode_wide(label).as_mut_ptr(); unsafe { SetMenuItemInfoW(self.parent_hmenu, self.id as u32, false.into(), &info) }; } diff --git a/src/util.rs b/src/platform_impl/windows/util.rs similarity index 63% rename from src/util.rs rename to src/platform_impl/windows/util.rs index b2d1926..d2782e9 100644 --- a/src/util.rs +++ b/src/platform_impl/windows/util.rs @@ -1,23 +1,3 @@ -#![allow(unused)] - -use std::sync::atomic::{AtomicU64, Ordering}; - -pub struct Counter(AtomicU64); - -impl Counter { - pub const fn new() -> Self { - Self(AtomicU64::new(1)) - } - - pub fn next(&self) -> u64 { - self.0.fetch_add(1, Ordering::Relaxed) - } - - pub fn current(&self) -> u64 { - self.0.load(Ordering::Relaxed) - } -} - #[cfg(target_os = "windows")] pub fn encode_wide(string: impl AsRef) -> Vec { std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())