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
@@ -0,0 +1,19 @@
|
||||
package core
|
||||
|
||||
import "github.com/egoist/mygo/ui"
|
||||
|
||||
type autoFocusKey struct{}
|
||||
|
||||
// AutoFocus focuses e the frame after it appears, once MyGo has recorded an overlay's opener from the focus it had.
|
||||
func AutoFocus(c *ui.Context, e ui.Element) ui.Element {
|
||||
step := ui.Local(e, autoFocusKey{}, func() int { return 0 })
|
||||
switch *step {
|
||||
case 0:
|
||||
*step = 1
|
||||
c.Invalidate()
|
||||
case 1:
|
||||
*step = 2
|
||||
e.Focus()
|
||||
}
|
||||
return e
|
||||
}
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
|
||||
// TestBasesBlockedByDisabledWrapper flips a focused MyGo base's wrapper to Disabled in the frame Space arrives.
|
||||
func TestBasesBlockedByDisabledWrapper(t *testing.T) {
|
||||
for name, base := range map[string]func(c *ui.Context, on *bool) *ui.Element{
|
||||
"CheckboxBase": func(c *ui.Context, on *bool) *ui.Element { return ui.CheckboxBase(c, on) },
|
||||
"RadioBase": func(c *ui.Context, on *bool) *ui.Element { return ui.RadioBase(c, on, true) },
|
||||
"SwitchBase": func(c *ui.Context, on *bool) *ui.Element { return ui.SwitchBase(c, on) },
|
||||
"ToggleBase": func(c *ui.Context, on *bool) *ui.Element { return ui.ToggleBase(c, on) },
|
||||
for name, base := range map[string]func(c *ui.Context, on *bool) ui.Element{
|
||||
"CheckboxBase": func(c *ui.Context, on *bool) ui.Element { return ui.CheckboxBase(c, on) },
|
||||
"RadioBase": func(c *ui.Context, on *bool) ui.Element { return ui.RadioBase(c, on, true) },
|
||||
"SwitchBase": func(c *ui.Context, on *bool) ui.Element { return ui.SwitchBase(c, on) },
|
||||
"ToggleBase": func(c *ui.Context, on *bool) ui.Element { return ui.ToggleBase(c, on) },
|
||||
} {
|
||||
for _, disable := range []bool{false, true} {
|
||||
on, off := false, false
|
||||
@@ -35,8 +35,8 @@ func TestBasesBlockedByDisabledWrapper(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestComboboxBaseIgnoresDisabledWrapper shows why Combobox hands its base a copy: a focused one still takes typing.
|
||||
func TestComboboxBaseIgnoresDisabledWrapper(t *testing.T) {
|
||||
// TestComboboxBaseBlockedByDisabledWrapper guards Combobox, which binds its base directly: a focused one takes no typing.
|
||||
func TestComboboxBaseBlockedByDisabledWrapper(t *testing.T) {
|
||||
q, off := "", false
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
ui.Row(c).Disabled(off).Children(func() {
|
||||
@@ -48,9 +48,10 @@ func TestComboboxBaseIgnoresDisabledWrapper(t *testing.T) {
|
||||
t.Fatal("not focused")
|
||||
}
|
||||
off = true
|
||||
tt.Frame()
|
||||
tt.Type("a")
|
||||
tt.Frame()
|
||||
if q != "a" {
|
||||
t.Fatalf("typing left %q; the wrapper now blocks it and Combobox can drop its copy", q)
|
||||
if q != "" {
|
||||
t.Fatalf("typing under a disabled wrapper set %q; Combobox must edit a copy again", q)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
// ChoiceResult is a choice control's element and whether its value changed this frame.
|
||||
type ChoiceResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
changed bool
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@ type ChoiceResult struct {
|
||||
func (r ChoiceResult) Changed() bool { return r.changed }
|
||||
|
||||
// NewChoiceResult reports e and whether its value changed this frame.
|
||||
func NewChoiceResult(e *ui.Element, changed bool) ChoiceResult { return ChoiceResult{e, changed} }
|
||||
func NewChoiceResult(e ui.Element, changed bool) ChoiceResult { return ChoiceResult{e, changed} }
|
||||
+4
-4
@@ -108,7 +108,7 @@ func DatetimeDayLabel(c *ui.Context, d Date) string {
|
||||
}
|
||||
|
||||
// DatetimeField styles row as a single-line field: solid fill and fine border.
|
||||
func DatetimeField(c *ui.Context, row *ui.Element, invalid, readOnly bool) *ui.Element {
|
||||
func DatetimeField(c *ui.Context, row ui.Element, invalid, readOnly bool) ui.Element {
|
||||
k := Tokens(c)
|
||||
d := Density(c)
|
||||
bg, border := k.Surface, k.ControlBorder
|
||||
@@ -127,7 +127,7 @@ func DatetimeField(c *ui.Context, row *ui.Element, invalid, readOnly bool) *ui.E
|
||||
}
|
||||
|
||||
// DatetimeTrigger is the popup button inside a field; Cmd+C copies its text.
|
||||
func DatetimeTrigger(c *ui.Context, text, placeholder, icon string) *ui.Element {
|
||||
func DatetimeTrigger(c *ui.Context, text, placeholder, icon string) ui.Element {
|
||||
k := Tokens(c)
|
||||
b := ui.ButtonBase(c).Grow(1).Shrink(1).Justify(ui.Start).Gap(Density(c).Unit() * 2).
|
||||
MinHeight(theme.MinHitSize).Radius(theme.ControlRadius).Role(ui.RolePopUpButton)
|
||||
@@ -147,13 +147,13 @@ func DatetimeTrigger(c *ui.Context, text, placeholder, icon string) *ui.Element
|
||||
}
|
||||
|
||||
// DatetimePopup shows fn below anchor while *open; a disabled or read-only anchor closes it.
|
||||
func DatetimePopup(c *ui.Context, anchor *ui.Element, open *bool, readOnly bool, fn func()) *ui.Element {
|
||||
func DatetimePopup(c *ui.Context, anchor ui.Element, open *bool, readOnly bool, fn func()) ui.Element {
|
||||
d := Density(c)
|
||||
if *open && (readOnly || anchor.IsDisabled()) {
|
||||
slog.Debug("mujica: datetime popup closed by state", "readOnly", readOnly)
|
||||
*open = false
|
||||
}
|
||||
return ui.PopoverBase(c, anchor, open, func(panel *ui.Element) {
|
||||
return ui.PopoverBase(c, anchor, open, func(panel ui.Element) {
|
||||
OverlayStyle(c, panel, OverlayEnter(c, panel))
|
||||
panel.MaxWidth(d.ControlHeight()*7+d.Unit()*6+2*theme.BorderWidth).Margin(d.Unit(), 0, 0, 0).Padding(d.Unit() * 3).Gap(d.Unit() * 3)
|
||||
fn()
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// IconAction is a square, borderless button showing icon, named label.
|
||||
func IconAction(c *ui.Context, icon *ui.SVG, label string) *ui.Element {
|
||||
func IconAction(c *ui.Context, icon *ui.SVG, label string) ui.Element {
|
||||
k := Tokens(c)
|
||||
b := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius).Label(label).TextColor(k.TextMuted)
|
||||
internal.Ring(b, k.Focus, theme.ControlRadius)
|
||||
|
||||
+14
-13
@@ -2,16 +2,16 @@ package core
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
// overlayLayers is the window's open MujicaUI layers, oldest first.
|
||||
type overlayLayers struct {
|
||||
frame, prev time.Time
|
||||
keys []any
|
||||
seen []time.Time
|
||||
root ui.Element // compared only: a new root means a new build pass
|
||||
pass int
|
||||
keys []any
|
||||
seen []int
|
||||
}
|
||||
|
||||
type overlayLayersKey struct{}
|
||||
@@ -19,11 +19,12 @@ type overlayLayersKey struct{}
|
||||
// OverlayLayer marks the layer key open in this frame and reports whether it is on top.
|
||||
func OverlayLayer(c *ui.Context, key any) bool {
|
||||
s := ui.Local(c.Root(), overlayLayersKey{}, func() overlayLayers { return overlayLayers{} })
|
||||
if now := c.Now(); now != s.frame {
|
||||
s.prev, s.frame = s.frame, now
|
||||
if root := c.Root(); root != s.root {
|
||||
s.root = root
|
||||
s.pass++
|
||||
n := 0
|
||||
for i, k := range s.keys {
|
||||
if !s.seen[i].Before(s.prev) { // drop layers the last frame did not build
|
||||
if s.seen[i] >= s.pass-1 { // drop layers the last pass did not build
|
||||
s.keys[n], s.seen[n] = k, s.seen[i]
|
||||
n++
|
||||
}
|
||||
@@ -38,10 +39,10 @@ func OverlayLayer(c *ui.Context, key any) bool {
|
||||
}
|
||||
}
|
||||
if at < 0 {
|
||||
s.keys, s.seen = append(s.keys, key), append(s.seen, s.frame)
|
||||
s.keys, s.seen = append(s.keys, key), append(s.seen, s.pass)
|
||||
at = len(s.keys) - 1
|
||||
}
|
||||
s.seen[at] = s.frame
|
||||
s.seen[at] = s.pass
|
||||
return at == len(s.keys)-1
|
||||
}
|
||||
|
||||
@@ -76,7 +77,7 @@ type overlaySiteState struct {
|
||||
}
|
||||
|
||||
// OverlaySite builds the invisible focus fallback at a layer's call site.
|
||||
func OverlaySite(c *ui.Context) (*ui.Element, *overlaySiteState) {
|
||||
func OverlaySite(c *ui.Context) (ui.Element, *overlaySiteState) {
|
||||
site := ui.Box(c).Size(0, 0).FocusRing(false)
|
||||
st := ui.Local(site, "mujica.overlay.site", func() overlaySiteState { return overlaySiteState{} })
|
||||
st.holding = st.holding && site.FocusWithin()
|
||||
@@ -89,12 +90,12 @@ func OverlaySite(c *ui.Context) (*ui.Element, *overlaySiteState) {
|
||||
}
|
||||
|
||||
// Shown records whether the focus is in the open layer's panel.
|
||||
func (st *overlaySiteState) Shown(panel *ui.Element) {
|
||||
st.focusIn, st.wait = panel != nil && panel.FocusWithin(), 0
|
||||
func (st *overlaySiteState) Shown(panel ui.Element) {
|
||||
st.focusIn, st.wait = panel.Valid() && panel.FocusWithin(), 0
|
||||
}
|
||||
|
||||
// Closed moves a lost focus to the site once MyGo has had its chance to restore it.
|
||||
func (st *overlaySiteState) Closed(c *ui.Context, site *ui.Element) {
|
||||
func (st *overlaySiteState) Closed(c *ui.Context, site ui.Element) {
|
||||
if st.focusIn {
|
||||
st.focusIn, st.wait = false, 2 // MyGo prunes the panel at the end of the next frame
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
// OverlayEnter returns how far, 0 to 1, the popup born with e has entered.
|
||||
func OverlayEnter(c *ui.Context, e *ui.Element) float32 {
|
||||
func OverlayEnter(c *ui.Context, e ui.Element) float32 {
|
||||
born := ui.Local(e, "mujica.overlay.born", func() time.Time { return c.Now() })
|
||||
d := Motion(c, theme.PopupDuration)
|
||||
if d == 0 {
|
||||
@@ -24,7 +24,7 @@ func OverlayEnter(c *ui.Context, e *ui.Element) float32 {
|
||||
}
|
||||
|
||||
// OverlayStyle gives a popup panel the surface, edge and shadow of the theme.
|
||||
func OverlayStyle(c *ui.Context, panel *ui.Element, p float32) {
|
||||
func OverlayStyle(c *ui.Context, panel ui.Element, p float32) {
|
||||
k := Tokens(c)
|
||||
panel.Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.PopupRadius).
|
||||
TextColor(k.Text).Opacity(p).Shadow(0, 6, 18, 0, ui.RGBA(0, 0, 0, 0.16*p))
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ func SeverityLook(k theme.Tokens, s Severity) (ui.Color, *ui.SVG) {
|
||||
// AlertResult is what an Alert built and reported this frame.
|
||||
type AlertResult struct {
|
||||
// Element is nil while the alert is closed.
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Action reports a click on the action button.
|
||||
Action bool
|
||||
// Closed reports the user dismissing the alert.
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ type SpinnerOptions struct {
|
||||
}
|
||||
|
||||
// Spinner turns four diamonds only while painted; hidden or under reduced motion it stops.
|
||||
func Spinner(c *ui.Context, opts SpinnerOptions) *ui.Element {
|
||||
func Spinner(c *ui.Context, opts SpinnerOptions) ui.Element {
|
||||
if !(opts.Size >= 0) || math.IsInf(float64(opts.Size), 1) {
|
||||
panic(fmt.Sprintf("mujica: Spinner Size %v invalid", opts.Size))
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func Spinner(c *ui.Context, opts SpinnerOptions) *ui.Element {
|
||||
row := ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center)
|
||||
row.Children(func() {
|
||||
// Keyed by motion so a flip repaints; MyGo's spinner carries the progress role.
|
||||
ui.Box(c).Key(still).Shrink(0).Children(func() { spinnerMark(c, label, size, color, still) })
|
||||
ui.Box(c.Key(still)).Shrink(0).Children(func() { spinnerMark(c, label, size, color, still) })
|
||||
if opts.ShowLabel {
|
||||
ui.Text(c, label).TextColor(k.TextMuted).SingleLine()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user