Files
8bitMyGoUI/input/tokenfield.go
T
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

82 lines
2.4 KiB
Go

package input
import (
"log/slog"
"slices"
"strings"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/display"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
type TokenFieldOptions struct {
Placeholder string
// Width is in dots; zero means 200.
Width int
Disabled bool
}
type TokenFieldResult struct {
Element ui.Element
Changed bool
}
type tokenKey struct{}
// editorSlot keys the editor so added tags keep its focus.
type editorSlot struct{}
// TokenField shows tokens as tags; Enter or comma adds.
func TokenField(c *ui.Context, text string, tokens *[]string, o TokenFieldOptions) TokenFieldResult {
k := pixel.K(c)
e := ui.Row(c).Wrap().Width(k.D(pixel.Or("TokenField Width", o.Width, 200))).Padding(k.D(3), k.D(4)).Gap(k.D(3)).AlignItems(ui.Center)
e.Role(ui.RoleGroup).Label(text).Description(strings.Join(*tokens, ", "))
draft := ui.Local(e, tokenKey{}, func() string { return "" })
// The row stays enabled, so off is checked here.
off := o.Disabled || pixel.Current(c).Inert
res := TokenFieldResult{Element: e}
add := func(s string) {
if s = strings.TrimSpace(s); s != "" && !slices.Contains(*tokens, s) {
*tokens, res.Changed = append(*tokens, s), true
}
}
// A comma typed since the last frame cuts the draft.
if parts := strings.Split(*draft, ","); len(parts) > 1 && !off {
for _, p := range parts[:len(parts)-1] {
add(p)
}
*draft = parts[len(parts)-1]
}
if !off && pixel.Shortcut(e, 0, ui.KeyEnter) {
add(*draft)
*draft = ""
}
if !off && *draft == "" && len(*tokens) > 0 && pixel.Shortcut(e, 0, ui.KeyBackspace) {
*tokens, res.Changed = (*tokens)[:len(*tokens)-1], true
}
var ed pixel.Edit
e.Children(func() {
for i := 0; i < len(*tokens); i++ {
tok, removed := (*tokens)[i], false
ui.Box(c).Key(tok).Children(func() {
removed = display.Tag(c, tok, display.TagOptions{Removable: true, Disabled: o.Disabled}).Removed
})
if removed {
*tokens, res.Changed = slices.Delete(*tokens, i, i+1), true
i--
}
}
ui.Box(c).Key(editorSlot{}).Grow(1).MinWidth(k.D(40)).Children(func() {
ed = pixel.Editor(c, draft, pixel.EditorOptions{Label: text, Placeholder: o.Placeholder, Disabled: o.Disabled})
})
})
focus := ed.Focused
e.Draw(func(p *ui.Painter, r ui.Rect) { well(k.Canvas(p, r), pixel.Edge, focus) })
if res.Changed {
slog.Debug("8bit: tokens", "label", text, "tokens", len(*tokens))
}
return res
}