perf(core): build overview rows in monitor scratch instead of per-frame maps
overviewWorkspaces and overviewRows ran twice per overview frame and rebuilt a groups map, a clone of the hidden rows, a rows map and the dividers each time. They now fill Monitor-owned scratch (a fixed 3-slot row table) and overviewOutline appends into the scene's own separators. Overview publish budget: 18 down to 7.
- commit
- fea658c393d227640857e3eee047846a9e403a4a
- author
- brice · · 2026-10-16 14:04 UTC
- parent
- 040eba7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +150 -58
M internal/core/monitor.go +8 -0
| @@ -3,6 +3,8 @@ package core | |||
| 3 | 3 | import ( | |
| 4 | 4 | "iter" | |
| 5 | 5 | "slices" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 6 | 8 | ) | |
| 7 | 9 | ||
| 8 | 10 | // Monitor owns an ordered list of workspaces for one output (ADR 011). Numbers | |
| @@ -46,6 +48,12 @@ type Monitor struct { | |||
| 46 | 48 | // one its swipe began on); switchView.off is then from the current | |
| 47 | 49 | // workspace's place in it. | |
| 48 | 50 | switchList []*Workspace | |
| 51 | + | // ovNamed, ovList and ovDividers are overviewWorkspaces' and | |
| 52 | + | // overviewRows' scratch (the ordered named rows, the rows and the | |
| 53 | + | // dividers), reused and never handed out past the call. | |
| 54 | + | ovNamed []*Workspace | |
| 55 | + | ovList []*Workspace | |
| 56 | + | ovDividers [2]ports.Separator | |
| 49 | 57 | // ov holds the overview selection and its Escape snapshot. | |
| 50 | 58 | ov overviewState | |
| 51 | 59 | // sw is the column switcher (switcher.go). | |
| @@ -3,6 +3,8 @@ package core | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "iter" | 4 | "iter" | ||
| 5 | "slices" | 5 | "slices" | ||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 6 | ) | 8 | ) | ||
| 7 | 9 | ||||
| 8 | // Monitor owns an ordered list of workspaces for one output (ADR 011). Numbers | 10 | // Monitor owns an ordered list of workspaces for one output (ADR 011). Numbers | ||
| @@ -46,6 +48,12 @@ type Monitor struct { | |||||
| 46 | // one its swipe began on); switchView.off is then from the current | 48 | // one its swipe began on); switchView.off is then from the current | ||
| 47 | // workspace's place in it. | 49 | // workspace's place in it. | ||
| 48 | switchList []*Workspace | 50 | switchList []*Workspace | ||
| 51 | + | // ovNamed, ovList and ovDividers are overviewWorkspaces' and | |||
| 52 | + | // overviewRows' scratch (the ordered named rows, the rows and the | |||
| 53 | + | // dividers), reused and never handed out past the call. | |||
| 54 | + | ovNamed []*Workspace | |||
| 55 | + | ovList []*Workspace | |||
| 56 | + | ovDividers [2]ports.Separator | |||
| 49 | // ov holds the overview selection and its Escape snapshot. | 57 | // ov holds the overview selection and its Escape snapshot. | ||
| 50 | ov overviewState | 58 | ov overviewState | ||
| 51 | // sw is the column switcher (switcher.go). | 59 | // sw is the column switcher (switcher.go). | ||
M internal/core/overview.go +87 -33
| @@ -326,6 +326,9 @@ func restoreRow(w *Workspace, s rowSnapshot) { | |||
| 326 | 326 | // in configured order within each group. Unattached rows follow the last | |
| 327 | 327 | // occupied numbered row (or the first). Empty invocation anchors stay visible; | |
| 328 | 328 | // other empty rows are skipped unless current. | |
| 329 | + | // | |
| 330 | + | // The result is the monitor's scratch, rebuilt by the next call: callers | |
| 331 | + | // read it and drop it before calling again (Clone it to keep it). | |
| 329 | 332 | func (m *Monitor) overviewWorkspaces() []*Workspace { | |
| 330 | 333 | fallback := m.Workspaces[0] | |
| 331 | 334 | for _, w := range m.Workspaces { | |
| @@ -333,8 +336,6 @@ func (m *Monitor) overviewWorkspaces() []*Workspace { | |||
| 333 | 336 | fallback = w | |
| 334 | 337 | } | |
| 335 | 338 | } | |
| 336 | - | groups := make(map[*Workspace][]*Workspace) | |
| 337 | - | named := slices.Clone(m.hidden) | |
| 338 | 339 | rank := func(w *Workspace) int { | |
| 339 | 340 | for i, spec := range m.named { | |
| 340 | 341 | if spec.Name == w.Name { | |
| @@ -343,24 +344,37 @@ func (m *Monitor) overviewWorkspaces() []*Workspace { | |||
| 343 | 344 | } | |
| 344 | 345 | return len(m.named) | |
| 345 | 346 | } | |
| 347 | + | named := append(m.ovNamed[:0], m.hidden...) | |
| 348 | + | m.ovNamed = named | |
| 346 | 349 | slices.SortStableFunc(named, func(a, b *Workspace) int { return rank(a) - rank(b) }) | |
| 347 | - | for _, w := range named { | |
| 348 | - | if w.empty() && w != m.Current() { | |
| 349 | - | continue | |
| 350 | - | } | |
| 351 | - | anchor := w.overviewAfter | |
| 352 | - | if indexOf(m.Workspaces, anchor) < 0 { | |
| 353 | - | anchor = fallback | |
| 354 | - | } | |
| 355 | - | groups[anchor] = append(groups[anchor], w) | |
| 356 | - | } | |
| 357 | - | var rows []*Workspace | |
| 350 | + | cur := m.Current() | |
| 351 | + | rows := m.ovList[:0] | |
| 358 | 352 | for _, w := range m.Workspaces { | |
| 359 | - | if !w.empty() || w == m.Current() || len(groups[w]) > 0 { | |
| 360 | - | rows = append(rows, w) | |
| 361 | - | rows = append(rows, groups[w]...) | |
| 353 | + | // The row is kept when occupied, current or anchoring a named | |
| 354 | + | // row: placed first, dropped again if none of those holds. | |
| 355 | + | at := len(rows) | |
| 356 | + | rows = append(rows, w) | |
| 357 | + | for _, n := range named { | |
| 358 | + | if n.empty() && n != cur { | |
| 359 | + | continue | |
| 360 | + | } | |
| 361 | + | anchor := n.overviewAfter | |
| 362 | + | if indexOf(m.Workspaces, anchor) < 0 { | |
| 363 | + | anchor = fallback | |
| 364 | + | } | |
| 365 | + | if anchor == w { | |
| 366 | + | rows = append(rows, n) | |
| 367 | + | } | |
| 368 | + | } | |
| 369 | + | if len(rows) == at+1 && w.empty() && w != cur { | |
| 370 | + | rows = rows[:at] | |
| 362 | 371 | } | |
| 363 | 372 | } | |
| 373 | + | // Clear the unused tails so a workspace gone from this monitor is not | |
| 374 | + | // kept reachable by the scratch. | |
| 375 | + | clear(named[len(named):cap(named)]) | |
| 376 | + | clear(rows[len(rows):cap(rows)]) | |
| 377 | + | m.ovList = rows | |
| 364 | 378 | return rows | |
| 365 | 379 | } | |
| 366 | 380 | ||
| @@ -674,16 +688,46 @@ func (m *Monitor) OverviewPick(id WindowID) { | |||
| 674 | 688 | // workspace's layout. | |
| 675 | 689 | func (m *Monitor) overviewAxis() layoutAxis { return m.Current().policy().workspace } | |
| 676 | 690 | ||
| 691 | + | // overviewSlots are the slots of the overview rows: the current workspace | |
| 692 | + | // and at most one neighbor on each side, in a fixed array (no allocation). | |
| 693 | + | type overviewSlots struct { | |
| 694 | + | n int | |
| 695 | + | w [3]*Workspace | |
| 696 | + | r [3]Rect | |
| 697 | + | } | |
| 698 | + | ||
| 699 | + | func (s *overviewSlots) add(w *Workspace, r Rect) { | |
| 700 | + | s.w[s.n], s.r[s.n] = w, r | |
| 701 | + | s.n++ | |
| 702 | + | } | |
| 703 | + | ||
| 704 | + | // at is the slot of w, the zero Rect if it has none. | |
| 705 | + | func (s *overviewSlots) at(w *Workspace) Rect { | |
| 706 | + | r, _ := s.lookup(w) | |
| 707 | + | return r | |
| 708 | + | } | |
| 709 | + | ||
| 710 | + | // lookup is the slot of w, if it has one. | |
| 711 | + | func (s *overviewSlots) lookup(w *Workspace) (Rect, bool) { | |
| 712 | + | for i := range s.n { | |
| 713 | + | if s.w[i] == w { | |
| 714 | + | return s.r[i], true | |
| 715 | + | } | |
| 716 | + | } | |
| 717 | + | return Rect{}, false | |
| 718 | + | } | |
| 719 | + | ||
| 677 | 720 | // overviewRows places the current workspace and its overview neighbors in | |
| 678 | 721 | // slots, in output-local pixels, along the overview axis; each workspace | |
| 679 | 722 | // places its previews in its slot. The numbered/named boundary gets extra | |
| 680 | - | // spacing and an inactive rule across the axis. | |
| 723 | + | // spacing and an inactive rule across the axis. The dividers are the | |
| 724 | + | // monitor's scratch, rewritten by the next call: callers copy them. | |
| 681 | 725 | // | |
| 682 | 726 | // Vertical axis: full-width slots, the current one centred; the rule is | |
| 683 | 727 | // horizontal. Horizontal axis: full-height slots as wide as their content, | |
| 684 | 728 | // stack peeks included (the stash pile takes a band of the slot), the current one | |
| 685 | 729 | // centred; the rule is vertical, in the spacing between two slots. | |
| 686 | - | func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | |
| 730 | + | func (m *Monitor) overviewRows() (overviewSlots, []ports.Separator) { | |
| 687 | 731 | cur := m.Current() | |
| 688 | 732 | u := cur.overviewArea() | |
| 689 | 733 | gap := max(1, u.H*3/100) | |
| @@ -696,11 +740,12 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | |||
| 696 | 740 | hc := cur.rowHeight(axis) | |
| 697 | 741 | cs = Rect{X: u.X, Y: u.Y + (u.H-hc)/2, W: u.W, H: hc} | |
| 698 | 742 | } | |
| 699 | - | rows := map[*Workspace]Rect{cur: cs} | |
| 700 | - | var dividers []ports.Separator | |
| 743 | + | var rows overviewSlots | |
| 744 | + | rows.add(cur, cs) | |
| 745 | + | dividers := 0 | |
| 701 | 746 | list := m.overviewWorkspaces() | |
| 702 | 747 | i := indexOf(list, cur) | |
| 703 | - | for _, next := range []int{i - 1, i + 1} { | |
| 748 | + | for _, next := range [2]int{i - 1, i + 1} { | |
| 704 | 749 | if next < 0 || next >= len(list) { | |
| 705 | 750 | continue | |
| 706 | 751 | } | |
| @@ -711,12 +756,16 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | |||
| 711 | 756 | spacing *= 2 | |
| 712 | 757 | } | |
| 713 | 758 | slot, gapAt := neighborSlot(axis, u, cs, w, next < i, spacing) | |
| 714 | - | rows[w] = slot | |
| 759 | + | rows.add(w, slot) | |
| 715 | 760 | if boundary { | |
| 716 | - | dividers = append(dividers, overviewDivider(axis, u, gapAt+spacing/2)) | |
| 761 | + | m.ovDividers[dividers] = overviewDivider(axis, u, gapAt+spacing/2) | |
| 762 | + | dividers++ | |
| 717 | 763 | } | |
| 718 | 764 | } | |
| 719 | - | return rows, dividers | |
| 765 | + | if dividers == 0 { | |
| 766 | + | return rows, nil | |
| 767 | + | } | |
| 768 | + | return rows, m.ovDividers[:dividers] | |
| 720 | 769 | } | |
| 721 | 770 | ||
| 722 | 771 | // neighborSlot is the slot of w, the neighbor before or after the current | |
| @@ -767,7 +816,7 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||
| 767 | 816 | var unfolded *Workspace | |
| 768 | 817 | var unfoldedSlot Rect | |
| 769 | 818 | for w := range m.all() { | |
| 770 | - | slot, shown := rows[w] | |
| 819 | + | slot, shown := rows.lookup(w) | |
| 771 | 820 | if shown { | |
| 772 | 821 | // The pile band goes under the row: the row is centred | |
| 773 | 822 | // with it. | |
| @@ -1274,12 +1323,17 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes | |||
| 1274 | 1323 | return tiles | |
| 1275 | 1324 | } | |
| 1276 | 1325 | ||
| 1326 | + | // outlineLines is the lines of an uncut outline: one per edge. | |
| 1327 | + | const outlineLines = 4 | |
| 1328 | + | ||
| 1277 | 1329 | // overviewOutline frames the selected preview with active lines of width | |
| 1278 | 1330 | // b, just outside it. The selection is one preview: a selected stash card | |
| 1279 | 1331 | // wins over any other focused one, which must then not be framed. A tile's | |
| 1280 | 1332 | // frame is cut where the cards of the pile sit over it, so it never runs | |
| 1281 | - | // across the pile; a selected card keeps its complete frame. | |
| 1282 | - | func overviewOutline(layout []Placement, b int) []ports.Separator { | |
| 1333 | + | // across the pile; a selected card keeps its complete frame. The lines are | |
| 1334 | + | // appended to dst, which the caller sizes for them (room for outlineLines | |
| 1335 | + | // past its length) so the scene's separators are allocated once. | |
| 1336 | + | func overviewOutline(dst []ports.Separator, layout []Placement, b int) []ports.Separator { | |
| 1283 | 1337 | sel := -1 | |
| 1284 | 1338 | for i, p := range layout { | |
| 1285 | 1339 | if p.Preview > 0 && p.Focused && !p.Hidden && (sel < 0 || p.Card && !layout[sel].Card) { | |
| @@ -1287,8 +1341,9 @@ func overviewOutline(layout []Placement, b int) []ports.Separator { | |||
| 1287 | 1341 | } | |
| 1288 | 1342 | } | |
| 1289 | 1343 | if sel < 0 { | |
| 1290 | - | return nil | |
| 1344 | + | return dst | |
| 1291 | 1345 | } | |
| 1346 | + | dst = slices.Grow(dst, outlineLines) | |
| 1292 | 1347 | r := layout[sel].Rect | |
| 1293 | 1348 | edges := [4]Rect{ | |
| 1294 | 1349 | {X: r.X - b, Y: r.Y - b, W: r.W + 2*b, H: b}, | |
| @@ -1296,7 +1351,6 @@ func overviewOutline(layout []Placement, b int) []ports.Separator { | |||
| 1296 | 1351 | {X: r.X - b, Y: r.Y, W: b, H: r.H}, | |
| 1297 | 1352 | {X: r.X + r.W, Y: r.Y, W: b, H: r.H}, | |
| 1298 | 1353 | } | |
| 1299 | - | out := make([]ports.Separator, 0, len(edges)) | |
| 1300 | 1354 | cut := false | |
| 1301 | 1355 | if !layout[sel].Card { | |
| 1302 | 1356 | for _, p := range layout { | |
| @@ -1308,9 +1362,9 @@ func overviewOutline(layout []Placement, b int) []ports.Separator { | |||
| 1308 | 1362 | } | |
| 1309 | 1363 | if !cut { | |
| 1310 | 1364 | for _, e := range edges { | |
| 1311 | - | out = append(out, ports.Separator{Rect: e, Active: true}) | |
| 1365 | + | dst = append(dst, ports.Separator{Rect: e, Active: true}) | |
| 1312 | 1366 | } | |
| 1313 | - | return out | |
| 1367 | + | return dst | |
| 1314 | 1368 | } | |
| 1315 | 1369 | // Stack buffers: the outline is rebuilt every overview frame. | |
| 1316 | 1370 | var cardBuf [overviewCards]Rect | |
| @@ -1323,10 +1377,10 @@ func overviewOutline(layout []Placement, b int) []ports.Separator { | |||
| 1323 | 1377 | } | |
| 1324 | 1378 | for _, e := range edges { | |
| 1325 | 1379 | for _, piece := range subtractRects(pieceBuf[:0], e, cards) { | |
| 1326 | - | out = append(out, ports.Separator{Rect: piece, Active: true}) | |
| 1380 | + | dst = append(dst, ports.Separator{Rect: piece, Active: true}) | |
| 1327 | 1381 | } | |
| 1328 | 1382 | } | |
| 1329 | - | return out | |
| 1383 | + | return dst | |
| 1330 | 1384 | } | |
| 1331 | 1385 | ||
| 1332 | 1386 | // subtractRects is what of r no rect of cuts covers, as rects. The edges of | |
| @@ -326,6 +326,9 @@ func restoreRow(w *Workspace, s rowSnapshot) { | |||||
| 326 | // in configured order within each group. Unattached rows follow the last | 326 | // in configured order within each group. Unattached rows follow the last | ||
| 327 | // occupied numbered row (or the first). Empty invocation anchors stay visible; | 327 | // occupied numbered row (or the first). Empty invocation anchors stay visible; | ||
| 328 | // other empty rows are skipped unless current. | 328 | // other empty rows are skipped unless current. | ||
| 329 | + | // | |||
| 330 | + | // The result is the monitor's scratch, rebuilt by the next call: callers | |||
| 331 | + | // read it and drop it before calling again (Clone it to keep it). | |||
| 329 | func (m *Monitor) overviewWorkspaces() []*Workspace { | 332 | func (m *Monitor) overviewWorkspaces() []*Workspace { | ||
| 330 | fallback := m.Workspaces[0] | 333 | fallback := m.Workspaces[0] | ||
| 331 | for _, w := range m.Workspaces { | 334 | for _, w := range m.Workspaces { | ||
| @@ -333,8 +336,6 @@ func (m *Monitor) overviewWorkspaces() []*Workspace { | |||||
| 333 | fallback = w | 336 | fallback = w | ||
| 334 | } | 337 | } | ||
| 335 | } | 338 | } | ||
| 336 | - | groups := make(map[*Workspace][]*Workspace) | |||
| 337 | - | named := slices.Clone(m.hidden) | |||
| 338 | rank := func(w *Workspace) int { | 339 | rank := func(w *Workspace) int { | ||
| 339 | for i, spec := range m.named { | 340 | for i, spec := range m.named { | ||
| 340 | if spec.Name == w.Name { | 341 | if spec.Name == w.Name { | ||
| @@ -343,24 +344,37 @@ func (m *Monitor) overviewWorkspaces() []*Workspace { | |||||
| 343 | } | 344 | } | ||
| 344 | return len(m.named) | 345 | return len(m.named) | ||
| 345 | } | 346 | } | ||
| 347 | + | named := append(m.ovNamed[:0], m.hidden...) | |||
| 348 | + | m.ovNamed = named | |||
| 346 | slices.SortStableFunc(named, func(a, b *Workspace) int { return rank(a) - rank(b) }) | 349 | slices.SortStableFunc(named, func(a, b *Workspace) int { return rank(a) - rank(b) }) | ||
| 347 | - | for _, w := range named { | 350 | + | cur := m.Current() |
| 348 | - | if w.empty() && w != m.Current() { | 351 | + | rows := m.ovList[:0] |
| 349 | - | continue | |||
| 350 | - | } | |||
| 351 | - | anchor := w.overviewAfter | |||
| 352 | - | if indexOf(m.Workspaces, anchor) < 0 { | |||
| 353 | - | anchor = fallback | |||
| 354 | - | } | |||
| 355 | - | groups[anchor] = append(groups[anchor], w) | |||
| 356 | - | } | |||
| 357 | - | var rows []*Workspace | |||
| 358 | for _, w := range m.Workspaces { | 352 | for _, w := range m.Workspaces { | ||
| 359 | - | if !w.empty() || w == m.Current() || len(groups[w]) > 0 { | 353 | + | // The row is kept when occupied, current or anchoring a named |
| 360 | - | rows = append(rows, w) | 354 | + | // row: placed first, dropped again if none of those holds. |
| 361 | - | rows = append(rows, groups[w]...) | 355 | + | at := len(rows) |
| 356 | + | rows = append(rows, w) | |||
| 357 | + | for _, n := range named { | |||
| 358 | + | if n.empty() && n != cur { | |||
| 359 | + | continue | |||
| 360 | + | } | |||
| 361 | + | anchor := n.overviewAfter | |||
| 362 | + | if indexOf(m.Workspaces, anchor) < 0 { | |||
| 363 | + | anchor = fallback | |||
| 364 | + | } | |||
| 365 | + | if anchor == w { | |||
| 366 | + | rows = append(rows, n) | |||
| 367 | + | } | |||
| 368 | + | } | |||
| 369 | + | if len(rows) == at+1 && w.empty() && w != cur { | |||
| 370 | + | rows = rows[:at] | |||
| 362 | } | 371 | } | ||
| 363 | } | 372 | } | ||
| 373 | + | // Clear the unused tails so a workspace gone from this monitor is not | |||
| 374 | + | // kept reachable by the scratch. | |||
| 375 | + | clear(named[len(named):cap(named)]) | |||
| 376 | + | clear(rows[len(rows):cap(rows)]) | |||
| 377 | + | m.ovList = rows | |||
| 364 | return rows | 378 | return rows | ||
| 365 | } | 379 | } | ||
| 366 | 380 | ||||
| @@ -674,16 +688,46 @@ func (m *Monitor) OverviewPick(id WindowID) { | |||||
| 674 | // workspace's layout. | 688 | // workspace's layout. | ||
| 675 | func (m *Monitor) overviewAxis() layoutAxis { return m.Current().policy().workspace } | 689 | func (m *Monitor) overviewAxis() layoutAxis { return m.Current().policy().workspace } | ||
| 676 | 690 | ||||
| 691 | + | // overviewSlots are the slots of the overview rows: the current workspace | |||
| 692 | + | // and at most one neighbor on each side, in a fixed array (no allocation). | |||
| 693 | + | type overviewSlots struct { | |||
| 694 | + | n int | |||
| 695 | + | w [3]*Workspace | |||
| 696 | + | r [3]Rect | |||
| 697 | + | } | |||
| 698 | + | ||||
| 699 | + | func (s *overviewSlots) add(w *Workspace, r Rect) { | |||
| 700 | + | s.w[s.n], s.r[s.n] = w, r | |||
| 701 | + | s.n++ | |||
| 702 | + | } | |||
| 703 | + | ||||
| 704 | + | // at is the slot of w, the zero Rect if it has none. | |||
| 705 | + | func (s *overviewSlots) at(w *Workspace) Rect { | |||
| 706 | + | r, _ := s.lookup(w) | |||
| 707 | + | return r | |||
| 708 | + | } | |||
| 709 | + | ||||
| 710 | + | // lookup is the slot of w, if it has one. | |||
| 711 | + | func (s *overviewSlots) lookup(w *Workspace) (Rect, bool) { | |||
| 712 | + | for i := range s.n { | |||
| 713 | + | if s.w[i] == w { | |||
| 714 | + | return s.r[i], true | |||
| 715 | + | } | |||
| 716 | + | } | |||
| 717 | + | return Rect{}, false | |||
| 718 | + | } | |||
| 719 | + | ||||
| 677 | // overviewRows places the current workspace and its overview neighbors in | 720 | // overviewRows places the current workspace and its overview neighbors in | ||
| 678 | // slots, in output-local pixels, along the overview axis; each workspace | 721 | // slots, in output-local pixels, along the overview axis; each workspace | ||
| 679 | // places its previews in its slot. The numbered/named boundary gets extra | 722 | // places its previews in its slot. The numbered/named boundary gets extra | ||
| 680 | - | // spacing and an inactive rule across the axis. | 723 | + | // spacing and an inactive rule across the axis. The dividers are the |
| 724 | + | // monitor's scratch, rewritten by the next call: callers copy them. | |||
| 681 | // | 725 | // | ||
| 682 | // Vertical axis: full-width slots, the current one centred; the rule is | 726 | // Vertical axis: full-width slots, the current one centred; the rule is | ||
| 683 | // horizontal. Horizontal axis: full-height slots as wide as their content, | 727 | // horizontal. Horizontal axis: full-height slots as wide as their content, | ||
| 684 | // stack peeks included (the stash pile takes a band of the slot), the current one | 728 | // stack peeks included (the stash pile takes a band of the slot), the current one | ||
| 685 | // centred; the rule is vertical, in the spacing between two slots. | 729 | // centred; the rule is vertical, in the spacing between two slots. | ||
| 686 | - | func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | 730 | + | func (m *Monitor) overviewRows() (overviewSlots, []ports.Separator) { |
| 687 | cur := m.Current() | 731 | cur := m.Current() | ||
| 688 | u := cur.overviewArea() | 732 | u := cur.overviewArea() | ||
| 689 | gap := max(1, u.H*3/100) | 733 | gap := max(1, u.H*3/100) | ||
| @@ -696,11 +740,12 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | |||||
| 696 | hc := cur.rowHeight(axis) | 740 | hc := cur.rowHeight(axis) | ||
| 697 | cs = Rect{X: u.X, Y: u.Y + (u.H-hc)/2, W: u.W, H: hc} | 741 | cs = Rect{X: u.X, Y: u.Y + (u.H-hc)/2, W: u.W, H: hc} | ||
| 698 | } | 742 | } | ||
| 699 | - | rows := map[*Workspace]Rect{cur: cs} | 743 | + | var rows overviewSlots |
| 700 | - | var dividers []ports.Separator | 744 | + | rows.add(cur, cs) |
| 745 | + | dividers := 0 | |||
| 701 | list := m.overviewWorkspaces() | 746 | list := m.overviewWorkspaces() | ||
| 702 | i := indexOf(list, cur) | 747 | i := indexOf(list, cur) | ||
| 703 | - | for _, next := range []int{i - 1, i + 1} { | 748 | + | for _, next := range [2]int{i - 1, i + 1} { |
| 704 | if next < 0 || next >= len(list) { | 749 | if next < 0 || next >= len(list) { | ||
| 705 | continue | 750 | continue | ||
| 706 | } | 751 | } | ||
| @@ -711,12 +756,16 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) { | |||||
| 711 | spacing *= 2 | 756 | spacing *= 2 | ||
| 712 | } | 757 | } | ||
| 713 | slot, gapAt := neighborSlot(axis, u, cs, w, next < i, spacing) | 758 | slot, gapAt := neighborSlot(axis, u, cs, w, next < i, spacing) | ||
| 714 | - | rows[w] = slot | 759 | + | rows.add(w, slot) |
| 715 | if boundary { | 760 | if boundary { | ||
| 716 | - | dividers = append(dividers, overviewDivider(axis, u, gapAt+spacing/2)) | 761 | + | m.ovDividers[dividers] = overviewDivider(axis, u, gapAt+spacing/2) |
| 762 | + | dividers++ | |||
| 717 | } | 763 | } | ||
| 718 | } | 764 | } | ||
| 719 | - | return rows, dividers | 765 | + | if dividers == 0 { |
| 766 | + | return rows, nil | |||
| 767 | + | } | |||
| 768 | + | return rows, m.ovDividers[:dividers] | |||
| 720 | } | 769 | } | ||
| 721 | 770 | ||||
| 722 | // neighborSlot is the slot of w, the neighbor before or after the current | 771 | // neighborSlot is the slot of w, the neighbor before or after the current | ||
| @@ -767,7 +816,7 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement { | |||||
| 767 | var unfolded *Workspace | 816 | var unfolded *Workspace | ||
| 768 | var unfoldedSlot Rect | 817 | var unfoldedSlot Rect | ||
| 769 | for w := range m.all() { | 818 | for w := range m.all() { | ||
| 770 | - | slot, shown := rows[w] | 819 | + | slot, shown := rows.lookup(w) |
| 771 | if shown { | 820 | if shown { | ||
| 772 | // The pile band goes under the row: the row is centred | 821 | // The pile band goes under the row: the row is centred | ||
| 773 | // with it. | 822 | // with it. | ||
| @@ -1274,12 +1323,17 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes | |||||
| 1274 | return tiles | 1323 | return tiles | ||
| 1275 | } | 1324 | } | ||
| 1276 | 1325 | ||||
| 1326 | + | // outlineLines is the lines of an uncut outline: one per edge. | |||
| 1327 | + | const outlineLines = 4 | |||
| 1328 | + | ||||
| 1277 | // overviewOutline frames the selected preview with active lines of width | 1329 | // overviewOutline frames the selected preview with active lines of width | ||
| 1278 | // b, just outside it. The selection is one preview: a selected stash card | 1330 | // b, just outside it. The selection is one preview: a selected stash card | ||
| 1279 | // wins over any other focused one, which must then not be framed. A tile's | 1331 | // wins over any other focused one, which must then not be framed. A tile's | ||
| 1280 | // frame is cut where the cards of the pile sit over it, so it never runs | 1332 | // frame is cut where the cards of the pile sit over it, so it never runs | ||
| 1281 | - | // across the pile; a selected card keeps its complete frame. | 1333 | + | // across the pile; a selected card keeps its complete frame. The lines are |
| 1282 | - | func overviewOutline(layout []Placement, b int) []ports.Separator { | 1334 | + | // appended to dst, which the caller sizes for them (room for outlineLines |
| 1335 | + | // past its length) so the scene's separators are allocated once. | |||
| 1336 | + | func overviewOutline(dst []ports.Separator, layout []Placement, b int) []ports.Separator { | |||
| 1283 | sel := -1 | 1337 | sel := -1 | ||
| 1284 | for i, p := range layout { | 1338 | for i, p := range layout { | ||
| 1285 | if p.Preview > 0 && p.Focused && !p.Hidden && (sel < 0 || p.Card && !layout[sel].Card) { | 1339 | if p.Preview > 0 && p.Focused && !p.Hidden && (sel < 0 || p.Card && !layout[sel].Card) { | ||
| @@ -1287,8 +1341,9 @@ func overviewOutline(layout []Placement, b int) []ports.Separator { | |||||
| 1287 | } | 1341 | } | ||
| 1288 | } | 1342 | } | ||
| 1289 | if sel < 0 { | 1343 | if sel < 0 { | ||
| 1290 | - | return nil | 1344 | + | return dst |
| 1291 | } | 1345 | } | ||
| 1346 | + | dst = slices.Grow(dst, outlineLines) | |||
| 1292 | r := layout[sel].Rect | 1347 | r := layout[sel].Rect | ||
| 1293 | edges := [4]Rect{ | 1348 | edges := [4]Rect{ | ||
| 1294 | {X: r.X - b, Y: r.Y - b, W: r.W + 2*b, H: b}, | 1349 | {X: r.X - b, Y: r.Y - b, W: r.W + 2*b, H: b}, | ||
| @@ -1296,7 +1351,6 @@ func overviewOutline(layout []Placement, b int) []ports.Separator { | |||||
| 1296 | {X: r.X - b, Y: r.Y, W: b, H: r.H}, | 1351 | {X: r.X - b, Y: r.Y, W: b, H: r.H}, | ||
| 1297 | {X: r.X + r.W, Y: r.Y, W: b, H: r.H}, | 1352 | {X: r.X + r.W, Y: r.Y, W: b, H: r.H}, | ||
| 1298 | } | 1353 | } | ||
| 1299 | - | out := make([]ports.Separator, 0, len(edges)) | |||
| 1300 | cut := false | 1354 | cut := false | ||
| 1301 | if !layout[sel].Card { | 1355 | if !layout[sel].Card { | ||
| 1302 | for _, p := range layout { | 1356 | for _, p := range layout { | ||
| @@ -1308,9 +1362,9 @@ func overviewOutline(layout []Placement, b int) []ports.Separator { | |||||
| 1308 | } | 1362 | } | ||
| 1309 | if !cut { | 1363 | if !cut { | ||
| 1310 | for _, e := range edges { | 1364 | for _, e := range edges { | ||
| 1311 | - | out = append(out, ports.Separator{Rect: e, Active: true}) | 1365 | + | dst = append(dst, ports.Separator{Rect: e, Active: true}) |
| 1312 | } | 1366 | } | ||
| 1313 | - | return out | 1367 | + | return dst |
| 1314 | } | 1368 | } | ||
| 1315 | // Stack buffers: the outline is rebuilt every overview frame. | 1369 | // Stack buffers: the outline is rebuilt every overview frame. | ||
| 1316 | var cardBuf [overviewCards]Rect | 1370 | var cardBuf [overviewCards]Rect | ||
| @@ -1323,10 +1377,10 @@ func overviewOutline(layout []Placement, b int) []ports.Separator { | |||||
| 1323 | } | 1377 | } | ||
| 1324 | for _, e := range edges { | 1378 | for _, e := range edges { | ||
| 1325 | for _, piece := range subtractRects(pieceBuf[:0], e, cards) { | 1379 | for _, piece := range subtractRects(pieceBuf[:0], e, cards) { | ||
| 1326 | - | out = append(out, ports.Separator{Rect: piece, Active: true}) | 1380 | + | dst = append(dst, ports.Separator{Rect: piece, Active: true}) |
| 1327 | } | 1381 | } | ||
| 1328 | } | 1382 | } | ||
| 1329 | - | return out | 1383 | + | return dst |
| 1330 | } | 1384 | } | ||
| 1331 | 1385 | ||||
| 1332 | // subtractRects is what of r no rect of cuts covers, as rects. The edges of | 1386 | // subtractRects is what of r no rect of cuts covers, as rects. The edges of | ||
M internal/core/overview_cascade_test.go +15 -15
| @@ -147,7 +147,7 @@ func TestOverviewCascadeNeighbors(t *testing.T) { | |||
| 147 | 147 | m := cascadeOverview() | |
| 148 | 148 | m.ToggleOverview() | |
| 149 | 149 | rows, _ := m.overviewRows() | |
| 150 | - | cur, left, right := rows[m.Workspaces[1]], rows[m.Workspaces[0]], rows[m.Workspaces[2]] | |
| 150 | + | cur, left, right := rows.at(m.Workspaces[1]), rows.at(m.Workspaces[0]), rows.at(m.Workspaces[2]) | |
| 151 | 151 | if left.X+left.W > cur.X || right.X < cur.X+cur.W { | |
| 152 | 152 | t.Fatalf("slots overlap: %v %v %v", left, cur, right) | |
| 153 | 153 | } | |
| @@ -227,9 +227,9 @@ func TestOverviewCascadeStashInSlot(t *testing.T) { | |||
| 227 | 227 | m.ToggleOverview() | |
| 228 | 228 | rows, _ := m.overviewRows() | |
| 229 | 229 | w := m.Current() | |
| 230 | - | cur, prev := rows[w], rows[m.Workspaces[0]] | |
| 231 | - | if cur != bare[w] { | |
| 232 | - | t.Fatalf("the pile changed the slot: %v, without it %v", cur, bare[w]) | |
| 230 | + | cur, prev := rows.at(w), rows.at(m.Workspaces[0]) | |
| 231 | + | if cur != bare.at(w) { | |
| 232 | + | t.Fatalf("the pile changed the slot: %v, without it %v", cur, bare.at(w)) | |
| 233 | 233 | } | |
| 234 | 234 | ps := m.Layout() | |
| 235 | 235 | lane := previewOf(t, ps, 8).Rect | |
| @@ -566,7 +566,7 @@ func TestOverviewCascadeNamedDivider(t *testing.T) { | |||
| 566 | 566 | if len(dividers) != 1 { | |
| 567 | 567 | t.Fatalf("dividers: %+v", dividers) | |
| 568 | 568 | } | |
| 569 | - | d, cur, dev := dividers[0].Rect, rows[m.Workspaces[1]], rows[m.byName("dev")] | |
| 569 | + | d, cur, dev := dividers[0].Rect, rows.at(m.Workspaces[1]), rows.at(m.byName("dev")) | |
| 570 | 570 | if d.W != 1 || d.H <= 1 || dividers[0].Active { | |
| 571 | 571 | t.Fatalf("divider is not an inactive vertical rule: %+v", dividers[0]) | |
| 572 | 572 | } | |
| @@ -638,8 +638,8 @@ func TestOverviewCascadeNeighborStashKeepsSlot(t *testing.T) { | |||
| 638 | 638 | m.Focus(1) | |
| 639 | 639 | m.ToggleOverview() | |
| 640 | 640 | rows, _ := m.overviewRows() | |
| 641 | - | if rows[prev] != bare[prev] || prev.overviewZoom(horizontalAxis) != zoom { | |
| 642 | - | t.Fatalf("neighbor slot %v zoom %v with a stash, %v %v without", rows[prev], prev.overviewZoom(horizontalAxis), bare[prev], zoom) | |
| 641 | + | if rows.at(prev) != bare.at(prev) || prev.overviewZoom(horizontalAxis) != zoom { | |
| 642 | + | t.Fatalf("neighbor slot %v zoom %v with a stash, %v %v without", rows.at(prev), prev.overviewZoom(horizontalAxis), bare.at(prev), zoom) | |
| 643 | 643 | } | |
| 644 | 644 | if p := previewOf(t, m.Layout(), 64); p.Hidden || !p.Peek { | |
| 645 | 645 | t.Fatalf("neighbor pile card: %+v", p) | |
| @@ -813,7 +813,7 @@ func TestOverviewMixedLayoutGestureAndCancel(t *testing.T) { | |||
| 813 | 813 | from := m.Current() | |
| 814 | 814 | m.ToggleOverview() | |
| 815 | 815 | rows, _ := m.overviewRows() | |
| 816 | - | if rows[m.Workspaces[1]].X <= rows[from].X { | |
| 816 | + | if rows.at(m.Workspaces[1]).X <= rows.at(from).X { | |
| 817 | 817 | t.Fatal("cascade neighbor not horizontal") | |
| 818 | 818 | } | |
| 819 | 819 | m.overviewScroll(ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: overviewScrollStep}}) | |
| @@ -994,7 +994,7 @@ func TestOverviewCascadeWideScrollNeighborIsClipped(t *testing.T) { | |||
| 994 | 994 | } | |
| 995 | 995 | m.ToggleOverview() | |
| 996 | 996 | rows, _ := m.overviewRows() | |
| 997 | - | cur, slot := rows[m.Current()], rows[prev] | |
| 997 | + | cur, slot := rows.at(m.Current()), rows.at(prev) | |
| 998 | 998 | hidden, shown := 0, 0 | |
| 999 | 999 | for _, p := range m.Layout() { | |
| 1000 | 1000 | if !prev.has(p.ID) { | |
| @@ -1053,7 +1053,7 @@ func TestOverviewCascadeScroll(t *testing.T) { | |||
| 1053 | 1053 | ||
| 1054 | 1054 | // outlineHits reports whether any outline line of ps covers a point of r. | |
| 1055 | 1055 | func outlineHits(ps []Placement, r Rect) bool { | |
| 1056 | - | for _, s := range overviewOutline(ps, 2) { | |
| 1056 | + | for _, s := range overviewOutline(nil, ps, 2) { | |
| 1057 | 1057 | if s.Rect.Overlaps(r) { | |
| 1058 | 1058 | return true | |
| 1059 | 1059 | } | |
| @@ -1125,7 +1125,7 @@ func TestOverviewCascadeOutlineAndPile(t *testing.T) { | |||
| 1125 | 1125 | if framed(ps) != 1 || !previewOf(t, ps, 8).Focused { | |
| 1126 | 1126 | t.Fatalf("lane selected: %d framed", framed(ps)) | |
| 1127 | 1127 | } | |
| 1128 | - | if len(overviewOutline(ps, 2)) == 0 { | |
| 1128 | + | if len(overviewOutline(nil, ps, 2)) == 0 { | |
| 1129 | 1129 | t.Fatal("lane selected: no frame") | |
| 1130 | 1130 | } | |
| 1131 | 1131 | for _, id := range []WindowID{40, 41, 42} { | |
| @@ -1154,7 +1154,7 @@ func TestOverviewCascadeOutlineAndPile(t *testing.T) { | |||
| 1154 | 1154 | if !card.Focused || !card.Card { | |
| 1155 | 1155 | t.Fatalf("selected card not framed: %+v", card) | |
| 1156 | 1156 | } | |
| 1157 | - | out := overviewOutline(ps, 2) | |
| 1157 | + | out := overviewOutline(nil, ps, 2) | |
| 1158 | 1158 | if len(out) != 4 { | |
| 1159 | 1159 | t.Fatalf("card frame is %d lines, want 4: %+v", len(out), out) | |
| 1160 | 1160 | } | |
| @@ -1188,7 +1188,7 @@ func TestOverviewOutlinePrefersCard(t *testing.T) { | |||
| 1188 | 1188 | {ID: 1, Rect: Rect{X: 0, Y: 0, W: 100, H: 100}, Preview: 0.5, Focused: true}, | |
| 1189 | 1189 | {ID: 2, Rect: Rect{X: 40, Y: 80, W: 30, H: 30}, Preview: 0.2, Focused: true, Card: true}, | |
| 1190 | 1190 | } | |
| 1191 | - | out := overviewOutline(ps, 2) | |
| 1191 | + | out := overviewOutline(nil, ps, 2) | |
| 1192 | 1192 | if len(out) != 4 || out[0].Rect != (Rect{X: 38, Y: 78, W: 34, H: 2}) { | |
| 1193 | 1193 | t.Fatalf("outline: %+v", out) | |
| 1194 | 1194 | } | |
| @@ -1206,10 +1206,10 @@ func TestOverviewOutlinePileAllocations(t *testing.T) { | |||
| 1206 | 1206 | for _, p := range ps { | |
| 1207 | 1207 | card = card || p.Card && p.Preview > 0 && !p.Hidden | |
| 1208 | 1208 | } | |
| 1209 | - | if !card || len(overviewOutline(ps, 2)) == 0 { | |
| 1209 | + | if !card || len(overviewOutline(nil, ps, 2)) == 0 { | |
| 1210 | 1210 | t.Fatal("setup: no pile under a framed lane tile") | |
| 1211 | 1211 | } | |
| 1212 | - | if n := testing.AllocsPerRun(50, func() { _ = overviewOutline(ps, 2) }); n > 2 { | |
| 1212 | + | if n := testing.AllocsPerRun(50, func() { _ = overviewOutline(nil, ps, 2) }); n > 2 { | |
| 1213 | 1213 | t.Fatalf("overviewOutline allocs = %v, want <= 2", n) | |
| 1214 | 1214 | } | |
| 1215 | 1215 | } | |
| @@ -147,7 +147,7 @@ func TestOverviewCascadeNeighbors(t *testing.T) { | |||||
| 147 | m := cascadeOverview() | 147 | m := cascadeOverview() | ||
| 148 | m.ToggleOverview() | 148 | m.ToggleOverview() | ||
| 149 | rows, _ := m.overviewRows() | 149 | rows, _ := m.overviewRows() | ||
| 150 | - | cur, left, right := rows[m.Workspaces[1]], rows[m.Workspaces[0]], rows[m.Workspaces[2]] | 150 | + | cur, left, right := rows.at(m.Workspaces[1]), rows.at(m.Workspaces[0]), rows.at(m.Workspaces[2]) |
| 151 | if left.X+left.W > cur.X || right.X < cur.X+cur.W { | 151 | if left.X+left.W > cur.X || right.X < cur.X+cur.W { | ||
| 152 | t.Fatalf("slots overlap: %v %v %v", left, cur, right) | 152 | t.Fatalf("slots overlap: %v %v %v", left, cur, right) | ||
| 153 | } | 153 | } | ||
| @@ -227,9 +227,9 @@ func TestOverviewCascadeStashInSlot(t *testing.T) { | |||||
| 227 | m.ToggleOverview() | 227 | m.ToggleOverview() | ||
| 228 | rows, _ := m.overviewRows() | 228 | rows, _ := m.overviewRows() | ||
| 229 | w := m.Current() | 229 | w := m.Current() | ||
| 230 | - | cur, prev := rows[w], rows[m.Workspaces[0]] | 230 | + | cur, prev := rows.at(w), rows.at(m.Workspaces[0]) |
| 231 | - | if cur != bare[w] { | 231 | + | if cur != bare.at(w) { |
| 232 | - | t.Fatalf("the pile changed the slot: %v, without it %v", cur, bare[w]) | 232 | + | t.Fatalf("the pile changed the slot: %v, without it %v", cur, bare.at(w)) |
| 233 | } | 233 | } | ||
| 234 | ps := m.Layout() | 234 | ps := m.Layout() | ||
| 235 | lane := previewOf(t, ps, 8).Rect | 235 | lane := previewOf(t, ps, 8).Rect | ||
| @@ -566,7 +566,7 @@ func TestOverviewCascadeNamedDivider(t *testing.T) { | |||||
| 566 | if len(dividers) != 1 { | 566 | if len(dividers) != 1 { | ||
| 567 | t.Fatalf("dividers: %+v", dividers) | 567 | t.Fatalf("dividers: %+v", dividers) | ||
| 568 | } | 568 | } | ||
| 569 | - | d, cur, dev := dividers[0].Rect, rows[m.Workspaces[1]], rows[m.byName("dev")] | 569 | + | d, cur, dev := dividers[0].Rect, rows.at(m.Workspaces[1]), rows.at(m.byName("dev")) |
| 570 | if d.W != 1 || d.H <= 1 || dividers[0].Active { | 570 | if d.W != 1 || d.H <= 1 || dividers[0].Active { | ||
| 571 | t.Fatalf("divider is not an inactive vertical rule: %+v", dividers[0]) | 571 | t.Fatalf("divider is not an inactive vertical rule: %+v", dividers[0]) | ||
| 572 | } | 572 | } | ||
| @@ -638,8 +638,8 @@ func TestOverviewCascadeNeighborStashKeepsSlot(t *testing.T) { | |||||
| 638 | m.Focus(1) | 638 | m.Focus(1) | ||
| 639 | m.ToggleOverview() | 639 | m.ToggleOverview() | ||
| 640 | rows, _ := m.overviewRows() | 640 | rows, _ := m.overviewRows() | ||
| 641 | - | if rows[prev] != bare[prev] || prev.overviewZoom(horizontalAxis) != zoom { | 641 | + | if rows.at(prev) != bare.at(prev) || prev.overviewZoom(horizontalAxis) != zoom { |
| 642 | - | t.Fatalf("neighbor slot %v zoom %v with a stash, %v %v without", rows[prev], prev.overviewZoom(horizontalAxis), bare[prev], zoom) | 642 | + | t.Fatalf("neighbor slot %v zoom %v with a stash, %v %v without", rows.at(prev), prev.overviewZoom(horizontalAxis), bare.at(prev), zoom) |
| 643 | } | 643 | } | ||
| 644 | if p := previewOf(t, m.Layout(), 64); p.Hidden || !p.Peek { | 644 | if p := previewOf(t, m.Layout(), 64); p.Hidden || !p.Peek { | ||
| 645 | t.Fatalf("neighbor pile card: %+v", p) | 645 | t.Fatalf("neighbor pile card: %+v", p) | ||
| @@ -813,7 +813,7 @@ func TestOverviewMixedLayoutGestureAndCancel(t *testing.T) { | |||||
| 813 | from := m.Current() | 813 | from := m.Current() | ||
| 814 | m.ToggleOverview() | 814 | m.ToggleOverview() | ||
| 815 | rows, _ := m.overviewRows() | 815 | rows, _ := m.overviewRows() | ||
| 816 | - | if rows[m.Workspaces[1]].X <= rows[from].X { | 816 | + | if rows.at(m.Workspaces[1]).X <= rows.at(from).X { |
| 817 | t.Fatal("cascade neighbor not horizontal") | 817 | t.Fatal("cascade neighbor not horizontal") | ||
| 818 | } | 818 | } | ||
| 819 | m.overviewScroll(ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: overviewScrollStep}}) | 819 | m.overviewScroll(ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: overviewScrollStep}}) | ||
| @@ -994,7 +994,7 @@ func TestOverviewCascadeWideScrollNeighborIsClipped(t *testing.T) { | |||||
| 994 | } | 994 | } | ||
| 995 | m.ToggleOverview() | 995 | m.ToggleOverview() | ||
| 996 | rows, _ := m.overviewRows() | 996 | rows, _ := m.overviewRows() | ||
| 997 | - | cur, slot := rows[m.Current()], rows[prev] | 997 | + | cur, slot := rows.at(m.Current()), rows.at(prev) |
| 998 | hidden, shown := 0, 0 | 998 | hidden, shown := 0, 0 | ||
| 999 | for _, p := range m.Layout() { | 999 | for _, p := range m.Layout() { | ||
| 1000 | if !prev.has(p.ID) { | 1000 | if !prev.has(p.ID) { | ||
| @@ -1053,7 +1053,7 @@ func TestOverviewCascadeScroll(t *testing.T) { | |||||
| 1053 | 1053 | ||||
| 1054 | // outlineHits reports whether any outline line of ps covers a point of r. | 1054 | // outlineHits reports whether any outline line of ps covers a point of r. | ||
| 1055 | func outlineHits(ps []Placement, r Rect) bool { | 1055 | func outlineHits(ps []Placement, r Rect) bool { | ||
| 1056 | - | for _, s := range overviewOutline(ps, 2) { | 1056 | + | for _, s := range overviewOutline(nil, ps, 2) { |
| 1057 | if s.Rect.Overlaps(r) { | 1057 | if s.Rect.Overlaps(r) { | ||
| 1058 | return true | 1058 | return true | ||
| 1059 | } | 1059 | } | ||
| @@ -1125,7 +1125,7 @@ func TestOverviewCascadeOutlineAndPile(t *testing.T) { | |||||
| 1125 | if framed(ps) != 1 || !previewOf(t, ps, 8).Focused { | 1125 | if framed(ps) != 1 || !previewOf(t, ps, 8).Focused { | ||
| 1126 | t.Fatalf("lane selected: %d framed", framed(ps)) | 1126 | t.Fatalf("lane selected: %d framed", framed(ps)) | ||
| 1127 | } | 1127 | } | ||
| 1128 | - | if len(overviewOutline(ps, 2)) == 0 { | 1128 | + | if len(overviewOutline(nil, ps, 2)) == 0 { |
| 1129 | t.Fatal("lane selected: no frame") | 1129 | t.Fatal("lane selected: no frame") | ||
| 1130 | } | 1130 | } | ||
| 1131 | for _, id := range []WindowID{40, 41, 42} { | 1131 | for _, id := range []WindowID{40, 41, 42} { | ||
| @@ -1154,7 +1154,7 @@ func TestOverviewCascadeOutlineAndPile(t *testing.T) { | |||||
| 1154 | if !card.Focused || !card.Card { | 1154 | if !card.Focused || !card.Card { | ||
| 1155 | t.Fatalf("selected card not framed: %+v", card) | 1155 | t.Fatalf("selected card not framed: %+v", card) | ||
| 1156 | } | 1156 | } | ||
| 1157 | - | out := overviewOutline(ps, 2) | 1157 | + | out := overviewOutline(nil, ps, 2) |
| 1158 | if len(out) != 4 { | 1158 | if len(out) != 4 { | ||
| 1159 | t.Fatalf("card frame is %d lines, want 4: %+v", len(out), out) | 1159 | t.Fatalf("card frame is %d lines, want 4: %+v", len(out), out) | ||
| 1160 | } | 1160 | } | ||
| @@ -1188,7 +1188,7 @@ func TestOverviewOutlinePrefersCard(t *testing.T) { | |||||
| 1188 | {ID: 1, Rect: Rect{X: 0, Y: 0, W: 100, H: 100}, Preview: 0.5, Focused: true}, | 1188 | {ID: 1, Rect: Rect{X: 0, Y: 0, W: 100, H: 100}, Preview: 0.5, Focused: true}, | ||
| 1189 | {ID: 2, Rect: Rect{X: 40, Y: 80, W: 30, H: 30}, Preview: 0.2, Focused: true, Card: true}, | 1189 | {ID: 2, Rect: Rect{X: 40, Y: 80, W: 30, H: 30}, Preview: 0.2, Focused: true, Card: true}, | ||
| 1190 | } | 1190 | } | ||
| 1191 | - | out := overviewOutline(ps, 2) | 1191 | + | out := overviewOutline(nil, ps, 2) |
| 1192 | if len(out) != 4 || out[0].Rect != (Rect{X: 38, Y: 78, W: 34, H: 2}) { | 1192 | if len(out) != 4 || out[0].Rect != (Rect{X: 38, Y: 78, W: 34, H: 2}) { | ||
| 1193 | t.Fatalf("outline: %+v", out) | 1193 | t.Fatalf("outline: %+v", out) | ||
| 1194 | } | 1194 | } | ||
| @@ -1206,10 +1206,10 @@ func TestOverviewOutlinePileAllocations(t *testing.T) { | |||||
| 1206 | for _, p := range ps { | 1206 | for _, p := range ps { | ||
| 1207 | card = card || p.Card && p.Preview > 0 && !p.Hidden | 1207 | card = card || p.Card && p.Preview > 0 && !p.Hidden | ||
| 1208 | } | 1208 | } | ||
| 1209 | - | if !card || len(overviewOutline(ps, 2)) == 0 { | 1209 | + | if !card || len(overviewOutline(nil, ps, 2)) == 0 { |
| 1210 | t.Fatal("setup: no pile under a framed lane tile") | 1210 | t.Fatal("setup: no pile under a framed lane tile") | ||
| 1211 | } | 1211 | } | ||
| 1212 | - | if n := testing.AllocsPerRun(50, func() { _ = overviewOutline(ps, 2) }); n > 2 { | 1212 | + | if n := testing.AllocsPerRun(50, func() { _ = overviewOutline(nil, ps, 2) }); n > 2 { |
| 1213 | t.Fatalf("overviewOutline allocs = %v, want <= 2", n) | 1213 | t.Fatalf("overviewOutline allocs = %v, want <= 2", n) | ||
| 1214 | } | 1214 | } | ||
| 1215 | } | 1215 | } | ||
M internal/core/overview_named_test.go +18 -0
| @@ -22,6 +22,24 @@ func namedOverviewMonitor() *Monitor { | |||
| 22 | 22 | return m | |
| 23 | 23 | } | |
| 24 | 24 | ||
| 25 | + | // The overview rebuilds its row order and slots every frame: with named | |
| 26 | + | // workspaces to group and sort, from the monitor's scratch, it costs nothing. | |
| 27 | + | func TestOverviewNamedRowsAllocations(t *testing.T) { | |
| 28 | + | m := namedOverviewMonitor() | |
| 29 | + | m.Focus(0) | |
| 30 | + | m.ToggleNamed("dev") | |
| 31 | + | m.ToggleOverview() | |
| 32 | + | if len(m.overviewWorkspaces()) < 4 { | |
| 33 | + | t.Fatal("setup: named rows missing") | |
| 34 | + | } | |
| 35 | + | if n := testing.AllocsPerRun(50, func() { m.overviewWorkspaces() }); n != 0 { | |
| 36 | + | t.Errorf("overviewWorkspaces allocs = %v, want 0", n) | |
| 37 | + | } | |
| 38 | + | if n := testing.AllocsPerRun(50, func() { m.overviewRows() }); n != 0 { | |
| 39 | + | t.Errorf("overviewRows allocs = %v, want 0", n) | |
| 40 | + | } | |
| 41 | + | } | |
| 42 | + | ||
| 25 | 43 | func TestOverviewNamedFollowsInvocationWorkspace(t *testing.T) { | |
| 26 | 44 | m := namedOverviewMonitor() | |
| 27 | 45 | numbered := slices.Clone(m.Workspaces) | |
| @@ -22,6 +22,24 @@ func namedOverviewMonitor() *Monitor { | |||||
| 22 | return m | 22 | return m | ||
| 23 | } | 23 | } | ||
| 24 | 24 | ||||
| 25 | + | // The overview rebuilds its row order and slots every frame: with named | |||
| 26 | + | // workspaces to group and sort, from the monitor's scratch, it costs nothing. | |||
| 27 | + | func TestOverviewNamedRowsAllocations(t *testing.T) { | |||
| 28 | + | m := namedOverviewMonitor() | |||
| 29 | + | m.Focus(0) | |||
| 30 | + | m.ToggleNamed("dev") | |||
| 31 | + | m.ToggleOverview() | |||
| 32 | + | if len(m.overviewWorkspaces()) < 4 { | |||
| 33 | + | t.Fatal("setup: named rows missing") | |||
| 34 | + | } | |||
| 35 | + | if n := testing.AllocsPerRun(50, func() { m.overviewWorkspaces() }); n != 0 { | |||
| 36 | + | t.Errorf("overviewWorkspaces allocs = %v, want 0", n) | |||
| 37 | + | } | |||
| 38 | + | if n := testing.AllocsPerRun(50, func() { m.overviewRows() }); n != 0 { | |||
| 39 | + | t.Errorf("overviewRows allocs = %v, want 0", n) | |||
| 40 | + | } | |||
| 41 | + | } | |||
| 42 | + | ||||
| 25 | func TestOverviewNamedFollowsInvocationWorkspace(t *testing.T) { | 43 | func TestOverviewNamedFollowsInvocationWorkspace(t *testing.T) { | ||
| 26 | m := namedOverviewMonitor() | 44 | m := namedOverviewMonitor() | ||
| 27 | numbered := slices.Clone(m.Workspaces) | 45 | numbered := slices.Clone(m.Workspaces) | ||
M internal/core/overview_test.go +1 -1
| @@ -508,7 +508,7 @@ func TestOverviewFixedOverflowGeometry(t *testing.T) { | |||
| 508 | 508 | t.Fatalf("%d not hit at %+v", id, p.Rect) | |
| 509 | 509 | } | |
| 510 | 510 | } | |
| 511 | - | if len(overviewOutline(ps, 2)) != 4 { | |
| 511 | + | if len(overviewOutline(nil, ps, 2)) != 4 { | |
| 512 | 512 | t.Fatal("missing selection outline") | |
| 513 | 513 | } | |
| 514 | 514 | m.OverviewMove(-1, 0) | |
| @@ -508,7 +508,7 @@ func TestOverviewFixedOverflowGeometry(t *testing.T) { | |||||
| 508 | t.Fatalf("%d not hit at %+v", id, p.Rect) | 508 | t.Fatalf("%d not hit at %+v", id, p.Rect) | ||
| 509 | } | 509 | } | ||
| 510 | } | 510 | } | ||
| 511 | - | if len(overviewOutline(ps, 2)) != 4 { | 511 | + | if len(overviewOutline(nil, ps, 2)) != 4 { |
| 512 | t.Fatal("missing selection outline") | 512 | t.Fatal("missing selection outline") | ||
| 513 | } | 513 | } | ||
| 514 | m.OverviewMove(-1, 0) | 514 | m.OverviewMove(-1, 0) | ||
M internal/core/publish_alloc_test.go +13 -6
| @@ -24,13 +24,16 @@ import ( | |||
| 24 | 24 | // (copied only when it changed). What is left is the scenes themselves | |
| 25 | 25 | // (windows, separators, the scene set). Leaving entries (a closed window | |
| 26 | 26 | // still fading) add none: 3, less than a frame with six moving windows | |
| 27 | - | // (fewer configure targets). The opening overview costs 18 (the stack | |
| 28 | - | // items, the row map and the neighbor list it rebuilds every frame; its | |
| 29 | - | // layouts reuse the buffers too). | |
| 27 | + | // (fewer configure targets). The opening overview costs 6 (7 under -race), | |
| 28 | + | // 4 alone: it was 11 and 13 when it rebuilt the row map, the neighbor list, | |
| 29 | + | // the workspace order (groups map, hidden clone) and the outline slice | |
| 30 | + | // (appended onto the dividers) every frame; the rows are now a fixed array, | |
| 31 | + | // the order and dividers monitor scratch, and the scene's separators one | |
| 32 | + | // slice. What is left is the scene, the stack items and the preview rows. | |
| 30 | 33 | const ( | |
| 31 | 34 | publishAllocBudget = 5 | |
| 32 | 35 | publishLeavingAllocBudget = 3 | |
| 33 | - | publishOverviewAllocBudget = 18 | |
| 36 | + | publishOverviewAllocBudget = 7 | |
| 34 | 37 | ) | |
| 35 | 38 | ||
| 36 | 39 | // publishRig is a Core on the real clock (a mock clock would count its own | |
| @@ -230,12 +233,16 @@ func TestPublishOverviewAllocations(t *testing.T) { | |||
| 230 | 233 | if neighbors { | |
| 231 | 234 | want, slots = 18, 3 | |
| 232 | 235 | } | |
| 233 | - | if rows, _ := m.overviewRows(); len(rows) != slots { | |
| 234 | - | t.Fatalf("setup: %d overview slots, want %d", len(rows), slots) | |
| 236 | + | if rows, _ := m.overviewRows(); rows.n != slots { | |
| 237 | + | t.Fatalf("setup: %d overview slots, want %d", rows.n, slots) | |
| 235 | 238 | } | |
| 236 | 239 | if !m.ov.open || m.overviewAxis() != m.Current().policy().workspace || len(c.overviewReal) != want || len(sc.rects) == 0 || !c.animating() { | |
| 237 | 240 | t.Fatalf("setup: overview %v, real layouts %d (want %d), rects %d, animating %v", m.ov.open, len(c.overviewReal), want, len(sc.rects), c.animating()) | |
| 238 | 241 | } | |
| 242 | + | // The rows themselves cost nothing: array slots, scratch order. | |
| 243 | + | if n := testing.AllocsPerRun(50, func() { m.overviewRows() }); n != 0 { | |
| 244 | + | t.Errorf("overviewRows allocs = %v, want 0", n) | |
| 245 | + | } | |
| 239 | 246 | n := testing.AllocsPerRun(50, step) | |
| 240 | 247 | t.Logf("%s overview allocs per step = %v", name, n) | |
| 241 | 248 | if len(sc.rects) == 0 { | |
| @@ -24,13 +24,16 @@ import ( | |||||
| 24 | // (copied only when it changed). What is left is the scenes themselves | 24 | // (copied only when it changed). What is left is the scenes themselves | ||
| 25 | // (windows, separators, the scene set). Leaving entries (a closed window | 25 | // (windows, separators, the scene set). Leaving entries (a closed window | ||
| 26 | // still fading) add none: 3, less than a frame with six moving windows | 26 | // still fading) add none: 3, less than a frame with six moving windows | ||
| 27 | - | // (fewer configure targets). The opening overview costs 18 (the stack | 27 | + | // (fewer configure targets). The opening overview costs 6 (7 under -race), |
| 28 | - | // items, the row map and the neighbor list it rebuilds every frame; its | 28 | + | // 4 alone: it was 11 and 13 when it rebuilt the row map, the neighbor list, |
| 29 | - | // layouts reuse the buffers too). | 29 | + | // the workspace order (groups map, hidden clone) and the outline slice |
| 30 | + | // (appended onto the dividers) every frame; the rows are now a fixed array, | |||
| 31 | + | // the order and dividers monitor scratch, and the scene's separators one | |||
| 32 | + | // slice. What is left is the scene, the stack items and the preview rows. | |||
| 30 | const ( | 33 | const ( | ||
| 31 | publishAllocBudget = 5 | 34 | publishAllocBudget = 5 | ||
| 32 | publishLeavingAllocBudget = 3 | 35 | publishLeavingAllocBudget = 3 | ||
| 33 | - | publishOverviewAllocBudget = 18 | 36 | + | publishOverviewAllocBudget = 7 |
| 34 | ) | 37 | ) | ||
| 35 | 38 | ||||
| 36 | // publishRig is a Core on the real clock (a mock clock would count its own | 39 | // publishRig is a Core on the real clock (a mock clock would count its own | ||
| @@ -230,12 +233,16 @@ func TestPublishOverviewAllocations(t *testing.T) { | |||||
| 230 | if neighbors { | 233 | if neighbors { | ||
| 231 | want, slots = 18, 3 | 234 | want, slots = 18, 3 | ||
| 232 | } | 235 | } | ||
| 233 | - | if rows, _ := m.overviewRows(); len(rows) != slots { | 236 | + | if rows, _ := m.overviewRows(); rows.n != slots { |
| 234 | - | t.Fatalf("setup: %d overview slots, want %d", len(rows), slots) | 237 | + | t.Fatalf("setup: %d overview slots, want %d", rows.n, slots) |
| 235 | } | 238 | } | ||
| 236 | if !m.ov.open || m.overviewAxis() != m.Current().policy().workspace || len(c.overviewReal) != want || len(sc.rects) == 0 || !c.animating() { | 239 | if !m.ov.open || m.overviewAxis() != m.Current().policy().workspace || len(c.overviewReal) != want || len(sc.rects) == 0 || !c.animating() { | ||
| 237 | t.Fatalf("setup: overview %v, real layouts %d (want %d), rects %d, animating %v", m.ov.open, len(c.overviewReal), want, len(sc.rects), c.animating()) | 240 | t.Fatalf("setup: overview %v, real layouts %d (want %d), rects %d, animating %v", m.ov.open, len(c.overviewReal), want, len(sc.rects), c.animating()) | ||
| 238 | } | 241 | } | ||
| 242 | + | // The rows themselves cost nothing: array slots, scratch order. | |||
| 243 | + | if n := testing.AllocsPerRun(50, func() { m.overviewRows() }); n != 0 { | |||
| 244 | + | t.Errorf("overviewRows allocs = %v, want 0", n) | |||
| 245 | + | } | |||
| 239 | n := testing.AllocsPerRun(50, step) | 246 | n := testing.AllocsPerRun(50, step) | ||
| 240 | t.Logf("%s overview allocs per step = %v", name, n) | 247 | t.Logf("%s overview allocs per step = %v", name, n) | ||
| 241 | if len(sc.rects) == 0 { | 248 | if len(sc.rects) == 0 { | ||
M internal/core/scene.go +8 -3
| @@ -54,11 +54,16 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 54 | 54 | } | |
| 55 | 55 | if sc.mon.ov.open { | |
| 56 | 56 | // Frame the selection and separate numbered and named row groups. | |
| 57 | - | _, scene.Separators = sc.mon.overviewRows() | |
| 58 | - | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | |
| 57 | + | // The scene's own slice (immutable once sent), sized for both. | |
| 58 | + | _, dividers := sc.mon.overviewRows() | |
| 59 | + | var seps []ports.Separator | |
| 60 | + | if len(dividers) > 0 { | |
| 61 | + | seps = append(make([]ports.Separator, 0, len(dividers)+outlineLines), dividers...) | |
| 62 | + | } | |
| 63 | + | scene.Separators = overviewOutline(seps, layout, max(c.cfg.Border.Width, 2)) | |
| 59 | 64 | } else if sc.mon.switcherShown() { | |
| 60 | 65 | // The switcher's cards: only the selection is framed. | |
| 61 | - | scene.Separators = overviewOutline(layout, max(c.cfg.Border.Width, 2)) | |
| 66 | + | scene.Separators = overviewOutline(nil, layout, max(c.cfg.Border.Width, 2)) | |
| 62 | 67 | } else { | |
| 63 | 68 | // Only the focused output lights the focused window's lines. | |
| 64 | 69 | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), view, i == c.focusScreen) | |
| @@ -54,11 +54,16 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 54 | } | 54 | } | ||
| 55 | if sc.mon.ov.open { | 55 | if sc.mon.ov.open { | ||
| 56 | // Frame the selection and separate numbered and named row groups. | 56 | // Frame the selection and separate numbered and named row groups. | ||
| 57 | - | _, scene.Separators = sc.mon.overviewRows() | 57 | + | // The scene's own slice (immutable once sent), sized for both. |
| 58 | - | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | 58 | + | _, dividers := sc.mon.overviewRows() |
| 59 | + | var seps []ports.Separator | |||
| 60 | + | if len(dividers) > 0 { | |||
| 61 | + | seps = append(make([]ports.Separator, 0, len(dividers)+outlineLines), dividers...) | |||
| 62 | + | } | |||
| 63 | + | scene.Separators = overviewOutline(seps, layout, max(c.cfg.Border.Width, 2)) | |||
| 59 | } else if sc.mon.switcherShown() { | 64 | } else if sc.mon.switcherShown() { | ||
| 60 | // The switcher's cards: only the selection is framed. | 65 | // The switcher's cards: only the selection is framed. | ||
| 61 | - | scene.Separators = overviewOutline(layout, max(c.cfg.Border.Width, 2)) | 66 | + | scene.Separators = overviewOutline(nil, layout, max(c.cfg.Border.Width, 2)) |
| 62 | } else { | 67 | } else { | ||
| 63 | // Only the focused output lights the focused window's lines. | 68 | // Only the focused output lights the focused window's lines. | ||
| 64 | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), view, i == c.focusScreen) | 69 | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), view, i == c.focusScreen) | ||