diff --git a/client/ui/assets/netbird-menu-16.png b/client/ui/assets/netbird-menu-16.png new file mode 100644 index 000000000..d5dcab446 Binary files /dev/null and b/client/ui/assets/netbird-menu-16.png differ diff --git a/client/ui/assets/netbird-menu-22.png b/client/ui/assets/netbird-menu-22.png new file mode 100644 index 000000000..87c6b5995 Binary files /dev/null and b/client/ui/assets/netbird-menu-22.png differ diff --git a/client/ui/assets/netbird-menu-24.png b/client/ui/assets/netbird-menu-24.png new file mode 100644 index 000000000..087c1c2ae Binary files /dev/null and b/client/ui/assets/netbird-menu-24.png differ diff --git a/client/ui/assets/netbird-menu-dot-connected-16.png b/client/ui/assets/netbird-menu-dot-connected-16.png new file mode 100644 index 000000000..3a7fa31a4 Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-connected-16.png differ diff --git a/client/ui/assets/netbird-menu-dot-connected-22.png b/client/ui/assets/netbird-menu-dot-connected-22.png new file mode 100644 index 000000000..78b068748 Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-connected-22.png differ diff --git a/client/ui/assets/netbird-menu-dot-connecting-16.png b/client/ui/assets/netbird-menu-dot-connecting-16.png new file mode 100644 index 000000000..f874706b5 Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-connecting-16.png differ diff --git a/client/ui/assets/netbird-menu-dot-connecting-22.png b/client/ui/assets/netbird-menu-dot-connecting-22.png new file mode 100644 index 000000000..d8e5970f5 Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-connecting-22.png differ diff --git a/client/ui/assets/netbird-menu-dot-error-16.png b/client/ui/assets/netbird-menu-dot-error-16.png new file mode 100644 index 000000000..cdc6254da Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-error-16.png differ diff --git a/client/ui/assets/netbird-menu-dot-error-22.png b/client/ui/assets/netbird-menu-dot-error-22.png new file mode 100644 index 000000000..d9bd013d6 Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-error-22.png differ diff --git a/client/ui/assets/netbird-menu-dot-idle-16.png b/client/ui/assets/netbird-menu-dot-idle-16.png new file mode 100644 index 000000000..354b5b860 Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-idle-16.png differ diff --git a/client/ui/assets/netbird-menu-dot-idle-22.png b/client/ui/assets/netbird-menu-dot-idle-22.png new file mode 100644 index 000000000..675cf1ffe Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-idle-22.png differ diff --git a/client/ui/assets/netbird-menu-dot-login-16.png b/client/ui/assets/netbird-menu-dot-login-16.png new file mode 100644 index 000000000..6d65365a2 Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-login-16.png differ diff --git a/client/ui/assets/netbird-menu-dot-login-22.png b/client/ui/assets/netbird-menu-dot-login-22.png new file mode 100644 index 000000000..563de48ed Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-login-22.png differ diff --git a/client/ui/assets/netbird-menu-dot-offline-16.png b/client/ui/assets/netbird-menu-dot-offline-16.png new file mode 100644 index 000000000..f9aa5c3e9 Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-offline-16.png differ diff --git a/client/ui/assets/netbird-menu-dot-offline-22.png b/client/ui/assets/netbird-menu-dot-offline-22.png new file mode 100644 index 000000000..5202c8baa Binary files /dev/null and b/client/ui/assets/netbird-menu-dot-offline-22.png differ diff --git a/client/ui/assets/svg/netbird-menu.svg b/client/ui/assets/svg/netbird-menu.svg new file mode 100644 index 000000000..bd4e9d65d --- /dev/null +++ b/client/ui/assets/svg/netbird-menu.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/client/ui/icons.go b/client/ui/icons.go index 3e8215b73..d5759cee7 100644 --- a/client/ui/icons.go +++ b/client/ui/icons.go @@ -59,24 +59,9 @@ var iconUpdateDisconnectedMacOS []byte //go:embed assets/netbird.png var iconWindow []byte -// Small colored dots shown next to the status menu entry. Rendered as -// regular NSImage/HBITMAP/GTK menu-item icons (not template), so the -// colours stay intact on every platform. - -//go:embed assets/netbird-menu-dot-connected.png -var iconMenuDotConnected []byte - -//go:embed assets/netbird-menu-dot-connecting.png -var iconMenuDotConnecting []byte - -//go:embed assets/netbird-menu-dot-login.png -var iconMenuDotLogin []byte - -//go:embed assets/netbird-menu-dot-error.png -var iconMenuDotError []byte - -//go:embed assets/netbird-menu-dot-idle.png -var iconMenuDotIdle []byte - -//go:embed assets/netbird-menu-dot-offline.png -var iconMenuDotOffline []byte +// Per-platform menu-row icons (status dots + NetBird brand mark) live in +// icons_menu_windows.go and icons_menu_other.go. Windows installs them +// into the Win32 check-mark slot, which expects SM_CXMENUCHECK-sized +// bitmaps (~16x16 at 100% DPI) — anything bigger gets cropped, anything +// smaller leaves blank space — so Windows ships its own 16x16 set +// while macOS/Linux keep the larger 24x24 assets that fit their menus. diff --git a/client/ui/icons_menu_darwin.go b/client/ui/icons_menu_darwin.go new file mode 100644 index 000000000..464e67096 --- /dev/null +++ b/client/ui/icons_menu_darwin.go @@ -0,0 +1,39 @@ +//go:build darwin + +package main + +import _ "embed" + +// 22x22 menu-row icons used on macOS. Apple's HIG recommends an 18–22 px +// glyph for NSMenuItem leading images; 22 sits at the top of that range +// and matches the visual weight of the surrounding row text. Windows +// ships a 16x16 variant (Win32 SM_CXMENUCHECK slot) and Linux a 24x24 +// variant (GTK menu row supports the larger range) — see the sibling +// icons_menu_*.go files. +// +// Regenerate the brand mark from assets/svg/netbird-menu.svg (vector +// source — re-rendering keeps the strokes crisp at every target size): +// inkscape assets/svg/netbird-menu.svg -o netbird-menu-22.png -w 22 -h 22 \ +// --export-background-opacity=0 +// Status dots are downscaled from the 24x24 originals with ImageMagick. + +//go:embed assets/netbird-menu-22.png +var iconMenuNetbird []byte + +//go:embed assets/netbird-menu-dot-connected-22.png +var iconMenuDotConnected []byte + +//go:embed assets/netbird-menu-dot-connecting-22.png +var iconMenuDotConnecting []byte + +//go:embed assets/netbird-menu-dot-login-22.png +var iconMenuDotLogin []byte + +//go:embed assets/netbird-menu-dot-error-22.png +var iconMenuDotError []byte + +//go:embed assets/netbird-menu-dot-idle-22.png +var iconMenuDotIdle []byte + +//go:embed assets/netbird-menu-dot-offline-22.png +var iconMenuDotOffline []byte diff --git a/client/ui/icons_menu_linux.go b/client/ui/icons_menu_linux.go new file mode 100644 index 000000000..8a562c639 --- /dev/null +++ b/client/ui/icons_menu_linux.go @@ -0,0 +1,40 @@ +//go:build linux + +package main + +import _ "embed" + +// 24x24 menu-row icons used on Linux. GTK4 menu rows accept icons in the +// 22–48 px range with no automatic downscaling at this size; 24 reads +// cleanly next to the row text across the GNOME / KDE / minimal-WM +// flavours we ship to. Windows ships a 16x16 variant (Win32 +// SM_CXMENUCHECK slot) and macOS a 22x22 variant — see the sibling +// icons_menu_*.go files. +// +// Regenerate the brand mark from assets/svg/netbird-menu.svg (vector +// source — re-rendering keeps the strokes crisp at every target size): +// inkscape assets/svg/netbird-menu.svg -o netbird-menu-24.png -w 24 -h 24 \ +// --export-background-opacity=0 +// Status dots are the canonical 24x24 originals used everywhere else +// in the legacy Fyne tray. + +//go:embed assets/netbird-menu-24.png +var iconMenuNetbird []byte + +//go:embed assets/netbird-menu-dot-connected.png +var iconMenuDotConnected []byte + +//go:embed assets/netbird-menu-dot-connecting.png +var iconMenuDotConnecting []byte + +//go:embed assets/netbird-menu-dot-login.png +var iconMenuDotLogin []byte + +//go:embed assets/netbird-menu-dot-error.png +var iconMenuDotError []byte + +//go:embed assets/netbird-menu-dot-idle.png +var iconMenuDotIdle []byte + +//go:embed assets/netbird-menu-dot-offline.png +var iconMenuDotOffline []byte diff --git a/client/ui/icons_menu_windows.go b/client/ui/icons_menu_windows.go new file mode 100644 index 000000000..d2b44af60 --- /dev/null +++ b/client/ui/icons_menu_windows.go @@ -0,0 +1,42 @@ +//go:build windows + +package main + +import _ "embed" + +// 16x16 menu-row icons used on Windows. The Win32 SetMenuItemBitmaps API +// paints the HBITMAP into the check-mark slot, sized to SM_CXMENUCHECK / +// SM_CYMENUCHECK (typically 16x16 at 100% DPI). Larger bitmaps overflow +// the row visually, so Windows ships its own scaled set instead of the +// 24x24 assets used on macOS/Linux. Regenerate the brand mark from +// assets/svg/netbird-menu.svg (vector source — re-rendering keeps the +// strokes crisp at every target size): +// inkscape assets/svg/netbird-menu.svg -o netbird-menu-16.png -w 16 -h 16 \ +// --export-background-opacity=0 +// The status dots are downscaled from the 24x24 originals with +// ImageMagick — simple solid-fill circles survive the bicubic resize +// without visible quality loss: +// magick netbird-menu-dot-.png -resize 16x16 \ +// -background none -gravity center -extent 16x16 \ +// netbird-menu-dot--16.png + +//go:embed assets/netbird-menu-16.png +var iconMenuNetbird []byte + +//go:embed assets/netbird-menu-dot-connected-16.png +var iconMenuDotConnected []byte + +//go:embed assets/netbird-menu-dot-connecting-16.png +var iconMenuDotConnecting []byte + +//go:embed assets/netbird-menu-dot-login-16.png +var iconMenuDotLogin []byte + +//go:embed assets/netbird-menu-dot-error-16.png +var iconMenuDotError []byte + +//go:embed assets/netbird-menu-dot-idle-16.png +var iconMenuDotIdle []byte + +//go:embed assets/netbird-menu-dot-offline-16.png +var iconMenuDotOffline []byte diff --git a/client/ui/tray.go b/client/ui/tray.go index eb1a4f227..f28aa4a83 100644 --- a/client/ui/tray.go +++ b/client/ui/tray.go @@ -242,7 +242,7 @@ func (t *Tray) reapplyMenuState() { if t.statusItem != nil && lastStatus != "" { t.statusItem.SetLabel(t.loc.StatusLabel(lastStatus)) - t.statusItem.SetEnabled(false) + t.statusItem.SetEnabled(statusRowEnabled()) t.applyStatusIndicator(lastStatus) } if t.sessionExpiresItem != nil { @@ -314,13 +314,18 @@ func (t *Tray) ShowWindow() { func (t *Tray) buildMenu() *application.Menu { menu := application.NewMenu() - // statusItem shows the daemon's current status. Disabled (and no - // OnClick handler) so clicks are no-ops — the row is informational - // only. The Connect entry below drives every actionable transition, - // including the SSO re-auth flow for NeedsLogin/SessionExpired - // (the daemon's Up RPC returns NeedsSSOLogin when applicable). + // statusItem shows the daemon's current status. Informational row + // with no OnClick handler — clicks are no-ops. Whether the row is + // kept enabled is platform-dependent (see statusRowEnabled): on + // Windows the disabled-state mask would desaturate the coloured + // status dot painted into the check-mark slot, so the row stays + // enabled there; macOS/Linux disable it so the greyed-out label + // signals that it is not clickable. The Connect entry below drives + // every actionable transition, including the SSO re-auth flow for + // NeedsLogin/SessionExpired (the daemon's Up RPC returns + // NeedsSSOLogin when applicable). t.statusItem = menu.Add(t.loc.T("tray.status.disconnected")). - SetEnabled(false). + SetEnabled(statusRowEnabled()). SetBitmap(iconMenuDotIdle) // sessionExpiresItem sits directly below the status row so the @@ -376,7 +381,11 @@ func (t *Tray) buildMenu() *application.Menu { menu.AddSeparator() - about := menu.AddSubmenu(t.loc.T("tray.menu.about")) + aboutLabel := t.loc.T("tray.menu.about") + about := menu.AddSubmenu(aboutLabel) + if aboutItem := menu.FindByLabel(aboutLabel); aboutItem != nil { + aboutItem.SetBitmap(iconMenuNetbird) + } about.Add(t.loc.T("tray.menu.github")).OnClick(func(*application.Context) { _ = t.app.Browser.OpenURL(urlGitHubRepo) }) @@ -621,11 +630,14 @@ func (t *Tray) applyStatus(st services.Status) { daemonUnavailable := strings.EqualFold(st.Status, services.StatusDaemonUnavailable) connecting := strings.EqualFold(st.Status, services.StatusConnecting) if t.statusItem != nil { - // Label-only: kept disabled (informational row). Swap the - // displayed text so the user sees a familiar phrase instead - // of the raw daemon enum. + // Label-only: row is informational (no OnClick). Enablement + // is platform-dependent via statusRowEnabled — Windows + // keeps it enabled so the Win32 disabled-state mask does + // not desaturate the coloured dot; macOS/Linux disable it. + // Swap the displayed text so the user sees a familiar + // phrase instead of the raw daemon enum. t.statusItem.SetLabel(t.loc.StatusLabel(st.Status)) - t.statusItem.SetEnabled(false) + t.statusItem.SetEnabled(statusRowEnabled()) t.applyStatusIndicator(st.Status) } if t.upItem != nil { diff --git a/client/ui/tray_status_enabled_other.go b/client/ui/tray_status_enabled_other.go new file mode 100644 index 000000000..1e4c8a13c --- /dev/null +++ b/client/ui/tray_status_enabled_other.go @@ -0,0 +1,12 @@ +//go:build !windows && !android && !ios && !freebsd && !js + +package main + +// statusRowEnabled reports whether the informational status row at the +// top of the tray menu should stay enabled. False on macOS and Linux: +// both platforms paint disabled menu rows at slightly reduced opacity +// without desaturating the leading bitmap, so the coloured status dot +// stays visible while the greyed-out label still signals to the user +// that the row is informational and not clickable. Windows opts in via +// the sibling tray_status_enabled_windows.go file. +func statusRowEnabled() bool { return false } diff --git a/client/ui/tray_status_enabled_windows.go b/client/ui/tray_status_enabled_windows.go new file mode 100644 index 000000000..cdfec41a6 --- /dev/null +++ b/client/ui/tray_status_enabled_windows.go @@ -0,0 +1,13 @@ +//go:build windows + +package main + +// statusRowEnabled reports whether the informational status row at the +// top of the tray menu should stay enabled. Always true on Windows: +// the Win32 disabled-state mask desaturates both the row text and the +// HBITMAP painted into the check-mark slot, so a disabled row would +// render the coloured status dot in greyscale and defeat the indicator. +// macOS/Linux disable the row (see tray_status_enabled_other.go) because +// neither platform applies that desaturation and the visual cue that +// the row is informational reads better. +func statusRowEnabled() bool { return true }