115 lines
3.0 KiB
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()
|
|
}
|