Add palettes and frame settings
This commit is contained in:
7 files changed
+542
-1
No files matched your search
@@ -54,6 +54,7 @@ Imports point down only. Same-tier imports are forbidden. `internal/archtest` en
|
||||
|
||||
- Signatures and results: PRD §5.1. Zero options give the default look. Sizes in options are dots.
|
||||
- `k := pixel.K(c)` once per component; layout sizes through `k.D(n)`.
|
||||
- Row builders that MyGo runs during layout (`List`, `GridView`, `Outline`) go through `pixel.Lazy(c, row)`.
|
||||
- Copy shown by a component comes from `k.Msg`; business copy comes from the caller.
|
||||
- Fail fast: panic on programmer errors such as bad options. Never swallow an error.
|
||||
- Composites log state changes: `slog.Debug("8bit: <component> <event>", …)`. Never log secrets.
|
||||
|
||||
@@ -2,7 +2,10 @@ module git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI
|
||||
|
||||
go 1.27.1
|
||||
|
||||
require github.com/egoist/mygo v0.2.10
|
||||
|
||||
require (
|
||||
github.com/ebitengine/purego v0.11.1 // indirect
|
||||
github.com/egoist/mygo v0.2.10 // indirect
|
||||
github.com/go-text/typesetting v0.3.5 // indirect
|
||||
golang.org/x/image v0.46.0 // indirect
|
||||
)
|
||||
@@ -2,3 +2,9 @@ github.com/ebitengine/purego v0.11.1 h1:2zpWRSQNVKN4eKsKO9eM1ILDgWfYMY9GwqRmK6Xe
|
||||
github.com/ebitengine/purego v0.11.1/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
|
||||
github.com/egoist/mygo v0.2.10 h1:Mfe6SpQngC9kLjrejpGM7L/k3Q62BdBAyJ8MrHfL6bQ=
|
||||
github.com/egoist/mygo v0.2.10/go.mod h1:1sRzZ6jEhKE7iuEQXjyoXDgxyxQ1uTgMHDcDKZvoDIU=
|
||||
github.com/go-text/typesetting v0.3.5 h1:XZPUooClHY0Vf/rFyUyuPRNEkawARaFzLMQcXLSEyPk=
|
||||
github.com/go-text/typesetting v0.3.5/go.mod h1:XZO1hD+nQVyvVa5IicQk7FsCa4PFQaJ2soWAP1f//68=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc h1:8FGo2It5K75XkavhTiCKExUfVaVDS1feBnLCru5qeoY=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
|
||||
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||
@@ -0,0 +1,98 @@
|
||||
// Package pixel draws 16-bit pixel art for MyGo.
|
||||
package pixel
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
// Slot names a palette color by its role.
|
||||
type Slot uint8
|
||||
|
||||
const (
|
||||
Back Slot = iota
|
||||
Face
|
||||
Lit
|
||||
Shade
|
||||
Edge
|
||||
Ink
|
||||
Faint
|
||||
Well
|
||||
Accent
|
||||
AccentLit
|
||||
AccentInk
|
||||
Good
|
||||
Warn
|
||||
Bad
|
||||
Info
|
||||
Magic
|
||||
)
|
||||
|
||||
const Slots = 16
|
||||
|
||||
type Season uint8
|
||||
|
||||
const (
|
||||
Spring Season = iota
|
||||
Summer
|
||||
Autumn
|
||||
Winter
|
||||
)
|
||||
|
||||
type Palette struct {
|
||||
Name, Zh string
|
||||
Season Season
|
||||
Night bool
|
||||
Colors [Slots]ui.Color
|
||||
}
|
||||
|
||||
func (p *Palette) C(s Slot) ui.Color { return p.Colors[s] }
|
||||
|
||||
var (
|
||||
SpringDay = palette("Spring Day", "春·昼", Spring, false, "F4EEDC E4DCC6 FFFBEF B3A68C 3A2A3E 33283A 6F6358 FFF6E6 B8395C F2A7B9 FFFFFF 4F9A3F D9982A C73A3A 3F7FCB 8C5BC4")
|
||||
SpringNight = palette("Spring Night", "春·夜", Spring, true, "1C1730 2E2850 4C4478 1A1530 E8DFF7 F3EEFF A79DC4 14102A F07EA0 FFC2D2 120E24 7CC66A F2C14E F0605F 6FA8F0 B58AF0")
|
||||
SummerDay = palette("Summer Day", "夏·昼", Summer, false, "F7F3E3 DCEBEE FFFFFF 98B3B8 1D3640 15303B 4F6870 F4FCFF 0E6E8C 7FD0E3 FFFFF6 3A9A4A E0A21A D14836 2F7FD0 9A57C8")
|
||||
SummerNight = palette("Summer Night", "夏·夜", Summer, true, "0B1630 172A55 2C4A86 0A1328 DDEBFF EAF3FF 98ACCF 08102A 36C2E0 A8EEFA 050C1E B8E35A F7D04A F26A5A 5FA8FF C58CFF")
|
||||
AutumnDay = palette("Autumn Day", "秋·昼", Autumn, false, "F2E6D0 E6D2B0 FFF6E2 B08F62 3A2416 33200F 6E5640 FFF1D8 B94A1C F0A060 FFFFFF 6E8F2E C98A12 B8302A 3F78B0 8E4E9E")
|
||||
AutumnNight = palette("Autumn Night", "秋·夜", Autumn, true, "1E1414 33221E 5A3D30 170F0E F1DCC4 FBEBD8 B39A86 140C0B F09A3A FFD08A 0E0808 A8C25A F6C65A F0624E 79A8E0 C88AD8")
|
||||
WinterDay = palette("Winter Day", "冬·昼", Winter, false, "EEF3F8 D8E2EC FFFFFF 98A9BC 1C2A3C 17243A 52637A F6FAFF 1F5FA8 9CC8F0 FCFEFF 3C8C6C B98A1E B23A48 2A86A8 7656B8")
|
||||
WinterNight = palette("Winter Night", "冬·夜", Winter, true, "0A1020 18233F 304674 080C1A E2F2FF EEF7FF 9DB2CF 060A16 7FD8F0 CFF4FF 040814 6FE0A8 F5D47A F07A8A 6FB0FF A994F5")
|
||||
)
|
||||
|
||||
var Palettes = []*Palette{SpringDay, SpringNight, SummerDay, SummerNight, AutumnDay, AutumnNight, WinterDay, WinterNight}
|
||||
|
||||
func Find(s Season, night bool) *Palette {
|
||||
for _, p := range Palettes {
|
||||
if p.Season == s && p.Night == night {
|
||||
return p
|
||||
}
|
||||
}
|
||||
panic("pixel: no palette for season")
|
||||
}
|
||||
|
||||
// Cycle rotates band by step, for color cycling.
|
||||
func Cycle(band []Slot, step int) []Slot {
|
||||
n := len(band)
|
||||
if n == 0 {
|
||||
panic("pixel: Cycle needs a band")
|
||||
}
|
||||
r := (step%n + n) % n
|
||||
out := make([]Slot, n)
|
||||
for i := range band {
|
||||
out[i] = band[(i+r)%n]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func palette(name, zh string, s Season, night bool, hex string) *Palette {
|
||||
f := strings.Fields(hex)
|
||||
if len(f) != Slots {
|
||||
panic("pixel: palette " + name + " needs 16 colors")
|
||||
}
|
||||
p := &Palette{Name: name, Zh: zh, Season: s, Night: night}
|
||||
for i, h := range f {
|
||||
p.Colors[i] = ui.Hex("#" + h)
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package pixel_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func luminance(c ui.Color) float64 {
|
||||
lin := func(v uint8) float64 {
|
||||
s := float64(v) / 255
|
||||
if s <= 0.04045 {
|
||||
return s / 12.92
|
||||
}
|
||||
return math.Pow((s+0.055)/1.055, 2.4)
|
||||
}
|
||||
return 0.2126*lin(c.R) + 0.7152*lin(c.G) + 0.0722*lin(c.B)
|
||||
}
|
||||
|
||||
func contrast(a, b ui.Color) float64 {
|
||||
la, lb := luminance(a), luminance(b)
|
||||
return (max(la, lb) + 0.05) / (min(la, lb) + 0.05)
|
||||
}
|
||||
|
||||
func TestPaletteContrast(t *testing.T) {
|
||||
rules := []struct {
|
||||
fg, bg pixel.Slot
|
||||
min float64
|
||||
}{
|
||||
{pixel.Ink, pixel.Face, 4.5},
|
||||
{pixel.Ink, pixel.Back, 4.5},
|
||||
{pixel.Ink, pixel.Well, 4.5},
|
||||
{pixel.AccentInk, pixel.Accent, 4.5},
|
||||
{pixel.Faint, pixel.Face, 3},
|
||||
{pixel.Edge, pixel.Back, 3},
|
||||
}
|
||||
for _, p := range pixel.Palettes {
|
||||
for _, r := range rules {
|
||||
if got := contrast(p.C(r.fg), p.C(r.bg)); got < r.min {
|
||||
t.Errorf("%s: slot %d on %d contrast %.2f, want ≥ %.1f", p.Name, r.fg, r.bg, got, r.min)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaletteColorsDistinctAndOpaque(t *testing.T) {
|
||||
if len(pixel.Palettes) != 8 {
|
||||
t.Fatalf("got %d palettes, want 8", len(pixel.Palettes))
|
||||
}
|
||||
for _, p := range pixel.Palettes {
|
||||
seen := map[ui.Color]pixel.Slot{}
|
||||
for s, c := range p.Colors {
|
||||
if c.A != 255 {
|
||||
t.Errorf("%s slot %d alpha %d", p.Name, s, c.A)
|
||||
}
|
||||
if prev, ok := seen[c]; ok {
|
||||
t.Errorf("%s slots %d and %d share %v", p.Name, prev, s, c)
|
||||
}
|
||||
seen[c] = pixel.Slot(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaletteBevelOrder(t *testing.T) {
|
||||
for _, p := range pixel.Palettes {
|
||||
face := luminance(p.C(pixel.Face))
|
||||
if luminance(p.C(pixel.Lit)) <= face || luminance(p.C(pixel.Shade)) >= face {
|
||||
t.Errorf("%s: Lit must be lighter and Shade darker than Face", p.Name)
|
||||
}
|
||||
if p.Night != (luminance(p.C(pixel.Back)) < luminance(p.C(pixel.Ink))) {
|
||||
t.Errorf("%s: night palettes are dark, day palettes light", p.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFind(t *testing.T) {
|
||||
seen := map[*pixel.Palette]bool{}
|
||||
for s := pixel.Spring; s <= pixel.Winter; s++ {
|
||||
for _, night := range []bool{false, true} {
|
||||
p := pixel.Find(s, night)
|
||||
if p.Season != s || p.Night != night {
|
||||
t.Errorf("Find(%d, %v) = %s", s, night, p.Name)
|
||||
}
|
||||
seen[p] = true
|
||||
}
|
||||
}
|
||||
if len(seen) != 8 {
|
||||
t.Fatalf("Find reached %d palettes, want 8", len(seen))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCycle(t *testing.T) {
|
||||
band := []pixel.Slot{pixel.Info, pixel.Accent, pixel.Lit}
|
||||
cases := []struct {
|
||||
step int
|
||||
want []pixel.Slot
|
||||
}{
|
||||
{0, []pixel.Slot{pixel.Info, pixel.Accent, pixel.Lit}},
|
||||
{1, []pixel.Slot{pixel.Accent, pixel.Lit, pixel.Info}},
|
||||
{-1, []pixel.Slot{pixel.Lit, pixel.Info, pixel.Accent}},
|
||||
{7, []pixel.Slot{pixel.Accent, pixel.Lit, pixel.Info}},
|
||||
{math.MaxInt, []pixel.Slot{pixel.Accent, pixel.Lit, pixel.Info}},
|
||||
{math.MinInt, []pixel.Slot{pixel.Accent, pixel.Lit, pixel.Info}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := pixel.Cycle(band, c.step); !slices.Equal(got, c.want) {
|
||||
t.Errorf("Cycle(step %d) = %v, want %v", c.step, got, c.want)
|
||||
}
|
||||
}
|
||||
if !slices.Equal(band, []pixel.Slot{pixel.Info, pixel.Accent, pixel.Lit}) {
|
||||
t.Fatalf("Cycle changed its input: %v", band)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package pixel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
type Locale uint8
|
||||
|
||||
const (
|
||||
En Locale = iota
|
||||
Zh
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Palette *Palette
|
||||
// Dot is DIPs per dot, 1–4; zero means 2.
|
||||
Dot int
|
||||
Locale Locale
|
||||
// Now replaces the frame clock, for tests.
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
// Kit is one frame's settings, captured by components.
|
||||
type Kit struct {
|
||||
pal *Palette
|
||||
dot float32
|
||||
locale Locale
|
||||
now time.Time
|
||||
reduce bool
|
||||
}
|
||||
|
||||
func (k Kit) Palette() *Palette { return k.pal }
|
||||
func (k Kit) C(s Slot) ui.Color { return k.pal.Colors[s] }
|
||||
func (k Kit) Dot() float32 { return k.dot }
|
||||
func (k Kit) D(n int) float32 { return float32(n) * k.dot }
|
||||
func (k Kit) Locale() Locale { return k.locale }
|
||||
func (k Kit) Now() time.Time { return k.now }
|
||||
func (k Kit) Reduce() bool { return k.reduce }
|
||||
|
||||
type frame struct {
|
||||
cur Settings
|
||||
at time.Time
|
||||
}
|
||||
|
||||
type frameKey struct{}
|
||||
|
||||
// Use must run once per frame, before any component.
|
||||
func Use(c *ui.Context, s Settings) {
|
||||
s = s.valid()
|
||||
f := frameOf(c)
|
||||
if f.cur.Palette != s.Palette {
|
||||
slog.Debug("8bit: palette", "name", s.Palette.Name)
|
||||
}
|
||||
f.cur, f.at = s, c.Now()
|
||||
c.SetTheme(theme(s.Palette))
|
||||
}
|
||||
|
||||
// Scope builds fn under other settings.
|
||||
func Scope(c *ui.Context, s Settings, fn func()) {
|
||||
f := current(c)
|
||||
prev := f.cur
|
||||
f.cur = s.valid()
|
||||
fn()
|
||||
f.cur = prev
|
||||
}
|
||||
|
||||
// Lazy keeps this scope for rows MyGo builds during layout.
|
||||
func Lazy[T any](c *ui.Context, fn func(T)) func(T) {
|
||||
s := current(c).cur
|
||||
return func(v T) { Scope(c, s, func() { fn(v) }) }
|
||||
}
|
||||
|
||||
func K(c *ui.Context) Kit {
|
||||
s := current(c).cur
|
||||
now := c.Now()
|
||||
if s.Now != nil {
|
||||
now = s.Now()
|
||||
}
|
||||
return Kit{pal: s.Palette, dot: float32(s.Dot), locale: s.Locale, now: now, reduce: c.Preferences().ReduceMotion}
|
||||
}
|
||||
|
||||
func frameOf(c *ui.Context) *frame {
|
||||
return ui.Local(c.Root(), frameKey{}, func() frame { return frame{} })
|
||||
}
|
||||
|
||||
func current(c *ui.Context) *frame {
|
||||
f := frameOf(c)
|
||||
if !f.at.Equal(c.Now()) {
|
||||
panic("pixel: Use was not called in this frame")
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (s Settings) valid() Settings {
|
||||
if s.Palette == nil {
|
||||
panic("pixel: Settings.Palette is nil")
|
||||
}
|
||||
if s.Dot == 0 {
|
||||
s.Dot = 2
|
||||
}
|
||||
if s.Dot < 1 || s.Dot > 4 {
|
||||
panic(fmt.Sprintf("pixel: Settings.Dot %d outside 1–4", s.Dot))
|
||||
}
|
||||
if s.Locale > Zh {
|
||||
panic(fmt.Sprintf("pixel: unknown Locale %d", s.Locale))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// theme keeps MyGo's own drawing inside the palette.
|
||||
func theme(p *Palette) *ui.Theme {
|
||||
t := ui.LightTheme()
|
||||
c := p.Colors
|
||||
t.Dark = p.Night
|
||||
t.Background, t.Surface, t.SurfaceHover, t.SurfacePressed = c[Back], c[Face], c[Lit], c[Shade]
|
||||
t.Border, t.Text, t.TextMuted = c[Edge], c[Ink], c[Faint]
|
||||
t.Accent, t.AccentHover, t.AccentPressed, t.AccentText = c[Accent], c[AccentLit], c[Accent], c[AccentInk]
|
||||
t.Danger, t.Warning, t.Success = c[Bad], c[Warn], c[Good]
|
||||
t.Selection, t.Focus, t.Scrollbar, t.Radius = c[AccentLit], c[Accent], ui.Transparent, 0
|
||||
return t
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package pixel_test
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func mustPanic(t *testing.T, what string, fn func()) {
|
||||
t.Helper()
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatalf("%s: no panic", what)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}
|
||||
|
||||
func TestUseSetsKitAndBackground(t *testing.T) {
|
||||
var k pixel.Kit
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.WinterNight, Dot: 3, Locale: pixel.Zh})
|
||||
k = pixel.K(c)
|
||||
}, 40, 30)
|
||||
if k.Palette() != pixel.WinterNight || k.Dot() != 3 || k.D(5) != 15 || k.Locale() != pixel.Zh {
|
||||
t.Fatalf("kit = palette %s dot %v D(5) %v locale %d", k.Palette().Name, k.Dot(), k.D(5), k.Locale())
|
||||
}
|
||||
if k.C(pixel.Ink) != pixel.WinterNight.C(pixel.Ink) {
|
||||
t.Fatalf("C(Ink) = %v", k.C(pixel.Ink))
|
||||
}
|
||||
back := pixel.WinterNight.C(pixel.Back)
|
||||
want := color.RGBA{back.R, back.G, back.B, 255}
|
||||
if got := tt.Image().RGBAAt(20, 15); got != want {
|
||||
t.Fatalf("window background %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseBridgesTheme(t *testing.T) {
|
||||
for _, p := range []*pixel.Palette{pixel.SummerDay, pixel.AutumnNight} {
|
||||
var th ui.Theme
|
||||
ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: p})
|
||||
th = *c.Theme()
|
||||
}, 10, 10)
|
||||
pairs := []struct {
|
||||
name string
|
||||
got, want ui.Color
|
||||
}{
|
||||
{"Background", th.Background, p.C(pixel.Back)},
|
||||
{"Surface", th.Surface, p.C(pixel.Face)},
|
||||
{"SurfaceHover", th.SurfaceHover, p.C(pixel.Lit)},
|
||||
{"SurfacePressed", th.SurfacePressed, p.C(pixel.Shade)},
|
||||
{"Border", th.Border, p.C(pixel.Edge)},
|
||||
{"Text", th.Text, p.C(pixel.Ink)},
|
||||
{"TextMuted", th.TextMuted, p.C(pixel.Faint)},
|
||||
{"Accent", th.Accent, p.C(pixel.Accent)},
|
||||
{"AccentHover", th.AccentHover, p.C(pixel.AccentLit)},
|
||||
{"AccentPressed", th.AccentPressed, p.C(pixel.Accent)},
|
||||
{"AccentText", th.AccentText, p.C(pixel.AccentInk)},
|
||||
{"Danger", th.Danger, p.C(pixel.Bad)},
|
||||
{"Warning", th.Warning, p.C(pixel.Warn)},
|
||||
{"Success", th.Success, p.C(pixel.Good)},
|
||||
{"Selection", th.Selection, p.C(pixel.AccentLit)},
|
||||
{"Focus", th.Focus, p.C(pixel.Accent)},
|
||||
{"Scrollbar", th.Scrollbar, ui.Transparent},
|
||||
}
|
||||
for _, pr := range pairs {
|
||||
if pr.got != pr.want {
|
||||
t.Errorf("%s: theme %s = %v, want %v", p.Name, pr.name, pr.got, pr.want)
|
||||
}
|
||||
}
|
||||
if th.Radius != 0 || th.Dark != p.Night {
|
||||
t.Errorf("%s: Radius %v Dark %v", p.Name, th.Radius, th.Dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLazyKeepsScopeForLayoutRows(t *testing.T) {
|
||||
seen := map[int]*pixel.Palette{}
|
||||
ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay})
|
||||
pixel.Scope(c, pixel.Settings{Palette: pixel.WinterNight}, func() {
|
||||
ui.List(c, nil, 100, pixel.Lazy(c, func(i int) {
|
||||
seen[i] = pixel.K(c).Palette()
|
||||
ui.Box(c).Height(10)
|
||||
})).Height(200)
|
||||
})
|
||||
}, 100, 220)
|
||||
if len(seen) < 20 {
|
||||
t.Fatalf("list built %d rows, want at least 20", len(seen))
|
||||
}
|
||||
for i, p := range seen {
|
||||
if p != pixel.WinterNight {
|
||||
t.Fatalf("row %d built under %s, want Winter Night", i, p.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDotDefaultsToTwo(t *testing.T) {
|
||||
var k pixel.Kit
|
||||
ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay})
|
||||
k = pixel.K(c)
|
||||
}, 10, 10)
|
||||
if k.Dot() != 2 {
|
||||
t.Fatalf("Dot = %v, want 2", k.Dot())
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopeOverridesAndRestores(t *testing.T) {
|
||||
var inside, after *pixel.Palette
|
||||
var insideDot float32
|
||||
ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.AutumnDay})
|
||||
pixel.Scope(c, pixel.Settings{Palette: pixel.SummerNight, Dot: 4}, func() {
|
||||
k := pixel.K(c)
|
||||
inside, insideDot = k.Palette(), k.Dot()
|
||||
})
|
||||
after = pixel.K(c).Palette()
|
||||
}, 10, 10)
|
||||
if inside != pixel.SummerNight || insideDot != 4 {
|
||||
t.Fatalf("inside Scope: %s dot %v", inside.Name, insideDot)
|
||||
}
|
||||
if after != pixel.AutumnDay {
|
||||
t.Fatalf("after Scope: %s, want Autumn Day", after.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKWithoutUsePanics(t *testing.T) {
|
||||
mustPanic(t, "K before Use", func() {
|
||||
ui.NewTester(func(c *ui.Context) { pixel.K(c) }, 10, 10)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUseIsRequiredEveryFrame(t *testing.T) {
|
||||
skip := false
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
if !skip {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay})
|
||||
}
|
||||
pixel.K(c)
|
||||
}, 10, 10)
|
||||
skip = true
|
||||
time.Sleep(time.Millisecond)
|
||||
mustPanic(t, "frame without Use", tt.Frame)
|
||||
}
|
||||
|
||||
func TestInvalidSettingsPanic(t *testing.T) {
|
||||
bad := map[string]pixel.Settings{
|
||||
"nil palette": {},
|
||||
"dot 5": {Palette: pixel.SpringDay, Dot: 5},
|
||||
"dot -1": {Palette: pixel.SpringDay, Dot: -1},
|
||||
"locale 9": {Palette: pixel.SpringDay, Locale: 9},
|
||||
}
|
||||
for name, s := range bad {
|
||||
mustPanic(t, name, func() {
|
||||
ui.NewTester(func(c *ui.Context) { pixel.Use(c, s) }, 10, 10)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNowOverride(t *testing.T) {
|
||||
fixed := time.Date(2026, 10, 11, 9, 30, 0, 0, time.UTC)
|
||||
var k pixel.Kit
|
||||
ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return fixed }})
|
||||
k = pixel.K(c)
|
||||
}, 10, 10)
|
||||
if !k.Now().Equal(fixed) {
|
||||
t.Fatalf("Now = %v, want %v", k.Now(), fixed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReduceMotionFollowsPreferences(t *testing.T) {
|
||||
var k pixel.Kit
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay})
|
||||
k = pixel.K(c)
|
||||
}, 10, 10)
|
||||
if k.Reduce() {
|
||||
t.Fatal("Reduce on by default")
|
||||
}
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
if !k.Reduce() {
|
||||
t.Fatal("Reduce off after ReduceMotion preference")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user