Elements are now pass-scoped value handles: tests that read bounds after a frame bind a ui.Handle. pixel tells frames apart by the build pass's root, since the Tester clock no longer moves on its own. Fix the regressions the upgrade brought: - Clipboard reads in input handlers go through c.Services(). - MyGo notes an overlay's opener after building it, so AutoFocus inside lost it; pixel.AutoFocus focuses through a Handle, and archtest bans the raw call. Dialogs hand the focus back again. - Elements around a press stay hovered: Tooltip tells a press-given focus by focus arriving under the pointer. - The web host moves the Tester clock with real time, at most 500 ms a call, and gives components the real clock. The gallery's source index also accepts CRLF checkouts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
123 lines
3.2 KiB
Go
123 lines
3.2 KiB
Go
package input
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"slices"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
|
)
|
|
|
|
type CheatCodeOptions struct {
|
|
// Sequence is the code; nil means ↑↑↓↓←→←→BA.
|
|
Sequence []ui.Key
|
|
Disabled bool
|
|
}
|
|
|
|
type CheatCodeResult struct {
|
|
Element ui.Element
|
|
Unlocked bool
|
|
}
|
|
|
|
var konami = []ui.Key{ui.KeyUp, ui.KeyUp, ui.KeyDown, ui.KeyDown, ui.KeyLeft, ui.KeyRight, ui.KeyLeft, ui.KeyRight, ui.KeyB, ui.KeyA}
|
|
|
|
type cheatKey struct{}
|
|
|
|
// cheat is the key trail and an unreported win.
|
|
type cheat struct {
|
|
trail []ui.Key
|
|
won, fresh bool
|
|
}
|
|
|
|
// CheatCode watches keys for a sequence and shows the trail.
|
|
func CheatCode(c *ui.Context, text string, o CheatCodeOptions) CheatCodeResult {
|
|
k := pixel.K(c)
|
|
seq := o.Sequence
|
|
if seq == nil {
|
|
seq = konami
|
|
}
|
|
shell, base, st := pixel.Shell(c, func() ui.Element {
|
|
return ui.Box(c).Focusable().Role(ui.RoleGroup).Label(text).Disabled(o.Disabled)
|
|
})
|
|
s := ui.Local(base, cheatKey{}, func() cheat { return cheat{} })
|
|
// The handler runs before this frame knows it is off.
|
|
if base.IsDisabled() {
|
|
*s = cheat{}
|
|
}
|
|
// Keys come in order here, however fast they are typed.
|
|
base.HandleInput(func(ev ui.InputEvent) bool {
|
|
if ev.Kind != ui.InputKeyDown || ev.Mods != 0 {
|
|
return false
|
|
}
|
|
s.won, s.trail = false, append(s.trail, ev.Key)
|
|
s.trail = s.trail[max(0, len(s.trail)-len(seq)):]
|
|
if slices.Equal(s.trail, seq) {
|
|
s.won, s.fresh, s.trail = true, true, nil
|
|
}
|
|
// Other keys break the code yet pass on, like Tab.
|
|
return slices.Contains(seq, ev.Key)
|
|
})
|
|
res := CheatCodeResult{Element: shell, Unlocked: s.fresh}
|
|
s.fresh = false
|
|
if res.Unlocked {
|
|
slog.Debug("8bit: cheat unlocked", "label", text)
|
|
}
|
|
got := matched(s.trail, seq)
|
|
base.Description(fmt.Sprintf("%d/%d", got, len(seq)))
|
|
trail, won := s.trail, s.won
|
|
shell.Size(k.D(len(seq)*10+4), k.D(14+pixel.LineHeight)).Draw(func(p *ui.Painter, r ui.Rect) {
|
|
g := k.Canvas(p, r)
|
|
g.Frame(0, 0, g.W, 14, pixel.Sunken, pixel.Well)
|
|
// With no trail yet, the code itself waits in Shade.
|
|
keys, ink := trail, pixel.Faint
|
|
switch {
|
|
case won:
|
|
keys, ink = seq, pixel.Good
|
|
g.Text(2, 14, k.Msg("lives"), pixel.Good, 1)
|
|
case len(trail) == 0:
|
|
keys, ink = seq, pixel.Shade
|
|
}
|
|
for i, key := range keys {
|
|
kink := ink
|
|
if len(trail) > 0 && i >= len(trail)-got {
|
|
kink = pixel.Accent
|
|
}
|
|
glyph(g, 2+i*10, 4, key, kink)
|
|
}
|
|
g.Finish(st)
|
|
})
|
|
return res
|
|
}
|
|
|
|
// matched is the longest end of trail that starts seq.
|
|
func matched(trail, seq []ui.Key) int {
|
|
for n := min(len(trail), len(seq)); n > 0; n-- {
|
|
if slices.Equal(trail[len(trail)-n:], seq[:n]) {
|
|
return n
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// glyph draws a key as an arrow or micro letter.
|
|
func glyph(g pixel.Canvas, x, y int, key ui.Key, ink pixel.Slot) {
|
|
switch {
|
|
case key == ui.KeyUp:
|
|
g.Sprite(x+2, y+1, tinyUp, 0, ink, 1)
|
|
case key == ui.KeyDown:
|
|
g.Sprite(x+2, y+1, tinyDown, 0, ink, 1)
|
|
case key == ui.KeyLeft:
|
|
g.Sprite(x+3, y, tinyLeft, 0, ink, 1)
|
|
case key == ui.KeyRight:
|
|
g.Sprite(x+3, y, tinyRight, 0, ink, 1)
|
|
case key >= ui.KeyA && key <= ui.KeyZ:
|
|
g.Micro(x+2, y, string(rune('A'+key-ui.KeyA)), ink, 1)
|
|
case key >= ui.Key0 && key <= ui.Key9:
|
|
g.Micro(x+2, y, string(rune('0'+key-ui.Key0)), ink, 1)
|
|
default:
|
|
g.Rect(x+2, y+1, 3, 3, ink)
|
|
}
|
|
}
|