diff --git a/.changes/icon-menu-item.md b/.changes/icon-menu-item.md new file mode 100644 index 0000000..56cafa8 --- /dev/null +++ b/.changes/icon-menu-item.md @@ -0,0 +1,5 @@ +--- +"muda": "minor" +--- + +Add `IconMenuItem` \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 8113212..51fbdc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/examples/icon.png b/examples/icon.png new file mode 100644 index 0000000..3c3627c Binary files /dev/null and b/examples/icon.png differ diff --git a/examples/tao.rs b/examples/tao.rs index e21f813..99613a8 100644 --- a/examples/tao.rs +++ b/examples/tao.rs @@ -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(&[©_i, &paste_i, &PredefinedMenuItem::separator()]); + edit_m.append_items(&[©_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") +} diff --git a/examples/winit.rs b/examples/winit.rs index 44d9190..9f909b8 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -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(&[©_i, &paste_i, &PredefinedMenuItem::separator()]); + edit_m.append_items(&[©_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") +} diff --git a/src/icon.rs b/src/icon.rs new file mode 100644 index 0000000..824cecb --- /dev/null +++ b/src/icon.rs @@ -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::(); + +#[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, + 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, width: u32, height: u32) -> Result { + 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, width: u32, height: u32) -> Result { + // 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, width: u32, height: u32) -> Result { + 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>( + path: P, + size: Option<(u32, u32)>, + ) -> Result { + 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 { + let win_icon = PlatformIcon::from_resource(ordinal, size)?; + Ok(Icon { inner: win_icon }) + } +} diff --git a/src/icon_menu_item.rs b/src/icon_menu_item.rs new file mode 100644 index 0000000..4404089 --- /dev/null +++ b/src/icon_menu_item.rs @@ -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>( + text: S, + enabled: bool, + icon: Option, + acccelerator: Option, + ) -> 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>(&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) { + self.0.set_icon(icon) + } +} diff --git a/src/lib.rs b/src/lib.rs index e7d4fa2..12a1c80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 { diff --git a/src/platform_impl/gtk/accelerator.rs b/src/platform_impl/gtk/accelerator.rs index c8fda72..28901bb 100644 --- a/src/platform_impl/gtk/accelerator.rs +++ b/src/platform_impl/gtk/accelerator.rs @@ -90,9 +90,12 @@ pub fn register_accelerator>( 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 } } diff --git a/src/platform_impl/gtk/icon.rs b/src/platform_impl/gtk/icon.rs new file mode 100644 index 0000000..14e88da --- /dev/null +++ b/src/platform_impl/gtk/icon.rs @@ -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, + width: i32, + height: i32, + row_stride: i32, +} + +impl From 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, width: u32, height: u32) -> Result { + 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() + } +} diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 600075e..d8c0459 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -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, + // icon menu item fields + icon: Option, + // submenu fields children: Option>>>, gtk_menus: HashMap>, @@ -157,6 +165,21 @@ impl MenuChild { self.is_syncing_checked_state .store(false, Ordering::Release); } + + fn set_icon(&mut self, icon: Option) { + 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::().unwrap(); + box_container.children()[0] + .downcast_ref::() + .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>); + +impl IconMenuItem { + pub fn new( + text: &str, + enabled: bool, + icon: Option, + accelerator: Option, + ) -> 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<>k::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) { + self.0.borrow_mut().set_icon(icon) + } +} + impl dyn crate::MenuItemExt + '_ { fn get_child(&self) -> Rc> { match self.type_() { - MenuItemType::Submenu => { - let submenu = self.as_any().downcast_ref::().unwrap(); - Rc::clone(&submenu.0 .0) - } - MenuItemType::Normal => { - let menuitem = self.as_any().downcast_ref::().unwrap(); - Rc::clone(&menuitem.0 .0) - } - MenuItemType::Check => { - let menuitem = self - .as_any() - .downcast_ref::() - .unwrap(); - Rc::clone(&menuitem.0 .0) - } - MenuItemType::Predefined => { - let menuitem = self - .as_any() - .downcast_ref::() - .unwrap(); - Rc::clone(&menuitem.0 .0) - } + MenuItemType::Submenu => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .0 + .clone(), + MenuItemType::Normal => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .0 + .clone(), + + MenuItemType::Predefined => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .0 + .clone(), + MenuItemType::Check => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .0 + .clone(), + MenuItemType::Icon => self + .as_any() + .downcast_ref::() + .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::().unwrap(); - submenu - .0 - .make_gtk_menu_item(menu_id, accel_group, add_to_cache) - } - MenuItemType::Normal => { - let menuitem = self.as_any().downcast_ref::().unwrap(); - menuitem - .0 - .make_gtk_menu_item(menu_id, accel_group, add_to_cache) - } - MenuItemType::Predefined => { - let menuitem = self - .as_any() - .downcast_ref::() - .unwrap(); - menuitem - .0 - .make_gtk_menu_item(menu_id, accel_group, add_to_cache) - } - MenuItemType::Check => { - let menuitem = self - .as_any() - .downcast_ref::() - .unwrap(); - menuitem - .0 - .make_gtk_menu_item(menu_id, accel_group, add_to_cache) - } + MenuItemType::Submenu => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .make_gtk_menu_item(menu_id, accel_group, add_to_cache), + MenuItemType::Normal => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .make_gtk_menu_item(menu_id, accel_group, add_to_cache), + MenuItemType::Predefined => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .make_gtk_menu_item(menu_id, accel_group, add_to_cache), + MenuItemType::Check => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .make_gtk_menu_item(menu_id, accel_group, add_to_cache), + MenuItemType::Icon => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .make_gtk_menu_item(menu_id, accel_group, add_to_cache), } } } diff --git a/src/platform_impl/macos/icon.rs b/src/platform_impl/macos/icon.rs new file mode 100644 index 0000000..3a57374 --- /dev/null +++ b/src/platform_impl/macos/icon.rs @@ -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, width: u32, height: u32) -> Result { + 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 { + 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 + } +} diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index d0a0f75..8bebb22 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -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, + // submenu fields children: Option>>>, ns_menus: HashMap>, @@ -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) { + 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::().unwrap(); menuitem.0 .0.borrow_mut() } + MenuItemType::Predefined => { + let menuitem = item + .as_any() + .downcast_ref::() + .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::() - .unwrap(); + MenuItemType::Icon => { + let menuitem = item.as_any().downcast_ref::().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>); + +impl IconMenuItem { + pub fn new( + text: &str, + enabled: bool, + icon: Option, + accelerator: Option, + ) -> 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) { + self.0.borrow_mut().set_icon(icon) + } +} + impl PredfinedMenuItemType { pub(crate) fn selector(&self) -> Option { match self { @@ -725,55 +828,76 @@ impl PredfinedMenuItemType { impl dyn MenuItemExt + '_ { fn get_child(&self) -> Rc> { match self.type_() { - MenuItemType::Submenu => { - let submenu = self.as_any().downcast_ref::().unwrap(); - Rc::clone(&submenu.0 .0) - } - MenuItemType::Normal => { - let menuitem = self.as_any().downcast_ref::().unwrap(); - Rc::clone(&menuitem.0 .0) - } - MenuItemType::Check => { - let menuitem = self - .as_any() - .downcast_ref::() - .unwrap(); - Rc::clone(&menuitem.0 .0) - } - MenuItemType::Predefined => { - let menuitem = self - .as_any() - .downcast_ref::() - .unwrap(); - Rc::clone(&menuitem.0 .0) - } + MenuItemType::Submenu => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .0 + .clone(), + MenuItemType::Normal => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .0 + .clone(), + MenuItemType::Predefined => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .0 + .clone(), + MenuItemType::Check => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .0 + .clone(), + MenuItemType::Icon => self + .as_any() + .downcast_ref::() + .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::().unwrap(); - submenu.0.make_ns_item_for_menu(menu_id) - } - MenuItemType::Normal => { - let menuitem = self.as_any().downcast_ref::().unwrap(); - menuitem.0.make_ns_item_for_menu(menu_id) - } - MenuItemType::Check => { - let menuitem = self - .as_any() - .downcast_ref::() - .unwrap(); - menuitem.0.make_ns_item_for_menu(menu_id) - } - MenuItemType::Predefined => { - let menuitem = self - .as_any() - .downcast_ref::() - .unwrap(); - menuitem.0.make_ns_item_for_menu(menu_id) - } + MenuItemType::Submenu => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .make_ns_item_for_menu(menu_id), + MenuItemType::Normal => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .make_ns_item_for_menu(menu_id), + MenuItemType::Predefined => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .make_ns_item_for_menu(menu_id), + MenuItemType::Check => self + .as_any() + .downcast_ref::() + .unwrap() + .0 + .make_ns_item_for_menu(menu_id), + MenuItemType::Icon => self + .as_any() + .downcast_ref::() + .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]; + } + } +} diff --git a/src/platform_impl/windows/accelerator.rs b/src/platform_impl/windows/accelerator.rs index 41baa61..adc3510 100644 --- a/src/platform_impl/windows/accelerator.rs +++ b/src/platform_impl/windows/accelerator.rs @@ -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, } } diff --git a/src/platform_impl/windows/icon.rs b/src/platform_impl/windows/icon.rs new file mode 100644 index 0000000..153eb63 --- /dev/null +++ b/src/platform_impl/windows/icon.rs @@ -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 { + 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, +} + +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::() 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, width: u32, height: u32) -> Result { + 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>( + path: P, + size: Option<(u32, u32)>, + ) -> Result { + // 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 { + // 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) + } +} diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 7798555..b1f48fd 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -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, + // submenu fields hmenu: HMENU, hpopupmenu: HMENU, @@ -164,6 +172,16 @@ impl MenuChild { }; } } + + fn set_icon(&mut self, icon: Option) { + 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::().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::().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::().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::().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>); + +impl IconMenuItem { + pub fn new( + text: &str, + enabled: bool, + icon: Option, + accelerator: Option, + ) -> 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) { + 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::() as _); } } + +fn create_icon_item_info(hbitmap: HBITMAP) -> MENUITEMINFOW { + let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; + info.cbSize = std::mem::size_of::() as _; + info.fMask = MIIM_BITMAP; + info.hbmpItem = hbitmap; + info +} diff --git a/src/platform_impl/windows/util.rs b/src/platform_impl/windows/util.rs index 9364f90..709fe12 100644 --- a/src/platform_impl/windows/util.rs +++ b/src/platform_impl/windows/util.rs @@ -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 _ } +}