Files
ZacharyZhang-NYandClaude Opus 5.5 03e1f91706 Upgrade MyGo to v0.5.5
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>
2026-10-17 01:31:08 -04:00

133 lines
3.0 KiB
Go

package gallery
import (
"log/slog"
"strings"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
// Dot is the gallery's DIPs per dot.
const Dot = 2
// App is the gallery that native and web share.
type App struct {
router *ui.Router
palette int
locale pixel.Locale
started bool
query string
search input.TextInputResult
logs map[string][]string
now func() time.Time
}
// New starts at path; the boot screen shows first.
func New(path string) *App {
r := ui.NewRouter(path)
r.Transition = ui.TransitionNone
return &App{router: r, logs: map[string][]string{}}
}
// Path is the page shown, for the web address bar.
func (a *App) Path() string { return a.router.Location() }
// Go opens path, skipping boot and clearing search.
func (a *App) Go(path string) {
a.started, a.query = true, ""
if path != a.router.Location() {
slog.Debug("8bit: gallery go", "path", path)
a.router.Push(path)
}
}
// SetClock gives the components a clock other than the frame's.
func (a *App) SetClock(now func() time.Time) { a.now = now }
func (a *App) View(c *ui.Context) {
pixel.Use(c, pixel.Settings{Palette: pixel.Palettes[a.palette], Dot: Dot, Locale: a.locale, Now: a.now})
if !a.started {
a.boot(c)
return
}
ui.Column(c).Fill().Children(func() {
a.topBar(c)
ui.Row(c).Grow(1).AlignItems(ui.Stretch).Children(func() {
a.sidebar(c)
ui.Column(c).Grow(1).Children(func() { a.content(c) })
})
a.statusBar(c)
})
a.keys(c)
}
// keys leaves single keys to a field being typed in.
func (a *App) keys(c *ui.Context) {
if pixel.Typing(c) {
return
}
switch {
case c.Shortcut(0, ui.KeySlash):
a.search.Input.Focus()
case c.Shortcut(0, ui.KeyP):
a.setPalette(a.palette + 1)
case c.Shortcut(ui.Shift, ui.KeyP):
a.setPalette(a.palette - 1)
case c.Shortcut(0, ui.KeyBracketLeft):
a.step(-1)
case c.Shortcut(0, ui.KeyBracketRight):
a.step(1)
}
}
func (a *App) setPalette(i int) {
n := len(pixel.Palettes)
a.palette = (i%n + n) % n
slog.Debug("8bit: gallery palette", "name", pixel.Palettes[a.palette].Name)
}
// step moves to the previous or next entry.
func (a *App) step(d int) {
all := Entries()
for i, e := range all {
if e.Path() == a.router.Path() {
a.Go(all[((i+d)%len(all)+len(all))%len(all)].Path())
return
}
}
}
// logger records a demo's events for the log panel.
func (a *App) logger(e Entry) func(string) {
return func(ev string) {
// The panel shows what was typed; logs never do.
slog.Debug("8bit: demo", "entry", e.Slug())
l := append(a.logs[e.Slug()], ev)
a.logs[e.Slug()] = l[max(0, len(l)-8):]
}
}
func find(slug string) (Entry, bool) {
for _, e := range Entries() {
if e.Slug() == slug {
return e, true
}
}
return Entry{}, false
}
func matches(q string) []Entry {
q = strings.ToLower(strings.TrimSpace(q))
var out []Entry
for _, e := range Entries() {
if strings.Contains(strings.ToLower(e.Name+" "+e.Zh+" "+e.Slug()), q) {
out = append(out, e)
}
}
return out
}