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

+4 -4
View File
@@ -31,7 +31,7 @@ type BreakpointListOptions struct {
// BreakpointListResult fields hold breakpoint IDs.
type BreakpointListResult struct {
Element *ui.Element
Element ui.Element
Toggled, Edited, Opened string
}
@@ -67,7 +67,7 @@ func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions
layout.ScrollArea(c, nil, layout.ScrollAreaOptions{}, func() {
for i := range *bps {
b := &(*bps)[i]
ui.Column(c).Key(b.ID).Padding(u, u*2).Gap(u).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(func() {
ui.Column(c.Key(b.ID)).Padding(u, u*2).Gap(u).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(func() {
ui.Row(c).MinHeight(d.ControlHeight()).Gap(u * 2).AlignItems(ui.Center).Children(func() {
on := b.Enabled
input.Checkbox(c, &on, "", input.CheckboxOptions{Disabled: opts.Disabled}).Label(fmt.Sprintf(core.Msg(c, "breakpoint_list.enable"), path.Base(b.File), b.Line))
@@ -86,7 +86,7 @@ func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions
switch {
case edit.id == b.ID:
in := input.TextInput(c, &edit.draft, input.TextInputOptions{Placeholder: core.Msg(c, "breakpoint_list.placeholder"), Label: core.Msg(c, "breakpoint_list.condition")})
in.Input.AutoFocus()
core.AutoFocus(c, in.Input)
if in.Input.Shortcut(0, ui.KeyEscape) {
edit.id = ""
} else if in.Submitted() && !disabled {
@@ -104,7 +104,7 @@ func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions
}
// breakpointLocation is a button opening the location.
func breakpointLocation(c *ui.Context, b Breakpoint) *ui.Element {
func breakpointLocation(c *ui.Context, b Breakpoint) ui.Element {
k := core.Tokens(c)
d := core.Density(c)
e := ui.ButtonBase(c).Grow(1).MinWidth(0).MinHeight(theme.MinHitSize).Justify(ui.Start).Gap(d.Unit()*2).Padding(0, d.Unit()).
+5 -5
View File
@@ -20,12 +20,12 @@ func codeFont(c *ui.Context) ui.Font {
}
// CodeMono is a single-line monospace text.
func CodeMono(c *ui.Context, s string) *ui.Element {
func CodeMono(c *ui.Context, s string) ui.Element {
return ui.Text(c, s).Font(theme.MonoFont).FontSize(core.FontSize(c, CodeSize)).SingleLine()
}
// CodeCaption is muted caption text.
func CodeCaption(c *ui.Context, s string) *ui.Element {
func CodeCaption(c *ui.Context, s string) ui.Element {
return ui.Text(c, s).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(core.Tokens(c).TextMuted).SingleLine()
}
@@ -116,13 +116,13 @@ func CodeMatchSpans(c *ui.Context, text, query string) []ui.Span {
}
// CodePanel is a framed surface holding a code tool.
func CodePanel(c *ui.Context) *ui.Element {
func CodePanel(c *ui.Context) ui.Element {
k := core.Tokens(c)
return ui.Column(c).MinHeight(0).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Clip()
}
// CodeHeader is a panel's top strip of tools.
func CodeHeader(c *ui.Context, fn func()) *ui.Element {
func CodeHeader(c *ui.Context, fn func()) ui.Element {
k := core.Tokens(c)
u := core.Density(c).Unit()
return ui.Row(c).Shrink(0).MinHeight(theme.MinHitSize+2*u).Padding(u, u*2).Gap(u*2).AlignItems(ui.Center).
@@ -140,7 +140,7 @@ func CodeSelectMark(c *ui.Context, on bool) {
}
// CodeRows builds a growing virtual list with marks.
func CodeRows(c *ui.Context, s *ui.ListState, n int, row func(i int)) *ui.Element {
func CodeRows(c *ui.Context, s *ui.ListState, n int, row func(i int)) ui.Element {
d := core.Density(c)
restore := data.DataSelectTheme(c, 0)
defer restore()
+1 -1
View File
@@ -140,7 +140,7 @@ func CodeViewer(c *ui.Context, lines []string, s *CodeViewerState, opts CodeView
}
// codeViewerKeys copies the selection and folds with Left and Right.
func codeViewerKeys(c *ui.Context, list *ui.Element, lines []string, s *CodeViewerState) {
func codeViewerKeys(c *ui.Context, list ui.Element, lines []string, s *CodeViewerState) {
if list.IsDisabled() {
return
}
+1 -1
View File
@@ -38,7 +38,7 @@ type CommandHistoryOptions struct {
// CommandHistoryResult carries the entry to replay.
type CommandHistoryResult struct {
Element *ui.Element
Element ui.Element
// Replayed is the entry to run again, or nil.
Replayed *HistoryEntry
}
+6 -5
View File
@@ -34,7 +34,7 @@ type CompletionMenuOptions struct {
// CompletionMenuResult carries the accepted candidate.
type CompletionMenuResult struct {
Element *ui.Element
Element ui.Element
Accepted *Completion
}
@@ -42,8 +42,8 @@ type CompletionMenuResult struct {
const completionRows = 8
// CompletionMenu shows candidates under the editor anchor.
func CompletionMenu(c *ui.Context, anchor *ui.Element, open *bool, items []Completion, s *CompletionMenuState, opts CompletionMenuOptions) CompletionMenuResult {
if anchor == nil || open == nil || s == nil {
func CompletionMenu(c *ui.Context, anchor ui.Element, open *bool, items []Completion, s *CompletionMenuState, opts CompletionMenuOptions) CompletionMenuResult {
if !anchor.Valid() || open == nil || s == nil {
panic("mujica: CompletionMenu needs an anchor, open and a state")
}
for _, it := range items {
@@ -70,8 +70,9 @@ func CompletionMenu(c *ui.Context, anchor *ui.Element, open *bool, items []Compl
s.rows.ScrollIntoView(s.Selected)
}
// Take the menu keys before the editor does.
disabled := anchor.IsDisabled()
anchor.HandleInput(func(ev ui.InputEvent) bool {
if ev.Kind != ui.InputKeyDown || ev.Mods != 0 || !*open || anchor.IsDisabled() {
if ev.Kind != ui.InputKeyDown || ev.Mods != 0 || !*open || disabled {
return false
}
switch ev.Key {
@@ -100,7 +101,7 @@ func CompletionMenu(c *ui.Context, anchor *ui.Element, open *bool, items []Compl
d := core.Density(c)
b := anchor.Bounds()
core.OverlayLayer(c, open)
res.Element = ui.PopoverBase(c, anchor, open, func(panel *ui.Element) {
res.Element = ui.PopoverBase(c, anchor, open, func(panel ui.Element) {
core.OverlayStyle(c, panel, overlay.OverlayAnchored(c, panel, b))
panel.Margin(4, 0, 0, 0).Padding(d.Unit()).Width(overlay.PopoverWide(c, 360)).MaxHeight(overlay.PopoverRoom(c, b)).
Label(core.Msg(c, "completion_menu.label")).Role(ui.RoleList)
+1 -1
View File
@@ -42,7 +42,7 @@ type DebugToolbarOptions struct {
// DebugToolbarResult carries the requested action.
type DebugToolbarResult struct {
Element *ui.Element
Element ui.Element
Action DebugAction
}
+1 -1
View File
@@ -50,7 +50,7 @@ type FindWidgetOptions struct {
// FindWidgetResult is a FindWidget and what happened in this frame.
type FindWidgetResult struct {
Element *ui.Element
Element ui.Element
// Moved reports navigation; Replaced counts replacements.
Moved bool
Replaced int
+1 -1
View File
@@ -109,7 +109,7 @@ func Flamegraph(c *ui.Context, root *FlameNode, s *FlamegraphState, opts Flamegr
}
// flameInput zooms, moves focus and tracks hover.
func flameInput(c *ui.Context, g *ui.Element, root *FlameNode, s *FlamegraphState, hover *[]int) bool {
func flameInput(c *ui.Context, g ui.Element, root *FlameNode, s *FlamegraphState, hover *[]int) bool {
b := g.Bounds()
x, y, over := g.PointerPosition()
*hover = nil
+1 -1
View File
@@ -128,7 +128,7 @@ func hexColumn(c *ui.Context, s *HexViewerState, data []byte, start, end int, cw
}
// hexKeys moves, extends and copies the selection.
func hexKeys(c *ui.Context, frame *ui.Element, data []byte, s *HexViewerState, move func(int, bool)) {
func hexKeys(c *ui.Context, frame ui.Element, data []byte, s *HexViewerState, move func(int, bool)) {
copyMods := ui.Cmd
if runtime.GOOS != "darwin" {
copyMods = ui.Ctrl
+1 -1
View File
@@ -34,7 +34,7 @@ type OutputPanelOptions struct {
// OutputPanelResult is an OutputPanel and what happened in this frame.
type OutputPanelResult struct {
Element *ui.Element
Element ui.Element
// Cleared names a channel to clear.
Changed bool
Cleared string
+1 -1
View File
@@ -58,7 +58,7 @@ type ProblemsPanelOptions struct {
// ProblemsPanelResult carries the problem opened.
type ProblemsPanelResult struct {
Element *ui.Element
Element ui.Element
Opened *Problem
}
+1 -1
View File
@@ -48,7 +48,7 @@ type ProcessListOptions struct {
// ProcessListResult carries a confirmed end request.
type ProcessListResult struct {
Element *ui.Element
Element ui.Element
// End is the confirmed process to end, or nil.
End *Process
}
+1 -1
View File
@@ -57,7 +57,7 @@ type SearchOpen struct {
// SearchPanelResult is a SearchPanel and what happened in this frame.
type SearchPanelResult struct {
Element *ui.Element
Element ui.Element
// Changed asks the caller to search again.
Changed bool
Opened *SearchOpen
+1 -1
View File
@@ -63,7 +63,7 @@ type SymbolOutlineOptions struct {
// SymbolOutlineResult carries the symbol to jump to.
type SymbolOutlineResult struct {
Element *ui.Element
Element ui.Element
Jumped *Symbol
}
+21 -8
View File
@@ -49,8 +49,14 @@ func terminalMetrics(f ui.Font) (cw, ch, base float32) {
return cw, ch, (ch-m.Ascent-m.Descent)/2 + m.Ascent
}
// terminalPointer is the selection drag and focus target kept across frames.
type terminalPointer struct {
handle ui.Handle
down bool
}
// Terminal draws a TerminalState and sends input to the PTY.
func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) *ui.Element {
func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) ui.Element {
if s == nil {
panic("mujica: Terminal needs a TerminalState")
}
@@ -94,13 +100,17 @@ func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) *ui.Element
return TerminalPos{Line: len(s.back) - s.offset + row, Col: col}
}
anchor := ui.Local(e, "terminal.anchor", func() TerminalPos { return TerminalPos{} })
ptr := ui.Local(e, "terminal.pointer", func() terminalPointer { return terminalPointer{} })
e.Bind(&ptr.handle)
disabled := e.IsDisabled()
svc := c.Services()
e.HandleInput(func(ev ui.InputEvent) bool {
if e.IsDisabled() {
if disabled {
return false
}
switch ev.Kind {
case ui.InputKeyDown:
return terminalKey(c, s, ev, send)
return terminalKey(svc, s, ev, send)
case ui.InputText:
send(ev.Text)
return true
@@ -117,15 +127,18 @@ func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) *ui.Element
if ev.Button != 0 {
return false
}
e.Focus()
ptr.down = true
ptr.handle.Focus()
*anchor = cell(ev.X, ev.Y)
s.selOn = false
return true
case ui.InputPointerMove:
if e.Pressed() {
if ptr.down {
s.Select(*anchor, cell(ev.X, ev.Y))
}
return true
case ui.InputPointerUp:
ptr.down = false
}
return false
})
@@ -144,7 +157,7 @@ func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) *ui.Element
}
// terminalKey encodes a key, handling copy, paste and scrollback.
func terminalKey(c *ui.Context, s *TerminalState, ev ui.InputEvent, send func(string)) bool {
func terminalKey(svc ui.Services, s *TerminalState, ev ui.InputEvent, send func(string)) bool {
clip := ui.Super
if runtime.GOOS != "darwin" {
clip = ui.Ctrl | ui.Shift
@@ -152,12 +165,12 @@ func terminalKey(c *ui.Context, s *TerminalState, ev ui.InputEvent, send func(st
switch {
case ev.Mods == clip && ev.Key == ui.KeyC:
if t := s.Selection(); t != "" {
c.WriteClipboard(t)
svc.WriteClipboard(t)
slog.Debug("mujica: terminal copy", "runes", len([]rune(t)))
}
return true
case ev.Mods == clip && ev.Key == ui.KeyV:
if t := c.ReadClipboard(); t != "" {
if t := svc.ReadClipboard(); t != "" {
send(t)
}
return true
+3 -3
View File
@@ -30,7 +30,7 @@ type TerminalTabsOptions struct {
// TerminalTabsResult is TerminalTabs and what happened in this frame.
type TerminalTabsResult struct {
Element *ui.Element
Element ui.Element
// Renamed and Closed hold a tab ID.
Changed bool
Renamed string
@@ -126,7 +126,7 @@ func TerminalTabs(c *ui.Context, tabs *[]TerminalTab, active *string, opts Termi
}
// terminalTabsTab builds one tab with title and close.
func terminalTabsTab(c *ui.Context, tab *ui.Element, t TerminalTab, on bool, st *terminalTabsState, rename func(string), askClose func(TerminalTab), disabled bool) {
func terminalTabsTab(c *ui.Context, tab ui.Element, t TerminalTab, on bool, st *terminalTabsState, rename func(string), askClose func(TerminalTab), disabled bool) {
k := core.Tokens(c)
d := core.Density(c)
u := d.Unit()
@@ -162,7 +162,7 @@ func terminalTabsTab(c *ui.Context, tab *ui.Element, t TerminalTab, on bool, st
if st.editing == t.ID {
in := input.TextInput(c, &st.draft, input.TextInputOptions{Label: core.Msg(c, "terminal_tabs.rename")})
in.Element.Width(u * 36)
in.Input.AutoFocus()
core.AutoFocus(c, in.Input)
switch {
case in.Input.Shortcut(0, ui.KeyEscape):
st.editing = ""
+2 -2
View File
@@ -43,7 +43,7 @@ type VariableEdit struct{ Path, Value string }
// VariablesPanelResult carries a committed edit.
type VariablesPanelResult struct {
Element *ui.Element
Element ui.Element
Edited *VariableEdit
}
@@ -80,7 +80,7 @@ func VariablesPanel(c *ui.Context, vars []Variable, s *VariablesPanelState, opts
if s.editing == p {
in := input.TextInput(c, &s.draft, input.TextInputOptions{Label: core.Msg(c, "variables_panel.value")})
in.Element.Grow(1).MinWidth(0)
in.Input.AutoFocus()
core.AutoFocus(c, in.Input)
switch {
case in.Input.Shortcut(0, ui.KeyEscape):
s.editing = ""