Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 52 additions & 5 deletions pixel.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import "github.com/go-gfx/gfx/vector"
// widget migrated to Painter renders identically to today's toolkit
// output.
type PixelPainter struct {
// textScale magnifies the built-in bitmap font; 0 and 1 both mean 1:1.
textScale int
// Buf is the destination RGBA byte slice (4 bytes per pixel).
// The buffer is written in place; callers own its lifecycle.
Buf []byte
Expand Down Expand Up @@ -257,22 +259,67 @@ func (p *PixelPainter) blendInto(off int, c RGBA) {
p.Buf[off+3] = uint8(a + uint32(p.Buf[off+3])*ia/255)
}

// Text paints s at (x, y) using the built-in 5×7 bitmap font (see
// font.go). Each glyph is 5 columns × 7 rows + 1 pixel of inter-
// glyph spacing (advance = 6).
// SetTextScale magnifies the built-in font by a whole number.
//
// The font is 5×7 PIXELS and was drawn at that size whatever the display: on a
// screen with two device pixels to the point, a glyph is three and a half
// points tall, which is not small type -- it is type nobody can read. Every
// other metric in the toolkit above this passes through a HiDPI scale; the one
// thing a person actually reads did not.
//
// A whole number, because a bitmap font magnified by 1.5 has rows of unequal
// thickness and reads worse than the smaller size it came from. Anything below
// 1 is 1.
func (p *PixelPainter) SetTextScale(n int) {
if n < 1 {
n = 1
}
p.textScale = n
}

// TextScale is the magnification the built-in font is drawn at.
func (p *PixelPainter) TextScale() int {
if p.textScale < 1 {
return 1
}
return p.textScale
}

// TextWidth is how wide s will be drawn, so a caller can right-align it or
// decide it does not fit. It was arithmetic every caller did for itself, with
// the advance hard-coded, which is a number that has just changed.
func (p *PixelPainter) TextWidth(s string) int {
return len(s) * glyphAdvance * p.TextScale()
}

// TextHeight is how tall a line of the built-in font is drawn.
func (p *PixelPainter) TextHeight() int { return glyphHeight * p.TextScale() }

// Text paints s at (x, y) using the built-in 5×7 bitmap font (see font.go).
// Each glyph is 5 columns × 7 rows + 1 pixel of inter-glyph spacing (advance =
// 6), magnified by [PixelPainter.TextScale].
func (p *PixelPainter) Text(x, y int, s string, ink RGBA) {
n := p.TextScale()
for k := 0; k < len(s); k++ {
bits, ok := font5x7[s[k]]
if !ok {
continue
}
gx := x + k*glyphAdvance
gx := x + k*glyphAdvance*n
for col := 0; col < 5; col++ {
cb := bits[col]
for row := 0; row < glyphHeight; row++ {
if cb&(1<<row) != 0 {
if cb&(1<<row) == 0 {
continue
}
if n == 1 {
p.PutPixel(gx+col, y+row, ink)
continue
}
// One lit bit becomes an n×n block, which is what magnifying a
// bitmap font means. Drawn as a rect rather than n² PutPixel
// calls: a window of text is tens of thousands of bits.
p.FillRect(Rect{X: gx + col*n, Y: y + row*n, W: n, H: n}, ink)
}
}
}
Expand Down
64 changes: 64 additions & 0 deletions pixel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,67 @@ func TestABGRAPainterWritesBlueWhereRedGoes(t *testing.T) {
t.Error("blended: nothing landed in the blue byte, so the swap missed the blend path")
}
}

// TestTheFontCanBeMagnified covers the one thing in this toolkit a person
// actually reads.
//
// The font is 5×7 PIXELS and was drawn at that size whatever the display: on a
// screen with two device pixels to the point, a glyph is three and a half
// points tall — not small type, type nobody can read. Every other metric above
// this passes through a HiDPI scale; this did not.
func TestTheFontCanBeMagnified(t *testing.T) {
lit := func(p *PixelPainter, w, h int) int {
n := 0
for i := 3; i < len(p.Buf); i += 4 {
if p.Buf[i] != 0 {
n++
}
}
return n
}

const w, h = 200, 60
one := NewPixelPainter(make([]byte, w*h*4), w, h)
one.Text(2, 2, "AB", RGBA{R: 255, A: 255})
small := lit(one, w, h)
if small == 0 {
t.Fatal("nothing was drawn at 1:1")
}

three := NewPixelPainter(make([]byte, w*h*4), w, h)
three.SetTextScale(3)
three.Text(2, 2, "AB", RGBA{R: 255, A: 255})
big := lit(three, w, h)

// One lit bit becomes an n×n block, so nine times the ink for a scale of
// three. Exactly, because the glyphs are the same and nothing is clipped.
if big != small*9 {
t.Errorf("at scale 3 the text lit %d pixels, want %d (9× %d)", big, small*9, small)
}
if got := three.TextScale(); got != 3 {
t.Errorf("TextScale = %d, want 3", got)
}
// Measuring must follow the scale, or every caller right-aligning text
// puts it in the wrong place.
if got, want := three.TextWidth("AB"), 2*glyphAdvance*3; got != want {
t.Errorf("TextWidth = %d, want %d", got, want)
}
if got, want := three.TextHeight(), glyphHeight*3; got != want {
t.Errorf("TextHeight = %d, want %d", got, want)
}

// A painter nobody configured draws as it always did: the zero value is
// 1:1, which is what every existing caller expects.
if got := one.TextScale(); got != 1 {
t.Errorf("an unconfigured painter reports scale %d", got)
}
// A scale below one is one, not an invisible line of text.
one.SetTextScale(0)
if got := one.TextScale(); got != 1 {
t.Errorf("scale 0 became %d", got)
}
one.SetTextScale(-4)
if got := one.TextScale(); got != 1 {
t.Errorf("a negative scale became %d", got)
}
}