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
@@ -47,7 +47,7 @@ type activityFeedCache struct {
}
// ActivityFeed lists events grouped by day, newest first as given, building only the rows in view.
func ActivityFeed(c *ui.Context, items []Activity, opts ActivityFeedOptions) *ui.Element {
func ActivityFeed(c *ui.Context, items []Activity, opts ActivityFeedOptions) ui.Element {
loc := cmp.Or(opts.Location, time.Local)
for _, a := range items {
if a.ID == "" || a.Actor == "" || a.Action == "" || a.Time.IsZero() {
@@ -92,7 +92,7 @@ func ActivityFeed(c *ui.Context, items []Activity, opts ActivityFeedOptions) *ui
ui.Box(c).FillWidth().Height(rowH * float32(len(cache.rows))).Children(func() {
for r := first; r < last; r++ {
row := cache.rows[r]
box := ui.Row(c).Key(r).Absolute().Left(0).Right(0).Top(rowH*float32(r)).Height(rowH).
box := ui.Row(c.Key(r)).Absolute().Left(0).Right(0).Top(rowH*float32(r)).Height(rowH).
AlignItems(ui.Center).Gap(d.Unit()*3).Padding(0, d.Unit()*3)
if row.item < 0 {
activityFeedHeader(c, k, box, row.day, today)
@@ -106,7 +106,7 @@ func ActivityFeed(c *ui.Context, items []Activity, opts ActivityFeedOptions) *ui
}
// activityFeedHeader is a day's title: Today, Yesterday or the date, over an ornament rule.
func activityFeedHeader(c *ui.Context, k theme.Tokens, box *ui.Element, day, today core.Date) {
func activityFeedHeader(c *ui.Context, k theme.Tokens, box ui.Element, day, today core.Date) {
label := core.DatetimeDayLabel(c, day)
switch day {
case today:
@@ -122,7 +122,7 @@ func activityFeedHeader(c *ui.Context, k theme.Tokens, box *ui.Element, day, tod
}
// activityFeedEvent is one event: avatar, "Actor action Object", and the time.
func activityFeedEvent(c *ui.Context, k theme.Tokens, box *ui.Element, a Activity, loc *time.Location) {
func activityFeedEvent(c *ui.Context, k theme.Tokens, box ui.Element, a Activity, loc *time.Location) {
when := a.Time.In(loc).Format("15:04")
box.Role(ui.RoleListItem).Label(a.Actor + " " + a.Action + " " + a.Object + ", " + when)
box.Children(func() {
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
// BarsLoader shows four bars rising and falling in turn.
func BarsLoader(c *ui.Context, opts LoaderOptions) *ui.Element {
func BarsLoader(c *ui.Context, opts LoaderOptions) ui.Element {
return loaderMark(c, opts, "bars_loader", 1.25, 1000, func(p *ui.Painter, r ui.Rect, t float64, col ui.Color, still bool) {
w := r.W / 7
for i := range 4 {
+1 -1
View File
@@ -29,7 +29,7 @@ type blinkState struct {
}
// BlinkHighlight tints value once on change and marks its direction.
func BlinkHighlight(c *ui.Context, value float64, opts BlinkHighlightOptions) *ui.Element {
func BlinkHighlight(c *ui.Context, value float64, opts BlinkHighlightOptions) ui.Element {
if math.IsNaN(value) || math.IsInf(value, 0) || opts.Duration < 0 {
panic("mujica: BlinkHighlight needs a finite value and a non-negative Duration")
}
+1 -2
View File
@@ -27,8 +27,7 @@ func TestBlinkHighlightFlashesOnceWithDirection(t *testing.T) {
if _, ok := tt.Find("12.00, up"); !ok {
t.Fatalf("direction not named: %q", tt.Texts())
}
time.Sleep(250 * time.Millisecond)
tt.Frame()
tt.Advance(250 * time.Millisecond)
if bytes.Equal(a, encode(t, tt.Image())) || motionBusy(tt, n) {
t.Fatal("flash did not fade and stop")
}
+1 -1
View File
@@ -28,7 +28,7 @@ type countUpState struct {
}
// CountUp counts from the last value to the new one.
func CountUp(c *ui.Context, value float64, opts CountUpOptions) *ui.Element {
func CountUp(c *ui.Context, value float64, opts CountUpOptions) ui.Element {
if math.IsNaN(value) || math.IsInf(value, 0) {
panic("mujica: CountUp value must be finite")
}
+2 -4
View File
@@ -22,8 +22,7 @@ func TestCountUpCountsAndSettles(t *testing.T) {
}
value = 200
tt.Frame()
time.Sleep(100 * time.Millisecond)
tt.Frame()
tt.Advance(100 * time.Millisecond)
mid := ""
for _, s := range tt.Texts() {
if s != "200" {
@@ -33,8 +32,7 @@ func TestCountUpCountsAndSettles(t *testing.T) {
if mid == "" || mid == "100" {
t.Fatalf("no value between: %q", tt.Texts())
}
time.Sleep(300 * time.Millisecond)
tt.Frame()
tt.Advance(300 * time.Millisecond)
if motionCount(tt, "200") != 2 || motionBusy(tt, n) {
t.Fatalf("did not settle: %q", tt.Texts())
}
+3 -3
View File
@@ -24,7 +24,7 @@ type LoaderOptions struct {
type loaderDraw func(p *ui.Painter, r ui.Rect, t float64, col ui.Color, still bool)
// loaderMark builds a loader that repaints only while painted.
func loaderMark(c *ui.Context, opts LoaderOptions, key string, aspect float32, period int64, draw loaderDraw) *ui.Element {
func loaderMark(c *ui.Context, opts LoaderOptions, key string, aspect float32, period int64, draw loaderDraw) ui.Element {
if !(opts.Size >= 0) || math.IsInf(float64(opts.Size), 1) {
panic("mujica: loader Size must be finite and not negative")
}
@@ -44,7 +44,7 @@ func loaderMark(c *ui.Context, opts LoaderOptions, key string, aspect float32, p
still := c.Preferences().ReduceMotion
row := ui.Row(c).Gap(core.Density(c).Unit() * 2).AlignItems(ui.Center).Shrink(0).Role(ui.RoleProgress).Label(label)
row.Children(func() {
ui.Box(c).Key(still).Size(size*aspect, size).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) {
ui.Box(c.Key(still)).Size(size*aspect, size).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) {
t := 0.0
if !still {
t = float64(p.Now().UnixMilli()%period) / float64(period)
@@ -64,7 +64,7 @@ func loaderMark(c *ui.Context, opts LoaderOptions, key string, aspect float32, p
func loaderWave(t float64) float32 { return float32(0.5 - 0.5*math.Cos(2*math.Pi*t)) }
// DotsLoader shows three dots lighting up in turn.
func DotsLoader(c *ui.Context, opts LoaderOptions) *ui.Element {
func DotsLoader(c *ui.Context, opts LoaderOptions) ui.Element {
return loaderMark(c, opts, "dots_loader", 2.5, 1200, func(p *ui.Painter, r ui.Rect, t float64, col ui.Color, still bool) {
d := r.H * 0.36
for i := range 3 {
+1 -1
View File
@@ -38,7 +38,7 @@ type EmptyStateOptions struct {
// EmptyStateResult is what an EmptyState built and reported this frame.
type EmptyStateResult struct {
Element *ui.Element
Element ui.Element
Action bool
}
+1 -1
View File
@@ -34,7 +34,7 @@ type EvalResultTableOptions struct {
// EvalResultTableResult is a built EvalResultTable and its events.
type EvalResultTableResult struct {
Element *ui.Element
Element ui.Element
Table data.DataTableView
diff string
}
+1 -1
View File
@@ -20,7 +20,7 @@ type LayoutTransitionOptions struct {
}
// LayoutTransition animates its size and place when they change.
func LayoutTransition(c *ui.Context, opts LayoutTransitionOptions, content func()) *ui.Element {
func LayoutTransition(c *ui.Context, opts LayoutTransitionOptions, content func()) ui.Element {
if opts.Duration < 0 {
panic("mujica: LayoutTransition Duration is negative")
}
+1 -2
View File
@@ -30,8 +30,7 @@ func TestLayoutTransitionResizes(t *testing.T) {
if *n-before < 3 {
t.Fatal("resize did not animate")
}
time.Sleep(260 * time.Millisecond)
tt.Frame()
tt.Advance(260 * time.Millisecond)
if motionBusy(tt, n) || w != 300 {
t.Fatalf("settled width %v", w)
}
+5 -7
View File
@@ -39,11 +39,11 @@ func motionCount(tt *ui.Tester, s string) int {
return n
}
var loaderCases = map[string]func(c *ui.Context, o LoaderOptions) *ui.Element{
var loaderCases = map[string]func(c *ui.Context, o LoaderOptions) ui.Element{
"dots": DotsLoader, "bars": BarsLoader, "pulse": PulseLoader, "orbit": OrbitLoader, "wave": WaveLoader,
}
func testLoaderBehavior(t *testing.T, loader func(*ui.Context, LoaderOptions) *ui.Element) {
func testLoaderBehavior(t *testing.T, loader func(*ui.Context, LoaderOptions) ui.Element) {
show := true
tt, n := motionFrames(func(c *ui.Context) {
if show {
@@ -57,8 +57,7 @@ func testLoaderBehavior(t *testing.T, loader func(*ui.Context, LoaderOptions) *u
t.Fatal("label shown without ShowLabel")
}
a := encode(t, tt.Image())
time.Sleep(120 * time.Millisecond)
tt.Frame()
tt.Advance(120 * time.Millisecond)
if bytes.Equal(a, encode(t, tt.Image())) || !motionBusy(tt, n) {
t.Fatal("loader does not move")
}
@@ -67,8 +66,7 @@ func testLoaderBehavior(t *testing.T, loader func(*ui.Context, LoaderOptions) *u
t.Fatalf("reduced motion: still repainting or no static label: %q", tt.Texts())
}
a = encode(t, tt.Image())
time.Sleep(120 * time.Millisecond)
tt.Frame()
tt.Advance(120 * time.Millisecond)
if !bytes.Equal(a, encode(t, tt.Image())) {
t.Fatal("loader moved under reduced motion")
}
@@ -96,7 +94,7 @@ func TestLoadersInvalidSize(t *testing.T) {
}
}
func loaderSnapshot(t *testing.T, name string, loader func(c *ui.Context, o LoaderOptions) *ui.Element) {
func loaderSnapshot(t *testing.T, name string, loader func(c *ui.Context, o LoaderOptions) ui.Element) {
snapshot(t, name, 480, 90, func(c *ui.Context) {
ui.Row(c).Padding(20).Gap(24).AlignItems(ui.Center).Children(func() {
loader(c, LoaderOptions{})
+1 -1
View File
@@ -22,7 +22,7 @@ type LoadingOverlayOptions struct {
// LoadingOverlayResult is what a LoadingOverlay built and reported this frame.
type LoadingOverlayResult struct {
Element *ui.Element
Element ui.Element
// Canceled reports the cancel button or Escape while loading.
Canceled bool
}
+3 -3
View File
@@ -35,7 +35,7 @@ type NotificationCenterOptions struct {
// NotificationCenterResult reports the frame's events by notification ID.
type NotificationCenterResult struct {
Element *ui.Element
Element ui.Element
Action string
// Toggled changed its Read state; Opened was clicked and is now read.
Toggled string
@@ -127,7 +127,7 @@ func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification,
if !n.Read {
name = core.Msg(c, "notification_center.unread_mark") + ": " + n.Title
}
row := ui.Row(c).Key(n.ID).FillWidth().AlignItems(ui.Start).Gap(u*2).Padding(u*3, u*3, u*3, u*2).
row := ui.Row(c.Key(n.ID)).FillWidth().AlignItems(ui.Start).Gap(u*2).Padding(u*3, u*3, u*3, u*2).
Role(ui.RoleListItem)
if row.Hovered() {
row.Background(k.SurfaceHover)
@@ -186,7 +186,7 @@ func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification,
}
}
func notificationIconButton(c *ui.Context, k theme.Tokens, u float32, icon *ui.SVG, label string) *ui.Element {
func notificationIconButton(c *ui.Context, k theme.Tokens, u float32, icon *ui.SVG, label string) ui.Element {
b := input.Button(c, "", input.ButtonOptions{Variant: input.Ghost, Icon: icon, Label: label})
b.TextColor(k.TextMuted).Padding(0, u).Tooltip(label)
return b
+1 -1
View File
@@ -21,7 +21,7 @@ func notificationTester(items *[]Notification, last *NotificationCenterResult) *
core.Use(c, core.Settings{})
ui.Column(c).Height(560).Children(func() {
r := NotificationCenter(c, items, NotificationCenterOptions{})
r.Element = nil
r.Element = ui.Element{}
if r != (NotificationCenterResult{}) {
*last = r
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
)
// OrbitLoader moves a diamond around a ring of diamonds.
func OrbitLoader(c *ui.Context, opts LoaderOptions) *ui.Element {
func OrbitLoader(c *ui.Context, opts LoaderOptions) ui.Element {
return loaderMark(c, opts, "orbit_loader", 1, 1600, func(p *ui.Painter, r ui.Rect, t float64, col ui.Color, still bool) {
cx, cy, rad := r.X+r.W/2, r.Y+r.H/2, r.W*0.36
at := func(turn float64) (float32, float32) {
+3 -3
View File
@@ -45,7 +45,7 @@ func presenceMotion(kind PresenceKind) *ui.Motion {
type presenceState struct{ shown bool }
// Presence builds content while show, entering and exiting with motion.
func Presence(c *ui.Context, show bool, opts PresenceOptions, content func()) *ui.Element {
func Presence(c *ui.Context, show bool, opts PresenceOptions, content func()) ui.Element {
m := presenceMotion(opts.Kind)
if opts.Duration < 0 {
panic("mujica: Presence Duration is negative")
@@ -64,11 +64,11 @@ func Presence(c *ui.Context, show bool, opts PresenceOptions, content func()) *u
if !show {
if c.Preferences().ReduceMotion {
// Rebuilding the empty slot cancels MyGo's active exit.
ui.Column(c).Key("presence").Invisible().Transition(ui.ElementTransition{Duration: d, Position: true})
ui.Column(c.Key("presence")).Invisible().Transition(ui.ElementTransition{Duration: d, Position: true})
}
return
}
box := ui.Column(c).AlignItems(ui.Stretch).Key("presence")
box := ui.Column(c.Key("presence")).AlignItems(ui.Stretch)
box.Transition(ui.ElementTransition{Duration: core.Motion(c, d), Enter: m, Exit: m})
box.Children(content)
})
+2 -4
View File
@@ -27,16 +27,14 @@ func TestPresenceEntersExitsAndRemoves(t *testing.T) {
if !tt.HasText("Notice") || *n-before < 3 {
t.Fatalf("kind %d: no entry motion (%d frames)", kind, *n-before)
}
time.Sleep(250 * time.Millisecond)
tt.Frame()
tt.Advance(250 * time.Millisecond)
show = false
before = *n
tt.Frame()
if *n-before < 3 {
t.Fatalf("kind %d: no exit motion", kind)
}
time.Sleep(250 * time.Millisecond)
tt.Frame()
tt.Advance(250 * time.Millisecond)
if tt.HasText("Notice") || motionBusy(tt, n) {
t.Fatalf("kind %d: not removed after exit", kind)
}
+4 -4
View File
@@ -33,7 +33,7 @@ type ProgressOptions struct {
}
// Progress shows work done, value 0 to 1; indeterminate ones hold still under reduced motion.
func Progress(c *ui.Context, value float64, opts ProgressOptions) *ui.Element {
func Progress(c *ui.Context, value float64, opts ProgressOptions) ui.Element {
if opts.Shape != ProgressLinear && opts.Shape != ProgressRing {
panic(fmt.Sprintf("mujica: unknown ProgressShape %d", int(opts.Shape)))
}
@@ -102,7 +102,7 @@ func Progress(c *ui.Context, value float64, opts ProgressOptions) *ui.Element {
return col
}
func progressRing(c *ui.Context, k theme.Tokens, u float32, acc float64, name, caption string, opts ProgressOptions, still bool) *ui.Element {
func progressRing(c *ui.Context, k theme.Tokens, u float32, acc float64, name, caption string, opts ProgressOptions, still bool) ui.Element {
d := opts.Size
if d == 0 {
d = 48
@@ -157,7 +157,7 @@ func progressRing(c *ui.Context, k theme.Tokens, u float32, acc float64, name, c
}
// progressAnimate eases toward target; under reduced motion it lands there and keeps the state in step.
func progressAnimate(e *ui.Element, target float32, still bool) float32 {
func progressAnimate(e ui.Element, target float32, still bool) float32 {
if still {
// A changed target lands at once under reduced motion; the detour stops a running transition.
e.Animate("progress.value", target+1, theme.StateDuration)
@@ -193,7 +193,7 @@ func meterLevel(v float64, l *ui.MeterLevels) int {
}
// Meter shows a level within a range with threshold ticks and a level word, not color alone.
func Meter(c *ui.Context, value float64, opts MeterOptions) *ui.Element {
func Meter(c *ui.Context, value float64, opts MeterOptions) ui.Element {
lo, hi := opts.Min, opts.Max
if lo == 0 && hi == 0 {
hi = 1
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
// PulseLoader shows one dot gently breathing, without a halo.
func PulseLoader(c *ui.Context, opts LoaderOptions) *ui.Element {
func PulseLoader(c *ui.Context, opts LoaderOptions) ui.Element {
return loaderMark(c, opts, "pulse_loader", 1, 1600, func(p *ui.Painter, r ui.Rect, t float64, col ui.Color, still bool) {
s, a := float32(1), float32(1)
if !still {
+3 -3
View File
@@ -20,7 +20,7 @@ type ReorderTransitionOptions struct {
}
// ReorderTransition slides keyed items to their new places.
func ReorderTransition[K comparable](c *ui.Context, keys []K, opts ReorderTransitionOptions, item func(key K)) *ui.Element {
func ReorderTransition[K comparable](c *ui.Context, keys []K, opts ReorderTransitionOptions, item func(key K)) ui.Element {
if opts.Duration < 0 || !(opts.Gap >= 0) || math.IsInf(float64(opts.Gap), 0) {
panic("mujica: ReorderTransition Duration and Gap must not be negative")
}
@@ -50,7 +50,7 @@ func ReorderTransition[K comparable](c *ui.Context, keys []K, opts ReorderTransi
st.keys = append(st.keys[:0], keys...)
col.Children(func() {
for _, k := range keys {
ui.Column(c).Key(k).AlignItems(ui.Stretch).Transition(move).Children(func() { item(k) })
ui.Column(c.Key(k)).AlignItems(ui.Stretch).Transition(move).Children(func() { item(k) })
}
for k, until := range st.leaving {
if seen[k] || !c.Now().Before(until) {
@@ -58,7 +58,7 @@ func ReorderTransition[K comparable](c *ui.Context, keys []K, opts ReorderTransi
continue
}
if c.Preferences().ReduceMotion {
ui.Column(c).Key(k).Absolute().Invisible().Transition(ui.ElementTransition{Duration: d, Position: true})
ui.Column(c.Key(k)).Absolute().Invisible().Transition(ui.ElementTransition{Duration: d, Position: true})
delete(st.leaving, k)
}
}
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
)
func TestReorderTransitionSlides(t *testing.T) {
@@ -24,9 +25,9 @@ func TestReorderTransitionSlides(t *testing.T) {
if *n-before < 3 {
t.Fatal("reorder did not animate")
}
tt.Advance(theme.StateDuration / 2)
mid, _ := tt.Find("Item a")
time.Sleep(260 * time.Millisecond)
tt.Frame()
tt.Advance(260 * time.Millisecond)
end, _ := tt.Find("Item a")
if !(mid.Y > ra.Y && mid.Y < end.Y) || motionBusy(tt, n) {
t.Fatalf("a moved %v → %v → %v", ra.Y, mid.Y, end.Y)
+1 -1
View File
@@ -35,7 +35,7 @@ type ResultOptions struct {
// ResultResult is what a Result built and reported this frame.
type ResultResult struct {
Element *ui.Element
Element ui.Element
// Action is the index of the clicked action, -1 for none.
Action int
}
+2 -2
View File
@@ -21,7 +21,7 @@ type ShimmerOptions struct {
}
// Shimmer sweeps a placeholder while loading, then builds content.
func Shimmer(c *ui.Context, loading bool, opts ShimmerOptions, content func()) *ui.Element {
func Shimmer(c *ui.Context, loading bool, opts ShimmerOptions, content func()) ui.Element {
if !(opts.Width >= 0) || !(opts.Height >= 0) || math.IsInf(float64(opts.Width+opts.Height), 1) {
panic("mujica: Shimmer size must be finite and not negative")
}
@@ -54,7 +54,7 @@ func Shimmer(c *ui.Context, loading bool, opts ShimmerOptions, content func()) *
}
// Keyed by motion so a flip repaints.
e.Children(func() {
ui.Box(c).Key(still).Grow(1).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) {
ui.Box(c.Key(still)).Grow(1).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) {
p.Fill(r, fill, theme.CardRadius)
if still {
return
+1 -2
View File
@@ -19,8 +19,7 @@ func TestShimmerOnlyWhileLoading(t *testing.T) {
t.Fatalf("loading: %q", tt.Texts())
}
a := encode(t, tt.Image())
time.Sleep(150 * time.Millisecond)
tt.Frame()
tt.Advance(150 * time.Millisecond)
if bytes.Equal(a, encode(t, tt.Image())) || !motionBusy(tt, n) {
t.Fatal("shimmer does not sweep")
}
+1 -1
View File
@@ -13,7 +13,7 @@ type ShimmerTextOptions struct {
}
// ShimmerText sweeps light across a status such as "Thinking".
func ShimmerText(c *ui.Context, text string, opts ShimmerTextOptions) *ui.Element {
func ShimmerText(c *ui.Context, text string, opts ShimmerTextOptions) ui.Element {
if text == "" {
panic("mujica: ShimmerText needs text")
}
+1 -2
View File
@@ -19,8 +19,7 @@ func TestShimmerTextSweepsAndStops(t *testing.T) {
t.Fatal("text not named")
}
a := encode(t, tt.Image())
time.Sleep(200 * time.Millisecond)
tt.Frame()
tt.Advance(200 * time.Millisecond)
if bytes.Equal(a, encode(t, tt.Image())) || !motionBusy(tt, n) {
t.Fatal("no sweep")
}
+1 -1
View File
@@ -31,7 +31,7 @@ type SkeletonOptions struct {
}
// Skeleton stands in for loading content; it pulses while painted, static under reduced motion.
func Skeleton(c *ui.Context, opts SkeletonOptions) *ui.Element {
func Skeleton(c *ui.Context, opts SkeletonOptions) ui.Element {
if opts.Shape != SkeletonText && opts.Shape != SkeletonCircle && opts.Shape != SkeletonRect {
panic(fmt.Sprintf("mujica: unknown SkeletonShape %d", int(opts.Shape)))
}
+2 -4
View File
@@ -43,15 +43,13 @@ func TestSkeletonStaticUnderReducedMotion(t *testing.T) {
Skeleton(c, SkeletonOptions{Shape: SkeletonRect})
}, 200, 100)
a := encode(t, tt.Image())
time.Sleep(300 * time.Millisecond)
tt.Frame()
tt.Advance(300 * time.Millisecond)
if bytes.Equal(a, encode(t, tt.Image())) {
t.Fatal("skeleton did not pulse")
}
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
a = encode(t, tt.Image())
time.Sleep(300 * time.Millisecond)
tt.Frame()
tt.Advance(300 * time.Millisecond)
if !bytes.Equal(a, encode(t, tt.Image())) {
t.Fatal("skeleton moved under reduced motion")
}
+2 -2
View File
@@ -26,7 +26,7 @@ type staggerState struct {
}
// Stagger builds n items entering one after another, once.
func Stagger(c *ui.Context, n int, opts StaggerOptions, item func(i int)) *ui.Element {
func Stagger(c *ui.Context, n int, opts StaggerOptions, item func(i int)) ui.Element {
if n < 0 || opts.Interval < 0 || opts.Duration < 0 {
panic("mujica: Stagger needs non-negative n, Interval and Duration")
}
@@ -65,7 +65,7 @@ func Stagger(c *ui.Context, n int, opts StaggerOptions, item func(i int)) *ui.El
}
box.Children(func() {
for i := range upto {
ui.Column(c).Key(i).AlignItems(ui.Stretch).
ui.Column(c.Key(i)).AlignItems(ui.Stretch).
Transition(ui.ElementTransition{Duration: core.Motion(c, d), Enter: enter}).Children(func() { item(i) })
}
})
+3 -6
View File
@@ -19,15 +19,12 @@ func TestStaggerRevealsInTurnOnce(t *testing.T) {
if !tt.HasText("Row 0") || tt.HasText("Row 4") {
t.Fatalf("first frame: %q", tt.Texts())
}
time.Sleep(120 * time.Millisecond)
tt.Frame()
tt.Advance(120 * time.Millisecond)
if !tt.HasText("Row 2") || tt.HasText("Row 4") {
t.Fatalf("after 120 ms: %q", tt.Texts())
}
time.Sleep(300 * time.Millisecond)
tt.Frame()
time.Sleep(250 * time.Millisecond)
tt.Frame()
tt.Advance(300 * time.Millisecond)
tt.Advance(250 * time.Millisecond)
if !tt.HasText("Row 4") || motionBusy(tt, n) {
t.Fatalf("did not finish and settle: %q", tt.Texts())
}
+1 -1
View File
@@ -44,7 +44,7 @@ type StatusIndicatorOptions struct {
}
// StatusIndicator shows a status by its own shape and a word, never by color alone.
func StatusIndicator(c *ui.Context, status Status, opts StatusIndicatorOptions) *ui.Element {
func StatusIndicator(c *ui.Context, status Status, opts StatusIndicatorOptions) ui.Element {
key := "status_indicator." + status.String() // panics on an unknown Status
k := core.Tokens(c)
u := core.Density(c).Unit()
+1 -1
View File
@@ -243,7 +243,7 @@ func Toaster(c *ui.Context) ToasterResult {
// toastCard builds one toast and reports its events and whether hover or focus holds it.
func toastCard(c *ui.Context, k theme.Tokens, u, width float32, m ToastMessage) (action, closed, held bool) {
tone, icon := core.SeverityLook(k, m.Severity)
card := ui.Row(c).Key(m.ID).Width(width).AlignItems(ui.Center).Gap(u*3).Padding(u*2, u*2, u*2, u*4).
card := ui.Row(c.Key(m.ID)).Width(width).AlignItems(ui.Center).Gap(u*3).Padding(u*2, u*2, u*2, u*4).
Radius(theme.PopupRadius).Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).
Role(ui.RoleStatus).Label(m.Message)
card.Shadow(0, 4, 12, 0, ui.RGBA(0, 0, 0, 0.14))
+4 -5
View File
@@ -179,10 +179,10 @@ func TestToastExpiresAndHoverPauses(t *testing.T) {
if !res.wake.IsZero() {
t.Fatal("hovered toast still schedules its expiry")
}
time.Sleep(2 * life)
released := time.Now()
tt.Advance(2 * life)
released := tt.Now()
tt.Move(5, 395)
done := time.Now()
done := tt.Now()
// The frame that ends the hover must schedule the expiry itself.
if res.wake.IsZero() || res.wake.Before(released) || res.wake.After(done.Add(life)) {
t.Fatalf("release scheduled wake %v, released %v", res.wake, released)
@@ -191,8 +191,7 @@ func TestToastExpiresAndHoverPauses(t *testing.T) {
t.Fatal("hovered toast expired")
}
// Stands in for the frame MyGo's timer runs at res.wake.
time.Sleep(time.Until(res.wake) + 5*time.Millisecond)
tt.Frame()
tt.Advance(res.wake.Sub(tt.Now()) + 5*time.Millisecond)
if tt.HasText("Copied") {
t.Fatal("toast stayed after its wake-up")
}
+2 -2
View File
@@ -37,7 +37,7 @@ type ToolRegistryPanelOptions struct {
// ToolRegistryResult is a built ToolRegistryPanel and its events.
type ToolRegistryResult struct {
Element *ui.Element
Element ui.Element
List data.ListView
toggled string
on bool
@@ -123,7 +123,7 @@ func toolSchema(c *ui.Context, t AgentTool) {
ui.Text(c, core.Msg(c, "tool_registry_panel.no_params")).TextColor(k.TextMuted)
return
}
ui.Column(c).Key(t.Name).Grow(1).MinHeight(0).Children(func() {
ui.Column(c.Key(t.Name)).Grow(1).MinHeight(0).Children(func() {
ui.List(c, nil, len(t.Params), func(i int) {
p := t.Params[i]
ui.Column(c).Gap(2).MinHeight(core.Density(c).ControlHeight()).Children(func() {
+1 -1
View File
@@ -160,7 +160,7 @@ func traceAttrs(c *ui.Context, sp TraceSpan, height float32) {
})
}
if len(sp.Attrs) > 0 {
ui.Column(c).Key(sp.ID).Height(min(float32(len(sp.Attrs))*core.Density(c).ControlHeight(), max(core.Density(c).ControlHeight(), min(200, height/4)))).Children(func() {
ui.Column(c.Key(sp.ID)).Height(min(float32(len(sp.Attrs))*core.Density(c).ControlHeight(), max(core.Density(c).ControlHeight(), min(200, height/4)))).Children(func() {
ui.List(c, nil, len(sp.Attrs), func(i int) {
a := sp.Attrs[i]
row(a.Key, a.Value)
+2 -4
View File
@@ -21,8 +21,7 @@ func TestTypewriterRevealsFinishesAndSkips(t *testing.T) {
if tt.HasText(text) && motionCount(tt, text) == 2 {
t.Fatal("whole text shown at once")
}
time.Sleep(120 * time.Millisecond)
tt.Frame()
tt.Advance(120 * time.Millisecond)
shown := 0
for _, s := range tt.Texts() {
if s != text && s != "Skip" && len(s) > shown {
@@ -43,8 +42,7 @@ func TestTypewriterRevealsFinishesAndSkips(t *testing.T) {
if motionCount(tt, "The court assembles at dusk.") != 1 {
t.Fatalf("appended text restarted: %q", tt.Texts())
}
time.Sleep(300 * time.Millisecond)
tt.Frame()
tt.Advance(300 * time.Millisecond)
if motionCount(tt, text) != 2 || finished != 2 {
t.Fatalf("append: finished %d %q", finished, tt.Texts())
}
+1 -1
View File
@@ -27,7 +27,7 @@ type UndoToastOptions struct {
// UndoToastResult reports how an UndoToast ended this frame, if it did.
type UndoToastResult struct {
Element *ui.Element
Element ui.Element
Undone bool
// Expired: the countdown ran out, or the user closed it; the action stands.
Expired bool
+2 -4
View File
@@ -50,14 +50,12 @@ func TestUndoToastCountdownPauseAndClose(t *testing.T) {
_ = tt.Click("Delete")
r, _ := tt.Find("File deleted")
tt.Move(r.X+r.W/2, r.Y+r.H/2)
time.Sleep(90 * time.Millisecond)
tt.Frame()
tt.Advance(90 * time.Millisecond)
if !open {
t.Fatal("hovered countdown ran out")
}
tt.Move(5, 5)
time.Sleep(90 * time.Millisecond)
tt.Frame()
tt.Advance(90 * time.Millisecond)
if open || !last.Expired || undos != 0 {
t.Fatalf("expiry: %v %+v %d", open, last, undos)
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
)
// WaveLoader draws a sine line flowing to the right.
func WaveLoader(c *ui.Context, opts LoaderOptions) *ui.Element {
func WaveLoader(c *ui.Context, opts LoaderOptions) ui.Element {
return loaderMark(c, opts, "wave_loader", 2.5, 1400, func(p *ui.Painter, r ui.Rect, t float64, col ui.Color, still bool) {
var path ui.Path
amp := r.H * 0.3