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.
374 lines
14 KiB
Go
374 lines
14 KiB
Go
package gallery
|
||
|
||
import (
|
||
"fmt"
|
||
"time"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/feedback"
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||
)
|
||
|
||
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},
|
||
{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},
|
||
{111, "Banner", "横幅", "feedback", "A full-width strip with actions and a ×.", demoBanner},
|
||
{112, "EmptyState", "空状态", "feedback", "NO ITEMS over an empty chest, and something to do about it.", demoEmptyState},
|
||
{113, "ErrorState", "错误态", "feedback", "GAME OVER, a CONTINUE? countdown and RETRY. The gallery's own errors use it.", demoErrorState},
|
||
{114, "SuccessState", "通关", "feedback", "STAGE CLEAR! The stars pop in one by one.", demoSuccessState},
|
||
{115, "LoadingScreen", "加载画面", "feedback", "NOW LOADING with stepping dots and a tip every four seconds.", demoLoadingScreen},
|
||
{116, "DotsLoader", "点点加载", "feedback", "Three dots hopping in turn.", demoDotsLoader},
|
||
{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 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 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},
|
||
}
|
||
|
||
type feedbackDemo struct {
|
||
progress, hp float64
|
||
closed bool
|
||
tries, replays, prompts, score int
|
||
level, cue, combo, start, shake int
|
||
toasts []feedback.Toast
|
||
hero, secret, motto string
|
||
saving, busy time.Time
|
||
}
|
||
|
||
func feedbackState(c *ui.Context) *feedbackDemo {
|
||
return ui.Local(c.Root(), demoKey("feedback"), func() feedbackDemo {
|
||
return feedbackDemo{progress: 0.3, hp: 0.5, level: 4}
|
||
})
|
||
}
|
||
|
||
func demoProgress(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||
feedback.Progress(c, "Download", s.progress, feedback.ProgressOptions{Percent: true})
|
||
feedback.Progress(c, "Searching", -1, feedback.ProgressOptions{})
|
||
ui.Row(c).Gap(k.D(4)).Children(func() {
|
||
if input.Button(c, "+10%", input.ButtonOptions{Size: input.Small}).Clicked {
|
||
s.progress = min(1, s.progress+0.1)
|
||
log(fmt.Sprintf("%.0f%%", s.progress*100))
|
||
}
|
||
if input.Button(c, "RESET", input.ButtonOptions{Size: input.Small}).Clicked {
|
||
s.progress = 0
|
||
log("reset")
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
func demoSpinner(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
ui.Row(c).Gap(k.D(12)).AlignItems(ui.Center).Children(func() {
|
||
for sc := 1; sc <= 3; sc++ {
|
||
feedback.Spinner(c, fmt.Sprintf("Loading ×%d", sc), feedback.SpinnerOptions{Scale: sc})
|
||
}
|
||
})
|
||
}
|
||
|
||
func demoMeter(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||
feedback.Meter(c, "Heat", s.hp, feedback.MeterOptions{Warning: 0.6, Critical: 0.85})
|
||
if input.Slider(c, "Heat level", &s.hp, input.SliderOptions{}).Changed {
|
||
log(fmt.Sprintf("heat %.2f", s.hp))
|
||
}
|
||
})
|
||
}
|
||
|
||
func demoSkeleton(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
ui.Column(c).Gap(k.D(10)).Children(func() {
|
||
feedback.Skeleton(c, feedback.SkeletonOptions{Avatar: true})
|
||
feedback.Skeleton(c, feedback.SkeletonOptions{Lines: 2, Width: 120})
|
||
})
|
||
}
|
||
|
||
func demoAlert(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
ui.Column(c).Gap(k.D(4)).Children(func() {
|
||
feedback.Alert(c, "Did you know?", "Pressing P paints the world anew.", feedback.AlertOptions{Kind: feedback.Info})
|
||
feedback.Alert(c, "Quest complete", "The princess was in this castle.", feedback.AlertOptions{Kind: feedback.Success})
|
||
feedback.Alert(c, "Low on potions", "Two left. Shop before the boss.", feedback.AlertOptions{Kind: feedback.Warning})
|
||
if !s.closed && feedback.Alert(c, "Save corrupted", "Just kidding. Close me.", feedback.AlertOptions{Kind: feedback.Danger, Closable: true}).Closed {
|
||
s.closed = true
|
||
log("closed")
|
||
}
|
||
})
|
||
}
|
||
|
||
func demoBanner(c *ui.Context, log func(string)) {
|
||
r := feedback.Banner(c, "A new patch is out: slimes now bounce higher.", feedback.BannerOptions{Kind: feedback.Info, Actions: []string{"UPDATE", "LATER"}, Dismissable: true})
|
||
if r.Action != "" {
|
||
log(r.Action)
|
||
}
|
||
if r.Dismissed {
|
||
log("dismissed")
|
||
}
|
||
}
|
||
|
||
func demoEmptyState(c *ui.Context, log func(string)) {
|
||
if feedback.EmptyState(c, "Your inventory is empty.", feedback.EmptyStateOptions{Action: "VISIT SHOP"}).Acted {
|
||
log("off to the shop")
|
||
}
|
||
}
|
||
|
||
func demoErrorState(c *ui.Context, log func(string)) {
|
||
s := feedbackState(c)
|
||
// A new key starts a fresh countdown.
|
||
ui.Box(c).Key(s.tries).Children(func() {
|
||
if feedback.ErrorState(c, "The slime won this round.", feedback.ErrorStateOptions{}).Continue {
|
||
s.tries++
|
||
log(fmt.Sprintf("continue #%d", s.tries))
|
||
}
|
||
})
|
||
}
|
||
|
||
func demoSuccessState(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
ui.Column(c).Gap(k.D(4)).AlignItems(ui.Center).Children(func() {
|
||
ui.Box(c).Key(s.replays).Children(func() {
|
||
feedback.SuccessState(c, "All 99 coins found.", feedback.SuccessStateOptions{})
|
||
})
|
||
if input.Button(c, "REPLAY", input.ButtonOptions{Size: input.Small}).Clicked {
|
||
s.replays++
|
||
log("replay")
|
||
}
|
||
})
|
||
}
|
||
|
||
func demoLoadingScreen(c *ui.Context, log func(string)) {
|
||
feedback.LoadingScreen(c, feedback.LoadingScreenOptions{Tips: []string{
|
||
"Tip: slimes fear the color red.", "Tip: press P to change the palette.", "Tip: save often. The dragon does not wait.",
|
||
}})
|
||
}
|
||
|
||
func demoDotsLoader(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
ui.Row(c).Gap(k.D(6)).AlignItems(ui.Center).Children(func() {
|
||
feedback.DotsLoader(c, "Thinking", feedback.DotsLoaderOptions{})
|
||
pixel.Text(c, "The wizard is thinking", pixel.TextOptions{Slot: pixel.Faint})
|
||
})
|
||
}
|
||
|
||
func demoBarsLoader(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
ui.Row(c).Gap(k.D(6)).AlignItems(ui.Center).Children(func() {
|
||
feedback.BarsLoader(c, "Tuning", feedback.BarsLoaderOptions{})
|
||
pixel.Text(c, "Tuning the chiptune", pixel.TextOptions{Slot: pixel.Faint})
|
||
})
|
||
}
|
||
|
||
func demoHourglass(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
ui.Row(c).Gap(k.D(6)).AlignItems(ui.Center).Children(func() {
|
||
feedback.Hourglass(c, "Waiting", feedback.HourglassOptions{})
|
||
pixel.Text(c, "The turn passes", pixel.TextOptions{Slot: pixel.Faint})
|
||
})
|
||
}
|
||
|
||
func demoToast(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
ui.Row(c).Gap(k.D(4)).Children(func() {
|
||
if input.Button(c, "SAVE GAME", input.ButtonOptions{Size: input.Small}).Clicked {
|
||
s.toasts = append(s.toasts, feedback.Toast{Message: "Game saved."})
|
||
log("toast: saved")
|
||
}
|
||
if input.Button(c, "DROP SWORD", input.ButtonOptions{Size: input.Small}).Clicked {
|
||
s.toasts = append(s.toasts, feedback.Toast{Message: "You dropped the sword.", Action: "UNDO", For: 5 * time.Second})
|
||
log("toast: dropped")
|
||
}
|
||
})
|
||
if r := feedback.Toasts(c, &s.toasts, feedback.ToastsOptions{}); r.Acted != nil {
|
||
log(r.Acted.Action + ": " + r.Acted.Message)
|
||
}
|
||
}
|
||
|
||
func demoInlineMessage(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
ui.Column(c).Gap(k.D(3)).Children(func() {
|
||
if input.TextInput(c, "Hero name", &s.hero, input.TextInputOptions{Placeholder: "Your name"}).Changed {
|
||
log("name " + s.hero)
|
||
}
|
||
if s.hero == "" {
|
||
feedback.InlineMessage(c, "A hero needs a name.", feedback.InlineMessageOptions{Kind: feedback.Danger})
|
||
} else {
|
||
feedback.InlineMessage(c, "That name is free!", feedback.InlineMessageOptions{Kind: feedback.Success})
|
||
}
|
||
})
|
||
}
|
||
|
||
func demoCountUp(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
|
||
feedback.CountUp(c, s.score, feedback.CountUpOptions{Scale: 2, Slot: pixel.Accent})
|
||
for _, add := range []int{100, 1000} {
|
||
if input.Button(c, fmt.Sprintf("+%d", add), input.ButtonOptions{Size: input.Small}).Clicked {
|
||
s.score += add
|
||
log(fmt.Sprintf("score %d", s.score))
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
func demoPulse(c *ui.Context, log func(string)) {
|
||
feedback.Pulse(c, feedback.PulseOptions{}, func() {
|
||
if input.IconButton(c, icon.Mail, "New mail", input.IconButtonOptions{}).Clicked {
|
||
log("mail opened")
|
||
}
|
||
})
|
||
}
|
||
|
||
func demoLevelUp(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
ui.Column(c).Gap(k.D(4)).Children(func() {
|
||
if input.Button(c, "GAIN XP", input.ButtonOptions{Size: input.Small}).Clicked {
|
||
s.level++
|
||
log(fmt.Sprintf("level %d", s.level))
|
||
}
|
||
feedback.LevelUp(c, s.level, feedback.LevelUpOptions{})
|
||
})
|
||
}
|
||
|
||
func demoAchievement(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
titles := []string{"Pressed Start", "Slime Tamer", "Touched Grass", "Read the Manual"}
|
||
ui.Column(c).Gap(k.D(4)).Children(func() {
|
||
if input.Button(c, "UNLOCK", input.ButtonOptions{Size: input.Small}).Clicked {
|
||
s.cue++
|
||
log("unlocked " + titles[s.cue%len(titles)])
|
||
}
|
||
feedback.Achievement(c, titles[s.cue%len(titles)], feedback.AchievementOptions{Cue: s.cue})
|
||
})
|
||
}
|
||
|
||
func demoComboCounter(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
ui.Column(c).Gap(k.D(4)).Children(func() {
|
||
ui.Row(c).Gap(k.D(4)).Children(func() {
|
||
if input.Button(c, "HIT", input.ButtonOptions{Size: input.Small, Variant: input.Primary}).Clicked {
|
||
s.combo++
|
||
log(fmt.Sprintf("combo %d", s.combo))
|
||
}
|
||
if input.Button(c, "MISS", input.ButtonOptions{Size: input.Small}).Clicked {
|
||
s.combo = 0
|
||
log("combo broken")
|
||
}
|
||
})
|
||
feedback.ComboCounter(c, s.combo, feedback.ComboCounterOptions{})
|
||
})
|
||
}
|
||
|
||
func demoContinuePrompt(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
ui.Column(c).Gap(k.D(4)).AlignItems(ui.Center).Children(func() {
|
||
ui.Box(c).Key(s.prompts).Children(func() {
|
||
r := feedback.ContinuePrompt(c, feedback.ContinuePromptOptions{})
|
||
switch {
|
||
case r.Yes:
|
||
log("yes: one more go")
|
||
case r.No:
|
||
log("no: thanks for playing")
|
||
case r.TimedOut:
|
||
log("time up")
|
||
}
|
||
})
|
||
if input.Button(c, "AGAIN", input.ButtonOptions{Size: input.Small}).Clicked {
|
||
s.prompts++
|
||
log("again")
|
||
}
|
||
})
|
||
}
|
||
|
||
func demoSaveIndicator(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
saving := k.Now().Before(s.saving)
|
||
if saving {
|
||
k.Tick(c, 4)
|
||
}
|
||
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
|
||
if input.Button(c, "SAVE", input.ButtonOptions{Size: input.Small, Disabled: saving}).Clicked {
|
||
s.saving = k.Now().Add(1500 * time.Millisecond)
|
||
log("saving")
|
||
}
|
||
feedback.SaveIndicator(c, saving, feedback.SaveIndicatorOptions{})
|
||
})
|
||
}
|
||
|
||
func demoBusyOverlay(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
busy := k.Now().Before(s.busy)
|
||
if busy {
|
||
k.Tick(c, 4)
|
||
}
|
||
feedback.BusyOverlay(c, busy, feedback.BusyOverlayOptions{}, func() {
|
||
ui.Column(c).Gap(k.D(4)).Padding(k.D(4)).Children(func() {
|
||
if input.TextInput(c, "Guild motto", &s.motto, input.TextInputOptions{Placeholder: "We came, we saved"}).Changed {
|
||
log("motto " + s.motto)
|
||
}
|
||
if input.Button(c, "SEND", input.ButtonOptions{Size: input.Small}).Clicked {
|
||
s.busy = k.Now().Add(2 * time.Second)
|
||
log("sending…")
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
func demoReadyGo(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
ui.Column(c).Gap(k.D(4)).AlignItems(ui.Center).Children(func() {
|
||
if input.Button(c, "START", input.ButtonOptions{Size: input.Small, Variant: input.Primary}).Clicked {
|
||
s.start++
|
||
log("ready")
|
||
}
|
||
if feedback.ReadyGo(c, feedback.ReadyGoOptions{Cue: s.start}).Done {
|
||
log("GO!")
|
||
}
|
||
})
|
||
}
|
||
|
||
func demoShake(c *ui.Context, log func(string)) {
|
||
k := pixel.K(c)
|
||
s := feedbackState(c)
|
||
ui.Column(c).Gap(k.D(4)).Children(func() {
|
||
feedback.Shake(c, feedback.ShakeOptions{Cue: s.shake}, func() {
|
||
input.PasswordInput(c, "Password", &s.secret, input.PasswordInputOptions{Placeholder: "Speak, friend"})
|
||
})
|
||
if input.Button(c, "ENTER", input.ButtonOptions{Size: input.Small}).Clicked {
|
||
s.shake++
|
||
log("wrong password")
|
||
}
|
||
})
|
||
}
|