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, + ) + }); + } +}