diff --git a/v3/examples/menu/README.md b/v3/examples/menu/README.md index 627bfdb9..cc926df7 100644 --- a/v3/examples/menu/README.md +++ b/v3/examples/menu/README.md @@ -20,4 +20,8 @@ go run . # Known Issues -- [Resize cursor still visible when not resizable](https://github.com/orgs/wailsapp/projects/6/views/1?pane=issue&itemId=40962163) \ No newline at end of file +- [Resize cursor still visible when not resizable](https://github.com/orgs/wailsapp/projects/6/views/1?pane=issue&itemId=40962163) + +--- + +Icon attribution: [Click icons created by kusumapotter - Flaticon](https://www.flaticon.com/free-icons/click) \ No newline at end of file diff --git a/v3/examples/menu/icon.png b/v3/examples/menu/icon.png new file mode 100644 index 00000000..e934687c Binary files /dev/null and b/v3/examples/menu/icon.png differ diff --git a/v3/examples/menu/main.go b/v3/examples/menu/main.go index 38a218fe..815b3fe6 100644 --- a/v3/examples/menu/main.go +++ b/v3/examples/menu/main.go @@ -8,6 +8,9 @@ import ( "github.com/wailsapp/wails/v3/pkg/application" ) +//go:embed icon.png +var clickBitmap []byte + func main() { app := application.New(application.Options{ @@ -32,7 +35,7 @@ func main() { myMenu.Add("Not Enabled").SetEnabled(false) // Click callbacks - myMenu.Add("Click Me!").OnClick(func(ctx *application.Context) { + myMenu.Add("Click Me!").SetBitmap(clickBitmap).OnClick(func(ctx *application.Context) { switch ctx.ClickedMenuItem().Label() { case "Click Me!": ctx.ClickedMenuItem().SetLabel("Thanks mate!") diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index 2a89ddde..5483597c 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -415,6 +415,13 @@ func menuItemSetLabel(widget pointer, label string) { C.free(unsafe.Pointer(value)) } +func menuItemSetBitmap(widget pointer, data []byte) { + image := C.gdk_pixbuf_new_from_inline(C.int(len(data)), (*C.guchar)(unsafe.Pointer(&data[0])), C.FALSE, nil) + imageWidget := C.gtk_image_new_from_pixbuf(image) + C.gtk_image_set_pixel_size((*C.GtkImage)(imageWidget), 16) + C.gtk_menu_item_set_image((*C.GtkMenuItem)(widget), imageWidget) +} + func menuItemSetToolTip(widget pointer, tooltip string) { value := C.CString(tooltip) C.gtk_widget_set_tooltip_text( diff --git a/v3/pkg/application/menu_darwin.go b/v3/pkg/application/menu_darwin.go index 250172f2..0bea925b 100644 --- a/v3/pkg/application/menu_darwin.go +++ b/v3/pkg/application/menu_darwin.go @@ -100,6 +100,9 @@ func (m *macosMenu) processMenu(parent unsafe.Pointer, menu *Menu) { case separator: C.addMenuSeparator(parent) } + if item.bitmap != nil { + C.setMenuItemBitmap(item.impl, (*C.uchar)(&item.bitmap[0]), C.int(len(item.bitmap))) + } } } diff --git a/v3/pkg/application/menu_windows.go b/v3/pkg/application/menu_windows.go index a7278d72..fbb58caf 100644 --- a/v3/pkg/application/menu_windows.go +++ b/v3/pkg/application/menu_windows.go @@ -78,6 +78,9 @@ func (w *windowsMenu) processMenu(parentMenu w32.HMENU, inputMenu *Menu) { var menuText = w32.MustStringToUTF16Ptr(item.Label()) w32.AppendMenu(parentMenu, flags, uintptr(itemID), menuText) + if item.bitmap != nil { + w32.SetMenuIcons(parentMenu, itemID, item.bitmap, nil) + } } } diff --git a/v3/pkg/application/menuitem.go b/v3/pkg/application/menuitem.go index 762d2ca6..b16619b5 100644 --- a/v3/pkg/application/menuitem.go +++ b/v3/pkg/application/menuitem.go @@ -40,6 +40,7 @@ type menuItemImpl interface { setChecked(checked bool) setAccelerator(accelerator *accelerator) setHidden(hidden bool) + setBitmap(bitmap []byte) } type MenuItem struct { @@ -49,6 +50,7 @@ type MenuItem struct { disabled bool checked bool hidden bool + bitmap []byte submenu *Menu callback func(*Context) itemType menuItemType @@ -259,6 +261,14 @@ func (m *MenuItem) SetEnabled(enabled bool) *MenuItem { return m } +func (m *MenuItem) SetBitmap(bitmap []byte) *MenuItem { + m.bitmap = bitmap + if m.impl != nil { + m.impl.setBitmap(bitmap) + } + return m +} + func (m *MenuItem) SetChecked(checked bool) *MenuItem { m.checked = checked if m.impl != nil { diff --git a/v3/pkg/application/menuitem_darwin.go b/v3/pkg/application/menuitem_darwin.go index e8303e13..f8e8fb0f 100644 --- a/v3/pkg/application/menuitem_darwin.go +++ b/v3/pkg/application/menuitem_darwin.go @@ -332,6 +332,11 @@ static void showAll(void) { [[NSApplication sharedApplication] unhideAllApplications:nil]; } +static void setMenuItemBitmap(void* nsMenuItem, unsigned char *bitmap, int length) { + MenuItem *menuItem = (MenuItem *)nsMenuItem; + NSImage *image = [[NSImage alloc] initWithData:[NSData dataWithBytes:bitmap length:length]]; + [menuItem setImage:image]; +} */ import "C" import ( @@ -339,33 +344,37 @@ import ( "unsafe" ) -type windowsMenuItem struct { +type macosMenuItem struct { menuItem *MenuItem nsMenuItem unsafe.Pointer } -func (m windowsMenuItem) setTooltip(tooltip string) { +func (m macosMenuItem) setTooltip(tooltip string) { C.setMenuItemTooltip(m.nsMenuItem, C.CString(tooltip)) } -func (m windowsMenuItem) setLabel(s string) { +func (m macosMenuItem) setLabel(s string) { C.setMenuItemLabel(m.nsMenuItem, C.CString(s)) } -func (m windowsMenuItem) setDisabled(disabled bool) { +func (m macosMenuItem) setDisabled(disabled bool) { C.setMenuItemDisabled(m.nsMenuItem, C.bool(disabled)) } -func (m windowsMenuItem) setChecked(checked bool) { +func (m macosMenuItem) setChecked(checked bool) { C.setMenuItemChecked(m.nsMenuItem, C.bool(checked)) } -func (m windowsMenuItem) setHidden(hidden bool) { +func (m macosMenuItem) setHidden(hidden bool) { C.setMenuItemHidden(m.nsMenuItem, C.bool(hidden)) } -func (m windowsMenuItem) setAccelerator(accelerator *accelerator) { +func (m macosMenuItem) setBitmap(bitmap []byte) { + C.setMenuItemBitmap(m.nsMenuItem, (*C.uchar)(&bitmap[0]), C.int(len(bitmap))) +} + +func (m macosMenuItem) setAccelerator(accelerator *accelerator) { // Set the keyboard shortcut of the menu item var modifier C.int var key *C.char @@ -378,8 +387,8 @@ func (m windowsMenuItem) setAccelerator(accelerator *accelerator) { C.setMenuItemKeyEquivalent(m.nsMenuItem, key, modifier) } -func newMenuItemImpl(item *MenuItem) *windowsMenuItem { - result := &windowsMenuItem{ +func newMenuItemImpl(item *MenuItem) *macosMenuItem { + result := &macosMenuItem{ menuItem: item, } diff --git a/v3/pkg/application/menuitem_linux.go b/v3/pkg/application/menuitem_linux.go index cce9414b..fda03483 100644 --- a/v3/pkg/application/menuitem_linux.go +++ b/v3/pkg/application/menuitem_linux.go @@ -26,6 +26,13 @@ func (l linuxMenuItem) blockSignal() { menuItemSignalBlock(l.native, l.handlerId, true) } } +func (l linuxMenuItem) setBitmap(data []byte) { + InvokeSync(func() { + l.blockSignal() + defer l.unBlockSignal() + menuItemSetBitmap(l.native, data) + }) +} func (l linuxMenuItem) unBlockSignal() { if l.handlerId != 0 { diff --git a/v3/pkg/application/menuitem_windows.go b/v3/pkg/application/menuitem_windows.go index 64cdf280..abea7a17 100644 --- a/v3/pkg/application/menuitem_windows.go +++ b/v3/pkg/application/menuitem_windows.go @@ -105,6 +105,20 @@ func (m *windowsMenuItem) setAccelerator(accelerator *accelerator) { //C.setMenuItemKeyEquivalent(m.nsMenuItem, key, modifier) } +func (m *windowsMenuItem) setBitmap(bitmap []byte) { + if m.menuItem.bitmap == nil { + return + } + + // Set the icon + err := w32.SetMenuIcons(m.hMenu, m.id, bitmap, nil) + if err != nil { + globalApplication.error("Unable to set bitmap on menu item", "error", err.Error()) + return + } + m.update() +} + func newMenuItemImpl(item *MenuItem, parentMenu w32.HMENU, ID int) *windowsMenuItem { result := &windowsMenuItem{ menuItem: item, diff --git a/v3/pkg/application/popupmenu_windows.go b/v3/pkg/application/popupmenu_windows.go index c386f8c8..0f5aaa6b 100644 --- a/v3/pkg/application/popupmenu_windows.go +++ b/v3/pkg/application/popupmenu_windows.go @@ -109,6 +109,12 @@ func (p *Win32Menu) buildMenu(parentMenu w32.HMENU, inputMenu *Menu) { if !ok { w32.Fatal(fmt.Sprintf("Error adding menu item: %s", menuText)) } + if item.bitmap != nil { + err := w32.SetMenuIcons(parentMenu, itemID, item.bitmap, nil) + if err != nil { + w32.Fatal(fmt.Sprintf("Error setting menu icons: %s", err.Error())) + } + } item.impl = menuItemImpl } diff --git a/v3/pkg/w32/icon.go b/v3/pkg/w32/icon.go index 610dd3ca..00947932 100644 --- a/v3/pkg/w32/icon.go +++ b/v3/pkg/w32/icon.go @@ -3,7 +3,11 @@ package w32 import ( + "bytes" "fmt" + "image" + "image/draw" + "image/png" "unsafe" ) @@ -89,3 +93,45 @@ func CreateLargeHIconFromImage(fileData []byte) (HICON, error) { func SetWindowIcon(hwnd HWND, icon HICON) { SendMessage(hwnd, WM_SETICON, ICON_SMALL, uintptr(icon)) } + +func pngToImage(data []byte) (*image.RGBA, error) { + img, err := png.Decode(bytes.NewReader(data)) + if err != nil { + return nil, err + } + + bounds := img.Bounds() + rgba := image.NewRGBA(bounds) + draw.Draw(rgba, bounds, img, bounds.Min, draw.Src) + return rgba, nil +} + +func SetMenuIcons(parentMenu HMENU, itemID int, unchecked []byte, checked []byte) error { + if unchecked == nil { + return fmt.Errorf("invalid unchecked bitmap") + } + var err error + var uncheckedIcon, checkedIcon HBITMAP + var uncheckedImage, checkedImage *image.RGBA + uncheckedImage, err = pngToImage(unchecked) + if err != nil { + return err + } + uncheckedIcon, err = CreateHBITMAPFromImage(uncheckedImage) + if err != nil { + return err + } + if checked != nil { + checkedImage, err = pngToImage(checked) + if err != nil { + return err + } + checkedIcon, err = CreateHBITMAPFromImage(checkedImage) + if err != nil { + return err + } + } else { + checkedIcon = uncheckedIcon + } + return SetMenuItemBitmaps(parentMenu, uint32(itemID), MF_BYCOMMAND, checkedIcon, uncheckedIcon) +} diff --git a/v3/pkg/w32/user32.go b/v3/pkg/w32/user32.go index 560717c0..50834d8b 100644 --- a/v3/pkg/w32/user32.go +++ b/v3/pkg/w32/user32.go @@ -9,6 +9,7 @@ package w32 import ( "fmt" + "golang.org/x/sys/windows" "runtime" "syscall" "unsafe" @@ -169,6 +170,8 @@ var ( procFlashWindowEx = moduser32.NewProc("FlashWindowEx") + procSetMenuItemBitmaps = moduser32.NewProc("SetMenuItemBitmaps") + mainThread HANDLE ) @@ -190,6 +193,20 @@ func RegisterClassEx(wndClassEx *WNDCLASSEX) ATOM { return ATOM(ret) } +func SetMenuItemBitmaps(hMenu HMENU, uPosition, uFlags uint32, hBitmapUnchecked HBITMAP, hBitmapChecked HBITMAP) error { + ret, _, _ := procSetMenuItemBitmaps.Call( + hMenu, + uintptr(uPosition), + uintptr(uFlags), + hBitmapUnchecked, + hBitmapChecked) + + if ret == 0 { + return windows.GetLastError() + } + return nil +} + func GetDesktopWindow() HWND { ret, _, _ := procGetDesktopWindow.Call() return ret