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>
93 lines
2.6 KiB
Go
93 lines
2.6 KiB
Go
package feedback
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
|
|
)
|
|
|
|
// SkeletonShape is the placeholder's form. The zero value is SkeletonText.
|
|
type SkeletonShape int
|
|
|
|
const (
|
|
SkeletonText SkeletonShape = iota
|
|
SkeletonCircle
|
|
SkeletonRect
|
|
)
|
|
|
|
// SkeletonOptions configure a Skeleton. Sizes are fixed so content does not shift.
|
|
type SkeletonOptions struct {
|
|
Shape SkeletonShape
|
|
// Width fills the parent when zero; a circle uses it as its diameter (40 DIP when zero).
|
|
Width float32
|
|
// Height is the rect height (ControlHeight when zero); text derives it from Lines.
|
|
Height float32
|
|
// Lines is the text line count, 1 when zero; the last of several is shorter.
|
|
Lines int
|
|
}
|
|
|
|
// Skeleton stands in for loading content; it pulses while painted, static under reduced motion.
|
|
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)))
|
|
}
|
|
if !(opts.Width >= 0) || !(opts.Height >= 0) || opts.Lines < 0 || math.IsInf(float64(opts.Width+opts.Height), 1) {
|
|
panic(fmt.Sprintf("mujica: Skeleton size %vx%v, %d lines invalid", opts.Width, opts.Height, opts.Lines))
|
|
}
|
|
k := core.Tokens(c)
|
|
still := c.Preferences().ReduceMotion
|
|
fill := k.SurfaceHover
|
|
if k.Dark {
|
|
fill = k.Border
|
|
}
|
|
paint := func(p *ui.Painter, r ui.Rect, radius float32) {
|
|
a := float32(1)
|
|
if !still {
|
|
p.AnimationFrame()
|
|
t := float64(p.Now().UnixMilli()%1600) / 1600
|
|
a = 0.55 + 0.45*float32(0.5+0.5*math.Cos(2*math.Pi*t))
|
|
}
|
|
p.Fill(r, fill.Alpha(a), radius)
|
|
}
|
|
e := ui.Box(c).Role(ui.RoleNone)
|
|
if opts.Width > 0 {
|
|
e.Width(opts.Width)
|
|
} else if opts.Shape != SkeletonCircle {
|
|
e.FillWidth()
|
|
}
|
|
switch opts.Shape {
|
|
case SkeletonCircle:
|
|
d := opts.Width
|
|
if d == 0 {
|
|
d = 40
|
|
}
|
|
e.Size(d, d).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { paint(p, r, r.W/2) })
|
|
case SkeletonRect:
|
|
h := opts.Height
|
|
if h == 0 {
|
|
h = core.Density(c).ControlHeight()
|
|
}
|
|
e.Height(h).Draw(func(p *ui.Painter, r ui.Rect) { paint(p, r, theme.CardRadius) })
|
|
default:
|
|
lines := max(opts.Lines, 1)
|
|
size := core.FontSize(c, theme.BodySize)
|
|
line := size * theme.LineHeight
|
|
e.Height(line * float32(lines)).Draw(func(p *ui.Painter, r ui.Rect) {
|
|
bar := size * 0.8
|
|
for i := range lines {
|
|
w := r.W
|
|
if lines > 1 && i == lines-1 {
|
|
w *= 0.6
|
|
}
|
|
y := r.Y + float32(i)*line + (line-bar)/2
|
|
paint(p, ui.Rect{X: r.X, Y: y, W: w, H: bar}, theme.ControlRadius)
|
|
}
|
|
})
|
|
}
|
|
return e
|
|
}
|