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
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user