wails/v3/pkg/application/systemtray_windows.go
Lea Anthony c4b614cb10
fix: use structured logging for debug/info methods (#4767)
* fix: use structured logging for debug/info methods

The debug() and info() methods were using fmt.Sprintf() which expects
printf-style format directives, but callers were using slog-style
key-value pairs. Changed to pass args directly to Logger.Debug/Info
which properly handles structured logging.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add changelog entries for build fixes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: remove temporary debug print statements from mobile merge

Remove emoji debug logs (🔴, 🟢, 🟠, 🔵, 🔥) that were accidentally left in
from the iOS/Android mobile platform support merge. These were development
debugging statements that should not have been included in the final code.

Files cleaned:
- application.go
- application_debug.go
- init_android.go
- init_ios.go
- mainthread_android.go
- mainthread_ios.go
- webview_window.go
- webview_window_ios.go

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add changelog entries for debug cleanup and breaking change

- Add breaking change note: production builds are now default
- Add entry for debug print statement removal

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: convert remaining printf-style debug calls to slog-style

Convert three debug() calls that were still using printf-style format
strings to slog-style structured logging (key-value pairs):

- systemtray_windows.go: ShellNotifyIcon show/hide failures
- application_darwin.go: window lookup failure

This addresses CodeRabbit review feedback and ensures consistency
with the refactored debug() method.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: convert all printf-style info() calls to slog-style

Convert remaining info() calls that were using printf-style format
strings to slog-style structured logging (key-value pairs):

- application_ios.go: iOS log messages and HandleJSMessage calls
- webview_window_windows.go: WM_SYSKEYDOWN logging
- application.go: handleWindowMessage and handleWebViewRequest logging

Also removed debug fmt.Printf statements from handleWebViewRequest.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add build tag to main.m to prevent Go from compiling it on non-iOS platforms

Go's toolchain tries to process .m (Objective-C) files when they're in a
directory with Go files. Adding a //go:build ios tag tells Go to only
process this file when building for iOS, matching how darwin .m files
handle this (e.g., //go:build darwin && !ios).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: remove orphaned wails-mimetype-migration submodule reference

The iOS merge added a submodule reference without a corresponding
.gitmodules file, causing Cloudflare and other CI systems to fail
with "No url found for submodule path" errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: auto-disable DMA-BUF renderer on Wayland with NVIDIA to prevent crashes

WebKitGTK has a known issue with the DMA-BUF renderer on NVIDIA proprietary
drivers running Wayland, causing "Error 71 (Protocol error)" crashes.

This fix automatically detects NVIDIA GPUs (via /sys/module/nvidia) and sets
WEBKIT_DISABLE_DMABUF_RENDERER=1 when running on Wayland.

Also removes leftover debug print statements from mobile platform merge.

See: https://bugs.webkit.org/show_bug.cgi?id=262607

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add NVIDIA driver info to wails3 doctor on Linux

Shows NVIDIA driver version and srcversion in doctor output to help
diagnose Wayland/NVIDIA compatibility issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-12 09:03:11 +11:00

636 lines
15 KiB
Go

//go:build windows
package application
import (
"errors"
"fmt"
"syscall"
"unsafe"
"github.com/wailsapp/wails/v3/pkg/icons"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/w32"
)
const (
wmUserSystray = w32.WM_USER + 1
)
type windowsSystemTray struct {
parent *SystemTray
menu *Win32Menu
cancelTheme func()
uid uint32
hwnd w32.HWND
lightModeIcon w32.HICON
lightModeIconOwned bool
darkModeIcon w32.HICON
darkModeIconOwned bool
currentIcon w32.HICON
currentIconOwned bool
}
// releaseIcon destroys an icon handle only when we own it and no new handle reuses it.
// Shared handles (e.g. from LoadIcon/LoadIconWithResourceID) must not be passed to DestroyIcon per https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-destroyicon.
func (s *windowsSystemTray) releaseIcon(handle w32.HICON, owned bool, keep ...w32.HICON) {
if !owned || handle == 0 {
return
}
for _, k := range keep {
if handle == k {
return
}
}
w32.DestroyIcon(handle)
}
func (s *windowsSystemTray) openMenu() {
if s.menu == nil {
return
}
// Get the system tray bounds
trayBounds, err := s.bounds()
if err != nil {
return
}
if trayBounds == nil {
return
}
// Show the menu at the tray bounds
s.menu.ShowAt(trayBounds.X, trayBounds.Y)
}
func (s *windowsSystemTray) positionWindow(window Window, offset int) error {
// Get the current screen trayBounds
currentScreen, err := s.getScreen()
if err != nil {
return err
}
screenBounds := currentScreen.WorkArea
windowBounds := window.Bounds()
newX := screenBounds.Width - windowBounds.Width - offset
newY := screenBounds.Height - windowBounds.Height - offset
// systray icons in windows can either be in the taskbar
// or in a flyout menu.
var iconIsInTrayBounds bool
iconIsInTrayBounds, err = s.iconIsInTrayBounds()
if err != nil {
return err
}
var trayBounds *Rect
var centerAlignX, centerAlignY int
// we only need the traybounds if the icon is in the tray
if iconIsInTrayBounds {
trayBounds, err = s.bounds()
if err != nil {
return err
}
if trayBounds == nil {
return errors.New("failed to get system tray bounds")
}
*trayBounds = PhysicalToDipRect(*trayBounds)
centerAlignX = trayBounds.X + (trayBounds.Width / 2) - (windowBounds.Width / 2)
centerAlignY = trayBounds.Y + (trayBounds.Height / 2) - (windowBounds.Height / 2)
}
taskbarBounds := w32.GetTaskbarPosition()
if taskbarBounds == nil {
return errors.New("failed to get taskbar position")
}
// Set the window position based on the icon location
// if the icon is in the taskbar (traybounds) then we need
// to adjust the position so the window is centered on the icon
switch taskbarBounds.UEdge {
case w32.ABE_LEFT:
if iconIsInTrayBounds && centerAlignY <= newY {
newY = centerAlignY
}
newX = screenBounds.X + offset
case w32.ABE_TOP:
if iconIsInTrayBounds && centerAlignX <= newX {
newX = centerAlignX
}
newY = screenBounds.Y + offset
case w32.ABE_RIGHT:
if iconIsInTrayBounds && centerAlignY <= newY {
newY = centerAlignY
}
case w32.ABE_BOTTOM:
if iconIsInTrayBounds && centerAlignX <= newX {
newX = centerAlignX
}
}
newPos := currentScreen.relativeToAbsoluteDipPoint(Point{X: newX, Y: newY})
windowBounds.X = newPos.X
windowBounds.Y = newPos.Y
window.SetBounds(windowBounds)
return nil
}
func (s *windowsSystemTray) bounds() (*Rect, error) {
if s.hwnd == 0 {
return nil, errors.New("system tray window handle not initialized")
}
bounds, err := w32.GetSystrayBounds(s.hwnd, s.uid)
if err != nil {
return nil, err
}
if bounds == nil {
return nil, errors.New("GetSystrayBounds returned nil")
}
monitor := w32.MonitorFromWindow(s.hwnd, w32.MONITOR_DEFAULTTONEAREST)
if monitor == 0 {
return nil, errors.New("failed to get monitor")
}
return &Rect{
X: int(bounds.Left),
Y: int(bounds.Top),
Width: int(bounds.Right - bounds.Left),
Height: int(bounds.Bottom - bounds.Top),
}, nil
}
func (s *windowsSystemTray) iconIsInTrayBounds() (bool, error) {
if s.hwnd == 0 {
return false, errors.New("system tray window handle not initialized")
}
bounds, err := w32.GetSystrayBounds(s.hwnd, s.uid)
if err != nil {
return false, err
}
if bounds == nil {
return false, errors.New("GetSystrayBounds returned nil")
}
taskbarRect := w32.GetTaskbarPosition()
if taskbarRect == nil {
return false, errors.New("failed to get taskbar position")
}
inTasksBar := w32.RectInRect(bounds, &taskbarRect.Rc)
if inTasksBar {
return true, nil
}
return false, nil
}
func (s *windowsSystemTray) getScreen() (*Screen, error) {
if s.hwnd == 0 {
return nil, errors.New("system tray window handle not initialized")
}
// Get the screen for this systray
return getScreenForWindowHwnd(s.hwnd)
}
func (s *windowsSystemTray) setMenu(menu *Menu) {
s.updateMenu(menu)
}
func (s *windowsSystemTray) run() {
s.hwnd = w32.CreateWindowEx(
0,
w32.MustStringToUTF16Ptr(globalApplication.options.Windows.WndClass),
nil,
0,
0,
0,
0,
0,
w32.HWND_MESSAGE,
0,
0,
nil)
if s.hwnd == 0 {
globalApplication.fatal("failed to create tray window: %s", syscall.GetLastError())
return
}
s.uid = uint32(s.parent.id)
// Resolve the base icons once so we can reuse them for light/dark modes
defaultIcon := w32.LoadIconWithResourceID(w32.GetModuleHandle(""), w32.RT_ICON)
// Priority: custom icon > default app icon > built-in icon
if s.parent.icon != nil {
icon, err := w32.CreateSmallHIconFromImage(s.parent.icon)
if err == nil {
s.lightModeIcon = icon
s.lightModeIconOwned = true
} else {
globalApplication.warning("failed to create systray icon: %v", err)
}
}
if s.lightModeIcon == 0 && defaultIcon != 0 {
s.lightModeIcon = defaultIcon
s.lightModeIconOwned = false
}
if s.lightModeIcon == 0 {
icon, err := w32.CreateSmallHIconFromImage(icons.SystrayLight)
if err != nil {
globalApplication.warning("failed to create systray icon: %v", err)
s.lightModeIcon = 0
s.lightModeIconOwned = false
} else {
s.lightModeIcon = icon
s.lightModeIconOwned = true
}
}
if s.parent.darkModeIcon != nil {
icon, err := w32.CreateSmallHIconFromImage(s.parent.darkModeIcon)
if err == nil {
s.darkModeIcon = icon
s.darkModeIconOwned = true
} else {
globalApplication.warning("failed to create systray dark mode icon: %v", err)
}
}
if s.darkModeIcon == 0 && s.parent.icon != nil && s.lightModeIcon != 0 {
s.darkModeIcon = s.lightModeIcon
s.darkModeIconOwned = false
}
if s.darkModeIcon == 0 && defaultIcon != 0 {
s.darkModeIcon = defaultIcon
s.darkModeIconOwned = false
}
if s.darkModeIcon == 0 {
icon, err := w32.CreateSmallHIconFromImage(icons.SystrayDark)
if err != nil {
globalApplication.warning("failed to create systray dark mode icon: %v", err)
s.darkModeIcon = 0
s.darkModeIconOwned = false
} else {
s.darkModeIcon = icon
s.darkModeIconOwned = true
}
}
if _, err := s.show(); err != nil {
// Initial systray add can fail when the shell is not available. This is handled downstream via TaskbarCreated message.
globalApplication.warning("initial systray add failed: %v", err)
}
if s.parent.menu != nil {
s.updateMenu(s.parent.menu)
}
// Set Default Callbacks
if s.parent.clickHandler == nil {
s.parent.clickHandler = func() {
globalApplication.debug("Left Button Clicked")
}
}
if s.parent.rightClickHandler == nil {
s.parent.rightClickHandler = func() {
if s.menu != nil {
s.openMenu()
}
}
}
// Listen for dark mode changes
s.cancelTheme = globalApplication.Event.OnApplicationEvent(events.Windows.SystemThemeChanged, func(event *ApplicationEvent) {
s.updateIcon()
})
// Register the system tray
getNativeApplication().registerSystemTray(s)
}
func (s *windowsSystemTray) updateIcon() {
var newIcon w32.HICON
if w32.IsCurrentlyDarkMode() {
newIcon = s.darkModeIcon
} else {
newIcon = s.lightModeIcon
}
if s.currentIcon == newIcon {
return
}
// Store the old icon to destroy it after updating
oldIcon := s.currentIcon
oldIconOwned := s.currentIconOwned
s.currentIcon = newIcon
nid := s.newNotifyIconData()
nid.UFlags = w32.NIF_ICON
if s.currentIcon != 0 {
nid.HIcon = s.currentIcon
}
if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
panic(syscall.GetLastError())
}
// Track ownership of the current icon so we know if we can destroy it later
currentOwned := false
if newIcon != 0 {
if newIcon == s.lightModeIcon && s.lightModeIconOwned {
currentOwned = true
} else if newIcon == s.darkModeIcon && s.darkModeIconOwned {
currentOwned = true
}
}
s.currentIconOwned = currentOwned
// Destroy the old icon handle if it exists, we owned it, and nothing else references it
s.releaseIcon(oldIcon, oldIconOwned, s.lightModeIcon, s.darkModeIcon)
}
func (s *windowsSystemTray) newNotifyIconData() w32.NOTIFYICONDATA {
nid := w32.NOTIFYICONDATA{
UID: s.uid,
HWnd: s.hwnd,
}
nid.CbSize = uint32(unsafe.Sizeof(nid))
return nid
}
func (s *windowsSystemTray) setIcon(icon []byte) {
newIcon, err := w32.CreateSmallHIconFromImage(icon)
if err != nil {
globalApplication.error("failed to create systray light mode icon: %v", err)
return
}
oldLight := s.lightModeIcon
oldLightOwned := s.lightModeIconOwned
oldDark := s.darkModeIcon
oldDarkOwned := s.darkModeIconOwned
s.lightModeIcon = newIcon
s.lightModeIconOwned = true
// Keep dark mode in sync when both modes shared the same handle (or dark was unset).
if s.darkModeIcon == 0 || s.darkModeIcon == oldLight {
s.darkModeIcon = newIcon
s.darkModeIconOwned = false
}
// Only free previous handles we own that are no longer referenced.
s.releaseIcon(oldLight, oldLightOwned, s.lightModeIcon, s.darkModeIcon)
if oldDark != s.darkModeIcon {
s.releaseIcon(oldDark, oldDarkOwned, s.lightModeIcon, s.darkModeIcon)
}
s.updateIcon()
}
func (s *windowsSystemTray) setDarkModeIcon(icon []byte) {
newIcon, err := w32.CreateSmallHIconFromImage(icon)
if err != nil {
globalApplication.error("failed to create systray dark mode icon: %v", err)
return
}
oldDark := s.darkModeIcon
oldDarkOwned := s.darkModeIconOwned
oldLight := s.lightModeIcon
oldLightOwned := s.lightModeIconOwned
s.darkModeIcon = newIcon
s.darkModeIconOwned = true
lightReplaced := false
// Keep light mode in sync when both modes shared the same handle (or light was unset).
if s.lightModeIcon == 0 || s.lightModeIcon == oldDark {
s.lightModeIcon = newIcon
s.lightModeIconOwned = false
lightReplaced = true
}
// Only free the previous handle if nothing else keeps a reference to it.
s.releaseIcon(oldDark, oldDarkOwned, s.lightModeIcon, s.darkModeIcon)
if lightReplaced {
s.releaseIcon(oldLight, oldLightOwned, s.lightModeIcon, s.darkModeIcon)
}
s.updateIcon()
}
func newSystemTrayImpl(parent *SystemTray) systemTrayImpl {
return &windowsSystemTray{
parent: parent,
}
}
func (s *windowsSystemTray) wndProc(msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case wmUserSystray:
msg := lParam & 0xffff
switch msg {
case w32.WM_LBUTTONUP:
if s.parent.clickHandler != nil {
s.parent.clickHandler()
}
case w32.WM_RBUTTONUP:
if s.parent.rightClickHandler != nil {
s.parent.rightClickHandler()
}
case w32.WM_LBUTTONDBLCLK:
if s.parent.doubleClickHandler != nil {
s.parent.doubleClickHandler()
}
case w32.WM_RBUTTONDBLCLK:
if s.parent.rightDoubleClickHandler != nil {
s.parent.rightDoubleClickHandler()
}
case w32.NIN_POPUPOPEN:
if s.parent.mouseEnterHandler != nil {
s.parent.mouseEnterHandler()
}
case w32.NIN_POPUPCLOSE:
if s.parent.mouseLeaveHandler != nil {
s.parent.mouseLeaveHandler()
}
}
// println(w32.WMMessageToString(msg))
// Menu processing
case w32.WM_COMMAND:
cmdMsgID := int(wParam & 0xffff)
if s.menu != nil {
s.menu.ProcessCommand(cmdMsgID)
}
default:
// msg := int(wParam & 0xffff)
// println(w32.WMMessageToString(uintptr(msg)))
}
return w32.DefWindowProc(s.hwnd, msg, wParam, lParam)
}
func (s *windowsSystemTray) updateMenu(menu *Menu) {
s.menu = NewPopupMenu(s.hwnd, menu)
s.menu.onMenuOpen = s.parent.onMenuOpen
s.menu.onMenuClose = s.parent.onMenuClose
s.menu.Update()
}
func (s *windowsSystemTray) setTooltip(tooltip string) {
// Create a new NOTIFYICONDATA structure
nid := s.newNotifyIconData()
nid.UFlags = w32.NIF_TIP | w32.NIF_SHOWTIP
// Ensure the tooltip length is within the limit (128 characters including null terminate characters for szTip for Windows 2000 and later)
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataw
tooltipUTF16, err := w32.StringToUTF16(truncateUTF16(tooltip, 127))
if err != nil {
return
}
copy(nid.SzTip[:], tooltipUTF16)
// Modify the tray icon with the new tooltip
if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
return
}
}
// ---- Unsupported ----
func (s *windowsSystemTray) setLabel(label string) {}
func (s *windowsSystemTray) setTemplateIcon(_ []byte) {
// Unsupported - do nothing
}
func (s *windowsSystemTray) setIconPosition(position IconPosition) {
// Unsupported - do nothing
}
func (s *windowsSystemTray) destroy() {
if s.cancelTheme != nil {
s.cancelTheme()
s.cancelTheme = nil
}
// Remove and delete the system tray
getNativeApplication().unregisterSystemTray(s)
if s.menu != nil {
s.menu.Destroy()
}
// destroy the notification icon
nid := s.newNotifyIconData()
if !w32.ShellNotifyIcon(w32.NIM_DELETE, &nid) {
globalApplication.debug(syscall.GetLastError().Error())
}
// Clean up icon handles
lightIcon := s.lightModeIcon
darkIcon := s.darkModeIcon
currentIcon := s.currentIcon
s.releaseIcon(lightIcon, s.lightModeIconOwned)
s.releaseIcon(darkIcon, s.darkModeIconOwned, lightIcon)
s.releaseIcon(currentIcon, s.currentIconOwned, lightIcon, darkIcon)
s.lightModeIcon = 0
s.lightModeIconOwned = false
s.darkModeIcon = 0
s.darkModeIconOwned = false
s.currentIcon = 0
s.currentIconOwned = false
w32.DestroyWindow(s.hwnd)
s.hwnd = 0
}
func (s *windowsSystemTray) Show() {
if s.hwnd == 0 {
return
}
nid := s.newNotifyIconData()
nid.UFlags = w32.NIF_STATE
nid.DwStateMask = w32.NIS_HIDDEN
nid.DwState = 0
if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
globalApplication.debug("ShellNotifyIcon NIM_MODIFY show failed", "error", syscall.GetLastError())
}
}
func (s *windowsSystemTray) Hide() {
if s.hwnd == 0 {
return
}
nid := s.newNotifyIconData()
nid.UFlags = w32.NIF_STATE
nid.DwStateMask = w32.NIS_HIDDEN
nid.DwState = w32.NIS_HIDDEN
if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
globalApplication.debug("ShellNotifyIcon NIM_MODIFY hide failed", "error", syscall.GetLastError())
}
}
func (s *windowsSystemTray) show() (w32.NOTIFYICONDATA, error) {
nid := s.newNotifyIconData()
nid.UFlags = w32.NIF_ICON | w32.NIF_MESSAGE
nid.HIcon = s.currentIcon
nid.UCallbackMessage = wmUserSystray
if !w32.ShellNotifyIcon(w32.NIM_ADD, &nid) {
err := syscall.GetLastError()
return nid, fmt.Errorf("ShellNotifyIcon NIM_ADD failed: %w", err)
}
nid.UVersion = w32.NOTIFYICON_VERSION_4
if !w32.ShellNotifyIcon(w32.NIM_SETVERSION, &nid) {
err := syscall.GetLastError()
return nid, fmt.Errorf("ShellNotifyIcon NIM_SETVERSION failed: %w", err)
}
s.updateIcon()
if s.parent.tooltip != "" {
s.setTooltip(s.parent.tooltip)
}
return nid, nil
}
func truncateUTF16(s string, maxUnits int) string {
var units int
for i, r := range s {
var u int
// check if rune will take 2 UTF-16 units
if r > 0xFFFF {
u = 2
} else {
u = 1
}
if units+u > maxUnits {
return s[:i]
}
units += u
}
return s
}