feat(layout): record module spans and look up the module at a column
- commit
- 60105079c3b767672267307f6b2c10612bf7b6c5
- author
- brice · · 2026-10-09 03:34 UTC
- parent
- 950b5b5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +192 -3
M internal/layout/layout.go +42 -3
| @@ -18,6 +18,16 @@ type Source struct { | |||
| 18 | 18 | input []byte // reset + erase + frame, reused for each Write | |
| 19 | 19 | cells []gpu.Cell | |
| 20 | 20 | failed bool | |
| 21 | + | ||
| 22 | + | // start and shown are where Compose put the module's cells in the row, and | |
| 23 | + | // how many of them survived clipping. | |
| 24 | + | start, shown int | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // Span is the part of the row a module occupies after Compose. | |
| 28 | + | type Span struct { | |
| 29 | + | M *module.Module | |
| 30 | + | Start, Width int | |
| 21 | 31 | } | |
| 22 | 32 | ||
| 23 | 33 | // Layout owns the sources and the composed row. | |
| @@ -208,6 +218,7 @@ func (l *Layout) Compose() []gpu.Cell { | |||
| 208 | 218 | } | |
| 209 | 219 | var left, center, right int | |
| 210 | 220 | for _, s := range l.sources { | |
| 221 | + | s.start, s.shown = 0, 0 | |
| 211 | 222 | n := len(s.cells) | |
| 212 | 223 | switch s.M.Zone { | |
| 213 | 224 | case module.Left: | |
| @@ -224,14 +235,18 @@ func (l *Layout) Compose() []gpu.Cell { | |||
| 224 | 235 | x := l.cols - right | |
| 225 | 236 | for _, s := range l.sources { | |
| 226 | 237 | if s.M.Zone == module.Right { | |
| 227 | - | x += copy(l.row[x:], s.cells[:min(len(s.cells), l.cols-x)]) | |
| 238 | + | s.start = x | |
| 239 | + | s.shown = copy(l.row[x:], s.cells[:min(len(s.cells), l.cols-x)]) | |
| 240 | + | x += s.shown | |
| 228 | 241 | } | |
| 229 | 242 | } | |
| 230 | 243 | // Left zone from the left edge, clipped to its budget. | |
| 231 | 244 | x, limit := 0, left | |
| 232 | 245 | for _, s := range l.sources { | |
| 233 | 246 | if s.M.Zone == module.Left && x < limit { | |
| 234 | - | x += copy(l.row[x:limit], s.cells) | |
| 247 | + | s.start = x | |
| 248 | + | s.shown = copy(l.row[x:limit], s.cells) | |
| 249 | + | x += s.shown | |
| 235 | 250 | } | |
| 236 | 251 | } | |
| 237 | 252 | // Center zone: on the middle of the bar, and only as far from it as the | |
| @@ -245,9 +260,33 @@ func (l *Layout) Compose() []gpu.Cell { | |||
| 245 | 260 | x, limit = start, start+center | |
| 246 | 261 | for _, s := range l.sources { | |
| 247 | 262 | if s.M.Zone == module.Center && x < limit { | |
| 248 | - | x += copy(l.row[x:limit], s.cells) | |
| 263 | + | s.start = x | |
| 264 | + | s.shown = copy(l.row[x:limit], s.cells) | |
| 265 | + | x += s.shown | |
| 249 | 266 | } | |
| 250 | 267 | } | |
| 251 | 268 | } | |
| 252 | 269 | return l.row | |
| 253 | 270 | } | |
| 271 | + | ||
| 272 | + | // Spans appends the span of every module that has cells in the row composed | |
| 273 | + | // last, in module order. | |
| 274 | + | func (l *Layout) Spans(dst []Span) []Span { | |
| 275 | + | for _, s := range l.sources { | |
| 276 | + | if s.shown > 0 { | |
| 277 | + | dst = append(dst, Span{M: s.M, Start: s.start, Width: s.shown}) | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | return dst | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | // At returns the module shown at column col of the row composed last, and the | |
| 284 | + | // column's offset inside the module. | |
| 285 | + | func (l *Layout) At(col int) (m *module.Module, offset int, ok bool) { | |
| 286 | + | for _, s := range l.sources { | |
| 287 | + | if col >= s.start && col < s.start+s.shown { | |
| 288 | + | return s.M, col - s.start, true | |
| 289 | + | } | |
| 290 | + | } | |
| 291 | + | return nil, 0, false | |
| 292 | + | } | |
| @@ -18,6 +18,16 @@ type Source struct { | |||||
| 18 | input []byte // reset + erase + frame, reused for each Write | 18 | input []byte // reset + erase + frame, reused for each Write | ||
| 19 | cells []gpu.Cell | 19 | cells []gpu.Cell | ||
| 20 | failed bool | 20 | failed bool | ||
| 21 | + | ||||
| 22 | + | // start and shown are where Compose put the module's cells in the row, and | |||
| 23 | + | // how many of them survived clipping. | |||
| 24 | + | start, shown int | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // Span is the part of the row a module occupies after Compose. | |||
| 28 | + | type Span struct { | |||
| 29 | + | M *module.Module | |||
| 30 | + | Start, Width int | |||
| 21 | } | 31 | } | ||
| 22 | 32 | ||||
| 23 | // Layout owns the sources and the composed row. | 33 | // Layout owns the sources and the composed row. | ||
| @@ -208,6 +218,7 @@ func (l *Layout) Compose() []gpu.Cell { | |||||
| 208 | } | 218 | } | ||
| 209 | var left, center, right int | 219 | var left, center, right int | ||
| 210 | for _, s := range l.sources { | 220 | for _, s := range l.sources { | ||
| 221 | + | s.start, s.shown = 0, 0 | |||
| 211 | n := len(s.cells) | 222 | n := len(s.cells) | ||
| 212 | switch s.M.Zone { | 223 | switch s.M.Zone { | ||
| 213 | case module.Left: | 224 | case module.Left: | ||
| @@ -224,14 +235,18 @@ func (l *Layout) Compose() []gpu.Cell { | |||||
| 224 | x := l.cols - right | 235 | x := l.cols - right | ||
| 225 | for _, s := range l.sources { | 236 | for _, s := range l.sources { | ||
| 226 | if s.M.Zone == module.Right { | 237 | if s.M.Zone == module.Right { | ||
| 227 | - | x += copy(l.row[x:], s.cells[:min(len(s.cells), l.cols-x)]) | 238 | + | s.start = x |
| 239 | + | s.shown = copy(l.row[x:], s.cells[:min(len(s.cells), l.cols-x)]) | |||
| 240 | + | x += s.shown | |||
| 228 | } | 241 | } | ||
| 229 | } | 242 | } | ||
| 230 | // Left zone from the left edge, clipped to its budget. | 243 | // Left zone from the left edge, clipped to its budget. | ||
| 231 | x, limit := 0, left | 244 | x, limit := 0, left | ||
| 232 | for _, s := range l.sources { | 245 | for _, s := range l.sources { | ||
| 233 | if s.M.Zone == module.Left && x < limit { | 246 | if s.M.Zone == module.Left && x < limit { | ||
| 234 | - | x += copy(l.row[x:limit], s.cells) | 247 | + | s.start = x |
| 248 | + | s.shown = copy(l.row[x:limit], s.cells) | |||
| 249 | + | x += s.shown | |||
| 235 | } | 250 | } | ||
| 236 | } | 251 | } | ||
| 237 | // Center zone: on the middle of the bar, and only as far from it as the | 252 | // Center zone: on the middle of the bar, and only as far from it as the | ||
| @@ -245,9 +260,33 @@ func (l *Layout) Compose() []gpu.Cell { | |||||
| 245 | x, limit = start, start+center | 260 | x, limit = start, start+center | ||
| 246 | for _, s := range l.sources { | 261 | for _, s := range l.sources { | ||
| 247 | if s.M.Zone == module.Center && x < limit { | 262 | if s.M.Zone == module.Center && x < limit { | ||
| 248 | - | x += copy(l.row[x:limit], s.cells) | 263 | + | s.start = x |
| 264 | + | s.shown = copy(l.row[x:limit], s.cells) | |||
| 265 | + | x += s.shown | |||
| 249 | } | 266 | } | ||
| 250 | } | 267 | } | ||
| 251 | } | 268 | } | ||
| 252 | return l.row | 269 | return l.row | ||
| 253 | } | 270 | } | ||
| 271 | + | ||||
| 272 | + | // Spans appends the span of every module that has cells in the row composed | |||
| 273 | + | // last, in module order. | |||
| 274 | + | func (l *Layout) Spans(dst []Span) []Span { | |||
| 275 | + | for _, s := range l.sources { | |||
| 276 | + | if s.shown > 0 { | |||
| 277 | + | dst = append(dst, Span{M: s.M, Start: s.start, Width: s.shown}) | |||
| 278 | + | } | |||
| 279 | + | } | |||
| 280 | + | return dst | |||
| 281 | + | } | |||
| 282 | + | ||||
| 283 | + | // At returns the module shown at column col of the row composed last, and the | |||
| 284 | + | // column's offset inside the module. | |||
| 285 | + | func (l *Layout) At(col int) (m *module.Module, offset int, ok bool) { | |||
| 286 | + | for _, s := range l.sources { | |||
| 287 | + | if col >= s.start && col < s.start+s.shown { | |||
| 288 | + | return s.M, col - s.start, true | |||
| 289 | + | } | |||
| 290 | + | } | |||
| 291 | + | return nil, 0, false | |||
| 292 | + | } | |||
M internal/layout/layout_test.go +150 -0
| @@ -221,3 +221,153 @@ func TestCenterMovesOnlyAsFarAsNeeded(t *testing.T) { | |||
| 221 | 221 | t.Fatalf("center starts at cell %d, want 16 (ending where the right zone starts): %q", i, got) | |
| 222 | 222 | } | |
| 223 | 223 | } | |
| 224 | + | ||
| 225 | + | func spanOf(spans []Span, m *module.Module) (Span, bool) { | |
| 226 | + | for _, s := range spans { | |
| 227 | + | if s.M == m { | |
| 228 | + | return s, true | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | return Span{}, false | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | func TestSpansPerZone(t *testing.T) { | |
| 235 | + | l, m := newTestLayout(20) | |
| 236 | + | publish(m[0], "LEFT") | |
| 237 | + | publish(m[1], "mid") | |
| 238 | + | publish(m[2], "RIGHT") | |
| 239 | + | l.Update() | |
| 240 | + | row := rowText(l) | |
| 241 | + | spans := l.Spans(nil) | |
| 242 | + | if len(spans) != 3 { | |
| 243 | + | t.Fatalf("spans = %+v, want 3", spans) | |
| 244 | + | } | |
| 245 | + | for i, want := range []string{"LEFT", "mid", "RIGHT"} { | |
| 246 | + | s, ok := spanOf(spans, m[i]) | |
| 247 | + | if !ok || s.Width != len(want) || row[s.Start:s.Start+s.Width] != want { | |
| 248 | + | t.Errorf("span %d = %+v (ok %v) in %q, want %q", i, s, ok, row, want) | |
| 249 | + | } | |
| 250 | + | } | |
| 251 | + | // Spans come in module order. | |
| 252 | + | if spans[0].M != m[0] || spans[1].M != m[1] || spans[2].M != m[2] { | |
| 253 | + | t.Errorf("spans out of order: %+v", spans) | |
| 254 | + | } | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | func TestSpansTwoModulesInOneZone(t *testing.T) { | |
| 258 | + | wake := make(chan struct{}, 1) | |
| 259 | + | log := slog.New(slog.DiscardHandler) | |
| 260 | + | a := module.New("a", module.Left, "true", wake, log) | |
| 261 | + | b := module.New("b", module.Left, "true", wake, log) | |
| 262 | + | l := New(10, [3]uint8{1, 1, 1}, [3]uint8{2, 2, 2}, testPalette, []*module.Module{a, b}) | |
| 263 | + | publish(a, "AA") | |
| 264 | + | publish(b, "BBB") | |
| 265 | + | l.Update() | |
| 266 | + | l.Compose() | |
| 267 | + | sa, _ := spanOf(l.Spans(nil), a) | |
| 268 | + | sb, _ := spanOf(l.Spans(nil), b) | |
| 269 | + | if sa != (Span{a, 0, 2}) || sb != (Span{b, 2, 3}) { | |
| 270 | + | t.Fatalf("spans = %+v %+v", sa, sb) | |
| 271 | + | } | |
| 272 | + | for col, want := range map[int]struct { | |
| 273 | + | m *module.Module | |
| 274 | + | off int | |
| 275 | + | ok bool | |
| 276 | + | }{0: {a, 0, true}, 1: {a, 1, true}, 2: {b, 0, true}, 4: {b, 2, true}, 5: {nil, 0, false}, -1: {nil, 0, false}, 99: {nil, 0, false}} { | |
| 277 | + | gm, off, ok := l.At(col) | |
| 278 | + | if gm != want.m || off != want.off || ok != want.ok { | |
| 279 | + | t.Errorf("At(%d) = %v, %d, %v; want %v, %d, %v", col, gm, off, ok, want.m, want.off, want.ok) | |
| 280 | + | } | |
| 281 | + | } | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | func TestSpansAreClipped(t *testing.T) { | |
| 285 | + | // The right zone wins over the left one: left is cut to the room left. | |
| 286 | + | l, m := newTestLayout(8) | |
| 287 | + | publish(m[0], "AAAAA") | |
| 288 | + | publish(m[2], "ZZZZZ") | |
| 289 | + | l.Update() | |
| 290 | + | l.Compose() | |
| 291 | + | spans := l.Spans(nil) | |
| 292 | + | if s, ok := spanOf(spans, m[0]); !ok || s != (Span{m[0], 0, 3}) { | |
| 293 | + | t.Errorf("left span = %+v, want {0,3}", s) | |
| 294 | + | } | |
| 295 | + | if s, ok := spanOf(spans, m[2]); !ok || s != (Span{m[2], 3, 5}) { | |
| 296 | + | t.Errorf("right span = %+v, want {3,5}", s) | |
| 297 | + | } | |
| 298 | + | if _, _, ok := l.At(3); !ok { | |
| 299 | + | t.Error("column 3 belongs to the right module") | |
| 300 | + | } | |
| 301 | + | if gm, off, _ := l.At(3); gm != m[2] || off != 0 { | |
| 302 | + | t.Errorf("At(3) = %v, %d", gm, off) | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | // The center is cut first, and may vanish entirely. | |
| 306 | + | l, m = newTestLayout(12) | |
| 307 | + | publish(m[0], "AAAAA") | |
| 308 | + | publish(m[1], "CCCCCCCC") | |
| 309 | + | publish(m[2], "ZZZZZ") | |
| 310 | + | l.Update() | |
| 311 | + | l.Compose() | |
| 312 | + | if s, ok := spanOf(l.Spans(nil), m[1]); !ok || s != (Span{m[1], 5, 2}) { | |
| 313 | + | t.Errorf("center span = %+v (ok %v), want {5,2}", s, ok) | |
| 314 | + | } | |
| 315 | + | l, m = newTestLayout(10) | |
| 316 | + | publish(m[0], "AAAAA") | |
| 317 | + | publish(m[1], "CC") | |
| 318 | + | publish(m[2], "ZZZZZ") | |
| 319 | + | l.Update() | |
| 320 | + | l.Compose() | |
| 321 | + | if _, ok := spanOf(l.Spans(nil), m[1]); ok { | |
| 322 | + | t.Error("a fully clipped module must have no span") | |
| 323 | + | } | |
| 324 | + | if _, _, ok := l.At(5); !ok { | |
| 325 | + | // column 5 is the first of the right zone | |
| 326 | + | t.Error("At(5) must find the right module") | |
| 327 | + | } | |
| 328 | + | } | |
| 329 | + | ||
| 330 | + | func TestSpansEmptyModuleAndRecompose(t *testing.T) { | |
| 331 | + | l, m := newTestLayout(10) | |
| 332 | + | publish(m[0], "") | |
| 333 | + | publish(m[2], "R") | |
| 334 | + | l.Update() | |
| 335 | + | l.Compose() | |
| 336 | + | if _, ok := spanOf(l.Spans(nil), m[0]); ok { | |
| 337 | + | t.Error("an empty module has no span") | |
| 338 | + | } | |
| 339 | + | if len(l.Spans(nil)) != 1 { | |
| 340 | + | t.Errorf("spans = %+v", l.Spans(nil)) | |
| 341 | + | } | |
| 342 | + | // A module that empties out loses its span at the next Compose. | |
| 343 | + | publish(m[2], "") | |
| 344 | + | l.Update() | |
| 345 | + | l.Compose() | |
| 346 | + | if got := l.Spans(nil); len(got) != 0 { | |
| 347 | + | t.Errorf("spans after emptying = %+v", got) | |
| 348 | + | } | |
| 349 | + | if _, _, ok := l.At(9); ok { | |
| 350 | + | t.Error("At must not find an emptied module") | |
| 351 | + | } | |
| 352 | + | } | |
| 353 | + | ||
| 354 | + | func TestComposeAndSpansAllocs(t *testing.T) { | |
| 355 | + | if racecheck.Enabled { | |
| 356 | + | t.Skip("the race detector allocates") | |
| 357 | + | } | |
| 358 | + | l, m := newTestLayout(40) | |
| 359 | + | publish(m[0], "left") | |
| 360 | + | publish(m[1], "center") | |
| 361 | + | publish(m[2], "right") | |
| 362 | + | l.Update() | |
| 363 | + | if got := testing.AllocsPerRun(100, func() { l.Compose() }); got > 0 { | |
| 364 | + | t.Errorf("Compose allocates %.1f objects; want 0", got) | |
| 365 | + | } | |
| 366 | + | buf := make([]Span, 0, 8) | |
| 367 | + | if got := testing.AllocsPerRun(100, func() { buf = l.Spans(buf[:0]) }); got > 0 { | |
| 368 | + | t.Errorf("Spans allocates %.1f objects; want 0", got) | |
| 369 | + | } | |
| 370 | + | if got := testing.AllocsPerRun(100, func() { l.At(3) }); got > 0 { | |
| 371 | + | t.Errorf("At allocates %.1f objects; want 0", got) | |
| 372 | + | } | |
| 373 | + | } | |
| @@ -221,3 +221,153 @@ func TestCenterMovesOnlyAsFarAsNeeded(t *testing.T) { | |||||
| 221 | t.Fatalf("center starts at cell %d, want 16 (ending where the right zone starts): %q", i, got) | 221 | t.Fatalf("center starts at cell %d, want 16 (ending where the right zone starts): %q", i, got) | ||
| 222 | } | 222 | } | ||
| 223 | } | 223 | } | ||
| 224 | + | ||||
| 225 | + | func spanOf(spans []Span, m *module.Module) (Span, bool) { | |||
| 226 | + | for _, s := range spans { | |||
| 227 | + | if s.M == m { | |||
| 228 | + | return s, true | |||
| 229 | + | } | |||
| 230 | + | } | |||
| 231 | + | return Span{}, false | |||
| 232 | + | } | |||
| 233 | + | ||||
| 234 | + | func TestSpansPerZone(t *testing.T) { | |||
| 235 | + | l, m := newTestLayout(20) | |||
| 236 | + | publish(m[0], "LEFT") | |||
| 237 | + | publish(m[1], "mid") | |||
| 238 | + | publish(m[2], "RIGHT") | |||
| 239 | + | l.Update() | |||
| 240 | + | row := rowText(l) | |||
| 241 | + | spans := l.Spans(nil) | |||
| 242 | + | if len(spans) != 3 { | |||
| 243 | + | t.Fatalf("spans = %+v, want 3", spans) | |||
| 244 | + | } | |||
| 245 | + | for i, want := range []string{"LEFT", "mid", "RIGHT"} { | |||
| 246 | + | s, ok := spanOf(spans, m[i]) | |||
| 247 | + | if !ok || s.Width != len(want) || row[s.Start:s.Start+s.Width] != want { | |||
| 248 | + | t.Errorf("span %d = %+v (ok %v) in %q, want %q", i, s, ok, row, want) | |||
| 249 | + | } | |||
| 250 | + | } | |||
| 251 | + | // Spans come in module order. | |||
| 252 | + | if spans[0].M != m[0] || spans[1].M != m[1] || spans[2].M != m[2] { | |||
| 253 | + | t.Errorf("spans out of order: %+v", spans) | |||
| 254 | + | } | |||
| 255 | + | } | |||
| 256 | + | ||||
| 257 | + | func TestSpansTwoModulesInOneZone(t *testing.T) { | |||
| 258 | + | wake := make(chan struct{}, 1) | |||
| 259 | + | log := slog.New(slog.DiscardHandler) | |||
| 260 | + | a := module.New("a", module.Left, "true", wake, log) | |||
| 261 | + | b := module.New("b", module.Left, "true", wake, log) | |||
| 262 | + | l := New(10, [3]uint8{1, 1, 1}, [3]uint8{2, 2, 2}, testPalette, []*module.Module{a, b}) | |||
| 263 | + | publish(a, "AA") | |||
| 264 | + | publish(b, "BBB") | |||
| 265 | + | l.Update() | |||
| 266 | + | l.Compose() | |||
| 267 | + | sa, _ := spanOf(l.Spans(nil), a) | |||
| 268 | + | sb, _ := spanOf(l.Spans(nil), b) | |||
| 269 | + | if sa != (Span{a, 0, 2}) || sb != (Span{b, 2, 3}) { | |||
| 270 | + | t.Fatalf("spans = %+v %+v", sa, sb) | |||
| 271 | + | } | |||
| 272 | + | for col, want := range map[int]struct { | |||
| 273 | + | m *module.Module | |||
| 274 | + | off int | |||
| 275 | + | ok bool | |||
| 276 | + | }{0: {a, 0, true}, 1: {a, 1, true}, 2: {b, 0, true}, 4: {b, 2, true}, 5: {nil, 0, false}, -1: {nil, 0, false}, 99: {nil, 0, false}} { | |||
| 277 | + | gm, off, ok := l.At(col) | |||
| 278 | + | if gm != want.m || off != want.off || ok != want.ok { | |||
| 279 | + | t.Errorf("At(%d) = %v, %d, %v; want %v, %d, %v", col, gm, off, ok, want.m, want.off, want.ok) | |||
| 280 | + | } | |||
| 281 | + | } | |||
| 282 | + | } | |||
| 283 | + | ||||
| 284 | + | func TestSpansAreClipped(t *testing.T) { | |||
| 285 | + | // The right zone wins over the left one: left is cut to the room left. | |||
| 286 | + | l, m := newTestLayout(8) | |||
| 287 | + | publish(m[0], "AAAAA") | |||
| 288 | + | publish(m[2], "ZZZZZ") | |||
| 289 | + | l.Update() | |||
| 290 | + | l.Compose() | |||
| 291 | + | spans := l.Spans(nil) | |||
| 292 | + | if s, ok := spanOf(spans, m[0]); !ok || s != (Span{m[0], 0, 3}) { | |||
| 293 | + | t.Errorf("left span = %+v, want {0,3}", s) | |||
| 294 | + | } | |||
| 295 | + | if s, ok := spanOf(spans, m[2]); !ok || s != (Span{m[2], 3, 5}) { | |||
| 296 | + | t.Errorf("right span = %+v, want {3,5}", s) | |||
| 297 | + | } | |||
| 298 | + | if _, _, ok := l.At(3); !ok { | |||
| 299 | + | t.Error("column 3 belongs to the right module") | |||
| 300 | + | } | |||
| 301 | + | if gm, off, _ := l.At(3); gm != m[2] || off != 0 { | |||
| 302 | + | t.Errorf("At(3) = %v, %d", gm, off) | |||
| 303 | + | } | |||
| 304 | + | ||||
| 305 | + | // The center is cut first, and may vanish entirely. | |||
| 306 | + | l, m = newTestLayout(12) | |||
| 307 | + | publish(m[0], "AAAAA") | |||
| 308 | + | publish(m[1], "CCCCCCCC") | |||
| 309 | + | publish(m[2], "ZZZZZ") | |||
| 310 | + | l.Update() | |||
| 311 | + | l.Compose() | |||
| 312 | + | if s, ok := spanOf(l.Spans(nil), m[1]); !ok || s != (Span{m[1], 5, 2}) { | |||
| 313 | + | t.Errorf("center span = %+v (ok %v), want {5,2}", s, ok) | |||
| 314 | + | } | |||
| 315 | + | l, m = newTestLayout(10) | |||
| 316 | + | publish(m[0], "AAAAA") | |||
| 317 | + | publish(m[1], "CC") | |||
| 318 | + | publish(m[2], "ZZZZZ") | |||
| 319 | + | l.Update() | |||
| 320 | + | l.Compose() | |||
| 321 | + | if _, ok := spanOf(l.Spans(nil), m[1]); ok { | |||
| 322 | + | t.Error("a fully clipped module must have no span") | |||
| 323 | + | } | |||
| 324 | + | if _, _, ok := l.At(5); !ok { | |||
| 325 | + | // column 5 is the first of the right zone | |||
| 326 | + | t.Error("At(5) must find the right module") | |||
| 327 | + | } | |||
| 328 | + | } | |||
| 329 | + | ||||
| 330 | + | func TestSpansEmptyModuleAndRecompose(t *testing.T) { | |||
| 331 | + | l, m := newTestLayout(10) | |||
| 332 | + | publish(m[0], "") | |||
| 333 | + | publish(m[2], "R") | |||
| 334 | + | l.Update() | |||
| 335 | + | l.Compose() | |||
| 336 | + | if _, ok := spanOf(l.Spans(nil), m[0]); ok { | |||
| 337 | + | t.Error("an empty module has no span") | |||
| 338 | + | } | |||
| 339 | + | if len(l.Spans(nil)) != 1 { | |||
| 340 | + | t.Errorf("spans = %+v", l.Spans(nil)) | |||
| 341 | + | } | |||
| 342 | + | // A module that empties out loses its span at the next Compose. | |||
| 343 | + | publish(m[2], "") | |||
| 344 | + | l.Update() | |||
| 345 | + | l.Compose() | |||
| 346 | + | if got := l.Spans(nil); len(got) != 0 { | |||
| 347 | + | t.Errorf("spans after emptying = %+v", got) | |||
| 348 | + | } | |||
| 349 | + | if _, _, ok := l.At(9); ok { | |||
| 350 | + | t.Error("At must not find an emptied module") | |||
| 351 | + | } | |||
| 352 | + | } | |||
| 353 | + | ||||
| 354 | + | func TestComposeAndSpansAllocs(t *testing.T) { | |||
| 355 | + | if racecheck.Enabled { | |||
| 356 | + | t.Skip("the race detector allocates") | |||
| 357 | + | } | |||
| 358 | + | l, m := newTestLayout(40) | |||
| 359 | + | publish(m[0], "left") | |||
| 360 | + | publish(m[1], "center") | |||
| 361 | + | publish(m[2], "right") | |||
| 362 | + | l.Update() | |||
| 363 | + | if got := testing.AllocsPerRun(100, func() { l.Compose() }); got > 0 { | |||
| 364 | + | t.Errorf("Compose allocates %.1f objects; want 0", got) | |||
| 365 | + | } | |||
| 366 | + | buf := make([]Span, 0, 8) | |||
| 367 | + | if got := testing.AllocsPerRun(100, func() { buf = l.Spans(buf[:0]) }); got > 0 { | |||
| 368 | + | t.Errorf("Spans allocates %.1f objects; want 0", got) | |||
| 369 | + | } | |||
| 370 | + | if got := testing.AllocsPerRun(100, func() { l.At(3) }); got > 0 { | |||
| 371 | + | t.Errorf("At allocates %.1f objects; want 0", got) | |||
| 372 | + | } | |||
| 373 | + | } | |||