269 lines
6.1 KiB
Go
269 lines
6.1 KiB
Go
package gui
|
|
|
|
import (
|
|
"image/color"
|
|
|
|
"gioui.org/font"
|
|
"gioui.org/text"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget/material"
|
|
)
|
|
|
|
// Spacing scale. Every gap in the UI is one of these; ad-hoc values are what
|
|
// makes an interface feel noisy.
|
|
const (
|
|
SpaceXS unit.Dp = 4
|
|
SpaceSM unit.Dp = 8
|
|
SpaceMD unit.Dp = 12
|
|
SpaceLG unit.Dp = 16
|
|
SpaceXL unit.Dp = 24
|
|
Space2XL unit.Dp = 32
|
|
)
|
|
|
|
// Corner radii.
|
|
const (
|
|
RadiusSM unit.Dp = 6
|
|
RadiusMD unit.Dp = 10
|
|
RadiusLG unit.Dp = 14
|
|
RadiusPill unit.Dp = 999
|
|
)
|
|
|
|
// Type scale. Only five sizes exist so hierarchy stays legible when a panel is
|
|
// dense with numbers.
|
|
const (
|
|
SizeDisplay unit.Sp = 23
|
|
SizeTitle unit.Sp = 17
|
|
SizeSubtitle unit.Sp = 14
|
|
SizeBody unit.Sp = 13
|
|
SizeCaption unit.Sp = 11.5
|
|
SizeMono unit.Sp = 12
|
|
)
|
|
|
|
// Palette holds every colour the UI is allowed to use.
|
|
type Palette struct {
|
|
// Surfaces, from furthest back to nearest front.
|
|
Bg color.NRGBA
|
|
BgElevated color.NRGBA
|
|
Surface color.NRGBA
|
|
SurfaceHi color.NRGBA
|
|
|
|
Border color.NRGBA
|
|
BorderHi color.NRGBA
|
|
|
|
// Three text weights carry the whole information hierarchy: primary for
|
|
// values, secondary for labels, dim for metadata.
|
|
TextPri color.NRGBA
|
|
TextSec color.NRGBA
|
|
TextDim color.NRGBA
|
|
|
|
Accent color.NRGBA
|
|
AccentDim color.NRGBA
|
|
AccentFg color.NRGBA
|
|
|
|
OK color.NRGBA
|
|
Warn color.NRGBA
|
|
Fail color.NRGBA
|
|
Info color.NRGBA
|
|
|
|
// Series colours for the latency chart, in assignment order. They are
|
|
// distinguishable at 2px stroke width and stay distinct in both themes.
|
|
Series []color.NRGBA
|
|
|
|
// Scrim dims the app behind the loading overlay.
|
|
Scrim color.NRGBA
|
|
}
|
|
|
|
func rgb(v uint32) color.NRGBA {
|
|
return color.NRGBA{R: uint8(v >> 16), G: uint8(v >> 8), B: uint8(v), A: 0xFF}
|
|
}
|
|
|
|
// DarkPalette is the default. The app is a diagnostic tool that people leave
|
|
// open in the background, so it defaults to the low-glare theme.
|
|
func DarkPalette() Palette {
|
|
return Palette{
|
|
Bg: rgb(0x0E1116),
|
|
BgElevated: rgb(0x141922),
|
|
Surface: rgb(0x1A202B),
|
|
SurfaceHi: rgb(0x222A38),
|
|
|
|
Border: rgb(0x252E3B),
|
|
BorderHi: rgb(0x364153),
|
|
|
|
TextPri: rgb(0xE7EBF3),
|
|
TextSec: rgb(0x9AA4B8),
|
|
TextDim: rgb(0x69738A),
|
|
|
|
Accent: rgb(0x4C8DFF),
|
|
AccentDim: rgb(0x27447A),
|
|
AccentFg: rgb(0xFFFFFF),
|
|
|
|
OK: rgb(0x3DCE87),
|
|
Warn: rgb(0xF0A93B),
|
|
Fail: rgb(0xFF6B6B),
|
|
Info: rgb(0x8B9BFF),
|
|
|
|
Series: []color.NRGBA{
|
|
rgb(0x4C8DFF), rgb(0x2DD4BF), rgb(0xA78BFA), rgb(0xFBBF24),
|
|
rgb(0xF472B6), rgb(0xA3E635), rgb(0x38BDF8), rgb(0xFB923C),
|
|
},
|
|
|
|
Scrim: color.NRGBA{R: 0x08, G: 0x0A, B: 0x0E, A: 0xC4},
|
|
}
|
|
}
|
|
|
|
// LightPalette mirrors the dark one for people working in bright rooms.
|
|
func LightPalette() Palette {
|
|
return Palette{
|
|
Bg: rgb(0xF6F7F9),
|
|
BgElevated: rgb(0xFFFFFF),
|
|
Surface: rgb(0xFFFFFF),
|
|
SurfaceHi: rgb(0xF0F2F6),
|
|
|
|
Border: rgb(0xE3E7ED),
|
|
BorderHi: rgb(0xCFD5DE),
|
|
|
|
TextPri: rgb(0x111826),
|
|
TextSec: rgb(0x4A5568),
|
|
TextDim: rgb(0x818C9E),
|
|
|
|
Accent: rgb(0x2563EB),
|
|
AccentDim: rgb(0xBFD3FA),
|
|
AccentFg: rgb(0xFFFFFF),
|
|
|
|
OK: rgb(0x0F9D58),
|
|
Warn: rgb(0xC77700),
|
|
Fail: rgb(0xD93636),
|
|
Info: rgb(0x4F5DD1),
|
|
|
|
Series: []color.NRGBA{
|
|
rgb(0x2563EB), rgb(0x0D9488), rgb(0x7C3AED), rgb(0xD97706),
|
|
rgb(0xDB2777), rgb(0x65A30D), rgb(0x0284C7), rgb(0xEA580C),
|
|
},
|
|
|
|
Scrim: color.NRGBA{R: 0x1A, G: 0x1F, B: 0x28, A: 0xB8},
|
|
}
|
|
}
|
|
|
|
// Theme bundles the Gio material theme with this app's design tokens.
|
|
type Theme struct {
|
|
*material.Theme
|
|
P Palette
|
|
Dark bool
|
|
|
|
// Mono is the typeface used for addresses, ports and log lines, where
|
|
// column alignment matters more than typographic polish.
|
|
Mono font.Typeface
|
|
|
|
// HasCJK reports whether a font with Chinese coverage was found. When it
|
|
// is false the UI falls back to English labels rather than rendering
|
|
// tofu boxes.
|
|
HasCJK bool
|
|
|
|
// Lang selects the label set.
|
|
Lang Lang
|
|
}
|
|
|
|
// NewTheme builds a theme from a shaper and font collection produced by
|
|
// [LoadFonts].
|
|
func NewTheme(fonts *FontSet, dark bool) *Theme {
|
|
mt := material.NewTheme()
|
|
mt.Shaper = text.NewShaper(text.WithCollection(fonts.Collection))
|
|
mt.TextSize = SizeBody
|
|
mt.Face = fonts.UI
|
|
mt.FingerSize = 26
|
|
|
|
th := &Theme{
|
|
Theme: mt,
|
|
Dark: dark,
|
|
Mono: fonts.Mono,
|
|
HasCJK: fonts.HasCJK,
|
|
Lang: LangEN,
|
|
}
|
|
if fonts.HasCJK {
|
|
th.Lang = LangZH
|
|
}
|
|
th.SetDark(dark)
|
|
return th
|
|
}
|
|
|
|
// SetDark switches palettes and keeps the embedded material palette in sync so
|
|
// stock Gio widgets pick up the right colours too.
|
|
func (t *Theme) SetDark(dark bool) {
|
|
t.Dark = dark
|
|
if dark {
|
|
t.P = DarkPalette()
|
|
} else {
|
|
t.P = LightPalette()
|
|
}
|
|
t.Theme.Palette = material.Palette{
|
|
Bg: t.P.Bg,
|
|
Fg: t.P.TextPri,
|
|
ContrastBg: t.P.Accent,
|
|
ContrastFg: t.P.AccentFg,
|
|
}
|
|
}
|
|
|
|
// T looks up a localised string. It is a method on Theme so call sites stay
|
|
// short: th.T(K.Peers).
|
|
func (t *Theme) T(k Key) string { return Tr(t.Lang, k) }
|
|
|
|
// StatusColor maps a traffic-light verdict onto the palette.
|
|
func (t *Theme) StatusColor(s StatusLevel) color.NRGBA {
|
|
switch s {
|
|
case LevelOK:
|
|
return t.P.OK
|
|
case LevelWarn:
|
|
return t.P.Warn
|
|
case LevelFail:
|
|
return t.P.Fail
|
|
case LevelInfo:
|
|
return t.P.Info
|
|
default:
|
|
return t.P.TextDim
|
|
}
|
|
}
|
|
|
|
// StatusLevel is the UI-side severity, deliberately decoupled from
|
|
// netdiag.Status so widgets do not depend on the diagnostics package.
|
|
type StatusLevel int
|
|
|
|
const (
|
|
LevelNeutral StatusLevel = iota
|
|
LevelOK
|
|
LevelWarn
|
|
LevelFail
|
|
LevelInfo
|
|
)
|
|
|
|
// SeriesColor returns a stable chart colour for index i.
|
|
func (t *Theme) SeriesColor(i int) color.NRGBA {
|
|
if len(t.P.Series) == 0 {
|
|
return t.P.Accent
|
|
}
|
|
return t.P.Series[i%len(t.P.Series)]
|
|
}
|
|
|
|
// WithAlpha returns c with its alpha scaled by a (0..1).
|
|
func WithAlpha(c color.NRGBA, a float32) color.NRGBA {
|
|
if a < 0 {
|
|
a = 0
|
|
}
|
|
if a > 1 {
|
|
a = 1
|
|
}
|
|
c.A = uint8(float32(c.A) * a)
|
|
return c
|
|
}
|
|
|
|
// Mix blends a into b by t (0 returns a, 1 returns b).
|
|
func Mix(a, b color.NRGBA, t float32) color.NRGBA {
|
|
if t < 0 {
|
|
t = 0
|
|
}
|
|
if t > 1 {
|
|
t = 1
|
|
}
|
|
lerp := func(x, y uint8) uint8 { return uint8(float32(x) + (float32(y)-float32(x))*t) }
|
|
return color.NRGBA{R: lerp(a.R, b.R), G: lerp(a.G, b.G), B: lerp(a.B, b.B), A: lerp(a.A, b.A)}
|
|
}
|