From 84f4eea5a5ee991b2e64f56d7092b771894c3609 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 30 Aug 2026 22:50:00 +0200 Subject: [PATCH 1/2] toolkit: a dot that says live A status light is the smallest thing a picture can say -- "this is on", "this is recording", "this is connected" -- and every caller that wanted one so far had to reach past the toolkit and call the painter itself. That is exactly what the desk's own barrier test refuses, and rightly: the rule is that a capability the toolkit lacks gets ADDED to the toolkit, not hand-drawn at the call site. DrawIconDot fills the rectangle it is given, less the inset every icon here already leaves, with a round rectangle whose radius is half its shorter side -- which IS a disc, drawn with the same antialiasing as the rest of the package. Nothing rasterises a circle by hand. The test judges roundness by the CORNERS: a square inks all four, a disc none. Area does not say it as plainly -- an anti-aliased disc touches about 85% of its bounding box where the disc itself is pi/4 of it -- and a filled rectangle of the same size is drawn alongside as a negative control, so a DrawIconDot that quietly lost its radius fails rather than passes. Below ten pixels the corner check is skipped and said to be skipped: a disc four pixels across touches all sixteen of them, corners included, so nothing there separates it from a square. --- icongridvector_test.go | 89 +++++++++++++++++++++++++++++++++++++++++- icons.go | 28 +++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/icongridvector_test.go b/icongridvector_test.go index 0e6e6bd..34fa439 100644 --- a/icongridvector_test.go +++ b/icongridvector_test.go @@ -386,11 +386,17 @@ func TestThePlusIsSymmetric(t *testing.T) { ink := RGB(0xFF, 0x00, 0x00) DrawIconPlus(newP(buf, w), box, ink) + // Anything that is not the surface as it was: a disc's rim is + // anti-aliased, so counting only the pixels that came out exactly + // the colour measures the square inscribed in it -- which is how the + // first version of this test called a disc a square -- and counting + // "any green" counts a background that is not black. + bg := pixelAt(buf, w, 0, 0) minX, minY, maxX, maxY := w, h, -1, -1 painted := 0 for y := range h { for x := range w { - if pixelAt(buf, w, x, y) != ink { + if pixelAt(buf, w, x, y) == bg { continue } painted++ @@ -430,3 +436,84 @@ func TestThePlusIsSymmetric(t *testing.T) { small := makeSurface(12, 12) DrawIconPlus(newP(small, 12), Rect{X: 0, Y: 0, W: 8, H: 8}, RGB(0xFF, 0, 0)) } + +// TestTheDotIsRoundAndCentred. +// +// A status light is read at a glance and often out of the corner of an eye, so +// what matters is that it is a disc where it was asked for and not a square, +// and that it stays inside its box: a dot drawn over another icon that leaked +// a pixel would smear the glyph under it. +func TestTheDotIsRoundAndCentred(t *testing.T) { + for _, side := range []int{8, 11, 16, 24, 40} { + w, h := side+8, side+8 + buf := makeSurface(w, h) + box := Rect{X: 4, Y: 4, W: side, H: side} + DrawIconDot(newP(buf, w), box, RGB(0x00, 0xFF, 0x00)) + + // Everything the dot TOUCHED, not only what came out exactly the + // colour: the rim of a disc is anti-aliased, and the fully covered + // pixels are a smaller disc drawn on a coarser grid -- 88% of its own + // box at 16 pixels, which barely tells a disc from a square. What the + // ink reached does: it is the disc itself. + bg := pixelAt(buf, w, 0, 0) + minX, minY, maxX, maxY := w, h, -1, -1 + painted := 0 + for y := range h { + for x := range w { + if pixelAt(buf, w, x, y) == bg { + continue + } + painted++ + minX, minY = min(minX, x), min(minY, y) + maxX, maxY = max(maxX, x), max(maxY, y) + } + } + if painted == 0 { + t.Errorf("side %d: nothing was drawn", side) + continue + } + // Inside its box, always: the inset every icon here leaves is the + // margin, and a dot that overflowed would smear whatever it sits on. + if minX < box.X || minY < box.Y || maxX >= box.X+box.W || maxY >= box.Y+box.H { + t.Errorf("side %d: ink spans %d..%d,%d..%d, outside the box at %d,%d %dx%d", + side, minX, maxX, minY, maxY, box.X, box.Y, box.W, box.H) + } + // Square in a square box, to the pixel. + if (maxX - minX) != (maxY - minY) { + t.Errorf("side %d: the ink is %dx%d; a dot in a square box is square", + side, maxX-minX+1, maxY-minY+1) + } + // And ROUND, not the square it would be without a radius. The corners + // are the test: a square inks all four of them, a disc none. Area does + // not say it as plainly -- an anti-aliased disc TOUCHES about 85% of + // its box, where the disc itself is pi/4 of it, about 79%. + // + // Only where there is room to tell. A disc four pixels across touches + // all sixteen of them, corners included, so nothing separates it from a + // square there, and asserting otherwise would assert something untrue + // of a dot at a real size. + if maxX-minX+1 < 10 { + continue + } + for _, c := range [][2]int{{minX, minY}, {maxX, minY}, {minX, maxY}, {maxX, maxY}} { + if pixelAt(buf, w, c[0], c[1]) != bg { + t.Errorf("side %d: the corner at %d,%d is inked; a disc leaves its box's corners alone", + side, c[0], c[1]) + } + } + } + // The corner check discriminates: a plain rectangle of the same size inks + // the very corners the dot leaves alone. Without this, a DrawIconDot that + // quietly lost its radius would pass everything above. + sq := makeSurface(48, 48) + bg := pixelAt(sq, 48, 0, 0) + newP(sq, 48).FillRect(Rect{X: 4, Y: 4, W: 40, H: 40}, RGB(0x00, 0xFF, 0x00)) + if pixelAt(sq, 48, 4, 4) == bg { + t.Error("a filled rectangle left its corner blank; the corner check above proves nothing") + } + + // A box with no room draws nothing rather than a stray pixel. + tiny := makeSurface(4, 4) + DrawIconDot(newP(tiny, 4), Rect{X: 0, Y: 0, W: 1, H: 1}, RGB(0xFF, 0, 0)) + DrawIconDot(newP(tiny, 4), Rect{X: 0, Y: 0, W: 4, H: 4}, RGB(0xFF, 0, 0)) +} diff --git a/icons.go b/icons.go index 80d0253..3140093 100644 --- a/icons.go +++ b/icons.go @@ -316,3 +316,31 @@ func DrawIconPlus(p painter.Painter, r Rect, ink RGBA) { fillRect(p, x+off, y, thick, side, ink) fillRect(p, x, y+off, side, thick, ink) } + +// DrawIconDot paints a filled disc: the smallest thing a picture can say. +// +// It is what a status LIGHT is — "this is live", "this is recording", "this is +// connected" — put on top of another icon or beside a row of them. A dot rather +// than a shape change, because a colour is read at a glance where a different +// outline has to be looked at twice, and because the one place these appear +// most is a menu bar somebody is not looking at yet. +// +// It fills the rectangle it is given, less the inset every icon here leaves, so +// a caller sizes it by sizing the rectangle. Round in a square box and an +// ellipse in any other: a caller asking for a wide box means a wide dot. +func DrawIconDot(p painter.Painter, r Rect, ink RGBA) { + inset := iconInset(r) + x, y := r.X+inset, r.Y+inset + w, h := r.W-2*inset, r.H-2*inset + if w < 1 || h < 1 { + return + } + // A round rectangle whose radius is half its shorter side IS a disc, and + // the painter already draws those with the antialiasing the rest of this + // package gets. Nothing here rasterises a circle by hand. + radius := w + if h < radius { + radius = h + } + p.FillRoundRect(Rect{X: x, Y: y, W: w, H: h}, radius/2, ink) +} From eb1a4d34956de3b7596262ed76029785b8b35f29 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 30 Aug 2026 22:52:12 +0200 Subject: [PATCH 2/2] toolkit: cover the dot in a box that is not square The radius follows the SHORTER side, and no test passed a box where the two differ -- so the line that picks it was never taken. A wide box and a tall one, each asserted to give a dot the same way round as its box. --- icongridvector_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/icongridvector_test.go b/icongridvector_test.go index 34fa439..1ea6b06 100644 --- a/icongridvector_test.go +++ b/icongridvector_test.go @@ -512,6 +512,28 @@ func TestTheDotIsRoundAndCentred(t *testing.T) { t.Error("a filled rectangle left its corner blank; the corner check above proves nothing") } + // A box that is not square gives a dot that is not round: a caller asking + // for a wide box means a wide dot, and the radius follows the SHORTER side + // so the ends stay half-circles instead of overshooting into the long one. + for _, box := range []Rect{{X: 4, Y: 4, W: 40, H: 16}, {X: 4, Y: 4, W: 16, H: 40}} { + buf := makeSurface(48, 48) + bg := pixelAt(buf, 48, 0, 0) + DrawIconDot(newP(buf, 48), box, RGB(0x00, 0xFF, 0x00)) + minX, minY, maxX, maxY := 48, 48, -1, -1 + for y := range 48 { + for x := range 48 { + if pixelAt(buf, 48, x, y) != bg { + minX, minY = min(minX, x), min(minY, y) + maxX, maxY = max(maxX, x), max(maxY, y) + } + } + } + gotW, gotH := maxX-minX+1, maxY-minY+1 + if wantWide := box.W > box.H; wantWide != (gotW > gotH) { + t.Errorf("a %dx%d box gave a %dx%d dot", box.W, box.H, gotW, gotH) + } + } + // A box with no room draws nothing rather than a stray pixel. tiny := makeSurface(4, 4) DrawIconDot(newP(tiny, 4), Rect{X: 0, Y: 0, W: 1, H: 1}, RGB(0xFF, 0, 0))