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:
1 parent
bc716d27be
commit
bcce34cda1
603 files changed
+1366
-1277
No files matched your search
@@ -41,7 +41,7 @@ type AttendeeListOptions struct {
|
||||
|
||||
// AttendeeListResult is the list and the attendee the user removed.
|
||||
type AttendeeListResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
removed int
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func AttendeeList(c *ui.Context, attendees []Attendee, opts AttendeeListOptions)
|
||||
ui.Text(c, core.Msg(c, "attendee_list.empty")).TextColor(k.TextMuted)
|
||||
}
|
||||
for i, a := range attendees {
|
||||
ui.Row(c).Key(i).MinHeight(d.ControlHeight()).Gap(d.Unit() * 2).AlignItems(ui.Center).Role(ui.RoleListItem).Children(func() {
|
||||
ui.Row(c.Key(i)).MinHeight(d.ControlHeight()).Gap(d.Unit() * 2).AlignItems(ui.Center).Role(ui.RoleListItem).Children(func() {
|
||||
display.Avatar(c, a.Name, display.AvatarOptions{Size: 28})
|
||||
ui.Column(c).Grow(1).Shrink(1).Children(func() {
|
||||
name := a.Name
|
||||
|
||||
@@ -223,7 +223,7 @@ func calendarDay(c *ui.Context, day, cur core.Date, cfg calendarConfig, size flo
|
||||
if focus {
|
||||
cell.Focus()
|
||||
} else if cfg.autoFocus {
|
||||
cell.AutoFocus()
|
||||
core.AutoFocus(c, cell)
|
||||
}
|
||||
}
|
||||
band := cfg.inRange != nil && cfg.inRange(day)
|
||||
|
||||
@@ -57,7 +57,7 @@ type CalendarChange struct {
|
||||
|
||||
// CalendarViewResult is a view and what the user asked.
|
||||
type CalendarViewResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
opened string
|
||||
change *CalendarChange
|
||||
day core.Date
|
||||
@@ -112,7 +112,7 @@ type eventsCache[T any] struct {
|
||||
}
|
||||
|
||||
// eventsDerived rebuilds build(hits) only when events or span change.
|
||||
func eventsDerived[T any](e *ui.Element, key string, events []CalendarEvent, version int, from, to time.Time, build func(hits []int) T) T {
|
||||
func eventsDerived[T any](e ui.Element, key string, events []CalendarEvent, version int, from, to time.Time, build func(hits []int) T) T {
|
||||
cache := ui.Local(e, key, func() *eventsCache[T] { return &eventsCache[T]{} })
|
||||
var first *CalendarEvent
|
||||
if len(events) > 0 {
|
||||
|
||||
@@ -204,7 +204,7 @@ func monthCell(c *ui.Context, day core.Date, month YearMonth, today core.Date, l
|
||||
*more = day
|
||||
}
|
||||
open := *more == day
|
||||
ui.PopoverBase(c, b, &open, func(panel *ui.Element) {
|
||||
ui.PopoverBase(c, b, &open, func(panel ui.Element) {
|
||||
core.OverlayStyle(c, panel, core.OverlayEnter(c, panel))
|
||||
_, wh := c.Size()
|
||||
panel.Padding(d.Unit() * 2).Gap(d.Unit()).MinWidth(d.ControlHeight() * 6).MaxHeight(wh / 2)
|
||||
|
||||
@@ -60,7 +60,7 @@ func timeGridSlot(name string, slot int) int {
|
||||
}
|
||||
|
||||
// timeGrid draws day columns and turns gestures into changes.
|
||||
func timeGrid(c *ui.Context, root *ui.Element, days []core.Date, events []CalendarEvent, cfg timeGridConfig, r *CalendarViewResult) {
|
||||
func timeGrid(c *ui.Context, root ui.Element, days []core.Date, events []CalendarEvent, cfg timeGridConfig, r *CalendarViewResult) {
|
||||
k := core.Tokens(c)
|
||||
d := core.Density(c)
|
||||
hourH := d.ControlHeight() * 2
|
||||
@@ -266,7 +266,7 @@ func timeChip(c *ui.Context, i int, pl timePlaced, ev CalendarEvent, cfg timeGri
|
||||
if mine && g.kind == CalendarResize {
|
||||
growY = float32(snap(g.dy)) * hourH / 60
|
||||
}
|
||||
box := ui.Box(c).Key(ev.ID).Absolute().Top(from + shiftY).Height(max(to-from+growY, theme.MinHitSize)).
|
||||
box := ui.Box(c.Key(ev.ID)).Absolute().Top(from + shiftY).Height(max(to-from+growY, theme.MinHitSize)).
|
||||
LeftPercent(100 * float32(pl.col) / float32(pl.cols)).WidthPercent(100 / float32(pl.cols)).Padding(1)
|
||||
box.Children(func() {
|
||||
chip := EventChip(c, ev, EventChipOptions{Location: cfg.loc, Hour12: cfg.hour12, Selected: mine, Inline: to-from < hourH*0.75}).Fill()
|
||||
@@ -291,7 +291,7 @@ func timeChip(c *ui.Context, i int, pl timePlaced, ev CalendarEvent, cfg timeGri
|
||||
}
|
||||
}
|
||||
for _, part := range []struct {
|
||||
e *ui.Element
|
||||
e ui.Element
|
||||
kind CalendarChangeKind
|
||||
}{{chip, CalendarMove}, {handle, CalendarResize}} {
|
||||
dx, dy, ok := part.e.Dragged()
|
||||
|
||||
@@ -31,7 +31,7 @@ type CountdownOptions struct {
|
||||
|
||||
// CountdownResult is the countdown and its finish.
|
||||
type CountdownResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
finished bool
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ type CurrentTimeIndicatorOptions struct {
|
||||
}
|
||||
|
||||
// CurrentTimeIndicator rules the current time across its parent.
|
||||
func CurrentTimeIndicator(c *ui.Context, opts CurrentTimeIndicatorOptions) *ui.Element {
|
||||
func CurrentTimeIndicator(c *ui.Context, opts CurrentTimeIndicatorOptions) ui.Element {
|
||||
loc := eventLocation("CurrentTimeIndicator", opts.Location)
|
||||
k := core.Tokens(c)
|
||||
now := opts.Now
|
||||
|
||||
@@ -106,7 +106,7 @@ func DatePicker(c *ui.Context, value *core.Date, opts DatePickerOptions) DateTim
|
||||
if placeholder == "" {
|
||||
placeholder = core.Msg(c, "date_picker.placeholder")
|
||||
}
|
||||
var field, inputEl *ui.Element
|
||||
var field, inputEl ui.Element
|
||||
col.Children(func() {
|
||||
field = core.DatetimeField(c, ui.Row(c), *problem != "", opts.ReadOnly)
|
||||
field.Children(func() {
|
||||
|
||||
@@ -85,7 +85,7 @@ func DateRangePicker(c *ui.Context, value *DateRange, opts DateRangePickerOption
|
||||
if !value.IsZero() {
|
||||
text = value.Start.Format(layout) + " – " + value.End.Format(layout)
|
||||
}
|
||||
var field *ui.Element
|
||||
var field ui.Element
|
||||
col.Children(func() {
|
||||
field = core.DatetimeField(c, ui.Row(c), *problem != "", opts.ReadOnly)
|
||||
field.Children(func() {
|
||||
|
||||
@@ -105,7 +105,7 @@ func datetimeGridCell(c *ui.Context, o, start, cursor int, cfg datetimeGridConfi
|
||||
if focus {
|
||||
cell.Focus()
|
||||
} else if cfg.autoFocus {
|
||||
cell.AutoFocus()
|
||||
core.AutoFocus(c, cell)
|
||||
}
|
||||
}
|
||||
fg := k.Text
|
||||
@@ -172,7 +172,7 @@ type datetimeGridField struct {
|
||||
}
|
||||
|
||||
// datetimeGridPicker draws f and its popup; reports a pick or clear.
|
||||
func datetimeGridPicker(c *ui.Context, f datetimeGridField, cfg datetimeGridConfig) (col *ui.Element, picked int, ok, cleared bool) {
|
||||
func datetimeGridPicker(c *ui.Context, f datetimeGridField, cfg datetimeGridConfig) (col ui.Element, picked int, ok, cleared bool) {
|
||||
d := core.Density(c)
|
||||
col = ui.Column(c).Width(d.ControlHeight() * 6).Gap(d.Unit()).AlignItems(ui.Stretch).Disabled(f.disabled)
|
||||
open := ui.Local(col, "datetime-grid-open", func() bool { return false })
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
|
||||
// DateTimeResult is a date or time component and what happened to it this frame.
|
||||
type DateTimeResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Input is the text field inside, for labels and focus; nil without one.
|
||||
Input *ui.Element
|
||||
Input ui.Element
|
||||
changed bool
|
||||
canceled bool
|
||||
}
|
||||
@@ -20,6 +20,6 @@ func (r DateTimeResult) Changed() bool { return r.changed }
|
||||
func (r DateTimeResult) Canceled() bool { return r.canceled }
|
||||
|
||||
// NewDateTimeResult reports a date or time control built from parts in another component.
|
||||
func NewDateTimeResult(e, input *ui.Element, changed, canceled bool) DateTimeResult {
|
||||
func NewDateTimeResult(e, input ui.Element, changed, canceled bool) DateTimeResult {
|
||||
return DateTimeResult{e, input, changed, canceled}
|
||||
}
|
||||
@@ -167,21 +167,23 @@ func TestTimePickerPopupPickClearsSegmentError(t *testing.T) {
|
||||
func TestFormFocusesDatePickerInput(t *testing.T) {
|
||||
var st input.FormState
|
||||
day := core.Date{}
|
||||
var field *ui.Element
|
||||
focused := false
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
core.Use(c, core.Settings{})
|
||||
var field ui.Element
|
||||
input.Form(c, &st, input.FormOptions{}, func(f *input.FormScope) {
|
||||
input.FormItem(c, f, "debut", &day, input.FormItemOptions[core.Date]{Label: "Debut date", Required: true}, func() *ui.Element {
|
||||
input.FormItem(c, f, "debut", &day, input.FormItemOptions[core.Date]{Label: "Debut date", Required: true}, func() ui.Element {
|
||||
r := DatePicker(c, &day, DatePickerOptions{Today: calendarToday})
|
||||
field = r.Input
|
||||
return r.Input
|
||||
})
|
||||
})
|
||||
focused = field.Valid() && field.Focused()
|
||||
}, 600, 500)
|
||||
if err := tt.Click("Submit"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if field == nil || !field.Focused() || !tt.Focused("Debut date") {
|
||||
if !focused || !tt.Focused("Debut date") {
|
||||
t.Fatal("a failed submit should focus the DatePicker's text box")
|
||||
}
|
||||
tt.Type("2026-10-12")
|
||||
|
||||
@@ -22,7 +22,7 @@ type EventChipOptions struct {
|
||||
}
|
||||
|
||||
// EventChip is a clickable, truncating event block.
|
||||
func EventChip(c *ui.Context, ev CalendarEvent, opts EventChipOptions) *ui.Element {
|
||||
func EventChip(c *ui.Context, ev CalendarEvent, opts EventChipOptions) ui.Element {
|
||||
loc := eventLocation("EventChip", opts.Location)
|
||||
k := core.Tokens(c)
|
||||
d := core.Density(c)
|
||||
|
||||
@@ -24,7 +24,7 @@ type EventEditorOptions struct {
|
||||
|
||||
// EventEditorResult is the editor and its save or cancel.
|
||||
type EventEditorResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
saved bool
|
||||
canceled bool
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func EventEditor(c *ui.Context, ev *CalendarEvent, opts EventEditorOptions) Even
|
||||
dr := &s.draft
|
||||
problems := eventEditorProblems(c, *dr, s.tried)
|
||||
root.Children(func() {
|
||||
input.FormField(c, core.Msg(c, "event_editor.title"), input.FormFieldOptions{Required: true, Error: problems["title"]}, func() *ui.Element {
|
||||
input.FormField(c, core.Msg(c, "event_editor.title"), input.FormFieldOptions{Required: true, Error: problems["title"]}, func() ui.Element {
|
||||
return input.TextInput(c, &dr.Title, input.TextInputOptions{Label: core.Msg(c, "event_editor.title"), Disabled: opts.Disabled, Error: problems["title"]}).Input
|
||||
})
|
||||
input.Checkbox(c, &dr.AllDay, core.Msg(c, "event_editor.all_day"), input.CheckboxOptions{Disabled: opts.Disabled})
|
||||
@@ -70,7 +70,7 @@ func EventEditor(c *ui.Context, ev *CalendarEvent, opts EventEditorOptions) Even
|
||||
key string
|
||||
t *time.Time
|
||||
}{{"event_editor.starts", &dr.Start}, {"event_editor.ends", &dr.End}} {
|
||||
input.FormField(c, core.Msg(c, f.key), input.FormFieldOptions{Error: problems[f.key]}, func() *ui.Element {
|
||||
input.FormField(c, core.Msg(c, f.key), input.FormFieldOptions{Error: problems[f.key]}, func() ui.Element {
|
||||
if dr.AllDay {
|
||||
day := core.DateOf(f.t.In(loc))
|
||||
r := DatePicker(c, &day, DatePickerOptions{Label: core.Msg(c, f.key), WeekStart: opts.WeekStart, Disabled: opts.Disabled})
|
||||
@@ -84,10 +84,10 @@ func EventEditor(c *ui.Context, ev *CalendarEvent, opts EventEditorOptions) Even
|
||||
})
|
||||
}
|
||||
})
|
||||
input.FormField(c, core.Msg(c, "event_editor.where"), input.FormFieldOptions{}, func() *ui.Element {
|
||||
input.FormField(c, core.Msg(c, "event_editor.where"), input.FormFieldOptions{}, func() ui.Element {
|
||||
return input.TextInput(c, &dr.Location, input.TextInputOptions{Label: core.Msg(c, "event_editor.where"), Disabled: opts.Disabled}).Input
|
||||
})
|
||||
input.FormField(c, core.Msg(c, "event_editor.guests"), input.FormFieldOptions{Description: core.Msg(c, "event_editor.guests_help")}, func() *ui.Element {
|
||||
input.FormField(c, core.Msg(c, "event_editor.guests"), input.FormFieldOptions{Description: core.Msg(c, "event_editor.guests_help")}, func() ui.Element {
|
||||
names := make([]string, len(dr.Attendees))
|
||||
for i, a := range dr.Attendees {
|
||||
names[i] = a.Name
|
||||
@@ -101,11 +101,11 @@ func EventEditor(c *ui.Context, ev *CalendarEvent, opts EventEditorOptions) Even
|
||||
if len(dr.Attendees) > 0 {
|
||||
AttendeeList(c, dr.Attendees, AttendeeListOptions{Disabled: opts.Disabled})
|
||||
}
|
||||
input.FormField(c, core.Msg(c, "event_editor.reminders"), input.FormFieldOptions{}, func() *ui.Element {
|
||||
input.FormField(c, core.Msg(c, "event_editor.reminders"), input.FormFieldOptions{}, func() ui.Element {
|
||||
return ReminderPicker(c, &dr.Reminders, ReminderPickerOptions{Disabled: opts.Disabled}).Element
|
||||
})
|
||||
var rec RecurrenceResult
|
||||
input.FormField(c, core.Msg(c, "recurrence_editor.repeat"), input.FormFieldOptions{}, func() *ui.Element {
|
||||
input.FormField(c, core.Msg(c, "recurrence_editor.repeat"), input.FormFieldOptions{}, func() ui.Element {
|
||||
rec = RecurrenceEditor(c, &dr.Repeat, RecurrenceEditorOptions{Start: core.DateOf(dr.Start.In(loc)), WeekStart: opts.WeekStart, Disabled: opts.Disabled})
|
||||
return rec.Element
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ type EventPopoverOptions struct {
|
||||
|
||||
// EventPopoverResult is the popover and the action the user chose.
|
||||
type EventPopoverResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
edit bool
|
||||
del bool
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func (r EventPopoverResult) Edit() bool { return r.edit }
|
||||
func (r EventPopoverResult) Delete() bool { return r.del }
|
||||
|
||||
// EventPopover summarizes an event beside anchor while *open.
|
||||
func EventPopover(c *ui.Context, anchor *ui.Element, open *bool, ev CalendarEvent, opts EventPopoverOptions) EventPopoverResult {
|
||||
func EventPopover(c *ui.Context, anchor ui.Element, open *bool, ev CalendarEvent, opts EventPopoverOptions) EventPopoverResult {
|
||||
loc := eventLocation("EventPopover", opts.Location)
|
||||
k := core.Tokens(c)
|
||||
d := core.Density(c)
|
||||
@@ -47,7 +47,7 @@ func EventPopover(c *ui.Context, anchor *ui.Element, open *bool, ev CalendarEven
|
||||
slog.Debug("mujica: event_popover close", "disabled", true)
|
||||
*open = false
|
||||
}
|
||||
r.Element = ui.PopoverBase(c, anchor, open, func(panel *ui.Element) {
|
||||
r.Element = ui.PopoverBase(c, anchor, open, func(panel ui.Element) {
|
||||
core.OverlayStyle(c, panel, core.OverlayEnter(c, panel))
|
||||
ww, wh := c.Size()
|
||||
panel.Margin(d.Unit(), 0, 0, 0).Padding(d.Unit() * 4).Gap(d.Unit() * 2).Width(min(d.ControlHeight()*9, ww-16)).MaxHeight(wh - 16).
|
||||
|
||||
@@ -55,7 +55,7 @@ type RecurrenceEditorOptions struct {
|
||||
|
||||
// RecurrenceResult is the editor, its change and its problem.
|
||||
type RecurrenceResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
changed bool
|
||||
problem string
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ type RelativeTimeOptions struct {
|
||||
}
|
||||
|
||||
// RelativeTime shows t as "3 minutes ago", refreshing while shown.
|
||||
func RelativeTime(c *ui.Context, t time.Time, opts RelativeTimeOptions) *ui.Element {
|
||||
func RelativeTime(c *ui.Context, t time.Time, opts RelativeTimeOptions) ui.Element {
|
||||
loc := eventLocation("RelativeTime", opts.Location)
|
||||
now := opts.Now
|
||||
if now.IsZero() {
|
||||
|
||||
@@ -192,7 +192,7 @@ func timeParts(cfg timeConfig) []timePart {
|
||||
}
|
||||
|
||||
// timeEditor draws segments and popup; reports edits and invalid entry.
|
||||
func timeEditor(c *ui.Context, draft *TimeOfDay, cfg timeConfig) (field *ui.Element, edited bool, bad string) {
|
||||
func timeEditor(c *ui.Context, draft *TimeOfDay, cfg timeConfig) (field ui.Element, edited bool, bad string) {
|
||||
k := core.Tokens(c)
|
||||
d := core.Density(c)
|
||||
field = core.DatetimeField(c, ui.Row(c), cfg.problem != "", cfg.readOnly).Gap(d.Unit() * 2)
|
||||
@@ -268,7 +268,7 @@ func timeLimits(part timePart, cfg timeConfig) (lo, hi int) {
|
||||
}
|
||||
|
||||
// timeSegment draws one segment; done reports a complete valid entry, bad an invalid one.
|
||||
func timeSegment(c *ui.Context, draft *TimeOfDay, part timePart, cfg timeConfig, bad *timeBad, edited *bool) (seg *ui.Element, done bool) {
|
||||
func timeSegment(c *ui.Context, draft *TimeOfDay, part timePart, cfg timeConfig, bad *timeBad, edited *bool) (seg ui.Element, done bool) {
|
||||
k := core.Tokens(c)
|
||||
d := core.Density(c)
|
||||
name := core.Msg(c, timePartNames[part])
|
||||
|
||||
@@ -114,7 +114,7 @@ func TimezoneSelect(c *ui.Context, zone *string, opts TimezoneSelectOptions) cor
|
||||
core.DatetimePopup(c, field, open, false, func() {
|
||||
list := timezoneFilter(zones, common, *query, now)
|
||||
in := input.TextInput(c, query, input.TextInputOptions{Placeholder: core.Msg(c, "timezone_select.search"), Label: core.Msg(c, "timezone_select.search")})
|
||||
in.Input.AutoFocus()
|
||||
core.AutoFocus(c, in.Input)
|
||||
if in.Changed() {
|
||||
*hi = 0
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user