feat: add IconMenuItem, closes #30 (#32)

* feat: add `IconMenuItem`

* Linux

* macOS
This commit is contained in:
Amr Bashir 2022-12-30 14:23:40 +02:00 committed by GitHub
parent 1c4587efe5
commit 7fc1b02cac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1179 additions and 133 deletions

View file

@ -0,0 +1,5 @@
---
"muda": "minor"
---
Add `IconMenuItem`

View file

@ -3,18 +3,18 @@ name = "muda"
version = "0.1.1"
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"]
[dependencies]
crossbeam-channel = "0.5"
keyboard-types = "0.6"
once_cell = "1"
thiserror = "1.0.38"
thiserror = "1"
[target."cfg(target_os = \"windows\")".dependencies.windows-sys]
version = "0.42"
@ -24,18 +24,22 @@ features = [
"Win32_Graphics_Gdi",
"Win32_UI_Shell",
"Win32_Globalization",
"Win32_UI_Input_KeyboardAndMouse"
"Win32_UI_Input_KeyboardAndMouse",
"Win32_System_SystemServices",
]
[target."cfg(target_os = \"linux\")".dependencies]
gdk = "0.15"
gtk = { version = "0.15", features = [ "v3_22" ] }
gtk = { version = "0.15", features = ["v3_22"] }
gdk = { version = "0.15", features = ["v3_22"] }
gdk-pixbuf = { version = "0.15", features = ["v2_36_8"] }
libxdo = "0.6.0"
[target."cfg(target_os = \"macos\")".dependencies]
cocoa = "0.24"
objc = "0.2"
png = "0.17"
[dev-dependencies]
winit = "0.27"
tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" }
image = "0.24"

BIN
examples/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -5,7 +5,7 @@
#![allow(unused)]
use muda::{
accelerator::{Accelerator, Code, Modifiers},
menu_event_receiver, AboutMetadata, CheckMenuItem, ContextMenu, Menu, MenuItem,
menu_event_receiver, AboutMetadata, CheckMenuItem, ContextMenu, IconMenuItem, Menu, MenuItem,
PredefinedMenuItem, Submenu,
};
#[cfg(target_os = "macos")]
@ -77,7 +77,16 @@ fn main() {
true,
Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyC)),
);
let custom_i_2 = MenuItem::new("Custom 2", false, None);
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png");
let icon = load_icon(std::path::Path::new(path));
let image_item = IconMenuItem::new(
"Image custom 1",
true,
Some(icon),
Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyC)),
);
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(
@ -93,7 +102,7 @@ fn main() {
file_m.append_items(&[
&custom_i_1,
&custom_i_2,
&image_item,
&window_m,
&PredefinedMenuItem::separator(),
&check_custom_i_1,
@ -113,11 +122,11 @@ fn main() {
}),
),
&check_custom_i_3,
&custom_i_2,
&image_item,
&custom_i_1,
]);
edit_m.append_items(&[&copy_i, &paste_i, &PredefinedMenuItem::separator()]);
edit_m.append_items(&[&copy_i, &PredefinedMenuItem::separator(), &paste_i]);
#[cfg(target_os = "windows")]
{
@ -194,3 +203,15 @@ fn show_context_menu(window: &Window, menu: &dyn ContextMenu, x: f64, y: f64) {
#[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

@ -5,11 +5,13 @@
#![allow(unused)]
use muda::{
accelerator::{Accelerator, Code, Modifiers},
menu_event_receiver, AboutMetadata, CheckMenuItem, ContextMenu, Menu, MenuItem,
menu_event_receiver, AboutMetadata, CheckMenuItem, ContextMenu, IconMenuItem, Menu, MenuItem,
PredefinedMenuItem, Submenu,
};
#[cfg(target_os = "macos")]
use winit::platform::macos::{EventLoopBuilderExtMacOS, WindowExtMacOS};
#[cfg(target_os = "linux")]
use winit::platform::unix::WindowExtUnix;
#[cfg(target_os = "windows")]
use winit::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows};
use winit::{
@ -77,7 +79,11 @@ fn main() {
true,
Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyC)),
);
let custom_i_2 = MenuItem::new("Custom 2", false, None);
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/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(
@ -93,7 +99,7 @@ fn main() {
file_m.append_items(&[
&custom_i_1,
&custom_i_2,
&image_item,
&window_m,
&PredefinedMenuItem::separator(),
&check_custom_i_1,
@ -113,11 +119,11 @@ fn main() {
}),
),
&check_custom_i_3,
&custom_i_2,
&image_item,
&custom_i_1,
]);
edit_m.append_items(&[&copy_i, &paste_i, &PredefinedMenuItem::separator()]);
edit_m.append_items(&[&copy_i, &PredefinedMenuItem::separator(), &paste_i]);
#[cfg(target_os = "windows")]
{
@ -163,7 +169,7 @@ fn main() {
..
} => {
if window_id == window2.id() {
show_context_menu(&window2, &window_m, x, y);
show_context_menu(&window2, &file_m, x, y);
}
}
Event::MainEventsCleared => {
@ -174,7 +180,7 @@ fn main() {
if let Ok(event) = menu_channel.try_recv() {
if event.id == custom_i_1.id() {
file_m.insert(&MenuItem::new("New Menu Item", false, None), 2);
file_m.insert(&MenuItem::new("New Menu Item", true, None), 2);
}
println!("{:?}", event);
}
@ -187,3 +193,15 @@ fn show_context_menu(window: &Window, menu: &dyn ContextMenu, x: f64, y: f64) {
#[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")
}

166
src/icon.rs Normal file
View file

@ -0,0 +1,166 @@
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// taken from https://github.com/rust-windowing/winit/blob/92fdf5ba85f920262a61cee4590f4a11ad5738d1/src/icon.rs
use crate::platform_impl::PlatformIcon;
use std::{error::Error, fmt, io, mem};
#[repr(C)]
#[derive(Debug)]
pub(crate) struct Pixel {
pub(crate) r: u8,
pub(crate) g: u8,
pub(crate) b: u8,
pub(crate) a: u8,
}
pub(crate) const PIXEL_SIZE: usize = mem::size_of::<Pixel>();
#[derive(Debug)]
/// An error produced when using [`Icon::from_rgba`] with invalid arguments.
pub enum BadIcon {
/// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be
/// safely interpreted as 32bpp RGBA pixels.
ByteCountNotDivisibleBy4 { byte_count: usize },
/// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`.
/// At least one of your arguments is incorrect.
DimensionsVsPixelCount {
width: u32,
height: u32,
width_x_height: usize,
pixel_count: usize,
},
/// Produced when underlying OS functionality failed to create the icon
OsError(io::Error),
}
impl fmt::Display for BadIcon {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BadIcon::ByteCountNotDivisibleBy4 { byte_count } => write!(f,
"The length of the `rgba` argument ({:?}) isn't divisible by 4, making it impossible to interpret as 32bpp RGBA pixels.",
byte_count,
),
BadIcon::DimensionsVsPixelCount {
width,
height,
width_x_height,
pixel_count,
} => write!(f,
"The specified dimensions ({:?}x{:?}) don't match the number of pixels supplied by the `rgba` argument ({:?}). For those dimensions, the expected pixel count is {:?}.",
width, height, pixel_count, width_x_height,
),
BadIcon::OsError(e) => write!(f, "OS error when instantiating the icon: {:?}", e),
}
}
}
impl Error for BadIcon {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RgbaIcon {
pub(crate) rgba: Vec<u8>,
pub(crate) width: u32,
pub(crate) height: u32,
}
/// For platforms which don't have window icons (e.g. web)
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NoIcon;
#[allow(dead_code)] // These are not used on every platform
mod constructors {
use super::*;
impl RgbaIcon {
pub fn from_rgba(rgba: Vec<u8>, width: u32, height: u32) -> Result<Self, BadIcon> {
if rgba.len() % PIXEL_SIZE != 0 {
return Err(BadIcon::ByteCountNotDivisibleBy4 {
byte_count: rgba.len(),
});
}
let pixel_count = rgba.len() / PIXEL_SIZE;
if pixel_count != (width * height) as usize {
Err(BadIcon::DimensionsVsPixelCount {
width,
height,
width_x_height: (width * height) as usize,
pixel_count,
})
} else {
Ok(RgbaIcon {
rgba,
width,
height,
})
}
}
}
impl NoIcon {
pub fn from_rgba(rgba: Vec<u8>, width: u32, height: u32) -> Result<Self, BadIcon> {
// Create the rgba icon anyway to validate the input
let _ = RgbaIcon::from_rgba(rgba, width, height)?;
Ok(NoIcon)
}
}
}
/// An icon used for the window titlebar, taskbar, etc.
#[derive(Clone)]
pub struct Icon {
pub(crate) inner: PlatformIcon,
}
impl fmt::Debug for Icon {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt::Debug::fmt(&self.inner, formatter)
}
}
impl Icon {
/// Creates an icon from 32bpp RGBA data.
///
/// The length of `rgba` must be divisible by 4, and `width * height` must equal
/// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error.
pub fn from_rgba(rgba: Vec<u8>, width: u32, height: u32) -> Result<Self, BadIcon> {
Ok(Icon {
inner: PlatformIcon::from_rgba(rgba, width, height)?,
})
}
/// Create an icon from a file path.
///
/// Specify `size` to load a specific icon size from the file, or `None` to load the default
/// icon size from the file.
///
/// In cases where the specified size does not exist in the file, Windows may perform scaling
/// to get an icon of the desired size.
#[cfg(windows)]
pub fn from_path<P: AsRef<std::path::Path>>(
path: P,
size: Option<(u32, u32)>,
) -> Result<Self, BadIcon> {
let win_icon = PlatformIcon::from_path(path, size)?;
Ok(Icon { inner: win_icon })
}
/// Create an icon from a resource embedded in this executable or library.
///
/// Specify `size` to load a specific icon size from the file, or `None` to load the default
/// icon size from the file.
///
/// In cases where the specified size does not exist in the file, Windows may perform scaling
/// to get an icon of the desired size.
#[cfg(windows)]
pub fn from_resource(ordinal: u16, size: Option<(u32, u32)>) -> Result<Self, BadIcon> {
let win_icon = PlatformIcon::from_resource(ordinal, size)?;
Ok(Icon { inner: win_icon })
}
}

79
src/icon_menu_item.rs Normal file
View file

@ -0,0 +1,79 @@
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{accelerator::Accelerator, icon::Icon, 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 IconMenuItem(pub(crate) crate::platform_impl::IconMenuItem);
unsafe impl MenuItemExt for IconMenuItem {
fn type_(&self) -> MenuItemType {
MenuItemType::Icon
}
fn as_any(&self) -> &(dyn std::any::Any + 'static) {
self
}
fn id(&self) -> u32 {
self.id()
}
}
impl IconMenuItem {
/// 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,
icon: Option<Icon>,
acccelerator: Option<Accelerator>,
) -> Self {
Self(crate::platform_impl::IconMenuItem::new(
text.as_ref(),
enabled,
icon,
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)
}
/// Change this menu item icon or remove it.
pub fn set_icon(&self, icon: Option<Icon>) {
self.0.set_icon(icon)
}
}

View file

@ -108,6 +108,7 @@ use once_cell::sync::Lazy;
pub mod accelerator;
mod check_menu_item;
mod error;
mod icon_menu_item;
mod menu;
mod menu_item;
mod platform_impl;
@ -121,7 +122,9 @@ extern crate objc;
pub use self::error::*;
pub use check_menu_item::CheckMenuItem;
pub use icon_menu_item::IconMenuItem;
pub use menu::Menu;
pub mod icon;
pub use menu_item::MenuItem;
pub use predefined::{AboutMetadata, PredefinedMenuItem};
pub use submenu::Submenu;
@ -132,6 +135,7 @@ pub enum MenuItemType {
Normal,
Predefined,
Check,
Icon,
}
impl Default for MenuItemType {

View file

@ -90,9 +90,12 @@ pub fn register_accelerator<M: IsA<gtk::Widget>>(
item: &M,
accel_group: &AccelGroup,
accelerator: &Accelerator,
) {
) -> Option<(gdk::ModifierType, u32)> {
if let Ok((mods, key)) = parse_accelerator(accelerator) {
item.add_accelerator("activate", accel_group, key, mods, gtk::AccelFlags::VISIBLE);
Some((mods, key))
} else {
None
}
}

View file

@ -0,0 +1,61 @@
// Copyright 2014-2021 The winit contributors
// Copyright 2021-2022 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
use gdk_pixbuf::{Colorspace, Pixbuf};
use crate::icon::BadIcon;
/// An icon used for the window titlebar, taskbar, etc.
#[derive(Debug, Clone)]
pub struct PlatformIcon {
raw: Vec<u8>,
width: i32,
height: i32,
row_stride: i32,
}
impl From<PlatformIcon> for Pixbuf {
fn from(icon: PlatformIcon) -> Self {
Pixbuf::from_mut_slice(
icon.raw,
gdk_pixbuf::Colorspace::Rgb,
true,
8,
icon.width,
icon.height,
icon.row_stride,
)
}
}
impl PlatformIcon {
/// Creates an `Icon` from 32bpp RGBA data.
///
/// The length of `rgba` must be divisible by 4, and `width * height` must equal
/// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error.
pub fn from_rgba(rgba: Vec<u8>, width: u32, height: u32) -> Result<Self, BadIcon> {
let row_stride =
Pixbuf::calculate_rowstride(Colorspace::Rgb, true, 8, width as i32, height as i32);
Ok(Self {
raw: rgba,
width: width as i32,
height: height as i32,
row_stride,
})
}
pub fn to_pixbuf(&self, w: i32, h: i32) -> Pixbuf {
Pixbuf::from_mut_slice(
self.raw.clone(),
gdk_pixbuf::Colorspace::Rgb,
true,
8,
self.width,
self.height,
self.row_stride,
)
.scale_simple(w, h, gdk_pixbuf::InterpType::Bilinear)
.unwrap()
}
}

View file

@ -3,9 +3,13 @@
// SPDX-License-Identifier: MIT
mod accelerator;
mod icon;
pub(crate) use icon::PlatformIcon;
use crate::{
accelerator::Accelerator,
icon::Icon,
predefined::PredfinedMenuItemType,
util::{AddOp, Counter},
MenuItemType,
@ -38,7 +42,8 @@ macro_rules! return_if_predefined_item_not_supported {
(
crate::MenuItemType::Submenu
| crate::MenuItemType::Normal
| crate::MenuItemType::Check,
| crate::MenuItemType::Check
| crate::MenuItemType::Icon,
_,
) => {}
_ => return,
@ -71,6 +76,9 @@ struct MenuChild {
checked: bool,
is_syncing_checked_state: Rc<AtomicBool>,
// icon menu item fields
icon: Option<Icon>,
// submenu fields
children: Option<Vec<Rc<RefCell<MenuChild>>>>,
gtk_menus: HashMap<u32, Vec<(u32, gtk::Menu)>>,
@ -157,6 +165,21 @@ impl MenuChild {
self.is_syncing_checked_state
.store(false, Ordering::Release);
}
fn set_icon(&mut self, icon: Option<Icon>) {
self.icon = icon.clone();
let pixbuf = icon.map(|i| i.inner.to_pixbuf(16, 16));
for items in self.gtk_menu_items.values() {
for i in items {
let box_container = i.child().unwrap().downcast::<gtk::Box>().unwrap();
box_container.children()[0]
.downcast_ref::<gtk::Image>()
.unwrap()
.set_pixbuf(pixbuf.as_ref())
}
}
}
}
struct InnerMenu {
@ -316,6 +339,7 @@ impl Menu {
Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone())))
}
MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))),
MenuItemType::Icon => Box::new(crate::IconMenuItem(IconMenuItem(c.clone()))),
}
})
.collect()
@ -648,6 +672,7 @@ impl Submenu {
Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone())))
}
MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))),
MenuItemType::Icon => Box::new(crate::IconMenuItem(IconMenuItem(c.clone()))),
}
})
.collect()
@ -1089,31 +1114,157 @@ impl CheckMenuItem {
}
}
#[derive(Clone)]
pub struct IconMenuItem(Rc<RefCell<MenuChild>>);
impl IconMenuItem {
pub fn new(
text: &str,
enabled: bool,
icon: Option<Icon>,
accelerator: Option<Accelerator>,
) -> Self {
let child = Rc::new(RefCell::new(MenuChild {
text: text.to_string(),
enabled,
icon,
accelerator,
id: COUNTER.next(),
type_: MenuItemType::Icon,
gtk_menu_items: HashMap::new(),
is_syncing_checked_state: Rc::new(AtomicBool::new(false)),
..Default::default()
}));
Self(child)
}
fn make_gtk_menu_item(
&self,
menu_id: u32,
accel_group: Option<&gtk::AccelGroup>,
add_to_cache: bool,
) -> gtk::MenuItem {
let mut self_ = self.0.borrow_mut();
let image = self_
.icon
.as_ref()
.map(|i| gtk::Image::from_pixbuf(Some(&i.inner.to_pixbuf(16, 16))))
.unwrap_or_else(gtk::Image::default);
let label = gtk::AccelLabel::builder()
.label(&to_gtk_mnemonic(&self_.text))
.use_underline(true)
.xalign(0.0)
.build();
let box_container = gtk::Box::new(Orientation::Horizontal, 6);
let style_context = box_container.style_context();
let css_provider = gtk::CssProvider::new();
let theme = r#"
box {
margin-left: -22px;
}
"#;
let _ = css_provider.load_from_data(theme.as_bytes());
style_context.add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
box_container.pack_start(&image, false, false, 0);
box_container.pack_start(&label, true, true, 0);
box_container.show_all();
let item = gtk::MenuItem::builder()
.child(&box_container)
.sensitive(self_.enabled)
.build();
if let Some(accelerator) = &self_.accelerator {
if let Some(accel_group) = accel_group {
if let Some((mods, key)) = register_accelerator(&item, accel_group, accelerator) {
label.set_accel(key, mods);
}
}
}
let id = self_.id;
item.connect_activate(move |_| {
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id });
});
if add_to_cache {
self_
.gtk_menu_items
.entry(menu_id)
.or_insert_with(Vec::new)
.push(item.clone());
}
item
}
pub fn id(&self) -> u32 {
self.0.borrow().id()
}
pub fn text(&self) -> String {
self.0.borrow().text()
}
pub fn set_text(&self, text: &str) {
self.0.borrow_mut().set_text(text)
}
pub fn is_enabled(&self) -> bool {
self.0.borrow().is_enabled()
}
pub fn set_enabled(&self, enabled: bool) {
self.0.borrow_mut().set_enabled(enabled)
}
pub fn set_icon(&self, icon: Option<Icon>) {
self.0.borrow_mut().set_icon(icon)
}
}
impl dyn crate::MenuItemExt + '_ {
fn get_child(&self) -> Rc<RefCell<MenuChild>> {
match self.type_() {
MenuItemType::Submenu => {
let submenu = self.as_any().downcast_ref::<crate::Submenu>().unwrap();
Rc::clone(&submenu.0 .0)
}
MenuItemType::Normal => {
let menuitem = self.as_any().downcast_ref::<crate::MenuItem>().unwrap();
Rc::clone(&menuitem.0 .0)
}
MenuItemType::Check => {
let menuitem = self
MenuItemType::Submenu => self
.as_any()
.downcast_ref::<crate::CheckMenuItem>()
.unwrap();
Rc::clone(&menuitem.0 .0)
}
MenuItemType::Predefined => {
let menuitem = self
.downcast_ref::<crate::Submenu>()
.unwrap()
.0
.0
.clone(),
MenuItemType::Normal => self
.as_any()
.downcast_ref::<crate::MenuItem>()
.unwrap()
.0
.0
.clone(),
MenuItemType::Predefined => self
.as_any()
.downcast_ref::<crate::PredefinedMenuItem>()
.unwrap();
Rc::clone(&menuitem.0 .0)
}
.unwrap()
.0
.0
.clone(),
MenuItemType::Check => self
.as_any()
.downcast_ref::<crate::CheckMenuItem>()
.unwrap()
.0
.0
.clone(),
MenuItemType::Icon => self
.as_any()
.downcast_ref::<crate::IconMenuItem>()
.unwrap()
.0
.0
.clone(),
}
}
@ -1124,36 +1275,36 @@ impl dyn crate::MenuItemExt + '_ {
add_to_cache: bool,
) -> gtk::MenuItem {
match self.type_() {
MenuItemType::Submenu => {
let submenu = self.as_any().downcast_ref::<crate::Submenu>().unwrap();
submenu
MenuItemType::Submenu => self
.as_any()
.downcast_ref::<crate::Submenu>()
.unwrap()
.0
.make_gtk_menu_item(menu_id, accel_group, add_to_cache)
}
MenuItemType::Normal => {
let menuitem = self.as_any().downcast_ref::<crate::MenuItem>().unwrap();
menuitem
.make_gtk_menu_item(menu_id, accel_group, add_to_cache),
MenuItemType::Normal => self
.as_any()
.downcast_ref::<crate::MenuItem>()
.unwrap()
.0
.make_gtk_menu_item(menu_id, accel_group, add_to_cache)
}
MenuItemType::Predefined => {
let menuitem = self
.make_gtk_menu_item(menu_id, accel_group, add_to_cache),
MenuItemType::Predefined => self
.as_any()
.downcast_ref::<crate::PredefinedMenuItem>()
.unwrap();
menuitem
.unwrap()
.0
.make_gtk_menu_item(menu_id, accel_group, add_to_cache)
}
MenuItemType::Check => {
let menuitem = self
.make_gtk_menu_item(menu_id, accel_group, add_to_cache),
MenuItemType::Check => self
.as_any()
.downcast_ref::<crate::CheckMenuItem>()
.unwrap();
menuitem
.unwrap()
.0
.make_gtk_menu_item(menu_id, accel_group, add_to_cache)
}
.make_gtk_menu_item(menu_id, accel_group, add_to_cache),
MenuItemType::Icon => self
.as_any()
.downcast_ref::<crate::IconMenuItem>()
.unwrap()
.0
.make_gtk_menu_item(menu_id, accel_group, add_to_cache),
}
}
}

View file

@ -0,0 +1,35 @@
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::icon::{BadIcon, RgbaIcon};
use std::io::Cursor;
#[derive(Debug, Clone)]
pub struct PlatformIcon(RgbaIcon);
impl PlatformIcon {
pub fn from_rgba(rgba: Vec<u8>, width: u32, height: u32) -> Result<Self, BadIcon> {
Ok(PlatformIcon(RgbaIcon::from_rgba(rgba, width, height)?))
}
pub fn get_size(&self) -> (u32, u32) {
(self.0.width, self.0.height)
}
pub fn to_png(&self) -> Vec<u8> {
let mut png = Vec::new();
{
let mut encoder =
png::Encoder::new(Cursor::new(&mut png), self.0.width as _, self.0.height as _);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().unwrap();
writer.write_image_data(&self.0.rgba).unwrap();
}
png
}
}

View file

@ -3,14 +3,17 @@
// SPDX-License-Identifier: MIT
mod accelerator;
mod icon;
mod util;
pub(crate) use icon::PlatformIcon;
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Once};
use cocoa::{
appkit::{CGFloat, NSApp, NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem},
appkit::{CGFloat, NSApp, NSApplication, NSEventModifierFlags, NSImage, NSMenu, NSMenuItem},
base::{id, nil, selector, NO, YES},
foundation::{NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSString},
foundation::{NSAutoreleasePool, NSData, NSInteger, NSPoint, NSRect, NSSize, NSString},
};
use objc::{
declare::ClassDecl,
@ -20,6 +23,7 @@ use objc::{
use self::util::{app_name_string, strip_mnemonic};
use crate::{
accelerator::Accelerator,
icon::Icon,
predefined::PredfinedMenuItemType,
util::{AddOp, Counter},
MenuItemExt, MenuItemType,
@ -31,7 +35,7 @@ static BLOCK_PTR: &str = "mudaMenuItemBlockPtr";
/// A generic child in a menu
///
/// Be careful when cloning this item and treat it as read-only
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug)]
#[allow(dead_code)]
struct MenuChild {
// shared fields between submenus and menu items
@ -51,6 +55,9 @@ struct MenuChild {
// check menu item fields
checked: bool,
// icon menu item fields
icon: Option<Icon>,
// submenu fields
children: Option<Vec<Rc<RefCell<MenuChild>>>>,
ns_menus: HashMap<u32, Vec<id>>,
@ -68,6 +75,7 @@ impl Default for MenuChild {
accelerator: Default::default(),
predefined_item_type: Default::default(),
checked: Default::default(),
icon: Default::default(),
children: Default::default(),
ns_menus: Default::default(),
ns_menu: (0, 0 as _),
@ -129,6 +137,15 @@ impl MenuChild {
}
}
}
fn set_icon(&mut self, icon: Option<Icon>) {
self.icon = icon.clone();
for ns_items in self.ns_menu_items.values() {
for &ns_item in ns_items {
menuitem_set_icon(ns_item, icon.as_ref());
}
}
}
}
#[derive(Clone, Debug)]
@ -180,6 +197,13 @@ impl Menu {
let menuitem = item.as_any().downcast_ref::<crate::MenuItem>().unwrap();
menuitem.0 .0.borrow_mut()
}
MenuItemType::Predefined => {
let menuitem = item
.as_any()
.downcast_ref::<crate::PredefinedMenuItem>()
.unwrap();
menuitem.0 .0.borrow_mut()
}
MenuItemType::Check => {
let menuitem = item
.as_any()
@ -187,11 +211,8 @@ impl Menu {
.unwrap();
menuitem.0 .0.borrow_mut()
}
MenuItemType::Predefined => {
let menuitem = item
.as_any()
.downcast_ref::<crate::PredefinedMenuItem>()
.unwrap();
MenuItemType::Icon => {
let menuitem = item.as_any().downcast_ref::<crate::IconMenuItem>().unwrap();
menuitem.0 .0.borrow_mut()
}
}
@ -230,6 +251,7 @@ impl Menu {
Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone())))
}
MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))),
MenuItemType::Icon => Box::new(crate::IconMenuItem(IconMenuItem(c.clone()))),
}
})
.collect()
@ -306,6 +328,7 @@ impl Submenu {
PredefinedMenuItem(item.clone()).make_ns_item_for_menu(menu_id)
}
MenuItemType::Check => CheckMenuItem(item.clone()).make_ns_item_for_menu(menu_id),
MenuItemType::Icon => IconMenuItem(item.clone()).make_ns_item_for_menu(menu_id),
};
unsafe { ns_submenu.addItem_(ns_item) };
}
@ -399,7 +422,7 @@ impl Submenu {
let children = self_.children.as_mut().unwrap();
let index = children
.iter()
.position(|e| e == &child)
.position(|e| e.borrow().id == item.id())
.ok_or(crate::Error::NotAChildOfThisMenu)?;
children.remove(index);
@ -422,6 +445,7 @@ impl Submenu {
Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone())))
}
MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))),
MenuItemType::Icon => Box::new(crate::IconMenuItem(IconMenuItem(c.clone()))),
}
})
.collect()
@ -697,6 +721,85 @@ impl CheckMenuItem {
}
}
#[derive(Clone, Debug)]
pub(crate) struct IconMenuItem(Rc<RefCell<MenuChild>>);
impl IconMenuItem {
pub fn new(
text: &str,
enabled: bool,
icon: Option<Icon>,
accelerator: Option<Accelerator>,
) -> Self {
Self(Rc::new(RefCell::new(MenuChild {
type_: MenuItemType::Icon,
text: text.to_string(),
enabled,
id: COUNTER.next(),
icon,
accelerator,
..Default::default()
})))
}
pub fn make_ns_item_for_menu(&self, menu_id: u32) -> id {
let mut child = self.0.borrow_mut();
let ns_menu_item = create_ns_menu_item(
&child.text,
Some(sel!(fireMenuItemAction:)),
&child.accelerator,
);
unsafe {
let _: () = msg_send![ns_menu_item, setTarget: ns_menu_item];
let _: () = msg_send![ns_menu_item, setTag:child.id()];
// Store a raw pointer to the `MenuChild` as an instance variable on the native menu item
let ptr = Box::into_raw(Box::new(&*child));
(*ns_menu_item).set_ivar(BLOCK_PTR, ptr as usize);
if !child.enabled {
let () = msg_send![ns_menu_item, setEnabled: NO];
}
menuitem_set_icon(ns_menu_item, child.icon.as_ref());
}
child
.ns_menu_items
.entry(menu_id)
.or_insert_with(Vec::new)
.push(ns_menu_item);
ns_menu_item
}
pub fn id(&self) -> u32 {
self.0.borrow().id()
}
pub fn text(&self) -> String {
self.0.borrow().text()
}
pub fn set_text(&self, text: &str) {
self.0.borrow_mut().set_text(text)
}
pub fn is_enabled(&self) -> bool {
self.0.borrow().is_enabled()
}
pub fn set_enabled(&self, enabled: bool) {
self.0.borrow_mut().set_enabled(enabled)
}
pub fn set_icon(&self, icon: Option<Icon>) {
self.0.borrow_mut().set_icon(icon)
}
}
impl PredfinedMenuItemType {
pub(crate) fn selector(&self) -> Option<Sel> {
match self {
@ -725,55 +828,76 @@ impl PredfinedMenuItemType {
impl dyn MenuItemExt + '_ {
fn get_child(&self) -> Rc<RefCell<MenuChild>> {
match self.type_() {
MenuItemType::Submenu => {
let submenu = self.as_any().downcast_ref::<crate::Submenu>().unwrap();
Rc::clone(&submenu.0 .0)
}
MenuItemType::Normal => {
let menuitem = self.as_any().downcast_ref::<crate::MenuItem>().unwrap();
Rc::clone(&menuitem.0 .0)
}
MenuItemType::Check => {
let menuitem = self
MenuItemType::Submenu => self
.as_any()
.downcast_ref::<crate::CheckMenuItem>()
.unwrap();
Rc::clone(&menuitem.0 .0)
}
MenuItemType::Predefined => {
let menuitem = self
.downcast_ref::<crate::Submenu>()
.unwrap()
.0
.0
.clone(),
MenuItemType::Normal => self
.as_any()
.downcast_ref::<crate::MenuItem>()
.unwrap()
.0
.0
.clone(),
MenuItemType::Predefined => self
.as_any()
.downcast_ref::<crate::PredefinedMenuItem>()
.unwrap();
Rc::clone(&menuitem.0 .0)
}
.unwrap()
.0
.0
.clone(),
MenuItemType::Check => self
.as_any()
.downcast_ref::<crate::CheckMenuItem>()
.unwrap()
.0
.0
.clone(),
MenuItemType::Icon => self
.as_any()
.downcast_ref::<crate::IconMenuItem>()
.unwrap()
.0
.0
.clone(),
}
}
fn make_ns_item_for_menu(&self, menu_id: u32) -> *mut Object {
match self.type_() {
MenuItemType::Submenu => {
let submenu = self.as_any().downcast_ref::<crate::Submenu>().unwrap();
submenu.0.make_ns_item_for_menu(menu_id)
}
MenuItemType::Normal => {
let menuitem = self.as_any().downcast_ref::<crate::MenuItem>().unwrap();
menuitem.0.make_ns_item_for_menu(menu_id)
}
MenuItemType::Check => {
let menuitem = self
MenuItemType::Submenu => self
.as_any()
.downcast_ref::<crate::CheckMenuItem>()
.unwrap();
menuitem.0.make_ns_item_for_menu(menu_id)
}
MenuItemType::Predefined => {
let menuitem = self
.downcast_ref::<crate::Submenu>()
.unwrap()
.0
.make_ns_item_for_menu(menu_id),
MenuItemType::Normal => self
.as_any()
.downcast_ref::<crate::MenuItem>()
.unwrap()
.0
.make_ns_item_for_menu(menu_id),
MenuItemType::Predefined => self
.as_any()
.downcast_ref::<crate::PredefinedMenuItem>()
.unwrap();
menuitem.0.make_ns_item_for_menu(menu_id)
}
.unwrap()
.0
.make_ns_item_for_menu(menu_id),
MenuItemType::Check => self
.as_any()
.downcast_ref::<crate::CheckMenuItem>()
.unwrap()
.0
.make_ns_item_for_menu(menu_id),
MenuItemType::Icon => self
.as_any()
.downcast_ref::<crate::IconMenuItem>()
.unwrap()
.0
.make_ns_item_for_menu(menu_id),
}
}
}
@ -842,16 +966,14 @@ fn create_ns_menu_item(
let selector = selector.unwrap_or_else(|| Sel::from_ptr(std::ptr::null()));
let key_equivalent = accelerator
.clone()
let key_equivalent = (*accelerator)
.map(|accel| accel.key_equivalent())
.unwrap_or_default();
let key_equivalent = NSString::alloc(nil)
.init_str(key_equivalent.as_str())
.autorelease();
let modifier_mask = accelerator
.clone()
let modifier_mask = (*accelerator)
.map(|accel| accel.key_modifier_mask())
.unwrap_or_else(NSEventModifierFlags::empty);
@ -863,3 +985,30 @@ fn create_ns_menu_item(
ns_menu_item.autorelease()
}
}
fn menuitem_set_icon(menuitem: id, icon: Option<&Icon>) {
if let Some(icon) = icon {
let (width, height) = icon.inner.get_size();
let icon = icon.inner.to_png();
let icon_height: f64 = 18.0;
let icon_width: f64 = (width as f64) / (height as f64 / icon_height);
unsafe {
let nsdata = NSData::dataWithBytes_length_(
nil,
icon.as_ptr() as *const std::os::raw::c_void,
icon.len() as u64,
);
let nsimage = NSImage::initWithData_(NSImage::alloc(nil), nsdata);
let new_size = NSSize::new(icon_width, icon_height);
let _: () = msg_send![nsimage, setSize: new_size];
let _: () = msg_send![menuitem, setImage: nsimage];
}
} else {
unsafe {
let _: () = msg_send![menuitem, setImage: nil];
}
}
}

View file

@ -41,8 +41,8 @@ impl Accelerator {
let raw_key = vk_code & 0x00ff;
ACCEL {
fVirt: virt_key as u8,
key: raw_key as u16,
fVirt: virt_key,
key: raw_key,
cmd: menu_id,
}
}

View file

@ -0,0 +1,193 @@
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// taken from https://github.com/rust-windowing/winit/blob/92fdf5ba85f920262a61cee4590f4a11ad5738d1/src/platform_impl/windows/icon.rs
use std::{fmt, io, mem, path::Path, sync::Arc};
use windows_sys::{
core::PCWSTR,
Win32::{
Foundation::RECT,
Graphics::Gdi::{
CreateCompatibleDC, CreateDIBSection, DeleteDC, GetDC, ReleaseDC, SelectObject,
BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, HBITMAP,
},
UI::WindowsAndMessaging::{
CreateIcon, DestroyIcon, DrawIconEx, LoadImageW, DI_NORMAL, HICON, IMAGE_ICON,
LR_DEFAULTSIZE, LR_LOADFROMFILE,
},
},
};
use crate::icon::*;
use super::util;
impl Pixel {
fn convert_to_bgra(&mut self) {
mem::swap(&mut self.r, &mut self.b);
}
}
impl RgbaIcon {
fn into_windows_icon(self) -> Result<WinIcon, BadIcon> {
let rgba = self.rgba;
let pixel_count = rgba.len() / PIXEL_SIZE;
let mut and_mask = Vec::with_capacity(pixel_count);
let pixels =
unsafe { std::slice::from_raw_parts_mut(rgba.as_ptr() as *mut Pixel, pixel_count) };
for pixel in pixels {
and_mask.push(pixel.a.wrapping_sub(std::u8::MAX)); // invert alpha channel
pixel.convert_to_bgra();
}
assert_eq!(and_mask.len(), pixel_count);
let handle = unsafe {
CreateIcon(
0,
self.width as i32,
self.height as i32,
1,
(PIXEL_SIZE * 8) as u8,
and_mask.as_ptr(),
rgba.as_ptr(),
)
};
if handle != 0 {
Ok(WinIcon::from_handle(handle))
} else {
Err(BadIcon::OsError(io::Error::last_os_error()))
}
}
}
#[derive(Debug)]
struct RaiiIcon {
handle: HICON,
}
#[derive(Clone)]
pub(crate) struct WinIcon {
inner: Arc<RaiiIcon>,
}
unsafe impl Send for WinIcon {}
impl WinIcon {
pub unsafe fn to_hbitmap(&self) -> HBITMAP {
let hdc = CreateCompatibleDC(0);
let rc = RECT {
left: 0,
top: 0,
right: 16,
bottom: 16,
};
let mut bitmap_info: BITMAPINFO = std::mem::zeroed();
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as _;
bitmap_info.bmiHeader.biWidth = rc.right;
bitmap_info.bmiHeader.biHeight = rc.bottom;
bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biBitCount = 32;
bitmap_info.bmiHeader.biCompression = BI_RGB;
let h_dc_bitmap = GetDC(0);
let hbitmap = CreateDIBSection(h_dc_bitmap, &bitmap_info, DIB_RGB_COLORS, 0 as _, 0, 0);
ReleaseDC(0, h_dc_bitmap);
let h_bitmap_old = SelectObject(hdc, hbitmap);
DrawIconEx(
hdc,
0,
0,
self.inner.handle,
rc.right,
rc.bottom,
0,
0,
DI_NORMAL,
);
SelectObject(hdc, h_bitmap_old);
DeleteDC(hdc);
hbitmap
}
pub fn from_rgba(rgba: Vec<u8>, width: u32, height: u32) -> Result<Self, BadIcon> {
let rgba_icon = RgbaIcon::from_rgba(rgba, width, height)?;
rgba_icon.into_windows_icon()
}
fn from_handle(handle: HICON) -> Self {
Self {
inner: Arc::new(RaiiIcon { handle }),
}
}
pub(crate) fn from_path<P: AsRef<Path>>(
path: P,
size: Option<(u32, u32)>,
) -> Result<Self, BadIcon> {
// width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size
let (width, height) = size.unwrap_or((0, 0));
let wide_path = util::encode_wide(path.as_ref());
let handle = unsafe {
LoadImageW(
0,
wide_path.as_ptr(),
IMAGE_ICON,
width as i32,
height as i32,
LR_DEFAULTSIZE | LR_LOADFROMFILE,
)
};
if handle != 0 {
Ok(WinIcon::from_handle(handle as HICON))
} else {
Err(BadIcon::OsError(io::Error::last_os_error()))
}
}
pub(crate) fn from_resource(
resource_id: u16,
size: Option<(u32, u32)>,
) -> Result<Self, BadIcon> {
// width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size
let (width, height) = size.unwrap_or((0, 0));
let handle = unsafe {
LoadImageW(
util::get_instance_handle(),
resource_id as PCWSTR,
IMAGE_ICON,
width as i32,
height as i32,
LR_DEFAULTSIZE,
)
};
if handle != 0 {
Ok(WinIcon::from_handle(handle as HICON))
} else {
Err(BadIcon::OsError(io::Error::last_os_error()))
}
}
}
impl Drop for RaiiIcon {
fn drop(&mut self) {
unsafe { DestroyIcon(self.handle) };
}
}
impl fmt::Debug for WinIcon {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
(*self.inner).fmt(formatter)
}
}

View file

@ -3,10 +3,14 @@
// SPDX-License-Identifier: MIT
mod accelerator;
mod icon;
mod util;
pub(crate) use self::icon::WinIcon as PlatformIcon;
use crate::{
accelerator::Accelerator,
icon::Icon,
predefined::PredfinedMenuItemType,
util::{AddOp, Counter},
MenuItemType,
@ -15,7 +19,7 @@ use std::{cell::RefCell, fmt::Debug, rc::Rc};
use util::{decode_wide, encode_wide, Accel};
use windows_sys::Win32::{
Foundation::{HWND, LPARAM, LRESULT, POINT, WPARAM},
Graphics::Gdi::ClientToScreen,
Graphics::Gdi::{ClientToScreen, HBITMAP},
UI::{
Input::KeyboardAndMouse::{SendInput, INPUT, INPUT_KEYBOARD, KEYEVENTF_KEYUP, VK_CONTROL},
Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass},
@ -26,7 +30,8 @@ use windows_sys::Win32::{
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_STATE, MIIM_STRING, SW_MINIMIZE, TPM_LEFTALIGN, WM_COMMAND, WM_DESTROY,
MIIM_BITMAP, MIIM_STATE, MIIM_STRING, SW_MINIMIZE, TPM_LEFTALIGN, WM_COMMAND,
WM_DESTROY,
},
},
};
@ -57,6 +62,9 @@ struct MenuChild {
// check menu item fields
checked: bool,
// icon menu item fields
icon: Option<Icon>,
// submenu fields
hmenu: HMENU,
hpopupmenu: HMENU,
@ -164,6 +172,16 @@ impl MenuChild {
};
}
}
fn set_icon(&mut self, icon: Option<Icon>) {
self.icon = icon.clone();
let hbitmap = icon.map(|i| unsafe { i.inner.to_hbitmap() }).unwrap_or(0);
let info = create_icon_item_info(hbitmap);
for parent in &self.parents_hemnu {
unsafe { SetMenuItemInfoW(*parent, self.id(), false.into(), &info) };
}
}
}
#[derive(Clone)]
@ -241,6 +259,14 @@ impl Menu {
flags |= MF_CHECKED;
}
child
}
MenuItemType::Icon => {
let item = item.as_any().downcast_ref::<crate::IconMenuItem>().unwrap();
let child = &item.0 .0;
flags |= MF_STRING;
child
}
}
@ -295,11 +321,31 @@ impl Menu {
}
}
}
{
let child_ = child.borrow();
if child_.type_ == MenuItemType::Icon {
let hbitmap = child_
.icon
.as_ref()
.map(|i| unsafe { i.inner.to_hbitmap() })
.unwrap_or(0);
let info = create_icon_item_info(hbitmap);
unsafe {
SetMenuItemInfoW(self.hmenu, child_.id, false.into(), &info);
SetMenuItemInfoW(self.hpopupmenu, child_.id, false.into(), &info);
};
}
}
{
let mut child_ = child.borrow_mut();
child_.parents_hemnu.push(self.hmenu);
child_.parents_hemnu.push(self.hpopupmenu);
}
{
let mut children = self.children.borrow_mut();
match op {
@ -342,6 +388,10 @@ impl Menu {
.unwrap();
&item.0 .0
}
MenuItemType::Icon => {
let item = item.as_any().downcast_ref::<crate::IconMenuItem>().unwrap();
&item.0 .0
}
};
{
@ -383,6 +433,7 @@ impl Menu {
Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone())))
}
MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))),
MenuItemType::Icon => Box::new(crate::IconMenuItem(IconMenuItem(c.clone()))),
}
})
.collect()
@ -586,6 +637,14 @@ impl Submenu {
flags |= MF_CHECKED;
}
child
}
MenuItemType::Icon => {
let item = item.as_any().downcast_ref::<crate::IconMenuItem>().unwrap();
let child = &item.0 .0;
flags |= MF_STRING;
child
}
}
@ -643,12 +702,33 @@ impl Submenu {
}
}
}
{
let self_ = self.0.borrow();
let child_ = child.borrow();
if child_.type_ == MenuItemType::Icon {
let hbitmap = child_
.icon
.as_ref()
.map(|i| unsafe { i.inner.to_hbitmap() })
.unwrap_or(0);
let info = create_icon_item_info(hbitmap);
unsafe {
SetMenuItemInfoW(self_.hmenu, child_.id, false.into(), &info);
SetMenuItemInfoW(self_.hpopupmenu, child_.id, false.into(), &info);
};
}
}
{
let self_ = self.0.borrow();
let mut child_ = child.borrow_mut();
child_.parents_hemnu.push(self_.hmenu);
child_.parents_hemnu.push(self_.hpopupmenu);
}
{
let mut self_ = self.0.borrow_mut();
let children = self_.children.as_mut().unwrap();
@ -688,6 +768,10 @@ impl Submenu {
.unwrap();
&item.0 .0
}
MenuItemType::Icon => {
let item = item.as_any().downcast_ref::<crate::IconMenuItem>().unwrap();
&item.0 .0
}
};
{
@ -733,6 +817,7 @@ impl Submenu {
Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone())))
}
MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))),
MenuItemType::Icon => Box::new(crate::IconMenuItem(IconMenuItem(c.clone()))),
}
})
.collect()
@ -908,6 +993,53 @@ impl CheckMenuItem {
}
}
#[derive(Clone, Debug)]
pub(crate) struct IconMenuItem(Rc<RefCell<MenuChild>>);
impl IconMenuItem {
pub fn new(
text: &str,
enabled: bool,
icon: Option<Icon>,
accelerator: Option<Accelerator>,
) -> Self {
Self(Rc::new(RefCell::new(MenuChild {
type_: MenuItemType::Icon,
text: text.to_string(),
enabled,
parents_hemnu: Vec::new(),
id: COUNTER.next(),
accelerator,
icon,
..Default::default()
})))
}
pub fn id(&self) -> u32 {
self.0.borrow().id()
}
pub fn text(&self) -> String {
self.0.borrow().text()
}
pub fn set_text(&self, text: &str) {
self.0.borrow_mut().set_text(text)
}
pub fn is_enabled(&self) -> bool {
self.0.borrow().is_enabled()
}
pub fn set_enabled(&self, enabled: bool) {
self.0.borrow_mut().set_enabled(enabled)
}
pub fn set_icon(&self, icon: Option<Icon>) {
self.0.borrow_mut().set_icon(icon)
}
}
const MENU_SUBCLASS_ID: usize = 200;
const SUBMENU_SUBCLASS_ID: usize = 201;
const WM_CLEAR_MENU_DATA: u32 = 600;
@ -1089,3 +1221,11 @@ fn execute_edit_command(command: EditCommand) {
SendInput(4, &inputs as *const _, std::mem::size_of::<INPUT>() as _);
}
}
fn create_icon_item_info(hbitmap: HBITMAP) -> MENUITEMINFOW {
let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() };
info.cbSize = std::mem::size_of::<MENUITEMINFOW>() as _;
info.fMask = MIIM_BITMAP;
info.hbmpItem = hbitmap;
info
}

View file

@ -51,3 +51,20 @@ impl DerefMut for Accel {
&mut self.0
}
}
// taken from winit's code base
// https://github.com/rust-windowing/winit/blob/ee88e38f13fbc86a7aafae1d17ad3cd4a1e761df/src/platform_impl/windows/util.rs#L138
pub fn get_instance_handle() -> windows_sys::Win32::Foundation::HINSTANCE {
// Gets the instance handle by taking the address of the
// pseudo-variable created by the microsoft linker:
// https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483
// This is preferred over GetModuleHandle(NULL) because it also works in DLLs:
// https://stackoverflow.com/questions/21718027/getmodulehandlenull-vs-hinstance
extern "C" {
static __ImageBase: windows_sys::Win32::System::SystemServices::IMAGE_DOS_HEADER;
}
unsafe { &__ImageBase as *const _ as _ }
}