feat: add common-controls-v6 (#69)

This commit is contained in:
Amr Bashir 2023-06-19 21:58:45 +03:00 committed by GitHub
parent 7af4477896
commit ac14222934
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 379 additions and 64 deletions

View file

@ -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.

View file

@ -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
command: test
- name: test common-controls-v6
if: matrix.os == "windows-latest"
working-directory: examples/windows-common-controls-v6
run: cargo run

View file

@ -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]

View file

@ -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)

View file

@ -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()
}),

View file

@ -0,0 +1,2 @@
/target
Cargo.lock

View file

@ -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"

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>

View file

@ -0,0 +1,4 @@
fn main() {
#[cfg(target_os = "windows")]
embed_resource::compile("manifest.rc");
}

View file

@ -0,0 +1,2 @@
#define RT_MANIFEST 24
1 RT_MANIFEST "app.exe.manifest"

View file

@ -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(&[&copy_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")
}

View file

@ -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()
}),

View file

@ -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::<TASKDIALOGCONFIG>() 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,
)
});
}
}