@ -8,6 +8,7 @@ package main
import (
import (
"bytes"
"bytes"
"context"
"context"
"image"
"image/color"
"image/color"
"image/png"
"image/png"
"sync"
"sync"
@ -17,113 +18,190 @@ import (
"github.com/fogleman/gg"
"github.com/fogleman/gg"
)
)
// tsLogo represents the state of the 3x3 dot grid in the Tailscale logo.
// tsLogo represents the Tailscale logo displayed as the systray icon.
// A 0 represents a gray dot, any other value is a white dot.
type tsLogo struct {
type tsLogo [ 9 ] byte
// dots represents the state of the 3x3 dot grid in the logo.
// A 0 represents a gray dot, any other value is a white dot.
dots [ 9 ] byte
// dotMask returns an image mask to be used when rendering the logo dots.
dotMask func ( dc * gg . Context , borderUnits int , radius int ) * image . Alpha
// overlay is called after the dots are rendered to draw an additional overlay.
overlay func ( dc * gg . Context , borderUnits int , radius int )
}
var (
var (
// disconnected is all gray dots
// disconnected is all gray dots
disconnected = tsLogo {
disconnected = tsLogo { dots : [ 9 ] byte {
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
}
} }
// connected is the normal Tailscale logo
// connected is the normal Tailscale logo
connected = tsLogo {
connected = tsLogo { dots : [ 9 ] byte {
0 , 0 , 0 ,
0 , 0 , 0 ,
1 , 1 , 1 ,
1 , 1 , 1 ,
0 , 1 , 0 ,
0 , 1 , 0 ,
}
} }
// loading is a special tsLogo value that is not meant to be rendered directly,
// loading is a special tsLogo value that is not meant to be rendered directly,
// but indicates that the loading animation should be shown.
// but indicates that the loading animation should be shown.
loading = tsLogo { 'l' , 'o' , 'a' , 'd' , 'i' , 'n' , 'g' }
loading = tsLogo { dots : [ 9 ] byte { 'l' , 'o' , 'a' , 'd' , 'i' , 'n' , 'g' } }
// loadingIcons are shown in sequence as an animated loading icon.
// loadingIcons are shown in sequence as an animated loading icon.
loadingLogos = [ ] tsLogo {
loadingLogos = [ ] tsLogo {
{
{ dots : [ 9 ] byte {
0 , 1 , 1 ,
0 , 1 , 1 ,
1 , 0 , 1 ,
1 , 0 , 1 ,
0 , 0 , 1 ,
0 , 0 , 1 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 1 , 1 ,
0 , 1 , 1 ,
0 , 0 , 1 ,
0 , 0 , 1 ,
0 , 1 , 0 ,
0 , 1 , 0 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 1 , 1 ,
0 , 1 , 1 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 1 ,
0 , 0 , 1 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 0 , 1 ,
0 , 0 , 1 ,
0 , 1 , 0 ,
0 , 1 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 1 , 0 ,
0 , 1 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 1 ,
0 , 0 , 1 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 0 , 1 ,
0 , 0 , 1 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
1 , 0 , 0 ,
1 , 0 , 0 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
0 , 0 , 0 ,
1 , 1 , 0 ,
1 , 1 , 0 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 0 , 0 ,
0 , 0 , 0 ,
1 , 0 , 0 ,
1 , 0 , 0 ,
1 , 1 , 0 ,
1 , 1 , 0 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 0 , 0 ,
0 , 0 , 0 ,
1 , 1 , 0 ,
1 , 1 , 0 ,
0 , 1 , 0 ,
0 , 1 , 0 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 0 , 0 ,
0 , 0 , 0 ,
1 , 1 , 0 ,
1 , 1 , 0 ,
0 , 1 , 1 ,
0 , 1 , 1 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 0 , 0 ,
0 , 0 , 0 ,
1 , 1 , 1 ,
1 , 1 , 1 ,
0 , 0 , 1 ,
0 , 0 , 1 ,
} ,
} } ,
{
{ dots : [ 9 ] byte {
0 , 1 , 0 ,
0 , 1 , 0 ,
0 , 1 , 1 ,
0 , 1 , 1 ,
1 , 0 , 1 ,
1 , 0 , 1 ,
} } ,
}
// exitNodeOnline is the Tailscale logo with an additional arrow overlay in the corner.
exitNodeOnline = tsLogo {
dots : [ 9 ] byte {
0 , 0 , 0 ,
1 , 1 , 1 ,
0 , 1 , 0 ,
} ,
dotMask : func ( dc * gg . Context , borderUnits int , radius int ) * image . Alpha {
bu , r := float64 ( borderUnits ) , float64 ( radius )
x1 := r * ( bu + 3.5 )
y := r * ( bu + 7 )
x2 := x1 + ( r * 5 )
mc := gg . NewContext ( dc . Width ( ) , dc . Height ( ) )
mc . DrawLine ( x1 , y , x2 , y )
mc . DrawLine ( x2 - ( 1.5 * r ) , y - ( 1.5 * r ) , x2 , y )
mc . DrawLine ( x2 - ( 1.5 * r ) , y + ( 1.5 * r ) , x2 , y )
mc . SetLineWidth ( r * 3 )
mc . Stroke ( )
return mc . AsMask ( )
} ,
overlay : func ( dc * gg . Context , borderUnits int , radius int ) {
bu , r := float64 ( borderUnits ) , float64 ( radius )
x1 := r * ( bu + 3.5 )
y := r * ( bu + 7 )
x2 := x1 + ( r * 5 )
dc . DrawLine ( x1 , y , x2 , y )
dc . DrawLine ( x2 - ( 1.5 * r ) , y - ( 1.5 * r ) , x2 , y )
dc . DrawLine ( x2 - ( 1.5 * r ) , y + ( 1.5 * r ) , x2 , y )
dc . SetColor ( fg )
dc . SetLineWidth ( r )
dc . Stroke ( )
} ,
}
// exitNodeOffline is the Tailscale logo with a red "x" in the corner.
exitNodeOffline = tsLogo {
dots : [ 9 ] byte {
0 , 0 , 0 ,
1 , 1 , 1 ,
0 , 1 , 0 ,
} ,
dotMask : func ( dc * gg . Context , borderUnits int , radius int ) * image . Alpha {
bu , r := float64 ( borderUnits ) , float64 ( radius )
x := r * ( bu + 3 )
mc := gg . NewContext ( dc . Width ( ) , dc . Height ( ) )
mc . DrawRectangle ( x , x , r * 6 , r * 6 )
mc . Fill ( )
return mc . AsMask ( )
} ,
overlay : func ( dc * gg . Context , borderUnits int , radius int ) {
bu , r := float64 ( borderUnits ) , float64 ( radius )
x1 := r * ( bu + 4 )
x2 := x1 + ( r * 3.5 )
dc . DrawLine ( x1 , x1 , x2 , x2 )
dc . DrawLine ( x1 , x2 , x2 , x1 )
dc . SetColor ( red )
dc . SetLineWidth ( r )
dc . Stroke ( )
} ,
} ,
}
}
)
)
var (
var (
black = color . NRGBA { 0 , 0 , 0 , 255 }
b g = color . NRGBA { 0 , 0 , 0 , 255 }
white = color . NRGBA { 255 , 255 , 255 , 255 }
fg = color . NRGBA { 255 , 255 , 255 , 255 }
gray = color . NRGBA { 255 , 255 , 255 , 102 }
gray = color . NRGBA { 255 , 255 , 255 , 102 }
red = color . NRGBA { 229 , 111 , 74 , 255 }
)
)
// render returns a PNG image of the logo.
// render returns a PNG image of the logo.
@ -140,15 +218,21 @@ func (logo tsLogo) renderWithBorder(borderUnits int) *bytes.Buffer {
dc := gg . NewContext ( dim , dim )
dc := gg . NewContext ( dim , dim )
dc . DrawRectangle ( 0 , 0 , float64 ( dim ) , float64 ( dim ) )
dc . DrawRectangle ( 0 , 0 , float64 ( dim ) , float64 ( dim ) )
dc . SetColor ( b lack )
dc . SetColor ( b g )
dc . Fill ( )
dc . Fill ( )
if logo . dotMask != nil {
mask := logo . dotMask ( dc , borderUnits , radius )
dc . SetMask ( mask )
dc . InvertMask ( )
}
for y := 0 ; y < 3 ; y ++ {
for y := 0 ; y < 3 ; y ++ {
for x := 0 ; x < 3 ; x ++ {
for x := 0 ; x < 3 ; x ++ {
px := ( borderUnits + 1 + 3 * x ) * radius
px := ( borderUnits + 1 + 3 * x ) * radius
py := ( borderUnits + 1 + 3 * y ) * radius
py := ( borderUnits + 1 + 3 * y ) * radius
col := white
col := fg
if logo [ y * 3 + x ] == 0 {
if logo .dots [y * 3 + x ] == 0 {
col = gray
col = gray
}
}
dc . DrawCircle ( float64 ( px ) , float64 ( py ) , radius )
dc . DrawCircle ( float64 ( px ) , float64 ( py ) , radius )
@ -157,6 +241,11 @@ func (logo tsLogo) renderWithBorder(borderUnits int) *bytes.Buffer {
}
}
}
}
if logo . overlay != nil {
dc . ResetClip ( )
logo . overlay ( dc , borderUnits , radius )
}
b := bytes . NewBuffer ( nil )
b := bytes . NewBuffer ( nil )
png . Encode ( b , dc . Image ( ) )
png . Encode ( b , dc . Image ( ) )
return b
return b
@ -164,7 +253,7 @@ func (logo tsLogo) renderWithBorder(borderUnits int) *bytes.Buffer {
// setAppIcon renders logo and sets it as the systray icon.
// setAppIcon renders logo and sets it as the systray icon.
func setAppIcon ( icon tsLogo ) {
func setAppIcon ( icon tsLogo ) {
if icon == loading {
if icon . dots == loading . dots {
startLoadingAnimation ( )
startLoadingAnimation ( )
} else {
} else {
stopLoadingAnimation ( )
stopLoadingAnimation ( )