From 3c8637f931f966b7f41d5532ccddbb9e9f359f20 Mon Sep 17 00:00:00 2001 From: flamingcow Date: Fri, 31 Jul 2026 17:05:31 -0700 Subject: [PATCH] Show recent errors as labelled chips and rebuild the reset button to match --- fb.go | 26 +++++++++++++++++++++ ui.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 85 insertions(+), 16 deletions(-) diff --git a/fb.go b/fb.go index d4dec6f..77fe8a0 100644 --- a/fb.go +++ b/fb.go @@ -3,6 +3,7 @@ package main import ( "encoding/binary" "fmt" + "math" "path/filepath" "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. func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) { if x < 0 || y < 0 || x >= fb.w || y >= fb.h || cov == 0 { diff --git a/ui.go b/ui.go index c4f17a2..5d8fb09 100644 --- a/ui.go +++ b/ui.go @@ -12,7 +12,6 @@ var ( uiOKEdge = rgb{0x3c, 0xe0, 0x70} uiErrFil = rgb{0x54, 0x18, 0x1c} uiErrEdg = rgb{0xff, 0x46, 0x46} - uiButton = rgb{0x25, 0x2b, 0x33} uiFg = rgb{0xe6, 0xe8, 0xea} uiDim = rgb{0x9a, 0xa2, 0xac} uiCyan = rgb{0x5c, 0xc8, 0xe0} @@ -30,7 +29,15 @@ const ( btnW = 240 btnH = 64 + btnRadius = 10 + btnBorder = 2 holdDuration = time.Second + + chipCols = 2 + chipPadY = 9 + chipGap = 8 + chipRadius = 8 + chipBorder = 2 ) type rect struct { @@ -88,7 +95,7 @@ func newDisplay() (*display, error) { // the loaded faces actually measure. gridLine := d.grid.cellH + rowGap 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 inner := fb.w - 2*uiMargin @@ -129,21 +136,23 @@ func (d *display) holdReset(x, y int, down bool, now time.Time) bool { 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() { 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 { - w := int(float64(r.w) * math.Min(d.holdFrac, 1)) - d.fb.rect(r.x, r.y, w, r.h, uiCyan) - } - for _, e := range []rect{ - {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) + w := int(float64(r.w-2*btnBorder) * math.Min(d.holdFrac, 1)) + d.fb.roundRect(r.x+btnBorder, r.y+btnBorder, w, r.h-2*btnBorder, + btnRadius-btnBorder, uiCyan) + split += w } label := "RESET" @@ -151,10 +160,9 @@ func (d *display) drawResetButton() { ly := r.y + (r.h-d.gridB.cellH)/2 // The label straddles the fill, so each glyph takes the colour that reads // against whatever is behind it. - split := r.x + int(float64(r.w)*math.Min(d.holdFrac, 1)) for i, c := range label { gx := lx + i*d.gridB.cellW - col := uiFg + col := uiCyan if gx+d.gridB.cellW/2 < split { col = uiBg } @@ -203,6 +211,41 @@ func (d *display) errBlock(x, w, y int, e errs) int { 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) { fill, edge := uiOKFill, uiOKEdge 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) 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) - 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) y = d.row(d.grid, x, w, y, "uptime", uptime(elapsed), uiFg)