Add input components 086-105; park focus off disabled controls

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 20:13:21 -04:00
1 parent 095eab2b4c
commit 03b789f533
34 files changed
+3058 -43

No files matched your search

+32
View File
@@ -218,3 +218,35 @@ func TestInputDemosLogEveryClick(t *testing.T) {
}
}
}
func TestControlDemosLogTheirEvents(t *testing.T) {
for slug, act := range map[string]func(tt *ui.Tester){
"088-combobox": func(tt *ui.Tester) { click(t, tt, "Item"); tt.Type("p") },
"089-autocomplete": func(tt *ui.Tester) { click(t, tt, "Spell"); tt.Type("f") },
"094-pininput": func(tt *ui.Tester) { click(t, tt, "Vault code"); tt.Type("1") },
"095-nameentry": func(tt *ui.Tester) { click(t, tt, "Initials") },
"096-virtualkeyboard": func(tt *ui.Tester) { click(t, tt, "Message") },
"103-joystick": func(tt *ui.Tester) { click(t, tt, "Stick"); tt.Key(0, ui.KeyRight) },
"105-slotpicker": func(tt *ui.Tester) { click(t, tt, "Reel 1"); tt.Key(0, ui.KeyDown) },
"101-fieldset": func(tt *ui.Tester) { click(t, tt, "Guild name"); tt.Type("a") },
} {
a, tt := open("/c/" + slug)
act(tt)
if len(a.logs[slug]) == 0 {
t.Errorf("%s logged nothing", slug)
}
}
}
func TestFieldDemoLogsSubmit(t *testing.T) {
a, tt := open("/c/100-field")
// The group shares its name; the input sits below.
r, _ := tt.Find("Call sign")
tt.ClickAt(r.X+20, r.Y+58)
tt.Type("Ace")
n := len(a.logs["100-field"])
tt.Key(0, ui.KeyEnter)
if len(a.logs["100-field"]) == n {
t.Fatal("Enter in the field logged nothing")
}
}
+291
View File
@@ -0,0 +1,291 @@
package gallery
import (
"fmt"
"slices"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
var controlEntries = []Entry{
{86, "Select", "下拉选择", "input", "The choice and ▼; the list opens in a window with a hand pointing. Arrows and Enter pick.", demoSelect},
{87, "MultiSelect", "多选", "input", "A popup of checkboxes; what you pick rides in the trigger as tags.", demoMultiSelect},
{88, "Combobox", "组合框", "input", "Type to narrow the list below; Enter or a click takes one.", demoCombobox},
{89, "Autocomplete", "自动补全", "input", "The rest of the word waits in grey. Tab or Enter takes it.", demoAutocomplete},
{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},
{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},
{97, "DPad", "方向键", "input", "A cross pad. Press an arm or use the arrow keys.", demoDPad},
{98, "HoldButton", "长按按钮", "input", "Hold it down while the bar fills in eight steps. Let go and it starts over.", demoHoldButton},
{99, "EditableText", "可编辑文本", "input", "Double click or press Enter to rename. Enter keeps it; Escape takes it back.", demoEditableText},
{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},
{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},
}
type controlDemo struct {
class, item, spell, hero, call, guild string
party, tags []string
mode, banner, stars, lives int
reel1, reel2, reel3 int
volume, bass float64
pin, initials, message string
cape pixel.Slot
dir input.Direction
}
func controlState(c *ui.Context) *controlDemo {
return ui.Local(c.Root(), demoKey("controls"), func() controlDemo {
return controlDemo{class: "Knight", hero: "Pixel Knight", tags: []string{"retro", "pixel"}, mode: 1, stars: 4, lives: 3,
volume: 0.7, bass: 0.3, cape: pixel.Accent, reel2: 1, reel3: 2}
})
}
func demoSelect(c *ui.Context, log func(string)) {
s := controlState(c)
if input.Select(c, "Class", &s.class, []string{"Knight", "Mage", "Thief", "Bard"}, input.SelectOptions{}).Changed {
log("class " + s.class)
}
}
func demoMultiSelect(c *ui.Context, log func(string)) {
s := controlState(c)
if input.MultiSelect(c, "Party", &s.party, []string{"Warrior", "Healer", "Rogue", "Wizard"}, input.MultiSelectOptions{}).Changed {
log(fmt.Sprintf("party %v", s.party))
}
}
func demoCombobox(c *ui.Context, log func(string)) {
s := controlState(c)
items := []string{"Potion", "Hi-Potion", "Ether", "Elixir", "Antidote", "Phoenix Down"}
r := input.Combobox(c, "Item", &s.item, items, input.ComboboxOptions{Placeholder: "Use an item…", Width: 140})
if r.Changed {
log("item " + s.item)
}
if r.Chosen {
log("used " + s.item)
}
}
func demoAutocomplete(c *ui.Context, log func(string)) {
s := controlState(c)
spells := []string{"Fire", "Fira", "Firaga", "Blizzard", "Thunder", "Cure", "Curaga"}
r := input.Autocomplete(c, "Spell", &s.spell, spells, input.AutocompleteOptions{Placeholder: "Cast…"})
if r.Changed {
log("spell " + s.spell)
}
if r.Accepted {
log("cast " + s.spell)
}
if r.Submitted {
log("fizzle: " + s.spell)
}
}
func demoTokenField(c *ui.Context, log func(string)) {
s := controlState(c)
if input.TokenField(c, "Tags", &s.tags, input.TokenFieldOptions{Placeholder: "add tag"}).Changed {
log(fmt.Sprintf("tags %v", s.tags))
}
}
func demoSegmented(c *ui.Context, log func(string)) {
s := controlState(c)
modes := []string{"EASY", "NORMAL", "HARD"}
if input.Segmented(c, "Difficulty", modes, &s.mode, input.SegmentedOptions{}).Changed {
log("difficulty " + modes[s.mode])
}
}
func demoRating(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := controlState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
if input.Rating(c, "Dungeon", &s.stars, input.RatingOptions{}).Changed {
log(fmt.Sprintf("%d stars", s.stars))
}
if input.Rating(c, "Lives", &s.lives, input.RatingOptions{Max: 3, Hearts: true}).Changed {
log(fmt.Sprintf("%d lives", s.lives))
}
})
}
func demoKnob(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := controlState(c)
ui.Row(c).Gap(k.D(12)).AlignItems(ui.Center).Children(func() {
for _, kn := range []struct {
name string
v *float64
}{{"VOL", &s.volume}, {"BASS", &s.bass}} {
ui.Column(c).Gap(k.D(2)).AlignItems(ui.Center).Children(func() {
if input.Knob(c, kn.name, kn.v, input.KnobOptions{Step: 0.05}).Changed {
log(fmt.Sprintf("%s %.2f", kn.name, *kn.v))
}
pixel.Text(c, fmt.Sprintf("%s %d", kn.name, int(*kn.v*100+0.5)), pixel.TextOptions{Slot: pixel.Faint})
})
}
})
}
func demoPinInput(c *ui.Context, log func(string)) {
s := controlState(c)
r := input.PinInput(c, "Vault code", &s.pin, input.PinInputOptions{})
if r.Changed {
log(fmt.Sprintf("%d digits", len(s.pin)))
}
if r.Complete {
if s.pin == "0451" {
log("ACCESS GRANTED")
} else {
log("WRONG CODE " + s.pin)
}
}
}
func demoNameEntry(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := controlState(c)
ui.Column(c).Gap(k.D(4)).AlignItems(ui.Center).Children(func() {
pixel.Text(c, "NEW HIGH SCORE 99999", pixel.TextOptions{Slot: pixel.Accent})
r := input.NameEntry(c, "Initials", &s.initials, input.NameEntryOptions{})
if r.Changed {
log("initials " + s.initials)
}
if r.Confirmed {
log("1ST " + s.initials + " 99999")
}
})
}
func demoVirtualKeyboard(c *ui.Context, log func(string)) {
s := controlState(c)
r := input.VirtualKeyboard(c, "Message", &s.message, input.VirtualKeyboardOptions{})
if r.Changed {
log("message " + s.message)
}
if r.Submitted {
log("sent " + s.message)
s.message = ""
}
}
func demoDPad(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := controlState(c)
ui.Row(c).Gap(k.D(12)).AlignItems(ui.Center).Children(func() {
if d := input.DPad(c, "Pad", input.DPadOptions{}).Pressed; d != input.None {
s.dir = d
log(d.String())
}
pixel.Text(c, "last: "+s.dir.String(), pixel.TextOptions{Slot: pixel.Faint})
})
}
func demoHoldButton(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Row(c).Gap(k.D(6)).Children(func() {
if input.HoldButton(c, "SKIP CUTSCENE", input.HoldButtonOptions{}).Confirmed {
log("cutscene skipped")
}
if input.HoldButton(c, "DELETE SAVE", input.HoldButtonOptions{Duration: 2 * time.Second, Danger: true}).Confirmed {
log("save deleted (not really)")
}
})
}
func demoEditableText(c *ui.Context, log func(string)) {
s := controlState(c)
if input.EditableText(c, "Hero name", &s.hero, input.EditableTextOptions{Placeholder: "Nameless"}).Changed {
log("renamed to " + s.hero)
}
}
func demoField(c *ui.Context, log func(string)) {
s := controlState(c)
o := input.FieldOptions{Description: "Shown on the leaderboard."}
if s.call == "" {
o.Error = "A hero needs a call sign!"
}
input.Field(c, "Call sign", o, func() {
r := input.TextInput(c, "Call sign", &s.call, input.TextInputOptions{MaxLength: 10})
if r.Changed {
log("call sign " + s.call)
}
if r.Submitted {
log("signed " + s.call)
}
})
}
func demoFieldset(c *ui.Context, log func(string)) {
s := controlState(c)
input.Fieldset(c, "Guild", input.FieldsetOptions{}, func() {
input.Field(c, "Name", input.FieldOptions{}, func() {
r := input.TextInput(c, "Guild name", &s.guild, input.TextInputOptions{Placeholder: "The Dot Matrix"})
if r.Changed {
log("guild " + s.guild)
}
if r.Submitted {
log("founded " + s.guild)
}
})
banners := []string{"RED", "BLUE", "GOLD"}
if input.Segmented(c, "Banner", banners, &s.banner, input.SegmentedOptions{}).Changed {
log("banner " + banners[s.banner])
}
})
}
func demoColorSlot(c *ui.Context, log func(string)) {
s := controlState(c)
if input.ColorSlot(c, "Cape", &s.cape, input.ColorSlotOptions{}).Changed {
log("cape " + s.cape.String())
}
}
func demoJoystick(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Row(c).Gap(k.D(12)).AlignItems(ui.Center).Children(func() {
r := input.Joystick(c, "Stick", input.JoystickOptions{})
if r.Moved {
log(fmt.Sprintf("x %+.2f y %+.2f", r.X, r.Y))
}
pixel.Text(c, fmt.Sprintf("X %+.2f Y %+.2f", r.X, r.Y), pixel.TextOptions{Slot: pixel.Faint})
})
}
func demoCheatCode(c *ui.Context, log func(string)) {
if input.CheatCode(c, "Cheat", input.CheatCodeOptions{}).Unlocked {
log("30 LIVES!")
}
}
func demoSlotPicker(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := controlState(c)
fruit := []string{"Cherry", "Bell", "Seven", "Melon", "BAR"}
ui.Row(c).Gap(k.D(4)).Children(func() {
changed := false
for i, v := range []*int{&s.reel1, &s.reel2, &s.reel3} {
if input.SlotPicker(c, fmt.Sprintf("Reel %d", i+1), v, fruit, input.SlotPickerOptions{}).Changed {
changed = true
log(fmt.Sprintf("reel %d %s", i+1, fruit[*v]))
}
}
if all := []int{s.reel1, s.reel2, s.reel3}; changed && slices.Max(all) == slices.Min(all) {
log("JACKPOT! " + fruit[s.reel1])
}
})
}
+1 -1
View File
@@ -56,7 +56,7 @@ var Categories = []Category{
{"tabletop", "Tabletop", "桌游", icon.Crown},
}
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries)
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries)
func Entries() []Entry { return entries }