Audit keyboard, focus, veils, motion and clocks across all 500

A cross-cutting pass, each fix with a test that failed first.

Veils: MyGo still hands a key to a base that turned off this frame,
so every element key now goes through pixel.Shortcut, which an
archtest enforces. Input handlers record and the build applies them
only while enabled; the editor turns away a veil-frame letter or
copy; value bases get a copy and keep only enabled turns. Popups
give a held focus back to their opener when the veil lifts.

Motion: S5 is enforced at the clock. Every Tick, Since, Step,
Toward and Shot runs at 4-15 fps; blinks keep 1 Hz on 4 fps steps,
fast animations step at 15, and drawn time is quantized to frames.

Clocks stepping back: Since holds at zero without overflow, and
k.Elapsed moves a stored start to now, so nothing goes negative,
runs backward or indexes out of range.

Also: logs count typed text instead of holding it, gallery names
use glyphs the font has, removable rows are keyed, components take
their own Options (archtest), and sweeps over all 508 demos check
veiled keys, clicks, wheels, the clock stepping back and names.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-12 16:03:04 -04:00
1 parent 81003b21ee
commit b4b4d67b92
198 files changed
+2708 -422

No files matched your search

+2 -1
View File
@@ -99,7 +99,8 @@ func (a *App) step(d int) {
// logger records a demo's events for the log panel.
func (a *App) logger(e Entry) func(string) {
return func(ev string) {
slog.Debug("8bit: demo", "entry", e.Slug(), "event", ev)
// The panel shows what was typed; logs never do.
slog.Debug("8bit: demo", "entry", e.Slug())
l := append(a.logs[e.Slug()], ev)
a.logs[e.Slug()] = l[max(0, len(l)-8):]
}
+35
View File
@@ -0,0 +1,35 @@
package gallery
import (
"strings"
"testing"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestChatDemosLogNoTypedText(t *testing.T) {
for _, tc := range []struct{ path, field string }{{"/c/323-composer", "Message"}, {"/c/329-mentioninput", "Mention"}} {
a, tt := open(tc.path)
click(t, tt, tc.field)
tt.Type("xyzzy")
tt.Key(0, ui.KeyEnter)
logs := a.logs[tc.path[3:]]
if len(logs) == 0 || strings.Contains(strings.Join(logs, "\n"), "xyzzy") {
t.Errorf("%s logged %q", tc.path, logs)
}
}
}
func TestPaletteInUseSpeaksTheLocale(t *testing.T) {
a, tt := open("/")
click(t, tt, "language")
if a.locale != pixel.Zh {
t.Fatal("no Chinese")
}
name := pixel.Palettes[a.palette].Name + ", 使用中"
if _, ok := tt.Find(name); !ok {
t.Fatalf("no %q in %q", name, tt.Texts())
}
}
+1 -1
View File
@@ -104,7 +104,7 @@ func (a *App) sidebar(c *ui.Context) {
}
func (a *App) statusBar(c *ui.Context) {
layout.StatusBar(c, "/ search · P palette · [ ] prev next", fmt.Sprintf("%d entries", len(Entries())))
layout.StatusBar(c, []string{"/ search · P palette · [ ] prev next", fmt.Sprintf("%d entries", len(Entries()))}, layout.StatusBarOptions{})
}
// press is a custom-drawn chrome tile; true when clicked.
+84
View File
@@ -0,0 +1,84 @@
package gallery
import (
"regexp"
"strings"
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestDemosSurviveTheClockSteppingBack(t *testing.T) {
negative := regexp.MustCompile(`(^|[\s(:])-\d`)
for _, e := range Entries() {
at := time.Date(2026, 10, 12, 12, 0, 0, 0, time.UTC)
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return at }})
ui.Column(c).Fill().Children(func() { e.Demo(c, func(string) {}) })
}, 640, 480)
// Press up to three buttons, run, then fall back.
for _, b := range buttons(tt, 3) {
if r, ok := tt.Find(b); ok {
tt.ClickAt(r.X+r.W/2, r.Y+r.H/2)
}
}
at = at.Add(500 * time.Millisecond)
tt.Frame()
// Negative numbers shown before are content, not faults.
before := map[string]bool{}
for _, s := range tt.Texts() {
before[s] = true
}
for _, d := range []time.Duration{-time.Hour, 300 * time.Millisecond} {
at = at.Add(d)
tt.Frame()
}
for _, s := range tt.Texts() {
if negative.MatchString(s) && !before[s] {
t.Errorf("%s shows %q after the clock stepped back", e.Slug(), s)
}
}
}
}
// buttons names up to n pressable words on screen.
func buttons(tt *ui.Tester, n int) []string {
var out []string
for _, s := range tt.Texts() {
if len(out) < n && len(s) > 1 && s == upper(s) && strings.ContainsAny(s, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
out = append(out, s)
}
}
return out
}
func upper(s string) string {
b := []byte(s)
for i, c := range b {
if c >= 'a' && c <= 'z' {
b[i] = c - 32
}
}
return string(b)
}
func TestPlayingMediaSurvivesTheClockSteppingBack(t *testing.T) {
for _, slug := range []string{"346-audioplayer", "353-cassette", "360-videoframe"} {
e, _ := find(slug)
at := time.Date(2026, 10, 12, 12, 0, 0, 0, time.UTC)
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return at }})
ui.Column(c).Fill().Children(func() { e.Demo(c, func(string) {}) })
}, 640, 480)
if err := tt.Click("PLAY"); err != nil {
t.Fatalf("%s: %v", slug, err)
}
for _, d := range []time.Duration{500 * time.Millisecond, -time.Second, 200 * time.Millisecond} {
at = at.Add(d)
tt.Frame()
}
}
}
+44
View File
@@ -0,0 +1,44 @@
package gallery
import (
"bytes"
"log/slog"
"strings"
"testing"
"github.com/egoist/mygo/ui"
)
func TestDemoEventsKeepTypedTextOutOfLogs(t *testing.T) {
var buf bytes.Buffer
prev := slog.Default()
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})))
defer slog.SetDefault(prev)
// A wrong vault code: four digits, then WRONG CODE.
a, tt := open("/c/094-pininput")
click(t, tt, "Vault code")
tt.Type("9876")
shown := strings.Join(a.logs["094-pininput"], "\n")
if !strings.Contains(shown, "WRONG CODE") || !strings.Contains(buf.String(), "8bit: demo") {
t.Fatalf("no demo event logged: %q", shown)
}
if strings.Contains(buf.String(), "9876") || strings.Contains(shown, "9876") {
t.Fatalf("the code reached a log: %q", shown)
}
}
func TestDemoPanelsShowWhatLogsDoNot(t *testing.T) {
var buf bytes.Buffer
prev := slog.Default()
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})))
defer slog.SetDefault(prev)
a, tt := open("/c/099-editabletext")
click(t, tt, "Hero name")
tt.Key(0, ui.KeyEnter)
tt.Type("Zork")
tt.Key(0, ui.KeyEnter)
shown := strings.Join(a.logs["099-editabletext"], "\n")
if !strings.Contains(shown, "Zork") || strings.Contains(buf.String(), "Zork") {
t.Fatalf("panel %q; the log has the name: %v", shown, strings.Contains(buf.String(), "Zork"))
}
}
+2 -2
View File
@@ -26,8 +26,8 @@ var chartEntries = []Entry{
{225, "Histogram", "直方图", "chart", "Damage rolls in bins. ROLL throws ten more.", demoHistogram},
{226, "ScatterPlot", "散点图", "chart", "Every hit by distance and damage, one 2×2 dot each.", demoScatterPlot},
{227, "BubbleChart", "气泡图", "chart", "Towns by trade and danger, sized by how many live there.", demoBubbleChart},
{228, "Heatmap", "热力图", "chart", "Raids by day and hour. Hot cells dither red.", demoHeatmap},
{229, "CalendarHeatmap", "日历热力图", "chart", "A year of play, a square a day. A commit graph for slimes.", demoCalendarHeatmap},
{228, "Heatmap", "密度图", "chart", "Raids by day and hour. Hot cells dither red.", demoHeatmap},
{229, "CalendarHeatmap", "日历密度图", "chart", "A year of play, a square a day. A commit graph for slimes.", demoCalendarHeatmap},
{230, "Gauge", "仪表", "chart", "Push the throttle; the needle follows.", demoGauge},
}
+7 -5
View File
@@ -3,6 +3,7 @@ package gallery
import (
"fmt"
"time"
"unicode/utf8"
"github.com/egoist/mygo/ui"
@@ -16,7 +17,7 @@ var chatEntries = []Entry{
{323, "Composer", "输入栏", "chat", "Enter sends, Shift+Enter breaks the line. SEND ▶ works too.", demoComposer},
{324, "TypingIndicator", "正在输入", "chat", "Three dots hop while someone types. Suspense!", demoTypingIndicator},
{325, "ChatList", "会话列表", "chat", "Who said what last, and how much you missed.", demoChatList},
{326, "ReadReceipt", "已读回执", "chat", "One check: sent. Two checks: seen. No excuses.", demoReadReceipt},
{326, "ReadReceipt", "已读标记", "chat", "One check: sent. Two checks: seen. No excuses.", demoReadReceipt},
{327, "Reactions", "表情回应", "chat", "Click a chip to add your face to the pile, again to take it back.", demoReactions},
{328, "EmojiPicker", "表情选择", "chat", "Faces, the world and loot, eight to a row.", demoEmojiPicker},
{329, "MentionInput", "提及输入", "chat", "Type @ and a few letters; Tab finishes the name.", demoMentionInput},
@@ -92,10 +93,11 @@ func demoComposer(c *ui.Context, log func(string)) {
s := chatState(c)
r := chat.Composer(c, "Message", &s.draft, chat.ComposerOptions{Placeholder: "Say something…"})
if r.Changed {
log(fmt.Sprintf("draft %q", s.draft))
log(fmt.Sprintf("draft of %d letters", utf8.RuneCountInString(s.draft)))
}
// Counts, not words: logs never hold what was typed.
if r.Sent != "" {
log("sent: " + r.Sent)
log(fmt.Sprintf("sent %d letters", utf8.RuneCountInString(r.Sent)))
}
}
@@ -149,13 +151,13 @@ func demoMentionInput(c *ui.Context, log func(string)) {
s := chatState(c)
r := chat.MentionInput(c, "Mention", &s.mention, []string{"Aria", "Arthur", "Brom", "Mittens"}, chat.MentionInputOptions{Placeholder: "Try @ar"})
if r.Changed {
log(fmt.Sprintf("text %q", s.mention))
log(fmt.Sprintf("text of %d letters", utf8.RuneCountInString(s.mention)))
}
if r.Mentioned != "" {
log("mentioned " + r.Mentioned)
}
if r.Submitted {
log("sent: " + s.mention)
log(fmt.Sprintf("sent %d letters", utf8.RuneCountInString(s.mention)))
}
}
+1 -1
View File
@@ -189,7 +189,7 @@ func demoFPSMeter(c *ui.Context, log func(string)) {
}
}
if !k.Reduce() {
if t := k.Tick(c, 12); s.last.IsZero() || k.Now().Sub(s.last) >= time.Second/12 {
if t := k.Tick(c, 12); s.last.IsZero() || k.Elapsed(&s.last) >= time.Second/12 {
s.last = k.Now()
add(t)
}
+4 -4
View File
@@ -19,7 +19,7 @@ var controlEntries = []Entry{
{90, "TokenField", "标记输入", "input", "Tags before the caret. Enter or a comma adds; Backspace on empty takes the last back.", demoTokenField},
{91, "Segmented", "分段控件", "input", "Joined segments, one lit. Arrows move the light.", demoSegmented},
{92, "Rating", "评分", "input", "Stars or hearts. Click or arrows to rate; click the same mark to clear.", demoRating},
{93, "Knob", "旋钮", "input", "A rotary knob that clicks through 16 angles. Drag up or down, or use the arrows.", demoKnob},
{93, "Knob", "转钮", "input", "A rotary knob that clicks through 16 angles. Drag up or down, or use the arrows.", demoKnob},
{94, "PinInput", "验证码", "input", "One box per digit: typing moves on, Backspace steps back, paste fills them all.", demoPinInput},
{95, "NameEntry", "姓名登记", "input", "HIGH SCORE! Three initials from the letter grid, then END.", demoNameEntry},
{96, "VirtualKeyboard", "屏幕键盘", "input", "A keyboard on screen, for pads and touch: letters, digits, space, erase, enter.", demoVirtualKeyboard},
@@ -29,9 +29,9 @@ var controlEntries = []Entry{
{100, "Field", "表单项", "input", "A label above, a hint below, and a red ! when something is wrong.", demoField},
{101, "Fieldset", "字段组", "input", "A framed group of fields under a legend.", demoFieldset},
{102, "ColorSlot", "色槽选择", "input", "Pick one of the sixteen palette slots. It follows the palette.", demoColorSlot},
{103, "Joystick", "摇杆", "input", "Drag the stick inside its ring; it springs home. Arrows nudge it.", demoJoystick},
{103, "Joystick", "操作杆", "input", "Drag the stick inside its ring; it springs home. Arrows nudge it.", demoJoystick},
{104, "CheatCode", "秘籍", "input", "Focus it and enter ↑↑↓↓←→←→BA. You know the one.", demoCheatCode},
{105, "SlotPicker", "滚轮选择", "input", "Reels that spin by drag, wheel or arrows. Line up three for the jackpot.", demoSlotPicker},
{105, "SlotPicker", "转轮选择", "input", "Reels that spin by drag, wheel or arrows. Line up three for the jackpot.", demoSlotPicker},
}
type controlDemo struct {
@@ -149,7 +149,7 @@ func demoPinInput(c *ui.Context, log func(string)) {
if s.pin == "0451" {
log("ACCESS GRANTED")
} else {
log("WRONG CODE " + s.pin)
log("WRONG CODE")
}
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ var deskEntries = []Entry{
{467, "DesktopIcon", "桌面图标", "desk", "Click to choose and it inverts; double-click or Enter opens it.", demoDesktopIcon},
{468, "Taskbar", "任务栏", "desk", "START, a button for each open window, the tray and the clock.", demoTaskbar},
{469, "StartMenu", "开始菜单", "desk", "Programs in cascading menus. Arrows walk in and out; Enter runs one.", demoStartMenu},
{470, "AppWindow", "应用窗口", "desk", "Drag a window by its title bar, or focus the bar and use the arrows.", demoAppWindow},
{470, "AppWindow", "应用框体", "desk", "Drag a window by its title bar, or focus the bar and use the arrows.", demoAppWindow},
{471, "Calculator", "计算器", "desk", "It really adds up. Divide by zero for a GAME OVER; C continues.", demoCalculator},
{472, "Notepad", "记事本", "desk", "A text area under FILE and EDIT. TIME/DATE stamps the moment.", demoNotepad},
{473, "Minesweeper", "扫雷", "desk", "Open with a click or Enter, flag with a right click or F. The face starts over.", demoMinesweeper},
+5 -5
View File
@@ -20,11 +20,11 @@ var displayEntries = []Entry{
{43, "Picture", "像素图", "display", "Any image reduced to the palette with Bayer dither. Press P: it recolors.", demoPicture},
{44, "Avatar", "头像", "display", "A face built from a name: skin, hair, eyes, mouth and background.", demoAvatar},
{45, "AvatarGroup", "头像组", "display", "Overlapping faces and a chip for the rest.", demoAvatarGroup},
{46, "Badge", "徽标", "display", "A count or a word in a notched pill; counts stop at 99+.", demoBadge},
{46, "Badge", "角标", "display", "A count or a word in a notched pill; counts stop at 99+.", demoBadge},
{47, "Tag", "标签", "display", "A chip with text, an icon and a remove box.", demoTag},
{48, "StatusDot", "状态点", "display", "Online, away, busy or offline; a live online dot blinks.", demoStatusDot},
{49, "Medal", "奖牌", "display", "Gold, silver and bronze on ribbons, rank on the face.", demoMedal},
{50, "Trophy", "奖杯", "display", "A gold cup over an engraved plaque.", demoTrophy},
{49, "Medal", "勋章", "display", "Gold, silver and bronze on ribbons, rank on the face.", demoMedal},
{50, "Trophy", "金杯", "display", "A gold cup over an engraved plaque.", demoTrophy},
{51, "Stat", "数据", "display", "A big value, its label and a change up or down.", demoStat},
{52, "KeyValue", "键值", "display", "A label and a value joined by a dotted leader.", demoKeyValue},
{53, "Portrait", "立绘框", "display", "A character in a frame over a nameplate.", demoPortrait},
@@ -73,8 +73,8 @@ func demoIcon(c *ui.Context, log func(string)) {
func demoSprite(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Row(c).Gap(k.D(12)).Children(func() {
display.Sprite(c, slime, display.SpriteOptions{Scale: 3, FPS: 2, Label: "slime"})
display.Sprite(c, slime, display.SpriteOptions{Scale: 3, FPS: 2, Mirror: true, Label: "slime, mirrored"})
display.Sprite(c, slime, display.SpriteOptions{Scale: 3, FPS: 4, Label: "slime"})
display.Sprite(c, slime, display.SpriteOptions{Scale: 3, FPS: 4, Mirror: true, Label: "slime, mirrored"})
})
}
+4 -4
View File
@@ -14,7 +14,7 @@ import (
var feedbackEntries = []Entry{
{106, "Progress", "进度条", "feedback", "A bar that fills chunk by chunk. Below zero, a block runs back and forth.", demoProgress},
{107, "Spinner", "旋转器", "feedback", "Eight frames of beads going round. It keeps turning under Reduce Motion: it means work.", demoSpinner},
{107, "Spinner", "转圈器", "feedback", "Eight frames of beads going round. It keeps turning under Reduce Motion: it means work.", demoSpinner},
{108, "Meter", "计量条", "feedback", "A level that turns Warn past a mark and Bad past the next.", demoMeter},
{109, "Skeleton", "骨架屏", "feedback", "Dithered stand-ins with a shine sweeping across in steps.", demoSkeleton},
{110, "Alert", "提示框", "feedback", "A framed callout: info, success, warning or danger, with an optional ×.", demoAlert},
@@ -27,15 +27,15 @@ var feedbackEntries = []Entry{
{117, "BarsLoader", "柱状加载", "feedback", "Five bars rising and falling, a step apart.", demoBarsLoader},
{118, "Hourglass", "沙漏", "feedback", "Sand falls in steps, then the glass flips.", demoHourglass},
{119, "Toast", "吐司", "feedback", "Short messages at the bottom, three at a time, each for a few seconds. Some can be undone.", demoToast},
{120, "InlineMessage", "行内提示", "feedback", "A small ! or ✓ note under a field.", demoInlineMessage},
{120, "InlineMessage", "行内提示", "feedback", "A small warning or check note under a field.", demoInlineMessage},
{121, "CountUp", "计数", "feedback", "Numbers count up in steps, the way scores should.", demoCountUp},
{122, "Pulse", "脉冲", "feedback", "A dithered ring grows around what wants attention.", demoPulse},
{123, "LevelUp", "升级", "feedback", "LEVEL UP! slides in with sparkles when the level rises.", demoLevelUp},
{124, "Achievement", "成就", "feedback", "A trophy card slides in, waits, and slides away.", demoAchievement},
{125, "ComboCounter", "连击", "feedback", "×12 COMBO! It pops a dot bigger with every hit.", demoComboCounter},
{126, "ContinuePrompt", "继续?", "feedback", "CONTINUE? YES or NO before the count runs out.", demoContinuePrompt},
{127, "SaveIndicator", "存档指示", "feedback", "A floppy spins while saving, then SAVED ✓.", demoSaveIndicator},
{128, "BusyOverlay", "忙碌遮罩", "feedback", "A checker veil and spinner over busy content; it takes the pointer and the focus.", demoBusyOverlay},
{127, "SaveIndicator", "存档指示", "feedback", "A floppy spins while saving, then SAVED with a check.", demoSaveIndicator},
{128, "BusyOverlay", "加载遮罩", "feedback", "A checker veil and spinner over busy content; it takes the pointer and the focus.", demoBusyOverlay},
{129, "ReadyGo", "预备开始", "feedback", "3, 2, 1, GO! in big digits, a second each.", demoReadyGo},
{130, "Shake", "抖动", "feedback", "Jolts its child side to side, for a wrong answer.", demoShake},
}
+1 -1
View File
@@ -20,7 +20,7 @@ var filesEntries = []Entry{
{409, "PathBar", "路径栏", "files", "Where you are, folder by folder. Click one to climb back up.", demoPathBar},
{410, "MemoryCard", "记忆卡", "files", "Fifteen blocks, like the grey card of old. DELETE frees one.", demoMemoryCard},
{411, "DiskUsage", "磁盘占用", "files", "Where the room went, stacked in one bar.", demoDiskUsage},
{412, "UploadQueue", "上传队列", "files", "Files on their way up. ✕ calls one back.", demoUploadQueue},
{412, "UploadQueue", "上传队列", "files", "Files on their way up. × calls one back.", demoUploadQueue},
{413, "Trash", "回收站", "files", "A bin that looks full when it is. EMPTY takes it out.", demoTrash},
}
+3 -1
View File
@@ -26,7 +26,9 @@ func demoPalettes(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Grid(c).Columns(2).Gap(k.D(6)).Children(func() {
for _, pal := range pixel.Palettes {
pixel.Scope(c, pixel.Settings{Palette: pal}, func() {
s := pixel.Current(c)
s.Palette = pal
pixel.Scope(c, s, func() {
kp := pixel.K(c)
shell, base, st := pixel.Shell(c, func() *ui.Element { return ui.ButtonBase(c).Label(pal.Name) })
shell.Height(kp.D(52)).Draw(func(p *ui.Painter, r ui.Rect) {
+2 -2
View File
@@ -61,7 +61,7 @@ func paletteOf(c *ui.Context, s pixel.Season, night bool) pixel.Settings {
// over stacks an effect on a same-sized sky.
func over(c *ui.Context, w, h int, effect func()) {
layout.Layers(c, func() { fx.Sky(c, fx.SkyOptions{Width: w, Height: h}) }, effect)
layout.Layers(c, layout.LayersOptions{}, func() { fx.Sky(c, fx.SkyOptions{Width: w, Height: h}) }, effect)
}
// portalBand rings the portal, outside in.
@@ -154,7 +154,7 @@ func demoLava(c *ui.Context, log func(string)) {
func demoAurora(c *ui.Context, log func(string)) {
pixel.Scope(c, paletteOf(c, pixel.K(c).Palette().Season, true), func() {
layout.Layers(c, func() { fx.Sky(c, fx.SkyOptions{Width: 140, Height: 80}) },
layout.Layers(c, layout.LayersOptions{}, func() { fx.Sky(c, fx.SkyOptions{Width: 140, Height: 80}) },
func() { fx.Aurora(c, fx.AuroraOptions{Width: 140, Height: 80, Label: "aurora"}) },
func() { fx.Mountains(c, fx.MountainsOptions{Width: 140, Height: 80}) })
})
+1 -1
View File
@@ -178,7 +178,7 @@ func demoTimerHUD(c *ui.Context, log func(string)) {
s := gameState(c)
left := max(0, s.deadline.Sub(k.Now()))
if left > 0 {
k.Tick(c, 2)
k.Tick(c, 4)
}
ui.Row(c).Gap(k.D(12)).AlignItems(ui.Center).Children(func() {
game.TimerHUD(c, left, game.TimerHUDOptions{})
+5 -5
View File
@@ -14,12 +14,12 @@ import (
var layoutEntries = []Entry{
{1, "Screen", "屏幕", "layout", "The root of a screen: Back fills the window, with an optional dithered vignette.", demoScreen},
{2, "Window", "窗口", "layout", "A beveled window with a title tab, the shape of every RPG menu.", demoWindow},
{2, "Window", "框体", "layout", "A beveled window with a title tab, the shape of every RPG menu.", demoWindow},
{3, "Panel", "面板", "layout", "A box framed raised, sunken, flat, double, window or notched.", demoPanel},
{4, "Card", "卡片", "layout", "A raised panel with a header strip, a body and a footer.", demoCard},
{5, "Section", "分节", "layout", "A titled group inside a dotted rule.", demoSection},
{6, "Divider", "分隔线", "layout", "Rules across or down: solid, dotted, dashed, double, ornament.", demoDivider},
{7, "ScrollArea", "滚动区", "layout", "A scroll view with a pixel scrollbar: arrows step, the track pages, the thumb drags.", demoScrollArea},
{7, "ScrollArea", "卷动区", "layout", "A scroll view with a pixel scrollbar: arrows step, the track pages, the thumb drags.", demoScrollArea},
{8, "Split", "分栏", "layout", "Two panes and a grip. Drag it, or focus it and use the arrow keys.", demoSplit},
{9, "Grid", "网格", "layout", "Equal columns with solid or dotted lines between the cells.", demoGrid},
{10, "Tiles", "平铺", "layout", "Equal tiles that wrap into rows as the width allows.", demoTiles},
@@ -207,7 +207,7 @@ func demoAspectBox(c *ui.Context, log func(string)) {
func demoLayers(c *ui.Context, log func(string)) {
k := pixel.K(c)
layout.Layers(c, func() {
layout.Layers(c, layout.LayersOptions{}, func() {
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Dither(0, 0, g.W, g.H, pixel.Well, pixel.Info, 6)
@@ -251,7 +251,7 @@ func demoTitleBar(c *ui.Context, log func(string)) {
func demoStatusBar(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Box(c).Width(k.D(260)).Children(func() {
layout.StatusBar(c, "Ready", "Ln 1, Col 1", "UTF-8")
layout.StatusBar(c, []string{"Ready", "Ln 1, Col 1", "UTF-8"}, layout.StatusBarOptions{})
})
}
@@ -275,7 +275,7 @@ func demoHandheld(c *ui.Context, log func(string)) {
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Rect(0, 0, g.W, g.H, pixel.Well)
g.Sprite(at[0], at[1], slime, g.Step(2), pixel.Ink, 1)
g.Sprite(at[0], at[1], slime, g.Step(4)/2, pixel.Ink, 1)
})
})
moves := map[string][2]int{"up": {0, -4}, "down": {0, 4}, "left": {-4, 0}, "right": {4, 0}}
+2 -2
View File
@@ -18,7 +18,7 @@ var lootEntries = []Entry{
{281, "Crafting", "合成台", "game", "Click a cell to swap ingredients. Herb over water makes a potion.", demoCrafting},
{282, "LootPopup", "获得物品", "game", "YOU GOT: something nice! The item rises out of the box.", demoLootPopup},
{283, "TreasureChest", "宝箱", "game", "Press the chest. It creaks open; the gem floats up.", demoTreasureChest},
{284, "GachaReel", "抽奖转轮", "game", "SPIN: three reels, stopping one by one. No real money involved.", demoGachaReel},
{284, "GachaReel", "扭蛋转轮", "game", "SPIN: three reels, stopping one by one. No real money involved.", demoGachaReel},
{285, "InteractPrompt", "互动提示", "game", "Walk up to the mayor and a TALK bubble bobs above him.", demoInteractPrompt},
{286, "TurnOrder", "行动顺序", "game", "Who acts next. The active one stands raised.", demoTurnOrder},
{287, "BattleLog", "战斗日志", "game", "Combat lines roll up as the fight goes on.", demoBattleLog},
@@ -222,7 +222,7 @@ func demoStaminaBar(c *ui.Context, log func(string)) {
s := lootState(c)
now := k.Now()
// Sprinting drains a quarter a second; rest refills a tenth.
if dt := now.Sub(s.last).Seconds(); s.sprint {
if dt := max(0, now.Sub(s.last).Seconds()); s.sprint {
s.stamina = max(0, s.stamina-dt/4)
} else {
s.stamina = min(1, s.stamina+dt/10)
+3 -3
View File
@@ -16,10 +16,10 @@ import (
var marketEntries = []Entry{
{444, "Quantity", "数量", "shop", "- and + within limits. The side at its limit fades.", demoQuantity},
{445, "PricingTiers", "价格方案", "shop", "Bronze, silver and gold. Gold comes with a hat.", demoPricingTiers},
{446, "Coupon", "优惠券", "shop", "A ticket with notched edges. COPY takes the code.", demoCoupon},
{447, "DailyReward", "每日奖励", "shop", "Seven days, seven chests. Come back daily; today's is ready.", demoDailyReward},
{446, "Coupon", "优惠码", "shop", "A ticket with notched edges. COPY takes the code.", demoCoupon},
{447, "DailyReward", "每日福利", "shop", "Seven days, seven chests. Come back daily; today's is ready.", demoDailyReward},
{448, "BidTicker", "竞价", "shop", "The latest bids on top; the highest wears the crown.", demoBidTicker},
{449, "TradeWindow", "交易窗口", "shop", "Two offers. Both must confirm, and any change takes the confirms back.", demoTradeWindow},
{449, "TradeWindow", "交易框", "shop", "Two offers. Both must confirm, and any change takes the confirms back.", demoTradeWindow},
{450, "VendingMachine", "自动售货机", "shop", "Type a code like B3 and press OK. Credit first, snacks after.", demoVendingMachine},
}
+3 -2
View File
@@ -15,7 +15,7 @@ import (
var mediaEntries = []Entry{
{346, "AudioPlayer", "音乐播放器", "media", "A whole deck in one box. Stop rewinds to the top.", demoAudioPlayer},
{347, "Transport", "播放控制", "media", "Back, play or pause, stop, skip: the four buttons every deck agrees on.", demoTransport},
{348, "Scrubber", "进度拖动", "media", "Drag to seek, or nudge with arrows. Time at both ends.", demoScrubber},
{348, "Scrubber", "进度定位", "media", "Drag to seek, or nudge with arrows. Time at both ends.", demoScrubber},
{349, "Volume", "音量", "media", "Eight bars; click one or use arrows. The speaker mutes.", demoVolume},
{350, "Equalizer", "均衡器", "media", "Five faders, ±12 dB. Bass boost at your own risk.", demoEqualizer},
{351, "Visualizer", "频谱", "media", "Bars bounce out from the middle, hottest at the tips.", demoVisualizer},
@@ -76,7 +76,8 @@ func beat(s string) [16]bool {
func spin(c *ui.Context, last *time.Time, playing bool, at *time.Duration, length time.Duration) {
k := pixel.K(c)
if playing && !last.IsZero() {
*at = (*at + k.Now().Sub(*last)) % length
// A clock stepping back plays nothing backward.
*at = (*at + max(0, k.Now().Sub(*last))) % length
}
*last = time.Time{}
if playing {
+3 -3
View File
@@ -16,14 +16,14 @@ var navigationEntries = []Entry{
{131, "Tabs", "标签页", "navigation", "Folder tabs; the chosen one joins its panel. Arrows move.", demoTabs},
{132, "VerticalTabs", "竖排标签", "navigation", "Tabs down the left edge, for settings screens.", demoVerticalTabs},
{133, "Sidebar", "侧边栏", "navigation", "Sections of items with icons and badges. Up and Down choose; one Tab stop.", demoSidebar},
{134, "Breadcrumbs", "面包屑", "navigation", "Where you are, split by ›. Every part but the last walks back.", demoBreadcrumbs},
{134, "Breadcrumbs", "面包屑", "navigation", "Where you are, split by pointers. Every part but the last walks back.", demoBreadcrumbs},
{135, "CommandMenu", "指令菜单", "navigation", "FIGHT, MAGIC, ITEM, RUN, with a bobbing ▶. Enter picks; Escape backs out.", demoCommandMenu},
{136, "MenuBar", "菜单栏", "navigation", "A bar of menus. Click one, then the arrows go across and down.", demoMenuBar},
{137, "DropdownMenu", "下拉菜单", "navigation", "A button with a menu: icons, rules and shortcut names.", demoDropdownMenu},
{138, "ContextMenu", "右键菜单", "navigation", "Right click, or the menu key, opens a menu at the pointer.", demoContextMenu},
{139, "Pagination", "分页", "navigation", "◀ 1 … 4 5 6 … 12 ▶. The current page is pressed in.", demoPagination},
{140, "PageDots", "页码点", "navigation", "A dot per page; click one, or use the arrows.", demoPageDots},
{141, "Steps", "关卡步骤", "navigation", "Flags along a trail: cleared, waving where you are, locked ahead.", demoSteps},
{141, "Steps", "关卡阶段", "navigation", "Flags along a trail: cleared, waving where you are, locked ahead.", demoSteps},
{142, "NavRail", "导航栏", "navigation", "A rail of icons. Hover or focus one and it says its name.", demoNavRail},
{143, "BottomNav", "底部导航", "navigation", "Icons over labels along the bottom, for small screens.", demoBottomNav},
{144, "CommandPalette", "命令面板", "navigation", "Cmd+K, type a few letters, press Enter.", demoCommandPalette},
@@ -68,7 +68,7 @@ func demoTabs(c *ui.Context, log func(string)) {
func demoVerticalTabs(c *ui.Context, log func(string)) {
s := navState(c)
names := []string{"Video", "Audio", "Controls"}
if navigation.VerticalTabs(c, "Settings", names, &s.vtab, navigation.TabsOptions{}, func() {
if navigation.VerticalTabs(c, "Settings", names, &s.vtab, navigation.VerticalTabsOptions{}, func() {
pixel.Text(c, names[s.vtab]+" settings go here.", pixel.TextOptions{Slot: pixel.Faint})
}).Changed {
log(names[s.vtab])
+1 -1
View File
@@ -16,7 +16,7 @@ import (
var overlayEntries = []Entry{
{176, "Dialog", "对话框", "overlay", "A titled window over a checkered backdrop. Escape or a click outside closes it.", demoDialog},
{177, "AlertDialog", "警告框", "overlay", "Icon, title, message, buttons. Enter presses the highlighted one.", demoAlertDialog},
{177, "AlertDialog", "提醒框", "overlay", "Icon, title, message, buttons. Enter presses the highlighted one.", demoAlertDialog},
{178, "ConfirmDialog", "确认框", "overlay", "ARE YOU SURE? The hand starts on NO, to be safe.", demoConfirmDialog},
{179, "Popover", "弹出层", "overlay", "A panel under its button, notched toward it. It flips above near the bottom.", demoPopover},
{180, "Tooltip", "提示气泡", "overlay", "Rest the pointer half a second, or Tab in, and a bubble says what it does.", demoTooltip},
+1 -1
View File
@@ -111,7 +111,7 @@ func demoTwoFactor(c *ui.Context, log func(string)) {
}
func demoAbout(c *ui.Context, log func(string)) {
account.About(c, "8BIT", account.AboutOptions{Version: "1.0.0", Credits: []account.Credit{
account.About(c, "8BIT", "1.0.0", account.AboutOptions{Credits: []account.Credit{
{Role: "Pixels and code", Name: "The 8bit team"}, {Role: "Native UI", Name: "MyGo"}, {Role: "Font", Name: "Ark Pixel"},
}})
}
+2 -2
View File
@@ -16,7 +16,7 @@ import (
var popupEntries = []Entry{
{186, "ChoicePrompt", "选项提示", "overlay", "A question and a hand. Arrows and Enter, or press 1–9.", demoChoicePrompt},
{187, "Coachmark", "新手引导", "overlay", "A guided tour: a bubble points at each button in turn.", demoCoachmark},
{188, "Spotlight", "聚光灯", "overlay", "Everything but one button sinks into dither.", demoSpotlight},
{188, "Spotlight", "探照灯", "overlay", "Everything but one button sinks into dither.", demoSpotlight},
{189, "Notification", "通知", "overlay", "Cards stack at the top right and leave on their own. × sends one away.", demoNotification},
{190, "Popconfirm", "气泡确认", "overlay", "A small YES / NO bubble before the button acts.", demoPopconfirm},
{191, "PauseMenu", "暂停菜单", "overlay", "PAUSED blinks over the game. Escape resumes.", demoPauseMenu},
@@ -173,7 +173,7 @@ func demoKeyHints(c *ui.Context, log func(string)) {
func demoTitleCard(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := overlayState(c)
layout.Layers(c, func() {
layout.Layers(c, layout.LayersOptions{}, func() {
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Dither(0, 0, g.W, g.H, pixel.Well, pixel.Info, 4)
+1 -1
View File
@@ -15,7 +15,7 @@ var projectEntries = []Entry{
{422, "TaskItem", "任务项", "project", "A box to tick, a title, how urgent, and who.", demoTaskItem},
{423, "TaskList", "任务列表", "project", "Tasks with OPEN, DONE and URGENT chips to narrow them down.", demoTaskList},
{424, "Milestones", "里程碑", "project", "Flags along the road. The little hero stands at today.", demoMilestones},
{425, "Burndown", "燃尽图", "project", "The straight line is the plan. The other one is what happened.", demoBurndown},
{425, "Burndown", "冲刺进度", "project", "The straight line is the plan. The other one is what happened.", demoBurndown},
{426, "Pomodoro", "番茄钟", "project", "Twenty-five minutes of work, five of rest. The tomato naps on the break.", demoPomodoro},
{427, "Priority", "优先级", "project", "LOW, MEDIUM, HIGH and, for the worst, CRITICAL: it is on fire.", demoPriority},
{428, "AssigneePicker", "指派", "project", "The party lines up. Pick who takes the quest, or nobody.", demoAssigneePicker},
+3 -3
View File
@@ -17,13 +17,13 @@ import (
var recordEntries = []Entry{
{164, "StatCompare", "属性对比", "data", "What a new sword does to you: ▲ in green, ▼ in red.", demoStatCompare},
{165, "Bestiary", "图鉴", "data", "Monsters you met, and ??? shadows of those you have not.", demoBestiary},
{165, "Bestiary", "怪物图录", "data", "Monsters you met, and ??? shadows of those you have not.", demoBestiary},
{166, "PropertyGrid", "属性表", "data", "Edit text, numbers and switches row by row.", demoPropertyGrid},
{167, "Pager", "分页器", "data", "Pick a page size; ◀ ▶ step through 312 items.", demoPager},
{168, "Feed", "动态流", "data", "What happened, newest first, with icons and times.", demoFeed},
{169, "Matrix", "矩阵", "data", "Who knows which spell. Arrows move; Space flips.", demoMatrix},
{170, "Sortable", "可排序列表", "data", "Drag a quest by its grip, or move it with Alt and the arrows.", demoSortable},
{171, "Transfer", "穿梭框", "data", "Pick a hero, then → or ← to swap benches.", demoTransfer},
{171, "Transfer", "双栏选择", "data", "Pick a hero, then → or ← to swap benches.", demoTransfer},
{172, "InfiniteList", "无限列表", "data", "Scroll to the end; it loads twenty more.", demoInfiniteList},
{173, "FilterBar", "筛选栏", "data", "Chips turn filters on; CLEAR turns them all off.", demoFilterBar},
{174, "SearchResults", "搜索结果", "data", "Type a spell name; matches light up.", demoSearchResults},
@@ -100,7 +100,7 @@ func demoInfiniteList(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := dataState(c)
// A pretend server answers after one second.
if !s.loading.IsZero() && k.Since(c, s.loading, 1, 1) == 1 {
if !s.loading.IsZero() && k.Since(c, s.loading, 4, 4) == 4 {
s.logs, s.loading = s.logs+20, time.Time{}
log(fmt.Sprintf("loaded %d rows", s.logs))
}
+1 -1
View File
@@ -140,7 +140,7 @@ func demoDamageNumbers(c *ui.Context, log func(string)) {
s, dice := rpgState(c), gameState(c).dice
ui.Column(c).Gap(k.D(4)).Children(func() {
// The numbers float over a slime having a bad day.
layout.Layers(c, func() {
layout.Layers(c, layout.LayersOptions{}, func() {
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Sprite((g.W-3*slime.W)/2, g.H-3*slime.H, slime, 0, pixel.Ink, 3)
+2 -2
View File
@@ -127,7 +127,7 @@ func demoGlitch(c *ui.Context, log func(string)) {
func demoSparkle(c *ui.Context, log func(string)) {
k := pixel.K(c)
layout.Layers(c, func() {
layout.Layers(c, layout.LayersOptions{}, func() {
ui.Box(c).Size(k.D(48), k.D(48)).AlignItems(ui.Center).Justify(ui.Center).Children(func() {
display.Gem(c, display.Legendary, display.GemOptions{Scale: 2})
})
@@ -148,7 +148,7 @@ func demoExplosion(c *ui.Context, log func(string)) {
func demoConfetti(c *ui.Context, log func(string)) {
k := pixel.K(c)
layout.Layers(c, func() {
layout.Layers(c, layout.LayersOptions{}, func() {
ui.Box(c).Size(k.D(140), k.D(64)).AlignItems(ui.Center).Justify(ui.Center).Children(func() {
pixel.Text(c, "YOU WIN!", pixel.TextOptions{Scale: 2, Slot: pixel.Accent})
})
+1 -1
View File
@@ -20,7 +20,7 @@ var studioEntries = []Entry{
{359, "Waveform", "波形", "media", "Every sample a line, up or down from zero.", demoWaveform},
{360, "VideoFrame", "视频框", "media", "A 16:9 screen with play and a scrubber. The slime does the rest.", demoVideoFrame},
{361, "ImageCompare", "对比滑块", "media", "Drag the grip: sketch on the left, paint on the right.", demoImageCompare},
{362, "Gallery", "画廊", "media", "Arrows walk the thumbnails; the big picture follows.", demoGallery},
{362, "Gallery", "相册", "media", "Arrows walk the thumbnails; the big picture follows.", demoGallery},
{363, "Filmstrip", "胶片条", "media", "A sprite's frames on film, sprocket holes and all.", demoFilmstrip},
{364, "RecordButton", "录制按钮", "media", "The dot blinks while it records, and the time runs.", demoRecordButton},
{365, "Tuner", "调频器", "media", "Drag the needle across FM. A few stations are on the air.", demoTuner},
+2 -2
View File
@@ -12,8 +12,8 @@ import (
)
var tabletopEntries = []Entry{
{486, "Dice", "骰子", "tabletop", "A d6 with pips. ROLL and it tumbles before it lands.", demoDice},
{487, "DiceTray", "骰盘", "tabletop", "Several dice on felt, and what they add up to.", demoDiceTray},
{486, "Dice", "色子", "tabletop", "A d6 with pips. ROLL and it tumbles before it lands.", demoDice},
{487, "DiceTray", "色子盘", "tabletop", "Several dice on felt, and what they add up to.", demoDiceTray},
{488, "PlayingCard", "扑克牌", "tabletop", "Rank and suit, court cards with their pieces, or the back.", demoPlayingCard},
{489, "CardHand", "手牌", "tabletop", "A fanned hand. The card you point at or choose lifts; a click plays it.", demoCardHand},
{490, "Deck", "牌堆", "tabletop", "A face-down pile with its count. Click to draw.", demoDeck},
+1 -1
View File
@@ -113,7 +113,7 @@ func demoBlinkText(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Row(c).Gap(k.D(12)).Children(func() {
text.BlinkText(c, "PRESS START", text.BlinkTextOptions{Scale: 2})
text.BlinkText(c, "LOW HP", text.BlinkTextOptions{Slot: pixel.Bad, FPS: 6})
text.BlinkText(c, "LOW HP", text.BlinkTextOptions{Slot: pixel.Bad, FPS: 12})
})
}
+27
View File
@@ -0,0 +1,27 @@
package gallery
import (
"testing"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestGalleryTextHasGlyphs(t *testing.T) {
// Every visible word comes from the font, never tofu.
check := func(where, s string) {
for _, r := range s {
if r != ' ' && !pixel.Has(r) {
t.Errorf("%s: the font has no %q in %q", where, r, s)
}
}
}
for _, e := range Entries() {
check(e.Slug(), e.Name+e.Zh+e.Summary)
}
for _, path := range []string{"/", "/cat/foundations", "/nowhere"} {
_, tt := open(path)
for _, s := range tt.Texts() {
check(path, s)
}
}
}
+37
View File
@@ -0,0 +1,37 @@
package gallery
import (
"testing"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestEveryFocusHasAName(t *testing.T) {
for _, e := range Entries() {
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay})
// A named start, so Tab always lands and laps.
input.Button(c, "SENTINEL", input.ButtonOptions{})
ui.Column(c).Fill().Children(func() { e.Demo(c, func(string) {}) })
}, 640, 480)
tt.Key(0, ui.KeyTab)
for i := range 12 {
tt.Key(0, ui.KeyTab)
if tt.Focused("SENTINEL") {
break
}
// Focused counts ancestors, so a named focus has a hit.
named := false
for _, s := range tt.Texts() {
named = named || tt.Focused(s)
}
if !named {
t.Errorf("%s: Tab %d focuses something with no name", e.Slug(), i+1)
break
}
}
}
}
+2 -2
View File
@@ -52,7 +52,7 @@ func (a *App) home(c *ui.Context) {
k := pixel.K(c)
p := k.Palette()
// The banner lives in the palette's own season and hour.
layout.Layers(c, func() {
layout.Layers(c, layout.LayersOptions{}, func() {
fx.SeasonScene(c, p.Season, hourOf(p), fx.SeasonSceneOptions{Height: 96, Label: p.Name + " scene"}).FillWidth()
}, func() {
ui.Column(c).Padding(k.D(10)).Children(func() {
@@ -94,7 +94,7 @@ func (a *App) palettes(c *ui.Context) {
for i, pal := range pixel.Palettes {
name := pal.Name
if i == a.palette {
name += ", in use"
name += ", " + strings.ToLower(k.Msg("in_use"))
}
set := pixel.Current(c)
set.Palette = pal
+41
View File
@@ -0,0 +1,41 @@
package gallery
import (
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestVeiledKeysDoNothing(t *testing.T) {
// KeyUnknown stands for typed text, "a1".
keys := []ui.Key{ui.KeyEnter, ui.KeySpace, ui.KeyUp, ui.KeyDown, ui.KeyLeft, ui.KeyRight, ui.Key1, ui.KeyUnknown}
for _, e := range Entries() {
for i := range 3 * len(keys) {
stop, key := i/len(keys), keys[i%len(keys)]
at, inert, logs := time.Date(2026, 10, 12, 12, 0, 0, 0, time.UTC), false, []string{}
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return at }, Inert: inert})
ui.Column(c).Fill().Children(func() { e.Demo(c, func(s string) { logs = append(logs, s) }) })
}, 640, 480)
// Each key at each of the first three Tab stops.
for range 1 + stop {
tt.Key(0, ui.KeyTab)
}
before := len(logs)
// The veil falls in the frame the key comes.
inert = true
if key == ui.KeyUnknown {
tt.Type("a1")
} else {
tt.Key(0, key)
}
if len(logs) > before {
t.Errorf("%s: %v under a falling veil logged %q", e.Slug(), key, logs[before:])
break
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
package gallery
import (
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestVeiledPointersDoNothing(t *testing.T) {
for _, e := range Entries() {
for _, at := range [][2]float32{{30, 20}, {120, 40}, {240, 60}, {60, 120}, {200, 160}, {360, 100}, {100, 260}, {300, 320}} {
now, inert, logs := time.Date(2026, 10, 12, 12, 0, 0, 0, time.UTC), false, []string{}
tt := ui.NewTester(func(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return now }, Inert: inert})
ui.Column(c).Fill().Children(func() { e.Demo(c, func(s string) { logs = append(logs, s) }) })
}, 640, 480)
tt.Move(at[0], at[1])
before := len(logs)
// The veil falls with the click, then with a notch.
inert = true
tt.ClickAt(at[0], at[1])
tt.Scroll(at[0], at[1], 0, 40)
if len(logs) > before {
t.Errorf("%s: a click or wheel at %v under a falling veil logged %q", e.Slug(), at, logs[before:])
break
}
}
}
}