add: gui and tsdiag
This commit is contained in:
+268
@@ -0,0 +1,268 @@
|
||||
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)}
|
||||
}
|
||||
Reference in New Issue
Block a user