Show recent errors as labelled chips and rebuild the reset button to match

This commit is contained in:
flamingcow
2026-07-31 17:05:31 -07:00
parent f512391ee4
commit 3c8637f931
2 changed files with 85 additions and 16 deletions
+26
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"math"
"path/filepath" "path/filepath"
"unsafe" "unsafe"
@@ -449,6 +450,31 @@ func (fb *framebuffer) rect(x0, y0, w, h int, c rgb) {
} }
} }
// Distance to the rectangle the corner radius sweeps around, which is zero
// across the whole flat middle and grows only near a corner. Taking coverage
// from that rather than from a plain inside test keeps the curves smooth
// instead of stepped.
func (fb *framebuffer) roundRect(x0, y0, w, h, r int, c rgb) {
// A radius past half the shorter side has no meaning and would put the
// swept rectangle inside out, which matters while something is growing from
// nothing.
r = min(r, min(w, h)/2)
ix0, iy0 := float64(x0+r), float64(y0+r)
ix1, iy1 := float64(x0+w-1-r), float64(y0+h-1-r)
for y := y0; y < y0+h; y++ {
for x := x0; x < x0+w; x++ {
fx, fy := float64(x), float64(y)
dx := math.Max(math.Max(ix0-fx, fx-ix1), 0)
dy := math.Max(math.Max(iy0-fy, fy-iy1), 0)
cov := float64(r) - math.Sqrt(dx*dx+dy*dy) + 0.5
if cov <= 0 {
continue
}
fb.blend(x, y, c, uint8(math.Min(cov, 1)*255))
}
}
}
// Blends src over the existing pixel, with cov as 0-255 coverage. // Blends src over the existing pixel, with cov as 0-255 coverage.
func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) { func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) {
if x < 0 || y < 0 || x >= fb.w || y >= fb.h || cov == 0 { if x < 0 || y < 0 || x >= fb.w || y >= fb.h || cov == 0 {
+59 -16
View File
@@ -12,7 +12,6 @@ var (
uiOKEdge = rgb{0x3c, 0xe0, 0x70} uiOKEdge = rgb{0x3c, 0xe0, 0x70}
uiErrFil = rgb{0x54, 0x18, 0x1c} uiErrFil = rgb{0x54, 0x18, 0x1c}
uiErrEdg = rgb{0xff, 0x46, 0x46} uiErrEdg = rgb{0xff, 0x46, 0x46}
uiButton = rgb{0x25, 0x2b, 0x33}
uiFg = rgb{0xe6, 0xe8, 0xea} uiFg = rgb{0xe6, 0xe8, 0xea}
uiDim = rgb{0x9a, 0xa2, 0xac} uiDim = rgb{0x9a, 0xa2, 0xac}
uiCyan = rgb{0x5c, 0xc8, 0xe0} uiCyan = rgb{0x5c, 0xc8, 0xe0}
@@ -30,7 +29,15 @@ const (
btnW = 240 btnW = 240
btnH = 64 btnH = 64
btnRadius = 10
btnBorder = 2
holdDuration = time.Second holdDuration = time.Second
chipCols = 2
chipPadY = 9
chipGap = 8
chipRadius = 8
chipBorder = 2
) )
type rect struct { type rect struct {
@@ -88,7 +95,7 @@ func newDisplay() (*display, error) {
// the loaded faces actually measure. // the loaded faces actually measure.
gridLine := d.grid.cellH + rowGap gridLine := d.grid.cellH + rowGap
errH := len(errRows) * gridLine errH := len(errRows) * gridLine
d.nowH = d.huge.cellH + rowGap + gridLine + blockGap + errH d.nowH = d.huge.cellH + rowGap + gridLine + blockGap + d.chipsH()
d.sinceH = 4*gridLine + blockGap + errH + blockGap + btnH d.sinceH = 4*gridLine + blockGap + errH + blockGap + btnH
inner := fb.w - 2*uiMargin inner := fb.w - 2*uiMargin
@@ -129,21 +136,23 @@ func (d *display) holdReset(x, y int, down bool, now time.Time) bool {
return true return true
} }
// Built like the error chips, since it sits among them: a coloured outline
// around a dark well. Cyan rather than the status colours because it is
// something to press, not something being reported.
func (d *display) drawResetButton() { func (d *display) drawResetButton() {
r := d.resetBtn r := d.resetBtn
d.fb.rect(r.x, r.y, r.w, r.h, uiButton) d.fb.roundRect(r.x, r.y, r.w, r.h, btnRadius, uiCyan)
d.fb.roundRect(r.x+btnBorder, r.y+btnBorder, r.w-2*btnBorder, r.h-2*btnBorder,
btnRadius-btnBorder, uiBg)
// The hold fills the well rather than the whole button, so the outline stays
// put and it reads as the button filling up.
split := r.x + btnBorder
if d.holdFrac > 0 { if d.holdFrac > 0 {
w := int(float64(r.w) * math.Min(d.holdFrac, 1)) w := int(float64(r.w-2*btnBorder) * math.Min(d.holdFrac, 1))
d.fb.rect(r.x, r.y, w, r.h, uiCyan) d.fb.roundRect(r.x+btnBorder, r.y+btnBorder, w, r.h-2*btnBorder,
} btnRadius-btnBorder, uiCyan)
for _, e := range []rect{ split += w
{r.x, r.y, r.w, 2},
{r.x, r.y + r.h - 2, r.w, 2},
{r.x, r.y, 2, r.h},
{r.x + r.w - 2, r.y, 2, r.h},
} {
d.fb.rect(e.x, e.y, e.w, e.h, uiCyan)
} }
label := "RESET" label := "RESET"
@@ -151,10 +160,9 @@ func (d *display) drawResetButton() {
ly := r.y + (r.h-d.gridB.cellH)/2 ly := r.y + (r.h-d.gridB.cellH)/2
// The label straddles the fill, so each glyph takes the colour that reads // The label straddles the fill, so each glyph takes the colour that reads
// against whatever is behind it. // against whatever is behind it.
split := r.x + int(float64(r.w)*math.Min(d.holdFrac, 1))
for i, c := range label { for i, c := range label {
gx := lx + i*d.gridB.cellW gx := lx + i*d.gridB.cellW
col := uiFg col := uiCyan
if gx+d.gridB.cellW/2 < split { if gx+d.gridB.cellW/2 < split {
col = uiBg col = uiBg
} }
@@ -203,6 +211,41 @@ func (d *display) errBlock(x, w, y int, e errs) int {
return y return y
} }
func (d *display) chipH() int { return d.grid.cellH + 2*chipPadY }
func (d *display) chipsH() int {
rows := (len(errRows) + chipCols - 1) / chipCols
return rows*(d.chipH()+chipGap) - chipGap
}
// The same kinds as errBlock, but answering whether rather than how many, and
// carrying their own labels so nothing has to be matched up across a row. Over
// a window this short a count is a number nobody can read before it changes;
// the only thing worth knowing at a glance is which kinds are happening now.
func (d *display) errChips(x, w, y int, e errs) int {
ch := d.chipH()
cw := (w - (chipCols-1)*chipGap) / chipCols
for i, r := range errRows {
col, row := i%chipCols, i/chipCols
// A last row with nothing to sit beside is centred, so the odd one out
// balances the rows above rather than hanging off the left of them.
n := min(len(errRows)-row*chipCols, chipCols)
cx := x + (w-(n*cw+(n-1)*chipGap))/2 + col*(cw+chipGap)
cy := y + row*(ch+chipGap)
// Outlined by drawing the border colour and then sinking a smaller well
// of background into it, so both curves get the same antialiasing.
c := errColor(r.get(e))
d.fb.roundRect(cx, cy, cw, ch, chipRadius, c)
d.fb.roundRect(cx+chipBorder, cy+chipBorder,
cw-2*chipBorder, ch-2*chipBorder, chipRadius-chipBorder, uiBg)
tx := cx + (cw-len([]rune(r.label))*d.grid.cellW)/2
d.grid.draw(d.fb, tx, cy+chipPadY, r.label, c)
}
return y + d.chipsH()
}
func (d *display) panel(p rect, e errs, contentH int) (int, int, int) { func (d *display) panel(p rect, e errs, contentH int) (int, int, int) {
fill, edge := uiOKFill, uiOKEdge fill, edge := uiOKFill, uiOKEdge
if e.total() > 0 { if e.total() > 0 {
@@ -257,7 +300,7 @@ func (d *display) render(v view, elapsed time.Duration, target float64, cable st
x, w, y := d.panel(d.nowPanel, v.window, d.nowH) x, w, y := d.panel(d.nowPanel, v.window, d.nowH)
y = d.rateRow(x, w, y, "RX", v.rxGbps, target) y = d.rateRow(x, w, y, "RX", v.rxGbps, target)
y = d.row(d.grid, x, w, y, "packets/s", commas(roundPPS(v.rxPPS)), uiFg) y = d.row(d.grid, x, w, y, "packets/s", commas(roundPPS(v.rxPPS)), uiFg)
d.errBlock(x, w, y+blockGap, v.window) d.errChips(x, w, y+blockGap, v.window)
x, w, y = d.panel(d.sincePanel, v.since, d.sinceH) x, w, y = d.panel(d.sincePanel, v.since, d.sinceH)
y = d.row(d.grid, x, w, y, "uptime", uptime(elapsed), uiFg) y = d.row(d.grid, x, w, y, "uptime", uptime(elapsed), uiFg)