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

+32
View File
@@ -292,3 +292,35 @@ func TestDataDemosLogTheirEvents(t *testing.T) {
}
}
}
func TestOverlayDemosOpenClean(t *testing.T) {
for slug, act := range map[string]func(tt *ui.Tester){
"176-dialog": func(tt *ui.Tester) { click(t, tt, "OPEN QUEST") },
"177-alertdialog": func(tt *ui.Tester) { click(t, tt, "FIGHT BOSS") },
"178-confirmdialog": func(tt *ui.Tester) { click(t, tt, "DROP SWORD") },
"179-popover": func(tt *ui.Tester) { click(t, tt, "MAP") },
"181-drawer": func(tt *ui.Tester) { click(t, tt, "RIGHT ▶") },
"182-sheet": func(tt *ui.Tester) { click(t, tt, "OPTIONS") },
"184-lightbox": func(tt *ui.Tester) { click(t, tt, "Cave") },
"186-choiceprompt": func(tt *ui.Tester) { click(t, tt, "A SLIME APPEARS") },
"188-spotlight": func(tt *ui.Tester) { click(t, tt, "SPOTLIGHT") },
"189-notification": func(tt *ui.Tester) { click(t, tt, "NEW MAIL") },
"191-pausemenu": func(tt *ui.Tester) { click(t, tt, "PAUSE") },
"192-promptdialog": func(tt *ui.Tester) { click(t, tt, "NEW GAME") },
"193-progressdialog": func(tt *ui.Tester) { click(t, tt, "SAVE") },
} {
a, tt := open("/c/" + slug)
act(tt)
tt.Frame()
if len(a.logs[slug]) == 0 {
t.Errorf("%s logged nothing", slug)
}
keepsStyle(t, slug+" open", tt.Image(), colors(pixel.Palettes...))
}
_, tt := open("/c/190-popconfirm")
click(t, tt, "SELL CROWN")
if !tt.HasText("Sell it for 5 gold?") {
t.Fatal("the popconfirm did not open")
}
keepsStyle(t, "popconfirm open", tt.Image(), colors(pixel.Palettes...))
}
+224
View File
@@ -0,0 +1,224 @@
package gallery
import (
"fmt"
"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"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/text"
)
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},
{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},
{181, "Drawer", "抽屉", "overlay", "A panel that slides in from an edge, four steps at a time.", demoDrawer},
{182, "Sheet", "底部面板", "overlay", "Rises from the bottom. Drag its bar down to put it away.", demoSheet},
{183, "HoverCard", "悬停卡片", "overlay", "Point at a name to see the card; the card stays while you read it.", demoHoverCard},
{184, "Lightbox", "灯箱", "overlay", "Pictures over a dark veil. ◀ ▶ or the arrows turn them.", demoLightbox},
{185, "DialogueBox", "剧情对话", "overlay", "An RPG text box. A press shows the line, the next turns the page.", demoDialogueBox},
}
type overlayDemo struct {
dialog, alert, confirm, popover, sheet, lightbox, choice, pause, prompt, saving bool
drawer display.Side
drawerOpen bool
picture, page, step, cue int
name string
notes []overlay.Notification
spot bool
since time.Time
}
func overlayState(c *ui.Context) *overlayDemo {
return ui.Local(c.Root(), demoKey("overlay"), func() overlayDemo { return overlayDemo{step: 1} })
}
func demoDialog(c *ui.Context, log func(string)) {
s := overlayState(c)
if input.Button(c, "OPEN QUEST", input.ButtonOptions{}).Clicked {
s.dialog = true
log("open")
}
r := overlay.Dialog(c, "Quest", &s.dialog, overlay.DialogOptions{Actions: []string{"DECLINE", "ACCEPT"}}, func() {
text.Paragraph(c, "Bring three slime jellies to the baker. Reward: one warm pie.", text.ParagraphOptions{})
})
if r.Action >= 0 {
log([]string{"declined", "accepted"}[r.Action])
}
if r.Closed {
log("closed")
}
}
func demoAlertDialog(c *ui.Context, log func(string)) {
s := overlayState(c)
if input.Button(c, "FIGHT BOSS", input.ButtonOptions{Variant: input.Danger}).Clicked {
s.alert = true
log("open")
}
buttons := []string{"NO", "HEAL"}
r := overlay.AlertDialog(c, "Low HP", "You have 3 HP. Heal before the boss?", &s.alert, overlay.AlertDialogOptions{Buttons: buttons, Default: 1})
if r.Pressed >= 0 {
log(buttons[r.Pressed])
}
if r.Closed {
log("closed")
}
}
func demoConfirmDialog(c *ui.Context, log func(string)) {
s := overlayState(c)
if input.Button(c, "DROP SWORD", input.ButtonOptions{}).Clicked {
s.confirm = true
log("open")
}
r := overlay.ConfirmDialog(c, "Drop the legendary sword in the lava?", &s.confirm, overlay.ConfirmDialogOptions{})
if r.Confirmed {
log("dropped. it is gone.")
}
if r.Canceled {
log("kept the sword")
}
}
func demoPopover(c *ui.Context, log func(string)) {
s := overlayState(c)
b := input.Button(c, "MAP", input.ButtonOptions{Icon: icon.Map})
if b.Clicked {
s.popover = !s.popover
log(fmt.Sprintf("open %v", s.popover))
}
overlay.Popover(c, "Map", b.Element, &s.popover, overlay.PopoverOptions{}, func() {
pixel.Text(c, "You are here: Town.", pixel.TextOptions{})
pixel.Text(c, "Cave: 2 screens west.", pixel.TextOptions{Slot: pixel.Faint})
})
}
func demoTooltip(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Row(c).Gap(k.D(6)).Children(func() {
for _, t := range []struct {
art *pixel.Sprite
name string
tip string
}{{icon.Sword, "Attack", "Swing for 12 damage."}, {icon.Shield, "Defend", "Halve damage this turn."}, {icon.Flame, "Magic", "Costs 4 MP."}} {
overlay.Tooltip(c, t.tip, overlay.TooltipOptions{}, func() {
if input.IconButton(c, t.art, t.name, input.IconButtonOptions{}).Clicked {
log(t.name)
}
})
}
})
}
func demoDrawer(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := overlayState(c)
ui.Row(c).Gap(k.D(6)).Children(func() {
if input.Button(c, "◀ LEFT", input.ButtonOptions{}).Clicked {
s.drawer, s.drawerOpen = display.Left, true
log("open left")
}
if input.Button(c, "RIGHT ▶", input.ButtonOptions{}).Clicked {
s.drawer, s.drawerOpen = display.Right, true
log("open right")
}
})
if overlay.Drawer(c, "Party", &s.drawerOpen, overlay.DrawerOptions{Side: s.drawer, Size: 120}, func() {
pixel.Text(c, "PARTY", pixel.TextOptions{Slot: pixel.Faint})
for _, h := range []string{"Aria LV 7", "Brom LV 5", "Cyra LV 9"} {
pixel.Text(c, h, pixel.TextOptions{})
}
}).Closed {
log("drawer closed")
}
}
func demoSheet(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := overlayState(c)
if input.Button(c, "OPTIONS", input.ButtonOptions{Icon: icon.Settings}).Clicked {
s.sheet = true
log("open")
}
if overlay.Sheet(c, "Options", &s.sheet, overlay.SheetOptions{}, func() {
ui.Column(c).Gap(k.D(4)).Children(func() {
pixel.Text(c, "Music: ON", pixel.TextOptions{})
pixel.Text(c, "Text speed: FAST", pixel.TextOptions{})
pixel.Text(c, "Drag the bar down to close.", pixel.TextOptions{Slot: pixel.Faint})
})
}).Closed {
log("sheet closed")
}
}
func demoHoverCard(c *ui.Context, log func(string)) {
k := pixel.K(c)
overlay.HoverCard(c, "Aria", overlay.HoverCardOptions{}, func() {
pixel.Text(c, "@aria", pixel.TextOptions{Slot: pixel.Info})
}, func() {
ui.Row(c).Gap(k.D(6)).Children(func() {
display.Avatar(c, "Aria", display.AvatarOptions{})
ui.Column(c).Children(func() {
pixel.Text(c, "Aria", pixel.TextOptions{})
pixel.Text(c, "Knight · LV 7", pixel.TextOptions{Slot: pixel.Faint})
})
})
})
}
var scenes = []struct {
art *pixel.Sprite
name string
}{{icon.Home, "Town"}, {icon.Map, "Field"}, {icon.Skull, "Cave"}}
func demoLightbox(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := overlayState(c)
ui.Row(c).Gap(k.D(4)).Children(func() {
for i, sc := range scenes {
if input.IconButton(c, sc.art, sc.name, input.IconButtonOptions{Size: input.Large}).Clicked {
s.picture, s.lightbox = i, true
log("open " + sc.name)
}
}
})
r := overlay.Lightbox(c, "Postcards", len(scenes), &s.picture, &s.lightbox, overlay.LightboxOptions{}, func(i int) {
ui.Column(c).AlignItems(ui.Center).Padding(k.D(8)).Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Frame(0, 0, g.W, g.H, pixel.Window, pixel.Well)
}).Children(func() {
display.Icon(c, scenes[i].art, display.IconOptions{Scale: 4, Label: scenes[i].name})
pixel.Text(c, scenes[i].name, pixel.TextOptions{})
})
})
if r.Changed {
log("showing " + scenes[s.picture].name)
}
if r.Closed {
log("closed")
}
}
func demoDialogueBox(c *ui.Context, log func(string)) {
s := overlayState(c)
pages := []string{"It is dangerous to go alone!", "Take this. It is a stick.", "Mostly harmless. Good luck."}
r := overlay.DialogueBox(c, pages, &s.page, overlay.DialogueBoxOptions{Name: "OLD MAN", Portrait: icon.User})
r.Element.Width(pixel.K(c).D(280))
if r.Changed {
log(fmt.Sprintf("page %d", s.page+1))
}
if r.Done {
s.page = 0
log("the old man starts over")
}
}
+189
View File
@@ -0,0 +1,189 @@
package gallery
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/layout"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/overlay"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
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},
{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},
{192, "PromptDialog", "输入对话框", "overlay", "ENTER YOUR NAME, eight letters at most.", demoPromptDialog},
{193, "ProgressDialog", "进度对话框", "overlay", "Saving, with a way out.", demoProgressDialog},
{194, "KeyHints", "按键提示", "overlay", "Pad buttons and keycaps beside what they do.", demoKeyHints},
{195, "TitleCard", "关卡标题", "overlay", "WORLD 1-1 dithers in, waits, and dithers out.", demoTitleCard},
}
func demoChoicePrompt(c *ui.Context, log func(string)) {
s := overlayState(c)
choices := []string{"Fight", "Talk", "Flee"}
if input.Button(c, "A SLIME APPEARS", input.ButtonOptions{}).Clicked {
s.choice = true
log("open")
}
r := overlay.ChoicePrompt(c, "A wild slime blocks the road. It wobbles.", choices, &s.choice, overlay.ChoicePromptOptions{})
if r.Chosen >= 0 {
log(choices[r.Chosen])
}
if r.Canceled {
log("stared at the slime")
}
}
func demoCoachmark(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := overlayState(c)
tips := []string{"Your bag. Potions live here.", "The map. You are the red dot.", "Save often. Slimes are tricky."}
var targets [3]*ui.Element
ui.Row(c).Gap(k.D(6)).Children(func() {
for i, b := range []struct {
art *pixel.Sprite
name string
}{{icon.Chest, "Bag"}, {icon.Map, "Map"}, {icon.Save, "Save"}} {
targets[i] = input.Button(c, b.name, input.ButtonOptions{Icon: b.art}).Element
}
})
if s.step > len(tips) {
if input.Button(c, "TOUR AGAIN", input.ButtonOptions{Size: input.Small}).Clicked {
s.step = 1
log("tour again")
}
return
}
if overlay.Coachmark(c, targets[s.step-1], tips[s.step-1], s.step, len(tips), overlay.CoachmarkOptions{}).Next {
s.step++
log(fmt.Sprintf("step %d", s.step))
}
}
func demoSpotlight(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := overlayState(c)
var shop *ui.Element
ui.Row(c).Gap(k.D(6)).Children(func() {
input.Button(c, "INN", input.ButtonOptions{})
shop = input.Button(c, "SHOP", input.ButtonOptions{Icon: icon.Cart}).Element
input.Button(c, "GUILD", input.ButtonOptions{})
})
if input.Toggle(c, "SPOTLIGHT", &s.spot, input.ToggleOptions{Size: input.Small}).Changed {
log(fmt.Sprintf("spotlight %v", s.spot))
}
if s.spot {
overlay.Spotlight(c, shop, "Potions are on sale!", overlay.SpotlightOptions{})
}
}
func demoNotification(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := overlayState(c)
ui.Row(c).Gap(k.D(4)).Children(func() {
if input.Button(c, "NEW MAIL", input.ButtonOptions{Size: input.Small}).Clicked {
s.notes = append(s.notes, overlay.Notification{Icon: icon.Mail, Title: "Letter from Mom", Text: "Eat your vegetables."})
log("mail")
}
if input.Button(c, "LEVEL UP", input.ButtonOptions{Size: input.Small}).Clicked {
s.notes = append(s.notes, overlay.Notification{Icon: icon.Star, Title: "Level 8!", Text: "STR +2, LUCK +1.", For: 8 * time.Second})
log("level up")
}
})
if r := overlay.Notifications(c, &s.notes, overlay.NotificationsOptions{}); r.Dismissed != nil {
log("dismissed " + r.Dismissed.Title)
}
}
func demoPopconfirm(c *ui.Context, log func(string)) {
r := overlay.Popconfirm(c, "SELL CROWN", "Sell it for 5 gold?", overlay.PopconfirmOptions{})
if r.Confirmed {
log("sold. what a deal.")
}
if r.Canceled {
log("kept the crown")
}
}
func demoPauseMenu(c *ui.Context, log func(string)) {
s := overlayState(c)
if input.Button(c, "PAUSE", input.ButtonOptions{Icon: icon.Pause}).Clicked {
s.pause = true
log("open")
}
if r := overlay.PauseMenu(c, &s.pause, overlay.PauseMenuOptions{}); r.Chosen >= 0 {
log([]string{"resume", "options", "quit"}[r.Chosen])
}
}
func demoPromptDialog(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := overlayState(c)
ui.Row(c).Gap(k.D(6)).AlignItems(ui.Center).Children(func() {
if input.Button(c, "NEW GAME", input.ButtonOptions{}).Clicked {
s.prompt = true
log("open")
}
if s.name != "" {
pixel.Text(c, "Hero: "+s.name, pixel.TextOptions{})
}
})
r := overlay.PromptDialog(c, "ENTER YOUR NAME", &s.name, &s.prompt, overlay.PromptDialogOptions{Placeholder: "LINK", MaxLength: 8})
if r.Submitted {
log("named " + s.name)
}
if r.Canceled {
log("canceled")
}
}
func demoProgressDialog(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := overlayState(c)
if input.Button(c, "SAVE", input.ButtonOptions{Icon: icon.Save}).Clicked {
s.saving, s.since = true, k.Now()
log("saving")
}
// The pretend save takes three seconds.
done := 0
if s.saving {
done = k.Since(c, s.since, 10, 30)
}
if done == 30 {
s.saving = false
log("saved")
}
if overlay.ProgressDialog(c, "Saving", float64(done)/30, &s.saving, overlay.ProgressDialogOptions{}).Canceled {
log(fmt.Sprintf("canceled at %d%%", done*100/30))
}
}
func demoKeyHints(c *ui.Context, log func(string)) {
overlay.KeyHints(c, []overlay.KeyHint{{Key: "A", Action: "CONFIRM"}, {Key: "B", Action: "BACK"}, {Key: "START", Action: "MENU"}}, overlay.KeyHintsOptions{})
}
func demoTitleCard(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := overlayState(c)
layout.Layers(c, 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)
g.Rect(0, g.H-12, g.W, 12, pixel.Good)
})
}, func() {
overlay.TitleCard(c, "WORLD 1-1", overlay.TitleCardOptions{Cue: s.cue})
}).Size(k.D(200), k.D(90))
if input.Button(c, "ENTER WORLD", input.ButtonOptions{Size: input.Small}).Clicked {
s.cue++
log("world 1-1")
}
}
+1 -1
View File
@@ -56,7 +56,7 @@ var Categories = []Category{
{"tabletop", "Tabletop", "桌游", icon.Crown},
}
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries, feedbackEntries, navigationEntries, dataEntries, recordEntries)
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries, feedbackEntries, navigationEntries, dataEntries, recordEntries, overlayEntries, popupEntries)
func Entries() []Entry { return entries }