Files
syncwarden/internal/icons/render.go

115 lines
3.0 KiB
Go

package icons
import (
"bytes"
"encoding/binary"
"image/png"
"math"
"runtime"
"github.com/fogleman/gg"
)
const iconSize = 64
// Render generates a tray icon for the given state.
// Returns ICO bytes on Windows, PNG bytes on other platforms.
func Render(state State) ([]byte, error) {
dc := gg.NewContext(iconSize, iconSize)
drawSyncIcon(dc, state)
var buf bytes.Buffer
if err := png.Encode(&buf, dc.Image()); err != nil {
return nil, err
}
pngData := buf.Bytes()
if runtime.GOOS == "windows" {
return wrapPNGInICO(pngData, iconSize, iconSize), nil
}
return pngData, nil
}
// drawSyncIcon draws a circular sync arrows icon in the state's color.
func drawSyncIcon(dc *gg.Context, state State) {
c := colorForState(state)
cx := float64(iconSize) / 2
cy := float64(iconSize) / 2
radius := float64(iconSize)*0.35
arrowWidth := 5.0
dc.SetColor(c)
dc.SetLineWidth(arrowWidth)
dc.SetLineCap(gg.LineCapRound)
// Draw two circular arcs (sync arrows)
// Top arc: from 220° to 320°
drawArcWithArrow(dc, cx, cy, radius, degToRad(220), degToRad(320), arrowWidth)
// Bottom arc: from 40° to 140°
drawArcWithArrow(dc, cx, cy, radius, degToRad(40), degToRad(140), arrowWidth)
}
func drawArcWithArrow(dc *gg.Context, cx, cy, radius, startAngle, endAngle, lineWidth float64) {
// Draw the arc
dc.DrawArc(cx, cy, radius, startAngle, endAngle)
dc.Stroke()
// Draw arrowhead at the end of the arc
endX := cx + radius*math.Cos(endAngle)
endY := cy + radius*math.Sin(endAngle)
// Arrow direction is tangent to the circle at the endpoint
tangentAngle := endAngle + math.Pi/2
arrowLen := lineWidth * 2.5
// Two points of the arrowhead
a1x := endX + arrowLen*math.Cos(tangentAngle+0.5)
a1y := endY + arrowLen*math.Sin(tangentAngle+0.5)
a2x := endX + arrowLen*math.Cos(tangentAngle-0.5)
a2y := endY + arrowLen*math.Sin(tangentAngle-0.5)
dc.MoveTo(a1x, a1y)
dc.LineTo(endX, endY)
dc.LineTo(a2x, a2y)
dc.Stroke()
}
func degToRad(deg float64) float64 {
return deg * math.Pi / 180
}
// 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()
}