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
@@ -66,7 +66,7 @@ func (k callKey) String() string {
}
// CallControls provides call actions and window shortcuts.
func CallControls(c *ui.Context, st CallState, opts CallControlsOptions) *ui.Element {
func CallControls(c *ui.Context, st CallState, opts CallControlsOptions) ui.Element {
if opts.OnMute == nil || opts.OnCamera == nil || opts.OnShare == nil || opts.OnHangUp == nil {
panic("mujica: CallControls needs every callback")
}
+1 -1
View File
@@ -175,7 +175,7 @@ func channelRow(c *ui.Context, k theme.Tokens, ch Channel, on bool) bool {
if ch.Unread > 0 {
label = fmt.Sprintf(core.Msg(c, "channel_list.unread"), ch.Name, ch.Unread)
}
b := ui.ButtonBase(c).Key(ch.ID).MinHeight(d.ControlHeight()).Justify(ui.Start).Gap(u*2).Padding(0, u*2, 0, u*5).
b := ui.ButtonBase(c.Key(ch.ID)).MinHeight(d.ControlHeight()).Justify(ui.Start).Gap(u*2).Padding(0, u*2, 0, u*5).
Radius(theme.ControlRadius).Label(label).Role(ui.RoleTreeItem).Selected(on).Background(ui.Transparent).
TextColor(k.Text).Transition(core.ColorFade)
internal.Ring(b, k.Focus, theme.ControlRadius)
+1 -1
View File
@@ -43,7 +43,7 @@ func ChatContinues(prev, cur ChatEntry) bool {
}
// ChatMessage displays text, timestamps, edits and interaction slots.
func ChatMessage(c *ui.Context, m ChatEntry, opts ChatMessageOptions) *ui.Element {
func ChatMessage(c *ui.Context, m ChatEntry, opts ChatMessageOptions) ui.Element {
if m.Author == "" {
panic("mujica: ChatMessage needs an Author")
}
+4 -4
View File
@@ -26,7 +26,7 @@ type EmojiPickerOptions struct {
// EmojiResult exposes the element and this frame’s chosen emoji.
type EmojiResult struct {
Element *ui.Element
Element ui.Element
// Picked contains the chosen emoji, or an empty string.
Picked string
}
@@ -139,7 +139,7 @@ func emojiTabs(c *ui.Context, k theme.Tokens, tab *int, recent bool) {
*tab = min(*tab, len(tabs)-1)
ui.Row(c).FocusGroup(ui.Horizontal).Role(ui.RoleTabList).BorderWidth(0, 0, 1, 0).BorderColor(k.Border).Children(func() {
for i, t := range tabs {
b := ui.ButtonBase(c).Key(t.key).Size(32, 32).Radius(theme.ControlRadius).Role(ui.RoleTab).Label(core.Msg(c, t.key)).
b := ui.ButtonBase(c.Key(t.key)).Size(32, 32).Radius(theme.ControlRadius).Role(ui.RoleTab).Label(core.Msg(c, t.key)).
Selected(i == *tab).Background(ui.Transparent).Tooltip(core.Msg(c, t.key))
internal.Ring(b, k.Focus, theme.ControlRadius)
switch {
@@ -160,7 +160,7 @@ func emojiToneRow(c *ui.Context, k theme.Tokens, tone *int) {
ui.Row(c).Gap(2).AlignItems(ui.Center).FocusGroup(ui.Horizontal).Role(ui.RoleRadioGroup).Label(core.Msg(c, "emoji_picker.tone")).Children(func() {
ui.Text(c, core.Msg(c, "emoji_picker.tone")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Margin(0, 6, 0, 0)
for i := range emojiTones {
b := ui.ButtonBase(c).Key(i).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius).Role(ui.RoleRadio).
b := ui.ButtonBase(c.Key(i)).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius).Role(ui.RoleRadio).
Label(fmt.Sprintf(core.Msg(c, "emoji_picker.toneN"), i)).Selected(i == *tone).Background(ui.Transparent)
internal.Ring(b, k.Focus, theme.ControlRadius)
if i == *tone {
@@ -179,7 +179,7 @@ func emojiToneRow(c *ui.Context, k theme.Tokens, tone *int) {
func emojiCell(c *ui.Context, k theme.Tokens, e emoji, tone int) string {
s := emojiToned(e, tone)
b := ui.ButtonBase(c).Key(e.char).Size(36, 36).Radius(theme.ControlRadius).Label(emojiLabel(c, e)).Tooltip(emojiLabel(c, e)).Transition(core.ColorFade)
b := ui.ButtonBase(c.Key(e.char)).Size(36, 36).Radius(theme.ControlRadius).Label(emojiLabel(c, e)).Tooltip(emojiLabel(c, e)).Transition(core.ColorFade)
internal.Ring(b, k.Focus, theme.ControlRadius)
if b.Hovered() || b.Focused() {
b.Background(k.SurfaceHover)
+3 -3
View File
@@ -49,7 +49,7 @@ func IncomingCallDialog(c *ui.Context, open *bool, call IncomingCall, opts Incom
u := core.Density(c).Unit()
title := fmt.Sprintf(core.Msg(c, "incoming_call_dialog.title"), call.Caller)
shown, accepted, video, declined, backClicked := true, false, false, false, false
panel := ui.DialogBase(c, &shown, func(back, panel *ui.Element) {
panel := ui.DialogBase(c, &shown, func(back, panel ui.Element) {
backClicked = back.Clicked() // a click outside neither answers nor refuses
p := core.OverlayEnter(c, panel)
back.Background(overlay.OverlayScrim(k).Alpha(p)).Padding(16)
@@ -68,9 +68,9 @@ func IncomingCallDialog(c *ui.Context, open *bool, call IncomingCall, opts Incom
decline := input.Button(c, core.Msg(c, "incoming_call_dialog.decline"), input.ButtonOptions{Variant: input.Danger, Icon: msgHangUp, Label: core.Msg(c, "incoming_call_dialog.decline")})
accept := input.Button(c, core.Msg(c, "incoming_call_dialog.accept"), input.ButtonOptions{Icon: msgPhone, Label: core.Msg(c, "incoming_call_dialog.accept")})
if opts.FocusAccept {
accept.AutoFocus()
core.AutoFocus(c, accept)
} else {
decline.AutoFocus()
core.AutoFocus(c, decline)
}
declined, accepted = decline.Clicked(), accept.Clicked()
if call.Video && input.Button(c, core.Msg(c, "incoming_call_dialog.acceptVideo"), input.ButtonOptions{Variant: input.Secondary, Icon: core.MediaCamera, Label: core.Msg(c, "incoming_call_dialog.acceptVideo")}).Clicked() {
+2 -2
View File
@@ -35,7 +35,7 @@ type MailComposerOptions struct {
// MailComposerResult is the composer's element and this frame's events.
type MailComposerResult struct {
Element *ui.Element
Element ui.Element
// Sent requests sending; Discarded requests discarding the draft.
Sent, Discarded bool
}
@@ -178,7 +178,7 @@ func composerAttachments(c *ui.Context, k theme.Tokens, d *MailDraft, disabled b
ui.Row(c).Gap(core.Density(c).Unit() * 2).Wrap().Children(func() {
for i, a := range d.Attachments {
var v display.TagView
ui.Box(c).Key(fmt.Sprint(i, a.Name)).Children(func() {
ui.Box(c.Key(fmt.Sprint(i, a.Name))).Children(func() {
v = display.Tag(c, a.Name, display.TagOptions{Icon: "file", Closable: true, Disabled: disabled})
})
if v.Closed {
+1 -1
View File
@@ -31,7 +31,7 @@ type MailListOptions struct {
// MailListResult is a MailList's element and this frame's events.
type MailListResult struct {
Element *ui.Element
Element ui.Element
changed bool
// Opened contains the ID activated by Enter or double-click.
Opened string
+2 -2
View File
@@ -42,7 +42,7 @@ type Mail struct {
// MailReaderResult is a MailReader's element and this frame's events.
type MailReaderResult struct {
Element *ui.Element
Element ui.Element
// Link contains the activated URL, or an empty string.
Link string
// Opened and Downloaded are attachment indexes, -1 for none.
@@ -122,7 +122,7 @@ func mailPlainBody(c *ui.Context, open *map[string]bool, body string) {
j++
}
chunk := strings.Join(lines[i:j], "\n")
ui.Box(c).Key(i).Children(func() {
ui.Box(c.Key(i)).Children(func() {
if quoted {
// Fold state is kept per quote text.
o := (*open)[chunk]
+1 -1
View File
@@ -51,7 +51,7 @@ type memberRows struct {
// MemberListResult is a MemberList's element and this frame's events.
type MemberListResult struct {
Element *ui.Element
Element ui.Element
changed bool
// Opened contains the ID activated by Enter or double-click.
Opened string
+1 -1
View File
@@ -43,7 +43,7 @@ type OnlineStatusOptions struct {
}
// OnlineStatus shows presence as a distinct shape plus its name.
func OnlineStatus(c *ui.Context, p PresenceStatus, opts OnlineStatusOptions) *ui.Element {
func OnlineStatus(c *ui.Context, p PresenceStatus, opts OnlineStatusOptions) ui.Element {
name := core.Msg(c, p.key())
if opts.Size < 0 {
panic("mujica: OnlineStatus Size must not be negative")
+1 -1
View File
@@ -13,7 +13,7 @@ import (
// PinnedMessagesResult is the list's element and this frame's events.
type PinnedMessagesResult struct {
Element *ui.Element
Element ui.Element
// Jump requests navigation; Unpinned requests removing a pin.
Jump, Unpinned string
}
+2 -2
View File
@@ -30,7 +30,7 @@ type ReadReceiptOptions struct {
}
// ReadReceipt displays delivery marks, text and reader avatars.
func ReadReceipt(c *ui.Context, state ReceiptState, opts ReadReceiptOptions) *ui.Element {
func ReadReceipt(c *ui.Context, state ReceiptState, opts ReadReceiptOptions) ui.Element {
if state < ReceiptSent || state > ReceiptRead || opts.Max < 0 {
panic(fmt.Sprintf("mujica: bad ReadReceipt state %d or Max %d", state, opts.Max))
}
@@ -62,7 +62,7 @@ func ReadReceipt(c *ui.Context, state ReceiptState, opts ReadReceiptOptions) *ui
}
ui.Row(c).Children(func() {
for i, r := range opts.Readers[:min(limit, len(opts.Readers))] {
cell := ui.Box(c).Key(i).Padding(1).Radius(9).Background(k.Background)
cell := ui.Box(c.Key(i)).Padding(1).Radius(9).Background(k.Background)
if i > 0 {
cell.Margin(0, 0, 0, -5)
}
+1 -1
View File
@@ -124,7 +124,7 @@ func RecipientInput(c *ui.Context, recipients *[]string, opts RecipientInputOpti
panel.Children(func() {
ui.Scroll(c).Shrink(1).Padding(4).Children(func() {
for i, ct := range s.matches {
row := ui.ButtonBase(c).Key(ct.Email).MinHeight(core.Density(c).ControlHeight()).Justify(ui.Start).Gap(8).Padding(0, 8).
row := ui.ButtonBase(c.Key(ct.Email)).MinHeight(core.Density(c).ControlHeight()).Justify(ui.Start).Gap(8).Padding(0, 8).
Radius(theme.ControlRadius).Role(ui.RoleListItem).Label(recipientFormat(ct)).FocusRing(false)
if i == s.hi || row.Hovered() {
row.Background(k.Selection)
+1 -1
View File
@@ -105,7 +105,7 @@ func SnoozePicker(c *ui.Context, until *time.Time, opts SnoozePickerOptions) cor
return b.Clicked() && !b.IsDisabled()
}
for _, ch := range SnoozeChoices(now, morning) {
ui.Box(c).Key(ch.Key).Children(func() {
ui.Box(c.Key(ch.Key)).Children(func() {
if row(core.Msg(c, ch.Key), snoozeWhen(c, now, ch.At), until.Equal(ch.At)) {
*until, s.custom = ch.At, false
}
+1 -1
View File
@@ -99,7 +99,7 @@ func StatusSetter(c *ui.Context, st *CustomStatus, opts StatusSetterOptions) cor
for i, o := range statusClear {
options[i] = input.SelectOption[int]{Value: i, Label: core.Msg(c, o.key)}
}
ui.Box(c).Key(live).Children(func() {
ui.Box(c.Key(live)).Children(func() {
if input.Select(c, &s.clear, options, input.SelectOptions{Label: core.Msg(c, "status_setter.clearAfter"), Placeholder: core.MessagingDate(c, st.Until.In(loc), "status_setter.until"), Disabled: !live}).Changed() {
st.Until = statusUntil(c.Now().In(loc), statusClear[s.clear].after)
}
+1 -1
View File
@@ -22,7 +22,7 @@ type ThreadPanelOptions struct {
// ThreadPanelResult is the panel's element and this frame's events.
type ThreadPanelResult struct {
Element *ui.Element
Element ui.Element
// Sent is a reply the user sent; empty is none.
Sent string
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
)
// UnreadDivider marks where unread messages begin; count zero shows "New".
func UnreadDivider(c *ui.Context, count int) *ui.Element {
func UnreadDivider(c *ui.Context, count int) ui.Element {
if count < 0 {
panic("mujica: UnreadDivider count must not be negative")
}
+1 -1
View File
@@ -31,7 +31,7 @@ type UserProfileCardOptions struct {
}
// UserProfileCard displays identity, presence, role and action slots.
func UserProfileCard(c *ui.Context, prof UserProfile, opts UserProfileCardOptions) *ui.Element {
func UserProfileCard(c *ui.Context, prof UserProfile, opts UserProfileCardOptions) ui.Element {
if prof.Name == "" {
panic("mujica: UserProfileCard needs a Name")
}
+2 -2
View File
@@ -31,7 +31,7 @@ type VideoCallGridOptions struct {
}
// VideoCallGrid displays participant frames and speaker marks.
func VideoCallGrid(c *ui.Context, people []CallParticipant, opts VideoCallGridOptions) *ui.Element {
func VideoCallGrid(c *ui.Context, people []CallParticipant, opts VideoCallGridOptions) ui.Element {
if opts.Columns < 0 {
panic("mujica: VideoCallGrid Columns must not be negative")
}
@@ -78,7 +78,7 @@ func callTile(c *ui.Context, k theme.Tokens, p CallParticipant) {
if len(notes) > 0 {
label += ", " + strings.Join(notes, ", ")
}
tile := ui.Box(c).Key(p.ID).AspectRatio(16.0 / 9).Radius(theme.CardRadius).Clip().Background(media.VideoBlack).Center().
tile := ui.Box(c.Key(p.ID)).AspectRatio(16.0 / 9).Radius(theme.CardRadius).Clip().Background(media.VideoBlack).Center().
Focusable().Role(ui.RoleImage).Label(label)
internal.Ring(tile, k.Focus, theme.CardRadius)
light := ui.Hex("#F2EBDD")