detached fea658c

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +150 -58
M internal/core/monitor.go +8 -0
@@ -3,6 +3,8 @@ package core
33 import (
44 "iter"
55 "slices"
6+
7+ "github.com/bnema/neferwl/internal/ports"
68 )
79
810 // Monitor owns an ordered list of workspaces for one output (ADR 011). Numbers
@@ -46,6 +48,12 @@ type Monitor struct {
4648 // one its swipe began on); switchView.off is then from the current
4749 // workspace's place in it.
4850 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
4957 // ov holds the overview selection and its Escape snapshot.
5058 ov overviewState
5159 // 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). Numbers10 // 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 current48 // 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 []*Workspace50 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 overviewState58 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) {
326326 // in configured order within each group. Unattached rows follow the last
327327 // occupied numbered row (or the first). Empty invocation anchors stay visible;
328328 // 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).
329332 func (m *Monitor) overviewWorkspaces() []*Workspace {
330333 fallback := m.Workspaces[0]
331334 for _, w := range m.Workspaces {
@@ -333,8 +336,6 @@ func (m *Monitor) overviewWorkspaces() []*Workspace {
333336 fallback = w
334337 }
335338 }
336- groups := make(map[*Workspace][]*Workspace)
337- named := slices.Clone(m.hidden)
338339 rank := func(w *Workspace) int {
339340 for i, spec := range m.named {
340341 if spec.Name == w.Name {
@@ -343,24 +344,37 @@ func (m *Monitor) overviewWorkspaces() []*Workspace {
343344 }
344345 return len(m.named)
345346 }
347+ named := append(m.ovNamed[:0], m.hidden...)
348+ m.ovNamed = named
346349 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]
358352 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]
362371 }
363372 }
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
364378 return rows
365379 }
366380
@@ -674,16 +688,46 @@ func (m *Monitor) OverviewPick(id WindowID) {
674688 // workspace's layout.
675689 func (m *Monitor) overviewAxis() layoutAxis { return m.Current().policy().workspace }
676690
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+
677720 // overviewRows places the current workspace and its overview neighbors in
678721 // slots, in output-local pixels, along the overview axis; each workspace
679722 // 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.
681725 //
682726 // Vertical axis: full-width slots, the current one centred; the rule is
683727 // horizontal. Horizontal axis: full-height slots as wide as their content,
684728 // stack peeks included (the stash pile takes a band of the slot), the current one
685729 // 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) {
687731 cur := m.Current()
688732 u := cur.overviewArea()
689733 gap := max(1, u.H*3/100)
@@ -696,11 +740,12 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
696740 hc := cur.rowHeight(axis)
697741 cs = Rect{X: u.X, Y: u.Y + (u.H-hc)/2, W: u.W, H: hc}
698742 }
699- rows := map[*Workspace]Rect{cur: cs}
700- var dividers []ports.Separator
743+ var rows overviewSlots
744+ rows.add(cur, cs)
745+ dividers := 0
701746 list := m.overviewWorkspaces()
702747 i := indexOf(list, cur)
703- for _, next := range []int{i - 1, i + 1} {
748+ for _, next := range [2]int{i - 1, i + 1} {
704749 if next < 0 || next >= len(list) {
705750 continue
706751 }
@@ -711,12 +756,16 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
711756 spacing *= 2
712757 }
713758 slot, gapAt := neighborSlot(axis, u, cs, w, next < i, spacing)
714- rows[w] = slot
759+ rows.add(w, slot)
715760 if boundary {
716- dividers = append(dividers, overviewDivider(axis, u, gapAt+spacing/2))
761+ m.ovDividers[dividers] = overviewDivider(axis, u, gapAt+spacing/2)
762+ dividers++
717763 }
718764 }
719- return rows, dividers
765+ if dividers == 0 {
766+ return rows, nil
767+ }
768+ return rows, m.ovDividers[:dividers]
720769 }
721770
722771 // neighborSlot is the slot of w, the neighbor before or after the current
@@ -767,7 +816,7 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement {
767816 var unfolded *Workspace
768817 var unfoldedSlot Rect
769818 for w := range m.all() {
770- slot, shown := rows[w]
819+ slot, shown := rows.lookup(w)
771820 if shown {
772821 // The pile band goes under the row: the row is centred
773822 // with it.
@@ -1274,12 +1323,17 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes
12741323 return tiles
12751324 }
12761325
1326+// outlineLines is the lines of an uncut outline: one per edge.
1327+const outlineLines = 4
1328+
12771329 // overviewOutline frames the selected preview with active lines of width
12781330 // b, just outside it. The selection is one preview: a selected stash card
12791331 // wins over any other focused one, which must then not be framed. A tile's
12801332 // 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 {
12831337 sel := -1
12841338 for i, p := range layout {
12851339 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 {
12871341 }
12881342 }
12891343 if sel < 0 {
1290- return nil
1344+ return dst
12911345 }
1346+ dst = slices.Grow(dst, outlineLines)
12921347 r := layout[sel].Rect
12931348 edges := [4]Rect{
12941349 {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 {
12961351 {X: r.X - b, Y: r.Y, W: b, H: r.H},
12971352 {X: r.X + r.W, Y: r.Y, W: b, H: r.H},
12981353 }
1299- out := make([]ports.Separator, 0, len(edges))
13001354 cut := false
13011355 if !layout[sel].Card {
13021356 for _, p := range layout {
@@ -1308,9 +1362,9 @@ func overviewOutline(layout []Placement, b int) []ports.Separator {
13081362 }
13091363 if !cut {
13101364 for _, e := range edges {
1311- out = append(out, ports.Separator{Rect: e, Active: true})
1365+ dst = append(dst, ports.Separator{Rect: e, Active: true})
13121366 }
1313- return out
1367+ return dst
13141368 }
13151369 // Stack buffers: the outline is rebuilt every overview frame.
13161370 var cardBuf [overviewCards]Rect
@@ -1323,10 +1377,10 @@ func overviewOutline(layout []Placement, b int) []ports.Separator {
13231377 }
13241378 for _, e := range edges {
13251379 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})
13271381 }
13281382 }
1329- return out
1383+ return dst
13301384 }
13311385
13321386 // 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 last326 // 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 = w336 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 rows378 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 in720 // overviewRows places the current workspace and its overview neighbors in
678 // slots, in output-local pixels, along the overview axis; each workspace721 // slots, in output-local pixels, along the overview axis; each workspace
679 // places its previews in its slot. The numbered/named boundary gets extra722 // 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 is726 // 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 one728 // 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.Separator744+ 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 continue750 continue
706 }751 }
@@ -711,12 +756,16 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
711 spacing *= 2756 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] = slot759+ 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, dividers765+ 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 current771 // 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 *Workspace816 var unfolded *Workspace
768 var unfoldedSlot Rect817 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 centred821 // 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 tiles1323 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 width1329 // overviewOutline frames the selected preview with active lines of width
1278 // b, just outside it. The selection is one preview: a selected stash card1330 // 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's1331 // 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 runs1332 // 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 := -11337 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 nil1344+ return dst
1291 }1345 }
1346+ dst = slices.Grow(dst, outlineLines)
1292 r := layout[sel].Rect1347 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 := false1354 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 out1367+ 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]Rect1370 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 out1383+ return dst
1330 }1384 }
1331 1385
1332 // subtractRects is what of r no rect of cuts covers, as rects. The edges of1386 // 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) {
147147 m := cascadeOverview()
148148 m.ToggleOverview()
149149 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])
151151 if left.X+left.W > cur.X || right.X < cur.X+cur.W {
152152 t.Fatalf("slots overlap: %v %v %v", left, cur, right)
153153 }
@@ -227,9 +227,9 @@ func TestOverviewCascadeStashInSlot(t *testing.T) {
227227 m.ToggleOverview()
228228 rows, _ := m.overviewRows()
229229 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))
233233 }
234234 ps := m.Layout()
235235 lane := previewOf(t, ps, 8).Rect
@@ -566,7 +566,7 @@ func TestOverviewCascadeNamedDivider(t *testing.T) {
566566 if len(dividers) != 1 {
567567 t.Fatalf("dividers: %+v", dividers)
568568 }
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"))
570570 if d.W != 1 || d.H <= 1 || dividers[0].Active {
571571 t.Fatalf("divider is not an inactive vertical rule: %+v", dividers[0])
572572 }
@@ -638,8 +638,8 @@ func TestOverviewCascadeNeighborStashKeepsSlot(t *testing.T) {
638638 m.Focus(1)
639639 m.ToggleOverview()
640640 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)
643643 }
644644 if p := previewOf(t, m.Layout(), 64); p.Hidden || !p.Peek {
645645 t.Fatalf("neighbor pile card: %+v", p)
@@ -813,7 +813,7 @@ func TestOverviewMixedLayoutGestureAndCancel(t *testing.T) {
813813 from := m.Current()
814814 m.ToggleOverview()
815815 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 {
817817 t.Fatal("cascade neighbor not horizontal")
818818 }
819819 m.overviewScroll(ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: overviewScrollStep}})
@@ -994,7 +994,7 @@ func TestOverviewCascadeWideScrollNeighborIsClipped(t *testing.T) {
994994 }
995995 m.ToggleOverview()
996996 rows, _ := m.overviewRows()
997- cur, slot := rows[m.Current()], rows[prev]
997+ cur, slot := rows.at(m.Current()), rows.at(prev)
998998 hidden, shown := 0, 0
999999 for _, p := range m.Layout() {
10001000 if !prev.has(p.ID) {
@@ -1053,7 +1053,7 @@ func TestOverviewCascadeScroll(t *testing.T) {
10531053
10541054 // outlineHits reports whether any outline line of ps covers a point of r.
10551055 func outlineHits(ps []Placement, r Rect) bool {
1056- for _, s := range overviewOutline(ps, 2) {
1056+ for _, s := range overviewOutline(nil, ps, 2) {
10571057 if s.Rect.Overlaps(r) {
10581058 return true
10591059 }
@@ -1125,7 +1125,7 @@ func TestOverviewCascadeOutlineAndPile(t *testing.T) {
11251125 if framed(ps) != 1 || !previewOf(t, ps, 8).Focused {
11261126 t.Fatalf("lane selected: %d framed", framed(ps))
11271127 }
1128- if len(overviewOutline(ps, 2)) == 0 {
1128+ if len(overviewOutline(nil, ps, 2)) == 0 {
11291129 t.Fatal("lane selected: no frame")
11301130 }
11311131 for _, id := range []WindowID{40, 41, 42} {
@@ -1154,7 +1154,7 @@ func TestOverviewCascadeOutlineAndPile(t *testing.T) {
11541154 if !card.Focused || !card.Card {
11551155 t.Fatalf("selected card not framed: %+v", card)
11561156 }
1157- out := overviewOutline(ps, 2)
1157+ out := overviewOutline(nil, ps, 2)
11581158 if len(out) != 4 {
11591159 t.Fatalf("card frame is %d lines, want 4: %+v", len(out), out)
11601160 }
@@ -1188,7 +1188,7 @@ func TestOverviewOutlinePrefersCard(t *testing.T) {
11881188 {ID: 1, Rect: Rect{X: 0, Y: 0, W: 100, H: 100}, Preview: 0.5, Focused: true},
11891189 {ID: 2, Rect: Rect{X: 40, Y: 80, W: 30, H: 30}, Preview: 0.2, Focused: true, Card: true},
11901190 }
1191- out := overviewOutline(ps, 2)
1191+ out := overviewOutline(nil, ps, 2)
11921192 if len(out) != 4 || out[0].Rect != (Rect{X: 38, Y: 78, W: 34, H: 2}) {
11931193 t.Fatalf("outline: %+v", out)
11941194 }
@@ -1206,10 +1206,10 @@ func TestOverviewOutlinePileAllocations(t *testing.T) {
12061206 for _, p := range ps {
12071207 card = card || p.Card && p.Preview > 0 && !p.Hidden
12081208 }
1209- if !card || len(overviewOutline(ps, 2)) == 0 {
1209+ if !card || len(overviewOutline(nil, ps, 2)) == 0 {
12101210 t.Fatal("setup: no pile under a framed lane tile")
12111211 }
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 {
12131213 t.Fatalf("overviewOutline allocs = %v, want <= 2", n)
12141214 }
12151215 }
@@ -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).Rect235 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, 0998 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 true1058 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.Hidden1207 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 {
2222 return m
2323 }
2424
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+
2543 func TestOverviewNamedFollowsInvocationWorkspace(t *testing.T) {
2644 m := namedOverviewMonitor()
2745 numbered := slices.Clone(m.Workspaces)
@@ -22,6 +22,24 @@ func namedOverviewMonitor() *Monitor {
22 return m22 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) {
508508 t.Fatalf("%d not hit at %+v", id, p.Rect)
509509 }
510510 }
511- if len(overviewOutline(ps, 2)) != 4 {
511+ if len(overviewOutline(nil, ps, 2)) != 4 {
512512 t.Fatal("missing selection outline")
513513 }
514514 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 (
2424 // (copied only when it changed). What is left is the scenes themselves
2525 // (windows, separators, the scene set). Leaving entries (a closed window
2626 // 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.
3033 const (
3134 publishAllocBudget = 5
3235 publishLeavingAllocBudget = 3
33- publishOverviewAllocBudget = 18
36+ publishOverviewAllocBudget = 7
3437 )
3538
3639 // publishRig is a Core on the real clock (a mock clock would count its own
@@ -230,12 +233,16 @@ func TestPublishOverviewAllocations(t *testing.T) {
230233 if neighbors {
231234 want, slots = 18, 3
232235 }
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)
235238 }
236239 if !m.ov.open || m.overviewAxis() != m.Current().policy().workspace || len(c.overviewReal) != want || len(sc.rects) == 0 || !c.animating() {
237240 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())
238241 }
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+ }
239246 n := testing.AllocsPerRun(50, step)
240247 t.Logf("%s overview allocs per step = %v", name, n)
241248 if len(sc.rects) == 0 {
@@ -24,13 +24,16 @@ import (
24 // (copied only when it changed). What is left is the scenes themselves24 // (copied only when it changed). What is left is the scenes themselves
25 // (windows, separators, the scene set). Leaving entries (a closed window25 // (windows, separators, the scene set). Leaving entries (a closed window
26 // still fading) add none: 3, less than a frame with six moving windows26 // still fading) add none: 3, less than a frame with six moving windows
27-// (fewer configure targets). The opening overview costs 18 (the stack27+// (fewer configure targets). The opening overview costs 6 (7 under -race),
28-// items, the row map and the neighbor list it rebuilds every frame; its28+// 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 = 534 publishAllocBudget = 5
32 publishLeavingAllocBudget = 335 publishLeavingAllocBudget = 3
33- publishOverviewAllocBudget = 1836+ publishOverviewAllocBudget = 7
34 )37 )
35 38
36 // publishRig is a Core on the real clock (a mock clock would count its own39 // 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, 3234 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
5454 }
5555 if sc.mon.ov.open {
5656 // 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))
5964 } else if sc.mon.switcherShown() {
6065 // 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))
6267 } else {
6368 // Only the focused output lights the focused window's lines.
6469 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)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 26

Tags 10

↑↓ moveenter openesc close