mirror of
https://github.com/italicsjenga/muda.git
synced 2024-12-23 20:11:29 +11:00
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:
parent
1f341d1e4e
commit
812ff0d37a
9
.github/workflows/clippy-fmt.yml
vendored
9
.github/workflows/clippy-fmt.yml
vendored
|
@ -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
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
/.vscode
|
11
Cargo.toml
11
Cargo.toml
|
@ -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" }
|
||||
|
|
77
README.md
77
README.md
|
@ -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(>k_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(>k_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
|
||||
```
|
||||
|
|
183
examples/tao.rs
183
examples/tao.rs
|
@ -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(&[©_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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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(&[©_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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
80
src/check_menu_item.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
22
src/error.rs
Normal 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>;
|
640
src/lib.rs
640
src/lib.rs
|
@ -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(>k_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(>k_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: >k::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
261
src/menu.rs
Normal 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: >k::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
62
src/menu_item.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
1691
src/platform_impl/gtk/mod.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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: >k::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: >k::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: >k::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(>k::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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
41
src/platform_impl/macos/util.rs
Normal file
41
src/platform_impl/macos/util.rs
Normal 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))
|
||||
}
|
|
@ -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
|
@ -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
244
src/predefined.rs
Normal 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
168
src/submenu.rs
Normal 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: >k::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
25
src/util.rs
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue