Add canvas, dither, sprites, clock and pixel elements

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 16:02:31 -04:00
1 parent 538031af31
commit d33bf0cbbb
11 files changed
+1274 -11

No files matched your search

+242
View File
@@ -0,0 +1,242 @@
package pixel_test
import (
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestSpriteLettersMatchSlots(t *testing.T) {
sp := pixel.MustSprite("kbflsenwaAigyrcm")
d := canvas(t, 16, 1, func(g pixel.Canvas) { g.Sprite(0, 0, sp, 0, pixel.Ink, 1) })
for i := range 16 {
if got := d.at(i, 0); got != pixel.Slot(i) {
t.Fatalf("letter %d drew slot %d", i, got)
}
}
}
func TestSpriteFramesMirrorScale(t *testing.T) {
sp := pixel.MustSprite(`
#a.
..g
.r.
.r.
`)
if sp.W != 3 || sp.H != 2 || sp.Frames() != 2 {
t.Fatalf("sprite %d×%d with %d frames", sp.W, sp.H, sp.Frames())
}
d := canvas(t, 6, 4, func(g pixel.Canvas) {
g.Sprite(0, 0, sp, 0, pixel.Magic, 1)
g.Sprite(3, 0, sp, 3, pixel.Ink, 1)
g.Sprite(0, 2, sp.Mirror(), 0, pixel.Magic, 1)
})
checks := map[[2]int]pixel.Slot{
{0, 0}: pixel.Magic, {1, 0}: pixel.Accent, {2, 0}: pixel.Back, {2, 1}: pixel.Good,
{4, 0}: pixel.Bad, {4, 1}: pixel.Bad, {3, 0}: pixel.Back,
{2, 2}: pixel.Magic, {1, 2}: pixel.Accent, {0, 3}: pixel.Good,
}
for p, want := range checks {
if got := d.at(p[0], p[1]); got != want {
t.Fatalf("dot %v = %d, want %d", p, got, want)
}
}
big := canvas(t, 6, 4, func(g pixel.Canvas) { g.Sprite(0, 0, sp, 0, pixel.Magic, 2) })
if big.count(0, 0, 2, 2, pixel.Magic) != 4 || big.count(2, 0, 2, 2, pixel.Accent) != 4 {
t.Fatal("scale 2 must fill 2×2 dots per art pixel")
}
for name, art := range map[string]string{"ragged": "ab\nabc", "letter": "az", "empty": " ", "frames": "aa\n\naa\naa"} {
mustPanic(t, name, func() { pixel.MustSprite(art) })
}
}
func TestClock(t *testing.T) {
at := time.Unix(1_700_000_001, 250_000_000)
var step, busy, tick int
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return at }})
k := pixel.K(c)
tick = k.Tick(c, 8)
ui.Box(c).Size(4, 4).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
step, busy = g.Step(4), g.Busy(4)
})
}, 10, 10)
if want := int(at.UnixNano() / int64(time.Second/4)); step != want || busy != want {
t.Fatalf("Step %d Busy %d, want %d", step, busy, want)
}
if want := int(at.UnixNano() / int64(time.Second/8)); tick != want {
t.Fatalf("Tick %d, want %d", tick, want)
}
pixel.ResetDue()
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
if step != 0 || busy == 0 {
t.Fatalf("under Reduce Motion: Step %d (want 0), Busy %d", step, busy)
}
if due := time.Until(pixel.Due()); due <= 0 || due > 125*time.Millisecond {
t.Fatalf("Due in %v, want the 8 fps wait on the real clock", due)
}
pixel.ResetDue()
if !pixel.Due().IsZero() {
t.Fatal("ResetDue left a deadline")
}
mustPanic(t, "fps 0", func() {
ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay})
pixel.K(c).Tick(c, 0)
}, 10, 10)
})
mustPanic(t, "Step 0 under Reduce Motion", func() {
on := false
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay})
k := pixel.K(c)
ui.Box(c).Size(4, 4).Draw(func(p *ui.Painter, r ui.Rect) {
if on {
k.Canvas(p, r).Step(0)
}
})
}, 10, 10)
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
on = true
tt.Frame()
})
}
func TestWrapFollowsSameFrameResize(t *testing.T) {
const s = "像素风格组件库,每个点都经过认真摆放,在限制中创造丰富。"
width := float32(120)
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Dot: 2})
ui.Column(c).Width(width).Children(func() {
pixel.Text(c, s, pixel.TextOptions{Wrap: true})
})
}, 400, 800)
for _, w := range []float32{60, 200, 120} {
width = w
tt.Frame()
r, _ := tt.Find(s)
if want := float32(len(pixel.Lines(s, int(w/2))) * 16 * 2); r.H != want {
t.Fatalf("width %v: text height %v, want %v", w, r.H, want)
}
}
}
func TestTextElement(t *testing.T) {
var width float32
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Dot: 2})
pixel.Text(c, "HELLO", pixel.TextOptions{Scale: 2})
ui.Column(c).Width(120).Children(func() {
pixel.Text(c, "像素风格组件库,每个点都经过认真摆放。", pixel.TextOptions{Wrap: true})
})
pixel.Text(c, "one\ntwo\nthree", pixel.TextOptions{MaxLines: 2})
width = float32(pixel.Measure("HELLO") * 2 * 2)
}, 400, 400)
r, ok := tt.Find("HELLO")
if !ok || r.W != width || r.H != 2*16*2 {
t.Fatalf("HELLO box %v (found %v), want %v×64", r, ok, width)
}
r, _ = tt.Find("像素风格组件库,每个点都经过认真摆放。")
if lines := len(pixel.Lines("像素风格组件库,每个点都经过认真摆放。", 60)); r.H != float32(lines*16*2) || lines < 3 {
t.Fatalf("wrapped box %v, want %d lines", r, lines)
}
if r, _ = tt.Find("one\ntwo\nthree"); r.H != 2*16*2 {
t.Fatalf("MaxLines box height %v, want 64", r.H)
}
}
func TestIconElement(t *testing.T) {
sp := pixel.MustSprite("aa\naa\naa")
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Dot: 2})
pixel.Icon(c, sp, pixel.IconOptions{Scale: 3, Label: "block"})
pixel.Icon(c, sp, pixel.IconOptions{})
}, 100, 100)
r, ok := tt.Find("block")
if !ok || r.W != 2*3*2 || r.H != 3*3*2 {
t.Fatalf("icon box %v found %v, want 12×18", r, ok)
}
if len(tt.Texts()) != 1 {
t.Fatalf("unlabeled icon is visible to assistive technology: %q", tt.Texts())
}
}
func shellView(disabled *bool, clicks *int, st *pixel.State) func(c *ui.Context) {
at := time.Unix(1_700_000_001, 250_000_000)
return func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SummerDay, Dot: 2, Now: func() time.Time { return at }})
k := pixel.K(c)
ui.Box(c).Size(k.D(16), k.D(4))
ui.Row(c).Children(func() {
ui.Box(c).Size(k.D(16), k.D(8))
shell, base, s := pixel.Shell(c, func() *ui.Element {
return ui.ButtonBase(c).Label("go").Disabled(*disabled)
})
*st = s
if base.Clicked() {
*clicks++
}
shell.Size(k.D(12), k.D(8)).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Rect(0, 0, 12, 8, pixel.Accent)
if s.Focus {
g.FocusMark(0, 0, 12, 8)
}
})
})
}
}
func TestShellKeepsLookAndBehavior(t *testing.T) {
disabled, clicks, st := false, 0, pixel.State{}
tt := ui.NewTester(shellView(&disabled, &clicks, &st), 100, 100)
tt.ClickAt(40, 16)
if clicks != 1 {
t.Fatalf("enabled shell: %d clicks", clicks)
}
tt.Key(0, ui.KeyTab)
tt.Frame()
if !st.Focus {
t.Fatal("Tab did not give the base visible focus")
}
d := dots{t: t, img: tt.Image(), pal: pixel.SummerDay}
if d.at(14, 2) != pixel.Accent || d.at(15, 2) != pixel.Back || d.at(29, 13) != pixel.Accent {
t.Fatal("focus ring dots missing around the shell")
}
if d.count(3, 5, 12, 6, pixel.Edge) == 0 || d.count(3, 5, 12, 6, pixel.Lit) == 0 {
t.Fatal("hand cursor missing left of the shell")
}
disabled = true
tt.Frame()
tt.ClickAt(40, 16)
if clicks != 1 || !st.Disabled || st.Focus {
t.Fatalf("disabled: clicks %d, state %+v", clicks, st)
}
d = dots{t: t, img: tt.Image(), pal: pixel.SummerDay}
if d.count(16, 4, 12, 8, pixel.Accent) != 96 {
t.Fatal("disabled base dimmed the shell's look")
}
}
func TestVeilAnchorsAcrossElements(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SummerDay, Dot: 2})
k := pixel.K(c)
ui.Box(c).Size(k.D(1), k.D(1))
ui.Row(c).Children(func() {
ui.Box(c).Size(k.D(3), k.D(2))
ui.Box(c).Size(k.D(4), k.D(2)).Draw(func(p *ui.Painter, r ui.Rect) {
k.Canvas(p, r).Checker(0, 0, 4, 2, pixel.Ink)
})
})
}, 14, 6)
d := dots{t: t, img: tt.Image(), pal: pixel.SummerDay}
if d.at(3, 1) != pixel.Ink || d.at(4, 1) != pixel.Back || d.at(3, 2) != pixel.Back || d.at(4, 2) != pixel.Ink {
t.Fatal("checker phase follows the element, not the window")
}
}