Add layout components 0001-0025
Stack through StickyHeader, with behavior tests and gallery demos. kit gains Arrive, Settle (paint-time layout check) and Shade. Several actions return named ui.Element results; mygo vet forbids elements in structs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b34562333b
commit
4efaf2680f
41 files changed
+1765
-11
No files matched your search
+26
-1
@@ -3,6 +3,7 @@ package kit
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/CrychicUI/sound"
|
||||
@@ -27,6 +28,18 @@ func Lift(e ui.Element) ui.Element {
|
||||
return e.AnimatePose(p, ui.Snappy)
|
||||
}
|
||||
|
||||
// Arrive eases from a pose to rest on first build.
|
||||
func Arrive(e ui.Element, from ui.Pose, delay time.Duration) ui.Element {
|
||||
p := e.Keyframes("crychic.arrive", []ui.Keyframe{{Value: 1}, {At: 200 * time.Millisecond, Value: 0, Ease: ui.EaseOut}},
|
||||
ui.KeyframeOptions{Delay: delay})
|
||||
s := from.Scale
|
||||
if s == 0 {
|
||||
s = 1
|
||||
}
|
||||
k := 1 + (s-1)*p
|
||||
return e.Translate(from.X*p, from.Y*p).Scale(k, k).Opacity(1 - from.Fade*p)
|
||||
}
|
||||
|
||||
var popSpring = ui.Bouncy
|
||||
|
||||
var (
|
||||
@@ -66,9 +79,21 @@ func Changed(e ui.Element, cue sound.Cue) bool {
|
||||
func Card(c *ui.Context) ui.Element {
|
||||
t := c.Theme()
|
||||
return ui.Column(c).Background(t.Background).Border(1, t.Border).Radius(t.Radius).
|
||||
Shadow(0, 1, 3, 0, ui.RGBA(0, 0, 0, 0.06))
|
||||
Shadow(0, 1, 3, 0, Shade(0.06))
|
||||
}
|
||||
|
||||
// Settle rebuilds when e's laid-out read differs from used.
|
||||
func Settle(c *ui.Context, e ui.Element, used []float32, read func() []float32) {
|
||||
e.DrawOver(func(*ui.Painter, ui.Rect) {
|
||||
if !slices.Equal(read(), used) {
|
||||
c.Invalidate()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Shade is black at alpha a, for shadows.
|
||||
func Shade(a float32) ui.Color { return ui.RGBA(0, 0, 0, a) }
|
||||
|
||||
// Muted returns secondary text.
|
||||
func Muted(c *ui.Context, s string) ui.Element {
|
||||
return ui.Text(c, s).TextColor(c.Theme().TextMuted)
|
||||
|
||||
@@ -200,3 +200,63 @@ func TestLog(t *testing.T) {
|
||||
t.Errorf("log = %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArriveFadesIn(t *testing.T) {
|
||||
for _, delay := range []time.Duration{0, 200 * time.Millisecond} {
|
||||
var bg, fill ui.Color
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
th := c.Theme()
|
||||
bg, fill = th.Background, th.Accent
|
||||
ui.Column(c).Size(100, 100).Background(bg).Children(func() {
|
||||
Arrive(ui.Box(c).Size(100, 100).Background(fill), ui.Pose{Fade: 1}, delay)
|
||||
})
|
||||
}, 100, 100)
|
||||
if !tt.FramePending() || !near(tt.Image(), 50, 50, bg) {
|
||||
t.Fatalf("delay %v: box shown before it arrived", delay)
|
||||
}
|
||||
tt.Advance(delay + 100*time.Millisecond)
|
||||
if near(tt.Image(), 50, 50, bg) || near(tt.Image(), 50, 50, fill) {
|
||||
t.Errorf("delay %v: box not mid-fade", delay)
|
||||
}
|
||||
tt.Advance(time.Second)
|
||||
if tt.FramePending() || !near(tt.Image(), 50, 50, fill) {
|
||||
t.Errorf("delay %v: box did not settle", delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShade(t *testing.T) {
|
||||
if s := Shade(0.5); s.R != 0 || s.G != 0 || s.B != 0 || s.A != 128 {
|
||||
t.Errorf("Shade(0.5) = %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettleRebuildsWhenLayoutMoves(t *testing.T) {
|
||||
var w, prev, used float32 = 40, 0, 0
|
||||
builds := 0
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
builds++
|
||||
b := ui.Box(c).FillWidth().MaxWidth(w).Height(10)
|
||||
used = prev
|
||||
prev = b.LayoutBounds().W
|
||||
Settle(c, b, []float32{used}, func() []float32 { return []float32{b.LayoutBounds().W} })
|
||||
}, 100, 100)
|
||||
if builds != 3 || used != 40 {
|
||||
t.Fatalf("first frame: %d builds used %v, want 3 using 40", builds, used)
|
||||
}
|
||||
builds = 0
|
||||
tt.Frame()
|
||||
if builds != 1 {
|
||||
t.Errorf("steady layout ran %d builds, want 1", builds)
|
||||
}
|
||||
builds, w = 0, 60
|
||||
tt.Frame()
|
||||
if builds != 3 || used != 60 {
|
||||
t.Errorf("changed layout: %d builds used %v, want 3 using 60", builds, used)
|
||||
}
|
||||
builds = 0
|
||||
tt.SetSize(50, 100)
|
||||
if builds != 3 || used != 50 {
|
||||
t.Errorf("resize: %d builds used %v, want 3 using 50", builds, used)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user