Files
syncwarden/internal/icons/render.go
Axel Meyer 110bb715ff
All checks were successful
Release / build (push) Successful in 2m34s
Redesign icon: wood texture shield with darken-blended Syncthing motif
Replace procedural icon renderer with pre-rendered textured icons:
- Use Stammtisch wood texture as shield face (crop tabletop planks)
- Apply Syncthing motif via darken blend ("warpaint on wood" effect)
- Fix Syncthing logo geometry: asymmetric node placement (76°/116°/168°
  gaps) with offset hub, matching the official SVG
- Metal rim with rivets and directional lighting
- Embed pre-rendered PNGs via go:embed (no runtime rendering)
- Icon generator in cmd/icongen/ takes wood texture as input

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:01:51 +01:00

109 lines
2.7 KiB
Go

package icons
import (
"bytes"
_ "embed"
"encoding/binary"
"image/png"
"runtime"
)
// Pre-rendered tray icons (64px) generated from Stammtisch wood texture
// with the Syncthing motif applied via darken blend.
// Regenerate with: go run ./cmd/icongen/ -wood /path/to/stammtisch.png
//go:embed tray_idle.png
var trayIdlePNG []byte
//go:embed tray_syncing.png
var traySyncingPNG []byte
//go:embed tray_paused.png
var trayPausedPNG []byte
//go:embed tray_error.png
var trayErrorPNG []byte
//go:embed tray_disconnected.png
var trayDisconnectedPNG []byte
// Render returns the tray icon for the given state.
// Returns ICO bytes on Windows, PNG bytes on other platforms.
func Render(state State) ([]byte, error) {
pngData := trayPNG(state)
if runtime.GOOS == "windows" {
return wrapPNGInICO(pngData, 64, 64), nil
}
return pngData, nil
}
func trayPNG(state State) []byte {
switch state {
case StateIdle:
return trayIdlePNG
case StateSyncing:
return traySyncingPNG
case StatePaused:
return trayPausedPNG
case StateError:
return trayErrorPNG
case StateDisconnected:
return trayDisconnectedPNG
default:
return trayDisconnectedPNG
}
}
// RenderPNG returns a PNG icon at 64px for the given state.
func RenderPNG(state State, size int) ([]byte, error) {
// Pre-rendered icons are only 64px; return them directly.
// For other sizes, the static assets in assets/ should be used.
return trayPNG(state), nil
}
// IconSize returns the size of the pre-rendered tray icons.
func IconSize() (int, int) {
data := trayIdlePNG
img, err := png.Decode(bytes.NewReader(data))
if err != nil {
return 64, 64
}
b := img.Bounds()
return b.Dx(), b.Dy()
}
// wrapPNGInICO wraps raw PNG bytes in a minimal ICO container.
func wrapPNGInICO(pngData []byte, width, height int) []byte {
const headerSize = 6
const entrySize = 16
imageOffset := headerSize + entrySize
buf := new(bytes.Buffer)
// ICONDIR header
binary.Write(buf, binary.LittleEndian, uint16(0)) // Reserved
binary.Write(buf, binary.LittleEndian, uint16(1)) // Type: 1 = icon
binary.Write(buf, binary.LittleEndian, uint16(1)) // Count: 1 image
// ICONDIRENTRY
w := byte(width)
if width >= 256 {
w = 0
}
h := byte(height)
if height >= 256 {
h = 0
}
buf.WriteByte(w)
buf.WriteByte(h)
buf.WriteByte(0) // ColorCount
buf.WriteByte(0) // Reserved
binary.Write(buf, binary.LittleEndian, uint16(1)) // Planes
binary.Write(buf, binary.LittleEndian, uint16(32)) // BitCount
binary.Write(buf, binary.LittleEndian, uint32(len(pngData))) // BytesInRes
binary.Write(buf, binary.LittleEndian, uint32(imageOffset)) // ImageOffset
buf.Write(pngData)
return buf.Bytes()
}