Files
8bitMyGoUI/overlay/panels_test.go
T
ZacharyZhang-NY 5c3c883b7d Add overlay components 176-195; pixel Harbor and Kit.Cue
Dialogs on DialogBase with focusable panels that harbor parked focus,
popovers on PopoverBase, bubbles, drawers, sheets, a lightbox, an RPG
dialogue box, notifications, a pause menu and a dithered title card.
Kit.Cue moves from feedback to pixel.
2026-10-11 23:11:46 -04:00

332 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package overlay_test
import (
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/display"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/overlay"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestDrawerSlidesInInSteps(t *testing.T) {
clk, open, closed := newClock(), false, 0
side := display.Right
tt := view(640, 400, clk, false, func(c *ui.Context) {
if input.Button(c, "MENU", input.ButtonOptions{}).Clicked {
open = true
}
if overlay.Drawer(c, "Menu", &open, overlay.DrawerOptions{Side: side, Size: 100}, func() {
pixel.Text(c, "Items", pixel.TextOptions{})
}).Closed {
closed++
}
})
click(t, tt, "MENU")
var xs []float32
for range 5 {
xs = append(xs, find(t, tt, "Items").X)
clk.t = clk.t.Add(70 * time.Millisecond)
tt.Frame()
}
// Four 50-pixel steps, then rest at 640-200+16.
want := []float32{456 + 200, 456 + 150, 456 + 100, 456 + 50, 456}
for i := range want {
if xs[i] != want[i] {
t.Fatalf("x by step %v, want %v", xs, want)
}
}
keeps(t, tt.Image())
tt.Key(0, ui.KeyEscape)
if open || closed != 1 {
t.Fatal("Escape left it open")
}
side, open = display.Left, true
tt.Frame()
clk.t = clk.t.Add(time.Second)
tt.Frame()
if x := find(t, tt, "Items").X; x != 16 {
t.Fatalf("a left drawer opened at %v", x)
}
}
func TestSheetClosesOnALongPull(t *testing.T) {
open, closed := true, 0
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
if overlay.Sheet(c, "Options", &open, overlay.SheetOptions{Height: 120}, func() {
pixel.Text(c, "Sound: ON", pixel.TextOptions{})
}).Closed {
closed++
}
})
// The bar tops the sheet, which is 240 pixels.
top := float32(400 - 240)
tt.Press(320, top+8)
tt.Move(320, top+40)
tt.Release(320, top+40)
if !open || find(t, tt, "Sound: ON").Y > top+40 {
t.Fatal("a short pull closed or moved the sheet")
}
tt.Press(320, top+8)
tt.Move(320, top+60)
tt.Move(320, top+120)
tt.Release(320, top+120)
if open || closed != 1 {
t.Fatalf("a long pull left open %v", open)
}
}
func TestLightboxTurnsWrapsAndCloses(t *testing.T) {
open, at, closed := true, 0, 0
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
if overlay.Lightbox(c, "Gallery", 3, &at, &open, overlay.LightboxOptions{}, func(i int) {
pixel.Text(c, []string{"Castle", "Cave", "Coast"}[i], pixel.TextOptions{})
}).Closed {
closed++
}
})
tt.Key(0, ui.KeyLeft)
if at != 2 || !shows(tt, "Coast") || !shows(tt, "3 / 3") {
t.Fatalf("Left from the first went to %d", at)
}
click(t, tt, "NEXT")
tt.Key(0, ui.KeyRight)
if at != 1 || !shows(tt, "Cave") {
t.Fatalf("▶, Right went to %d", at)
}
keeps(t, tt.Image())
click(t, tt, "CLOSE")
if open || closed != 1 {
t.Fatal("× did not close it")
}
}
func TestDialogueBoxTypesThenTurns(t *testing.T) {
clk, page, done := newClock(), 0, 0
pages := []string{"It is dangerous to go alone!", "Take this."}
tt := view(640, 400, clk, false, func(c *ui.Context) {
if overlay.DialogueBox(c, pages, &page, overlay.DialogueBoxOptions{Name: "OLD MAN", Portrait: icon.User}).Done {
done++
}
})
r := find(t, tt, "OLD MAN "+pages[0])
tt.ClickAt(r.X+4, r.Y+4)
if page != 0 {
t.Fatal("the first press turned the page before the text was out")
}
tt.Key(0, ui.KeyEnter)
if page != 1 || !shows(tt, "OLD MAN Take this.") {
t.Fatalf("Enter after the text left page %d", page)
}
clk.t = clk.t.Add(2 * time.Second)
tt.Frame()
tt.Key(0, ui.KeyEnter)
if done != 1 || !shows(tt, "OLD MAN") {
t.Fatalf("Enter on the last page: done %d", done)
}
keeps(t, tt.Image())
}
func TestDialogueBoxUnderReduceMotionTurnsAtOnce(t *testing.T) {
page := 0
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
overlay.DialogueBox(c, []string{"One.", "Two.", "Three."}, &page, overlay.DialogueBoxOptions{})
})
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
click(t, tt, "One.")
tt.Key(0, ui.KeyEnter)
if page != 2 {
t.Fatalf("two presses under Reduce Motion reached page %d", page)
}
}
func TestNotificationsExpireAndDismiss(t *testing.T) {
clk := newClock()
queue := []overlay.Notification{{Title: "Saved", Text: "Slot 1."}, {Icon: icon.Mail, Title: "Letter", Text: "From mom.", For: 10 * time.Second}}
var dismissed string
tt := view(640, 400, clk, false, func(c *ui.Context) {
if r := overlay.Notifications(c, &queue, overlay.NotificationsOptions{}); r.Dismissed != nil {
dismissed = r.Dismissed.Title
}
})
a, b := find(t, tt, "Saved"), find(t, tt, "Letter")
if a.X+a.W != 640-16 || b.Y <= a.Y {
t.Fatalf("cards at %v and %v", a, b)
}
clk.t = clk.t.Add(6 * time.Second)
tt.Frame()
if shows(tt, "Saved") || len(queue) != 1 {
t.Fatalf("the first card outlived five seconds: %d left", len(queue))
}
click(t, tt, "CLOSE")
if dismissed != "Letter" || len(queue) != 0 {
t.Fatalf("× dismissed %q", dismissed)
}
}
func TestKeyHintsDrawPadButtonsAndKeycaps(t *testing.T) {
tt := view(640, 100, newClock(), false, func(c *ui.Context) {
overlay.KeyHints(c, []overlay.KeyHint{{Key: "A", Action: "CONFIRM"}, {Key: "START", Action: "MENU"}}, overlay.KeyHintsOptions{})
})
r := find(t, tt, "A CONFIRM")
if s := slot(t, tt.Image(), int(r.X)+18, int(r.Y)+r2i(r.H/2)); s != pixel.Ink {
t.Fatalf("pad button is %v", s)
}
if !shows(tt, "START") || !shows(tt, "MENU") {
t.Fatalf("texts %q", tt.Texts())
}
keeps(t, tt.Image())
}
func r2i(f float32) int { return int(f) }
func TestTitleCardDithersInHoldsAndGoes(t *testing.T) {
clk, cue := newClock(), 0
tt := view(320, 200, clk, false, func(c *ui.Context) {
ui.Box(c).Size(300, 160).Children(func() {
overlay.TitleCard(c, "WORLD 1-1", overlay.TitleCardOptions{Cue: cue, Hold: time.Second})
})
})
area := ui.Rect{X: 0, Y: 0, W: 300, H: 160}
if n := count(t, tt.Image(), area, pixel.Ink); n != 0 || shows(tt, "WORLD 1-1") {
t.Fatal("the card showed before its cue")
}
cue = 1
tt.Frame()
var inks []int
for range 4 {
inks = append(inks, count(t, tt.Image(), area, pixel.Ink))
clk.t = clk.t.Add(time.Second / 8)
tt.Frame()
}
if !(inks[0] < inks[1] && inks[1] < inks[2] && inks[2] < inks[3]) || !shows(tt, "WORLD 1-1") {
t.Fatalf("ink by step %v", inks)
}
clk.t = clk.t.Add(2 * time.Second)
tt.Frame()
if shows(tt, "WORLD 1-1") || count(t, tt.Image(), area, pixel.Ink) != 0 {
t.Fatal("the card stayed past its hold")
}
}
func TestOffDialogueBoxStaysPut(t *testing.T) {
for _, inert := range []bool{false, true} {
page := 0
tt := view(640, 400, newClock(), inert, func(c *ui.Context) {
overlay.DialogueBox(c, []string{"One.", "Two."}, &page, overlay.DialogueBoxOptions{Disabled: !inert})
})
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
click(t, tt, "One.")
click(t, tt, "One.")
tt.Key(0, ui.KeyEnter)
if page != 0 {
t.Fatalf("inert %v: an off box turned to page %d", inert, page)
}
}
}
func TestSheetVanishesOnTheReleaseThatClosesIt(t *testing.T) {
open := true
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
overlay.Sheet(c, "Options", &open, overlay.SheetOptions{Height: 120}, func() {
pixel.Text(c, "Sound: ON", pixel.TextOptions{})
})
})
top := float32(400 - 240)
tt.Press(320, top+8)
tt.Move(320, top+120)
tt.Release(320, top+120)
if open || shows(tt, "Sound: ON") {
t.Fatal("the sheet stayed on screen after the pull closed it")
}
}
func TestPanelsWithoutControlsTakeTheFocus(t *testing.T) {
for _, kind := range []string{"drawer", "sheet", "dialog", "alert", "confirm", "choice", "pause", "prompt", "progress"} {
open, opened := false, 0
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
if input.Button(c, "OPEN", input.ButtonOptions{}).Clicked {
open = true
opened++
}
body := func() { pixel.Text(c, "Just words.", pixel.TextOptions{}) }
name := "Menu"
switch kind {
case "drawer":
overlay.Drawer(c, name, &open, overlay.DrawerOptions{Side: display.Left}, body)
case "sheet":
overlay.Sheet(c, name, &open, overlay.SheetOptions{}, body)
case "dialog":
overlay.Dialog(c, name, &open, overlay.DialogOptions{}, body)
case "alert":
overlay.AlertDialog(c, name, "Hi.", &open, overlay.AlertDialogOptions{Disabled: true})
case "confirm":
overlay.ConfirmDialog(c, "Hi.", &open, overlay.ConfirmDialogOptions{Disabled: true})
case "choice":
overlay.ChoicePrompt(c, name, []string{"A"}, &open, overlay.ChoicePromptOptions{Disabled: true})
case "pause":
overlay.PauseMenu(c, &open, overlay.PauseMenuOptions{Disabled: true})
case "prompt":
s := ""
overlay.PromptDialog(c, name, &s, &open, overlay.PromptDialogOptions{Disabled: true})
case "progress":
overlay.ProgressDialog(c, name, 0.5, &open, overlay.ProgressDialogOptions{Disabled: true})
}
})
click(t, tt, "OPEN")
tt.Key(0, ui.KeyEnter)
if opened != 1 || tt.Focused("OPEN") {
t.Fatalf("%s: Enter reached the opener behind it (%d opens)", kind, opened)
}
}
}
func TestOffSheetTakesNoPull(t *testing.T) {
for _, inert := range []bool{false, true} {
open := true
tt := view(640, 400, newClock(), inert, func(c *ui.Context) {
overlay.Sheet(c, "Options", &open, overlay.SheetOptions{Height: 120, Disabled: !inert}, func() {})
})
top := float32(400 - 240)
tt.Press(320, top+8)
tt.Move(320, top+120)
tt.Release(320, top+120)
if !open {
t.Fatalf("inert %v: a pull closed an off sheet", inert)
}
}
}
func TestDisabledPanelsTakeNoClicks(t *testing.T) {
open, at, saving, next := true, 0, true, 0
queue := []overlay.Notification{{Title: "Saved", Text: "Slot 1."}}
tt := view(640, 400, newClock(), false, func(c *ui.Context) {
overlay.Lightbox(c, "Gallery", 3, &at, &open, overlay.LightboxOptions{Disabled: true}, func(int) {})
})
tt.Key(0, ui.KeyRight)
click(t, tt, "NEXT")
click(t, tt, "CLOSE")
tt = view(640, 400, newClock(), false, func(c *ui.Context) {
overlay.ProgressDialog(c, "Saving", 0.2, &saving, overlay.ProgressDialogOptions{Disabled: true})
})
click(t, tt, "CANCEL")
tt = view(640, 400, newClock(), false, func(c *ui.Context) {
ui.Column(c).Padding(40).Children(func() {
b := input.Button(c, "BAG", input.ButtonOptions{})
if overlay.Coachmark(c, b.Element, "Bag.", 1, 2, overlay.CoachmarkOptions{Disabled: true}).Next {
next++
}
})
overlay.Notifications(c, &queue, overlay.NotificationsOptions{Disabled: true})
})
click(t, tt, "NEXT ▶")
click(t, tt, "CLOSE")
if at != 0 || !open || !saving || next != 0 || len(queue) != 1 {
t.Fatalf("disabled: at %d open %v saving %v next %d queue %d", at, open, saving, next, len(queue))
}
}