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() }