refactor: rewrite (#18)

* refactor: rewrite

* fix syncing check items and cleanup

* clippy

* Add `append`, `prepend` and `insert`

* accept different menu items in `*_list` methods

* add context menu for gtk

* add `with_items`

* add `items` getter

* chore: unreachable! and typos

* implement remove

* `*_list`  -> `*_items`

* fix winit example

* add `show_context_menu_for_gtk_window` on `Submenu` type

* Add windows implementation

* TextMenuItem -> MenuItem, MenuItem trait -> MenuEntry

* Add `PredfinedMenuItem`

* move internal mod into its own file

* update tao example to latest tao's `muda` branch

* fix build on linux with latest tao changes

* Fix accelerators on Linux

* update examples

* remove recursive removal of submenus

* remvoe gtk menu items recursively

* fix tao example on macos

* On Windows, remove parents hmenu when removing an item

* Add documentation

* update README.md

* use insert_items with postion 0 for prepend_items

* Add menu mnemonics in examples

* Add `ContextMenu` trait

* Add methods to `ContextMenu` trait necessary for tray icon

* fix linux build

* fix context menu on gtk

* Expose gtk::Menu in ContextMenu trait

* Revert context menu to create a gtk::Menu on each call

* clippy lints

* cleanup crate structure

* update docs

* Fix doc tests and links

* more docs fixes

* error handling

* macOS implementation (#19)

* partial macOS implementation

* fix context menu examples

* add accelerator support for macOS

* strip ampersands from titles on macOS

* add CMD_OR_CTRL shorthand for modifiers

* implement actions for predefined menu items on macos

* fix examples

* more predefined items

* implement insert for macos

* refactor macOS implementation

* menu state getters and setters on macOS

* implement remove for macOS

* code tweaks

* add show_context_menu_for_nsview for Submenu on macOS

* docs improvements

* allow adding item to the same menu multiple times on macOS

* implement `items` for macOS

* strip only single ampersands from menu titles

* add support for menu item actions on macOS

* add app name to macOS About, Hide, Quit menu items

* add methods to set app window and help menus on macOS

* fix clickable submenu titles on macOS

* refactor submenu for safe reuse on macOS

* fmt & clippy

* few cleanups

* fix docs

* clippy

* fix docs

* cleanup examples

* fix tests

* fix clippy??

* use cargo action instead

* ???

* Replace popUpContextMenu with popUpMenuPositioningItem

Co-authored-by: Caesar Schinas <caesar@caesarschinas.com>
Co-authored-by: Wu Wayne <yuweiwu@pm.me>
This commit is contained in:
Amr Bashir 2022-11-23 18:29:52 +02:00 committed by GitHub
parent 1f341d1e4e
commit 812ff0d37a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 4949 additions and 2231 deletions

View file

@ -19,6 +19,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: install system deps
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libxdo-dev libayatana-appindicator3-dev
- name: install stable
uses: actions-rs/toolchain@v1
with:
@ -27,10 +31,9 @@ jobs:
override: true
components: clippy
- name: clippy
uses: actions-rs/clippy-check@v1
- uses: actions-rs/cargo@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
command: clippy
args: --all-targets --all-features -- -D warnings
fmt:

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
Cargo.lock
/.vscode

View file

@ -12,23 +12,24 @@ categories = ["gui"]
[dependencies]
crossbeam-channel = "0.5"
once_cell = "1.10"
keyboard-types = "0.6"
once_cell = "1"
thiserror = "1.0.37"
[target.'cfg(target_os = "windows")'.dependencies.windows-sys]
version = "0.34"
version = "0.42"
features = [
"Win32_UI_WindowsAndMessaging",
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_UI_Shell",
"Win32_Globalization",
"Win32_UI_Input_KeyboardAndMouse"
"Win32_UI_Input_KeyboardAndMouse",
]
[target.'cfg(target_os = "linux")'.dependencies]
gdk = "0.15"
gtk = "0.15"
gtk = { version = "0.15", features = ["v3_22"] }
libxdo = "0.6.0"
[target.'cfg(target_os = "macos")'.dependencies]
@ -36,5 +37,5 @@ cocoa = "0.24"
objc = "0.2"
[dev-dependencies]
winit = { git = "https://github.com/rust-windowing/winit" }
winit = "0.27"
tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" }

View file

@ -1,21 +1,32 @@
# muda
Menu Utilities for Desktop Applications.
muda is a Menu Utilities library for Desktop Applications.
## Example
Create the root menu and add submenus and men items.
Create the menu and add your items
```rs
let mut menu = Menu::new();
let menu = Menu::new();
let menu_item2 = MenuItem::new("Menu item #2", false, None);
let submenu = Submenu::with_items("Submenu Outer", true,&[
&MenuItem::new("Menu item #1", true, Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyD))),
&PredefinedMenuItem::separator(),
&menu_item2,
&MenuItem::new("Menu item #3", true, None),
&PredefinedMenuItem::separator(),
&Submenu::with_items("Submenu Inner", true,&[
&MenuItem::new("Submenu item #1", true, None),
&PredefinedMenuItem::separator(),
&menu_item2,
])
]);
let file_menu = menu.add_submenu("File", true);
let open_item = file_menu.add_text_item("Open", true);
let save_item = file_menu.add_text_item("Save", true);
```
let edit_menu = menu.add_submenu("Edit", true);
let copy_item = file_menu.add_text_item("Copy", true);
let cut_item = file_menu.add_text_item("Cut", true);
Then Add your root menu to a Window on Windows and Linux Only or use it
as your global app menu on macOS
```rs
// --snip--
#[cfg(target_os = "windows")]
menu.init_for_hwnd(window.hwnd() as isize);
#[cfg(target_os = "linux")]
@ -23,7 +34,26 @@ menu.init_for_gtk_window(&gtk_window);
#[cfg(target_os = "macos")]
menu.init_for_nsapp();
```
Then listen for the events
## Context menus (Popup menus)
You can also use a [`Menu`] or a [`Submenu`] show a context menu.
```rs
// --snip--
let x = 100;
let y = 120;
#[cfg(target_os = "windows")]
menu.show_context_menu_for_hwnd(window.hwnd() as isize, x, y);
#[cfg(target_os = "linux")]
menu.show_context_menu_for_gtk_window(&gtk_window, x, y);
#[cfg(target_os = "macos")]
menu.show_context_menu_for_nsview(nsview, x, y);
```
## Processing menu events
You can use [`menu_event_receiver`](https://docs.rs/muda/latest/muda/fn.menu_event_receiver.html) to get a reference to the [`MenuEventReceiver`](https://docs.rs/muda/latest/muda/type.MenuEventReceiver.html)
which you can use to listen to events when a menu item is activated
```rs
if let Ok(event) = menu_event_receiver().try_recv() {
match event.id {
@ -34,3 +64,26 @@ if let Ok(event) = menu_event_receiver().try_recv() {
}
}
```
## Platform-specific notes:
### Accelerators on Windows
Accelerators don't work unless the win32 message loop calls
[`TranslateAcceleratorW`](https://docs.rs/windows-sys/latest/windows_sys/Win32/UI/WindowsAndMessaging/fn.TranslateAcceleratorW.html)
See [`Menu::init_for_hwnd`](https://docs.rs/muda/latest/muda/struct.Menu.html#method.init_for_hwnd) for more details
### Linux
`libx` is used to make the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu items work. Be sure to install following packages before building:
Arch Linux / Manjaro:
```sh
pacman -S xdotool
```
Debian / Ubuntu:
```sh
sudo apt install libxdo-dev
```

View file

@ -1,42 +1,119 @@
use keyboard_types::Code;
#![allow(unused)]
use muda::{
accelerator::{Accelerator, Mods},
menu_event_receiver, Menu, NativeMenuItem,
accelerator::{Accelerator, Code, Modifiers},
menu_event_receiver, AboutMetadata, CheckMenuItem, ContextMenu, Menu, MenuItem,
PredefinedMenuItem, Submenu,
};
#[cfg(target_os = "macos")]
use tao::platform::macos::WindowExtMacOS;
#[cfg(target_os = "linux")]
use tao::platform::unix::WindowExtUnix;
#[cfg(target_os = "windows")]
use tao::platform::windows::WindowExtWindows;
use tao::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows};
use tao::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
event::{ElementState, Event, MouseButton, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder},
window::WindowBuilder,
};
fn main() {
let event_loop = EventLoop::new();
let mut event_loop_builder = EventLoopBuilder::new();
let window = WindowBuilder::new().build(&event_loop).unwrap();
let window2 = WindowBuilder::new().build(&event_loop).unwrap();
let menu_bar = Menu::new();
let mut menu_bar = Menu::new();
#[cfg(target_os = "windows")]
{
let menu_bar_c = menu_bar.clone();
event_loop_builder.with_msg_hook(move |msg| {
use windows_sys::Win32::UI::WindowsAndMessaging::{TranslateAcceleratorW, MSG};
unsafe {
let msg = msg as *const MSG;
let translated = TranslateAcceleratorW((*msg).hwnd, menu_bar_c.haccel(), msg);
translated == 1
}
});
}
let mut file_menu = menu_bar.add_submenu("&File", true);
let mut open_item = file_menu.add_item("&Open", true, None);
let mut save_item = file_menu.add_item(
"&Save",
let event_loop = event_loop_builder.build();
let window = WindowBuilder::new()
.with_title("Window 1")
.build(&event_loop)
.unwrap();
let window2 = WindowBuilder::new()
.with_title("Window 2")
.build(&event_loop)
.unwrap();
#[cfg(target_os = "macos")]
{
let app_m = Submenu::new("App", true);
menu_bar.append(&app_m);
app_m.append_items(&[
&PredefinedMenuItem::about(None, None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::services(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::hide(None),
&PredefinedMenuItem::hide_others(None),
&PredefinedMenuItem::show_all(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::quit(None),
]);
}
let file_m = Submenu::new("&File", true);
let edit_m = Submenu::new("&Edit", true);
let window_m = Submenu::new("&Window", true);
menu_bar.append_items(&[&file_m, &edit_m, &window_m]);
let custom_i_1 = MenuItem::new(
"C&ustom 1",
true,
Some(Accelerator::new(Mods::Ctrl, Code::KeyS)),
Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyC)),
);
let custom_i_2 = MenuItem::new("Custom 2", false, None);
let check_custom_i_1 = CheckMenuItem::new("Check Custom 1", true, true, None);
let check_custom_i_2 = CheckMenuItem::new("Check Custom 2", false, true, None);
let check_custom_i_3 = CheckMenuItem::new(
"Check Custom 3",
true,
true,
Some(Accelerator::new(Some(Modifiers::SHIFT), Code::KeyD)),
);
file_menu.add_native_item(NativeMenuItem::Minimize);
file_menu.add_native_item(NativeMenuItem::CloseWindow);
file_menu.add_native_item(NativeMenuItem::Quit);
let mut edit_menu = menu_bar.add_submenu("&Edit", true);
edit_menu.add_native_item(NativeMenuItem::Cut);
edit_menu.add_native_item(NativeMenuItem::Copy);
edit_menu.add_native_item(NativeMenuItem::Paste);
edit_menu.add_native_item(NativeMenuItem::SelectAll);
let copy_i = PredefinedMenuItem::copy(None);
let cut_i = PredefinedMenuItem::cut(None);
let paste_i = PredefinedMenuItem::paste(None);
file_m.append_items(&[
&custom_i_1,
&custom_i_2,
&window_m,
&PredefinedMenuItem::separator(),
&check_custom_i_1,
]);
window_m.append_items(&[
&PredefinedMenuItem::minimize(None),
&PredefinedMenuItem::maximize(None),
&PredefinedMenuItem::close_window(Some("Close")),
&PredefinedMenuItem::fullscreen(None),
&PredefinedMenuItem::about(
None,
Some(AboutMetadata {
name: Some("tao".to_string()),
copyright: Some("Copyright tao".to_string()),
..Default::default()
}),
),
&check_custom_i_3,
&custom_i_2,
&custom_i_1,
]);
edit_m.append_items(&[&copy_i, &paste_i, &PredefinedMenuItem::separator()]);
#[cfg(target_os = "windows")]
{
@ -48,44 +125,64 @@ fn main() {
menu_bar.init_for_gtk_window(window.gtk_window());
menu_bar.init_for_gtk_window(window2.gtk_window());
}
#[cfg(target_os = "macos")]
{
menu_bar.init_for_nsapp();
window_m.set_windows_menu_for_nsapp();
}
let menu_channel = menu_event_receiver();
let mut open_item_disabled = false;
let mut counter = 0;
let mut x = 0_f64;
let mut y = 0_f64;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
#[cfg(target_os = "macos")]
Event::NewEvents(tao::event::StartCause::Init) => {
menu_bar.init_for_nsapp();
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::WindowEvent {
event: WindowEvent::CursorMoved { position, .. },
window_id,
..
} => {
if window_id == window.id() {
x = position.x;
y = position.y;
}
}
Event::WindowEvent {
event:
WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Right,
..
},
window_id,
..
} => {
if window_id == window2.id() {
#[cfg(target_os = "windows")]
window_m.show_context_menu_for_hwnd(window2.hwnd() as _, x, y);
#[cfg(target_os = "linux")]
window_m.show_context_menu_for_gtk_window(window2.gtk_window(), x, y);
#[cfg(target_os = "macos")]
menu_bar.show_context_menu_for_nsview(window2.ns_view() as _, x, y);
}
}
Event::MainEventsCleared => {
// window.request_redraw();
window.request_redraw();
}
_ => (),
}
if let Ok(event) = menu_channel.try_recv() {
match event.id {
_ if event.id == save_item.id() => {
println!("Save menu item activated!");
counter += 1;
save_item.set_label(format!("&Save activated {counter} times"));
if !open_item_disabled {
println!("Open item disabled!");
open_item.set_enabled(false);
open_item_disabled = true;
}
}
_ => {}
if event.id == custom_i_1.id() {
file_m.insert(&MenuItem::new("New Menu Item", false, None), 2);
}
println!("{:?}", event);
}
})
}

View file

@ -1,14 +1,15 @@
use keyboard_types::Code;
#![allow(unused)]
use muda::{
accelerator::{Accelerator, Mods},
menu_event_receiver, Menu, NativeMenuItem,
accelerator::{Accelerator, Code, Modifiers},
menu_event_receiver, AboutMetadata, CheckMenuItem, ContextMenu, Menu, MenuItem,
PredefinedMenuItem, Submenu,
};
#[cfg(target_os = "macos")]
use winit::platform::macos::EventLoopBuilderExtMacOS;
use winit::platform::macos::{EventLoopBuilderExtMacOS, WindowExtMacOS};
#[cfg(target_os = "windows")]
use winit::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows};
use winit::{
event::{Event, WindowEvent},
event::{ElementState, Event, MouseButton, WindowEvent},
event_loop::{ControlFlow, EventLoopBuilder},
window::WindowBuilder,
};
@ -16,7 +17,7 @@ use winit::{
fn main() {
let mut event_loop_builder = EventLoopBuilder::new();
let mut menu_bar = Menu::new();
let menu_bar = Menu::new();
#[cfg(target_os = "windows")]
{
@ -24,7 +25,7 @@ fn main() {
event_loop_builder.with_msg_hook(move |msg| {
use windows_sys::Win32::UI::WindowsAndMessaging::{TranslateAcceleratorW, MSG};
unsafe {
let msg = msg as *mut MSG;
let msg = msg as *const MSG;
let translated = TranslateAcceleratorW((*msg).hwnd, menu_bar_c.haccel(), msg);
translated == 1
}
@ -33,56 +34,137 @@ fn main() {
#[cfg(target_os = "macos")]
event_loop_builder.with_default_menu(false);
#[allow(unused_mut)]
let mut event_loop = event_loop_builder.build();
let event_loop = event_loop_builder.build();
let window = WindowBuilder::new().build(&event_loop).unwrap();
let _window2 = WindowBuilder::new().build(&event_loop).unwrap();
let window = WindowBuilder::new()
.with_title("Window 1")
.build(&event_loop)
.unwrap();
let window2 = WindowBuilder::new()
.with_title("Window 2")
.build(&event_loop)
.unwrap();
let mut file_menu = menu_bar.add_submenu("&File", true);
let mut open_item = file_menu.add_item("&Open", true, None);
let mut save_item = file_menu.add_item(
"&Save",
#[cfg(target_os = "macos")]
{
let app_m = Submenu::new("App", true);
menu_bar.append(&app_m);
app_m.append_items(&[
&PredefinedMenuItem::about(None, None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::services(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::hide(None),
&PredefinedMenuItem::hide_others(None),
&PredefinedMenuItem::show_all(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::quit(None),
]);
}
let file_m = Submenu::new("&File", true);
let edit_m = Submenu::new("&Edit", true);
let window_m = Submenu::new("&Window", true);
menu_bar.append_items(&[&file_m, &edit_m, &window_m]);
let custom_i_1 = MenuItem::new(
"C&ustom 1",
true,
Some(Accelerator::new(Mods::Ctrl, Code::KeyS)),
Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyC)),
);
let custom_i_2 = MenuItem::new("Custom 2", false, None);
let check_custom_i_1 = CheckMenuItem::new("Check Custom 1", true, true, None);
let check_custom_i_2 = CheckMenuItem::new("Check Custom 2", false, true, None);
let check_custom_i_3 = CheckMenuItem::new(
"Check Custom 3",
true,
true,
Some(Accelerator::new(Some(Modifiers::SHIFT), Code::KeyD)),
);
file_menu.add_native_item(NativeMenuItem::Minimize);
file_menu.add_native_item(NativeMenuItem::CloseWindow);
file_menu.add_native_item(NativeMenuItem::Quit);
let mut edit_menu = menu_bar.add_submenu("&Edit", true);
edit_menu.add_native_item(NativeMenuItem::Cut);
edit_menu.add_native_item(NativeMenuItem::Copy);
edit_menu.add_native_item(NativeMenuItem::Paste);
edit_menu.add_native_item(NativeMenuItem::SelectAll);
let copy_i = PredefinedMenuItem::copy(None);
let cut_i = PredefinedMenuItem::cut(None);
let paste_i = PredefinedMenuItem::paste(None);
file_m.append_items(&[
&custom_i_1,
&custom_i_2,
&window_m,
&PredefinedMenuItem::separator(),
&check_custom_i_1,
]);
window_m.append_items(&[
&PredefinedMenuItem::minimize(None),
&PredefinedMenuItem::maximize(None),
&PredefinedMenuItem::close_window(Some("Close")),
&PredefinedMenuItem::fullscreen(None),
&PredefinedMenuItem::about(
None,
Some(AboutMetadata {
name: Some("winit".to_string()),
copyright: Some("Copyright winit".to_string()),
..Default::default()
}),
),
&check_custom_i_3,
&custom_i_2,
&custom_i_1,
]);
edit_m.append_items(&[&copy_i, &paste_i, &PredefinedMenuItem::separator()]);
#[cfg(target_os = "windows")]
{
menu_bar.init_for_hwnd(window.hwnd() as _);
menu_bar.init_for_hwnd(_window2.hwnd() as _);
menu_bar.init_for_hwnd(window2.hwnd() as _);
}
#[cfg(target_os = "macos")]
{
menu_bar.init_for_nsapp();
window_m.set_windows_menu_for_nsapp();
}
let menu_channel = menu_event_receiver();
let mut open_item_disabled = false;
let mut counter = 0;
let mut x = 0_f64;
let mut y = 0_f64;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
#[cfg(target_os = "macos")]
Event::NewEvents(winit::event::StartCause::Init) => {
menu_bar.init_for_nsapp();
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::WindowEvent {
event: WindowEvent::CursorMoved { position, .. },
window_id,
..
} => {
if window_id == window.id() {
x = position.x;
y = position.y;
}
}
Event::WindowEvent {
event:
WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Right,
..
},
window_id,
..
} => {
if window_id == window2.id() {
#[cfg(target_os = "windows")]
window_m.show_context_menu_for_hwnd(window2.hwnd(), x, y);
#[cfg(target_os = "macos")]
menu_bar.show_context_menu_for_nsview(window2.ns_view() as _, x, y);
}
}
Event::MainEventsCleared => {
window.request_redraw();
}
@ -90,20 +172,10 @@ fn main() {
}
if let Ok(event) = menu_channel.try_recv() {
match event.id {
_ if event.id == save_item.id() => {
println!("Save menu item activated!");
counter += 1;
save_item.set_label(format!("&Save activated {counter} times"));
if !open_item_disabled {
println!("Open item disabled!");
open_item.set_enabled(false);
open_item_disabled = true;
}
}
_ => {}
if event.id == custom_i_1.id() {
file_m.insert(&MenuItem::new("New Menu Item", false, None), 2);
}
println!("{:?}", event);
}
})
}

View file

@ -1,28 +1,22 @@
//! Accelerators describe keyboard shortcuts defined by the application.
//! Accelerators describe keyboard shortcuts for menu items.
//!
//! [`Accelerator`s](crate::accelerator::Accelerator) are used to define a keyboard shortcut consisting
//! of an optional combination of modifier keys (provided by [`SysMods`](crate::accelerator::SysMods),
//! [`RawMods`](crate::accelerator::RawMods) or [`Modifiers`](crate::accelerator::Modifiers)) and
//! of an optional combination of modifier keys (provided by [`Modifiers`](crate::accelerator::Modifiers)) and
//! one key ([`Code`](crate::accelerator::Code)).
//!
//! # Examples
//! They can be created directly
//! ```
//! # use muda::accelerator::{Accelerator, Mods, Modifiers, Code};
//! #
//! let accelerator = Accelerator::new(Mods::Shift, Code::KeyQ);
//! let accelerator_with_raw_mods = Accelerator::new(Mods::Shift, Code::KeyQ);
//! ```no_run
//! # use muda::accelerator::{Accelerator, Modifiers, Code};
//! let accelerator = Accelerator::new(Some(Modifiers::SHIFT), Code::KeyQ);
//! let accelerator_without_mods = Accelerator::new(None, Code::KeyQ);
//! # assert_eq!(accelerator, accelerator_with_raw_mods);
//! ```
//! or from `&str`, note that all modifiers
//! have to be listed before the non-modifier key, `shift+alt+KeyQ` is legal,
//! whereas `shift+q+alt` is not.
//! ```
//! # use muda::accelerator::{Accelerator, Mods};
//! #
//! ```no_run
//! # use muda::accelerator::{Accelerator};
//! let accelerator: Accelerator = "shift+alt+KeyQ".parse().unwrap();
//! #
//! # // This assert exists to ensure a test breaks once the
//! # // statement above about ordering is no longer valid.
//! # assert!("shift+KeyQ+alt".parse::<Accelerator>().is_err());
@ -32,8 +26,10 @@
pub use keyboard_types::{Code, Modifiers};
use std::{borrow::Borrow, hash::Hash, str::FromStr};
/// Base `Accelerator` functions.
#[derive(Debug, Clone, PartialEq, Hash)]
/// A keyboard shortcut that consists of an optional combination
/// of modifier keys (provided by [`Modifiers`](crate::accelerator::Modifiers)) and
/// one key ([`Code`](crate::accelerator::Code)).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Accelerator {
pub(crate) mods: Modifiers,
pub(crate) key: Code,
@ -41,20 +37,22 @@ pub struct Accelerator {
impl Accelerator {
/// Creates a new accelerator to define keyboard shortcuts throughout your application.
pub fn new(mods: impl Into<Option<Modifiers>>, key: Code) -> Self {
/// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::META`]/[`Modifiers::SUPER`]
pub fn new(mods: Option<Modifiers>, key: Code) -> Self {
Self {
mods: mods.into().unwrap_or_else(Modifiers::empty),
mods: mods.unwrap_or_else(Modifiers::empty),
key,
}
}
/// Returns `true` if this [`Code`] and [`Modifiers`] matches this `Accelerator`.
///
/// [`Code`]: Code
/// [`Modifiers`]: crate::accelerator::Modifiers
pub fn matches(&self, modifiers: impl Borrow<Modifiers>, key: impl Borrow<Code>) -> bool {
// Should be a const but const bit_or doesn't work here.
let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::SUPER;
let base_mods = Modifiers::SHIFT
| Modifiers::CONTROL
| Modifiers::ALT
| Modifiers::META
| Modifiers::SUPER;
let modifiers = modifiers.borrow();
let key = key.borrow();
self.mods == *modifiers & base_mods && self.key == *key
@ -65,96 +63,20 @@ impl Accelerator {
// compatible with tauri and it also open the option
// to generate accelerator from string
impl FromStr for Accelerator {
type Err = AcceleratorParseError;
type Err = crate::Error;
fn from_str(accelerator_string: &str) -> Result<Self, Self::Err> {
parse_accelerator(accelerator_string)
}
}
/// Represents the active modifier keys.
///
/// This is intended to be clearer than [`Modifiers`], when describing accelerators.
///
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Hash)]
pub enum Mods {
None,
Alt,
Ctrl,
Command,
CommandOrCtrl,
Meta,
Shift,
AltCtrl,
AltMeta,
AltShift,
CtrlShift,
CtrlMeta,
MetaShift,
AltCtrlMeta,
AltCtrlShift,
AltMetaShift,
CtrlMetaShift,
AltCtrlMetaShift,
}
impl From<Mods> for Option<Modifiers> {
fn from(src: Mods) -> Option<Modifiers> {
Some(src.into())
}
}
impl From<Mods> for Modifiers {
fn from(src: Mods) -> Modifiers {
let (alt, ctrl, meta, shift) = match src {
Mods::None => (false, false, false, false),
Mods::Alt => (true, false, false, false),
Mods::Ctrl => (false, true, false, false),
Mods::Command => (false, false, true, false),
#[cfg(target_os = "macos")]
Mods::CommandOrCtrl => (false, false, true, false),
#[cfg(not(target_os = "macos"))]
Mods::CommandOrCtrl => (false, true, false, false),
Mods::Meta => (false, false, true, false),
Mods::Shift => (false, false, false, true),
Mods::AltCtrl => (true, true, false, false),
Mods::AltMeta => (true, false, true, false),
Mods::AltShift => (true, false, false, true),
Mods::CtrlMeta => (false, true, true, false),
Mods::CtrlShift => (false, true, false, true),
Mods::MetaShift => (false, false, true, true),
Mods::AltCtrlMeta => (true, true, true, false),
Mods::AltMetaShift => (true, false, true, true),
Mods::AltCtrlShift => (true, true, false, true),
Mods::CtrlMetaShift => (false, true, true, true),
Mods::AltCtrlMetaShift => (true, true, true, true),
};
let mut mods = Modifiers::empty();
mods.set(Modifiers::ALT, alt);
mods.set(Modifiers::CONTROL, ctrl);
mods.set(Modifiers::SUPER, meta);
mods.set(Modifiers::SHIFT, shift);
mods
}
}
#[derive(Debug, Clone)]
pub struct AcceleratorParseError(String);
impl std::fmt::Display for AcceleratorParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[AcceleratorParseError]: {}", self.0)
}
}
fn parse_accelerator(accelerator_string: &str) -> Result<Accelerator, AcceleratorParseError> {
fn parse_accelerator(accelerator_string: &str) -> crate::Result<Accelerator> {
let mut mods = Modifiers::empty();
let mut key = Code::Unidentified;
for raw in accelerator_string.split('+') {
let token = raw.trim().to_string();
if token.is_empty() {
return Err(AcceleratorParseError(
return Err(crate::Error::AcceleratorParseError(
"Unexpected empty token while parsing accelerator".into(),
));
}
@ -165,7 +87,7 @@ fn parse_accelerator(accelerator_string: &str) -> Result<Accelerator, Accelerato
// examples:
// 1. "Ctrl+Shift+C+A" => only one main key should be allowd.
// 2. "Ctrl+C+Shift" => wrong order
return Err(AcceleratorParseError(format!(
return Err(crate::Error::AcceleratorParseError(format!(
"Unexpected accelerator string format: \"{}\"",
accelerator_string
)));
@ -179,14 +101,14 @@ fn parse_accelerator(accelerator_string: &str) -> Result<Accelerator, Accelerato
mods.set(Modifiers::CONTROL, true);
}
"COMMAND" | "CMD" | "SUPER" => {
mods.set(Modifiers::SUPER, true);
mods.set(Modifiers::META, true);
}
"SHIFT" => {
mods.set(Modifiers::SHIFT, true);
}
"COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => {
#[cfg(target_os = "macos")]
mods.set(Modifiers::SUPER, true);
mods.set(Modifiers::META, true);
#[cfg(not(target_os = "macos"))]
mods.set(Modifiers::CONTROL, true);
}
@ -194,7 +116,7 @@ fn parse_accelerator(accelerator_string: &str) -> Result<Accelerator, Accelerato
if let Ok(code) = Code::from_str(token.as_str()) {
match code {
Code::Unidentified => {
return Err(AcceleratorParseError(format!(
return Err(crate::Error::AcceleratorParseError(format!(
"Couldn't identify \"{}\" as a valid `Code`",
token
)))
@ -202,7 +124,7 @@ fn parse_accelerator(accelerator_string: &str) -> Result<Accelerator, Accelerato
_ => key = code,
}
} else {
return Err(AcceleratorParseError(format!(
return Err(crate::Error::AcceleratorParseError(format!(
"Couldn't identify \"{}\" as a valid `Code`",
token
)));
@ -240,7 +162,7 @@ fn test_parse_accelerator() {
assert_eq!(
parse_accelerator("super+ctrl+SHIFT+alt+ArrowUp").unwrap(),
Accelerator {
mods: Modifiers::SUPER | Modifiers::CONTROL | Modifiers::SHIFT | Modifiers::ALT,
mods: Modifiers::META | Modifiers::CONTROL | Modifiers::SHIFT | Modifiers::ALT,
key: Code::ArrowUp,
}
);
@ -276,7 +198,7 @@ fn test_parse_accelerator() {
parse_accelerator("CmdOrCtrl+Space").unwrap(),
Accelerator {
#[cfg(target_os = "macos")]
mods: Modifiers::SUPER,
mods: Modifiers::META,
#[cfg(not(target_os = "macos"))]
mods: Modifiers::CONTROL,
key: Code::Space,

80
src/check_menu_item.rs Normal file
View file

@ -0,0 +1,80 @@
use crate::{accelerator::Accelerator, MenuItemExt, MenuItemType};
/// A check menu item inside a [`Menu`] or [`Submenu`]
/// and usually contains a text and a check mark or a similar toggle
/// that corresponds to a checked and unchecked states.
///
/// [`Menu`]: crate::Menu
/// [`Submenu`]: crate::Submenu
#[derive(Clone)]
pub struct CheckMenuItem(pub(crate) crate::platform_impl::CheckMenuItem);
unsafe impl MenuItemExt for CheckMenuItem {
fn type_(&self) -> MenuItemType {
MenuItemType::Check
}
fn as_any(&self) -> &(dyn std::any::Any + 'static) {
self
}
fn id(&self) -> u32 {
self.id()
}
}
impl CheckMenuItem {
/// Create a new check menu item.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this check menu item. To display a `&` without assigning a mnemenonic, use `&&`
pub fn new<S: AsRef<str>>(
text: S,
enabled: bool,
checked: bool,
acccelerator: Option<Accelerator>,
) -> Self {
Self(crate::platform_impl::CheckMenuItem::new(
text.as_ref(),
enabled,
checked,
acccelerator,
))
}
/// Returns a unique identifier associated with this submenu.
pub fn id(&self) -> u32 {
self.0.id()
}
/// Get the text for this check menu item.
pub fn text(&self) -> String {
self.0.text()
}
/// Get the text for this check menu item. `text` could optionally contain
/// an `&` before a character to assign this character as the mnemonic
/// for this check menu item. To display a `&` without assigning a mnemenonic, use `&&`
pub fn set_text<S: AsRef<str>>(&self, text: S) {
self.0.set_text(text.as_ref())
}
/// Get whether this check menu item is enabled or not.
pub fn is_enabled(&self) -> bool {
self.0.is_enabled()
}
/// Enable or disable this check menu item.
pub fn set_enabled(&self, enabled: bool) {
self.0.set_enabled(enabled)
}
/// Get whether this check menu item is checked or not.
pub fn is_checked(&self) -> bool {
self.0.is_checked()
}
/// Check or Uncheck this check menu item.
pub fn set_checked(&self, checked: bool) {
self.0.set_checked(checked)
}
}

View file

@ -1,23 +0,0 @@
#![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)
}
}

22
src/error.rs Normal file
View file

@ -0,0 +1,22 @@
use thiserror::Error;
/// Errors returned by muda.
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum Error {
#[error("This menu item is not a child of this `Menu` or `Submenu`")]
NotAChildOfThisMenu,
#[cfg(windows)]
#[error("This menu has not been initialized for this hwnd`")]
NotInitialized,
#[cfg(target_os = "linux")]
#[error("This menu has not been initialized for this gtk window`")]
NotInitialized,
#[error("{0}")]
AcceleratorParseError(String),
#[error("Cannot map {0} to gdk key")]
AcceleratorKeyNotSupported(keyboard_types::Code),
}
/// Convenient type alias of Result type for muda.
pub type Result<T> = std::result::Result<T, Error>;

View file

@ -1,498 +1,230 @@
//! muda is a Menu Utilities library for Desktop Applications.
//! # Creating root menus
//!
//! Before you can add submenus and menu items, you first need a root or a base menu.
//! # Example
//!
//! Create the menu and add your items
//!
//! ```no_run
//! let mut menu = muda::Menu::new();
//! # use muda::{Menu, Submenu, MenuItem, accelerator::{Code, Modifiers, Accelerator}, PredefinedMenuItem};
//! let menu = Menu::new();
//! let menu_item2 = MenuItem::new("Menu item #2", false, None);
//! let submenu = Submenu::with_items(
//! "Submenu Outer",
//! true,
//! &[
//! &MenuItem::new(
//! "Menu item #1",
//! true,
//! Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyD)),
//! ),
//! &PredefinedMenuItem::separator(),
//! &menu_item2,
//! &MenuItem::new("Menu item #3", true, None),
//! &PredefinedMenuItem::separator(),
//! &Submenu::with_items(
//! "Submenu Inner",
//! true,
//! &[
//! &MenuItem::new("Submenu item #1", true, None),
//! &PredefinedMenuItem::separator(),
//! &menu_item2,
//! ],
//! ),
//! ],
//! );
//! ```
//!
//! # Adding submens to the root menu
//! Then Add your root menu to a Window on Windows and Linux Only or use it
//! as your global app menu on macOS
//!
//! Once you have a root menu you can start adding [`Submenu`]s by using [`Menu::add_submenu`].
//! ```no_run
//! let mut menu = muda::Menu::new();
//! let file_menu = menu.add_submenu("File", true);
//! let edit_menu = menu.add_submenu("Edit", true);
//! ```
//!
//! # Aadding menu items and submenus within another submenu
//!
//! Once you have a [`Submenu`] you can star creating more [`Submenu`]s or [`MenuItem`]s.
//! ```no_run
//! let mut menu = muda::Menu::new();
//!
//! let mut file_menu = menu.add_submenu("File", true);
//! let open_item = file_menu.add_item("Open", true, None);
//! let save_item = file_menu.add_item("Save", true, None);
//!
//! let mut edit_menu = menu.add_submenu("Edit", true);
//! let copy_item = file_menu.add_item("Copy", true, None);
//! let cut_item = file_menu.add_item("Cut", true, None);
//! ```
//!
//! # Add your root menu to a Window (Windows and Linux Only)
//!
//! You can use [`Menu`] to display a top menu in a Window on Windows and Linux.
//! ```ignore
//! let mut menu = muda::Menu::new();
//! # let menu = muda::Menu::new();
//! # let window_hwnd = 0;
//! # #[cfg(target_os = "linux")]
//! # let gtk_window = gtk::ApplicationWindow::builder().build();
//! // --snip--
//! #[cfg(target_os = "windows")]
//! menu.init_for_hwnd(window.hwnd() as isize);
//! menu.init_for_hwnd(window_hwnd);
//! #[cfg(target_os = "linux")]
//! menu.init_for_gtk_window(&gtk_window);
//! #[cfg(target_os = "macos")]
//! menu.init_for_nsapp();
//! ```
//!
//! # Context menus (Popup menus)
//!
//! You can also use a [`Menu`] or a [`Submenu`] show a context menu.
//!
//! ```no_run
//! use muda::ContextMenu;
//! # let menu = muda::Menu::new();
//! # let window_hwnd = 0;
//! # #[cfg(target_os = "linux")]
//! # let gtk_window = gtk::ApplicationWindow::builder().build();
//! # #[cfg(target_os = "macos")]
//! # let nsview = 0 as *mut objc::runtime::Object;
//! // --snip--
//! let x = 100.0;
//! let y = 120.0;
//! #[cfg(target_os = "windows")]
//! menu.show_context_menu_for_hwnd(window_hwnd, x, y);
//! #[cfg(target_os = "linux")]
//! menu.show_context_menu_for_gtk_window(&gtk_window, x, y);
//! #[cfg(target_os = "macos")]
//! menu.show_context_menu_for_nsview(nsview, x, y);
//! ```
//! # Processing menu events
//!
//! You can use [`menu_event_receiver`] to get a reference to the [`MenuEventReceiver`]
//! which you can use to listen to events when a menu item is activated
//! ```ignore
//! if let Ok(event) = muda::menu_event_receiver().try_recv() {
//! ```no_run
//! # use muda::menu_event_receiver;
//! #
//! # let save_item: muda::MenuItem = unsafe { std::mem::zeroed() };
//! if let Ok(event) = menu_event_receiver().try_recv() {
//! match event.id {
//! _ if event.id == save_item.id() => {
//! id if id == save_item.id() => {
//! println!("Save menu item activated");
//! },
//! _ => {}
//! }
//! }
//! ```
//!
//! # Accelerators on Windows
//!
//! Accelerators don't work unless the win32 message loop calls
//! [`TranslateAcceleratorW`](windows_sys::Win32::UI::WindowsAndMessaging::TranslateAcceleratorW)
//!
//! See [`Menu::init_for_hwnd`] for more details
use accelerator::Accelerator;
use crossbeam_channel::{unbounded, Receiver, Sender};
use once_cell::sync::Lazy;
pub mod accelerator;
mod counter;
mod check_menu_item;
mod error;
mod menu;
mod menu_item;
mod platform_impl;
mod predefined;
mod submenu;
mod util;
static MENU_CHANNEL: Lazy<(Sender<MenuEvent>, Receiver<MenuEvent>)> = Lazy::new(|| unbounded());
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
/// Gets a reference to the event channel's [Receiver<MenuEvent>]
/// which can be used to listen for menu events.
pub fn menu_event_receiver<'a>() -> &'a Receiver<MenuEvent> {
&MENU_CHANNEL.1
pub use self::error::*;
pub use check_menu_item::CheckMenuItem;
pub use menu::Menu;
pub use menu_item::MenuItem;
pub use predefined::{AboutMetadata, PredefinedMenuItem};
pub use submenu::Submenu;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MenuItemType {
Submenu,
Normal,
Check,
Predefined,
}
impl Default for MenuItemType {
fn default() -> Self {
Self::Normal
}
}
/// A trait that defines a generic item in a menu, which may be one of [MenuItemType]
///
/// # Safety
///
/// This trait is ONLY meant to be implemented internally.
// TODO(amrbashir): first person to replace this trait with an enum while keeping `Menu.append_items`
// taking mix of types (`MenuItem`, `CheckMenuItem`, `Submenu`...etc) in the same call, gets a cookie.
pub unsafe trait MenuItemExt {
/// Get the type of this menu entry
fn type_(&self) -> MenuItemType;
/// Casts this menu entry to [`Any`](std::any::Any).
///
/// You can use this to get the concrete underlying type
/// when calling [`Menu::items`] or [`Submenu::items`] by calling [`downcast_ref`](https://doc.rust-lang.org/std/any/trait.Any.html#method.downcast_ref-1)
///
/// ## Example
///
/// ```no_run
/// # use muda::{Submenu, MenuItem};
/// let submenu = Submenu::new("Submenu", true);
/// let item = MenuItem::new("Text", true, None);
/// submenu.append(&item);
/// // --snip--
/// let item = &submenu.items()[0];
/// let item = item.as_any().downcast_ref::<MenuItem>().unwrap();
/// item.set_text("New text")
/// ````
fn as_any(&self) -> &(dyn std::any::Any + 'static);
/// Returns the id associated with this menu entry
fn id(&self) -> u32;
}
pub trait ContextMenu {
/// Get the popup [`HMENU`] for this menu.
///
/// [`HMENU`]: windows_sys::Win32::UI::WindowsAndMessaging::HMENU
#[cfg(target_os = "windows")]
fn hpopupmenu(&self) -> windows_sys::Win32::UI::WindowsAndMessaging::HMENU;
/// Shows this menu as a context menu inside a win32 window.
///
/// `x` and `y` are relative to the window's top-left corner.
#[cfg(target_os = "windows")]
fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64);
/// Attach the menu subclass handler to the given hwnd
/// so you can recieve events from that window using [menu_event_receiver]
///
/// This can be used along with [`ContextMenu::hpopupmenu`] when implementing a tray icon menu.
#[cfg(target_os = "windows")]
fn attach_menu_subclass_for_hwnd(&self, hwnd: isize);
/// Remove the menu subclass handler from the given hwnd
#[cfg(target_os = "windows")]
fn detach_menu_subclass_from_hwnd(&self, hwnd: isize);
/// Shows this menu as a context menu inside a [`gtk::ApplicationWindow`]
///
/// `x` and `y` are relative to the window's top-left corner.
#[cfg(target_os = "linux")]
fn show_context_menu_for_gtk_window(&self, w: &gtk::ApplicationWindow, x: f64, y: f64);
/// Get the underlying gtk menu reserved for context menus.
#[cfg(target_os = "linux")]
fn gtk_context_menu(&self) -> gtk::Menu;
/// Shows this menu as a context menu for the specified `NSView`.
///
/// The menu will be shown at the coordinates of the current event
/// (the click which triggered the menu to be shown).
#[cfg(target_os = "macos")]
fn show_context_menu_for_nsview(&self, view: cocoa::base::id, x: f64, y: f64);
}
/// Describes a menu event emitted when a menu item is activated
#[derive(Debug)]
pub struct MenuEvent {
/// Id of the menu item which triggered this event
pub id: u64,
pub id: u32,
}
/// This is the root menu type to which you can add
/// more submenus and later be add to the top of a window (on Windows and Linux)
/// or used as the menubar menu (on macOS) or displayed as a popup menu.
///
/// # Example
///
/// ```no_run
/// let mut menu = muda::Menu::new();
/// let file_menu = menu.add_submenu("File", true);
/// let edit_menu = menu.add_submenu("Edit", true);
/// ```
#[derive(Clone)]
pub struct Menu(platform_impl::Menu);
/// A reciever that could be used to listen to menu events.
pub type MenuEventReceiver = Receiver<MenuEvent>;
impl Menu {
/// Creates a new root menu.
pub fn new() -> Self {
Self(platform_impl::Menu::new())
}
static MENU_CHANNEL: Lazy<(Sender<MenuEvent>, MenuEventReceiver)> = Lazy::new(unbounded);
/// Creates a new [`Submenu`] whithin this menu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** The menu label can contain `&` 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<S: AsRef<str>>(&mut self, label: S, enabled: bool) -> Submenu {
Submenu(self.0.add_submenu(label, enabled))
}
/// Adds this menu to a [`gtk::ApplicationWindow`]
///
/// This method adds a [`gtk::Box`] then adds a [`gtk::MenuBar`] as its first child and returns the [`gtk::Box`].
/// So if more widgets need to be added, then [`gtk::prelude::BoxExt::pack_start`] or
/// similiar methods should be used on the returned [`gtk::Box`].
///
/// ## Safety:
///
/// This should be called before anything is added to the window.
///
/// ## Panics:
///
/// Panics if the gtk event loop hasn't been initialized on the thread.
#[cfg(target_os = "linux")]
pub fn init_for_gtk_window<W>(&self, w: &W) -> std::rc::Rc<gtk::Box>
where
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
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()
}
/// Removes this menu from a [`gtk::ApplicationWindow`]
#[cfg(target_os = "linux")]
pub fn remove_for_gtk_window<W>(&self, w: &W)
where
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
W: gtk::prelude::IsA<gtk::Window>,
{
self.0.remove_for_gtk_window(w)
}
/// Removes this menu from a win32 window
#[cfg(target_os = "windows")]
pub fn remove_for_hwnd(&self, hwnd: isize) {
self.0.remove_for_hwnd(hwnd)
}
/// Hides this menu from a [`gtk::ApplicationWindow`]
#[cfg(target_os = "linux")]
pub fn hide_for_gtk_window<W>(&self, w: &W)
where
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
{
self.0.hide_for_gtk_window(w)
}
/// Hides this menu from a win32 window
#[cfg(target_os = "windows")]
pub fn hide_for_hwnd(&self, hwnd: isize) {
self.0.hide_for_hwnd(hwnd)
}
/// Shows this menu from a [`gtk::ApplicationWindow`]
#[cfg(target_os = "linux")]
pub fn show_for_gtk_window<W>(&self, w: &W)
where
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
{
self.0.show_for_gtk_window(w)
}
/// Shows this menu from a win32 window
#[cfg(target_os = "windows")]
pub fn show_for_hwnd(&self, hwnd: isize) {
self.0.show_for_hwnd(hwnd)
}
/// Adds this menu to an NSApp.
#[cfg(target_os = "macos")]
pub fn init_for_nsapp(&self) {
self.0.init_for_nsapp()
}
/// Removes this menu from an NSApp.
#[cfg(target_os = "macos")]
pub fn remove_for_nsapp(&self) {
self.0.remove_for_nsapp()
}
}
/// This is a Submenu within another [`Submenu`] or [`Menu`].
#[derive(Clone)]
pub struct Submenu(platform_impl::Submenu);
impl Submenu {
/// Gets the submenus's current label.
pub fn label(&self) -> String {
self.0.label()
}
/// Sets a new label for the submenu.
pub fn set_label<S: AsRef<str>>(&mut self, label: S) {
self.0.set_label(label)
}
/// Gets the submenu's current state, whether enabled or not.
pub fn enabled(&self) -> bool {
self.0.enabled()
}
/// Enables or disables the submenu
pub fn set_enabled(&mut self, enabled: bool) {
self.0.set_enabled(enabled)
}
/// Creates a new [`Submenu`] whithin this submenu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** The menu label can contain `&` 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<S: AsRef<str>>(&mut self, label: S, enabled: bool) -> Submenu {
Submenu(self.0.add_submenu(label, enabled))
}
/// Creates a new [`MenuItem`] whithin this submenu.
///
/// ## Platform-specific:
///
/// - **Windows / Linux:** The menu item label can contain `&` 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_item<S: AsRef<str>>(
&mut self,
label: S,
enabled: bool,
accelerator: Option<Accelerator>,
) -> MenuItem {
MenuItem(self.0.add_item(label, enabled, accelerator))
}
/// Creates a new [`NativeMenuItem`] within this submenu.
pub fn add_native_item(&mut self, item: NativeMenuItem) {
self.0.add_native_item(item)
}
/// Creates a new [`CheckMenuItem`] within this submenu.
pub fn add_check_item<S: AsRef<str>>(
&mut self,
label: S,
enabled: bool,
checked: bool,
accelerator: Option<Accelerator>,
) -> CheckMenuItem {
CheckMenuItem(self.0.add_check_item(label, enabled, checked, accelerator))
}
}
/// This is a normal menu item within a [`Submenu`].
#[derive(Clone)]
pub struct MenuItem(platform_impl::MenuItem);
impl MenuItem {
/// Gets the menu item's current label.
pub fn label(&self) -> String {
self.0.label()
}
/// Sets a new label for the menu item.
pub fn set_label<S: AsRef<str>>(&mut self, label: S) {
self.0.set_label(label)
}
/// Gets the menu item's current state, whether enabled or not.
pub fn enabled(&self) -> bool {
self.0.enabled()
}
/// Enables or disables the menu item.
pub fn set_enabled(&mut self, enabled: bool) {
self.0.set_enabled(enabled)
}
/// Gets the unique id for this menu item.
pub fn id(&self) -> u64 {
self.0.id()
}
}
/// This is a menu item with a checkmark icon within a [`Submenu`].
#[derive(Clone)]
pub struct CheckMenuItem(platform_impl::CheckMenuItem);
impl CheckMenuItem {
/// Gets the menu item's current label.
pub fn label(&self) -> String {
self.0.label()
}
/// Sets a new label for the menu item.
pub fn set_label<S: AsRef<str>>(&mut self, label: S) {
self.0.set_label(label)
}
/// Gets the menu item's current state, whether enabled or not.
pub fn enabled(&self) -> bool {
self.0.enabled()
}
/// Enables or disables the menu item.
pub fn set_enabled(&mut self, enabled: bool) {
self.0.set_enabled(enabled)
}
/// Gets the menu item's current state, whether checked or not.
pub fn checked(&self) -> bool {
self.0.checked()
}
/// Enables or disables the menu item.
pub fn set_checked(&mut self, checked: bool) {
self.0.set_checked(checked)
}
/// Gets the unique id for this menu item.
pub fn id(&self) -> u64 {
self.0.id()
}
}
/// This is a Native menu item within a [`Submenu`] with a predefined behavior.
#[non_exhaustive]
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum NativeMenuItem {
/// A native “About” menu item.
///
/// The first value is the application name, and the second is its metadata.
///
/// ## platform-specific:
///
/// - **macOS:** the metadata is ignore.
About(String, AboutMetadata),
/// A native “hide the app” menu item.
///
/// ## platform-specific:
///
/// - **Windows / Linux:** Unsupported.
Hide,
/// A native “hide all other windows" menu item.
///
/// ## platform-specific:
///
/// - **Windows / Linux:** Unsupported.
HideOthers,
/// A native "Show all windows for this app" menu item.
///
/// ## platform-specific:
///
/// - **Windows / Linux:** Unsupported.
ShowAll,
/// A native "Services" menu item.
///
/// ## platform-specific:
///
/// - **Windows / Linux:** Unsupported.
Services,
/// A native "Close current window" menu item.
CloseWindow,
/// A native "Quit///
Quit,
/// A native "Copy" menu item.
///
/// ## Platform-specific:
///
/// - **macOS:** macOS require this menu item to enable "Copy" keyboard shortcut for your app.
/// - **Linux Wayland:** Not implmeneted.
Copy,
/// A native "Cut" menu item.
///
/// ## Platform-specific:
///
/// - **macOS:** macOS require this menu item to enable "Cut" keyboard shortcut for your app.
/// - **Linux Wayland:** Not implmeneted.
Cut,
/// A native "Paste" menu item.
///
/// ## Platform-specific:
///
/// - **macOS:** macOS require this menu item to enable "Paste" keyboard shortcut for your app.
/// - **Linux Wayland:** Not implmeneted.
Paste,
/// A native "Undo" menu item.
///
/// ## Platform-specific:
///
/// - **macOS:** macOS require this menu item to enable "Undo" keyboard shortcut for your app.
/// - **Windows / Linux:** Unsupported.
Undo,
/// A native "Redo" menu item.
///
/// ## Platform-specific:
///
/// - **macOS:** macOS require this menu item to enable "Redo" keyboard shortcut for your app.
/// - **Windows / Linux:** Unsupported.
Redo,
/// A native "Select All" menu item.
///
/// ## Platform-specific:
///
/// - **macOS:** macOS require this menu item to enable "Select All" keyboard shortcut for your app.
/// - **Linux Wayland:** Not implmeneted.
SelectAll,
/// A native "Toggle fullscreen" menu item.
///
/// ## platform-specific:
///
/// - **Windows / Linux:** Unsupported.
ToggleFullScreen,
/// A native "Minimize current window" menu item.
Minimize,
/// A native "Zoom" menu item.
///
/// ## platform-specific:
///
/// - **Windows / Linux:** Unsupported.
Zoom,
/// Represends a Separator in the menu.
Separator,
}
/// Application metadata for the [`NativeMenuItem::About`].
///
/// ## Platform-specific
///
/// - **macOS:** The metadata is ignored.
#[derive(PartialEq, Eq, Debug, Clone, Default)]
pub struct AboutMetadata {
/// The application name.
pub version: Option<String>,
/// The authors of the application.
pub authors: Option<Vec<String>>,
/// Application comments.
pub comments: Option<String>,
/// The copyright of the application.
pub copyright: Option<String>,
/// The license of the application.
pub license: Option<String>,
/// The application website.
pub website: Option<String>,
/// The website label.
pub website_label: Option<String>,
/// Gets a reference to the event channel's [MenuEventReceiver]
/// which can be used to listen for menu events.
pub fn menu_event_receiver<'a>() -> &'a MenuEventReceiver {
&MENU_CHANNEL.1
}

261
src/menu.rs Normal file
View file

@ -0,0 +1,261 @@
use crate::{ContextMenu, MenuItemExt};
/// A root menu that can be added to a Window on Windows and Linux
/// and used as the app global menu on macOS.
#[derive(Clone)]
pub struct Menu(crate::platform_impl::Menu);
impl Default for Menu {
fn default() -> Self {
Self::new()
}
}
impl ContextMenu for Menu {
#[cfg(target_os = "windows")]
fn hpopupmenu(&self) -> windows_sys::Win32::UI::WindowsAndMessaging::HMENU {
self.0.hpopupmenu()
}
#[cfg(target_os = "windows")]
fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64) {
self.0.show_context_menu_for_hwnd(hwnd, x, y)
}
#[cfg(target_os = "windows")]
fn attach_menu_subclass_for_hwnd(&self, hwnd: isize) {
self.0.attach_menu_subclass_for_hwnd(hwnd)
}
#[cfg(target_os = "windows")]
fn detach_menu_subclass_from_hwnd(&self, hwnd: isize) {
self.0.detach_menu_subclass_from_hwnd(hwnd)
}
#[cfg(target_os = "linux")]
fn show_context_menu_for_gtk_window(&self, w: &gtk::ApplicationWindow, x: f64, y: f64) {
self.0.show_context_menu_for_gtk_window(w, x, y)
}
#[cfg(target_os = "linux")]
fn gtk_context_menu(&self) -> gtk::Menu {
self.0.gtk_context_menu()
}
#[cfg(target_os = "macos")]
fn show_context_menu_for_nsview(&self, view: cocoa::base::id, x: f64, y: f64) {
self.0.show_context_menu_for_nsview(view, x, y)
}
}
impl Menu {
/// Creates a new menu.
pub fn new() -> Self {
Self(crate::platform_impl::Menu::new())
}
/// Creates a new menu with given `items`. It calls [`Menu::new`] and [`Menu::append_items`] internally.
pub fn with_items(items: &[&dyn MenuItemExt]) -> Self {
let menu = Self::new();
menu.append_items(items);
menu
}
/// Add a menu item to the end of this menu.
///
/// ## Platform-spcific:
///
/// - **macOS:** Only [`Submenu`] can be added to the menu
///
/// [`Submenu`]: crate::Submenu
pub fn append(&self, item: &dyn MenuItemExt) {
self.0.append(item)
}
/// Add menu items to the end of this menu. It calls [`Menu::append`] in a loop internally.
///
/// ## Platform-spcific:
///
/// - **macOS:** Only [`Submenu`] can be added to the menu
///
/// [`Submenu`]: crate::Submenu
pub fn append_items(&self, items: &[&dyn MenuItemExt]) {
for item in items {
self.append(*item);
}
}
/// Add a menu item to the beginning of this menu.
///
/// ## Platform-spcific:
///
/// - **macOS:** Only [`Submenu`] can be added to the menu
///
/// [`Submenu`]: crate::Submenu
pub fn prepend(&self, item: &dyn MenuItemExt) {
self.0.prepend(item)
}
/// Add menu items to the beginning of this menu. It calls [`Menu::insert_items`] with position of `0` internally.
///
/// ## Platform-spcific:
///
/// - **macOS:** Only [`Submenu`] can be added to the menu
///
/// [`Submenu`]: crate::Submenu
pub fn prepend_items(&self, items: &[&dyn MenuItemExt]) {
self.insert_items(items, 0);
}
/// Insert a menu item at the specified `postion` in the menu.
///
/// ## Platform-spcific:
///
/// - **macOS:** Only [`Submenu`] can be added to the menu
///
/// [`Submenu`]: crate::Submenu
pub fn insert(&self, item: &dyn MenuItemExt, position: usize) {
self.0.insert(item, position)
}
/// Insert menu items at the specified `postion` in the menu.
///
/// ## Platform-spcific:
///
/// - **macOS:** Only [`Submenu`] can be added to the menu
///
/// [`Submenu`]: crate::Submenu
pub fn insert_items(&self, items: &[&dyn MenuItemExt], position: usize) {
for (i, item) in items.iter().enumerate() {
self.insert(*item, position + i)
}
}
/// Remove a menu item from this menu.
pub fn remove(&self, item: &dyn MenuItemExt) -> crate::Result<()> {
self.0.remove(item)
}
/// Returns a list of menu items that has been added to this menu.
pub fn items(&self) -> Vec<Box<dyn MenuItemExt>> {
self.0.items()
}
/// Adds this menu to a [`gtk::ApplicationWindow`]
///
/// This method adds a [`gtk::Box`] then adds a [`gtk::MenuBar`] as its first child and returns the [`gtk::Box`].
/// So if more widgets need to be added, then [`gtk::prelude::BoxExt::pack_start`] or
/// similiar methods should be used on the returned [`gtk::Box`].
///
/// ## Safety:
///
/// This should be called before anything is added to the window.
///
/// ## Panics:
///
/// Panics if the gtk event loop hasn't been initialized on the thread.
#[cfg(target_os = "linux")]
pub fn init_for_gtk_window<W>(&self, w: &W) -> std::rc::Rc<gtk::Box>
where
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
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:
/// ```no_run
/// # use muda::Menu;
/// # use windows_sys::Win32::UI::WindowsAndMessaging::{MSG, GetMessageW, TranslateMessage, DispatchMessageW, TranslateAcceleratorW};
/// let menu = Menu::new();
/// unsafe {
/// let mut msg: MSG = std::mem::zeroed();
/// while GetMessageW(&mut msg, 0, 0, 0) == 1 {
/// let translated = TranslateAcceleratorW(msg.hwnd, menu.haccel(), &msg as *const _);
/// if translated != 1{
/// 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()
}
/// Removes this menu from a [`gtk::ApplicationWindow`]
#[cfg(target_os = "linux")]
pub fn remove_for_gtk_window<W>(&self, w: &W) -> crate::Result<()>
where
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
W: gtk::prelude::IsA<gtk::Window>,
{
self.0.remove_for_gtk_window(w)
}
/// Removes this menu from a win32 window
#[cfg(target_os = "windows")]
pub fn remove_for_hwnd(&self, hwnd: isize) -> crate::Result<()> {
self.0.remove_for_hwnd(hwnd)
}
/// Hides this menu from a [`gtk::ApplicationWindow`]
#[cfg(target_os = "linux")]
pub fn hide_for_gtk_window<W>(&self, w: &W) -> crate::Result<()>
where
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
{
self.0.hide_for_gtk_window(w)
}
/// Hides this menu from a win32 window
#[cfg(target_os = "windows")]
pub fn hide_for_hwnd(&self, hwnd: isize) -> crate::Result<()> {
self.0.hide_for_hwnd(hwnd)
}
/// Shows this menu on a [`gtk::ApplicationWindow`]
#[cfg(target_os = "linux")]
pub fn show_for_gtk_window<W>(&self, w: &W) -> crate::Result<()>
where
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
{
self.0.show_for_gtk_window(w)
}
/// Shows this menu on a win32 window
#[cfg(target_os = "windows")]
pub fn show_for_hwnd(&self, hwnd: isize) -> crate::Result<()> {
self.0.show_for_hwnd(hwnd)
}
/// Adds this menu to an NSApp.
#[cfg(target_os = "macos")]
pub fn init_for_nsapp(&self) {
self.0.init_for_nsapp()
}
/// Removes this menu from an NSApp.
#[cfg(target_os = "macos")]
pub fn remove_for_nsapp(&self) {
self.0.remove_for_nsapp()
}
}

62
src/menu_item.rs Normal file
View file

@ -0,0 +1,62 @@
use crate::{accelerator::Accelerator, MenuItemExt, MenuItemType};
/// A menu item inside a [`Menu`] or [`Submenu`] and contains only text.
///
/// [`Menu`]: crate::Menu
/// [`Submenu`]: crate::Submenu
#[derive(Clone)]
pub struct MenuItem(pub(crate) crate::platform_impl::MenuItem);
unsafe impl MenuItemExt for MenuItem {
fn type_(&self) -> MenuItemType {
MenuItemType::Normal
}
fn as_any(&self) -> &(dyn std::any::Any + 'static) {
self
}
fn id(&self) -> u32 {
self.id()
}
}
impl MenuItem {
/// Create a new menu item.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`
pub fn new<S: AsRef<str>>(text: S, enabled: bool, acccelerator: Option<Accelerator>) -> Self {
Self(crate::platform_impl::MenuItem::new(
text.as_ref(),
enabled,
acccelerator,
))
}
/// Returns a unique identifier associated with this menu item.
pub fn id(&self) -> u32 {
self.0.id()
}
/// Get the text for this menu item.
pub fn text(&self) -> String {
self.0.text()
}
/// Set the text for this menu item. `text` could optionally contain
/// an `&` before a character to assign this character as the mnemonic
/// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`
pub fn set_text<S: AsRef<str>>(&self, text: S) {
self.0.set_text(text.as_ref())
}
/// Get whether this menu item is enabled or not.
pub fn is_enabled(&self) -> bool {
self.0.is_enabled()
}
/// Enable or disable this menu item.
pub fn set_enabled(&self, enabled: bool) {
self.0.set_enabled(enabled)
}
}

View file

@ -3,20 +3,25 @@ use keyboard_types::{Code, Modifiers};
use crate::accelerator::Accelerator;
pub fn to_gtk_menemenoic<S: AsRef<str>>(string: S) -> String {
pub fn to_gtk_mnemonic<S: AsRef<str>>(string: S) -> String {
string
.as_ref()
.replace("&&", "[~~]")
.replace("&", "_")
.replace('&', "_")
.replace("[~~]", "&&")
.replace("[~~]", "&")
}
pub fn register_accelerator<M: IsA<gtk::Widget>>(
item: &M,
accel_group: &AccelGroup,
menu_key: &Accelerator,
) {
let accel_key = match &menu_key.key {
pub fn from_gtk_mnemonic<S: AsRef<str>>(string: S) -> String {
string
.as_ref()
.replace("__", "[~~]")
.replace('_', "&")
.replace("[~~]", "__")
}
pub fn parse_accelerator(accelerator: &Accelerator) -> crate::Result<(gdk::ModifierType, u32)> {
let key = match &accelerator.key {
Code::KeyA => 'A' as u32,
Code::KeyB => 'B' as u32,
Code::KeyC => 'C' as u32,
@ -69,19 +74,22 @@ pub fn register_accelerator<M: IsA<gtk::Widget>>(
if let Some(gdk_key) = key_to_raw_key(k) {
*gdk_key
} else {
dbg!("Cannot map key {:?}", k);
return;
return Err(crate::Error::AcceleratorKeyNotSupported(*k));
}
}
};
item.add_accelerator(
"activate",
accel_group,
accel_key,
modifiers_to_gdk_modifier_type(menu_key.mods),
gtk::AccelFlags::VISIBLE,
);
Ok((modifiers_to_gdk_modifier_type(accelerator.mods), key))
}
pub fn register_accelerator<M: IsA<gtk::Widget>>(
item: &M,
accel_group: &AccelGroup,
accelerator: &Accelerator,
) {
if let Ok((mods, key)) = parse_accelerator(accelerator) {
item.add_accelerator("activate", accel_group, key, mods, gtk::AccelFlags::VISIBLE);
}
}
fn modifiers_to_gdk_modifier_type(modifiers: Modifiers) -> gdk::ModifierType {

1691
src/platform_impl/gtk/mod.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,690 +0,0 @@
mod accelerator;
use crate::{accelerator::Accelerator, counter::Counter, NativeMenuItem};
use accelerator::{register_accelerator, to_gtk_menemenoic};
use gtk::{prelude::*, Orientation};
use std::{cell::RefCell, collections::HashMap, rc::Rc};
static COUNTER: Counter = Counter::new();
/// Generic shared type describing a menu entry. It can be one of [`MenuEntryType`]
#[derive(Debug, Default)]
struct MenuEntry {
label: String,
enabled: bool,
checked: bool,
id: u64,
accelerator: Option<Accelerator>,
r#type: MenuEntryType,
entries: Option<Vec<Rc<RefCell<MenuEntry>>>>,
}
#[derive(PartialEq, Eq, Debug)]
enum MenuEntryType {
// 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`] if its a menu
// and push to it every time [`Menu::init_for_gtk_window`] is called.
Submenu(Vec<(gtk::MenuItem, gtk::Menu)>),
MenuItem(Vec<gtk::MenuItem>),
CheckMenuItem(Vec<gtk::CheckMenuItem>),
NativeMenuItem(NativeMenuItem),
}
impl Default for MenuEntryType {
fn default() -> Self {
Self::MenuItem(Default::default())
}
}
struct InnerMenu {
entries: Vec<Rc<RefCell<MenuEntry>>>,
// NOTE(amrbashir): because gtk doesn't allow using the same [`gtk::MenuBar`] and [`gtk::Box`]
// multiple times, and thus can't be used in multiple windows. each menu
// keeps a hashmap of window pointer as the key and a tuple of [`gtk::MenuBar`] and [`gtk::Box`] as the value
// and push to it every time `Menu::init_for_gtk_window` is called.
native_menus: HashMap<isize, (Option<gtk::MenuBar>, Rc<gtk::Box>)>,
accel_group: Rc<gtk::AccelGroup>,
}
#[derive(Clone)]
pub struct Menu(Rc<RefCell<InnerMenu>>);
impl Menu {
pub fn new() -> Self {
Self(Rc::new(RefCell::new(InnerMenu {
entries: Vec::new(),
native_menus: HashMap::new(),
accel_group: Rc::new(gtk::AccelGroup::new()),
})))
}
pub fn add_submenu<S: AsRef<str>>(&mut self, label: S, enabled: bool) -> Submenu {
let label = label.as_ref().to_string();
let entry = Rc::new(RefCell::new(MenuEntry {
label: label.clone(),
enabled,
entries: Some(Vec::new()),
r#type: MenuEntryType::Submenu(Vec::new()),
..Default::default()
}));
let mut inner = self.0.borrow_mut();
for (_, (menu_bar, _)) in inner.native_menus.iter() {
if let Some(menu_bar) = menu_bar {
let (item, submenu) = create_gtk_submenu(&label, enabled);
menu_bar.append(&item);
let mut native_menus = entry.borrow_mut();
if let MenuEntryType::Submenu(m) = &mut native_menus.r#type {
m.push((item, submenu));
}
}
}
inner.entries.push(entry.clone());
Submenu(entry, Rc::clone(&inner.accel_group))
}
pub fn init_for_gtk_window<W>(&self, window: &W) -> Rc<gtk::Box>
where
W: IsA<gtk::ApplicationWindow>,
W: IsA<gtk::Container>,
W: IsA<gtk::Window>,
{
let mut inner = self.0.borrow_mut();
// This is the first time this method has been called on this window
// so we need to create the menubar and its parent box
if inner.native_menus.get(&(window.as_ptr() as _)).is_none() {
let menu_bar = gtk::MenuBar::new();
let vbox = gtk::Box::new(Orientation::Vertical, 0);
window.add(&vbox);
vbox.show();
inner
.native_menus
.insert(window.as_ptr() as _, (Some(menu_bar), Rc::new(vbox)));
}
if let Some((menu_bar, vbox)) = inner.native_menus.get(&(window.as_ptr() as _)) {
// This is NOT the first time this method has been called on a window.
// So it already contains a [`gtk::Box`] but it doesn't have a [`gtk::MenuBar`]
// because it was probably removed using [`Menu::remove_for_gtk_window`]
// so we only need to create the menubar
if menu_bar.is_none() {
let vbox = Rc::clone(vbox);
inner
.native_menus
.insert(window.as_ptr() as _, (Some(gtk::MenuBar::new()), vbox));
}
}
// Construct the entries of the menubar
let (menu_bar, vbox) = inner.native_menus.get(&(window.as_ptr() as _)).unwrap();
add_entries_to_menu(
menu_bar.as_ref().unwrap(),
&inner.entries,
&inner.accel_group,
);
window.add_accel_group(&*inner.accel_group);
// Show the menubar on the window
vbox.pack_start(menu_bar.as_ref().unwrap(), false, false, 0);
menu_bar.as_ref().unwrap().show();
Rc::clone(vbox)
}
pub fn remove_for_gtk_window<W>(&self, window: &W)
where
W: IsA<gtk::ApplicationWindow>,
W: IsA<gtk::Window>,
{
let mut inner = self.0.borrow_mut();
if let Some((Some(menu_bar), vbox)) = inner.native_menus.get(&(window.as_ptr() as _)) {
// Remove the [`gtk::Menubar`] from the widget tree
unsafe { menu_bar.destroy() };
// Detach the accelerators from the window
window.remove_accel_group(&*inner.accel_group);
// Remove the removed [`gtk::Menubar`] from our cache
let vbox = Rc::clone(vbox);
inner
.native_menus
.insert(window.as_ptr() as _, (None, vbox));
}
}
pub fn hide_for_gtk_window<W>(&self, window: &W)
where
W: IsA<gtk::ApplicationWindow>,
{
if let Some((Some(menu_bar), _)) = self
.0
.borrow()
.native_menus
.get(&(window.as_ptr() as isize))
{
menu_bar.hide();
}
}
pub fn show_for_gtk_window<W>(&self, window: &W)
where
W: IsA<gtk::ApplicationWindow>,
{
if let Some((Some(menu_bar), _)) = self
.0
.borrow()
.native_menus
.get(&(window.as_ptr() as isize))
{
menu_bar.show_all();
}
}
}
#[derive(Clone)]
pub struct Submenu(Rc<RefCell<MenuEntry>>, Rc<gtk::AccelGroup>);
impl Submenu {
pub fn label(&self) -> String {
self.0.borrow().label.clone()
}
pub fn set_label<S: AsRef<str>>(&mut self, label: S) {
let label = label.as_ref().to_string();
let mut entry = self.0.borrow_mut();
if let MenuEntryType::Submenu(native_menus) = &mut entry.r#type {
for (item, _) in native_menus {
item.set_label(&to_gtk_menemenoic(&label));
}
}
entry.label = label;
}
pub fn enabled(&self) -> bool {
self.0.borrow().enabled
}
pub fn set_enabled(&mut self, enabled: bool) {
let mut entry = self.0.borrow_mut();
entry.enabled = true;
if let MenuEntryType::Submenu(native_menus) = &mut entry.r#type {
for (item, _) in native_menus {
item.set_sensitive(enabled);
}
}
}
pub fn add_submenu<S: AsRef<str>>(&mut self, label: S, enabled: bool) -> Submenu {
let label = label.as_ref().to_string();
let entry = Rc::new(RefCell::new(MenuEntry {
label: label.clone(),
enabled,
entries: Some(Vec::new()),
r#type: MenuEntryType::Submenu(Vec::new()),
..Default::default()
}));
let mut inner = self.0.borrow_mut();
if let MenuEntryType::Submenu(native_menus) = &mut inner.r#type {
for (_, menu) in native_menus {
let (item, submenu) = create_gtk_submenu(&label, enabled);
menu.append(&item);
if let MenuEntryType::Submenu(menus) = &mut entry.borrow_mut().r#type {
menus.push((item, submenu));
}
}
}
inner.entries.as_mut().unwrap().push(entry.clone());
Submenu(entry, Rc::clone(&self.1))
}
pub fn add_item<S: AsRef<str>>(
&mut self,
label: S,
enabled: bool,
accelerator: Option<Accelerator>,
) -> MenuItem {
let label = label.as_ref().to_string();
let id = COUNTER.next();
let entry = Rc::new(RefCell::new(MenuEntry {
label: label.clone(),
enabled,
r#type: MenuEntryType::MenuItem(Vec::new()),
id,
accelerator: accelerator.clone(),
..Default::default()
}));
let mut inner = self.0.borrow_mut();
if let MenuEntryType::Submenu(native_menus) = &mut inner.r#type {
for (_, menu) in native_menus {
let item = create_gtk_menu_item(&label, enabled, &accelerator, id, &*self.1);
menu.append(&item);
if let MenuEntryType::MenuItem(native_items) = &mut entry.borrow_mut().r#type {
native_items.push(item);
}
}
}
inner.entries.as_mut().unwrap().push(entry.clone());
MenuItem(entry)
}
pub fn add_native_item(&mut self, item: NativeMenuItem) {
let mut inner = self.0.borrow_mut();
if let MenuEntryType::Submenu(native_menus) = &mut inner.r#type {
for (_, menu) in native_menus {
item.add_to_gtk_menu(menu);
}
}
let entry = Rc::new(RefCell::new(MenuEntry {
r#type: MenuEntryType::NativeMenuItem(item),
..Default::default()
}));
inner.entries.as_mut().unwrap().push(entry);
}
pub fn add_check_item<S: AsRef<str>>(
&mut self,
label: S,
enabled: bool,
checked: bool,
accelerator: Option<Accelerator>,
) -> CheckMenuItem {
let label = label.as_ref().to_string();
let id = COUNTER.next();
let entry = Rc::new(RefCell::new(MenuEntry {
label: label.clone(),
enabled,
checked,
r#type: MenuEntryType::CheckMenuItem(Vec::new()),
id,
accelerator: accelerator.clone(),
..Default::default()
}));
let mut inner = self.0.borrow_mut();
if let MenuEntryType::Submenu(native_menus) = &mut inner.r#type {
for (_, menu) in native_menus {
let item = create_gtk_check_menu_item(
&label,
enabled,
checked,
&accelerator,
id,
&*self.1,
);
menu.append(&item);
if let MenuEntryType::CheckMenuItem(native_items) = &mut entry.borrow_mut().r#type {
native_items.push(item);
}
}
}
inner.entries.as_mut().unwrap().push(entry.clone());
CheckMenuItem(entry)
}
}
#[derive(Clone)]
pub struct MenuItem(Rc<RefCell<MenuEntry>>);
impl MenuItem {
pub fn label(&self) -> String {
self.0.borrow().label.clone()
}
pub fn set_label<S: AsRef<str>>(&mut self, label: S) {
let label = label.as_ref().to_string();
let mut entry = self.0.borrow_mut();
if let MenuEntryType::MenuItem(native_items) = &mut entry.r#type {
for item in native_items {
item.set_label(&to_gtk_menemenoic(&label));
}
}
entry.label = label;
}
pub fn enabled(&self) -> bool {
self.0.borrow().enabled
}
pub fn set_enabled(&mut self, enabled: bool) {
let mut entry = self.0.borrow_mut();
if let MenuEntryType::MenuItem(native_items) = &mut entry.r#type {
for item in native_items {
item.set_sensitive(enabled);
}
}
entry.enabled = enabled;
}
pub fn id(&self) -> u64 {
self.0.borrow().id
}
}
#[derive(Clone)]
pub struct CheckMenuItem(Rc<RefCell<MenuEntry>>);
impl CheckMenuItem {
pub fn label(&self) -> String {
self.0.borrow().label.clone()
}
pub fn set_label<S: AsRef<str>>(&mut self, label: S) {
let label = label.as_ref().to_string();
let mut entry = self.0.borrow_mut();
if let MenuEntryType::CheckMenuItem(native_items) = &mut entry.r#type {
for item in native_items {
item.set_label(&to_gtk_menemenoic(&label));
}
}
entry.label = label;
}
pub fn enabled(&self) -> bool {
self.0.borrow().enabled
}
pub fn set_enabled(&mut self, enabled: bool) {
let mut entry = self.0.borrow_mut();
if let MenuEntryType::CheckMenuItem(native_items) = &mut entry.r#type {
for item in native_items {
item.set_sensitive(enabled);
}
}
entry.enabled = enabled;
}
pub fn checked(&self) -> bool {
let entry = self.0.borrow();
let mut checked = entry.checked;
if let MenuEntryType::CheckMenuItem(native_items) = &entry.r#type {
if let Some(item) = native_items.get(0) {
checked = item.is_active();
}
}
checked
}
pub fn set_checked(&mut self, checked: bool) {
let mut entry = self.0.borrow_mut();
if let MenuEntryType::CheckMenuItem(native_items) = &mut entry.r#type {
for item in native_items {
item.set_active(checked);
}
}
entry.checked = checked;
}
pub fn id(&self) -> u64 {
self.0.borrow().id
}
}
fn add_entries_to_menu<M: IsA<gtk::MenuShell>>(
gtk_menu: &M,
entries: &Vec<Rc<RefCell<MenuEntry>>>,
accel_group: &gtk::AccelGroup,
) {
for entry in entries {
let mut entry = entry.borrow_mut();
let (item, submenu) = match &mut entry.r#type {
MenuEntryType::Submenu(_) => {
let (item, submenu) = create_gtk_submenu(&entry.label, entry.enabled);
gtk_menu.append(&item);
add_entries_to_menu(&submenu, entry.entries.as_ref().unwrap(), accel_group);
(Some(item), Some(submenu))
}
MenuEntryType::MenuItem(_) => {
let item = create_gtk_menu_item(
&entry.label,
entry.enabled,
&entry.accelerator,
entry.id,
accel_group,
);
gtk_menu.append(&item);
(Some(item), None)
}
MenuEntryType::CheckMenuItem(_) => {
let item = create_gtk_check_menu_item(
&entry.label,
entry.enabled,
entry.checked,
&entry.accelerator,
entry.id,
accel_group,
);
gtk_menu.append(&item);
(Some(item.upcast::<gtk::MenuItem>()), None)
}
MenuEntryType::NativeMenuItem(native_menu_item) => {
native_menu_item.add_to_gtk_menu(gtk_menu);
(None, None)
}
};
match &mut entry.r#type {
MenuEntryType::Submenu(native_menus) => {
native_menus.push((item.unwrap(), submenu.unwrap()));
}
MenuEntryType::MenuItem(native_items) => {
native_items.push(item.unwrap());
}
MenuEntryType::CheckMenuItem(native_items) => {
native_items.push(item.unwrap().downcast().unwrap());
}
MenuEntryType::NativeMenuItem(_) => {}
};
}
}
fn create_gtk_submenu(label: &str, enabled: bool) -> (gtk::MenuItem, gtk::Menu) {
let item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(label));
item.set_sensitive(enabled);
let menu = gtk::Menu::new();
item.set_submenu(Some(&menu));
item.show();
(item, menu)
}
fn create_gtk_menu_item(
label: &str,
enabled: bool,
accelerator: &Option<Accelerator>,
id: u64,
accel_group: &gtk::AccelGroup,
) -> gtk::MenuItem {
let item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(label));
item.set_sensitive(enabled);
if let Some(accelerator) = accelerator {
register_accelerator(&item, accel_group, accelerator);
}
item.connect_activate(move |_| {
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id });
});
item.show();
item
}
fn create_gtk_check_menu_item(
label: &str,
enabled: bool,
checked: bool,
accelerator: &Option<Accelerator>,
id: u64,
accel_group: &gtk::AccelGroup,
) -> gtk::CheckMenuItem {
let item = gtk::CheckMenuItem::with_mnemonic(&to_gtk_menemenoic(label));
item.set_sensitive(enabled);
item.set_active(checked);
if let Some(accelerator) = accelerator {
register_accelerator(&item, accel_group, accelerator);
}
item.connect_activate(move |_| {
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id });
});
item.show();
item
}
impl NativeMenuItem {
fn add_to_gtk_menu<M: IsA<gtk::MenuShell>>(&self, gtk_menu: &M) {
match self {
NativeMenuItem::Copy => {
let item = gtk::MenuItem::with_mnemonic("_Copy");
let (key, modifiers) = gtk::accelerator_parse("<Ctrl>X");
item.child()
.unwrap()
.downcast::<gtk::AccelLabel>()
.unwrap()
.set_accel(key, modifiers);
item.connect_activate(move |_| {
// TODO: wayland
if let Ok(xdo) = libxdo::XDo::new(None) {
let _ = xdo.send_keysequence("ctrl+c", 0);
}
});
item.show();
gtk_menu.append(&item);
}
NativeMenuItem::Cut => {
let item = gtk::MenuItem::with_mnemonic("Cu_t");
let (key, modifiers) = gtk::accelerator_parse("<Ctrl>X");
item.child()
.unwrap()
.downcast::<gtk::AccelLabel>()
.unwrap()
.set_accel(key, modifiers);
item.connect_activate(move |_| {
// TODO: wayland
if let Ok(xdo) = libxdo::XDo::new(None) {
let _ = xdo.send_keysequence("ctrl+x", 0);
}
});
item.show();
gtk_menu.append(&item);
}
NativeMenuItem::Paste => {
let item = gtk::MenuItem::with_mnemonic("_Paste");
let (key, modifiers) = gtk::accelerator_parse("<Ctrl>V");
item.child()
.unwrap()
.downcast::<gtk::AccelLabel>()
.unwrap()
.set_accel(key, modifiers);
item.connect_activate(move |_| {
// TODO: wayland
if let Ok(xdo) = libxdo::XDo::new(None) {
let _ = xdo.send_keysequence("ctrl+v", 0);
}
});
item.show();
gtk_menu.append(&item);
}
NativeMenuItem::SelectAll => {
let item = gtk::MenuItem::with_mnemonic("Select _All");
let (key, modifiers) = gtk::accelerator_parse("<Ctrl>A");
item.child()
.unwrap()
.downcast::<gtk::AccelLabel>()
.unwrap()
.set_accel(key, modifiers);
item.connect_activate(move |_| {
// TODO: wayland
if let Ok(xdo) = libxdo::XDo::new(None) {
let _ = xdo.send_keysequence("ctrl+a", 0);
}
});
item.show();
gtk_menu.append(&item);
}
NativeMenuItem::Separator => {
gtk_menu.append(&gtk::SeparatorMenuItem::new());
}
NativeMenuItem::Minimize => {
let item = gtk::MenuItem::with_mnemonic("_Minimize");
item.connect_activate(move |m| {
if let Some(window) = m.window() {
window.iconify()
}
});
item.show();
gtk_menu.append(&item);
}
NativeMenuItem::CloseWindow => {
let item = gtk::MenuItem::with_mnemonic("C_lose Window");
item.connect_activate(move |m| {
if let Some(window) = m.window() {
window.destroy()
}
});
item.show();
gtk_menu.append(&item);
}
NativeMenuItem::Quit => {
let item = gtk::MenuItem::with_mnemonic("_Quit");
item.connect_activate(move |_| {
std::process::exit(0);
});
item.show();
gtk_menu.append(&item);
}
NativeMenuItem::About(app_name, metadata) => {
let app_name = app_name.clone();
let metadata = metadata.clone();
let item = gtk::MenuItem::with_label(&format!("About {}", app_name));
item.connect_activate(move |_| {
let mut builder = gtk::builders::AboutDialogBuilder::new()
.program_name(&app_name)
.modal(true)
.resizable(false);
if let Some(version) = &metadata.version {
builder = builder.version(version);
}
if let Some(authors) = &metadata.authors {
builder = builder.authors(authors.clone());
}
if let Some(comments) = &metadata.comments {
builder = builder.comments(comments);
}
if let Some(copyright) = &metadata.copyright {
builder = builder.copyright(copyright);
}
if let Some(license) = &metadata.license {
builder = builder.license(license);
}
if let Some(website) = &metadata.website {
builder = builder.website(website);
}
if let Some(website_label) = &metadata.website_label {
builder = builder.website_label(website_label);
}
let about = builder.build();
about.run();
unsafe {
about.destroy();
}
});
item.show();
gtk_menu.append(&item);
}
_ => {}
}
}
}

View file

@ -3,17 +3,11 @@ use keyboard_types::{Code, Modifiers};
use crate::accelerator::Accelerator;
/// Mnemonic is deprecated since macOS 10
pub fn remove_mnemonic(string: impl AsRef<str>) -> String {
string.as_ref().replace("&", "")
}
impl Accelerator {
/// Return the string value of this hotkey, for use with Cocoa `NSResponder`
/// objects.
/// Return the string value of this hotkey, without modifiers.
///
/// Returns the empty string if no key equivalent is known.
pub fn key_equivalent(&self) -> String {
pub fn key_equivalent(self) -> String {
match self.key {
Code::KeyA => "a".into(),
Code::KeyB => "b".into(),
@ -113,13 +107,14 @@ impl Accelerator {
}
}
pub fn key_modifier_mask(&self) -> NSEventModifierFlags {
/// Return the modifiers of this hotkey, as an NSEventModifierFlags bitflag.
pub fn key_modifier_mask(self) -> NSEventModifierFlags {
let mods: Modifiers = self.mods;
let mut flags = NSEventModifierFlags::empty();
if mods.contains(Modifiers::SHIFT) {
flags.insert(NSEventModifierFlags::NSShiftKeyMask);
}
if mods.contains(Modifiers::SUPER) {
if mods.contains(Modifiers::META) {
flags.insert(NSEventModifierFlags::NSCommandKeyMask);
}
if mods.contains(Modifiers::ALT) {

View file

@ -1,271 +0,0 @@
use crate::accelerator::Accelerator;
use crate::counter::Counter;
use crate::platform_impl::platform_impl::accelerator::remove_mnemonic;
use cocoa::{
appkit::{NSButton, NSEventModifierFlags, NSMenuItem},
base::{id, nil, BOOL, NO, YES},
foundation::NSString,
};
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{Class, Object, Sel},
sel, sel_impl,
};
use std::rc::Rc;
use std::sync::Once;
static COUNTER: Counter = Counter::new();
#[derive(Debug, Clone)]
pub struct MenuItem {
pub(crate) id: u64,
pub(crate) ns_menu_item: id,
label: Rc<str>,
}
impl MenuItem {
pub fn new<S: AsRef<str>>(
label: S,
enabled: bool,
selector: Sel,
accelerator: Option<Accelerator>,
) -> Self {
let (id, ns_menu_item) = make_menu_item(&remove_mnemonic(&label), selector, accelerator);
unsafe {
(&mut *ns_menu_item).set_ivar(MENU_IDENTITY, id);
let () = msg_send![&*ns_menu_item, setTarget:&*ns_menu_item];
if !enabled {
let () = msg_send![ns_menu_item, setEnabled: NO];
}
}
Self {
id,
ns_menu_item,
label: Rc::from(label.as_ref()),
}
}
pub fn label(&self) -> String {
self.label.to_string()
}
pub fn set_label<S: AsRef<str>>(&mut self, label: S) {
unsafe {
let title = NSString::alloc(nil).init_str(&remove_mnemonic(&label));
self.ns_menu_item.setTitle_(title);
}
self.label = Rc::from(label.as_ref());
}
pub fn enabled(&self) -> bool {
unsafe {
let enabled: BOOL = msg_send![self.ns_menu_item, isEnabled];
enabled == YES
}
}
pub fn set_enabled(&mut self, enabled: bool) {
unsafe {
let status = match enabled {
true => YES,
false => NO,
};
let () = msg_send![self.ns_menu_item, setEnabled: status];
}
}
pub fn id(&self) -> u64 {
self.id
}
}
#[derive(Debug, Clone)]
pub struct CheckMenuItem {
pub(crate) id: u64,
pub(crate) ns_menu_item: id,
label: Rc<str>,
}
impl CheckMenuItem {
pub fn new<S: AsRef<str>>(
label: S,
enabled: bool,
checked: bool,
selector: Sel,
accelerator: Option<Accelerator>,
) -> Self {
let (id, ns_menu_item) = make_menu_item(&remove_mnemonic(&label), selector, accelerator);
unsafe {
(&mut *ns_menu_item).set_ivar(MENU_IDENTITY, id);
let () = msg_send![&*ns_menu_item, setTarget:&*ns_menu_item];
if !enabled {
let () = msg_send![ns_menu_item, setEnabled: NO];
}
if checked {
let () = msg_send![ns_menu_item, setState: 1_isize];
}
}
Self {
id,
ns_menu_item,
label: Rc::from(label.as_ref()),
}
}
pub fn label(&self) -> String {
self.label.to_string()
}
pub fn set_label<S: AsRef<str>>(&mut self, label: S) {
unsafe {
let title = NSString::alloc(nil).init_str(&remove_mnemonic(&label));
self.ns_menu_item.setTitle_(title);
}
self.label = Rc::from(label.as_ref());
}
pub fn enabled(&self) -> bool {
unsafe {
let enabled: BOOL = msg_send![self.ns_menu_item, isEnabled];
enabled == YES
}
}
pub fn set_enabled(&mut self, enabled: bool) {
unsafe {
let status = match enabled {
true => YES,
false => NO,
};
let () = msg_send![self.ns_menu_item, setEnabled: status];
}
}
pub fn checked(&self) -> bool {
unsafe {
let checked: isize = msg_send![self.ns_menu_item, state];
checked == 1_isize
}
}
pub fn set_checked(&mut self, checked: bool) {
unsafe {
let state = match checked {
true => 1_isize,
false => 0_isize,
};
let () = msg_send![self.ns_menu_item, setState: state];
}
}
pub fn id(&self) -> u64 {
self.id
}
}
pub fn make_menu_item(
title: &str,
selector: Sel,
accelerator: Option<Accelerator>,
) -> (u64, *mut Object) {
let alloc = make_menu_item_alloc();
let menu_id = COUNTER.next();
unsafe {
let title = NSString::alloc(nil).init_str(title);
let menu_item = make_menu_item_from_alloc(alloc, title, selector, accelerator);
(menu_id, menu_item)
}
}
fn make_menu_item_alloc() -> *mut Object {
unsafe { msg_send![make_menu_item_class(), alloc] }
}
static MENU_IDENTITY: &str = "MenuItemIdentity";
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("MenuItem", superclass).unwrap();
decl.add_ivar::<u64>(MENU_IDENTITY);
decl.add_method(
sel!(dealloc),
dealloc_custom_menuitem as extern "C" fn(&Object, _),
);
decl.add_method(
sel!(fireMenubarAction:),
fire_menu_bar_click as extern "C" fn(&Object, _, id),
);
decl.add_method(
sel!(fireStatusbarAction:),
fire_status_bar_click as extern "C" fn(&Object, _, id),
);
APP_CLASS = decl.register();
});
unsafe { APP_CLASS }
}
fn make_menu_item_from_alloc(
alloc: *mut Object,
title: *mut Object,
selector: Sel,
accelerator: Option<Accelerator>,
) -> *mut Object {
unsafe {
let (key_equivalent, masks) = match accelerator {
Some(accelerator) => {
let key = accelerator.key_equivalent();
let mods = accelerator.key_modifier_mask();
let key = NSString::alloc(nil).init_str(&key);
(key, mods)
}
None => (
NSString::alloc(nil).init_str(""),
NSEventModifierFlags::empty(),
),
};
// allocate our item to our class
let item: id =
msg_send![alloc, initWithTitle: title action: selector keyEquivalent: key_equivalent];
item.setKeyEquivalentModifierMask_(masks);
item
}
}
extern "C" fn fire_menu_bar_click(this: &Object, _: Sel, _item: id) {
send_event(this);
}
extern "C" fn fire_status_bar_click(this: &Object, _: Sel, _item: id) {
send_event(this);
}
extern "C" fn dealloc_custom_menuitem(this: &Object, _: Sel) {
unsafe {
let _: () = msg_send![super(this, class!(NSMenuItem)), dealloc];
}
}
fn send_event(this: &Object) {
let id: u64 = unsafe { *this.get_ivar(MENU_IDENTITY) };
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id: id as _ });
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
use std::{slice, str};
use cocoa::{
base::{id, nil},
foundation::NSString,
};
/// Strips single `&` characters from the string.
///
/// `&` can be escaped as `&&` to prevent stripping, in which case a single `&` will be output.
pub fn strip_mnemonic<S: AsRef<str>>(string: S) -> String {
string
.as_ref()
.replace("&&", "[~~]")
.replace('&', "")
.replace("[~~]", "&")
}
/// Copies the contents of the NSString into a `String` which gets returned.
pub(crate) unsafe fn ns_string_to_rust(ns_string: id) -> String {
let slice = slice::from_raw_parts(ns_string.UTF8String() as *mut u8, ns_string.len());
let string = str::from_utf8_unchecked(slice);
string.to_owned()
}
/// Gets the app's name from the `localizedName` property of `NSRunningApplication`.
pub(crate) unsafe fn app_name() -> Option<id> {
let app_class = class!(NSRunningApplication);
let app: id = msg_send![app_class, currentApplication];
let app_name: id = msg_send![app, localizedName];
if app_name != nil {
Some(app_name)
} else {
None
}
}
/// Gets the app's name as a `String` from the `localizedName` property of `NSRunningApplication`.
pub(crate) unsafe fn app_name_string() -> Option<String> {
app_name().map(|name| ns_string_to_rust(name))
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,49 @@
#[cfg(target_os = "windows")]
use std::ops::{Deref, DerefMut};
use windows_sys::Win32::UI::WindowsAndMessaging::ACCEL;
pub fn encode_wide<S: AsRef<std::ffi::OsStr>>(string: S) -> Vec<u16> {
std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())
.chain(std::iter::once(0))
.collect()
}
#[cfg(target_os = "windows")]
#[allow(non_snake_case)]
pub fn LOWORD(dword: u32) -> u16 {
(dword & 0xFFFF) as u16
}
#[cfg(target_os = "windows")]
pub fn decode_wide(w_str: *mut u16) -> String {
let len = unsafe { windows_sys::Win32::Globalization::lstrlenW(w_str) } as usize;
let w_str_slice = unsafe { std::slice::from_raw_parts(w_str, len) };
String::from_utf16_lossy(w_str_slice)
}
/// ACCEL wrapper to implement Debug
#[derive(Clone)]
#[repr(transparent)]
pub struct Accel(pub ACCEL);
impl std::fmt::Debug for Accel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ACCEL")
.field("key", &self.0.key)
.field("cmd", &self.0.cmd)
.field("fVirt", &self.0.fVirt)
.finish()
}
}
impl Deref for Accel {
type Target = ACCEL;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Accel {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

244
src/predefined.rs Normal file
View file

@ -0,0 +1,244 @@
use crate::{accelerator::Accelerator, MenuItemExt, MenuItemType};
use keyboard_types::{Code, Modifiers};
#[cfg(target_os = "macos")]
pub const CMD_OR_CTRL: Modifiers = Modifiers::META;
#[cfg(not(target_os = "macos"))]
pub const CMD_OR_CTRL: Modifiers = Modifiers::CONTROL;
/// A predefined (native) menu item which has a predfined behavior by the OS or by this crate.
pub struct PredefinedMenuItem(pub(crate) crate::platform_impl::PredefinedMenuItem);
unsafe impl MenuItemExt for PredefinedMenuItem {
fn type_(&self) -> MenuItemType {
MenuItemType::Predefined
}
fn as_any(&self) -> &(dyn std::any::Any + 'static) {
self
}
fn id(&self) -> u32 {
self.id()
}
}
impl PredefinedMenuItem {
pub fn separator() -> PredefinedMenuItem {
PredefinedMenuItem::new::<&str>(PredfinedMenuItemType::Separator, None)
}
pub fn copy(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::Copy, text)
}
pub fn cut(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::Cut, text)
}
pub fn paste(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::Paste, text)
}
pub fn select_all(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::SelectAll, text)
}
pub fn undo(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::Undo, text)
}
pub fn redo(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::Redo, text)
}
pub fn minimize(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::Minimize, text)
}
pub fn maximize(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::Maximize, text)
}
pub fn fullscreen(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::Fullscreen, text)
}
pub fn hide(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::Hide, text)
}
pub fn hide_others(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::HideOthers, text)
}
pub fn show_all(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::ShowAll, text)
}
pub fn close_window(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::CloseWindow, text)
}
pub fn quit(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::Quit, text)
}
pub fn about(text: Option<&str>, metadata: Option<AboutMetadata>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::About(metadata), text)
}
pub fn services(text: Option<&str>) -> PredefinedMenuItem {
PredefinedMenuItem::new(PredfinedMenuItemType::Services, text)
}
fn new<S: AsRef<str>>(item: PredfinedMenuItemType, text: Option<S>) -> Self {
Self(crate::platform_impl::PredefinedMenuItem::new(
item,
text.map(|t| t.as_ref().to_string()),
))
}
fn id(&self) -> u32 {
self.0.id()
}
/// Get the text for this predefined menu item.
pub fn text(&self) -> String {
self.0.text()
}
/// Set the text for this predefined menu item.
pub fn set_text<S: AsRef<str>>(&self, text: S) {
self.0.set_text(text.as_ref())
}
}
/// Application metadata for the [`PredefinedMenuItem::about`].
///
/// ## Platform-specific
///
/// - **macOS:** The metadata is ignored.
#[derive(PartialEq, Eq, Debug, Clone, Default)]
pub struct AboutMetadata {
/// The application name.
pub name: Option<String>,
/// The application version.
pub version: Option<String>,
/// The authors of the application.
pub authors: Option<Vec<String>>,
/// Application comments.
pub comments: Option<String>,
/// The copyright of the application.
pub copyright: Option<String>,
/// The license of the application.
pub license: Option<String>,
/// The application website.
pub website: Option<String>,
/// The website label.
pub website_label: Option<String>,
}
#[derive(PartialEq, Eq, Debug, Clone)]
#[non_exhaustive]
pub(crate) enum PredfinedMenuItemType {
Separator,
Copy,
Cut,
Paste,
SelectAll,
Undo,
Redo,
Minimize,
Maximize,
Fullscreen,
Hide,
HideOthers,
ShowAll,
CloseWindow,
Quit,
About(Option<AboutMetadata>),
Services,
None,
}
impl Default for PredfinedMenuItemType {
fn default() -> Self {
Self::None
}
}
impl PredfinedMenuItemType {
pub(crate) fn text(&self) -> &str {
match self {
PredfinedMenuItemType::Separator => "",
PredfinedMenuItemType::Copy => "&Copy",
PredfinedMenuItemType::Cut => "Cu&t",
PredfinedMenuItemType::Paste => "&Paste",
PredfinedMenuItemType::SelectAll => "Select &All",
PredfinedMenuItemType::Undo => "Undo",
PredfinedMenuItemType::Redo => "Redo",
PredfinedMenuItemType::Minimize => "&Minimize",
#[cfg(target_os = "macos")]
PredfinedMenuItemType::Maximize => "Zoom",
#[cfg(not(target_os = "macos"))]
PredfinedMenuItemType::Maximize => "Maximize",
PredfinedMenuItemType::Fullscreen => "Toggle Full Screen",
PredfinedMenuItemType::Hide => "Hide",
PredfinedMenuItemType::HideOthers => "Hide Others",
PredfinedMenuItemType::ShowAll => "Show All",
#[cfg(windows)]
PredfinedMenuItemType::CloseWindow => "Close",
#[cfg(not(windows))]
PredfinedMenuItemType::CloseWindow => "C&lose Window",
#[cfg(windows)]
PredfinedMenuItemType::Quit => "&Exit",
#[cfg(not(windows))]
PredfinedMenuItemType::Quit => "&Quit",
PredfinedMenuItemType::About(_) => "&About",
PredfinedMenuItemType::Services => "Services",
PredfinedMenuItemType::None => "",
}
}
pub(crate) fn accelerator(&self) -> Option<Accelerator> {
match self {
PredfinedMenuItemType::Copy => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyC)),
PredfinedMenuItemType::Cut => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyX)),
PredfinedMenuItemType::Paste => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyV)),
PredfinedMenuItemType::Undo => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyZ)),
#[cfg(target_os = "macos")]
PredfinedMenuItemType::Redo => Some(Accelerator::new(
Some(CMD_OR_CTRL | Modifiers::SHIFT),
Code::KeyZ,
)),
#[cfg(not(target_os = "macos"))]
PredfinedMenuItemType::Redo => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyY)),
PredfinedMenuItemType::SelectAll => {
Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyA))
}
PredfinedMenuItemType::Minimize => {
Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyM))
}
#[cfg(target_os = "macos")]
PredfinedMenuItemType::Fullscreen => Some(Accelerator::new(
Some(Modifiers::META | Modifiers::CONTROL),
Code::KeyF,
)),
PredfinedMenuItemType::Hide => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyH)),
PredfinedMenuItemType::HideOthers => Some(Accelerator::new(
Some(CMD_OR_CTRL | Modifiers::ALT),
Code::KeyH,
)),
#[cfg(target_os = "macos")]
PredfinedMenuItemType::CloseWindow => {
Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyW))
}
#[cfg(not(target_os = "macos"))]
PredfinedMenuItemType::CloseWindow => {
Some(Accelerator::new(Some(Modifiers::ALT), Code::F4))
}
#[cfg(target_os = "macos")]
PredfinedMenuItemType::Quit => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyQ)),
_ => None,
}
}
}

168
src/submenu.rs Normal file
View file

@ -0,0 +1,168 @@
use crate::{ContextMenu, MenuItemExt, MenuItemType};
/// A menu that can be added to a [`Menu`] or another [`Submenu`].
///
/// [`Menu`]: crate::Menu
#[derive(Clone)]
pub struct Submenu(pub(crate) crate::platform_impl::Submenu);
unsafe impl MenuItemExt for Submenu {
fn type_(&self) -> MenuItemType {
MenuItemType::Submenu
}
fn as_any(&self) -> &(dyn std::any::Any + 'static) {
self
}
fn id(&self) -> u32 {
self.id()
}
}
impl ContextMenu for Submenu {
#[cfg(target_os = "windows")]
fn hpopupmenu(&self) -> windows_sys::Win32::UI::WindowsAndMessaging::HMENU {
self.0.hpopupmenu()
}
#[cfg(target_os = "windows")]
fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64) {
self.0.show_context_menu_for_hwnd(hwnd, x, y)
}
#[cfg(target_os = "windows")]
fn attach_menu_subclass_for_hwnd(&self, hwnd: isize) {
self.0.attach_menu_subclass_for_hwnd(hwnd)
}
#[cfg(target_os = "windows")]
fn detach_menu_subclass_from_hwnd(&self, hwnd: isize) {
self.0.detach_menu_subclass_from_hwnd(hwnd)
}
#[cfg(target_os = "linux")]
fn show_context_menu_for_gtk_window(&self, w: &gtk::ApplicationWindow, x: f64, y: f64) {
self.0.show_context_menu_for_gtk_window(w, x, y)
}
#[cfg(target_os = "linux")]
fn gtk_context_menu(&self) -> gtk::Menu {
self.0.gtk_context_menu()
}
#[cfg(target_os = "macos")]
fn show_context_menu_for_nsview(&self, view: cocoa::base::id, x: f64, y: f64) {
self.0.show_context_menu_for_nsview(view, x, y)
}
}
impl Submenu {
/// Create a new submenu.
///
/// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
/// for this submenu. To display a `&` without assigning a mnemenonic, use `&&`
pub fn new<S: AsRef<str>>(text: S, enabled: bool) -> Self {
Self(crate::platform_impl::Submenu::new(text.as_ref(), enabled))
}
/// Creates a new submenu with given `items`. It calls [`Submenu::new`] and [`Submenu::append_items`] internally.
pub fn with_items<S: AsRef<str>>(text: S, enabled: bool, items: &[&dyn MenuItemExt]) -> Self {
let menu = Self::new(text, enabled);
menu.append_items(items);
menu
}
/// Returns a unique identifier associated with this submenu.
pub fn id(&self) -> u32 {
self.0.id()
}
/// Add a menu item to the end of this menu.
pub fn append(&self, item: &dyn MenuItemExt) {
self.0.append(item)
}
/// Add menu items to the end of this submenu. It calls [`Submenu::append`] in a loop.
pub fn append_items(&self, items: &[&dyn MenuItemExt]) {
for item in items {
self.append(*item);
}
}
/// Add a menu item to the beginning of this submenu.
pub fn prepend(&self, item: &dyn MenuItemExt) {
self.0.prepend(item)
}
/// Add menu items to the beginning of this submenu.
/// It calls [`Menu::prepend`](crate::Menu::prepend) on the first element and
/// passes the rest to [`Menu::insert_items`](crate::Menu::insert_items) with position of `1`.
pub fn prepend_items(&self, items: &[&dyn MenuItemExt]) {
self.prepend(items[0]);
self.insert_items(&items[1..], 1);
}
/// Insert a menu item at the specified `postion` in the submenu.
pub fn insert(&self, item: &dyn MenuItemExt, position: usize) {
self.0.insert(item, position)
}
/// Insert menu items at the specified `postion` in the submenu.
pub fn insert_items(&self, items: &[&dyn MenuItemExt], position: usize) {
for (i, item) in items.iter().enumerate() {
self.insert(*item, position + i)
}
}
/// Remove a menu item from this submenu.
pub fn remove(&self, item: &dyn MenuItemExt) -> crate::Result<()> {
self.0.remove(item)
}
/// Returns a list of menu items that has been added to this submenu.
pub fn items(&self) -> Vec<Box<dyn MenuItemExt>> {
self.0.items()
}
/// Get the text for this submenu.
pub fn text(&self) -> String {
self.0.text()
}
/// Set the text for this submenu. `text` could optionally contain
/// an `&` before a character to assign this character as the mnemonic
/// for this submenu. To display a `&` without assigning a mnemenonic, use `&&`
pub fn set_text<S: AsRef<str>>(&self, text: S) {
self.0.set_text(text.as_ref())
}
/// Get whether this submenu is enabled or not.
pub fn is_enabled(&self) -> bool {
self.0.is_enabled()
}
/// Enable or disable this submenu.
pub fn set_enabled(&self, enabled: bool) {
self.0.set_enabled(enabled)
}
/// Set this submenu as the Window menu for the application on macOS.
///
/// This will cause macOS to automatically add window-switching items and
/// certain other items to the menu.
#[cfg(target_os = "macos")]
pub fn set_windows_menu_for_nsapp(&self) {
self.0.set_windows_menu_for_nsapp()
}
/// Set this submenu as the Help menu for the application on macOS.
///
/// This will cause macOS to automatically add a search box to the menu.
///
/// If no menu is set as the Help menu, macOS will automatically use any menu
/// which has a title matching the localized word "Help".
#[cfg(target_os = "macos")]
pub fn set_help_menu_for_nsapp(&self) {
self.0.set_help_menu_for_nsapp()
}
}

25
src/util.rs Normal file
View file

@ -0,0 +1,25 @@
use std::sync::atomic::{AtomicU32, Ordering};
#[derive(Clone, Copy, Debug)]
pub enum AddOp {
Append,
Insert(usize),
}
pub struct Counter(AtomicU32);
impl Counter {
#[allow(unused)]
pub const fn new() -> Self {
Self(AtomicU32::new(1))
}
#[allow(unused)]
pub const fn new_with_start(start: u32) -> Self {
Self(AtomicU32::new(start))
}
pub fn next(&self) -> u32 {
self.0.fetch_add(1, Ordering::Relaxed)
}
}