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

@ -14,7 +14,7 @@ categories = [ "gui" ]
crossbeam-channel = "0.5" crossbeam-channel = "0.5"
keyboard-types = "0.6" keyboard-types = "0.6"
once_cell = "1" once_cell = "1"
thiserror = "1.0.38" thiserror = "1"
[target."cfg(target_os = \"windows\")".dependencies.windows-sys] [target."cfg(target_os = \"windows\")".dependencies.windows-sys]
version = "0.42" version = "0.42"
@ -24,18 +24,22 @@ features = [
"Win32_Graphics_Gdi", "Win32_Graphics_Gdi",
"Win32_UI_Shell", "Win32_UI_Shell",
"Win32_Globalization", "Win32_Globalization",
"Win32_UI_Input_KeyboardAndMouse" "Win32_UI_Input_KeyboardAndMouse",
"Win32_System_SystemServices",
] ]
[target."cfg(target_os = \"linux\")".dependencies] [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" libxdo = "0.6.0"
[target."cfg(target_os = \"macos\")".dependencies] [target."cfg(target_os = \"macos\")".dependencies]
cocoa = "0.24" cocoa = "0.24"
objc = "0.2" objc = "0.2"
png = "0.17"
[dev-dependencies] [dev-dependencies]
winit = "0.27" winit = "0.27"
tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" } 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)] #![allow(unused)]
use muda::{ use muda::{
accelerator::{Accelerator, Code, Modifiers}, accelerator::{Accelerator, Code, Modifiers},
menu_event_receiver, AboutMetadata, CheckMenuItem, ContextMenu, Menu, MenuItem, menu_event_receiver, AboutMetadata, CheckMenuItem, ContextMenu, IconMenuItem, Menu, MenuItem,
PredefinedMenuItem, Submenu, PredefinedMenuItem, Submenu,
}; };
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@ -77,7 +77,16 @@ fn main() {
true, true,
Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyC)), 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_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_2 = CheckMenuItem::new("Check Custom 2", false, true, None);
let check_custom_i_3 = CheckMenuItem::new( let check_custom_i_3 = CheckMenuItem::new(
@ -93,7 +102,7 @@ fn main() {
file_m.append_items(&[ file_m.append_items(&[
&custom_i_1, &custom_i_1,
&custom_i_2, &image_item,
&window_m, &window_m,
&PredefinedMenuItem::separator(), &PredefinedMenuItem::separator(),
&check_custom_i_1, &check_custom_i_1,
@ -113,11 +122,11 @@ fn main() {
}), }),
), ),
&check_custom_i_3, &check_custom_i_3,
&custom_i_2, &image_item,
&custom_i_1, &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")] #[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")] #[cfg(target_os = "macos")]
menu.show_context_menu_for_nsview(window.ns_view() as _, x, y); 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)] #![allow(unused)]
use muda::{ use muda::{
accelerator::{Accelerator, Code, Modifiers}, accelerator::{Accelerator, Code, Modifiers},
menu_event_receiver, AboutMetadata, CheckMenuItem, ContextMenu, Menu, MenuItem, menu_event_receiver, AboutMetadata, CheckMenuItem, ContextMenu, IconMenuItem, Menu, MenuItem,
PredefinedMenuItem, Submenu, PredefinedMenuItem, Submenu,
}; };
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use winit::platform::macos::{EventLoopBuilderExtMacOS, WindowExtMacOS}; use winit::platform::macos::{EventLoopBuilderExtMacOS, WindowExtMacOS};
#[cfg(target_os = "linux")]
use winit::platform::unix::WindowExtUnix;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use winit::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows}; use winit::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows};
use winit::{ use winit::{
@ -77,7 +79,11 @@ fn main() {
true, true,
Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyC)), 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_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_2 = CheckMenuItem::new("Check Custom 2", false, true, None);
let check_custom_i_3 = CheckMenuItem::new( let check_custom_i_3 = CheckMenuItem::new(
@ -93,7 +99,7 @@ fn main() {
file_m.append_items(&[ file_m.append_items(&[
&custom_i_1, &custom_i_1,
&custom_i_2, &image_item,
&window_m, &window_m,
&PredefinedMenuItem::separator(), &PredefinedMenuItem::separator(),
&check_custom_i_1, &check_custom_i_1,
@ -113,11 +119,11 @@ fn main() {
}), }),
), ),
&check_custom_i_3, &check_custom_i_3,
&custom_i_2, &image_item,
&custom_i_1, &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")] #[cfg(target_os = "windows")]
{ {
@ -163,7 +169,7 @@ fn main() {
.. ..
} => { } => {
if window_id == window2.id() { if window_id == window2.id() {
show_context_menu(&window2, &window_m, x, y); show_context_menu(&window2, &file_m, x, y);
} }
} }
Event::MainEventsCleared => { Event::MainEventsCleared => {
@ -174,7 +180,7 @@ fn main() {
if let Ok(event) = menu_channel.try_recv() { if let Ok(event) = menu_channel.try_recv() {
if event.id == custom_i_1.id() { 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); println!("{:?}", event);
} }
@ -187,3 +193,15 @@ fn show_context_menu(window: &Window, menu: &dyn ContextMenu, x: f64, y: f64) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
menu.show_context_menu_for_nsview(window.ns_view() as _, x, y); 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; pub mod accelerator;
mod check_menu_item; mod check_menu_item;
mod error; mod error;
mod icon_menu_item;
mod menu; mod menu;
mod menu_item; mod menu_item;
mod platform_impl; mod platform_impl;
@ -121,7 +122,9 @@ extern crate objc;
pub use self::error::*; pub use self::error::*;
pub use check_menu_item::CheckMenuItem; pub use check_menu_item::CheckMenuItem;
pub use icon_menu_item::IconMenuItem;
pub use menu::Menu; pub use menu::Menu;
pub mod icon;
pub use menu_item::MenuItem; pub use menu_item::MenuItem;
pub use predefined::{AboutMetadata, PredefinedMenuItem}; pub use predefined::{AboutMetadata, PredefinedMenuItem};
pub use submenu::Submenu; pub use submenu::Submenu;
@ -132,6 +135,7 @@ pub enum MenuItemType {
Normal, Normal,
Predefined, Predefined,
Check, Check,
Icon,
} }
impl Default for MenuItemType { impl Default for MenuItemType {

View file

@ -90,9 +90,12 @@ pub fn register_accelerator<M: IsA<gtk::Widget>>(
item: &M, item: &M,
accel_group: &AccelGroup, accel_group: &AccelGroup,
accelerator: &Accelerator, accelerator: &Accelerator,
) { ) -> Option<(gdk::ModifierType, u32)> {
if let Ok((mods, key)) = parse_accelerator(accelerator) { if let Ok((mods, key)) = parse_accelerator(accelerator) {
item.add_accelerator("activate", accel_group, key, mods, gtk::AccelFlags::VISIBLE); 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 // SPDX-License-Identifier: MIT
mod accelerator; mod accelerator;
mod icon;
pub(crate) use icon::PlatformIcon;
use crate::{ use crate::{
accelerator::Accelerator, accelerator::Accelerator,
icon::Icon,
predefined::PredfinedMenuItemType, predefined::PredfinedMenuItemType,
util::{AddOp, Counter}, util::{AddOp, Counter},
MenuItemType, MenuItemType,
@ -38,7 +42,8 @@ macro_rules! return_if_predefined_item_not_supported {
( (
crate::MenuItemType::Submenu crate::MenuItemType::Submenu
| crate::MenuItemType::Normal | crate::MenuItemType::Normal
| crate::MenuItemType::Check, | crate::MenuItemType::Check
| crate::MenuItemType::Icon,
_, _,
) => {} ) => {}
_ => return, _ => return,
@ -71,6 +76,9 @@ struct MenuChild {
checked: bool, checked: bool,
is_syncing_checked_state: Rc<AtomicBool>, is_syncing_checked_state: Rc<AtomicBool>,
// icon menu item fields
icon: Option<Icon>,
// submenu fields // submenu fields
children: Option<Vec<Rc<RefCell<MenuChild>>>>, children: Option<Vec<Rc<RefCell<MenuChild>>>>,
gtk_menus: HashMap<u32, Vec<(u32, gtk::Menu)>>, gtk_menus: HashMap<u32, Vec<(u32, gtk::Menu)>>,
@ -157,6 +165,21 @@ impl MenuChild {
self.is_syncing_checked_state self.is_syncing_checked_state
.store(false, Ordering::Release); .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 { struct InnerMenu {
@ -316,6 +339,7 @@ impl Menu {
Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone()))) Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone())))
} }
MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))), MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))),
MenuItemType::Icon => Box::new(crate::IconMenuItem(IconMenuItem(c.clone()))),
} }
}) })
.collect() .collect()
@ -648,6 +672,7 @@ impl Submenu {
Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone()))) Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone())))
} }
MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))), MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))),
MenuItemType::Icon => Box::new(crate::IconMenuItem(IconMenuItem(c.clone()))),
} }
}) })
.collect() .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 + '_ { impl dyn crate::MenuItemExt + '_ {
fn get_child(&self) -> Rc<RefCell<MenuChild>> { fn get_child(&self) -> Rc<RefCell<MenuChild>> {
match self.type_() { match self.type_() {
MenuItemType::Submenu => { MenuItemType::Submenu => self
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
.as_any() .as_any()
.downcast_ref::<crate::CheckMenuItem>() .downcast_ref::<crate::Submenu>()
.unwrap(); .unwrap()
Rc::clone(&menuitem.0 .0) .0
} .0
MenuItemType::Predefined => { .clone(),
let menuitem = self MenuItemType::Normal => self
.as_any()
.downcast_ref::<crate::MenuItem>()
.unwrap()
.0
.0
.clone(),
MenuItemType::Predefined => self
.as_any() .as_any()
.downcast_ref::<crate::PredefinedMenuItem>() .downcast_ref::<crate::PredefinedMenuItem>()
.unwrap(); .unwrap()
Rc::clone(&menuitem.0 .0) .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, add_to_cache: bool,
) -> gtk::MenuItem { ) -> gtk::MenuItem {
match self.type_() { match self.type_() {
MenuItemType::Submenu => { MenuItemType::Submenu => self
let submenu = self.as_any().downcast_ref::<crate::Submenu>().unwrap(); .as_any()
submenu .downcast_ref::<crate::Submenu>()
.unwrap()
.0 .0
.make_gtk_menu_item(menu_id, accel_group, add_to_cache) .make_gtk_menu_item(menu_id, accel_group, add_to_cache),
} MenuItemType::Normal => self
MenuItemType::Normal => { .as_any()
let menuitem = self.as_any().downcast_ref::<crate::MenuItem>().unwrap(); .downcast_ref::<crate::MenuItem>()
menuitem .unwrap()
.0 .0
.make_gtk_menu_item(menu_id, accel_group, add_to_cache) .make_gtk_menu_item(menu_id, accel_group, add_to_cache),
} MenuItemType::Predefined => self
MenuItemType::Predefined => {
let menuitem = self
.as_any() .as_any()
.downcast_ref::<crate::PredefinedMenuItem>() .downcast_ref::<crate::PredefinedMenuItem>()
.unwrap(); .unwrap()
menuitem
.0 .0
.make_gtk_menu_item(menu_id, accel_group, add_to_cache) .make_gtk_menu_item(menu_id, accel_group, add_to_cache),
} MenuItemType::Check => self
MenuItemType::Check => {
let menuitem = self
.as_any() .as_any()
.downcast_ref::<crate::CheckMenuItem>() .downcast_ref::<crate::CheckMenuItem>()
.unwrap(); .unwrap()
menuitem
.0 .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 // SPDX-License-Identifier: MIT
mod accelerator; mod accelerator;
mod icon;
mod util; mod util;
pub(crate) use icon::PlatformIcon;
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Once}; use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Once};
use cocoa::{ use cocoa::{
appkit::{CGFloat, NSApp, NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem}, appkit::{CGFloat, NSApp, NSApplication, NSEventModifierFlags, NSImage, NSMenu, NSMenuItem},
base::{id, nil, selector, NO, YES}, base::{id, nil, selector, NO, YES},
foundation::{NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSString}, foundation::{NSAutoreleasePool, NSData, NSInteger, NSPoint, NSRect, NSSize, NSString},
}; };
use objc::{ use objc::{
declare::ClassDecl, declare::ClassDecl,
@ -20,6 +23,7 @@ use objc::{
use self::util::{app_name_string, strip_mnemonic}; use self::util::{app_name_string, strip_mnemonic};
use crate::{ use crate::{
accelerator::Accelerator, accelerator::Accelerator,
icon::Icon,
predefined::PredfinedMenuItemType, predefined::PredfinedMenuItemType,
util::{AddOp, Counter}, util::{AddOp, Counter},
MenuItemExt, MenuItemType, MenuItemExt, MenuItemType,
@ -31,7 +35,7 @@ static BLOCK_PTR: &str = "mudaMenuItemBlockPtr";
/// A generic child in a menu /// A generic child in a menu
/// ///
/// Be careful when cloning this item and treat it as read-only /// Be careful when cloning this item and treat it as read-only
#[derive(Debug, PartialEq, Eq)] #[derive(Debug)]
#[allow(dead_code)] #[allow(dead_code)]
struct MenuChild { struct MenuChild {
// shared fields between submenus and menu items // shared fields between submenus and menu items
@ -51,6 +55,9 @@ struct MenuChild {
// check menu item fields // check menu item fields
checked: bool, checked: bool,
// icon menu item fields
icon: Option<Icon>,
// submenu fields // submenu fields
children: Option<Vec<Rc<RefCell<MenuChild>>>>, children: Option<Vec<Rc<RefCell<MenuChild>>>>,
ns_menus: HashMap<u32, Vec<id>>, ns_menus: HashMap<u32, Vec<id>>,
@ -68,6 +75,7 @@ impl Default for MenuChild {
accelerator: Default::default(), accelerator: Default::default(),
predefined_item_type: Default::default(), predefined_item_type: Default::default(),
checked: Default::default(), checked: Default::default(),
icon: Default::default(),
children: Default::default(), children: Default::default(),
ns_menus: Default::default(), ns_menus: Default::default(),
ns_menu: (0, 0 as _), 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)] #[derive(Clone, Debug)]
@ -180,6 +197,13 @@ impl Menu {
let menuitem = item.as_any().downcast_ref::<crate::MenuItem>().unwrap(); let menuitem = item.as_any().downcast_ref::<crate::MenuItem>().unwrap();
menuitem.0 .0.borrow_mut() menuitem.0 .0.borrow_mut()
} }
MenuItemType::Predefined => {
let menuitem = item
.as_any()
.downcast_ref::<crate::PredefinedMenuItem>()
.unwrap();
menuitem.0 .0.borrow_mut()
}
MenuItemType::Check => { MenuItemType::Check => {
let menuitem = item let menuitem = item
.as_any() .as_any()
@ -187,11 +211,8 @@ impl Menu {
.unwrap(); .unwrap();
menuitem.0 .0.borrow_mut() menuitem.0 .0.borrow_mut()
} }
MenuItemType::Predefined => { MenuItemType::Icon => {
let menuitem = item let menuitem = item.as_any().downcast_ref::<crate::IconMenuItem>().unwrap();
.as_any()
.downcast_ref::<crate::PredefinedMenuItem>()
.unwrap();
menuitem.0 .0.borrow_mut() menuitem.0 .0.borrow_mut()
} }
} }
@ -230,6 +251,7 @@ impl Menu {
Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone()))) Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone())))
} }
MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))), MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))),
MenuItemType::Icon => Box::new(crate::IconMenuItem(IconMenuItem(c.clone()))),
} }
}) })
.collect() .collect()
@ -306,6 +328,7 @@ impl Submenu {
PredefinedMenuItem(item.clone()).make_ns_item_for_menu(menu_id) PredefinedMenuItem(item.clone()).make_ns_item_for_menu(menu_id)
} }
MenuItemType::Check => CheckMenuItem(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) }; unsafe { ns_submenu.addItem_(ns_item) };
} }
@ -399,7 +422,7 @@ impl Submenu {
let children = self_.children.as_mut().unwrap(); let children = self_.children.as_mut().unwrap();
let index = children let index = children
.iter() .iter()
.position(|e| e == &child) .position(|e| e.borrow().id == item.id())
.ok_or(crate::Error::NotAChildOfThisMenu)?; .ok_or(crate::Error::NotAChildOfThisMenu)?;
children.remove(index); children.remove(index);
@ -422,6 +445,7 @@ impl Submenu {
Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone()))) Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone())))
} }
MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))), MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))),
MenuItemType::Icon => Box::new(crate::IconMenuItem(IconMenuItem(c.clone()))),
} }
}) })
.collect() .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 { impl PredfinedMenuItemType {
pub(crate) fn selector(&self) -> Option<Sel> { pub(crate) fn selector(&self) -> Option<Sel> {
match self { match self {
@ -725,55 +828,76 @@ impl PredfinedMenuItemType {
impl dyn MenuItemExt + '_ { impl dyn MenuItemExt + '_ {
fn get_child(&self) -> Rc<RefCell<MenuChild>> { fn get_child(&self) -> Rc<RefCell<MenuChild>> {
match self.type_() { match self.type_() {
MenuItemType::Submenu => { MenuItemType::Submenu => self
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
.as_any() .as_any()
.downcast_ref::<crate::CheckMenuItem>() .downcast_ref::<crate::Submenu>()
.unwrap(); .unwrap()
Rc::clone(&menuitem.0 .0) .0
} .0
MenuItemType::Predefined => { .clone(),
let menuitem = self MenuItemType::Normal => self
.as_any()
.downcast_ref::<crate::MenuItem>()
.unwrap()
.0
.0
.clone(),
MenuItemType::Predefined => self
.as_any() .as_any()
.downcast_ref::<crate::PredefinedMenuItem>() .downcast_ref::<crate::PredefinedMenuItem>()
.unwrap(); .unwrap()
Rc::clone(&menuitem.0 .0) .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 { fn make_ns_item_for_menu(&self, menu_id: u32) -> *mut Object {
match self.type_() { match self.type_() {
MenuItemType::Submenu => { MenuItemType::Submenu => self
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
.as_any() .as_any()
.downcast_ref::<crate::CheckMenuItem>() .downcast_ref::<crate::Submenu>()
.unwrap(); .unwrap()
menuitem.0.make_ns_item_for_menu(menu_id) .0
} .make_ns_item_for_menu(menu_id),
MenuItemType::Predefined => { MenuItemType::Normal => self
let menuitem = self .as_any()
.downcast_ref::<crate::MenuItem>()
.unwrap()
.0
.make_ns_item_for_menu(menu_id),
MenuItemType::Predefined => self
.as_any() .as_any()
.downcast_ref::<crate::PredefinedMenuItem>() .downcast_ref::<crate::PredefinedMenuItem>()
.unwrap(); .unwrap()
menuitem.0.make_ns_item_for_menu(menu_id) .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 selector = selector.unwrap_or_else(|| Sel::from_ptr(std::ptr::null()));
let key_equivalent = accelerator let key_equivalent = (*accelerator)
.clone()
.map(|accel| accel.key_equivalent()) .map(|accel| accel.key_equivalent())
.unwrap_or_default(); .unwrap_or_default();
let key_equivalent = NSString::alloc(nil) let key_equivalent = NSString::alloc(nil)
.init_str(key_equivalent.as_str()) .init_str(key_equivalent.as_str())
.autorelease(); .autorelease();
let modifier_mask = accelerator let modifier_mask = (*accelerator)
.clone()
.map(|accel| accel.key_modifier_mask()) .map(|accel| accel.key_modifier_mask())
.unwrap_or_else(NSEventModifierFlags::empty); .unwrap_or_else(NSEventModifierFlags::empty);
@ -863,3 +985,30 @@ fn create_ns_menu_item(
ns_menu_item.autorelease() 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; let raw_key = vk_code & 0x00ff;
ACCEL { ACCEL {
fVirt: virt_key as u8, fVirt: virt_key,
key: raw_key as u16, key: raw_key,
cmd: menu_id, 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 // SPDX-License-Identifier: MIT
mod accelerator; mod accelerator;
mod icon;
mod util; mod util;
pub(crate) use self::icon::WinIcon as PlatformIcon;
use crate::{ use crate::{
accelerator::Accelerator, accelerator::Accelerator,
icon::Icon,
predefined::PredfinedMenuItemType, predefined::PredfinedMenuItemType,
util::{AddOp, Counter}, util::{AddOp, Counter},
MenuItemType, MenuItemType,
@ -15,7 +19,7 @@ use std::{cell::RefCell, fmt::Debug, rc::Rc};
use util::{decode_wide, encode_wide, Accel}; use util::{decode_wide, encode_wide, Accel};
use windows_sys::Win32::{ use windows_sys::Win32::{
Foundation::{HWND, LPARAM, LRESULT, POINT, WPARAM}, Foundation::{HWND, LPARAM, LRESULT, POINT, WPARAM},
Graphics::Gdi::ClientToScreen, Graphics::Gdi::{ClientToScreen, HBITMAP},
UI::{ UI::{
Input::KeyboardAndMouse::{SendInput, INPUT, INPUT_KEYBOARD, KEYEVENTF_KEYUP, VK_CONTROL}, Input::KeyboardAndMouse::{SendInput, INPUT, INPUT_KEYBOARD, KEYEVENTF_KEYUP, VK_CONTROL},
Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass}, Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass},
@ -26,7 +30,8 @@ use windows_sys::Win32::{
SetMenuItemInfoW, ShowWindow, TrackPopupMenu, HACCEL, HMENU, MB_ICONINFORMATION, SetMenuItemInfoW, ShowWindow, TrackPopupMenu, HACCEL, HMENU, MB_ICONINFORMATION,
MENUITEMINFOW, MFS_CHECKED, MFS_DISABLED, MF_BYCOMMAND, MF_BYPOSITION, MF_CHECKED, 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, 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 // check menu item fields
checked: bool, checked: bool,
// icon menu item fields
icon: Option<Icon>,
// submenu fields // submenu fields
hmenu: HMENU, hmenu: HMENU,
hpopupmenu: 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)] #[derive(Clone)]
@ -241,6 +259,14 @@ impl Menu {
flags |= MF_CHECKED; 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 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(); let mut child_ = child.borrow_mut();
child_.parents_hemnu.push(self.hmenu); child_.parents_hemnu.push(self.hmenu);
child_.parents_hemnu.push(self.hpopupmenu); child_.parents_hemnu.push(self.hpopupmenu);
} }
{ {
let mut children = self.children.borrow_mut(); let mut children = self.children.borrow_mut();
match op { match op {
@ -342,6 +388,10 @@ impl Menu {
.unwrap(); .unwrap();
&item.0 .0 &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()))) Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone())))
} }
MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))), MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))),
MenuItemType::Icon => Box::new(crate::IconMenuItem(IconMenuItem(c.clone()))),
} }
}) })
.collect() .collect()
@ -586,6 +637,14 @@ impl Submenu {
flags |= MF_CHECKED; 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 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 self_ = self.0.borrow();
let mut child_ = child.borrow_mut(); let mut child_ = child.borrow_mut();
child_.parents_hemnu.push(self_.hmenu); child_.parents_hemnu.push(self_.hmenu);
child_.parents_hemnu.push(self_.hpopupmenu); child_.parents_hemnu.push(self_.hpopupmenu);
} }
{ {
let mut self_ = self.0.borrow_mut(); let mut self_ = self.0.borrow_mut();
let children = self_.children.as_mut().unwrap(); let children = self_.children.as_mut().unwrap();
@ -688,6 +768,10 @@ impl Submenu {
.unwrap(); .unwrap();
&item.0 .0 &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()))) Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone())))
} }
MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))), MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))),
MenuItemType::Icon => Box::new(crate::IconMenuItem(IconMenuItem(c.clone()))),
} }
}) })
.collect() .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 MENU_SUBCLASS_ID: usize = 200;
const SUBMENU_SUBCLASS_ID: usize = 201; const SUBMENU_SUBCLASS_ID: usize = 201;
const WM_CLEAR_MENU_DATA: u32 = 600; 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 _); 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 &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 _ }
}