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>
254 lines
7.9 KiB
Go
254 lines
7.9 KiB
Go
package layout
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
)
|
|
|
|
func TestStackGapsAndResizes(t *testing.T) {
|
|
more := false
|
|
var gap float32
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
gap = c.Theme().Space(4)
|
|
Stack(c, 4, func() {
|
|
ui.Text(c, "Alpha")
|
|
ui.Text(c, "Beta")
|
|
if more {
|
|
ui.Text(c, "Gamma")
|
|
}
|
|
})
|
|
}, 300, 300)
|
|
a, b := find(t, tt, "Alpha"), find(t, tt, "Beta")
|
|
if got := b.Y - (a.Y + a.H); abs(got-gap) > 0.5 {
|
|
t.Errorf("gap %v, want %v", got, gap)
|
|
}
|
|
more = true
|
|
tt.Frame()
|
|
settles(t, tt, "Stack size")
|
|
find(t, tt, "Gamma")
|
|
}
|
|
|
|
func TestInlineWraps(t *testing.T) {
|
|
var gap float32
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
gap = c.Theme().Space(2)
|
|
Inline(c, 2, func() {
|
|
for _, s := range []string{"Alpha", "Beta", "Gamma", "Delta", "Epsilon"} {
|
|
ui.Text(c, s)
|
|
}
|
|
})
|
|
}, 160, 300)
|
|
a, b, e := find(t, tt, "Alpha"), find(t, tt, "Beta"), find(t, tt, "Epsilon")
|
|
if got := b.X - (a.X + a.W); abs(got-gap) > 0.5 {
|
|
t.Errorf("gap %v, want %v", got, gap)
|
|
}
|
|
if e.Y <= a.Y {
|
|
t.Error("items did not wrap")
|
|
}
|
|
}
|
|
|
|
func TestLayersOverlapAndFade(t *testing.T) {
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
Layers(c, []func(){func() { ui.Text(c, "Back") }, func() { ui.Text(c, "Front") }})
|
|
}, 300, 200)
|
|
settles(t, tt, "Layers fade")
|
|
if b, f := find(t, tt, "Back"), find(t, tt, "Front"); b.X != f.X || b.Y != f.Y {
|
|
t.Errorf("layers apart: %v %v", b, f)
|
|
}
|
|
panics(t, "Layers(nil)", func() { ui.Render(func(c *ui.Context) { Layers(c, nil) }, 10, 10, 1) })
|
|
}
|
|
|
|
func TestCentered(t *testing.T) {
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
Centered(c, func() { ui.Text(c, "Mid") })
|
|
}, 300, 200)
|
|
settles(t, tt, "Centered scale")
|
|
r := find(t, tt, "Mid")
|
|
if abs(r.X+r.W/2-150) > 1 || abs(r.Y+r.H/2-100) > 1 {
|
|
t.Errorf("Mid at %v, not centered", r)
|
|
}
|
|
}
|
|
|
|
func TestContainerWidth(t *testing.T) {
|
|
var pad, rem float32
|
|
width := float32(400)
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
pad, rem = c.Theme().Space(6), c.Theme().Rem(64)
|
|
Container(c, ContainerOptions{MaxWidth: width}, func() { ui.Text(c, "Edge") })
|
|
}, 1200, 200)
|
|
if x := find(t, tt, "Edge").X; abs(x-((1200-width)/2+pad)) > 0.5 {
|
|
t.Errorf("content at %v", x)
|
|
}
|
|
width = 0
|
|
tt.Frame()
|
|
settles(t, tt, "Container width")
|
|
if x := find(t, tt, "Edge").X; abs(x-((1200-rem)/2+pad)) > 0.5 {
|
|
t.Errorf("default width puts content at %v", x)
|
|
}
|
|
panics(t, "negative MaxWidth", func() {
|
|
ui.Render(func(c *ui.Context) { Container(c, ContainerOptions{MaxWidth: -1}, nil) }, 10, 10, 1)
|
|
})
|
|
}
|
|
|
|
// inked finds ink in a column span of pixels.
|
|
func inked(tt *ui.Tester, x int, y0, y1 float32) bool {
|
|
img := tt.Image()
|
|
for y := max(int(y0), 0); y <= min(int(y1), img.Bounds().Dy()-1); y++ {
|
|
if lum(img, x, y) < lum(img, 299, 199)-20000 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestSectionDividerGrows(t *testing.T) {
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
Section(c, "Title", func() { ui.Text(c, "Body") })
|
|
}, 300, 200)
|
|
title, body := find(t, tt, "Title"), find(t, tt, "Body")
|
|
if body.Y <= title.Y {
|
|
t.Fatal("body above title")
|
|
}
|
|
if inked(tt, 280, title.Y+title.H, body.Y-1) {
|
|
t.Error("divider drawn before it grew")
|
|
}
|
|
settles(t, tt, "Section divider")
|
|
if !inked(tt, 280, title.Y+title.H, body.Y-1) {
|
|
t.Error("divider never reached the right edge")
|
|
}
|
|
}
|
|
|
|
func TestSurfaces(t *testing.T) {
|
|
var th ui.Theme
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
th = *c.Theme()
|
|
ui.Column(c).Gap(10).Children(func() {
|
|
Panel(c, func() { ui.Box(c).Size(100, 40) })
|
|
Inset(c, func() { ui.Box(c).Size(100, 40) })
|
|
Well(c, func() { ui.Box(c).Size(100, 40) })
|
|
})
|
|
}, 300, 300)
|
|
tt.Advance(time.Second)
|
|
img := tt.Image()
|
|
for _, p := range [][2]int{{60, 30}, {60, 100}, {60, 170}} {
|
|
if !near(img, p[0], p[1], th.Surface) {
|
|
t.Errorf("pixel %v is not Surface", p)
|
|
}
|
|
}
|
|
tt.SetDark(true)
|
|
if !tt.FramePending() {
|
|
t.Error("Inset color did not animate to the dark theme")
|
|
}
|
|
tt.Advance(2 * time.Second)
|
|
if !near(tt.Image(), 60, 100, ui.DarkTheme().Surface) {
|
|
t.Error("Inset did not settle on the dark surface")
|
|
}
|
|
}
|
|
|
|
func TestCardLifts(t *testing.T) {
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
ui.Column(c).Padding(20).Children(func() {
|
|
Card(c, func() { ui.Box(c).Size(100, 60) })
|
|
})
|
|
}, 300, 200)
|
|
tt.Move(70, 50)
|
|
settles(t, tt, "Card lift")
|
|
}
|
|
|
|
func TestFrameClips(t *testing.T) {
|
|
var th ui.Theme
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
th = *c.Theme()
|
|
ui.Column(c).Padding(20).Children(func() {
|
|
Frame(c, func() { ui.Box(c).Size(100, 60).Background(th.Accent) })
|
|
})
|
|
}, 300, 200)
|
|
settles(t, tt, "Frame fade")
|
|
if img := tt.Image(); near(img, 20, 20, th.Accent) || !near(img, 70, 50, th.Accent) {
|
|
t.Error("Frame did not clip its corner")
|
|
}
|
|
}
|
|
|
|
func TestAspectBox(t *testing.T) {
|
|
var th ui.Theme
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
th = *c.Theme()
|
|
ui.Column(c).Width(300).Children(func() {
|
|
AspectBox(c, 2, func() { ui.Box(c).Fill().Background(th.Accent) })
|
|
})
|
|
}, 300, 300)
|
|
if img := tt.Image(); !near(img, 150, 140, th.Accent) || near(img, 150, 160, th.Accent) {
|
|
t.Error("AspectBox is not 300x150")
|
|
}
|
|
panics(t, "ratio 0", func() { ui.Render(func(c *ui.Context) { AspectBox(c, 0, nil) }, 10, 10, 1) })
|
|
}
|
|
|
|
func TestAppFrame(t *testing.T) {
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
AppFrame(c, func() { ui.Text(c, "Top") }, func() { ui.Text(c, "Body") }, func() { ui.Text(c, "Status") })
|
|
}, 400, 300)
|
|
top, body, st := find(t, tt, "Top"), find(t, tt, "Body"), find(t, tt, "Status")
|
|
if !(top.Y < body.Y && body.Y < st.Y) {
|
|
t.Errorf("order %v %v %v", top, body, st)
|
|
}
|
|
mid := func(r ui.Rect) int { return int(r.X + r.W/2) }
|
|
if inked(tt, mid(top), top.Y, top.Y+top.H) || inked(tt, mid(st), st.Y, st.Y+st.H) {
|
|
t.Error("bars shown before they slid in")
|
|
}
|
|
settles(t, tt, "AppFrame bars")
|
|
if !inked(tt, mid(top), top.Y-2, top.Y+top.H+2) || !inked(tt, mid(st), st.Y-2, st.Y+st.H+2) {
|
|
t.Error("bars never arrived")
|
|
}
|
|
}
|
|
|
|
func TestHolyGrail(t *testing.T) {
|
|
aside := true
|
|
tt := ui.NewTester(func(c *ui.Context) {
|
|
s := HolyGrailSlots{
|
|
Header: func() { ui.Text(c, "Head") }, Nav: func() { ui.Text(c, "Nav") },
|
|
Main: func() { ui.Text(c, "Main") }, Footer: func() { ui.Text(c, "Foot") },
|
|
}
|
|
if aside {
|
|
s.Aside = func() { ui.Text(c, "Aside") }
|
|
}
|
|
HolyGrail(c, s)
|
|
}, 600, 400)
|
|
nav, main, side := find(t, tt, "Nav"), find(t, tt, "Main"), find(t, tt, "Aside")
|
|
if !(nav.X < main.X && main.X < side.X) || find(t, tt, "Head").Y >= main.Y || find(t, tt, "Foot").Y <= main.Y {
|
|
t.Error("slots out of place")
|
|
}
|
|
aside = false
|
|
tt.Frame()
|
|
settles(t, tt, "HolyGrail aside exit")
|
|
if tt.HasText("Aside") {
|
|
t.Error("aside still shown")
|
|
}
|
|
panics(t, "no Main", func() { ui.Render(func(c *ui.Context) { HolyGrail(c, HolyGrailSlots{}) }, 10, 10, 1) })
|
|
}
|
|
|
|
func TestRejectsBadNumbers(t *testing.T) {
|
|
nan, inf := float32(math.NaN()), float32(math.Inf(1))
|
|
none := func() {}
|
|
cases := map[string]func(c *ui.Context){
|
|
"Stack gap -1": func(c *ui.Context) { Stack(c, -1, none) },
|
|
"Stack gap NaN": func(c *ui.Context) { Stack(c, nan, none) },
|
|
"Inline gap Inf": func(c *ui.Context) { Inline(c, inf, none) },
|
|
"Container NaN": func(c *ui.Context) { Container(c, ContainerOptions{MaxWidth: nan}, none) },
|
|
"Container Inf": func(c *ui.Context) { Container(c, ContainerOptions{MaxWidth: inf}, none) },
|
|
"AspectBox NaN": func(c *ui.Context) { AspectBox(c, nan, none) },
|
|
"AspectBox Inf": func(c *ui.Context) { AspectBox(c, inf, none) },
|
|
"AutoGrid NaN": func(c *ui.Context) { AutoGrid(c, nan, nil) },
|
|
"AutoGrid Inf": func(c *ui.Context) { AutoGrid(c, inf, nil) },
|
|
"SplitPane size NaN": func(c *ui.Context) { SplitPane(c, &nan, none, none) },
|
|
"SplitStack size Inf": func(c *ui.Context) { SplitStack(c, &inf, none, none) },
|
|
"SplitPane size -1": func(c *ui.Context) { SplitPane(c, new(float32(-1)), none, none) },
|
|
"SplitStack size -1": func(c *ui.Context) { SplitStack(c, new(float32(-1)), none, none) },
|
|
}
|
|
for what, build := range cases {
|
|
panics(t, what, func() { ui.Render(func(c *ui.Context) { build(c) }, 10, 10, 1) })
|
|
}
|
|
}
|