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

+1 -1
View File
@@ -143,7 +143,7 @@ func (g *gallery) statusBar(c *ui.Context) {
}
func (g *gallery) content(c *ui.Context) {
ui.Box(c).Key(g.selected).FillWidth().Children(func() {
ui.Box(c.Key(g.selected)).FillWidth().Children(func() {
switch g.selected {
case overtureID:
g.overturePage(c)
+1 -1
View File
@@ -319,7 +319,7 @@ func (g *gallery) renameDialog(c *ui.Context, log func(string)) {
commit()
}
}}, func() {
input.FormField(c, g.t("New name", "新名称"), input.FormFieldOptions{Error: bad}, func() *ui.Element {
input.FormField(c, g.t("New name", "新名称"), input.FormFieldOptions{Error: bad}, func() ui.Element {
r := input.TextInput(c, &s.newName, input.TextInputOptions{Error: bad})
if r.Submitted() {
commit()
+5 -5
View File
@@ -135,7 +135,7 @@ func (g *gallery) formFields(c *ui.Context, f *input.FormScope, log func(string)
s.checks <- formAsync{version, msg}
c.Invalidate()
}()
}}, func() *ui.Element {
}}, func() ui.Element {
return input.TextInput(c, &s.name, input.TextInputOptions{}).Input
})
input.FormItem(c, f, "password", &s.password, input.FormItemOptions[string]{Label: g.t("Password", "密码"), Required: true,
@@ -144,14 +144,14 @@ func (g *gallery) formFields(c *ui.Context, f *input.FormScope, log func(string)
return g.t("Use at least 8 characters.", "至少 8 个字符。")
}
return ""
}}, func() *ui.Element {
}}, func() ui.Element {
r := input.PasswordInput(c, &s.password, input.PasswordInputOptions{})
if r.Submitted() {
s.st.Submit()
}
return r.Input
})
input.FormItem(c, f, "debut", &s.debut, input.FormItemOptions[core.Date]{Label: g.t("Debut date", "首演日期"), Required: true}, func() *ui.Element {
input.FormItem(c, f, "debut", &s.debut, input.FormItemOptions[core.Date]{Label: g.t("Debut date", "首演日期"), Required: true}, func() ui.Element {
r := datetime.DatePicker(c, &s.debut, datetime.DatePickerOptions{Clearable: true})
if r.Changed() {
log("debut " + s.debut.Format("2006-01-02"))
@@ -159,7 +159,7 @@ func (g *gallery) formFields(c *ui.Context, f *input.FormScope, log func(string)
return r.Input
})
input.FormItem(c, f, "roles", &s.tags, input.FormItemOptions[[]string]{Label: g.t("Roles", "担当"), Required: true,
Description: g.t("Up to 4; press Enter after each.", "最多 4 个;每个后按 Enter。")}, func() *ui.Element {
Description: g.t("Up to 4; press Enter after each.", "最多 4 个;每个后按 Enter。")}, func() ui.Element {
r := input.TagsInput(c, &s.tags, input.TagsInputOptions{Max: 4, Invalid: s.st.Error("roles") != "", Placeholder: g.t("keyboard, bass…", "键盘、贝斯…")})
if r.Changed() {
log(fmt.Sprintf("roles %q", s.tags))
@@ -172,7 +172,7 @@ func (g *gallery) formFields(c *ui.Context, f *input.FormScope, log func(string)
return g.t("Accept the terms to continue.", "请先同意条款。")
}
return ""
}}, func() *ui.Element {
}}, func() ui.Element {
e := input.Checkbox(c, &s.terms, g.t("I accept the covenant of the masquerade", "我接受假面舞会的盟约"), input.CheckboxOptions{})
if e.Changed() {
log(fmt.Sprintf("terms %v", s.terms))
+12 -12
View File
@@ -79,14 +79,14 @@ func (g *gallery) settingsGeneral(c *ui.Context, log func(string)) {
settingsPanel(c, func() {
bad := g.workspaceError(d.workspace)
input.FormField(c, g.t("Workspace name", "工作区名称"), input.FormFieldOptions{Required: true, Error: bad,
Description: g.t("Shown in the title bar.", "显示在标题栏中。")}, func() *ui.Element {
Description: g.t("Shown in the title bar.", "显示在标题栏中。")}, func() ui.Element {
r := input.TextInput(c, &d.workspace, input.TextInputOptions{Error: bad})
if r.Changed() {
log(fmt.Sprintf("workspace %q", d.workspace))
}
return r.Input
})
input.FormField(c, g.t("Language", "语言"), input.FormFieldOptions{}, func() *ui.Element {
input.FormField(c, g.t("Language", "语言"), input.FormFieldOptions{}, func() ui.Element {
r := input.Select(c, &d.language, []input.SelectOption[string]{
{Value: "en", Label: "English"}, {Value: "zh", Label: "简体中文"}, {Value: "ja", Label: "日本語"},
}, input.SelectOptions{})
@@ -95,7 +95,7 @@ func (g *gallery) settingsGeneral(c *ui.Context, log func(string)) {
}
return r.Element
})
input.FormField(c, g.t("On startup", "启动时"), input.FormFieldOptions{}, func() *ui.Element {
input.FormField(c, g.t("On startup", "启动时"), input.FormFieldOptions{}, func() ui.Element {
r := input.RadioGroup(c, &d.startPage, []input.RadioItem[string]{
{Value: "blank", Label: g.t("Open a blank page", "打开空白页")},
{Value: "last", Label: g.t("Reopen the last session", "恢复上次会话")},
@@ -106,7 +106,7 @@ func (g *gallery) settingsGeneral(c *ui.Context, log func(string)) {
}
return r.Element
})
input.FormField(c, g.t("Login item", "登录项"), input.FormFieldOptions{}, func() *ui.Element {
input.FormField(c, g.t("Login item", "登录项"), input.FormFieldOptions{}, func() ui.Element {
e := input.Switch(c, &d.launch, g.t("Launch at login", "登录时启动"), input.SwitchOptions{})
if e.Changed() {
log(fmt.Sprintf("launch at login %v", d.launch))
@@ -119,7 +119,7 @@ func (g *gallery) settingsGeneral(c *ui.Context, log func(string)) {
func (g *gallery) settingsAppearance(c *ui.Context, log func(string)) {
d := &g.cSettings.draft
settingsPanel(c, func() {
input.FormField(c, g.t("Theme", "主题"), input.FormFieldOptions{}, func() *ui.Element {
input.FormField(c, g.t("Theme", "主题"), input.FormFieldOptions{}, func() ui.Element {
r := input.RadioGroup(c, &d.theme, []input.RadioItem[string]{
{Value: "system", Label: g.t("System", "跟随系统")}, {Value: "light", Label: g.t("Light", "浅色")}, {Value: "dark", Label: g.t("Dark", "深色")},
}, input.RadioGroupOptions{Horizontal: true})
@@ -128,7 +128,7 @@ func (g *gallery) settingsAppearance(c *ui.Context, log func(string)) {
}
return r.Element
})
input.FormField(c, g.t("Text size", "文字大小"), input.FormFieldOptions{}, func() *ui.Element {
input.FormField(c, g.t("Text size", "文字大小"), input.FormFieldOptions{}, func() ui.Element {
r := input.Slider(c, &d.fontSize, input.SliderOptions{Min: 12, Max: 20, Step: 1, ShowValue: true,
Marks: []input.SliderMark{{Value: 12, Label: "12"}, {Value: 16, Label: "16"}, {Value: 20, Label: "20"}},
Format: func(v float64) string { return fmt.Sprintf("%.0f pt", v) }})
@@ -137,14 +137,14 @@ func (g *gallery) settingsAppearance(c *ui.Context, log func(string)) {
}
return r.Element
})
input.FormField(c, g.t("Sidebar width", "侧栏宽度"), input.FormFieldOptions{Description: g.t("In DIPs, from 180 to 360.", "单位 DIP,180 至 360。")}, func() *ui.Element {
input.FormField(c, g.t("Sidebar width", "侧栏宽度"), input.FormFieldOptions{Description: g.t("In DIPs, from 180 to 360.", "单位 DIP,180 至 360。")}, func() ui.Element {
r := input.NumberInput(c, &d.sidebar, input.NumberInputOptions{Min: 180, Max: 360, Step: 10})
if r.Changed() {
log(fmt.Sprintf("sidebar width %.0f", d.sidebar))
}
return r.Input
})
input.FormField(c, g.t("Motion", "动效"), input.FormFieldOptions{}, func() *ui.Element {
input.FormField(c, g.t("Motion", "动效"), input.FormFieldOptions{}, func() ui.Element {
e := input.Switch(c, &d.reduce, g.t("Reduce motion", "减少动效"), input.SwitchOptions{})
if e.Changed() {
log(fmt.Sprintf("reduce motion %v", d.reduce))
@@ -157,28 +157,28 @@ func (g *gallery) settingsAppearance(c *ui.Context, log func(string)) {
func (g *gallery) settingsPrivacy(c *ui.Context, log func(string)) {
d := &g.cSettings.draft
settingsPanel(c, func() {
input.FormField(c, g.t("Usage data", "使用数据"), input.FormFieldOptions{Description: g.t("Nothing leaves this demo.", "本示例不会发送任何数据。")}, func() *ui.Element {
input.FormField(c, g.t("Usage data", "使用数据"), input.FormFieldOptions{Description: g.t("Nothing leaves this demo.", "本示例不会发送任何数据。")}, func() ui.Element {
e := input.Switch(c, &d.telemetry, g.t("Share anonymous usage data", "共享匿名使用数据"), input.SwitchOptions{})
if e.Changed() {
log(fmt.Sprintf("usage data %v", d.telemetry))
}
return e
})
input.FormField(c, g.t("Crash reports", "崩溃报告"), input.FormFieldOptions{}, func() *ui.Element {
input.FormField(c, g.t("Crash reports", "崩溃报告"), input.FormFieldOptions{}, func() ui.Element {
e := input.Switch(c, &d.crashes, g.t("Send crash reports", "发送崩溃报告"), input.SwitchOptions{})
if e.Changed() {
log(fmt.Sprintf("crash reports %v", d.crashes))
}
return e
})
input.FormField(c, g.t("Keep history", "保留历史"), input.FormFieldOptions{Description: g.t("Days, from 0 to 365; 0 keeps none.", "天数,0 至 365;0 表示不保留。")}, func() *ui.Element {
input.FormField(c, g.t("Keep history", "保留历史"), input.FormFieldOptions{Description: g.t("Days, from 0 to 365; 0 keeps none.", "天数,0 至 365;0 表示不保留。")}, func() ui.Element {
r := input.NumberInput(c, &d.history, input.NumberInputOptions{Min: 0, Max: 365, Step: 1})
if r.Changed() {
log(fmt.Sprintf("history %.0f days", d.history))
}
return r.Input
})
input.FormField(c, g.t("Lock after", "自动锁定"), input.FormFieldOptions{}, func() *ui.Element {
input.FormField(c, g.t("Lock after", "自动锁定"), input.FormFieldOptions{}, func() ui.Element {
r := input.Select(c, &d.lock, []input.SelectOption[string]{
{Value: "never", Label: g.t("Never", "从不")}, {Value: "5", Label: g.t("5 minutes", "5 分钟")},
{Value: "15", Label: g.t("15 minutes", "15 分钟")}, {Value: "60", Label: g.t("1 hour", "1 小时")},
+1 -2
View File
@@ -26,8 +26,7 @@ func click(t *testing.T, tt *ui.Tester, label string) {
// await renders frames until done holds or 5 seconds pass.
func await(tt *ui.Tester, done func() bool) {
for deadline := time.Now().Add(5 * time.Second); !done() && time.Now().Before(deadline); {
time.Sleep(20 * time.Millisecond)
tt.Frame()
tt.Advance(20 * time.Millisecond)
}
}
+1 -1
View File
@@ -104,7 +104,7 @@ func (g *gallery) categoryGrid(c *ui.Context, width float32) {
}
layout.Grid(c, layout.GridOptions{Columns: tracks}, func() {
for _, cat := range cats {
ui.Box(c).Key(cat).Children(func() {
ui.Box(c.Key(cat)).Children(func() {
data.Card(c, data.CardOptions{Title: g.categoryName(cat), Footer: func() {
if input.Button(c, g.t("Enter", "进入"), input.ButtonOptions{Variant: input.Outline}).Clicked() {
g.selected = first[cat].ID
+1 -1
View File
@@ -91,7 +91,7 @@ func accountSettingsPage(c *ui.Context, d *accountSettingsState, log func(string
// accountSwitchRow is a settings row with a switch.
func accountSwitchRow(c *ui.Context, id, label, desc string, v *bool, log func(string)) {
account.SettingsRow(c, label, account.SettingsRowOptions{ID: id, Description: desc}, func() *ui.Element {
account.SettingsRow(c, label, account.SettingsRowOptions{ID: id, Description: desc}, func() ui.Element {
sw := input.Switch(c, v, "", input.SwitchOptions{})
if sw.Changed() {
log(fmt.Sprintf("%s %v", label, *v))
+1 -1
View File
@@ -192,7 +192,7 @@ func accountCoachDemo(c *ui.Context, log func(string)) {
d.open, d.step = true, 0
log("tour started")
}
var compose, archive *ui.Element
var compose, archive ui.Element
ui.Row(c).Gap(48).Padding(8).Children(func() {
compose = input.Button(c, "Compose", input.ButtonOptions{Disabled: d.disabled})
archive = input.Button(c, "Archive", input.ButtonOptions{Variant: input.Secondary})
+1 -1
View File
@@ -115,7 +115,7 @@ func c2PartsChart(c *ui.Context, frame chart.ChartFrame, show bool, s *c2PartsSt
Tooltip: func(i int) []chart.ChartTooltipLine {
return []chart.ChartTooltipLine{{Text: x.Label(pts[i][0])}, {Text: "Pews " + y.Label(pts[i][1]), Color: colors[0]}, {Text: "Candles " + y.Label(pts[i][1]/2), Color: colors[1], Shape: 1}}
},
Input: func(el *ui.Element, r ui.Rect) {
Input: func(el ui.Element, r ui.Rect) {
if annotation.Drag(el, plotOf(r)) {
log("ChartAnnotation: moved")
}
+1 -1
View File
@@ -82,7 +82,7 @@ var chatmsgBlockPages = []page{
inputFlags(c, log, map[string]*bool{"LineNumbers": &s.numbers, "Wrap at first": &s.wrap}, "LineNumbers", "Wrap at first")
})
inputSection(c, "Live", func() {
ui.Column(c).Key(s.lang + fmt.Sprint(s.wrap)).Children(func() {
ui.Column(c.Key(s.lang + fmt.Sprint(s.wrap))).Children(func() {
if chat.CodeBlock(c, samples[s.lang], chat.CodeBlockOptions{Language: s.lang, LineNumbers: s.numbers, Wrap: s.wrap}).Copied() {
log("code copied")
}
+1 -1
View File
@@ -98,7 +98,7 @@ func chatDemoConversationItem(c *ui.Context, log func(string)) {
ui.Column(c).Width(420).Gap(4).Children(func() {
for i := range s.convs {
conv := &s.convs[i]
ui.Row(c).Key(conv.ID).Children(func() {
ui.Row(c.Key(conv.ID)).Children(func() {
v := chat.ConversationItem(c, conv, chat.ConversationItemOptions{Selected: s.selected && i == 0, Disabled: s.disabled})
switch {
case v.Opened:
+1 -1
View File
@@ -286,7 +286,7 @@ func tagDemo(c *ui.Context, log func(string)) {
for i := 0; i < len(s.tags); i++ {
t := s.tags[i]
var v display.TagView
ui.Box(c).Key(t).Children(func() {
ui.Box(c.Key(t)).Children(func() {
v = display.Tag(c, t, display.TagOptions{Icon: icon, Clickable: s.clickable, Closable: s.closable, Disabled: s.dis})
})
if v.Clicked {
+1 -1
View File
@@ -22,7 +22,7 @@ func displayControls(c *ui.Context, fn func()) {
}
// displayLog logs a control's new value when it changed.
func displayLog[T any](log func(string), e *ui.Element, name string, v *T) {
func displayLog[T any](log func(string), e ui.Element, name string, v *T) {
if e.Changed() {
log(fmt.Sprintf("%s = %v", name, *v))
}
+2 -2
View File
@@ -44,7 +44,7 @@ func mentionInputDemo(c *ui.Context, log func(string)) {
log("mention: submitted")
}
for _, m := range s.mentions {
ui.Text(c, fmt.Sprintf("@%s → %s", m.Name, m.ID)).Key(m.ID)
ui.Text(c.Key(m.ID), fmt.Sprintf("@%s → %s", m.Name, m.ID))
}
})
})
@@ -139,7 +139,7 @@ func undoToastDemo(c *ui.Context, log func(string)) {
ui.Slider(c, &s.seconds, 2, 10).Width(200)
})
for i, n := range s.notes {
ui.Row(c).Key(n).Gap(12).AlignItems(ui.Center).Children(func() {
ui.Row(c.Key(n)).Gap(12).AlignItems(ui.Center).Children(func() {
ui.Text(c, n).Grow(1)
if input.Button(c, "Delete", input.ButtonOptions{Variant: input.Ghost}).Clicked() {
s.removed, s.open = n, true
+4 -4
View File
@@ -131,7 +131,7 @@ var formPages = []page{
async <- formAsync{version, msg}
c.Invalidate()
}()
}}, func() *ui.Element {
}}, func() ui.Element {
return input.InputGroup(c, &d.name, input.InputGroupOptions{Invalid: d.st.Error("name") != ""}).Input
})
input.FormItem(c, f, "email", &d.email, input.FormItemOptions[string]{Label: "Email", Required: true,
@@ -140,14 +140,14 @@ var formPages = []page{
return "Enter an email address."
}
return ""
}}, func() *ui.Element {
}}, func() ui.Element {
g := input.InputGroup(c, &d.email, input.InputGroupOptions{Invalid: d.st.Error("email") != ""})
if g.Submitted() {
d.st.Submit()
}
return g.Input
})
input.FormItem(c, f, "tags", &d.tags, input.FormItemOptions[[]string]{Label: "Roles", Required: true, Initial: []string{"keyboard"}}, func() *ui.Element {
input.FormItem(c, f, "tags", &d.tags, input.FormItemOptions[[]string]{Label: "Roles", Required: true, Initial: []string{"keyboard"}}, func() ui.Element {
return input.TagsInput(c, &d.tags, input.TagsInputOptions{Max: 4, Invalid: d.st.Error("tags") != ""}).Input
})
})
@@ -177,7 +177,7 @@ var formPages = []page{
e = "This name is already in the guest list."
}
input.FormField(c, "Guest name", input.FormFieldOptions{Required: d.required, Horizontal: d.horizontal,
Description: "As written on the invitation.", Error: e}, func() *ui.Element {
Description: "As written on the invitation.", Error: e}, func() ui.Element {
g := input.InputGroup(c, &d.value, input.InputGroupOptions{Invalid: d.withError})
if g.Changed() {
log(fmt.Sprintf("value %q", d.value))
+1 -1
View File
@@ -68,7 +68,7 @@ func layoutSlider(c *ui.Context, v *float64, label string, lo, hi float64, log f
func layoutControls(c *ui.Context, fn func()) { ui.Row(c).Wrap().Gap(12).Children(fn) }
func layoutFrame(c *ui.Context, h float32, fn func()) *ui.Element {
func layoutFrame(c *ui.Context, h float32, fn func()) ui.Element {
return ui.Column(c).Height(h).Border(1, core.Tokens(c).Border).Radius(4).Clip().Children(fn)
}
+1 -1
View File
@@ -87,7 +87,7 @@ func mktDemo(c *ui.Context, log func(string), id string) {
}
case "149":
for _, chartID := range []string{"price", "other"} {
ui.Column(c).Key(chartID).Children(func() {
ui.Column(c.Key(chartID)).Children(func() {
if s.sync.Link(c, chartID).Changed() {
log("Sync: " + chartID)
}
+3 -3
View File
@@ -14,7 +14,7 @@ import (
)
// motionLoaderDemo shows a loader while loading.
func motionLoaderDemo(id string, loader func(c *ui.Context, o feedback.LoaderOptions) *ui.Element) func(c *ui.Context, log func(string)) {
func motionLoaderDemo(id string, loader func(c *ui.Context, o feedback.LoaderOptions) ui.Element) func(c *ui.Context, log func(string)) {
return func(c *ui.Context, log func(string)) {
type state struct {
loading, label bool
@@ -92,7 +92,7 @@ func motionTypewriterDemo(c *ui.Context, log func(string)) {
log("Restarted")
}
}, func() {
ui.Column(c).Key(s.round).Children(func() {
ui.Column(c.Key(s.round)).Children(func() {
if feedback.Typewriter(c, s.text, feedback.TypewriterOptions{Speed: s.speed, Skippable: true}).Changed() {
log("Finished")
}
@@ -127,7 +127,7 @@ func motionStaggerDemo(c *ui.Context, log func(string)) {
log(fmt.Sprintf("Replay %d", s.round))
}
}, func() {
ui.Column(c).Key(s.round).Children(func() {
ui.Column(c.Key(s.round)).Children(func() {
feedback.Stagger(c, len(guests), feedback.StaggerOptions{Interval: time.Duration(s.interval) * time.Millisecond, Horizontal: s.horizontal}, func(i int) {
ui.Text(c, guests[i]+" arrives").Padding(6, 10).Background(core.Tokens(c).Surface).Radius(4).Margin(0, 0, 4, 0)
})
+2 -2
View File
@@ -298,8 +298,8 @@ func hoverCardDemo(c *ui.Context, log func(string)) {
log("follow")
}
})
if (card != nil) != s.shown {
s.shown = card != nil
if card.Valid() != s.shown {
s.shown = card.Valid()
log(fmt.Sprintf("card shown = %v", s.shown))
}
}
+8
View File
@@ -81,6 +81,7 @@ func (m *mirror) loop() {
case <-time.After(wait):
}
m.mu.Lock()
m.sync()
m.tt.Frame()
changed := m.capture()
m.mu.Unlock()
@@ -92,6 +93,13 @@ func (m *mirror) loop() {
}
}
// sync moves the Tester's virtual clock up to the wall clock; the caller holds mu.
func (m *mirror) sync() {
if d := time.Since(m.tt.Now()); d > 0 {
m.tt.Advance(d)
}
}
func (m *mirror) kick() {
select {
case m.wake <- struct{}{}:
+1
View File
@@ -36,6 +36,7 @@ func (m *mirror) serveInput(w http.ResponseWriter, r *http.Request) {
return
}
m.mu.Lock()
m.sync()
err := m.applyWatchingClipboard(in)
if err == nil {
m.capture()