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
@@ -0,0 +1,253 @@
|
||||
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) })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user