mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
221 lines
3.7 KiB
Go
221 lines
3.7 KiB
Go
5 months ago
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
|
||
|
//go:build cgo || !darwin
|
||
|
|
||
|
package main
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"image/color"
|
||
|
"image/png"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"fyne.io/systray"
|
||
|
"github.com/fogleman/gg"
|
||
|
)
|
||
|
|
||
|
// tsLogo represents the state of the 3x3 dot grid in the Tailscale logo.
|
||
|
// A 0 represents a gray dot, any other value is a white dot.
|
||
|
type tsLogo [9]byte
|
||
|
|
||
|
var (
|
||
|
// disconnected is all gray dots
|
||
|
disconnected = tsLogo{
|
||
|
0, 0, 0,
|
||
|
0, 0, 0,
|
||
|
0, 0, 0,
|
||
|
}
|
||
|
|
||
|
// connected is the normal Tailscale logo
|
||
|
connected = tsLogo{
|
||
|
0, 0, 0,
|
||
|
1, 1, 1,
|
||
|
0, 1, 0,
|
||
|
}
|
||
|
|
||
|
// loading is a special tsLogo value that is not meant to be rendered directly,
|
||
|
// but indicates that the loading animation should be shown.
|
||
|
loading = tsLogo{'l', 'o', 'a', 'd', 'i', 'n', 'g'}
|
||
|
|
||
|
// loadingIcons are shown in sequence as an animated loading icon.
|
||
|
loadingLogos = []tsLogo{
|
||
|
{
|
||
|
0, 1, 1,
|
||
|
1, 0, 1,
|
||
|
0, 0, 1,
|
||
|
},
|
||
|
{
|
||
|
0, 1, 1,
|
||
|
0, 0, 1,
|
||
|
0, 1, 0,
|
||
|
},
|
||
|
{
|
||
|
0, 1, 1,
|
||
|
0, 0, 0,
|
||
|
0, 0, 1,
|
||
|
},
|
||
|
{
|
||
|
0, 0, 1,
|
||
|
0, 1, 0,
|
||
|
0, 0, 0,
|
||
|
},
|
||
|
{
|
||
|
0, 1, 0,
|
||
|
0, 0, 0,
|
||
|
0, 0, 0,
|
||
|
},
|
||
|
{
|
||
|
0, 0, 0,
|
||
|
0, 0, 1,
|
||
|
0, 0, 0,
|
||
|
},
|
||
|
{
|
||
|
0, 0, 0,
|
||
|
0, 0, 0,
|
||
|
0, 0, 0,
|
||
|
},
|
||
|
{
|
||
|
0, 0, 1,
|
||
|
0, 0, 0,
|
||
|
0, 0, 0,
|
||
|
},
|
||
|
{
|
||
|
0, 0, 0,
|
||
|
0, 0, 0,
|
||
|
1, 0, 0,
|
||
|
},
|
||
|
{
|
||
|
0, 0, 0,
|
||
|
0, 0, 0,
|
||
|
1, 1, 0,
|
||
|
},
|
||
|
{
|
||
|
0, 0, 0,
|
||
|
1, 0, 0,
|
||
|
1, 1, 0,
|
||
|
},
|
||
|
{
|
||
|
0, 0, 0,
|
||
|
1, 1, 0,
|
||
|
0, 1, 0,
|
||
|
},
|
||
|
{
|
||
|
0, 0, 0,
|
||
|
1, 1, 0,
|
||
|
0, 1, 1,
|
||
|
},
|
||
|
{
|
||
|
0, 0, 0,
|
||
|
1, 1, 1,
|
||
|
0, 0, 1,
|
||
|
},
|
||
|
{
|
||
|
0, 1, 0,
|
||
|
0, 1, 1,
|
||
|
1, 0, 1,
|
||
|
},
|
||
|
}
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
black = color.NRGBA{0, 0, 0, 255}
|
||
|
white = color.NRGBA{255, 255, 255, 255}
|
||
|
gray = color.NRGBA{255, 255, 255, 102}
|
||
|
)
|
||
|
|
||
|
// render returns a PNG image of the logo.
|
||
|
func (logo tsLogo) render() *bytes.Buffer {
|
||
|
const radius = 25
|
||
|
const borderUnits = 1
|
||
|
dim := radius * (8 + borderUnits*2)
|
||
|
|
||
|
dc := gg.NewContext(dim, dim)
|
||
|
dc.DrawRectangle(0, 0, float64(dim), float64(dim))
|
||
|
dc.SetColor(black)
|
||
|
dc.Fill()
|
||
|
|
||
|
for y := 0; y < 3; y++ {
|
||
|
for x := 0; x < 3; x++ {
|
||
|
px := (borderUnits + 1 + 3*x) * radius
|
||
|
py := (borderUnits + 1 + 3*y) * radius
|
||
|
col := white
|
||
|
if logo[y*3+x] == 0 {
|
||
|
col = gray
|
||
|
}
|
||
|
dc.DrawCircle(float64(px), float64(py), radius)
|
||
|
dc.SetColor(col)
|
||
|
dc.Fill()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
b := bytes.NewBuffer(nil)
|
||
|
png.Encode(b, dc.Image())
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
// setAppIcon renders logo and sets it as the systray icon.
|
||
|
func setAppIcon(icon tsLogo) {
|
||
|
if icon == loading {
|
||
|
startLoadingAnimation()
|
||
|
} else {
|
||
|
stopLoadingAnimation()
|
||
|
systray.SetIcon(icon.render().Bytes())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
loadingMu sync.Mutex // protects loadingCancel
|
||
|
|
||
|
// loadingCancel stops the loading animation in the systray icon.
|
||
|
// This is nil if the animation is not currently active.
|
||
|
loadingCancel func()
|
||
|
)
|
||
|
|
||
|
// startLoadingAnimation starts the animated loading icon in the system tray.
|
||
|
// The animation continues until [stopLoadingAnimation] is called.
|
||
|
// If the loading animation is already active, this func does nothing.
|
||
|
func startLoadingAnimation() {
|
||
|
loadingMu.Lock()
|
||
|
defer loadingMu.Unlock()
|
||
|
|
||
|
if loadingCancel != nil {
|
||
|
// loading icon already displayed
|
||
|
return
|
||
|
}
|
||
|
|
||
|
ctx := context.Background()
|
||
|
ctx, loadingCancel = context.WithCancel(ctx)
|
||
|
|
||
|
go func() {
|
||
|
t := time.NewTicker(500 * time.Millisecond)
|
||
|
var i int
|
||
|
for {
|
||
|
select {
|
||
|
case <-ctx.Done():
|
||
|
return
|
||
|
case <-t.C:
|
||
|
systray.SetIcon(loadingLogos[i].render().Bytes())
|
||
|
i++
|
||
|
if i >= len(loadingLogos) {
|
||
|
i = 0
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}()
|
||
|
}
|
||
|
|
||
|
// stopLoadingAnimation stops the animated loading icon in the system tray.
|
||
|
// If the loading animation is not currently active, this func does nothing.
|
||
|
func stopLoadingAnimation() {
|
||
|
loadingMu.Lock()
|
||
|
defer loadingMu.Unlock()
|
||
|
|
||
|
if loadingCancel != nil {
|
||
|
loadingCancel()
|
||
|
loadingCancel = nil
|
||
|
}
|
||
|
}
|