From beb4c02c61b6631b50f3b8bc5164b0f0874f54f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 14:28:55 -0400 Subject: [PATCH] WIP essentials 11 --- masonry.go | 139 ++++++++++++++++++++ masonry_test.go | 78 +++++++++++ testdata/snapshots/darwin/masonry-dark.png | Bin 0 -> 14270 bytes testdata/snapshots/darwin/masonry-light.png | Bin 0 -> 13801 bytes 4 files changed, 217 insertions(+) create mode 100644 masonry.go create mode 100644 masonry_test.go create mode 100644 testdata/snapshots/darwin/masonry-dark.png create mode 100644 testdata/snapshots/darwin/masonry-light.png diff --git a/masonry.go b/masonry.go new file mode 100644 index 0000000..3c1d7ad --- /dev/null +++ b/masonry.go @@ -0,0 +1,139 @@ +package mujica + +import ( + "cmp" + "log/slog" + "math" + "sort" + + "github.com/egoist/mygo/ui" +) + +// MasonryOptions configure a Masonry. +type MasonryOptions struct { + // Key is required: a stable, unique key per item. + Key func(i int) string + // ColumnWidth is the narrowest column; zero is 220. Columns fill the width. + ColumnWidth float32 + // Gap is between items; zero is 3 spacing units. + Gap float32 + // Estimate is the height assumed until an item is measured; zero is 160. + Estimate float32 +} + +// masonryState is the scroll, the measured heights and the cached placement. +type masonryState struct { + scroll ui.ScrollState + heights map[string]float32 + version int + laidOut masonryLayout + // built are the items built last frame; their keys are checked for changed data. + built []int + // sized: a frame was asked for once the box had a width. + sized bool +} + +// masonryLayout places n items in cols columns; byCol lists each column's items top to bottom. +type masonryLayout struct { + n, cols, version int + colW, gap float32 + keys []string + x, y, h []float32 + byCol [][]int + total float32 +} + +// Masonry lays n items in as many columns as fit, each into the shortest column, building only those in view. +func Masonry(c *ui.Context, n int, opts MasonryOptions, item func(i int)) *ui.Element { + if opts.Key == nil || item == nil { + panic("mujica: Masonry needs Key and item") + } + if n < 0 || opts.ColumnWidth < 0 || opts.Gap < 0 || opts.Estimate < 0 { + panic("mujica: Masonry count and sizes must not be negative") + } + u := Density(c).Unit() + colMin := cmp.Or(opts.ColumnWidth, 220) + gap := cmp.Or(opts.Gap, 3*u) + est := cmp.Or(opts.Estimate, 160) + sc := ui.Scroll(c) + st := ui.Local(sc, "masonry", func() *masonryState { return &masonryState{heights: map[string]float32{}} }) + s := *st + sc.TrackScroll(&s.scroll) + view := sc.Bounds() + if view.W == 0 && !s.sized { + // Unmeasured: lay out again once the box has its width. + s.sized = true + c.Invalidate() + } + cols := max(1, int((view.W+gap)/(colMin+gap))) + colW := (view.W - gap*float32(cols-1)) / float32(cols) + l := &s.laidOut + stale := l.n != n || l.cols != cols || l.colW != colW || l.gap != gap || l.version != s.version + for _, i := range s.built { + stale = stale || i >= n || l.keys[i] != opts.Key(i) + } + if stale { + *l = masonryPlace(n, cols, colW, gap, est, s.heights, opts.Key) + l.version = s.version + slog.Debug("mujica: masonry layout", "items", n, "columns", cols) + } + top, bottom := s.scroll.Y-view.H/2, s.scroll.Y+view.H*1.5 + var seen []int + sc.Children(func() { + ui.Box(c).FillWidth().Height(l.total).Children(func() { masonryBuild(c, s, l, colW, top, bottom, item, &seen) }) + }) + s.built = seen + return sc +} + +// masonryBuild builds the items between top and bottom and records measured heights. +func masonryBuild(c *ui.Context, s *masonryState, l *masonryLayout, colW, top, bottom float32, item func(int), seen *[]int) { + for _, list := range l.byCol { + from := sort.Search(len(list), func(j int) bool { i := list[j]; return l.y[i]+l.h[i] >= top }) + for _, i := range list[from:] { + if l.y[i] > bottom { + break + } + *seen = append(*seen, i) + key := l.keys[i] + box := ui.Column(c).Key(key).Absolute().Left(l.x[i]).Top(l.y[i]).Width(colW) + box.Children(func() { item(i) }) + if h := box.Bounds().H; h > 0 && math.Abs(float64(h-l.h[i])) > 0.5 { + s.heights[key] = h + s.version++ + c.Invalidate() + } + } + } +} + +// masonryPlace puts each item into the shortest column, using measured heights or est. +func masonryPlace(n, cols int, colW, gap, est float32, heights map[string]float32, key func(int) string) masonryLayout { + l := masonryLayout{n: n, cols: cols, colW: colW, gap: gap, keys: make([]string, n), x: make([]float32, n), y: make([]float32, n), h: make([]float32, n), byCol: make([][]int, cols)} + colH := make([]float32, cols) + seen := make(map[string]bool, n) + for i := range n { + k := key(i) + if seen[k] { + panic("mujica: Masonry key repeated: " + k) + } + seen[k] = true + col := 0 + for j := range cols { + if colH[j] < colH[col] { + col = j + } + } + h, ok := heights[k] + if !ok { + h = est + } + l.keys[i], l.x[i], l.y[i], l.h[i] = k, float32(col)*(colW+gap), colH[col], h + l.byCol[col] = append(l.byCol[col], i) + colH[col] += h + gap + } + for _, h := range colH { + l.total = max(l.total, h-gap) + } + return l +} diff --git a/masonry_test.go b/masonry_test.go new file mode 100644 index 0000000..71349e8 --- /dev/null +++ b/masonry_test.go @@ -0,0 +1,78 @@ +package mujica + +import ( + "strconv" + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestMasonryPlaceShortestColumn(t *testing.T) { + hs := map[string]float32{"0": 100, "1": 40, "2": 40, "3": 50} + l := masonryPlace(4, 2, 100, 10, 160, hs, strconv.Itoa) + // 0 left; 1 right; 2 right again (50 < 110); 3 right (100 = 110? no: left 110, right 100). + want := [][2]float32{{0, 0}, {110, 0}, {110, 50}, {110, 100}} + for i, w := range want { + if l.x[i] != w[0] || l.y[i] != w[1] { + t.Errorf("item %d at %v,%v, want %v", i, l.x[i], l.y[i], w) + } + } + if l.total != 150 { + t.Errorf("total %v", l.total) + } + defer func() { + if recover() == nil { + t.Error("repeated key accepted") + } + }() + masonryPlace(2, 1, 100, 0, 10, nil, func(int) string { return "same" }) +} + +func TestMasonryVirtualizesAndMeasures(t *testing.T) { + built := map[int]bool{} + n := 100000 + width := 700 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Masonry(c, n, MasonryOptions{Key: strconv.Itoa, ColumnWidth: 200, Gap: 10}, func(i int) { + built[i] = true + ui.Box(c).Height(float32(40 + i%3*30)).Label("tile " + strconv.Itoa(i)) + }).Size(float32(width), 500) + }, 800, 600) + tt.Frame() + clear(built) + tt.Frame() + if len(built) == 0 || len(built) > 120 { + t.Fatalf("built %d of %d", len(built), n) + } + if !built[0] || !built[2] || built[5000] { + t.Fatalf("wrong items built: %v", built[5000]) + } + a, _ := tt.Find("tile 0") + b, _ := tt.Find("tile 1") + d, _ := tt.Find("tile 3") + if a.W < 226 || a.W > 227 || b.X < 236 || b.X > 237 { + t.Fatalf("three columns in 700: tile 0 %v, tile 1 %v", a, b) + } + // Measured: tile 0 is 40 high, so tile 3 goes under it at 40 + 10. + if d.X != 0 || d.Y != 50 { + t.Fatalf("tile 3 at %v; texts %q", d, tt.Texts()) + } + tt.Scroll(100, 100, 0, 600000) + clear(built) + tt.Frame() + if len(built) == 0 || len(built) > 120 || built[0] { + t.Fatalf("after scrolling built %d, includes first %v", len(built), built[0]) + } +} + +func TestMasonrySnapshot(t *testing.T) { + snapshot(t, "masonry", 520, 300, func(c *ui.Context) { + k := Tokens(c) + Masonry(c, 9, MasonryOptions{Key: strconv.Itoa, ColumnWidth: 150}, func(i int) { + ui.Column(c).Height(float32(50+i%4*25)).Padding(8).Background(k.Surface).Border(1, k.Border).Radius(4).Children(func() { + ui.Text(c, "Card "+strconv.Itoa(i+1)) + }) + }).Size(500, 280).Margin(10) + }) +} diff --git a/testdata/snapshots/darwin/masonry-dark.png b/testdata/snapshots/darwin/masonry-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..4c4ee52d495a9bf0d3cc9b1f0b94609ad8f6a6a5 GIT binary patch literal 14270 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 OU-Hr_Qq>ZsA^!&@w->Mg literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/masonry-light.png b/testdata/snapshots/darwin/masonry-light.png new file mode 100644 index 0000000000000000000000000000000000000000..44e6eb2dd0726094ae3894fa8944c5b9e4e46f56 GIT binary patch literal 13801 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