Files
CrychicUI/layout/grid_test.go
T
ZacharyZhang-NYandClaude Opus 5.5 4efaf2680f 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>
2026-10-17 02:49:32 -04:00

150 lines
4.5 KiB
Go

package layout
import (
"fmt"
"testing"
"github.com/egoist/mygo/ui"
)
func block(c *ui.Context, label string, h float32) func() {
return func() { ui.Column(c).Height(h).Children(func() { ui.Text(c, label) }) }
}
func TestMasonryFillsShortest(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
Masonry(c, 2, []func(){block(c, "I0", 120), block(c, "I1", 20), block(c, "I2", 20), block(c, "I3", 20)})
}, 400, 400)
tt.Advance(time2s)
i0, i1, i2, i3 := find(t, tt, "I0"), find(t, tt, "I1"), find(t, tt, "I2"), find(t, tt, "I3")
if i1.X == i0.X || i2.X != i1.X || i3.X != i1.X || !(i1.Y < i2.Y && i2.Y < i3.Y) {
t.Errorf("placement %v %v %v %v", i0, i1, i2, i3)
}
panics(t, "0 columns", func() { ui.Render(func(c *ui.Context) { Masonry(c, 0, nil) }, 10, 10, 1) })
}
func TestAutoGridColumns(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
var items []func()
for i := range 4 {
items = append(items, block(c, fmt.Sprintf("G%d", i), 30))
}
AutoGrid(c, 120, items)
}, 412, 300)
tt.Advance(time2s)
g0, g1, g2, g3 := find(t, tt, "G0"), find(t, tt, "G1"), find(t, tt, "G2"), find(t, tt, "G3")
if !(g0.X < g1.X && g1.X < g2.X) || g0.Y != g2.Y || g3.X != g0.X || g3.Y <= g0.Y {
t.Errorf("not 3 columns: %v %v %v %v", g0, g1, g2, g3)
}
tt.SetSize(260, 300)
tt.Advance(time2s)
if g1, g2 := find(t, tt, "G1"), find(t, tt, "G2"); g2.X != find(t, tt, "G0").X || g1.Y != find(t, tt, "G0").Y {
t.Error("not 2 columns after resize")
}
panics(t, "minWidth 0", func() { ui.Render(func(c *ui.Context) { AutoGrid(c, 0, nil) }, 10, 10, 1) })
}
func TestBentoSpans(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
Bento(c, 3, []BentoTile{
{Cols: 2, Content: func() { ui.Text(c, "Wide") }},
{Content: func() { ui.Text(c, "One") }},
{Content: func() { ui.Text(c, "Two") }},
})
}, 600, 300)
w, one, two := find(t, tt, "Wide"), find(t, tt, "One"), find(t, tt, "Two")
if one.Y != w.Y || one.X < 380 || two.Y <= w.Y || two.X > w.X+1 {
t.Errorf("spans wrong: %v %v %v", w, one, two)
}
tt.Move(500, 40)
settles(t, tt, "Bento lift")
panics(t, "span 4 of 3", func() {
ui.Render(func(c *ui.Context) { Bento(c, 3, []BentoTile{{Cols: 4}}) }, 10, 10, 1)
})
}
func TestClusterAligns(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
Cluster(c, ui.End, []func(){func() { ui.Text(c, "Tail") }})
}, 300, 100)
if r := find(t, tt, "Tail"); abs(r.X+r.W-300) > 1 {
t.Errorf("Tail ends at %v", r.X+r.W)
}
panics(t, "Stretch", func() { ui.Render(func(c *ui.Context) { Cluster(c, ui.Stretch, nil) }, 10, 10, 1) })
}
func TestColumnsEqual(t *testing.T) {
var gap float32
n := 2
tt := ui.NewTester(func(c *ui.Context) {
gap = c.Theme().Space(4)
items := []func(){func() { ui.Text(c, "C0") }, func() { ui.Text(c, "C1") }, func() { ui.Text(c, "C2") }}
Columns(c, items[:n])
}, 400, 100)
if x := find(t, tt, "C1").X; abs(x-(200+gap/2)) > 0.5 {
t.Errorf("C1 at %v", x)
}
n = 3
tt.Frame()
settles(t, tt, "Columns width")
if x := find(t, tt, "C1").X; abs(x-(400-2*gap)/3-gap) > 0.5 {
t.Errorf("C1 at %v after a third column", x)
}
panics(t, "no items", func() { ui.Render(func(c *ui.Context) { Columns(c, nil) }, 10, 10, 1) })
}
func TestMasonryReplacesWhenAnItemShrinks(t *testing.T) {
h0 := float32(120)
tt := ui.NewTester(func(c *ui.Context) {
Masonry(c, 2, []func(){block(c, "I0", h0), block(c, "I1", 20), block(c, "I2", 20)})
}, 400, 400)
tt.Advance(time2s)
if find(t, tt, "I2").X == find(t, tt, "I0").X {
t.Fatal("I2 starts under I0")
}
x2 := find(t, tt, "I2").X
h0 = 10
tt.Frame()
if find(t, tt, "I2").X != x2 {
t.Error("I2 jumped instead of sliding")
}
tt.Advance(time2s)
if find(t, tt, "I2").X != find(t, tt, "I0").X {
t.Error("I2 stayed beside I0 after I0 shrank")
}
}
func TestMasonrySettlesOnAnEmptyItem(t *testing.T) {
full := true
tt := ui.NewTester(func(c *ui.Context) {
first := func() {
if full {
ui.Box(c).Height(120)
}
}
Masonry(c, 2, []func(){first, block(c, "I1", 20), block(c, "I2", 20)})
}, 400, 400)
tt.Advance(time2s)
full = false
tt.Frame()
tt.Advance(time2s)
if tt.FramePending() {
t.Fatal("Masonry kept rebuilding after an item emptied")
}
if find(t, tt, "I2").Y >= find(t, tt, "I1").Y+20 {
t.Error("I2 did not move into the emptied column")
}
}
func TestMasonrySettlesAtZeroWidth(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
ui.Column(c).Width(0).ClipX().Children(func() {
Masonry(c, 2, []func(){block(c, "I0", 40), block(c, "I1", 20)})
})
}, 400, 400)
tt.Advance(time2s)
if tt.FramePending() {
t.Error("zero-width Masonry kept rebuilding")
}
}