Initial scaffold: project structure, .gitignore, go.mod
This commit is contained in:
114
internal/icons/render.go
Normal file
114
internal/icons/render.go
Normal file
@@ -0,0 +1,114 @@
|
||||
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()
|
||||
}
|
||||
30
internal/icons/states.go
Normal file
30
internal/icons/states.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package icons
|
||||
|
||||
import "image/color"
|
||||
|
||||
// State represents the current sync state for icon rendering.
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateIdle State = iota // Green — all folders idle
|
||||
StateSyncing // Blue — syncing/scanning
|
||||
StatePaused // Gray — all paused
|
||||
StateError // Red — folder error
|
||||
StateDisconnected // Dark gray — cannot reach Syncthing
|
||||
)
|
||||
|
||||
// colors maps each state to its icon color.
|
||||
var colors = map[State]color.RGBA{
|
||||
StateIdle: {76, 175, 80, 255}, // green
|
||||
StateSyncing: {33, 150, 243, 255}, // blue
|
||||
StatePaused: {158, 158, 158, 255}, // gray
|
||||
StateError: {244, 67, 54, 255}, // red
|
||||
StateDisconnected: {97, 97, 97, 255}, // dark gray
|
||||
}
|
||||
|
||||
func colorForState(s State) color.RGBA {
|
||||
if c, ok := colors[s]; ok {
|
||||
return c
|
||||
}
|
||||
return colors[StateDisconnected]
|
||||
}
|
||||
Reference in New Issue
Block a user