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.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 23:11:46 -04:00
1 parent 03680980da
commit 5c3c883b7d
37 files changed
+2835 -31

No files matched your search

+331
View File
@@ -0,0 +1,331 @@
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))
}
}