mirror of
https://github.com/italicsjenga/muda.git
synced 2024-12-23 20:11:29 +11:00
feat: accelerators (#6)
* feat: accelerators * add accelerators on windows
This commit is contained in:
parent
68f16f15a3
commit
6b98160e49
|
@ -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" }
|
||||
|
|
|
@ -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!");
|
||||
|
|
|
@ -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,
|
||||
..
|
||||
|
|
23
src/counter.rs
Normal file
23
src/counter.rs
Normal 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)
|
||||
}
|
||||
}
|
66
src/lib.rs
66
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<MenuEvent>, Receiver<MenuEvent>)> = 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<str>, enabled: bool) -> Submenu {
|
||||
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>
|
||||
where
|
||||
W: gtk::prelude::IsA<gtk::Container>,
|
||||
W: gtk::prelude::IsA<gtk::Window>,
|
||||
{
|
||||
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<str>, 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<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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
44
src/platform_impl/linux/accelerator.rs
Normal file
44
src/platform_impl/linux/accelerator.rs
Normal 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),
|
||||
}
|
||||
}
|
|
@ -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<u64>,
|
||||
accelerator: Option<String>,
|
||||
// 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<gtk::Box>)>,
|
||||
accel_group: gtk::AccelGroup,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Menu(Rc<RefCell<InnerMenu>>);
|
||||
|
||||
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<W>(&self, w: &W) -> Rc<gtk::Box>
|
||||
where
|
||||
W: IsA<gtk::Container>,
|
||||
W: IsA<gtk::Window>,
|
||||
{
|
||||
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<M: IsA<gtk::MenuShell>>(
|
||||
gtk_menu: &M,
|
||||
entries: &Vec<Rc<RefCell<MenuEntry>>>,
|
||||
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<M: IsA<gtk::MenuShell>>(
|
|||
.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<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 {
|
||||
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()))),
|
||||
}));
|
|
@ -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<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
|
||||
}
|
||||
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);
|
||||
|
|
|
@ -23,11 +23,7 @@ impl Menu {
|
|||
|
||||
pub fn add_submenu(&mut self, label: impl AsRef<str>, 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);
|
||||
|
|
|
@ -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"]
|
||||
|
|
115
src/platform_impl/windows/accelerator.rs
Normal file
115
src/platform_impl/windows/accelerator.rs
Normal 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),
|
||||
}
|
||||
}
|
|
@ -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<ACCEL>,
|
||||
haccel: HACCEL,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Menu(Rc<RefCell<InnerMenu>>);
|
||||
|
||||
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<str>, 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<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 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::<u16>::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<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() };
|
||||
info.cbSize = std::mem::size_of::<MENUITEMINFOW>() 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) };
|
||||
}
|
|
@ -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<std::ffi::OsStr>) -> Vec<u16> {
|
||||
std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())
|
Loading…
Reference in a new issue