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>
150 lines
4.5 KiB
Go
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")
|
|
}
|
|
}
|