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

+2 -2
View File
@@ -42,7 +42,7 @@ type AlertListOptions struct {
// AlertListResult reports this frame's request.
type AlertListResult struct {
Element *ui.Element
Element ui.Element
// Ack and Silence report requested alert IDs; empty means idle.
Ack, Silence string
}
@@ -97,7 +97,7 @@ func alertRow(c *ui.Context, a AlertItem, locked bool) (ack, silence bool) {
sev := [...]core.Severity{core.SeverityInfo, core.SeverityWarning, core.SeverityError}[a.Level]
word := core.Msg(c, [...]string{"alert_list.info", "alert_list.warning", "alert_list.critical"}[a.Level])
silenced := a.Silence.After(c.Now())
row := ui.Row(c).Key(a.ID).Gap(u*3).AlignItems(ui.Center).MinHeight(d.ControlHeight()+u*2).Padding(u*2, u*3).
row := ui.Row(c.Key(a.ID)).Gap(u*3).AlignItems(ui.Center).MinHeight(d.ControlHeight()+u*2).Padding(u*2, u*3).
Label(word + ", " + a.Title)
if tone, _ := core.SeverityLook(k, sev); a.Level == AlertCritical && !a.Acked {
row.DrawOver(func(p *ui.Painter, r ui.Rect) { p.Fill(ui.Rect{X: r.X, Y: r.Y, W: 3, H: r.H}, tone, 0) })
+1 -1
View File
@@ -36,7 +36,7 @@ type ApiRequestBuilderOptions struct {
// ApiRequestResult reports this frame's events.
type ApiRequestResult struct {
Element *ui.Element
Element ui.Element
// Send asks the caller to send the request.
Send bool
Changed bool
+1 -1
View File
@@ -27,6 +27,6 @@ func devtoolsSeverity(c *ui.Context, s core.Severity, word string) {
}
// devtoolsTitle is a serif panel title.
func devtoolsTitle(c *ui.Context, s string) *ui.Element {
func devtoolsTitle(c *ui.Context, s string) ui.Element {
return ui.Text(c, s).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600)
}
+4 -5
View File
@@ -16,15 +16,14 @@ func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) {
snaptest.Run(t, name, w, h, snapshotTheme, view, nil)
}
// overlayUntil draws frames until cond holds, for at most 5 s of wall time.
// overlayUntil advances the Tester clock until cond holds, for at most 5 s.
func overlayUntil(tt *ui.Tester, cond func() bool) bool {
end := time.Now().Add(5 * time.Second)
end := tt.Now().Add(5 * time.Second)
for !cond() {
if time.Now().After(end) {
if tt.Now().After(end) {
return false
}
time.Sleep(2 * time.Millisecond)
tt.Frame()
tt.Advance(2 * time.Millisecond)
}
return true
}
+1 -1
View File
@@ -49,7 +49,7 @@ type Incident struct {
}
// IncidentCard shows an incident's title, status, impact and update timeline.
func IncidentCard(c *ui.Context, inc Incident) *ui.Element {
func IncidentCard(c *ui.Context, inc Incident) ui.Element {
if inc.Title == "" {
panic("mujica: IncidentCard needs a Title")
}
+1 -1
View File
@@ -30,7 +30,7 @@ type JsonEditorOptions struct {
// JsonEditorResult reports this frame's events and the parse state.
type JsonEditorResult struct {
Element *ui.Element
Element ui.Element
Changed bool
// Valid reports *text parses as one JSON value.
Valid bool
+1 -1
View File
@@ -43,7 +43,7 @@ type JsonViewerOptions struct {
// JsonViewerResult reports this frame's events.
type JsonViewerResult struct {
Element *ui.Element
Element ui.Element
// Copied is the path written to the clipboard, else "".
Copied string
}
+1 -1
View File
@@ -58,7 +58,7 @@ func KeyValueInput(c *ui.Context, rows *[]KeyValue, opts KeyValueInputOptions) c
r.Element.Children(func() {
agent.AgentRows(c, len(*rows), func(i int) {
kv := &(*rows)[i]
ui.Row(c).Key(i).Gap(u * 2).AlignItems(ui.Center).Children(func() {
ui.Row(c.Key(i)).Gap(u * 2).AlignItems(ui.Center).Children(func() {
name := fmt.Sprintf(core.Msg(c, "key_value_input.row"), i+1)
on := kv.Enabled
box := input.Checkbox(c, &on, "", input.CheckboxOptions{Disabled: locked}).Label(name)
+1 -1
View File
@@ -23,7 +23,7 @@ type RefreshIntervalSelectorOptions struct {
// RefreshResult reports this frame's events.
type RefreshResult struct {
Element *ui.Element
Element ui.Element
// Refresh requests caller loading, manually or after the interval.
Refresh bool
// Changed reports a new interval.
+5 -8
View File
@@ -31,8 +31,7 @@ func TestRefreshIntervalSelectorManualAndAuto(t *testing.T) {
if refreshes != 1 {
t.Fatalf("manual refreshes %d", refreshes)
}
time.Sleep(1100 * time.Millisecond)
tt.Frame()
tt.Advance(1100 * time.Millisecond)
if refreshes != 1 {
t.Fatal("refreshed while off")
}
@@ -41,11 +40,11 @@ func TestRefreshIntervalSelectorManualAndAuto(t *testing.T) {
if interval != time.Second || changes != 1 {
t.Fatalf("interval %v changes %d", interval, changes)
}
start := time.Now()
start := tt.Now()
if !overlayUntil(tt, func() bool { return refreshes == 2 }) {
t.Fatal("no automatic refresh")
}
if time.Since(start) < 900*time.Millisecond {
if tt.Now().Sub(start) < 900*time.Millisecond {
t.Fatal("refreshed before the interval")
}
}
@@ -56,15 +55,13 @@ func TestRefreshIntervalSelectorRefreshingAndDisabled(t *testing.T) {
refreshes, changes := 0, 0
tt := refreshTester(&interval, &opts, &refreshes, &changes)
_ = tt.Click("Refresh")
time.Sleep(1100 * time.Millisecond)
tt.Frame()
tt.Advance(1100 * time.Millisecond)
if refreshes != 0 || !tt.HasText("Refresh") {
t.Fatalf("refreshing: %d", refreshes)
}
opts = RefreshIntervalSelectorOptions{Intervals: []time.Duration{time.Second}, Disabled: true}
tt.Frame()
time.Sleep(1100 * time.Millisecond)
tt.Frame()
tt.Advance(1100 * time.Millisecond)
_ = tt.Click("Refresh")
if refreshes != 0 {
t.Fatalf("disabled refreshed %d", refreshes)
+1 -1
View File
@@ -66,7 +66,7 @@ func ResourceGauge(c *ui.Context, readings []ResourceReading, opts ResourceGauge
panic(fmt.Sprintf("mujica: ResourceGauge reading %d needs a Name and 0 <= Used <= Total", i))
}
frac := r.Used / r.Total
ui.Column(c).Key(r.Name).Width(200).Gap(u).AlignItems(ui.Center).Children(func() {
ui.Column(c.Key(r.Name)).Width(200).Gap(u).AlignItems(ui.Center).Children(func() {
ui.Text(c, r.Name).FontWeight(600)
gauge := chart.Gauge(c, frac*100, chart.GaugeOptions{
Frame: chart.ChartFrame{Label: r.Name, Height: h, Status: opts.Status, Error: opts.Error}, Min: 0, Max: 100, Unit: "%",
+2 -2
View File
@@ -35,7 +35,7 @@ type ServiceStatusOptions struct {
}
// ServiceStatus lists service health and an overall summary.
func ServiceStatus(c *ui.Context, services []ServiceItem, opts ServiceStatusOptions) *ui.Element {
func ServiceStatus(c *ui.Context, services []ServiceItem, opts ServiceStatusOptions) ui.Element {
if opts.MaxHeight < 0 {
panic("mujica: ServiceStatus MaxHeight is negative")
}
@@ -75,7 +75,7 @@ func ServiceStatus(c *ui.Context, services []ServiceItem, opts ServiceStatusOpti
agent.AgentRows(c, len(services), func(i int) {
s := services[i]
word := serviceWord(c, s.Health)
ui.Row(c).Key(i).Gap(u*3).AlignItems(ui.Center).MinHeight(core.Density(c).ControlHeight()).Padding(u, u*3).
ui.Row(c.Key(i)).Gap(u*3).AlignItems(ui.Center).MinHeight(core.Density(c).ControlHeight()).Padding(u, u*3).
Label(s.Name + ", " + word).Children(func() {
ui.Column(c).Grow(1).Shrink(1).Children(func() {
ui.Text(c, s.Name).SingleLine()
+1 -1
View File
@@ -56,7 +56,7 @@ func SystemMonitor(c *ui.Context, times []time.Time, metrics []MonitorMetric, op
return
}
for _, m := range metrics {
panel := ui.Column(c).Key(m.Name).Gap(u)
panel := ui.Column(c.Key(m.Name)).Gap(u)
series := ui.Local(panel, "mujica.monitor.series", func() []chart.ChartSeries { return []chart.ChartSeries{{Name: m.Name, Values: m.Samples}} })
(*series)[0].Name, (*series)[0].Values = m.Name, m.Samples
panel.Children(func() {
+1 -1
View File
@@ -142,7 +142,7 @@ func traceSpanRow(c *ui.Context, s WaterfallSpan, depth int, total time.Duration
k := core.Tokens(c)
d := core.Density(c)
u := d.Unit()
row := ui.ButtonBase(c).Key(s.ID).Justify(ui.Start).Gap(u*2).MinHeight(d.ControlHeight()).Padding(0, u*2).
row := ui.ButtonBase(c.Key(s.ID)).Justify(ui.Start).Gap(u*2).MinHeight(d.ControlHeight()).Padding(0, u*2).
Label(fmt.Sprintf("%s, %s, %s", s.Name, s.Service, agent.AgentDuration(s.Duration)))
internal.Ring(row, k.Focus, theme.ControlRadius)
if selected {