package main import ( "fmt" "math" "time" ) var ( uiBg = rgb{0x12, 0x14, 0x18} uiOKFill = rgb{0x18, 0x42, 0x26} uiOKEdge = rgb{0x3c, 0xe0, 0x70} uiErrFil = rgb{0x54, 0x18, 0x1c} uiErrEdg = rgb{0xff, 0x46, 0x46} uiFg = rgb{0xe6, 0xe8, 0xea} uiDim = rgb{0x9a, 0xa2, 0xac} uiCyan = rgb{0x5c, 0xc8, 0xe0} uiGreen = rgb{0x6c, 0xdc, 0x86} uiRed = rgb{0xf0, 0x6b, 0x6b} ) // Distances between ink, since layout measures a line from the top of a digit // to the baseline rather than across a cell with accent and descender slack in // it. Values carried over from spacing cells will look too small here. const ( step = 4 spaceTight = step * 2 spaceGroup = step * 4 spaceRow = step * 8 ) const ( uiMargin = spaceGroup uiPad = spaceGroup uiBorder = step * 3 pairGap = spaceGroup blockGap = spaceGroup btnW = 300 btnH = 80 holdDuration = time.Second chipRadius = spaceTight chipBorder = 2 gridCols = 2 chipPadY = spaceGroup chipGap = spaceTight statRowGap = spaceRow ) type rect struct { x, y, w, h int } func (r rect) contains(x, y int) bool { return x >= r.x && x < r.x+r.w && y >= r.y && y < r.y+r.h } type display struct { fb *framebuffer big *textFace grid *textFace gridB *textFace nowPanel rect sincePanel rect nowYs []int sinceYs []int resetBtn rect holdStart time.Time holdFrac float64 fired bool } func newDisplay() (*display, error) { fb, err := openFramebuffer() if err != nil { return nil, err } d := &display{fb: fb} for _, spec := range []struct { dst **textFace bold bool size float64 }{ {&d.big, true, 40}, {&d.grid, false, 34}, {&d.gridB, true, 34}, } { face, err := loadFace(spec.bold, spec.size) if err != nil { fb.close() return nil, err } *spec.dst = face } if d.grid.cellW != d.gridB.cellW { fb.close() return nil, fmt.Errorf("grid faces disagree on cell width: %d vs %d", d.grid.cellW, d.gridB.cellW) } now := []int{d.statsH(d.big, 2), d.chipsH()} since := []int{d.statsH(d.gridB, 4), d.countsH(), btnH} // One gap for both panels, and vertically the frame is the border alone. // Insetting by uiPad as well would add it to the gaps at a panel's ends but // not to the ones between blocks, which is not equal spacing however evenly // the remainder is divided. gaps := len(now) + len(since) + 2 spare := fb.h - 2*uiMargin - blockGap - 4*uiBorder - sum(now) - sum(since) if spare < 0 { fb.close() return nil, fmt.Errorf("panel content is %dpx taller than the screen", -spare) } gap := spare / gaps inner := fb.w - 2*uiMargin nowH := 2*uiBorder + sum(now) + (len(now)+1)*gap sinceH := 2*uiBorder + sum(since) + (len(since)+1)*gap d.nowPanel = rect{uiMargin, uiMargin, inner, nowH} d.sincePanel = rect{uiMargin, uiMargin + nowH + blockGap, inner, sinceH} d.nowYs = stack(d.nowPanel.y+uiBorder+gap, now, gap) d.sinceYs = stack(d.sincePanel.y+uiBorder+gap, since, gap) d.resetBtn = rect{ x: d.sincePanel.x + (inner-btnW)/2, y: d.sinceYs[len(d.sinceYs)-1], w: btnW, h: btnH, } return d, nil } func sum(hs []int) int { var t int for _, h := range hs { t += h } return t } func stack(y int, hs []int, gap int) []int { ys := make([]int, len(hs)) for i, h := range hs { ys[i] = y y += h + gap } return ys } // Lifting or sliding off cancels, and the press has to be released before it // can arm again. func (d *display) holdReset(x, y int, down bool, now time.Time) bool { if !down { d.holdStart, d.holdFrac, d.fired = time.Time{}, 0, false return false } if d.fired || !d.resetBtn.contains(x, y) { d.holdStart, d.holdFrac = time.Time{}, 0 return false } if d.holdStart.IsZero() { d.holdStart = now } d.holdFrac = now.Sub(d.holdStart).Seconds() / holdDuration.Seconds() if d.holdFrac < 1 { return false } d.holdFrac, d.fired, d.holdStart = 0, true, time.Time{} return true } // 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.roundRect(r.x, r.y, r.w, r.h, chipRadius, uiCyan) d.fb.roundRect(r.x+chipBorder, r.y+chipBorder, r.w-2*chipBorder, r.h-2*chipBorder, chipRadius-chipBorder, uiBg) split := r.x + chipBorder if d.holdFrac > 0 { w := int(float64(r.w-2*chipBorder) * math.Min(d.holdFrac, 1)) d.fb.roundRect(r.x+chipBorder, r.y+chipBorder, w, r.h-2*chipBorder, chipRadius-chipBorder, uiCyan) split += w } label := "RESET" lx := r.x + (r.w-len(label)*d.gridB.cellW)/2 ly := r.y + (r.h-d.gridB.lineH)/2 - d.gridB.capTop // The label straddles the fill, so each glyph takes the colour that reads // against whatever is behind it. for i, c := range label { gx := lx + i*d.gridB.cellW col := uiCyan if gx+d.gridB.cellW/2 < split { col = uiBg } d.gridB.draw(d.fb, gx, ly, string(c), col) } } func (d *display) close() { d.fb.close() } // y is the top of the line as read, not the top of the cell, so text and a // bordered box placed the same distance apart look it. func (d *display) centerIn(f *textFace, x, w, y int, s string, col rgb) int { f.draw(d.fb, x+(w-len([]rune(s))*f.cellW)/2, y-f.capTop, s, col) return y + f.lineH + pairGap } type statCell struct { value string label string col rgb } func (d *display) statPairH(vf *textFace) int { return vf.lineH + pairGap + d.grid.lineH } func gridRows(n int) int { return (n + gridCols - 1) / gridCols } func (d *display) statsH(vf *textFace, n int) int { return gridRows(n)*(d.statPairH(vf)+statRowGap) - statRowGap } // A last row that does not fill the grid is centred. func gridCell(i, n, x, w int) (cx, cw int) { cw = (w - (gridCols-1)*chipGap) / gridCols inRow := min(n-(i/gridCols)*gridCols, gridCols) cx = x + (w-(inRow*cw+(inRow-1)*chipGap))/2 + (i%gridCols)*(cw+chipGap) return cx, cw } // An empty value takes its space without drawing, so nothing below it moves // when it arrives. func (d *display) stats(vf *textFace, x, w, y int, cells []statCell) int { for i, c := range cells { if c.value == "" { continue } cx, cw := gridCell(i, len(cells), x, w) cy := y + (i/gridCols)*(d.statPairH(vf)+statRowGap) ly := d.centerIn(vf, cx, cw, cy, c.value, c.col) d.centerIn(d.grid, cx, cw, ly, c.label, uiDim) } return y + d.statsH(vf, len(cells)) } var errRows = []struct { label string get func(errs) uint64 }{ {"lost", func(e errs) uint64 { return e.lost }}, {"corrupt", func(e errs) uint64 { return e.corrupt }}, {"link", func(e errs) uint64 { return e.link }}, {"internal", func(e errs) uint64 { return e.internal }}, } func (d *display) chipH() int { return d.grid.lineH + 2*chipPadY } func (d *display) countChipH() int { return d.chipH() + d.gridB.lineH + pairGap } func (d *display) chipsH() int { return gridRows(len(errRows))*(d.chipH()+chipGap) - chipGap } func (d *display) countsH() int { return gridRows(len(errRows))*(d.countChipH()+chipGap) - chipGap } // Outlined by drawing the border colour and sinking a smaller well of // background into it, so both curves get the same antialiasing. func (d *display) chipAt(i, x, w, y, h int, c rgb) (int, int, int) { cx, cw := gridCell(i, len(errRows), x, w) cy := y + (i/gridCols)*(h+chipGap) d.fb.roundRect(cx, cy, cw, h, chipRadius, c) d.fb.roundRect(cx+chipBorder, cy+chipBorder, cw-2*chipBorder, h-2*chipBorder, chipRadius-chipBorder, uiBg) return cx, cw, cy } // Whether rather than how many: over a window this short a count changes faster // than it can be read. func (d *display) errChips(x, w, y int, e errs) int { for i, r := range errRows { c := errColor(r.get(e)) cx, cw, cy := d.chipAt(i, x, w, y, d.chipH(), c) d.centerIn(d.grid, cx, cw, cy+chipPadY, r.label, c) } return y + d.chipsH() } func (d *display) errCounts(x, w, y int, e errs) int { for i, r := range errRows { n := r.get(e) c := errColor(n) cx, cw, cy := d.chipAt(i, x, w, y, d.countChipH(), c) ty := d.centerIn(d.gridB, cx, cw, cy+chipPadY, commas(n), c) d.centerIn(d.grid, cx, cw, ty, r.label, c) } return y + d.countsH() } func (d *display) panel(p rect, e errs) (int, int) { fill, edge := uiOKFill, uiOKEdge if e.total() > 0 { fill, edge = uiErrFil, uiErrEdg } d.fb.rect(p.x, p.y, p.w, p.h, edge) d.fb.rect(p.x+uiBorder, p.y+uiBorder, p.w-2*uiBorder, p.h-2*uiBorder, fill) inset := uiBorder + uiPad return p.x + inset, p.w - 2*inset } func errColor(n uint64) rgb { if n == 0 { return uiGreen } return uiRed } func (d *display) render(v view, elapsed time.Duration, cable string) error { fb := d.fb fb.fill(uiBg) x, w := d.panel(d.nowPanel, v.window) d.stats(d.big, x, w, d.nowYs[0], []statCell{ {scaleSI(v.rxGbps * 1e9), "bits/s", uiFg}, {scaleSI(v.rxPPS), "packets/s", uiFg}, }) d.errChips(x, w, d.nowYs[1], v.window) x, w = d.panel(d.sincePanel, v.since) d.stats(d.gridB, x, w, d.sinceYs[0], []statCell{ {scaleTime(elapsed), "elapsed", uiFg}, {scaleSI(float64(v.rxFrames)), "packets", uiFg}, {scaleSI(float64(v.rxGot)), "bytes", uiFg}, {cable, "m", uiFg}, }) d.errCounts(x, w, d.sinceYs[1], v.since) d.drawResetButton() return fb.flush() }