feat: accelerators (#6)

* feat: accelerators

* add accelerators on windows
This commit is contained in:
Amr Bashir 2022-06-07 13:05:20 +02:00 committed by GitHub
parent 68f16f15a3
commit 6b98160e49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 422 additions and 126 deletions

View file

@ -32,5 +32,5 @@ cocoa = "0.24"
objc = "0.2" objc = "0.2"
[dev-dependencies] [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" } tao = { git = "https://github.com/tauri-apps/tao", branch = "muda/disable-gtk-menu-creation" }

View file

@ -16,16 +16,16 @@ fn main() {
let window2 = WindowBuilder::new().build(&event_loop).unwrap(); let window2 = WindowBuilder::new().build(&event_loop).unwrap();
let mut menu_bar = Menu::new(); let mut menu_bar = Menu::new();
let mut file_menu = menu_bar.add_submenu("File", true); let mut file_menu = menu_bar.add_submenu("&File", true);
let mut edit_menu = menu_bar.add_submenu("Edit", 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 mut save_item = file_menu.add_text_item("&Save", true, Some("CommandOrCtrl+S"));
let _quit_item = file_menu.add_text_item("Quit", true); let _quit_item = file_menu.add_text_item("&Quit", true, None);
let _copy_item = edit_menu.add_text_item("Copy", true); let _copy_item = edit_menu.add_text_item("&Copy", true, None);
let _cut_item = edit_menu.add_text_item("Cut", true); let _cut_item = edit_menu.add_text_item("C&ut", true, None);
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
@ -55,7 +55,7 @@ fn main() {
_ if event.id == save_item.id() => { _ if event.id == save_item.id() => {
println!("Save menu item activated!"); println!("Save menu item activated!");
counter += 1; counter += 1;
save_item.set_label(format!("Save activated {counter} times")); save_item.set_label(format!("&Save activated {counter} times"));
if !open_item_disabled { if !open_item_disabled {
println!("Open item disabled!"); println!("Open item disabled!");

View file

@ -1,29 +1,49 @@
use muda::{menu_event_receiver, Menu}; use muda::{menu_event_receiver, Menu};
#[cfg(target_os = "macos")]
use winit::platform::macOS::EventLoopExtMacOS;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use winit::platform::windows::WindowExtWindows; use winit::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows};
use winit::{ use winit::{
event::{Event, WindowEvent}, event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop}, event_loop::{ControlFlow, EventLoopBuilder},
window::WindowBuilder, window::WindowBuilder,
}; };
fn main() { 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 window = WindowBuilder::new().build(&event_loop).unwrap();
let _window2 = 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 file_menu = menu_bar.add_submenu("File", true); let mut edit_menu = menu_bar.add_submenu("&Edit", 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 mut save_item = file_menu.add_text_item("&Save", true, Some("CommandOrCtrl+S"));
let _quit_item = file_menu.add_text_item("Quit", true); let _quit_item = file_menu.add_text_item("&Quit", true, None);
let _copy_item = edit_menu.add_text_item("Copy", true); let _copy_item = edit_menu.add_text_item("&Copy", true, Some("Ctrl+C"));
let _cut_item = edit_menu.add_text_item("Cut", true); let _cut_item = edit_menu.add_text_item("C&ut", true, None);
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
@ -43,7 +63,7 @@ fn main() {
_ if event.id == save_item.id() => { _ if event.id == save_item.id() => {
println!("Save menu item activated!"); println!("Save menu item activated!");
counter += 1; counter += 1;
save_item.set_label(format!("Save activated {counter} times")); save_item.set_label(format!("&Save activated {counter} times"));
if !open_item_disabled { if !open_item_disabled {
println!("Open item disabled!"); println!("Open item disabled!");
@ -59,7 +79,7 @@ fn main() {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
Event::NewEvents(winit::event::StartCause::Init) => { Event::NewEvents(winit::event::StartCause::Init) => {
menu_bar.init_for_nsapp(); menu_bar.init_for_nsapp();
}, }
Event::WindowEvent { Event::WindowEvent {
event: WindowEvent::CloseRequested, event: WindowEvent::CloseRequested,
.. ..

23
src/counter.rs Normal file
View file

@ -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)
}
}

View file

@ -62,8 +62,8 @@
use crossbeam_channel::{unbounded, Receiver, Sender}; use crossbeam_channel::{unbounded, Receiver, Sender};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
mod counter;
mod platform_impl; mod platform_impl;
mod util;
static MENU_CHANNEL: Lazy<(Sender<MenuEvent>, Receiver<MenuEvent>)> = Lazy::new(|| unbounded()); static MENU_CHANNEL: Lazy<(Sender<MenuEvent>, Receiver<MenuEvent>)> = Lazy::new(|| unbounded());
@ -93,6 +93,7 @@ pub struct MenuEvent {
/// let file_menu = menu.add_submenu("File", true); /// let file_menu = menu.add_submenu("File", true);
/// let edit_menu = menu.add_submenu("Edit", true); /// let edit_menu = menu.add_submenu("Edit", true);
/// ``` /// ```
#[derive(Clone)]
pub struct Menu(platform_impl::Menu); pub struct Menu(platform_impl::Menu);
impl Menu { impl Menu {
@ -102,6 +103,13 @@ impl Menu {
} }
/// Creates a new [`Submenu`] whithin this 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<str>, enabled: bool) -> Submenu { pub fn add_submenu(&mut self, label: impl AsRef<str>, enabled: bool) -> Submenu {
Submenu(self.0.add_submenu(label, enabled)) Submenu(self.0.add_submenu(label, enabled))
} }
@ -119,16 +127,47 @@ impl Menu {
pub fn init_for_gtk_window<W>(&self, w: &W) -> std::rc::Rc<gtk::Box> pub fn init_for_gtk_window<W>(&self, w: &W) -> std::rc::Rc<gtk::Box>
where where
W: gtk::prelude::IsA<gtk::Container>, W: gtk::prelude::IsA<gtk::Container>,
W: gtk::prelude::IsA<gtk::Window>,
{ {
self.0.init_for_gtk_window(w) self.0.init_for_gtk_window(w)
} }
/// Adds this menu to a win32 window. /// 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")] #[cfg(target_os = "windows")]
pub fn init_for_hwnd(&self, hwnd: isize) { pub fn init_for_hwnd(&self, hwnd: isize) {
self.0.init_for_hwnd(hwnd) 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. /// Adds this menu to NSApp.
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn init_for_nsapp(&self) { pub fn init_for_nsapp(&self) {
@ -136,8 +175,8 @@ impl Menu {
} }
} }
#[derive(Clone)]
/// This is a submenu within another [`Submenu`] or [`Menu`]. /// This is a submenu within another [`Submenu`] or [`Menu`].
#[derive(Clone)]
pub struct Submenu(platform_impl::Submenu); pub struct Submenu(platform_impl::Submenu);
impl Submenu { impl Submenu {
@ -162,13 +201,32 @@ impl Submenu {
} }
/// Creates a new [`Submenu`] whithin this 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<str>, enabled: bool) -> Submenu { pub fn add_submenu(&mut self, label: impl AsRef<str>, enabled: bool) -> Submenu {
Submenu(self.0.add_submenu(label, enabled)) Submenu(self.0.add_submenu(label, enabled))
} }
/// Creates a new [`TextMenuItem`] whithin this submenu. /// Creates a new [`TextMenuItem`] whithin this submenu.
pub fn add_text_item(&mut self, label: impl AsRef<str>, 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<str>,
enabled: bool,
accelerator: Option<&str>,
) -> TextMenuItem {
TextMenuItem(self.0.add_text_item(label, enabled, accelerator))
} }
} }

View file

@ -0,0 +1,44 @@
pub fn to_gtk_menemenoic(string: impl AsRef<str>) -> String {
string
.as_ref()
.replace("&&", "[~~]")
.replace("&", "_")
.replace("[~~]", "&&")
}
pub fn to_gtk_accelerator(accelerator: impl AsRef<str>) -> 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" => "<Shift>",
"CONTROL" | "CTRL" | "COMMAND" | "COMMANDORCONTROL" | "COMMANDORCTRL" => "<Ctrl>",
"ALT" => "<Alt>",
"SUPER" | "META" | "WIN" => "<Meta>",
_ => panic!("Unsupported modifier: {}", modifier),
}
}

View file

@ -1,11 +1,14 @@
#![cfg(target_os = "linux")] mod accelerator;
use crate::util::Counter; use crate::counter::Counter;
use gtk::{prelude::*, Orientation}; use gtk::{prelude::*, Orientation};
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use self::accelerator::{to_gtk_accelerator, to_gtk_menemenoic};
static COUNTER: Counter = Counter::new(); static COUNTER: Counter = Counter::new();
#[derive(PartialEq, Eq)]
enum MenuEntryType { enum MenuEntryType {
Submenu, Submenu,
Text, Text,
@ -17,6 +20,7 @@ struct MenuEntry {
enabled: bool, enabled: bool,
r#type: MenuEntryType, r#type: MenuEntryType,
item_id: Option<u64>, item_id: Option<u64>,
accelerator: Option<String>,
// NOTE(amrbashir): because gtk doesn't allow using the same `gtk::MenuItem` // 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 // 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` // 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` // 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. // and push to it every time `Menu::init_for_gtk_window` is called.
gtk_items: Vec<(gtk::MenuBar, Rc<gtk::Box>)>, gtk_items: Vec<(gtk::MenuBar, Rc<gtk::Box>)>,
accel_group: gtk::AccelGroup,
} }
#[derive(Clone)]
pub struct Menu(Rc<RefCell<InnerMenu>>); pub struct Menu(Rc<RefCell<InnerMenu>>);
impl Menu { impl Menu {
@ -42,6 +48,7 @@ impl Menu {
Self(Rc::new(RefCell::new(InnerMenu { Self(Rc::new(RefCell::new(InnerMenu {
entries: Vec::new(), entries: Vec::new(),
gtk_items: Vec::new(), gtk_items: Vec::new(),
accel_group: gtk::AccelGroup::new(),
}))) })))
} }
@ -53,6 +60,7 @@ impl Menu {
entries: Some(Vec::new()), entries: Some(Vec::new()),
r#type: MenuEntryType::Submenu, r#type: MenuEntryType::Submenu,
item_id: None, item_id: None,
accelerator: None,
menu_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))), menu_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))),
item_gtk_items: None, item_gtk_items: None,
})); }));
@ -63,9 +71,12 @@ impl Menu {
pub fn init_for_gtk_window<W>(&self, w: &W) -> Rc<gtk::Box> pub fn init_for_gtk_window<W>(&self, w: &W) -> Rc<gtk::Box>
where where
W: IsA<gtk::Container>, W: IsA<gtk::Container>,
W: IsA<gtk::Window>,
{ {
let mut inner = self.0.borrow_mut();
let menu_bar = gtk::MenuBar::new(); 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); let vbox = gtk::Box::new(Orientation::Vertical, 0);
vbox.pack_start(&menu_bar, false, false, 0); vbox.pack_start(&menu_bar, false, false, 0);
@ -75,7 +86,7 @@ impl Menu {
let vbox = Rc::new(vbox); let vbox = Rc::new(vbox);
let vbox_c = Rc::clone(&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 vbox_c
} }
@ -84,16 +95,17 @@ impl Menu {
fn add_entries_to_menu<M: IsA<gtk::MenuShell>>( fn add_entries_to_menu<M: IsA<gtk::MenuShell>>(
gtk_menu: &M, gtk_menu: &M,
entries: &Vec<Rc<RefCell<MenuEntry>>>, entries: &Vec<Rc<RefCell<MenuEntry>>>,
accel_group: &gtk::AccelGroup,
) { ) {
for entry in entries { for entry in entries {
let mut entry = entry.borrow_mut(); 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(&gtk_item); gtk_menu.append(&gtk_item);
gtk_item.set_sensitive(entry.enabled); 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(); let gtk_menu = gtk::Menu::new();
gtk_item.set_submenu(Some(&gtk_menu)); gtk_item.set_submenu(Some(&gtk_menu));
add_entries_to_menu(&gtk_menu, entry.entries.as_ref().unwrap()); add_entries_to_menu(&gtk_menu, entry.entries.as_ref().unwrap(), accel_group);
entry entry
.menu_gtk_items .menu_gtk_items
.as_mut() .as_mut()
@ -101,6 +113,17 @@ fn add_entries_to_menu<M: IsA<gtk::MenuShell>>(
.borrow_mut() .borrow_mut()
.push((gtk_item, gtk_menu)); .push((gtk_item, gtk_menu));
} else { } 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(); let id = entry.item_id.unwrap_or_default();
gtk_item.connect_activate(move |_| { gtk_item.connect_activate(move |_| {
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id });
@ -151,6 +174,7 @@ impl Submenu {
entries: Some(Vec::new()), entries: Some(Vec::new()),
r#type: MenuEntryType::Submenu, r#type: MenuEntryType::Submenu,
item_id: None, item_id: None,
accelerator: None,
menu_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))), menu_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))),
item_gtk_items: None, item_gtk_items: None,
})); }));
@ -163,13 +187,19 @@ impl Submenu {
Submenu(entry) Submenu(entry)
} }
pub fn add_text_item(&mut self, label: impl AsRef<str>, enabled: bool) -> TextMenuItem { pub fn add_text_item(
&mut self,
label: impl AsRef<str>,
enabled: bool,
accelerator: Option<&str>,
) -> TextMenuItem {
let entry = Rc::new(RefCell::new(MenuEntry { let entry = Rc::new(RefCell::new(MenuEntry {
label: label.as_ref().to_string(), label: to_gtk_menemenoic(label),
enabled, enabled,
entries: None, entries: None,
r#type: MenuEntryType::Text, r#type: MenuEntryType::Text,
item_id: Some(COUNTER.next()), item_id: Some(COUNTER.next()),
accelerator: accelerator.map(|s| s.to_string()),
menu_gtk_items: None, menu_gtk_items: None,
item_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))), item_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))),
})); }));

View file

@ -1,3 +1,4 @@
use crate::counter::Counter;
use cocoa::{ use cocoa::{
appkit::NSButton, appkit::NSButton,
base::{id, nil, BOOL, NO, YES}, base::{id, nil, BOOL, NO, YES},
@ -17,43 +18,11 @@ use std::{
hash::{Hash, Hasher}, hash::{Hash, Hasher},
}; };
/// Identifier of a custom menu item. static COUNTER: Counter = Counter::new();
///
/// 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<MenuId> 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
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TextMenuItem { pub struct TextMenuItem {
pub(crate) id: MenuId, pub(crate) id: u64,
pub(crate) ns_menu_item: id, pub(crate) ns_menu_item: id,
} }
@ -62,7 +31,7 @@ impl TextMenuItem {
let (id, ns_menu_item) = make_menu_item(label.as_ref(), selector); let (id, ns_menu_item) = make_menu_item(label.as_ref(), selector);
unsafe { 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]; let () = msg_send![&*ns_menu_item, setTarget:&*ns_menu_item];
if !enabled { if !enabled {
@ -108,7 +77,7 @@ impl TextMenuItem {
} }
pub fn id(&self) -> u64 { pub fn id(&self) -> u64 {
self.id.0 self.id
} }
} }
@ -119,7 +88,7 @@ pub fn make_menu_item(
//menu_type: MenuType, //menu_type: MenuType,
) -> (MenuId, *mut Object) { ) -> (MenuId, *mut Object) {
let alloc = make_menu_item_alloc(); let alloc = make_menu_item_alloc();
let menu_id = MenuId::new(title); let menu_id = COUNTER.next();
unsafe { unsafe {
let title = NSString::alloc(nil).init_str(title); let title = NSString::alloc(nil).init_str(title);

View file

@ -23,11 +23,7 @@ impl Menu {
pub fn add_submenu(&mut self, label: impl AsRef<str>, enabled: bool) -> Submenu { pub fn add_submenu(&mut self, label: impl AsRef<str>, enabled: bool) -> Submenu {
let menu = Menu::new(); let menu = Menu::new();
let menu_item = TextMenuItem::new( let menu_item = TextMenuItem::new("", enabled, sel!(fireMenubarAction:));
"",
enabled,
sel!(fireMenubarAction:),
);
unsafe { unsafe {
menu_item.ns_menu_item.setSubmenu_(menu.0); menu_item.ns_menu_item.setSubmenu_(menu.0);

View file

@ -1,10 +1,10 @@
pub use self::platform_impl::*; pub use self::platform_impl::*;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[path = "windows.rs"] #[path = "windows/mod.rs"]
mod platform_impl; mod platform_impl;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[path = "linux.rs"] #[path = "linux/mod.rs"]
mod platform_impl; mod platform_impl;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[path = "macos/mod.rs"] #[path = "macos/mod.rs"]

View file

@ -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<str>) -> (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),
}
}

View file

@ -1,25 +1,43 @@
#![cfg(target_os = "windows")] #![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::{ use windows_sys::Win32::{
Foundation::{HWND, LPARAM, LRESULT, WPARAM}, Foundation::{HWND, LPARAM, LRESULT, WPARAM},
UI::{ UI::{
Shell::{DefSubclassProc, SetWindowSubclass}, Shell::{DefSubclassProc, SetWindowSubclass},
WindowsAndMessaging::{ WindowsAndMessaging::{
AppendMenuW, CreateMenu, EnableMenuItem, GetMenuItemInfoW, SetMenu, SetMenuItemInfoW, AppendMenuW, CreateAcceleratorTableW, CreateMenu, EnableMenuItem, GetMenuItemInfoW,
MENUITEMINFOW, MFS_DISABLED, MF_DISABLED, MF_ENABLED, MF_GRAYED, MF_POPUP, MIIM_STATE, SetMenu, SetMenuItemInfoW, ACCEL, HACCEL, HMENU, MENUITEMINFOW, MFS_DISABLED,
MIIM_STRING, WM_COMMAND, 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<ACCEL>,
haccel: HACCEL,
}
#[derive(Clone)]
pub struct Menu(Rc<RefCell<InnerMenu>>);
impl Menu { impl Menu {
pub fn new() -> Self { 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<str>, enabled: bool) -> Submenu { pub fn add_submenu(&mut self, label: impl AsRef<str>, enabled: bool) -> Submenu {
@ -30,7 +48,7 @@ impl Menu {
} }
unsafe { unsafe {
AppendMenuW( AppendMenuW(
self.0, self.0.borrow().hmenu,
flags, flags,
hmenu as _, hmenu as _,
encode_wide(label.as_ref()).as_ptr(), encode_wide(label.as_ref()).as_ptr(),
@ -38,22 +56,35 @@ impl Menu {
}; };
Submenu { Submenu {
hmenu, hmenu,
parent_hmenu: self.0, parent_hmenu: self.0.borrow().hmenu,
parent_menu: self.clone(),
} }
} }
pub fn init_for_hwnd(&self, hwnd: isize) { pub fn init_for_hwnd(&self, hwnd: isize) {
unsafe { unsafe {
SetMenu(hwnd, self.0); SetMenu(hwnd, self.0.borrow().hmenu);
SetWindowSubclass(hwnd, Some(menu_subclass_proc), 22, 0); 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)] #[derive(Clone)]
pub struct Submenu { pub struct Submenu {
hmenu: isize, hmenu: HMENU,
parent_hmenu: isize, parent_hmenu: HMENU,
parent_menu: Menu,
} }
impl Submenu { impl Submenu {
@ -72,6 +103,7 @@ impl Submenu {
unsafe { GetMenuItemInfoW(self.parent_hmenu, self.hmenu as _, false.into(), &mut info) }; 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) decode_wide(info.dwTypeData)
} }
@ -121,23 +153,41 @@ impl Submenu {
Submenu { Submenu {
hmenu, hmenu,
parent_hmenu: self.hmenu, parent_hmenu: self.hmenu,
parent_menu: self.parent_menu.clone(),
} }
} }
pub fn add_text_item(&mut self, label: impl AsRef<str>, enabled: bool) -> TextMenuItem { pub fn add_text_item(
&mut self,
label: impl AsRef<str>,
enabled: bool,
accelerator: Option<&str>,
) -> TextMenuItem {
let id = COUNTER.next(); let id = COUNTER.next();
let mut flags = MF_POPUP; let mut flags = MF_POPUP;
if !enabled { if !enabled {
flags |= MF_GRAYED; flags |= MF_GRAYED;
} }
unsafe {
AppendMenuW( let mut label = label.as_ref().to_string();
self.hmenu, if let Some(accelerator) = accelerator {
flags, let (key, mods, accel_str) = parse_accelerator(accelerator);
id as _, let accel = ACCEL {
encode_wide(label.as_ref()).as_ptr(), 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 { TextMenuItem {
id, id,
parent_hmenu: self.hmenu, parent_hmenu: self.hmenu,
@ -148,11 +198,19 @@ impl Submenu {
#[derive(Clone)] #[derive(Clone)]
pub struct TextMenuItem { pub struct TextMenuItem {
id: u64, id: u64,
parent_hmenu: isize, parent_hmenu: HMENU,
} }
impl TextMenuItem { impl TextMenuItem {
pub fn label(&self) -> String { 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::<u16>::new(); let mut label = Vec::<u16>::new();
let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; 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) }; unsafe { GetMenuItemInfoW(self.parent_hmenu, self.id as _, false.into(), &mut info) };
decode_wide(info.dwTypeData) decode_wide(info.dwTypeData)
.split("\t")
.next()
.unwrap_or_default()
.to_string()
} }
pub fn set_label(&mut self, label: impl AsRef<str>) { pub fn set_label(&mut self, label: impl AsRef<str>) {
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() }; let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() };
info.cbSize = std::mem::size_of::<MENUITEMINFOW>() as _; info.cbSize = std::mem::size_of::<MENUITEMINFOW>() as _;
info.fMask = MIIM_STRING; 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) }; unsafe { SetMenuItemInfoW(self.parent_hmenu, self.id as u32, false.into(), &info) };
} }

View file

@ -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")] #[cfg(target_os = "windows")]
pub fn encode_wide(string: impl AsRef<std::ffi::OsStr>) -> Vec<u16> { pub fn encode_wide(string: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())