Files
2026-07-26 20:44:49 +08:00

360 lines
11 KiB
Go

package gui
import (
"image"
"image/color"
"math"
"gioui.org/f32"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
)
// IconFunc draws an icon of the given pixel size in col, occupying a square of
// that size.
//
// The icons are drawn as vector line art rather than pulled from an icon font
// or the shiny material set: a dozen hand-drawn paths keep the binary small,
// avoid a dependency, and let every glyph share one stroke weight so the
// toolbar reads as a set.
type IconFunc func(gtx C, size int, col color.NRGBA) D
// defaultStroke is the icon stroke width as a fraction of the icon box.
const defaultStroke = 0.085
// iconCanvas sets up a unit coordinate space (0..1 in both axes) and strokes
// whatever the draw function puts on the path.
func iconCanvas(gtx C, size int, col color.NRGBA, width float32, draw func(p *clip.Path, pt func(x, y float32) f32.Point)) D {
if size <= 0 {
return D{}
}
s := float32(size)
pt := func(x, y float32) f32.Point { return f32.Pt(x*s, y*s) }
var p clip.Path
p.Begin(gtx.Ops)
draw(&p, pt)
w := width * s
if w < 1 {
w = 1
}
paint.FillShape(gtx.Ops, col, clip.Stroke{Path: p.End(), Width: w}.Op())
return D{Size: image.Pt(size, size)}
}
// arcAt appends a circle (or arc) centred at (cx, cy) with radius r, in unit
// coordinates.
func arcAt(p *clip.Path, pt func(x, y float32) f32.Point, cx, cy, r, startAngle, sweep float32) {
start := pt(
cx+r*float32(math.Cos(float64(startAngle))),
cy+r*float32(math.Sin(float64(startAngle))),
)
c := pt(cx, cy)
p.MoveTo(start)
d := c.Sub(start)
p.Arc(d, d, sweep)
}
func poly(p *clip.Path, pt func(x, y float32) f32.Point, pts ...[2]float32) {
if len(pts) == 0 {
return
}
p.MoveTo(pt(pts[0][0], pts[0][1]))
for _, q := range pts[1:] {
p.LineTo(pt(q[0], q[1]))
}
}
func line(p *clip.Path, pt func(x, y float32) f32.Point, x1, y1, x2, y2 float32) {
p.MoveTo(pt(x1, y1))
p.LineTo(pt(x2, y2))
}
func rect(p *clip.Path, pt func(x, y float32) f32.Point, x, y, w, h float32) {
p.MoveTo(pt(x, y))
p.LineTo(pt(x+w, y))
p.LineTo(pt(x+w, y+h))
p.LineTo(pt(x, y+h))
p.Close()
}
// dot paints a filled circle in unit coordinates, for icons that need a solid
// node rather than an outline.
func dot(gtx C, size int, col color.NRGBA, cx, cy, r float32) {
s := float32(size)
d := int(2 * r * s)
if d < 2 {
d = 2
}
off := op.Offset(image.Pt(int(cx*s)-d/2, int(cy*s)-d/2)).Push(gtx.Ops)
Circle(gtx, d, col)
off.Pop()
}
// ---------------------------------------------------------------------------
// Navigation icons
// ---------------------------------------------------------------------------
// IconGrid is the overview page: four panes.
func IconGrid(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
rect(p, pt, 0.14, 0.14, 0.30, 0.30)
rect(p, pt, 0.56, 0.14, 0.30, 0.30)
rect(p, pt, 0.14, 0.56, 0.30, 0.30)
rect(p, pt, 0.56, 0.56, 0.30, 0.30)
})
}
// IconNodes is the peers page: three linked nodes.
func IconNodes(gtx C, size int, col color.NRGBA) D {
d := iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
line(p, pt, 0.50, 0.24, 0.22, 0.72)
line(p, pt, 0.50, 0.24, 0.78, 0.72)
line(p, pt, 0.22, 0.72, 0.78, 0.72)
})
dot(gtx, size, col, 0.50, 0.22, 0.13)
dot(gtx, size, col, 0.21, 0.75, 0.13)
dot(gtx, size, col, 0.79, 0.75, 0.13)
return d
}
// IconBroadcast is the LAN page: a source radiating outwards.
func IconBroadcast(gtx C, size int, col color.NRGBA) D {
d := iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
const q = math.Pi / 4
arcAt(p, pt, 0.5, 0.5, 0.22, -q, 2*q)
arcAt(p, pt, 0.5, 0.5, 0.40, -q, 2*q)
arcAt(p, pt, 0.5, 0.5, 0.22, float32(math.Pi)-q, 2*q)
arcAt(p, pt, 0.5, 0.5, 0.40, float32(math.Pi)-q, 2*q)
})
dot(gtx, size, col, 0.5, 0.5, 0.12)
return d
}
// IconPulse is the diagnostics page: an activity trace.
func IconPulse(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
poly(p, pt,
[2]float32{0.08, 0.52},
[2]float32{0.28, 0.52},
[2]float32{0.40, 0.22},
[2]float32{0.56, 0.80},
[2]float32{0.68, 0.52},
[2]float32{0.92, 0.52},
)
})
}
// IconList is the logs page.
func IconList(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
line(p, pt, 0.16, 0.28, 0.84, 0.28)
line(p, pt, 0.16, 0.50, 0.84, 0.50)
line(p, pt, 0.16, 0.72, 0.60, 0.72)
})
}
// IconSliders is the settings page.
func IconSliders(gtx C, size int, col color.NRGBA) D {
d := iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
line(p, pt, 0.12, 0.30, 0.88, 0.30)
line(p, pt, 0.12, 0.70, 0.88, 0.70)
})
dot(gtx, size, col, 0.34, 0.30, 0.13)
dot(gtx, size, col, 0.66, 0.70, 0.13)
return d
}
// ---------------------------------------------------------------------------
// Action icons
// ---------------------------------------------------------------------------
// IconCopy is the copy-to-clipboard action.
func IconCopy(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
rect(p, pt, 0.32, 0.32, 0.54, 0.54)
poly(p, pt,
[2]float32{0.68, 0.20},
[2]float32{0.14, 0.20},
[2]float32{0.14, 0.68},
)
})
}
// IconUpload is the share/upload action.
func IconUpload(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
line(p, pt, 0.5, 0.16, 0.5, 0.64)
poly(p, pt,
[2]float32{0.30, 0.36},
[2]float32{0.50, 0.16},
[2]float32{0.70, 0.36},
)
poly(p, pt,
[2]float32{0.16, 0.62},
[2]float32{0.16, 0.86},
[2]float32{0.84, 0.86},
[2]float32{0.84, 0.62},
)
})
}
// IconSave is the write-to-disk action.
func IconSave(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
line(p, pt, 0.5, 0.14, 0.5, 0.62)
poly(p, pt,
[2]float32{0.30, 0.42},
[2]float32{0.50, 0.62},
[2]float32{0.70, 0.42},
)
poly(p, pt,
[2]float32{0.16, 0.62},
[2]float32{0.16, 0.86},
[2]float32{0.84, 0.86},
[2]float32{0.84, 0.62},
)
})
}
// IconRefresh is the re-run action.
func IconRefresh(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
arcAt(p, pt, 0.5, 0.5, 0.32, -1.9, 4.9)
poly(p, pt,
[2]float32{0.60, 0.06},
[2]float32{0.61, 0.30},
[2]float32{0.38, 0.24},
)
})
}
// IconCheck marks a passed check.
func IconCheck(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, 0.11, func(p *clip.Path, pt func(x, y float32) f32.Point) {
poly(p, pt,
[2]float32{0.18, 0.52},
[2]float32{0.42, 0.74},
[2]float32{0.82, 0.28},
)
})
}
// IconWarn marks a warning.
func IconWarn(gtx C, size int, col color.NRGBA) D {
d := iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
poly(p, pt,
[2]float32{0.50, 0.12},
[2]float32{0.92, 0.84},
[2]float32{0.08, 0.84},
)
p.Close()
line(p, pt, 0.5, 0.40, 0.5, 0.60)
})
dot(gtx, size, col, 0.5, 0.72, 0.055)
return d
}
// IconChevronRight indicates an expandable row.
func IconChevronRight(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
poly(p, pt,
[2]float32{0.40, 0.24},
[2]float32{0.66, 0.50},
[2]float32{0.40, 0.76},
)
})
}
// IconChevronDown indicates an expanded row.
func IconChevronDown(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
poly(p, pt,
[2]float32{0.24, 0.40},
[2]float32{0.50, 0.66},
[2]float32{0.76, 0.40},
)
})
}
// IconSearch prefixes the log filter field.
func IconSearch(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
arcAt(p, pt, 0.44, 0.44, 0.28, 0, 2*math.Pi)
line(p, pt, 0.64, 0.64, 0.86, 0.86)
})
}
// IconGlobe marks anything about the public internet.
func IconGlobe(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
arcAt(p, pt, 0.5, 0.5, 0.38, 0, 2*math.Pi)
line(p, pt, 0.12, 0.5, 0.88, 0.5)
// Two meridians, drawn as opposing quadratic bows.
p.MoveTo(pt(0.5, 0.12))
p.QuadTo(pt(0.22, 0.5), pt(0.5, 0.88))
p.MoveTo(pt(0.5, 0.12))
p.QuadTo(pt(0.78, 0.5), pt(0.5, 0.88))
})
}
// IconServer marks a discovered game server.
func IconServer(gtx C, size int, col color.NRGBA) D {
d := iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
rect(p, pt, 0.14, 0.18, 0.72, 0.26)
rect(p, pt, 0.14, 0.56, 0.72, 0.26)
})
dot(gtx, size, col, 0.26, 0.31, 0.05)
dot(gtx, size, col, 0.26, 0.69, 0.05)
return d
}
// IconLink marks a peer referenced by a config rule.
func IconLink(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
arcAt(p, pt, 0.34, 0.66, 0.22, -2.36, 3.14)
arcAt(p, pt, 0.66, 0.34, 0.22, 0.78, 3.14)
line(p, pt, 0.38, 0.62, 0.62, 0.38)
})
}
// IconShield marks NAT and firewall findings.
func IconShield(gtx C, size int, col color.NRGBA) D {
return iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
p.MoveTo(pt(0.5, 0.10))
p.LineTo(pt(0.84, 0.24))
p.LineTo(pt(0.84, 0.52))
p.QuadTo(pt(0.84, 0.80), pt(0.5, 0.92))
p.QuadTo(pt(0.16, 0.80), pt(0.16, 0.52))
p.LineTo(pt(0.16, 0.24))
p.Close()
})
}
// IconRouter marks port-mapping results.
func IconRouter(gtx C, size int, col color.NRGBA) D {
d := iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
rect(p, pt, 0.10, 0.54, 0.80, 0.30)
line(p, pt, 0.32, 0.54, 0.32, 0.34)
line(p, pt, 0.32, 0.34, 0.62, 0.20)
line(p, pt, 0.68, 0.54, 0.68, 0.30)
})
dot(gtx, size, col, 0.24, 0.69, 0.05)
dot(gtx, size, col, 0.40, 0.69, 0.05)
return d
}
// IconRoute marks the local-interface section.
func IconRoute(gtx C, size int, col color.NRGBA) D {
d := iconCanvas(gtx, size, col, defaultStroke, func(p *clip.Path, pt func(x, y float32) f32.Point) {
p.MoveTo(pt(0.22, 0.78))
p.QuadTo(pt(0.22, 0.50), pt(0.50, 0.50))
p.QuadTo(pt(0.78, 0.50), pt(0.78, 0.22))
})
dot(gtx, size, col, 0.22, 0.80, 0.11)
dot(gtx, size, col, 0.78, 0.20, 0.11)
return d
}