per-platform tray menu icons and Windows-specific status row
The Windows menu renderer paints leading bitmaps into the Win32
check-mark slot (SetMenuItemBitmaps), which differs from how Cocoa
and GTK handle NSMenuItem.image / menu-row icons:
- SM_CXMENUCHECK sizing: Windows expects ~16x16 at 100% DPI in the
check-mark slot and visually overflows the row for anything bigger.
- Disabled-state mask: Windows desaturates both the row text and the
bitmap when MFS_DISABLED is set, so a disabled informational row
renders the coloured status dot in greyscale.
Per the platform icon guidelines:
Platform | Size | Notes
---------|----------------|-----------------------------------------
Windows | 16x16 | check-mark slot, status row stays enabled
macOS | 22x22 (18-22) | NSMenuItem leading image, HIG
Linux | 24x24 (22-48) | GTK4 menu-row icon channel
Changes:
* Split the menu-row icon embeds into icons_menu_{windows,darwin,linux}.go
so each platform pulls its own size; the brand mark is rendered from
assets/svg/netbird-menu.svg (new vector source) at 16/22/24 px with
Inkscape, and the Windows status dots ship as 8x8 content centred on
a 16x16 transparent canvas (the renderer upscales the bitmap, so the
padding keeps the dot visually proportional to the row text).
* Introduce statusRowEnabled() in tray_status_enabled_{windows,other}.go:
true on Windows so the disabled-state mask does not strip the dot's
colour; false on macOS/Linux where disabled menu rows fade the label
without desaturating the leading bitmap, signalling that the row is
informational.
* Add an icon to the About submenu using the same brand mark.
|
After Width: | Height: | Size: 526 B |
|
After Width: | Height: | Size: 687 B |
|
After Width: | Height: | Size: 739 B |
|
After Width: | Height: | Size: 508 B |
|
After Width: | Height: | Size: 615 B |
|
After Width: | Height: | Size: 520 B |
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 532 B |
|
After Width: | Height: | Size: 629 B |
|
After Width: | Height: | Size: 490 B |
|
After Width: | Height: | Size: 602 B |
|
After Width: | Height: | Size: 537 B |
|
After Width: | Height: | Size: 641 B |
|
After Width: | Height: | Size: 512 B |
|
After Width: | Height: | Size: 605 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(0.5 4.5)">
|
||||
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
|
||||
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
|
||||
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 556 B |
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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-<state>.png -resize 16x16 \
|
||||
// -background none -gravity center -extent 16x16 \
|
||||
// netbird-menu-dot-<state>-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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||