diff --git a/.changes/common-controls-v6.md b/.changes/common-controls-v6.md
new file mode 100644
index 0000000..ec16c3d
--- /dev/null
+++ b/.changes/common-controls-v6.md
@@ -0,0 +1,5 @@
+---
+"muda": "minor"
+---
+
+Add `common-controls-v6` feature flag, disabled by default, which could be used to enable usage of `TaskDialogIndirect` API from `ComCtl32.dll` v6 on Windows for The predefined `About` menu item.
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 09163d0..f3f48ce 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -14,15 +14,15 @@ env:
RUST_BACKTRACE: 1
concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
jobs:
test:
strategy:
fail-fast: false
matrix:
- os: ['windows-latest', 'macos-latest', 'ubuntu-latest']
+ os: ["windows-latest", "macos-latest", "ubuntu-latest"]
runs-on: ${{ matrix.os }}
@@ -44,4 +44,9 @@ jobs:
- uses: actions-rs/cargo@v1
with:
- command: test
\ No newline at end of file
+ command: test
+
+ - name: test common-controls-v6
+ if: matrix.os == "windows-latest"
+ working-directory: examples/windows-common-controls-v6
+ run: cargo run
diff --git a/Cargo.toml b/Cargo.toml
index ca62584..64e7c15 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,16 +3,17 @@ name = "muda"
version = "0.5.0"
description = "Menu Utilities for Desktop Applications"
edition = "2021"
-keywords = [ "windowing", "menu" ]
+keywords = ["windowing", "menu"]
license = "Apache-2.0 OR MIT"
readme = "README.md"
repository = "https://github.com/amrbashir/muda"
documentation = "https://docs.rs/muda"
-categories = [ "gui" ]
+categories = ["gui"]
[features]
-default = [ "libxdo" ]
-libxdo = [ "dep:libxdo" ]
+default = ["libxdo"]
+libxdo = ["dep:libxdo"]
+common-controls-v6 = ["windows-sys/Win32_UI_Controls"]
[dependencies]
crossbeam-channel = "0.5"
@@ -29,7 +30,7 @@ features = [
"Win32_UI_Shell",
"Win32_Globalization",
"Win32_UI_Input_KeyboardAndMouse",
- "Win32_System_SystemServices"
+ "Win32_System_SystemServices",
]
[target."cfg(target_os = \"linux\")".dependencies]
diff --git a/README.md b/README.md
index 3895641..b658e74 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,8 @@ muda is a Menu Utilities library for Desktop Applications.
### Cargo Features
-- `libxdo` (enabled by default): Enables linking to `libxdo` which is used for the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu item.
+- `common-controls-v6`: Use `TaskDialogIndirect` API from `ComCtl32.dll` v6 on Windows for showing the predefined `About` menu item dialog.
+- `libxdo`: Enables linking to `libxdo` on Linux which is used for the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu item.
## Dependencies (Linux Only)
diff --git a/examples/tao.rs b/examples/tao.rs
index a395d4b..45f8625 100644
--- a/examples/tao.rs
+++ b/examples/tao.rs
@@ -118,6 +118,7 @@ fn main() {
None,
Some(AboutMetadata {
name: Some("tao".to_string()),
+ version: Some("1.2.3".to_string()),
copyright: Some("Copyright tao".to_string()),
..Default::default()
}),
diff --git a/examples/windows-common-controls-v6/.gitignore b/examples/windows-common-controls-v6/.gitignore
new file mode 100644
index 0000000..869df07
--- /dev/null
+++ b/examples/windows-common-controls-v6/.gitignore
@@ -0,0 +1,2 @@
+/target
+Cargo.lock
\ No newline at end of file
diff --git a/examples/windows-common-controls-v6/Cargo.toml b/examples/windows-common-controls-v6/Cargo.toml
new file mode 100644
index 0000000..d2aa668
--- /dev/null
+++ b/examples/windows-common-controls-v6/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "windows-common-controls-v6"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+muda = { path = "../../", features = ["common-controls-v6"] }
+winit = "0.28"
+image = "0.24"
+
+[target."cfg(target_os = \"windows\")".dependencies.windows-sys]
+version = "0.48"
+features = ["Win32_UI_WindowsAndMessaging", "Win32_Foundation"]
+
+[build-dependencies]
+embed-resource = "1.6"
diff --git a/examples/windows-common-controls-v6/app.exe.manifest b/examples/windows-common-controls-v6/app.exe.manifest
new file mode 100644
index 0000000..d4da5bc
--- /dev/null
+++ b/examples/windows-common-controls-v6/app.exe.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/windows-common-controls-v6/build.rs b/examples/windows-common-controls-v6/build.rs
new file mode 100644
index 0000000..38154a2
--- /dev/null
+++ b/examples/windows-common-controls-v6/build.rs
@@ -0,0 +1,4 @@
+fn main() {
+ #[cfg(target_os = "windows")]
+ embed_resource::compile("manifest.rc");
+}
diff --git a/examples/windows-common-controls-v6/manifest.rc b/examples/windows-common-controls-v6/manifest.rc
new file mode 100644
index 0000000..6dae794
--- /dev/null
+++ b/examples/windows-common-controls-v6/manifest.rc
@@ -0,0 +1,2 @@
+#define RT_MANIFEST 24
+1 RT_MANIFEST "app.exe.manifest"
\ No newline at end of file
diff --git a/examples/windows-common-controls-v6/src/main.rs b/examples/windows-common-controls-v6/src/main.rs
new file mode 100644
index 0000000..efb6aac
--- /dev/null
+++ b/examples/windows-common-controls-v6/src/main.rs
@@ -0,0 +1,207 @@
+// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#![allow(unused)]
+use muda::{
+ accelerator::{Accelerator, Code, Modifiers},
+ AboutMetadata, CheckMenuItem, ContextMenu, IconMenuItem, Menu, MenuEvent, MenuItem,
+ PredefinedMenuItem, Submenu,
+};
+#[cfg(target_os = "macos")]
+use winit::platform::macos::{EventLoopBuilderExtMacOS, WindowExtMacOS};
+#[cfg(target_os = "windows")]
+use winit::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows};
+use winit::{
+ event::{ElementState, Event, MouseButton, WindowEvent},
+ event_loop::{ControlFlow, EventLoopBuilder},
+ window::{Window, WindowBuilder},
+};
+
+fn main() {
+ let mut event_loop_builder = EventLoopBuilder::new();
+
+ let 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
+ }
+ });
+ }
+ #[cfg(target_os = "macos")]
+ event_loop_builder.with_default_menu(false);
+
+ 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(Some(Modifiers::ALT), Code::KeyC)),
+ );
+
+ let path = concat!(env!("CARGO_MANIFEST_DIR"), "../../icon.png");
+ let icon = load_icon(std::path::Path::new(path));
+ let image_item = IconMenuItem::new("Image Custom 1", true, Some(icon), 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)),
+ );
+
+ 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,
+ &image_item,
+ &window_m,
+ &PredefinedMenuItem::separator(),
+ &check_custom_i_1,
+ &check_custom_i_2,
+ ]);
+
+ 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()),
+ version: Some("1.2.3".to_string()),
+ copyright: Some("Copyright winit".to_string()),
+ ..Default::default()
+ }),
+ ),
+ &check_custom_i_3,
+ &image_item,
+ &custom_i_1,
+ ]);
+
+ edit_m.append_items(&[©_i, &PredefinedMenuItem::separator(), &paste_i]);
+
+ #[cfg(target_os = "windows")]
+ {
+ menu_bar.init_for_hwnd(window.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 = MenuEvent::receiver();
+
+ let mut x = 0_f64;
+ let mut y = 0_f64;
+ event_loop.run(move |event, _, control_flow| {
+ *control_flow = ControlFlow::Wait;
+
+ match event {
+ Event::WindowEvent {
+ event: WindowEvent::CloseRequested,
+ ..
+ } => *control_flow = ControlFlow::Exit,
+ Event::WindowEvent {
+ event: WindowEvent::CursorMoved { position, .. },
+ window_id,
+ ..
+ } => {
+ if window_id == window2.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() {
+ show_context_menu(&window2, &file_m, x, y);
+ }
+ }
+ Event::MainEventsCleared => {
+ window.request_redraw();
+ }
+ _ => (),
+ }
+
+ if let Ok(event) = menu_channel.try_recv() {
+ if event.id == custom_i_1.id() {
+ file_m.insert(&MenuItem::new("New Menu Item", true, None), 2);
+ }
+ println!("{event:?}");
+ }
+ })
+}
+
+fn show_context_menu(window: &Window, menu: &dyn ContextMenu, x: f64, y: f64) {
+ #[cfg(target_os = "windows")]
+ menu.show_context_menu_for_hwnd(window.hwnd() as _, x, y);
+ #[cfg(target_os = "macos")]
+ menu.show_context_menu_for_nsview(window.ns_view() as _, x, y);
+}
+
+fn load_icon(path: &std::path::Path) -> muda::icon::Icon {
+ let (icon_rgba, icon_width, icon_height) = {
+ let image = image::open(path)
+ .expect("Failed to open icon path")
+ .into_rgba8();
+ let (width, height) = image.dimensions();
+ let rgba = image.into_raw();
+ (rgba, width, height)
+ };
+ muda::icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon")
+}
diff --git a/examples/winit.rs b/examples/winit.rs
index e6d5896..5018887 100644
--- a/examples/winit.rs
+++ b/examples/winit.rs
@@ -113,6 +113,7 @@ fn main() {
None,
Some(AboutMetadata {
name: Some("winit".to_string()),
+ version: Some("1.2.3".to_string()),
copyright: Some("Copyright winit".to_string()),
..Default::default()
}),
diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs
index 2575021..5a1a1e8 100644
--- a/src/platform_impl/windows/mod.rs
+++ b/src/platform_impl/windows/mod.rs
@@ -13,7 +13,7 @@ use crate::{
icon::Icon,
predefined::PredfinedMenuItemType,
util::{AddOp, Counter},
- MenuEvent, MenuItemType,
+ AboutMetadata, MenuEvent, MenuItemType,
};
use std::{
cell::{RefCell, RefMut},
@@ -31,12 +31,11 @@ use windows_sys::Win32::{
WindowsAndMessaging::{
AppendMenuW, CreateAcceleratorTableW, CreateMenu, CreatePopupMenu,
DestroyAcceleratorTable, DrawMenuBar, EnableMenuItem, GetMenuItemInfoW, InsertMenuW,
- MessageBoxW, PostQuitMessage, RemoveMenu, SendMessageW, SetMenu, SetMenuItemInfoW,
- ShowWindow, TrackPopupMenu, HACCEL, HMENU, MB_ICONINFORMATION, MENUITEMINFOW,
- MFS_CHECKED, MFS_DISABLED, MF_BYCOMMAND, MF_BYPOSITION, MF_CHECKED, MF_DISABLED,
- MF_ENABLED, MF_GRAYED, MF_POPUP, MF_SEPARATOR, MF_STRING, MF_UNCHECKED, MIIM_BITMAP,
- MIIM_STATE, MIIM_STRING, SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, TPM_LEFTALIGN, WM_CLOSE,
- WM_COMMAND, WM_DESTROY,
+ PostQuitMessage, RemoveMenu, SendMessageW, SetMenu, SetMenuItemInfoW, ShowWindow,
+ TrackPopupMenu, HACCEL, HMENU, MENUITEMINFOW, MFS_CHECKED, MFS_DISABLED, MF_BYCOMMAND,
+ MF_BYPOSITION, MF_CHECKED, MF_DISABLED, MF_ENABLED, MF_GRAYED, MF_POPUP, MF_SEPARATOR,
+ MF_STRING, MF_UNCHECKED, MIIM_BITMAP, MIIM_STATE, MIIM_STRING, SW_HIDE, SW_MAXIMIZE,
+ SW_MINIMIZE, TPM_LEFTALIGN, WM_CLOSE, WM_COMMAND, WM_DESTROY,
},
},
};
@@ -1160,53 +1159,7 @@ unsafe extern "system" fn menu_subclass_proc(
PostQuitMessage(0);
}
PredfinedMenuItemType::About(Some(metadata)) => {
- use std::fmt::Write;
-
- let mut message = String::new();
- if let Some(name) = &metadata.name {
- let _ = writeln!(&mut message, "Name: {}", name);
- }
- if let Some(version) = &metadata.version {
- let _ = writeln!(&mut message, "Version: {}", version);
- }
- if let Some(authors) = &metadata.authors {
- let _ = writeln!(&mut message, "Authors: {}", authors.join(", "));
- }
- if let Some(license) = &metadata.license {
- let _ = writeln!(&mut message, "License: {}", license);
- }
- match (&metadata.website_label, &metadata.website) {
- (Some(label), None) => {
- let _ = writeln!(&mut message, "Website: {}", label);
- }
- (None, Some(url)) => {
- let _ = writeln!(&mut message, "Website: {}", url);
- }
- (Some(label), Some(url)) => {
- let _ = writeln!(&mut message, "Website: {} {}", label, url);
- }
- _ => {}
- }
- if let Some(comments) = &metadata.comments {
- let _ = writeln!(&mut message, "\n{}", comments);
- }
- if let Some(copyright) = &metadata.copyright {
- let _ = writeln!(&mut message, "\n{}", copyright);
- }
-
- let message = encode_wide(message);
- let title = encode_wide(format!(
- "About {}",
- metadata.name.as_deref().unwrap_or_default()
- ));
- std::thread::spawn(move || {
- MessageBoxW(
- hwnd,
- message.as_ptr(),
- title.as_ptr(),
- MB_ICONINFORMATION,
- );
- });
+ show_about_dialog(hwnd, metadata)
}
_ => {}
@@ -1322,3 +1275,103 @@ fn create_icon_item_info(hbitmap: HBITMAP) -> MENUITEMINFOW {
info.hbmpItem = hbitmap;
info
}
+
+fn show_about_dialog(hwnd: HWND, metadata: &AboutMetadata) {
+ use std::fmt::Write;
+
+ let mut message = String::new();
+ if let Some(name) = &metadata.name {
+ let _ = writeln!(&mut message, "Name: {}", name);
+ }
+ if let Some(version) = &metadata.version {
+ let _ = writeln!(&mut message, "Version: {}", version);
+ }
+ if let Some(authors) = &metadata.authors {
+ let _ = writeln!(&mut message, "Authors: {}", authors.join(", "));
+ }
+ if let Some(license) = &metadata.license {
+ let _ = writeln!(&mut message, "License: {}", license);
+ }
+ match (&metadata.website_label, &metadata.website) {
+ (Some(label), None) => {
+ let _ = writeln!(&mut message, "Website: {}", label);
+ }
+ (None, Some(url)) => {
+ let _ = writeln!(&mut message, "Website: {}", url);
+ }
+ (Some(label), Some(url)) => {
+ let _ = writeln!(&mut message, "Website: {} {}", label, url);
+ }
+ _ => {}
+ }
+ if let Some(comments) = &metadata.comments {
+ let _ = writeln!(&mut message, "\n{}", comments);
+ }
+ if let Some(copyright) = &metadata.copyright {
+ let _ = writeln!(&mut message, "\n{}", copyright);
+ }
+
+ let message = encode_wide(message);
+ let title = encode_wide(format!(
+ "About {}",
+ metadata.name.as_deref().unwrap_or_default()
+ ));
+
+ #[cfg(not(feature = "common-controls-v6"))]
+ std::thread::spawn(move || unsafe {
+ use windows_sys::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_ICONINFORMATION};
+ MessageBoxW(hwnd, message.as_ptr(), title.as_ptr(), MB_ICONINFORMATION);
+ });
+
+ #[cfg(feature = "common-controls-v6")]
+ {
+ use windows_sys::Win32::UI::Controls::{
+ TaskDialogIndirect, TASKDIALOGCONFIG, TASKDIALOGCONFIG_0, TASKDIALOGCONFIG_1,
+ TDCBF_OK_BUTTON, TDF_ALLOW_DIALOG_CANCELLATION, TD_INFORMATION_ICON,
+ };
+
+ std::thread::spawn(move || unsafe {
+ let task_dialog_config = TASKDIALOGCONFIG {
+ cbSize: core::mem::size_of::() as u32,
+ hwndParent: hwnd,
+ dwFlags: TDF_ALLOW_DIALOG_CANCELLATION,
+ pszWindowTitle: title.as_ptr(),
+ pszContent: message.as_ptr(),
+ Anonymous1: TASKDIALOGCONFIG_0 {
+ pszMainIcon: TD_INFORMATION_ICON,
+ },
+ Anonymous2: TASKDIALOGCONFIG_1 {
+ pszFooterIcon: std::ptr::null(),
+ },
+ dwCommonButtons: TDCBF_OK_BUTTON,
+ pButtons: std::ptr::null(),
+ cButtons: 0,
+ pRadioButtons: std::ptr::null(),
+ cRadioButtons: 0,
+ cxWidth: 0,
+ hInstance: 0,
+ pfCallback: None,
+ lpCallbackData: 0,
+ nDefaultButton: 0,
+ nDefaultRadioButton: 0,
+ pszCollapsedControlText: std::ptr::null(),
+ pszExpandedControlText: std::ptr::null(),
+ pszExpandedInformation: std::ptr::null(),
+ pszMainInstruction: std::ptr::null(),
+ pszVerificationText: std::ptr::null(),
+ pszFooter: std::ptr::null(),
+ };
+
+ let mut pf_verification_flag_checked = 0;
+ let mut pn_button = 0;
+ let mut pn_radio_button = 0;
+
+ TaskDialogIndirect(
+ &task_dialog_config,
+ &mut pn_button,
+ &mut pn_radio_button,
+ &mut pf_verification_flag_checked,
+ )
+ });
+ }
+}