Upgrade MyGo to v0.5.5

MyGo v0.5.5 returns ui.Element as a checked value handle instead of a
pointer. Migrated with `mygo migrate-ui`, then fixed what the codemod
leaves to review:

- Element nil checks become .Valid(); absent elements are ui.Element{}.
- Input handlers and paint callbacks run outside the build pass, where
  elements expire and Context methods are inert: read IsDisabled into a
  local during the build, keep a ui.Handle for Focus, use c.Services()
  for clipboard access, create ui.Local state during the build.
- Add core.AutoFocus: v0.5.5 records an overlay's opener after its
  content builds, so same-frame autofocus lost focus restore on close.
  It focuses one frame later; cascader rows defer their focus likewise.
- core.OverlayLayer identifies build passes by the root element, not
  c.Now(), which repeats across passes and under the Tester clock.
- Context menus survive the opening press counted as PressedOutside;
  split button tooltip no longer reorders its open menu; disabled
  menubar rows pass presses to the panel; Form defers blur-started
  async checks so the growing message no longer moves Submit away
  from the release; Combobox drops its disabled copy now that MyGo
  blocks typing under a disabled wrapper (canary flipped to a guard).
- The Tester clock is virtual: tests use Advance/Now instead of
  time.Sleep/time.Now and read element state inside the view. The web
  mirror advances the clock to wall time before each frame and input.
- AGENTS.md and docs describe the value Element API and the new pin.

Snapshot goldens are darwin-only and were not re-verified here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-17 01:37:54 -04:00
1 parent bc716d27be
commit bcce34cda1
603 files changed
+1366 -1277

No files matched your search

+27 -12
View File
@@ -41,6 +41,14 @@ type mentionInputState struct {
panel ui.Rect // the popup's box last frame, which keeps it through a click
offset float32
changed, submitted bool
field ui.Handle
pointer []mentionPointer // presses and drags, hit-tested at the next build
}
// mentionPointer is a pointer event waiting for the build, where text can be measured.
type mentionPointer struct {
x float32
extend bool
}
// MentionInput edits *value with "@" mentions kept in *mentions; a mention is deleted and stepped over whole.
@@ -57,7 +65,7 @@ func MentionInput(c *ui.Context, value *string, mentions *[]Mention, opts Mentio
k := core.Tokens(c)
var r InputResult
look := &textInputLook{disabled: opts.Disabled, err: opts.Error}
r.Element, r.Input = textInputFrame(c, look, func(frame *ui.Element) *ui.Element {
r.Element, r.Input = textInputFrame(c, look, func(frame ui.Element) ui.Element {
in := ui.Box(c).Grow(1).MinWidth(core.Density(c).Unit() * 20).Focusable().Clip().Role(ui.RoleTextField).Label(opts.Label).Cursor(ui.CursorText)
s := *ui.Local(in, "mention_input", func() *mentionInputState { return &mentionInputState{at: -1} })
if s.doc.text == nil || s.synced != *value || !slices.Equal(s.syncedMentions, *mentions) {
@@ -66,16 +74,23 @@ func MentionInput(c *ui.Context, value *string, mentions *[]Mention, opts Mentio
s.undo, s.redo = nil, nil
s.synced, s.syncedMentions = *value, slices.Clone(*mentions)
}
for _, p := range s.pointer {
s.doc.moveTo(mentionInputHit(c, s, p.x), p.extend)
}
s.pointer = s.pointer[:0]
mentionInputQuery(s, opts)
px, py, _ := frame.PointerPosition()
fb := frame.Bounds()
onPanel := s.panel.Contains(fb.X+px, fb.Y+py)
popup := (in.Focused() || onPanel) && s.at >= 0 && !s.dismissed && len(s.found) > 0
disabled := in.IsDisabled()
in.Bind(&s.field)
svc := c.Services()
in.HandleInput(func(ev ui.InputEvent) bool {
if in.IsDisabled() {
if disabled {
return false
}
return mentionInputEvent(c, s, ev, popup, in)
return mentionInputEvent(svc, s, ev, popup)
})
if s.changed && !look.disabled {
*value, *mentions = string(s.doc.text), slices.Clone(s.doc.mentions)
@@ -110,7 +125,7 @@ func mentionInputQuery(s *mentionInputState, opts MentionInputOptions) {
}
// mentionInputEvent applies one input event to the document and reports whether it took it.
func mentionInputEvent(c *ui.Context, s *mentionInputState, ev ui.InputEvent, popup bool, in *ui.Element) bool {
func mentionInputEvent(svc ui.Services, s *mentionInputState, ev ui.InputEvent, popup bool) bool {
edit := func(f func()) {
s.undo = append(s.undo, s.doc.clone())
s.redo = nil
@@ -125,23 +140,23 @@ func mentionInputEvent(c *ui.Context, s *mentionInputState, ev ui.InputEvent, po
case ui.InputCompose:
s.compose = ev.Text
case ui.InputPointerDown:
in.Focus()
s.doc.moveTo(mentionInputHit(c, s, ev.X), extend)
s.field.Focus()
s.pointer = append(s.pointer, mentionPointer{ev.X, extend})
case ui.InputPointerMove:
if ev.Button != 0 {
return false
}
s.doc.moveTo(mentionInputHit(c, s, ev.X), true)
s.pointer = append(s.pointer, mentionPointer{ev.X, true})
case ui.InputCommand:
a, b := s.doc.span()
switch ev.Text {
case "copy", "cut":
c.WriteClipboard(string(s.doc.text[a:b]))
svc.WriteClipboard(string(s.doc.text[a:b]))
if ev.Text == "cut" && a != b {
edit(func() { s.doc.erase(false) })
}
case "paste":
edit(func() { s.doc.insert(c.ReadClipboard()) })
edit(func() { s.doc.insert(svc.ReadClipboard()) })
case "selectAll":
s.doc.anchor, s.doc.caret = 0, len(s.doc.text)
case "delete":
@@ -230,7 +245,7 @@ func mentionInputHit(c *ui.Context, s *mentionInputState, x float32) int {
}
// mentionInputView draws the text with mention chips, the selection and the caret, scrolled to keep the caret in view.
func mentionInputView(c *ui.Context, k theme.Tokens, s *mentionInputState, in *ui.Element, placeholder string) {
func mentionInputView(c *ui.Context, k theme.Tokens, s *mentionInputState, in ui.Element, placeholder string) {
size := core.FontSize(c, theme.BodySize)
measure := func(i int) float32 {
w, _ := c.MeasureText(0, ui.Span{Text: string(s.doc.text[:i]), Size: size})
@@ -298,7 +313,7 @@ func mentionInputSpans(k theme.Tokens, s *mentionInputState, size float32, place
}
// mentionInputPopup lists the candidates under the field; a click or Enter picks one.
func mentionInputPopup(c *ui.Context, k theme.Tokens, s *mentionInputState, frame, in *ui.Element) {
func mentionInputPopup(c *ui.Context, k theme.Tokens, s *mentionInputState, frame, in ui.Element) {
b := frame.Bounds()
u := core.Density(c).Unit()
ui.Overlay(c, func() {
@@ -311,7 +326,7 @@ func mentionInputPopup(c *ui.Context, k theme.Tokens, s *mentionInputState, fram
panel.Children(func() {
ui.Scroll(c).Shrink(1).Children(func() {
for i, m := range s.found {
it := ui.Row(c).Key(m.ID).MinHeight(core.Density(c).ControlHeight()).AlignItems(ui.Center).Padding(0, u*2).
it := ui.Row(c.Key(m.ID)).MinHeight(core.Density(c).ControlHeight()).AlignItems(ui.Center).Padding(0, u*2).
Radius(theme.ControlRadius).Role(ui.RoleListItem).Label(m.Name).Cursor(ui.CursorPointer)
if i == s.pick {
it.Selected(true).Background(k.Selection).TextColor(k.AccentText)