Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e8adcda
feat(macos): add icon setting for menu items
s00d Feb 26, 2025
872ad25
feat(submenu): add set_icon method to change menu item icons
s00d Feb 26, 2025
365e66f
chore(example): add submenu with icons to the window menu
s00d Feb 26, 2025
876f042
feat(macos): add support for native icons in menu items
s00d Feb 26, 2025
b1adc81
feat(tui): simplify import statements and update submenu icon handling
s00d Feb 26, 2025
959aa7b
chore(example): add support for native icons in submenu items
s00d Feb 26, 2025
2bc706a
fix(submenu): format import statements for better readability
s00d Feb 26, 2025
e074079
feat(windows): add support for submenu icons in the menu system
s00d Feb 27, 2025
72feadf
fix(macos): update icon handling for menu items
s00d Feb 27, 2025
5a7b57b
refactor(src/main): improve code formatting and readability
s00d Feb 27, 2025
1ea4b13
feat(target): add `gtk` dependency for Linux support in Cargo.toml
s00d Feb 27, 2025
3241cae
feat(target): add `gtk` dependency for Linux support in Cargo.toml
s00d Feb 27, 2025
90c8d44
feat(linux): add GTK prelude import for Linux platform support
s00d Feb 27, 2025
9444fbe
feat(gtk): enhance menu item creation with customizable layout
s00d Feb 27, 2025
cd523de
Merge branch 'dev' into dev
s00d Mar 3, 2025
607e660
feat(items): add new methods for creating submenus with icons
s00d Jun 14, 2025
bddb6c3
feat(submenu): add support for icons in submenu builder
s00d Jun 14, 2025
a7fdc4b
refactor(gtk): simplify context menu item addition and signal connect…
s00d Jun 14, 2025
2f04746
feat(linux): implement context menu for GTK window
s00d Jun 14, 2025
a706719
feat(platform_impl): add support for icons in menu items
s00d Jun 14, 2025
72241a9
fix(windows-common-controls): remove temporary window comment
s00d Jun 14, 2025
df82aff
fix(windows): correct method call for internal_id retrieval
s00d Jun 14, 2025
1e106c1
refactor(windows): simplify menu item info setting
s00d Jun 14, 2025
60dc41d
refactor(gtk): streamline menu item handling by reducing block scope
s00d Jun 14, 2025
0d60839
Merge branch 'dev' into dev
s00d Jun 14, 2025
4e50fe5
refactor(platform_impl): streamline GTK menu connection closures
s00d Jun 16, 2025
9926a23
fix(gtk): simplify menu item handling in context menu
s00d Jun 16, 2025
ed7fe4d
style(gtk): format long lines in `mod.rs` for better readability
s00d Jun 16, 2025
0dbf3b2
refactor(gtk): remove unnecessary line breaks in menu item creation
s00d Jun 16, 2025
1d3dfa2
feat(platform_impl): update menu item info parameter from FALSE to 0
s00d Jun 16, 2025
08f2bc5
revert common controls example changes
amrbashir Jun 17, 2025
819a08b
revert 0i32 back to FALSE, revert WM_NCPAINT redundant block, remove …
amrbashir Jun 17, 2025
8630249
change file, revert constructor items on Submenu, builder is enough
amrbashir Jun 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changes/submenu-set-icon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"muda": "patch"
---

Add support for icons on `Submenu` so we added:

- `Submenu::set_icon`
- `Submenu::set_native_icon`
- `SubmenuBuilder::icon`
- `SubmenuBuilder::native_icon`
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 11 additions & 9 deletions examples/windows-common-controls-v6/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use muda::{
PredefinedMenuItem, Submenu,
};
#[cfg(target_os = "macos")]
use tao::platform::macos::{EventLoopBuilderExtMacOS, WindowExtMacOS};
use tao::platform::macos::WindowExtMacOS;
#[cfg(target_os = "windows")]
use tao::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows};
use tao::{
Expand All @@ -36,8 +36,6 @@ fn main() {
}
});
}
#[cfg(target_os = "macos")]
event_loop_builder.with_default_menu(false);

let event_loop = event_loop_builder.build();

Expand Down Expand Up @@ -79,8 +77,6 @@ fn main() {
Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyC)),
);

let path = concat!(env!("CARGO_MANIFEST_DIR"), "../../icon.png");
let icon = load_icon(std::path::Path::new(path));
let image_item = IconMenuItem::new("Image Custom 1", true, Some(icon), None);

let check_custom_i_1 = CheckMenuItem::new("Check Custom 1", true, true, None);
Expand Down Expand Up @@ -128,13 +124,13 @@ fn main() {
edit_m.append_items(&[&copy_i, &PredefinedMenuItem::separator(), &paste_i]);

#[cfg(target_os = "windows")]
{
unsafe {
use tao::rwh_06::*;
if let RawWindowHandle::Win32(handle) = window.window_handle().unwrap().as_raw() {
menu_bar.init_for_hwnd(handle.hwnd.get());
menu_bar.init_for_hwnd(handle.hwnd.get()).unwrap();
}
if let RawWindowHandle::Win32(handle) = window2.window_handle().unwrap().as_raw() {
menu_bar.init_for_hwnd(handle.hwnd.get());
menu_bar.init_for_hwnd(handle.hwnd.get()).unwrap();
}
}
#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -206,16 +202,22 @@ fn show_context_menu(window: &Window, menu: &dyn ContextMenu, position: Option<P
{
use tao::rwh_06::*;
if let RawWindowHandle::Win32(handle) = window.window_handle().unwrap().as_raw() {
menu.show_context_menu_for_hwnd(handle.hwnd.get(), position);
unsafe { menu.show_context_menu_for_hwnd(handle.hwnd.get(), position) };
}
}

#[cfg(target_os = "macos")]
{
use tao::rwh_06::*;
if let RawWindowHandle::AppKit(handle) = window.window_handle().unwrap().as_raw() {
unsafe { menu.show_context_menu_for_nsview(handle.ns_view.as_ptr() as _, position) };
}
}

#[cfg(target_os = "linux")]
{
menu.show_context_menu_for_gtk(window.gtk_window().as_ref(), position);
}
}

fn load_icon(path: &std::path::Path) -> muda::Icon {
Expand Down
32 changes: 28 additions & 4 deletions src/builders/submenu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use crate::{IsMenuItem, MenuId, Submenu};
use crate::{Icon, IsMenuItem, MenuId, NativeIcon, Submenu};

/// A builder type for [`Submenu`]
#[derive(Clone, Default)]
Expand All @@ -11,6 +11,8 @@ pub struct SubmenuBuilder<'a> {
enabled: bool,
id: Option<MenuId>,
items: Vec<&'a dyn IsMenuItem>,
icon: Option<Icon>,
native_icon: Option<NativeIcon>,
}

impl std::fmt::Debug for SubmenuBuilder<'_> {
Expand Down Expand Up @@ -59,12 +61,34 @@ impl<'a> SubmenuBuilder<'a> {
self
}

/// Set an icon for this submenu.
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}

/// Set a native icon for this submenu.
pub fn native_icon(mut self, icon: NativeIcon) -> Self {
self.native_icon = Some(icon);
self
}

/// Build this menu item.
pub fn build(self) -> crate::Result<Submenu> {
if let Some(id) = self.id {
Submenu::with_id_and_items(id, self.text, self.enabled, &self.items)
let submenu = if let Some(id) = self.id {
Submenu::with_id_and_items(id, self.text, self.enabled, &self.items)?
} else {
Submenu::with_items(self.text, self.enabled, &self.items)
Submenu::with_items(self.text, self.enabled, &self.items)?
};

if let Some(icon) = self.icon {
submenu.set_icon(Some(icon));
}

if let Some(native_icon) = self.native_icon {
submenu.set_native_icon(Some(native_icon));
}

Ok(submenu)
}
}
19 changes: 17 additions & 2 deletions src/items/submenu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
use std::{cell::RefCell, mem, rc::Rc};

use crate::{
dpi::Position, sealed::IsMenuItemBase, util::AddOp, ContextMenu, IsMenuItem, MenuId,
MenuItemKind,
dpi::Position, sealed::IsMenuItemBase, util::AddOp, ContextMenu, Icon, IsMenuItem, MenuId,
MenuItemKind, NativeIcon,
};

/// A menu that can be added to a [`Menu`] or another [`Submenu`].
Expand Down Expand Up @@ -208,6 +208,21 @@ impl Submenu {
self.id().clone()
}
}

/// Change this menu item icon or remove it.
pub fn set_icon(&self, icon: Option<Icon>) {
self.inner.borrow_mut().set_icon(icon)
}

/// Change this menu item icon to a native image or remove it.
///
/// ## Platform-specific:
///
/// - **Windows / Linux**: Unsupported.
pub fn set_native_icon(&self, _icon: Option<NativeIcon>) {
#[cfg(target_os = "macos")]
self.inner.borrow_mut().set_native_icon(_icon)
}
}

impl ContextMenu for Submenu {
Expand Down
83 changes: 52 additions & 31 deletions src/platform_impl/gtk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,14 @@ impl Menu {
gtk_item.show();
}

{
if let (menu_id, Some(menu)) = &self.gtk_menu {
let gtk_item =
item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?;
match op {
AddOp::Append => menu.append(&gtk_item),
AddOp::Insert(position) => menu.insert(&gtk_item, position as i32),
}
gtk_item.show();
if let (menu_id, Some(menu)) = &self.gtk_menu {
let gtk_item =
item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?;
match op {
AddOp::Append => menu.append(&gtk_item),
AddOp::Insert(position) => menu.insert(&gtk_item, position as i32),
}
gtk_item.show();
}
}

Expand Down Expand Up @@ -144,8 +142,7 @@ impl Menu {
fn add_menu_item_to_context_menu(&self, item: &dyn crate::IsMenuItem) -> crate::Result<()> {
return_if_item_not_supported!(item);

let (menu_id, menu) = &self.gtk_menu;
if let Some(menu) = menu {
if let (menu_id, Some(menu)) = &self.gtk_menu {
let gtk_item =
item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?;
menu.append(&gtk_item);
Expand Down Expand Up @@ -793,16 +790,14 @@ impl MenuChild {
}
}

{
if let (menu_id, Some(menu)) = self.gtk_menu.as_ref().unwrap() {
let gtk_item =
item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?;
match op {
AddOp::Append => menu.append(&gtk_item),
AddOp::Insert(position) => menu.insert(&gtk_item, position as i32),
}
gtk_item.show();
if let Some((menu_id, Some(menu))) = self.gtk_menu.as_ref() {
let gtk_item =
item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?;
match op {
AddOp::Append => menu.append(&gtk_item),
AddOp::Insert(position) => menu.insert(&gtk_item, position as i32),
}
gtk_item.show();
}
}

Expand Down Expand Up @@ -836,8 +831,7 @@ impl MenuChild {
fn add_menu_item_to_context_menu(&self, item: &dyn crate::IsMenuItem) -> crate::Result<()> {
return_if_item_not_supported!(item);

let (menu_id, menu) = self.gtk_menu.as_ref().unwrap();
if let Some(menu) = menu {
if let Some((menu_id, Some(menu))) = self.gtk_menu.as_ref() {
let gtk_item =
item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?;
menu.append(&gtk_item);
Expand Down Expand Up @@ -1005,17 +999,45 @@ impl MenuChild {
menu_id: u32,
accel_group: Option<&gtk::AccelGroup>,
add_to_cache: bool,
for_menu_bar: bool,
) -> crate::Result<gtk::MenuItem> {
let submenu = gtk::Menu::new();
let item = gtk::MenuItem::builder()

let image = self
.icon
.as_ref()
.map(|icon| gtk::Image::from_pixbuf(Some(&icon.inner.to_pixbuf_scale(16, 16))))
.unwrap_or_default();

let label = gtk::AccelLabel::builder()
.label(to_gtk_mnemonic(&self.text))
.use_underline(true)
.submenu(&submenu)
.xalign(0.0)
.build();

let box_container = gtk::Box::new(gtk::Orientation::Horizontal, 6);
if !for_menu_bar {
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();

item.show();
item.set_submenu(Some(&submenu));
item.show();

self.accel_group = accel_group.cloned();

Expand All @@ -1036,12 +1058,12 @@ impl MenuChild {
.push((id, submenu.clone()));
}

for item in self.items() {
for child_item in self.items() {
if add_to_cache {
self.add_menu_item_with_id(item.as_ref(), id)?;
self.add_menu_item_with_id(child_item.as_ref(), id)?;
} else {
let gtk_item = item.make_gtk_menu_item(0, None, false, false)?;
submenu.append(&gtk_item);
let gtk_child_item = child_item.make_gtk_menu_item(0, None, false, false)?;
submenu.append(&gtk_child_item);
}
}

Expand Down Expand Up @@ -1326,7 +1348,7 @@ impl MenuItemKind {
let mut child = self.child_mut();
match child.item_type() {
MenuItemType::Submenu => {
child.create_gtk_item_for_submenu(menu_id, accel_group, add_to_cache)
child.create_gtk_item_for_submenu(menu_id, accel_group, add_to_cache, for_menu_bar)
}
MenuItemType::MenuItem => {
child.create_gtk_item_for_menu_item(menu_id, accel_group, add_to_cache)
Expand Down Expand Up @@ -1415,7 +1437,6 @@ fn show_context_menu(
let tx_clone = tx.clone();
let id = gtk_menu.connect_cancel(move |_| tx_clone.send(false).unwrap_or(()));
let id2 = gtk_menu.connect_selection_done(move |_| tx.send(true).unwrap_or(()));

gtk_menu.popup_at_rect(
&window,
&gdk::Rectangle::new(pos.0, pos.1, 0, 0),
Expand Down
8 changes: 8 additions & 0 deletions src/platform_impl/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,14 @@ impl MenuChild {
ns_submenu.setAutoenablesItems(false);

ns_menu_item.setEnabled(self.enabled);

if let Some(native_icon) = self.native_icon {
menuitem_set_native_icon(&ns_menu_item, Some(native_icon));
}

if let Some(icon) = self.icon.as_ref() {
menuitem_set_icon(&ns_menu_item, Some(icon));
}
}

let id = COUNTER.next();
Expand Down
12 changes: 6 additions & 6 deletions src/platform_impl/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,9 @@ impl Menu {

{
let child_ = child.borrow();
let item_type = child_.item_type();

if child_.item_type() == MenuItemType::Icon {
if matches!(item_type, MenuItemType::Icon | MenuItemType::Submenu) {
let hbitmap = child_
.icon
.as_ref()
Expand Down Expand Up @@ -873,18 +874,18 @@ impl MenuChild {

{
let child_ = child.borrow();
let item_type = child_.item_type();

if child_.item_type() == MenuItemType::Icon {
if matches!(item_type, MenuItemType::Icon | MenuItemType::Submenu) {
let hbitmap = child_
.icon
.as_ref()
.map(|i| unsafe { i.inner.to_hbitmap() })
.unwrap_or(std::ptr::null_mut());
let info = create_icon_item_info(hbitmap);

unsafe {
SetMenuItemInfoW(self.hmenu, child_.internal_id, FALSE, &info);
SetMenuItemInfoW(self.hpopupmenu, child_.internal_id, FALSE, &info);
SetMenuItemInfoW(self.hmenu, child_.internal_id(), FALSE, &info);
SetMenuItemInfoW(self.hpopupmenu, child_.internal_id(), FALSE, &info);
};
}
}
Expand Down Expand Up @@ -1170,7 +1171,6 @@ unsafe extern "system" fn menu_subclass_proc(

res
}

_ => DefSubclassProc(hwnd as _, msg, wparam, lparam),
}
}
Expand Down
Loading