All checks were successful
Release / build (push) Successful in 2m34s
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>
109 lines
2.7 KiB
Go
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()
|
|
}
|