detached 24a63d3

Merge pull request 'feat(core): centre the cascade overview stash across the lane's bottom edge' (#51) from feat/cascade-stash-bottom into main

Reviewed-on: https://git.bnema.dev/bnema/neferwl/pulls/51
commit
24a63d348a52bb45cfcd71127e950e402ca5e221
author
bnema · · 2026-10-14 20:05 UTC
merge of
f4ebc85 + 4183d39 · diff against the first parent
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
5 files · +282 -130
M internal/core/layout_buffers_test.go +3 -3
@@ -146,7 +146,7 @@ func TestLayoutResultsDoNotAlias(t *testing.T) {
146146 buf = m.layoutInto(buf)
147147 buf = m.Current().layoutInto(buf)
148148 m.Current().previewTiles()
149- m.Current().overviewZoom()
149+ m.Current().overviewZoom(m.overviewAxis())
150150 }
151151 // layoutInto builds in the caller's buffer, never in a scratch of
152152 // the workspace: with the capacity there it returns that storage.
@@ -164,8 +164,8 @@ func TestLayoutResultsDoNotAlias(t *testing.T) {
164164 // unchanged.
165165 mine := w.layoutInto(make([]Placement, 0, len(res)))
166166 mineKept := slices.Clone(mine)
167- w.overviewZoom()
168- w.peekStep()
167+ w.overviewZoom(m.overviewAxis())
168+ w.peekStep(m.overviewAxis())
169169 w.previewTiles()
170170 w.columnRects()
171171 _ = w.Layout()
@@ -146,7 +146,7 @@ func TestLayoutResultsDoNotAlias(t *testing.T) {
146 buf = m.layoutInto(buf)146 buf = m.layoutInto(buf)
147 buf = m.Current().layoutInto(buf)147 buf = m.Current().layoutInto(buf)
148 m.Current().previewTiles()148 m.Current().previewTiles()
149- m.Current().overviewZoom()149+ m.Current().overviewZoom(m.overviewAxis())
150 }150 }
151 // layoutInto builds in the caller's buffer, never in a scratch of151 // layoutInto builds in the caller's buffer, never in a scratch of
152 // the workspace: with the capacity there it returns that storage.152 // the workspace: with the capacity there it returns that storage.
@@ -164,8 +164,8 @@ func TestLayoutResultsDoNotAlias(t *testing.T) {
164 // unchanged.164 // unchanged.
165 mine := w.layoutInto(make([]Placement, 0, len(res)))165 mine := w.layoutInto(make([]Placement, 0, len(res)))
166 mineKept := slices.Clone(mine)166 mineKept := slices.Clone(mine)
167- w.overviewZoom()167+ w.overviewZoom(m.overviewAxis())
168- w.peekStep()168+ w.peekStep(m.overviewAxis())
169 w.previewTiles()169 w.previewTiles()
170 w.columnRects()170 w.columnRects()
171 _ = w.Layout()171 _ = w.Layout()
M internal/core/overview.go +108 -86
@@ -53,10 +53,12 @@ type rowSnapshot struct {
5353 // workspace. A pinned fullscreen window is the front card of its row, the
5454 // windows it hides behind it; choosing one of them leaves fullscreen.
5555
56-// A workspace's stash shows as a pile of cards pinned to the left edge of
57-// its row: the selected stashed window in front at overviewCardZoom, up to
58-// overviewCards-1 others behind it, each overviewCardStep further up and
59-// right, dimmed. The row takes the width the pile leaves.
56+// A workspace's stash shows as a pile of cards: the selected stashed window
57+// in front at overviewCardZoom, up to overviewCards-1 others behind it, each
58+// overviewCardStep further up and right, dimmed. A vertical overview pins
59+// the pile to the left edge of its row, which takes the width the pile
60+// leaves. A horizontal one centres it on its row, straddling the bottom edge
61+// of the selected lane, over the tiles.
6062 const (
6163 overviewCardZoom = 0.2
6264 overviewCards = 4
@@ -423,15 +425,22 @@ func (m *Monitor) cycleCard(w *Workspace, d int) bool {
423425 // overviewLane is the vertical move of an overview whose workspaces sit side
424426 // by side (cascade): a selected stash card cycles the pile; else the
425427 // selection goes to the previous or next lane (band), at the closest column,
426-// stopping at the first and last. Covering floats stack along the horizontal
427-// axis, not this one.
428+// stopping at the first. Down past the last lane, or from a card that is not
429+// the columns, enters the stash, which sits below. Covering floats stack
430+// along the horizontal axis, not this one.
428431 func (m *Monitor) overviewLane(dy int) {
429432 w := m.Current()
430- if m.cycleCard(w, dy) || m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 {
433+ if m.cycleCard(w, dy) {
431434 return
432435 }
433- if i := w.laneNeighbor(dy); i >= 0 {
434- m.selectLaneColumn(w, i)
436+ if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 {
437+ if i := w.laneNeighbor(dy); i >= 0 {
438+ m.selectLaneColumn(w, i)
439+ return
440+ }
441+ }
442+ if dy > 0 && len(w.Stash) > 0 {
443+ m.selectCard(w, w.stashAt)
435444 }
436445 }
437446
@@ -444,10 +453,11 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) {
444453
445454 // overviewAcross is the horizontal move of an overview whose workspaces sit
446455 // side by side (cascade): a column inside the selected lane. Past the
447-// lane's first column it goes to the stash, else to the previous workspace;
448-// past the last column, to the next workspace. Covering floats fan along
449-// this axis and are traversed before the stash or a workspace: the card on
450-// the left is the one behind.
456+// lane's first column it goes to the previous workspace; past the last
457+// column, to the next workspace. From a stash card, left goes to the
458+// previous workspace and right back to the columns. Covering floats fan
459+// along this axis and are traversed before a workspace: the card on the
460+// left is the one behind.
451461 func (m *Monitor) overviewAcross(dx int) {
452462 w := m.Current()
453463 if m.cardAt(w) >= 0 {
@@ -459,15 +469,18 @@ func (m *Monitor) overviewAcross(dx int) {
459469 }
460470 m.clearCard()
461471 m.ov.selected = 0
462- if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 {
463- // The first column of the focused lane.
472+ if len(w.Columns) > 0 {
473+ // The columns come to the front, even when down entered the
474+ // stash from a covering float: at the first column of the
475+ // focused lane.
476+ m.setFront(w, stackItem{kind: stackColumns})
464477 i, _ := w.laneBounds(w.laneOf(w.Focus))
465478 m.selectLaneColumn(w, i)
466479 }
467480 return
468481 }
469482 if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 {
470- m.laneEdge(w, dx)
483+ m.laneEdge(dx)
471484 return
472485 }
473486 first, last := w.laneBounds(w.laneOf(w.Focus))
@@ -475,20 +488,15 @@ func (m *Monitor) overviewAcross(dx int) {
475488 m.selectLaneColumn(w, next)
476489 return
477490 }
478- m.laneEdge(w, dx)
491+ m.laneEdge(dx)
479492 }
480493
481494 // laneEdge is a horizontal move off the edge of the selected lane, or off a
482-// card that is not the columns: the stack first, then the stash on the left
483-// or a workspace.
484-func (m *Monitor) laneEdge(w *Workspace, dx int) {
495+// card that is not the columns: the stack first, then a workspace.
496+func (m *Monitor) laneEdge(dx int) {
485497 if m.moveStack(-dx) {
486498 return
487499 }
488- if dx < 0 && len(w.Stash) > 0 {
489- m.selectCard(w, w.stashAt)
490- return
491- }
492500 m.overviewStep(dx)
493501 }
494502
@@ -628,8 +636,8 @@ func (m *Monitor) overviewAxis() layoutAxis { return m.Current().policy().worksp
628636 //
629637 // Vertical axis: full-width slots, the current one centred; the rule is
630638 // horizontal. Horizontal axis: full-height slots as wide as their content,
631-// stash pile and stack peeks included, the current one centred; the rule is
632-// vertical, in the spacing between two slots.
639+// stack peeks included (the stash pile sits over the row), the current one
640+// centred; the rule is vertical, in the spacing between two slots.
633641 func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
634642 cur := m.Current()
635643 u := cur.overviewArea()
@@ -637,13 +645,10 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
637645 axis := m.overviewAxis()
638646 var cs Rect
639647 if axis == horizontalAxis {
640- // The row stays centred on the area; the pile extends the slot to
641- // the left, pushing the previous workspace, unless it would leave
642- // the area.
643- pile, row := cur.pileWidth(), cur.rowWidth()
644- cs = Rect{X: max(u.X+(u.W-row)/2-pile, u.X), Y: u.Y, W: pile + row, H: u.H}
648+ row := cur.rowWidth(axis)
649+ cs = Rect{X: u.X + (u.W-row)/2, Y: u.Y, W: row, H: u.H}
645650 } else {
646- hc := cur.rowHeight()
651+ hc := cur.rowHeight(axis)
647652 cs = Rect{X: u.X, Y: u.Y + (u.H-hc)/2, W: u.W, H: hc}
648653 }
649654 rows := map[*Workspace]Rect{cur: cs}
@@ -673,7 +678,7 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
673678 // slot cs along axis, spacing away; gapAt is where the spacing begins.
674679 func neighborSlot(axis layoutAxis, u, cs Rect, w *Workspace, before bool, spacing int) (slot Rect, gapAt int) {
675680 if axis == horizontalAxis {
676- slot = Rect{Y: u.Y, W: w.slotWidth(), H: u.H}
681+ slot = Rect{Y: u.Y, W: w.rowWidth(axis), H: u.H}
677682 if before {
678683 gapAt = cs.X - spacing
679684 slot.X = gapAt - slot.W
@@ -683,7 +688,7 @@ func neighborSlot(axis layoutAxis, u, cs Rect, w *Workspace, before bool, spacin
683688 }
684689 return slot, gapAt
685690 }
686- h := w.rowHeight()
691+ h := w.rowHeight(axis)
687692 slot = Rect{X: u.X, W: u.W, H: h}
688693 if before {
689694 gapAt = cs.Y - spacing
@@ -718,7 +723,7 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement {
718723 at := m.cardAt(w)
719724 result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...)
720725 if len(w.Stash) > 0 {
721- result = append(result, w.pile(w.pileBand(axis, slot), w != cur, at)...)
726+ result = append(result, w.pile(axis, slot, w != cur, at)...)
722727 }
723728 }
724729 for _, f := range w.Floats {
@@ -802,8 +807,8 @@ func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool,
802807 return out
803808 }
804809
805-// pileWidth is the room the stash pile of w takes on the left of its row,
806-// its margin included; 0 without a stash.
810+// pileWidth is the room the stash pile of w takes on the left of its row in
811+// a vertical overview, its margin included; 0 without a stash.
807812 func (w *Workspace) pileWidth() int {
808813 if len(w.Stash) == 0 {
809814 return 0
@@ -820,11 +825,11 @@ func (w *Workspace) cardStep() int {
820825 return min(overviewCardStep, a.W/100, a.H/50)
821826 }
822827
823-// pile places the stash of w as cards on the left of band (see pileBand):
824-// entry front (the stash selection when -1) in
828+// pile places the stash of w as cards in its slot along axis (see the
829+// overview's stash comment): entry front (the stash selection when -1) in
825830 // front, the next ones behind it, dimmed, the rest hidden. Only a front card
826831 // of the current row (dim false) with front set is selected.
827-func (w *Workspace) pile(band Rect, dim bool, front int) []Placement {
832+func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, front int) []Placement {
828833 n := len(w.Stash)
829834 if n == 0 {
830835 return nil
@@ -838,10 +843,21 @@ func (w *Workspace) pile(band Rect, dim bool, front int) []Placement {
838843 u := w.overviewArea()
839844 cw := int(math.Round(float64(r.W) * overviewCardZoom))
840845 ch := int(math.Round(float64(r.H) * overviewCardZoom))
841- rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom()))
842- // Stack rows reserve two peek steps above the front card. Align the
843- // stash with that front band, not with the top of its envelope.
844- x0, y0 := band.X+u.W/100, band.Y+(rowH-ch)/2
846+ rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis)))
847+ var x0, y0 int
848+ if axis == horizontalAxis {
849+ // Centred on the row, across the bottom edge of the selected
850+ // lane, which is centred in the slot. Both scale with the area
851+ // (the lane at most overviewMaxZoom of it), so the pile stays
852+ // inside whatever its size.
853+ x0 = slot.X + (slot.W-cw)/2
854+ y0 = slot.Y + (slot.H-rowH)/2 + rowH - ch/2
855+ } else {
856+ // Stack rows reserve two peek steps above the front card. Align
857+ // the stash with that front band, not with the top of its envelope.
858+ band := w.frontSlot(axis, slot)
859+ x0, y0 = band.X+u.W/100, band.Y+(rowH-ch)/2
860+ }
845861 cards := make([][]Placement, n)
846862 for i := range w.Stash {
847863 cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: Rect{X: x0, Y: y0, W: cw, H: ch}, Preview: overviewCardZoom, Focused: selected}}
@@ -853,22 +869,26 @@ func (w *Workspace) pile(band Rect, dim bool, front int) []Placement {
853869 }
854870
855871 // overviewZoom is the scale that fits every column of w in the usable
856-// width, within the overview bounds.
857-func (w *Workspace) overviewZoom() float64 {
858- z, _ := w.zoomSpan()
872+// width, within the overview bounds, in an overview along axis: only a
873+// vertical one sets the stash pile beside the row.
874+func (w *Workspace) overviewZoom(axis layoutAxis) float64 {
875+ z, _ := w.zoomSpan(axis)
859876 return z
860877 }
861878
862879 // zoomSpan is overviewZoom with the unscaled span of the tiles it fits. A
863880 // cascade workspace spans one screen whatever its band count.
864-func (w *Workspace) zoomSpan() (float64, int) {
881+func (w *Workspace) zoomSpan(axis layoutAxis) (float64, int) {
865882 // Only the span is read: the tiles go in the zoom scratch, dead on return.
866883 var span int
867884 w.zoomBuf, span, _ = w.previewTilesInto(w.zoomBuf[:0])
868885 if span <= 0 {
869886 return overviewMaxZoom, span
870887 }
871- room := float64(w.overviewArea().W-w.pileWidth()) * 0.96
888+ room := float64(w.overviewArea().W) * 0.96
889+ if axis == verticalAxis {
890+ room = float64(w.overviewArea().W-w.pileWidth()) * 0.96
891+ }
872892 z := min(max(room/float64(span), overviewMinZoom), overviewMaxZoom)
873893 if w.stacked() {
874894 // Reserve two clear peeks on either side of the front row.
@@ -877,31 +897,25 @@ func (w *Workspace) zoomSpan() (float64, int) {
877897 return z, span
878898 }
879899
880-// slotWidth is the width of the slot of w in a horizontal overview: its
881-// stash pile and its row.
882-func (w *Workspace) slotWidth() int {
883- return w.pileWidth() + w.rowWidth()
884-}
885-
886-// rowWidth is the width of the row of w in a horizontal overview: what its
887-// tiles take (at most what the pile leaves of the area), plus the room for
888-// the covering floats peeking on either side of the front card.
889-func (w *Workspace) rowWidth() int {
890- z, span := w.zoomSpan()
900+// rowWidth is the width of the slot of w in a horizontal overview: what its
901+// tiles take (at most the area), plus the room for the covering floats
902+// peeking on either side of the front card.
903+func (w *Workspace) rowWidth(axis layoutAxis) int {
904+ z, span := w.zoomSpan(axis)
891905 if span <= 0 {
892906 // An empty workspace keeps the room of a screen.
893907 span = w.Usable.W
894908 }
895- width := min(int(math.Round(float64(span)*z)), w.overviewArea().W-w.pileWidth())
909+ width := min(int(math.Round(float64(span)*z)), w.overviewArea().W)
896910 if w.stacked() {
897- width += 4 * w.peekStep()
911+ width += 4 * w.peekStep(axis)
898912 }
899913 return width
900914 }
901915
902916 // peekStep is the shared step for a stack row and its front-card inset.
903-func (w *Workspace) peekStep() int {
904- return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom()*overviewPeekStep)))
917+func (w *Workspace) peekStep(axis layoutAxis) int {
918+ return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom(axis)*overviewPeekStep)))
905919 }
906920
907921 // frontSlot is the part of slot the front card of w takes: slot shifted
@@ -910,7 +924,7 @@ func (w *Workspace) peekStep() int {
910924 // the room of the two after it. A single-card row keeps slot.
911925 func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect {
912926 if w.stacked() {
913- p := w.peekStep()
927+ p := w.peekStep(axis)
914928 axis.offset(&slot, 2*p)
915929 if axis == horizontalAxis {
916930 slot.W -= 4 * p
@@ -919,24 +933,12 @@ func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect {
919933 return slot
920934 }
921935
922-// pileBand is the band the stash pile of w aligns with in its slot. A
923-// vertical overview aligns it with the front card (see frontSlot). A
924-// horizontal one puts it level with the selected lane, left of the whole
925-// stack of cards.
926-func (w *Workspace) pileBand(axis layoutAxis, slot Rect) Rect {
927- if axis == verticalAxis {
928- return w.frontSlot(axis, slot)
929- }
930- slot.Y += (slot.H - int(math.Round(float64(w.Usable.H)*w.overviewZoom()))) / 2
931- return slot
932-}
933-
934936 // rowHeight includes the largest possible stack envelope (two peeks above
935937 // and two below). Neighbor rows use the same measure as the current row.
936-func (w *Workspace) rowHeight() int {
937- h := int(math.Round(float64(w.Usable.H) * w.overviewZoom()))
938+func (w *Workspace) rowHeight(axis layoutAxis) int {
939+ h := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis)))
938940 if w.stacked() {
939- h += 4 * w.peekStep()
941+ h += 4 * w.peekStep(axis)
940942 }
941943 return h
942944 }
@@ -1024,7 +1026,7 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
10241026
10251027 // previewRow scales the tiles of w into a row placed in slot, centred
10261028 // in the slot's width, or scrolled so the focused column shows when the
1027-// row is wider, right of the stash pile. Dimmed rows (neighbor
1029+// row is wider (right of the stash pile in a vertical overview). Dimmed rows (neighbor
10281030 // workspaces) have no focus; neither has a row whose pile has it (lit
10291031 // false).
10301032 func (w *Workspace) previewRow(axis layoutAxis, slot Rect, dim, lit bool) []Placement {
@@ -1043,7 +1045,7 @@ func (w *Workspace) previewRow(axis layoutAxis, slot Rect, dim, lit bool) []Plac
10431045 // the overview axis shows only its selected lane. In a horizontal overview,
10441046 // tiles entirely outside the area are hidden.
10451047 func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes bool, tiles []Placement, span int, sel Rect) []Placement {
1046- z := w.overviewZoom()
1048+ z := w.overviewZoom(axis)
10471049 scale := func(v int) int { return int(math.Round(float64(v) * z)) }
10481050 width := scale(span)
10491051 u := w.overviewArea()
@@ -1051,9 +1053,13 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes
10511053 if axis == horizontalAxis {
10521054 top = slot.Y + (slot.H-scale(w.Usable.H))/2
10531055 }
1054- // Rows stay centred on the output like rows without a stash; the pile
1055- // only pushes a row right when they would overlap.
1056- left := w.pileWidth()
1056+ // Rows stay centred on the output like rows without a stash; in a
1057+ // vertical overview the pile only pushes a row right when they would
1058+ // overlap. A horizontal one draws the pile over the row.
1059+ left := 0
1060+ if axis == verticalAxis {
1061+ left = w.pileWidth()
1062+ }
10571063 x := max(slot.X+(slot.W-width)/2, slot.X+left)
10581064 ux, uw := slot.X+left, slot.W-left
10591065 if width > uw {
@@ -1142,6 +1148,22 @@ func overviewIn(layout []Placement, x, y float64) WindowID {
11421148 return 0
11431149 }
11441150
1151+// overviewHit is overviewIn with the stash piles first: a pile is drawn
1152+// over its row (a horizontal overview), the cards behind its front one
1153+// included.
1154+func (m *Monitor) overviewHit(layout []Placement, x, y float64) WindowID {
1155+ var piles []Placement
1156+ for _, p := range layout {
1157+ if w, _ := m.find(p.ID); w != nil && w.stashIndex(p.ID) >= 0 {
1158+ piles = append(piles, p)
1159+ }
1160+ }
1161+ if id := overviewIn(piles, x, y); id != 0 {
1162+ return id
1163+ }
1164+ return overviewIn(layout, x, y)
1165+}
1166+
11451167 // overviewClick closes the overview on the preview under the pointer, on
11461168 // the output it shows on. picked is false when no preview is there.
11471169 func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) {
@@ -1155,7 +1177,7 @@ func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) {
11551177 }
11561178 // The cards are hit where they are drawn: a click during the open or a
11571179 // navigation animation picks what moves under the pointer.
1158- id := overviewIn(sc.shownLayout(), c.cursorX-float64(sc.x), c.cursorY-float64(sc.y))
1180+ id := sc.mon.overviewHit(sc.shownLayout(), c.cursorX-float64(sc.x), c.cursorY-float64(sc.y))
11591181 if id == 0 {
11601182 return false, nil
11611183 }
@@ -53,10 +53,12 @@ type rowSnapshot struct {
53 // workspace. A pinned fullscreen window is the front card of its row, the53 // workspace. A pinned fullscreen window is the front card of its row, the
54 // windows it hides behind it; choosing one of them leaves fullscreen.54 // windows it hides behind it; choosing one of them leaves fullscreen.
55 55
56-// A workspace's stash shows as a pile of cards pinned to the left edge of56+// A workspace's stash shows as a pile of cards: the selected stashed window
57-// its row: the selected stashed window in front at overviewCardZoom, up to57+// in front at overviewCardZoom, up to overviewCards-1 others behind it, each
58-// overviewCards-1 others behind it, each overviewCardStep further up and58+// overviewCardStep further up and right, dimmed. A vertical overview pins
59-// right, dimmed. The row takes the width the pile leaves.59+// the pile to the left edge of its row, which takes the width the pile
60+// leaves. A horizontal one centres it on its row, straddling the bottom edge
61+// of the selected lane, over the tiles.
60 const (62 const (
61 overviewCardZoom = 0.263 overviewCardZoom = 0.2
62 overviewCards = 464 overviewCards = 4
@@ -423,15 +425,22 @@ func (m *Monitor) cycleCard(w *Workspace, d int) bool {
423 // overviewLane is the vertical move of an overview whose workspaces sit side425 // overviewLane is the vertical move of an overview whose workspaces sit side
424 // by side (cascade): a selected stash card cycles the pile; else the426 // by side (cascade): a selected stash card cycles the pile; else the
425 // selection goes to the previous or next lane (band), at the closest column,427 // selection goes to the previous or next lane (band), at the closest column,
426-// stopping at the first and last. Covering floats stack along the horizontal428+// stopping at the first. Down past the last lane, or from a card that is not
427-// axis, not this one.429+// the columns, enters the stash, which sits below. Covering floats stack
430+// along the horizontal axis, not this one.
428 func (m *Monitor) overviewLane(dy int) {431 func (m *Monitor) overviewLane(dy int) {
429 w := m.Current()432 w := m.Current()
430- if m.cycleCard(w, dy) || m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 {433+ if m.cycleCard(w, dy) {
431 return434 return
432 }435 }
433- if i := w.laneNeighbor(dy); i >= 0 {436+ if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 {
434- m.selectLaneColumn(w, i)437+ if i := w.laneNeighbor(dy); i >= 0 {
438+ m.selectLaneColumn(w, i)
439+ return
440+ }
441+ }
442+ if dy > 0 && len(w.Stash) > 0 {
443+ m.selectCard(w, w.stashAt)
435 }444 }
436 }445 }
437 446
@@ -444,10 +453,11 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) {
444 453
445 // overviewAcross is the horizontal move of an overview whose workspaces sit454 // overviewAcross is the horizontal move of an overview whose workspaces sit
446 // side by side (cascade): a column inside the selected lane. Past the455 // side by side (cascade): a column inside the selected lane. Past the
447-// lane's first column it goes to the stash, else to the previous workspace;456+// lane's first column it goes to the previous workspace; past the last
448-// past the last column, to the next workspace. Covering floats fan along457+// column, to the next workspace. From a stash card, left goes to the
449-// this axis and are traversed before the stash or a workspace: the card on458+// previous workspace and right back to the columns. Covering floats fan
450-// the left is the one behind.459+// along this axis and are traversed before a workspace: the card on the
460+// left is the one behind.
451 func (m *Monitor) overviewAcross(dx int) {461 func (m *Monitor) overviewAcross(dx int) {
452 w := m.Current()462 w := m.Current()
453 if m.cardAt(w) >= 0 {463 if m.cardAt(w) >= 0 {
@@ -459,15 +469,18 @@ func (m *Monitor) overviewAcross(dx int) {
459 }469 }
460 m.clearCard()470 m.clearCard()
461 m.ov.selected = 0471 m.ov.selected = 0
462- if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 {472+ if len(w.Columns) > 0 {
463- // The first column of the focused lane.473+ // The columns come to the front, even when down entered the
474+ // stash from a covering float: at the first column of the
475+ // focused lane.
476+ m.setFront(w, stackItem{kind: stackColumns})
464 i, _ := w.laneBounds(w.laneOf(w.Focus))477 i, _ := w.laneBounds(w.laneOf(w.Focus))
465 m.selectLaneColumn(w, i)478 m.selectLaneColumn(w, i)
466 }479 }
467 return480 return
468 }481 }
469 if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 {482 if m.stackFront(w).kind != stackColumns || len(w.Columns) == 0 {
470- m.laneEdge(w, dx)483+ m.laneEdge(dx)
471 return484 return
472 }485 }
473 first, last := w.laneBounds(w.laneOf(w.Focus))486 first, last := w.laneBounds(w.laneOf(w.Focus))
@@ -475,20 +488,15 @@ func (m *Monitor) overviewAcross(dx int) {
475 m.selectLaneColumn(w, next)488 m.selectLaneColumn(w, next)
476 return489 return
477 }490 }
478- m.laneEdge(w, dx)491+ m.laneEdge(dx)
479 }492 }
480 493
481 // laneEdge is a horizontal move off the edge of the selected lane, or off a494 // laneEdge is a horizontal move off the edge of the selected lane, or off a
482-// card that is not the columns: the stack first, then the stash on the left495+// card that is not the columns: the stack first, then a workspace.
483-// or a workspace.496+func (m *Monitor) laneEdge(dx int) {
484-func (m *Monitor) laneEdge(w *Workspace, dx int) {
485 if m.moveStack(-dx) {497 if m.moveStack(-dx) {
486 return498 return
487 }499 }
488- if dx < 0 && len(w.Stash) > 0 {
489- m.selectCard(w, w.stashAt)
490- return
491- }
492 m.overviewStep(dx)500 m.overviewStep(dx)
493 }501 }
494 502
@@ -628,8 +636,8 @@ func (m *Monitor) overviewAxis() layoutAxis { return m.Current().policy().worksp
628 //636 //
629 // Vertical axis: full-width slots, the current one centred; the rule is637 // Vertical axis: full-width slots, the current one centred; the rule is
630 // horizontal. Horizontal axis: full-height slots as wide as their content,638 // horizontal. Horizontal axis: full-height slots as wide as their content,
631-// stash pile and stack peeks included, the current one centred; the rule is639+// stack peeks included (the stash pile sits over the row), the current one
632-// vertical, in the spacing between two slots.640+// centred; the rule is vertical, in the spacing between two slots.
633 func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {641 func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
634 cur := m.Current()642 cur := m.Current()
635 u := cur.overviewArea()643 u := cur.overviewArea()
@@ -637,13 +645,10 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
637 axis := m.overviewAxis()645 axis := m.overviewAxis()
638 var cs Rect646 var cs Rect
639 if axis == horizontalAxis {647 if axis == horizontalAxis {
640- // The row stays centred on the area; the pile extends the slot to648+ row := cur.rowWidth(axis)
641- // the left, pushing the previous workspace, unless it would leave649+ cs = Rect{X: u.X + (u.W-row)/2, Y: u.Y, W: row, H: u.H}
642- // the area.
643- pile, row := cur.pileWidth(), cur.rowWidth()
644- cs = Rect{X: max(u.X+(u.W-row)/2-pile, u.X), Y: u.Y, W: pile + row, H: u.H}
645 } else {650 } else {
646- hc := cur.rowHeight()651+ hc := cur.rowHeight(axis)
647 cs = Rect{X: u.X, Y: u.Y + (u.H-hc)/2, W: u.W, H: hc}652 cs = Rect{X: u.X, Y: u.Y + (u.H-hc)/2, W: u.W, H: hc}
648 }653 }
649 rows := map[*Workspace]Rect{cur: cs}654 rows := map[*Workspace]Rect{cur: cs}
@@ -673,7 +678,7 @@ func (m *Monitor) overviewRows() (map[*Workspace]Rect, []ports.Separator) {
673 // slot cs along axis, spacing away; gapAt is where the spacing begins.678 // slot cs along axis, spacing away; gapAt is where the spacing begins.
674 func neighborSlot(axis layoutAxis, u, cs Rect, w *Workspace, before bool, spacing int) (slot Rect, gapAt int) {679 func neighborSlot(axis layoutAxis, u, cs Rect, w *Workspace, before bool, spacing int) (slot Rect, gapAt int) {
675 if axis == horizontalAxis {680 if axis == horizontalAxis {
676- slot = Rect{Y: u.Y, W: w.slotWidth(), H: u.H}681+ slot = Rect{Y: u.Y, W: w.rowWidth(axis), H: u.H}
677 if before {682 if before {
678 gapAt = cs.X - spacing683 gapAt = cs.X - spacing
679 slot.X = gapAt - slot.W684 slot.X = gapAt - slot.W
@@ -683,7 +688,7 @@ func neighborSlot(axis layoutAxis, u, cs Rect, w *Workspace, before bool, spacin
683 }688 }
684 return slot, gapAt689 return slot, gapAt
685 }690 }
686- h := w.rowHeight()691+ h := w.rowHeight(axis)
687 slot = Rect{X: u.X, W: u.W, H: h}692 slot = Rect{X: u.X, W: u.W, H: h}
688 if before {693 if before {
689 gapAt = cs.Y - spacing694 gapAt = cs.Y - spacing
@@ -718,7 +723,7 @@ func (m *Monitor) overviewLayout(result []Placement) []Placement {
718 at := m.cardAt(w)723 at := m.cardAt(w)
719 result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...)724 result = append(result, m.stackRow(w, axis, slot, w != cur, at < 0)...)
720 if len(w.Stash) > 0 {725 if len(w.Stash) > 0 {
721- result = append(result, w.pile(w.pileBand(axis, slot), w != cur, at)...)726+ result = append(result, w.pile(axis, slot, w != cur, at)...)
722 }727 }
723 }728 }
724 for _, f := range w.Floats {729 for _, f := range w.Floats {
@@ -802,8 +807,8 @@ func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool,
802 return out807 return out
803 }808 }
804 809
805-// pileWidth is the room the stash pile of w takes on the left of its row,810+// pileWidth is the room the stash pile of w takes on the left of its row in
806-// its margin included; 0 without a stash.811+// a vertical overview, its margin included; 0 without a stash.
807 func (w *Workspace) pileWidth() int {812 func (w *Workspace) pileWidth() int {
808 if len(w.Stash) == 0 {813 if len(w.Stash) == 0 {
809 return 0814 return 0
@@ -820,11 +825,11 @@ func (w *Workspace) cardStep() int {
820 return min(overviewCardStep, a.W/100, a.H/50)825 return min(overviewCardStep, a.W/100, a.H/50)
821 }826 }
822 827
823-// pile places the stash of w as cards on the left of band (see pileBand):828+// pile places the stash of w as cards in its slot along axis (see the
824-// entry front (the stash selection when -1) in829+// overview's stash comment): entry front (the stash selection when -1) in
825 // front, the next ones behind it, dimmed, the rest hidden. Only a front card830 // front, the next ones behind it, dimmed, the rest hidden. Only a front card
826 // of the current row (dim false) with front set is selected.831 // of the current row (dim false) with front set is selected.
827-func (w *Workspace) pile(band Rect, dim bool, front int) []Placement {832+func (w *Workspace) pile(axis layoutAxis, slot Rect, dim bool, front int) []Placement {
828 n := len(w.Stash)833 n := len(w.Stash)
829 if n == 0 {834 if n == 0 {
830 return nil835 return nil
@@ -838,10 +843,21 @@ func (w *Workspace) pile(band Rect, dim bool, front int) []Placement {
838 u := w.overviewArea()843 u := w.overviewArea()
839 cw := int(math.Round(float64(r.W) * overviewCardZoom))844 cw := int(math.Round(float64(r.W) * overviewCardZoom))
840 ch := int(math.Round(float64(r.H) * overviewCardZoom))845 ch := int(math.Round(float64(r.H) * overviewCardZoom))
841- rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom()))846+ rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis)))
842- // Stack rows reserve two peek steps above the front card. Align the847+ var x0, y0 int
843- // stash with that front band, not with the top of its envelope.848+ if axis == horizontalAxis {
844- x0, y0 := band.X+u.W/100, band.Y+(rowH-ch)/2849+ // Centred on the row, across the bottom edge of the selected
850+ // lane, which is centred in the slot. Both scale with the area
851+ // (the lane at most overviewMaxZoom of it), so the pile stays
852+ // inside whatever its size.
853+ x0 = slot.X + (slot.W-cw)/2
854+ y0 = slot.Y + (slot.H-rowH)/2 + rowH - ch/2
855+ } else {
856+ // Stack rows reserve two peek steps above the front card. Align
857+ // the stash with that front band, not with the top of its envelope.
858+ band := w.frontSlot(axis, slot)
859+ x0, y0 = band.X+u.W/100, band.Y+(rowH-ch)/2
860+ }
845 cards := make([][]Placement, n)861 cards := make([][]Placement, n)
846 for i := range w.Stash {862 for i := range w.Stash {
847 cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: Rect{X: x0, Y: y0, W: cw, H: ch}, Preview: overviewCardZoom, Focused: selected}}863 cards[i] = []Placement{{ID: w.Stash[i].ID, Rect: Rect{X: x0, Y: y0, W: cw, H: ch}, Preview: overviewCardZoom, Focused: selected}}
@@ -853,22 +869,26 @@ func (w *Workspace) pile(band Rect, dim bool, front int) []Placement {
853 }869 }
854 870
855 // overviewZoom is the scale that fits every column of w in the usable871 // overviewZoom is the scale that fits every column of w in the usable
856-// width, within the overview bounds.872+// width, within the overview bounds, in an overview along axis: only a
857-func (w *Workspace) overviewZoom() float64 {873+// vertical one sets the stash pile beside the row.
858- z, _ := w.zoomSpan()874+func (w *Workspace) overviewZoom(axis layoutAxis) float64 {
875+ z, _ := w.zoomSpan(axis)
859 return z876 return z
860 }877 }
861 878
862 // zoomSpan is overviewZoom with the unscaled span of the tiles it fits. A879 // zoomSpan is overviewZoom with the unscaled span of the tiles it fits. A
863 // cascade workspace spans one screen whatever its band count.880 // cascade workspace spans one screen whatever its band count.
864-func (w *Workspace) zoomSpan() (float64, int) {881+func (w *Workspace) zoomSpan(axis layoutAxis) (float64, int) {
865 // Only the span is read: the tiles go in the zoom scratch, dead on return.882 // Only the span is read: the tiles go in the zoom scratch, dead on return.
866 var span int883 var span int
867 w.zoomBuf, span, _ = w.previewTilesInto(w.zoomBuf[:0])884 w.zoomBuf, span, _ = w.previewTilesInto(w.zoomBuf[:0])
868 if span <= 0 {885 if span <= 0 {
869 return overviewMaxZoom, span886 return overviewMaxZoom, span
870 }887 }
871- room := float64(w.overviewArea().W-w.pileWidth()) * 0.96888+ room := float64(w.overviewArea().W) * 0.96
889+ if axis == verticalAxis {
890+ room = float64(w.overviewArea().W-w.pileWidth()) * 0.96
891+ }
872 z := min(max(room/float64(span), overviewMinZoom), overviewMaxZoom)892 z := min(max(room/float64(span), overviewMinZoom), overviewMaxZoom)
873 if w.stacked() {893 if w.stacked() {
874 // Reserve two clear peeks on either side of the front row.894 // Reserve two clear peeks on either side of the front row.
@@ -877,31 +897,25 @@ func (w *Workspace) zoomSpan() (float64, int) {
877 return z, span897 return z, span
878 }898 }
879 899
880-// slotWidth is the width of the slot of w in a horizontal overview: its900+// rowWidth is the width of the slot of w in a horizontal overview: what its
881-// stash pile and its row.901+// tiles take (at most the area), plus the room for the covering floats
882-func (w *Workspace) slotWidth() int {902+// peeking on either side of the front card.
883- return w.pileWidth() + w.rowWidth()903+func (w *Workspace) rowWidth(axis layoutAxis) int {
884-}904+ z, span := w.zoomSpan(axis)
885-
886-// rowWidth is the width of the row of w in a horizontal overview: what its
887-// tiles take (at most what the pile leaves of the area), plus the room for
888-// the covering floats peeking on either side of the front card.
889-func (w *Workspace) rowWidth() int {
890- z, span := w.zoomSpan()
891 if span <= 0 {905 if span <= 0 {
892 // An empty workspace keeps the room of a screen.906 // An empty workspace keeps the room of a screen.
893 span = w.Usable.W907 span = w.Usable.W
894 }908 }
895- width := min(int(math.Round(float64(span)*z)), w.overviewArea().W-w.pileWidth())909+ width := min(int(math.Round(float64(span)*z)), w.overviewArea().W)
896 if w.stacked() {910 if w.stacked() {
897- width += 4 * w.peekStep()911+ width += 4 * w.peekStep(axis)
898 }912 }
899 return width913 return width
900 }914 }
901 915
902 // peekStep is the shared step for a stack row and its front-card inset.916 // peekStep is the shared step for a stack row and its front-card inset.
903-func (w *Workspace) peekStep() int {917+func (w *Workspace) peekStep(axis layoutAxis) int {
904- return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom()*overviewPeekStep)))918+ return max(1, int(math.Round(float64(w.Usable.H)*w.overviewZoom(axis)*overviewPeekStep)))
905 }919 }
906 920
907 // frontSlot is the part of slot the front card of w takes: slot shifted921 // frontSlot is the part of slot the front card of w takes: slot shifted
@@ -910,7 +924,7 @@ func (w *Workspace) peekStep() int {
910 // the room of the two after it. A single-card row keeps slot.924 // the room of the two after it. A single-card row keeps slot.
911 func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect {925 func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect {
912 if w.stacked() {926 if w.stacked() {
913- p := w.peekStep()927+ p := w.peekStep(axis)
914 axis.offset(&slot, 2*p)928 axis.offset(&slot, 2*p)
915 if axis == horizontalAxis {929 if axis == horizontalAxis {
916 slot.W -= 4 * p930 slot.W -= 4 * p
@@ -919,24 +933,12 @@ func (w *Workspace) frontSlot(axis layoutAxis, slot Rect) Rect {
919 return slot933 return slot
920 }934 }
921 935
922-// pileBand is the band the stash pile of w aligns with in its slot. A
923-// vertical overview aligns it with the front card (see frontSlot). A
924-// horizontal one puts it level with the selected lane, left of the whole
925-// stack of cards.
926-func (w *Workspace) pileBand(axis layoutAxis, slot Rect) Rect {
927- if axis == verticalAxis {
928- return w.frontSlot(axis, slot)
929- }
930- slot.Y += (slot.H - int(math.Round(float64(w.Usable.H)*w.overviewZoom()))) / 2
931- return slot
932-}
933-
934 // rowHeight includes the largest possible stack envelope (two peeks above936 // rowHeight includes the largest possible stack envelope (two peeks above
935 // and two below). Neighbor rows use the same measure as the current row.937 // and two below). Neighbor rows use the same measure as the current row.
936-func (w *Workspace) rowHeight() int {938+func (w *Workspace) rowHeight(axis layoutAxis) int {
937- h := int(math.Round(float64(w.Usable.H) * w.overviewZoom()))939+ h := int(math.Round(float64(w.Usable.H) * w.overviewZoom(axis)))
938 if w.stacked() {940 if w.stacked() {
939- h += 4 * w.peekStep()941+ h += 4 * w.peekStep(axis)
940 }942 }
941 return h943 return h
942 }944 }
@@ -1024,7 +1026,7 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
1024 1026
1025 // previewRow scales the tiles of w into a row placed in slot, centred1027 // previewRow scales the tiles of w into a row placed in slot, centred
1026 // in the slot's width, or scrolled so the focused column shows when the1028 // in the slot's width, or scrolled so the focused column shows when the
1027-// row is wider, right of the stash pile. Dimmed rows (neighbor1029+// row is wider (right of the stash pile in a vertical overview). Dimmed rows (neighbor
1028 // workspaces) have no focus; neither has a row whose pile has it (lit1030 // workspaces) have no focus; neither has a row whose pile has it (lit
1029 // false).1031 // false).
1030 func (w *Workspace) previewRow(axis layoutAxis, slot Rect, dim, lit bool) []Placement {1032 func (w *Workspace) previewRow(axis layoutAxis, slot Rect, dim, lit bool) []Placement {
@@ -1043,7 +1045,7 @@ func (w *Workspace) previewRow(axis layoutAxis, slot Rect, dim, lit bool) []Plac
1043 // the overview axis shows only its selected lane. In a horizontal overview,1045 // the overview axis shows only its selected lane. In a horizontal overview,
1044 // tiles entirely outside the area are hidden.1046 // tiles entirely outside the area are hidden.
1045 func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes bool, tiles []Placement, span int, sel Rect) []Placement {1047 func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes bool, tiles []Placement, span int, sel Rect) []Placement {
1046- z := w.overviewZoom()1048+ z := w.overviewZoom(axis)
1047 scale := func(v int) int { return int(math.Round(float64(v) * z)) }1049 scale := func(v int) int { return int(math.Round(float64(v) * z)) }
1048 width := scale(span)1050 width := scale(span)
1049 u := w.overviewArea()1051 u := w.overviewArea()
@@ -1051,9 +1053,13 @@ func (w *Workspace) previewRowTiles(axis layoutAxis, slot Rect, dim, lit, lanes
1051 if axis == horizontalAxis {1053 if axis == horizontalAxis {
1052 top = slot.Y + (slot.H-scale(w.Usable.H))/21054 top = slot.Y + (slot.H-scale(w.Usable.H))/2
1053 }1055 }
1054- // Rows stay centred on the output like rows without a stash; the pile1056+ // Rows stay centred on the output like rows without a stash; in a
1055- // only pushes a row right when they would overlap.1057+ // vertical overview the pile only pushes a row right when they would
1056- left := w.pileWidth()1058+ // overlap. A horizontal one draws the pile over the row.
1059+ left := 0
1060+ if axis == verticalAxis {
1061+ left = w.pileWidth()
1062+ }
1057 x := max(slot.X+(slot.W-width)/2, slot.X+left)1063 x := max(slot.X+(slot.W-width)/2, slot.X+left)
1058 ux, uw := slot.X+left, slot.W-left1064 ux, uw := slot.X+left, slot.W-left
1059 if width > uw {1065 if width > uw {
@@ -1142,6 +1148,22 @@ func overviewIn(layout []Placement, x, y float64) WindowID {
1142 return 01148 return 0
1143 }1149 }
1144 1150
1151+// overviewHit is overviewIn with the stash piles first: a pile is drawn
1152+// over its row (a horizontal overview), the cards behind its front one
1153+// included.
1154+func (m *Monitor) overviewHit(layout []Placement, x, y float64) WindowID {
1155+ var piles []Placement
1156+ for _, p := range layout {
1157+ if w, _ := m.find(p.ID); w != nil && w.stashIndex(p.ID) >= 0 {
1158+ piles = append(piles, p)
1159+ }
1160+ }
1161+ if id := overviewIn(piles, x, y); id != 0 {
1162+ return id
1163+ }
1164+ return overviewIn(layout, x, y)
1165+}
1166+
1145 // overviewClick closes the overview on the preview under the pointer, on1167 // overviewClick closes the overview on the preview under the pointer, on
1146 // the output it shows on. picked is false when no preview is there.1168 // the output it shows on. picked is false when no preview is there.
1147 func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) {1169 func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) {
@@ -1155,7 +1177,7 @@ func (c *Core) overviewClick(ctx context.Context) (picked bool, err error) {
1155 }1177 }
1156 // The cards are hit where they are drawn: a click during the open or a1178 // The cards are hit where they are drawn: a click during the open or a
1157 // navigation animation picks what moves under the pointer.1179 // navigation animation picks what moves under the pointer.
1158- id := overviewIn(sc.shownLayout(), c.cursorX-float64(sc.x), c.cursorY-float64(sc.y))1180+ id := sc.mon.overviewHit(sc.shownLayout(), c.cursorX-float64(sc.x), c.cursorY-float64(sc.y))
1159 if id == 0 {1181 if id == 0 {
1160 return false, nil1182 return false, nil
1161 }1183 }
M internal/core/overview_behavior_test.go +5 -4
@@ -50,12 +50,13 @@ func TestOverviewPeekEnvelopeAndSingleCardHorizontal(t *testing.T) {
5050 front := previewOf(t, m.Layout(), 7)
5151 back := previewOf(t, m.Layout(), 8)
5252 step := float64(front.Rect.Y - back.Rect.Y)
53- if step < float64(w.Usable.H)*w.overviewZoom()*0.06 {
53+ if step < float64(w.Usable.H)*w.overviewZoom(verticalAxis)*0.06 {
5454 t.Fatalf("peek step %.1f", step)
5555 }
56- y := w.Usable.Y + (w.Usable.H-w.rowHeight())/2
57- if back.Rect.Y < y || front.Rect.Y+front.Rect.H > y+w.rowHeight() {
58- t.Fatalf("outside row front %+v back %+v row %d..%d", front, back, y, y+w.rowHeight())
56+ h := w.rowHeight(verticalAxis)
57+ y := w.Usable.Y + (w.Usable.H-h)/2
58+ if back.Rect.Y < y || front.Rect.Y+front.Rect.H > y+h {
59+ t.Fatalf("outside row front %+v back %+v row %d..%d", front, back, y, y+h)
5960 }
6061 m.OverviewMove(1, 0)
6162 if m.stackFront(w) != (stackItem{stackFloat, 7}) {
@@ -50,12 +50,13 @@ func TestOverviewPeekEnvelopeAndSingleCardHorizontal(t *testing.T) {
50 front := previewOf(t, m.Layout(), 7)50 front := previewOf(t, m.Layout(), 7)
51 back := previewOf(t, m.Layout(), 8)51 back := previewOf(t, m.Layout(), 8)
52 step := float64(front.Rect.Y - back.Rect.Y)52 step := float64(front.Rect.Y - back.Rect.Y)
53- if step < float64(w.Usable.H)*w.overviewZoom()*0.06 {53+ if step < float64(w.Usable.H)*w.overviewZoom(verticalAxis)*0.06 {
54 t.Fatalf("peek step %.1f", step)54 t.Fatalf("peek step %.1f", step)
55 }55 }
56- y := w.Usable.Y + (w.Usable.H-w.rowHeight())/256+ h := w.rowHeight(verticalAxis)
57- if back.Rect.Y < y || front.Rect.Y+front.Rect.H > y+w.rowHeight() {57+ y := w.Usable.Y + (w.Usable.H-h)/2
58- t.Fatalf("outside row front %+v back %+v row %d..%d", front, back, y, y+w.rowHeight())58+ if back.Rect.Y < y || front.Rect.Y+front.Rect.H > y+h {
59+ t.Fatalf("outside row front %+v back %+v row %d..%d", front, back, y, y+h)
59 }60 }
60 m.OverviewMove(1, 0)61 m.OverviewMove(1, 0)
61 if m.stackFront(w) != (stackItem{stackFloat, 7}) {62 if m.stackFront(w) != (stackItem{stackFloat, 7}) {
M internal/core/overview_cascade_test.go +165 -36
@@ -2,6 +2,7 @@ package core
22
33 import (
44 "fmt"
5+ "math"
56 "slices"
67 "testing"
78
@@ -70,7 +71,7 @@ func hiddenOff(p Placement) bool { return p.Hidden && p.Preview == 0 && !p.Focus
7071
7172 // The zoom does not depend on the number of bands.
7273 func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) {
73- if one, three := cascadeWorkspace(3).overviewZoom(), cascadeWorkspace(7).overviewZoom(); one != three || one != overviewMaxZoom {
74+ if one, three := cascadeWorkspace(3).overviewZoom(horizontalAxis), cascadeWorkspace(7).overviewZoom(horizontalAxis); one != three || one != overviewMaxZoom {
7475 t.Fatalf("zoom with one band %v, with three %v", one, three)
7576 }
7677 }
@@ -216,31 +217,40 @@ func TestOverviewCascadeNeighborBelowScrollShowsOneBand(t *testing.T) {
216217 }
217218 }
218219
219-// The stash pile is inside the current slot, left of the band; the
220-// previous workspace stays clear of it.
220+// The stash pile is centred on the current row, straddling the bottom edge
221+// of the selected lane, over the tiles; it takes no room in the slot.
221222 func TestOverviewCascadeStashInSlot(t *testing.T) {
222223 m := cascadeOverview()
224+ m.ToggleOverview()
225+ bare, _ := m.overviewRows()
226+ m.CancelOverview()
223227 withStash(m, 2)
224228 m.ToggleOverview()
225229 rows, _ := m.overviewRows()
226- cur, prev := rows[m.Current()], rows[m.Workspaces[0]]
230+ w := m.Current()
231+ cur, prev := rows[w], rows[m.Workspaces[0]]
232+ if cur != bare[w] {
233+ t.Fatalf("the pile changed the slot: %v, without it %v", cur, bare[w])
234+ }
227235 ps := m.Layout()
228- left := cur.X + cur.W
229- for _, id := range []WindowID{7, 8, 9} {
230- left = min(left, previewOf(t, ps, id).Rect.X)
236+ lane := previewOf(t, ps, 8).Rect
237+ // 41 is the front card of the pile, 40 behind it.
238+ front := previewOf(t, ps, 41)
239+ if front.Hidden || front.Peek || front.Preview != overviewCardZoom {
240+ t.Fatalf("front card: %+v", front)
231241 }
232- for _, id := range []WindowID{40, 41} {
233- p := previewOf(t, ps, id)
234- if p.Hidden || p.Preview != overviewCardZoom {
235- t.Fatalf("pile card %d: %+v", id, p)
236- }
237- if p.Rect.X < cur.X || p.Rect.X+p.Rect.W > left {
238- t.Fatalf("pile card %d not inside the slot, left of the band (%d..%d): %+v", id, cur.X, left, p.Rect)
239- }
240- // 41 is the front card of the pile.
241- if id == 41 && !within1(centreY(p.Rect), centreY(previewOf(t, ps, 8).Rect)) {
242- t.Fatalf("pile not level with the selected lane: %+v", p.Rect)
243- }
242+ if back := previewOf(t, ps, 40); back.Hidden || !back.Peek {
243+ t.Fatalf("card behind: %+v", back)
244+ }
245+ if !within1(front.Rect.X+front.Rect.W/2, cur.X+cur.W/2) {
246+ t.Fatalf("pile not centred on the row %v: %+v", cur, front.Rect)
247+ }
248+ // The lane is centred in the slot; its tiles stop a gap short of its
249+ // bottom edge.
250+ rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(horizontalAxis)))
251+ bottom := cur.Y + (cur.H-rowH)/2 + rowH
252+ if !within1(centreY(front.Rect), bottom) || front.Rect.Y >= lane.Y+lane.H {
253+ t.Fatalf("pile not across the lane's bottom edge %d (lane %v): %+v", bottom, lane, front.Rect)
244254 }
245255 // The pile does not shift the band: its row stays centred on the area.
246256 u := m.Current().overviewArea()
@@ -248,11 +258,6 @@ func TestOverviewCascadeStashInSlot(t *testing.T) {
248258 if mid := (lo.X + hi.X + hi.W) / 2; !within1(mid, u.X+u.W/2) {
249259 t.Fatalf("band centre %d with a stash, area centre %d", mid, u.X+u.W/2)
250260 }
251- for _, id := range []WindowID{1, 2, 3} {
252- if p := previewOf(t, ps, id); !p.Hidden && p.Rect.X+p.Rect.W > cur.X {
253- t.Fatalf("previous workspace overlaps the slot with the pile: %+v", p.Rect)
254- }
255- }
256261 if prev.X+prev.W > cur.X {
257262 t.Fatalf("slots overlap: %v %v", prev, cur)
258263 }
@@ -334,21 +339,32 @@ func TestOverviewCascadeKeys(t *testing.T) {
334339 }
335340 m.CancelOverview()
336341
337- // With a stash: left from the first column enters it, right
338- // comes back to the band's first column, left twice goes to
339- // the previous workspace.
342+ // With a stash: left from the first column still goes to the
343+ // previous workspace. Down past the last band enters the
344+ // stash, right comes back to the band's first column, and
345+ // left from the stash goes to the previous workspace.
340346 withStash(m, 2)
341347 w.FocusID(7)
342348 m.ToggleOverview()
343349 d.move(m, -1, 0)
350+ if m.Current() != prev {
351+ t.Fatal("left from the first column with a stash did not reach the previous workspace")
352+ }
353+ m.CancelOverview()
354+ m.ToggleOverview()
355+ d.move(m, 0, 1)
356+ if m.cardAt(w) >= 0 || sel() != 10 {
357+ t.Fatalf("down to the last band: card %d, column %d", m.cardAt(w), sel())
358+ }
359+ d.move(m, 0, 1)
344360 if m.cardAt(w) < 0 {
345- t.Fatal("left from the first column did not select the stash")
361+ t.Fatal("down past the last band did not select the stash")
346362 }
347363 d.move(m, 1, 0)
348- if m.cardAt(w) >= 0 || sel() != 7 {
364+ if m.cardAt(w) >= 0 || sel() != 10 {
349365 t.Fatalf("right from the stash: card %d, column %d", m.cardAt(w), sel())
350366 }
351- d.move(m, -1, 0)
367+ d.move(m, 0, 1)
352368 d.move(m, -1, 0)
353369 if m.Current() != prev {
354370 t.Fatal("left from the stash did not reach the previous workspace")
@@ -369,9 +385,9 @@ func TestOverviewCascadeStashPile(t *testing.T) {
369385 m := cascadeOverview()
370386 withStash(m, 3)
371387 w := m.Current()
372- w.FocusID(7)
388+ w.FocusID(10)
373389 m.ToggleOverview()
374- d.move(m, -1, 0)
390+ d.move(m, 0, 1)
375391 if m.cardAt(w) != w.stashAt {
376392 t.Fatalf("entered at %d", m.cardAt(w))
377393 }
@@ -467,6 +483,23 @@ func TestOverviewCascadeThreeFinger(t *testing.T) {
467483 }
468484 }
469485
486+// Three fingers down past the last band enter the stash below it.
487+func TestOverviewCascadeThreeFingerDownEntersStash(t *testing.T) {
488+ c, m := cascadeCore(t)
489+ withStash(m, 2)
490+ w := m.Current()
491+ w.FocusID(6)
492+ m.ToggleOverview()
493+ c.swipeBegin(ports.SwipeBegin{Fingers: 3})
494+ for range 4 {
495+ c.swipeUpdate(ports.SwipeUpdate{DY: 40})
496+ }
497+ c.swipeEnd(ports.SwipeEnd{})
498+ if m.Current() != w || m.cardAt(w) != w.stashAt {
499+ t.Fatalf("swipe down from the last band: card %d, want %d", m.cardAt(w), w.stashAt)
500+ }
501+}
502+
470503 // Right from a stash card lands on the maximized column, which keeps its
471504 // maximization: it is alone in its band.
472505 func TestOverviewCascadeStashToMaximized(t *testing.T) {
@@ -505,6 +538,102 @@ func TestOverviewCascadeNamedDivider(t *testing.T) {
505538 }
506539 }
507540
541+// A card behind the front one of the pile is clicked where it shows,
542+// over the selected lane's tiles.
543+func TestOverviewCascadeStashPeekClick(t *testing.T) {
544+ m := cascadeOverview()
545+ withStash(m, 2)
546+ m.ToggleOverview()
547+ ps := m.Layout()
548+ front, back := previewOf(t, ps, 41).Rect, previewOf(t, ps, 40).Rect
549+ // The strip of 40 above the front card, inside the lane's tiles.
550+ x, y := float64(back.X+back.W-1), float64(back.Y)
551+ if back.Y >= front.Y || back.Y < previewOf(t, ps, 8).Rect.Y {
552+ t.Fatalf("setup: back %v front %v", back, front)
553+ }
554+ if id := overviewIn(ps, x, y); id == 40 {
555+ t.Fatal("setup: the tiles do not cover the strip")
556+ }
557+ if id := m.overviewHit(ps, x, y); id != 40 {
558+ t.Fatalf("clicked %d, want 40", id)
559+ }
560+ if id := m.overviewHit(ps, float64(front.X+front.W/2), float64(front.Y+front.H/2)); id != 41 {
561+ t.Fatalf("front card clicked %d, want 41", id)
562+ }
563+}
564+
565+// The pile stays inside the area, down to a tiny output.
566+func TestOverviewCascadeStashInsideArea(t *testing.T) {
567+ for _, size := range [][2]int{{300, 200}, {1920, 1080}, {1080, 1920}, {3440, 1440}, {60, 40}} {
568+ m := cascadeOverview()
569+ m.SetOutput(size[0], size[1])
570+ withStash(m, 4)
571+ m.ToggleOverview()
572+ u := m.Current().overviewArea()
573+ for _, id := range []WindowID{40, 41, 42, 43} {
574+ p := previewOf(t, m.Layout(), id)
575+ if !p.Hidden && (p.Rect.Y < u.Y || p.Rect.Y+p.Rect.H > u.Y+u.H || p.Rect.X < u.X || p.Rect.X+p.Rect.W > u.X+u.W) {
576+ t.Fatalf("%v: card %d %v outside %v", size, id, p.Rect, u)
577+ }
578+ }
579+ }
580+}
581+
582+// A stash takes no room in a cascade overview, on a neighbor either: a
583+// wide scroll workspace beside it keeps its zoom and slot.
584+func TestOverviewCascadeNeighborStashKeepsSlot(t *testing.T) {
585+ m := cascadeOverview()
586+ prev := m.Workspaces[0]
587+ m.Focus(0)
588+ for id := WindowID(60); id < 64; id++ {
589+ m.AddWindow(id)
590+ }
591+ m.Focus(1)
592+ m.ToggleOverview()
593+ bare, _ := m.overviewRows()
594+ zoom := prev.overviewZoom(horizontalAxis)
595+ m.CancelOverview()
596+ if zoom <= overviewMinZoom || zoom >= overviewMaxZoom {
597+ t.Fatalf("setup: neighbor zoom %v is at a bound", zoom)
598+ }
599+ // The same columns, with one more window stashed.
600+ m.Focus(0)
601+ m.AddWindow(64)
602+ prev.FocusID(64)
603+ prev.ToggleWindowStash()
604+ m.Focus(1)
605+ m.ToggleOverview()
606+ rows, _ := m.overviewRows()
607+ if rows[prev] != bare[prev] || prev.overviewZoom(horizontalAxis) != zoom {
608+ t.Fatalf("neighbor slot %v zoom %v with a stash, %v %v without", rows[prev], prev.overviewZoom(horizontalAxis), bare[prev], zoom)
609+ }
610+ if p := previewOf(t, m.Layout(), 64); p.Hidden || !p.Peek {
611+ t.Fatalf("neighbor pile card: %+v", p)
612+ }
613+}
614+
615+// Down from a covering float enters the stash; right brings the columns
616+// back to the front.
617+func TestOverviewCascadeFloatStashRight(t *testing.T) {
618+ m := cascadeOverview()
619+ withStash(m, 1)
620+ w := m.Current()
621+ w.AddFloating(90, 300, 200)
622+ w.FocusID(90)
623+ m.ToggleOverview()
624+ if f := m.stackFront(w); f.kind != stackFloat {
625+ t.Fatalf("setup: front %+v", f)
626+ }
627+ m.OverviewMove(0, 1)
628+ if m.cardAt(w) < 0 {
629+ t.Fatal("down from the float did not select the stash")
630+ }
631+ m.OverviewMove(1, 0)
632+ if m.cardAt(w) >= 0 || m.stackFront(w).kind != stackColumns || m.overviewTarget() == 0 {
633+ t.Fatalf("right from the stash: card %d, front %+v, target %d", m.cardAt(w), m.stackFront(w), m.overviewTarget())
634+ }
635+}
636+
508637 // Covering floats fan along the horizontal axis: left goes deeper in the
509638 // stack (the cards behind show on the left), right comes back to the front.
510639 func TestOverviewCascadeCoveringFloat(t *testing.T) {
@@ -553,8 +682,8 @@ func TestOverviewCascadeCoveringFloat(t *testing.T) {
553682 // The cards behind fan to the left: coming to the front, the
554683 // columns move right by one peek step.
555684 front := previewOf(t, m.Layout(), 10)
556- if front.Peek || front.Rect.X-behind.Rect.X != w.peekStep() {
557- t.Fatalf("columns behind %v, in front %v, peek step %d", behind.Rect, front.Rect, w.peekStep())
685+ if front.Peek || front.Rect.X-behind.Rect.X != w.peekStep(horizontalAxis) {
686+ t.Fatalf("columns behind %v, in front %v, peek step %d", behind.Rect, front.Rect, w.peekStep(horizontalAxis))
558687 }
559688 d.move(m, 1, 0)
560689 if f := m.stackFront(w); f.kind != stackFloat || f.id != 90 {
@@ -617,9 +746,9 @@ func TestOverviewCascadeFocusBinds(t *testing.T) {
617746
618747 // A selected stash card keeps the pile cycle.
619748 withStash(m, 3)
620- w.FocusID(7)
749+ w.FocusID(10)
621750 m.ToggleOverview()
622- m.OverviewMove(-1, 0)
751+ m.OverviewMove(0, 1)
623752 at := m.cardAt(w)
624753 m.overviewFocus(ActionFocusWorkspacePrev)
625754 if m.Current() != w || m.cardAt(w) != at-1 {
@@ -2,6 +2,7 @@ package core
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5+ "math"
5 "slices"6 "slices"
6 "testing"7 "testing"
7 8
@@ -70,7 +71,7 @@ func hiddenOff(p Placement) bool { return p.Hidden && p.Preview == 0 && !p.Focus
70 71
71 // The zoom does not depend on the number of bands.72 // The zoom does not depend on the number of bands.
72 func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) {73 func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) {
73- if one, three := cascadeWorkspace(3).overviewZoom(), cascadeWorkspace(7).overviewZoom(); one != three || one != overviewMaxZoom {74+ if one, three := cascadeWorkspace(3).overviewZoom(horizontalAxis), cascadeWorkspace(7).overviewZoom(horizontalAxis); one != three || one != overviewMaxZoom {
74 t.Fatalf("zoom with one band %v, with three %v", one, three)75 t.Fatalf("zoom with one band %v, with three %v", one, three)
75 }76 }
76 }77 }
@@ -216,31 +217,40 @@ func TestOverviewCascadeNeighborBelowScrollShowsOneBand(t *testing.T) {
216 }217 }
217 }218 }
218 219
219-// The stash pile is inside the current slot, left of the band; the220+// The stash pile is centred on the current row, straddling the bottom edge
220-// previous workspace stays clear of it.221+// of the selected lane, over the tiles; it takes no room in the slot.
221 func TestOverviewCascadeStashInSlot(t *testing.T) {222 func TestOverviewCascadeStashInSlot(t *testing.T) {
222 m := cascadeOverview()223 m := cascadeOverview()
224+ m.ToggleOverview()
225+ bare, _ := m.overviewRows()
226+ m.CancelOverview()
223 withStash(m, 2)227 withStash(m, 2)
224 m.ToggleOverview()228 m.ToggleOverview()
225 rows, _ := m.overviewRows()229 rows, _ := m.overviewRows()
226- cur, prev := rows[m.Current()], rows[m.Workspaces[0]]230+ w := m.Current()
231+ cur, prev := rows[w], rows[m.Workspaces[0]]
232+ if cur != bare[w] {
233+ t.Fatalf("the pile changed the slot: %v, without it %v", cur, bare[w])
234+ }
227 ps := m.Layout()235 ps := m.Layout()
228- left := cur.X + cur.W236+ lane := previewOf(t, ps, 8).Rect
229- for _, id := range []WindowID{7, 8, 9} {237+ // 41 is the front card of the pile, 40 behind it.
230- left = min(left, previewOf(t, ps, id).Rect.X)238+ front := previewOf(t, ps, 41)
239+ if front.Hidden || front.Peek || front.Preview != overviewCardZoom {
240+ t.Fatalf("front card: %+v", front)
231 }241 }
232- for _, id := range []WindowID{40, 41} {242+ if back := previewOf(t, ps, 40); back.Hidden || !back.Peek {
233- p := previewOf(t, ps, id)243+ t.Fatalf("card behind: %+v", back)
234- if p.Hidden || p.Preview != overviewCardZoom {244+ }
235- t.Fatalf("pile card %d: %+v", id, p)245+ if !within1(front.Rect.X+front.Rect.W/2, cur.X+cur.W/2) {
236- }246+ t.Fatalf("pile not centred on the row %v: %+v", cur, front.Rect)
237- if p.Rect.X < cur.X || p.Rect.X+p.Rect.W > left {247+ }
238- t.Fatalf("pile card %d not inside the slot, left of the band (%d..%d): %+v", id, cur.X, left, p.Rect)248+ // The lane is centred in the slot; its tiles stop a gap short of its
239- }249+ // bottom edge.
240- // 41 is the front card of the pile.250+ rowH := int(math.Round(float64(w.Usable.H) * w.overviewZoom(horizontalAxis)))
241- if id == 41 && !within1(centreY(p.Rect), centreY(previewOf(t, ps, 8).Rect)) {251+ bottom := cur.Y + (cur.H-rowH)/2 + rowH
242- t.Fatalf("pile not level with the selected lane: %+v", p.Rect)252+ if !within1(centreY(front.Rect), bottom) || front.Rect.Y >= lane.Y+lane.H {
243- }253+ t.Fatalf("pile not across the lane's bottom edge %d (lane %v): %+v", bottom, lane, front.Rect)
244 }254 }
245 // The pile does not shift the band: its row stays centred on the area.255 // The pile does not shift the band: its row stays centred on the area.
246 u := m.Current().overviewArea()256 u := m.Current().overviewArea()
@@ -248,11 +258,6 @@ func TestOverviewCascadeStashInSlot(t *testing.T) {
248 if mid := (lo.X + hi.X + hi.W) / 2; !within1(mid, u.X+u.W/2) {258 if mid := (lo.X + hi.X + hi.W) / 2; !within1(mid, u.X+u.W/2) {
249 t.Fatalf("band centre %d with a stash, area centre %d", mid, u.X+u.W/2)259 t.Fatalf("band centre %d with a stash, area centre %d", mid, u.X+u.W/2)
250 }260 }
251- for _, id := range []WindowID{1, 2, 3} {
252- if p := previewOf(t, ps, id); !p.Hidden && p.Rect.X+p.Rect.W > cur.X {
253- t.Fatalf("previous workspace overlaps the slot with the pile: %+v", p.Rect)
254- }
255- }
256 if prev.X+prev.W > cur.X {261 if prev.X+prev.W > cur.X {
257 t.Fatalf("slots overlap: %v %v", prev, cur)262 t.Fatalf("slots overlap: %v %v", prev, cur)
258 }263 }
@@ -334,21 +339,32 @@ func TestOverviewCascadeKeys(t *testing.T) {
334 }339 }
335 m.CancelOverview()340 m.CancelOverview()
336 341
337- // With a stash: left from the first column enters it, right342+ // With a stash: left from the first column still goes to the
338- // comes back to the band's first column, left twice goes to343+ // previous workspace. Down past the last band enters the
339- // the previous workspace.344+ // stash, right comes back to the band's first column, and
345+ // left from the stash goes to the previous workspace.
340 withStash(m, 2)346 withStash(m, 2)
341 w.FocusID(7)347 w.FocusID(7)
342 m.ToggleOverview()348 m.ToggleOverview()
343 d.move(m, -1, 0)349 d.move(m, -1, 0)
350+ if m.Current() != prev {
351+ t.Fatal("left from the first column with a stash did not reach the previous workspace")
352+ }
353+ m.CancelOverview()
354+ m.ToggleOverview()
355+ d.move(m, 0, 1)
356+ if m.cardAt(w) >= 0 || sel() != 10 {
357+ t.Fatalf("down to the last band: card %d, column %d", m.cardAt(w), sel())
358+ }
359+ d.move(m, 0, 1)
344 if m.cardAt(w) < 0 {360 if m.cardAt(w) < 0 {
345- t.Fatal("left from the first column did not select the stash")361+ t.Fatal("down past the last band did not select the stash")
346 }362 }
347 d.move(m, 1, 0)363 d.move(m, 1, 0)
348- if m.cardAt(w) >= 0 || sel() != 7 {364+ if m.cardAt(w) >= 0 || sel() != 10 {
349 t.Fatalf("right from the stash: card %d, column %d", m.cardAt(w), sel())365 t.Fatalf("right from the stash: card %d, column %d", m.cardAt(w), sel())
350 }366 }
351- d.move(m, -1, 0)367+ d.move(m, 0, 1)
352 d.move(m, -1, 0)368 d.move(m, -1, 0)
353 if m.Current() != prev {369 if m.Current() != prev {
354 t.Fatal("left from the stash did not reach the previous workspace")370 t.Fatal("left from the stash did not reach the previous workspace")
@@ -369,9 +385,9 @@ func TestOverviewCascadeStashPile(t *testing.T) {
369 m := cascadeOverview()385 m := cascadeOverview()
370 withStash(m, 3)386 withStash(m, 3)
371 w := m.Current()387 w := m.Current()
372- w.FocusID(7)388+ w.FocusID(10)
373 m.ToggleOverview()389 m.ToggleOverview()
374- d.move(m, -1, 0)390+ d.move(m, 0, 1)
375 if m.cardAt(w) != w.stashAt {391 if m.cardAt(w) != w.stashAt {
376 t.Fatalf("entered at %d", m.cardAt(w))392 t.Fatalf("entered at %d", m.cardAt(w))
377 }393 }
@@ -467,6 +483,23 @@ func TestOverviewCascadeThreeFinger(t *testing.T) {
467 }483 }
468 }484 }
469 485
486+// Three fingers down past the last band enter the stash below it.
487+func TestOverviewCascadeThreeFingerDownEntersStash(t *testing.T) {
488+ c, m := cascadeCore(t)
489+ withStash(m, 2)
490+ w := m.Current()
491+ w.FocusID(6)
492+ m.ToggleOverview()
493+ c.swipeBegin(ports.SwipeBegin{Fingers: 3})
494+ for range 4 {
495+ c.swipeUpdate(ports.SwipeUpdate{DY: 40})
496+ }
497+ c.swipeEnd(ports.SwipeEnd{})
498+ if m.Current() != w || m.cardAt(w) != w.stashAt {
499+ t.Fatalf("swipe down from the last band: card %d, want %d", m.cardAt(w), w.stashAt)
500+ }
501+}
502+
470 // Right from a stash card lands on the maximized column, which keeps its503 // Right from a stash card lands on the maximized column, which keeps its
471 // maximization: it is alone in its band.504 // maximization: it is alone in its band.
472 func TestOverviewCascadeStashToMaximized(t *testing.T) {505 func TestOverviewCascadeStashToMaximized(t *testing.T) {
@@ -505,6 +538,102 @@ func TestOverviewCascadeNamedDivider(t *testing.T) {
505 }538 }
506 }539 }
507 540
541+// A card behind the front one of the pile is clicked where it shows,
542+// over the selected lane's tiles.
543+func TestOverviewCascadeStashPeekClick(t *testing.T) {
544+ m := cascadeOverview()
545+ withStash(m, 2)
546+ m.ToggleOverview()
547+ ps := m.Layout()
548+ front, back := previewOf(t, ps, 41).Rect, previewOf(t, ps, 40).Rect
549+ // The strip of 40 above the front card, inside the lane's tiles.
550+ x, y := float64(back.X+back.W-1), float64(back.Y)
551+ if back.Y >= front.Y || back.Y < previewOf(t, ps, 8).Rect.Y {
552+ t.Fatalf("setup: back %v front %v", back, front)
553+ }
554+ if id := overviewIn(ps, x, y); id == 40 {
555+ t.Fatal("setup: the tiles do not cover the strip")
556+ }
557+ if id := m.overviewHit(ps, x, y); id != 40 {
558+ t.Fatalf("clicked %d, want 40", id)
559+ }
560+ if id := m.overviewHit(ps, float64(front.X+front.W/2), float64(front.Y+front.H/2)); id != 41 {
561+ t.Fatalf("front card clicked %d, want 41", id)
562+ }
563+}
564+
565+// The pile stays inside the area, down to a tiny output.
566+func TestOverviewCascadeStashInsideArea(t *testing.T) {
567+ for _, size := range [][2]int{{300, 200}, {1920, 1080}, {1080, 1920}, {3440, 1440}, {60, 40}} {
568+ m := cascadeOverview()
569+ m.SetOutput(size[0], size[1])
570+ withStash(m, 4)
571+ m.ToggleOverview()
572+ u := m.Current().overviewArea()
573+ for _, id := range []WindowID{40, 41, 42, 43} {
574+ p := previewOf(t, m.Layout(), id)
575+ if !p.Hidden && (p.Rect.Y < u.Y || p.Rect.Y+p.Rect.H > u.Y+u.H || p.Rect.X < u.X || p.Rect.X+p.Rect.W > u.X+u.W) {
576+ t.Fatalf("%v: card %d %v outside %v", size, id, p.Rect, u)
577+ }
578+ }
579+ }
580+}
581+
582+// A stash takes no room in a cascade overview, on a neighbor either: a
583+// wide scroll workspace beside it keeps its zoom and slot.
584+func TestOverviewCascadeNeighborStashKeepsSlot(t *testing.T) {
585+ m := cascadeOverview()
586+ prev := m.Workspaces[0]
587+ m.Focus(0)
588+ for id := WindowID(60); id < 64; id++ {
589+ m.AddWindow(id)
590+ }
591+ m.Focus(1)
592+ m.ToggleOverview()
593+ bare, _ := m.overviewRows()
594+ zoom := prev.overviewZoom(horizontalAxis)
595+ m.CancelOverview()
596+ if zoom <= overviewMinZoom || zoom >= overviewMaxZoom {
597+ t.Fatalf("setup: neighbor zoom %v is at a bound", zoom)
598+ }
599+ // The same columns, with one more window stashed.
600+ m.Focus(0)
601+ m.AddWindow(64)
602+ prev.FocusID(64)
603+ prev.ToggleWindowStash()
604+ m.Focus(1)
605+ m.ToggleOverview()
606+ rows, _ := m.overviewRows()
607+ if rows[prev] != bare[prev] || prev.overviewZoom(horizontalAxis) != zoom {
608+ t.Fatalf("neighbor slot %v zoom %v with a stash, %v %v without", rows[prev], prev.overviewZoom(horizontalAxis), bare[prev], zoom)
609+ }
610+ if p := previewOf(t, m.Layout(), 64); p.Hidden || !p.Peek {
611+ t.Fatalf("neighbor pile card: %+v", p)
612+ }
613+}
614+
615+// Down from a covering float enters the stash; right brings the columns
616+// back to the front.
617+func TestOverviewCascadeFloatStashRight(t *testing.T) {
618+ m := cascadeOverview()
619+ withStash(m, 1)
620+ w := m.Current()
621+ w.AddFloating(90, 300, 200)
622+ w.FocusID(90)
623+ m.ToggleOverview()
624+ if f := m.stackFront(w); f.kind != stackFloat {
625+ t.Fatalf("setup: front %+v", f)
626+ }
627+ m.OverviewMove(0, 1)
628+ if m.cardAt(w) < 0 {
629+ t.Fatal("down from the float did not select the stash")
630+ }
631+ m.OverviewMove(1, 0)
632+ if m.cardAt(w) >= 0 || m.stackFront(w).kind != stackColumns || m.overviewTarget() == 0 {
633+ t.Fatalf("right from the stash: card %d, front %+v, target %d", m.cardAt(w), m.stackFront(w), m.overviewTarget())
634+ }
635+}
636+
508 // Covering floats fan along the horizontal axis: left goes deeper in the637 // Covering floats fan along the horizontal axis: left goes deeper in the
509 // stack (the cards behind show on the left), right comes back to the front.638 // stack (the cards behind show on the left), right comes back to the front.
510 func TestOverviewCascadeCoveringFloat(t *testing.T) {639 func TestOverviewCascadeCoveringFloat(t *testing.T) {
@@ -553,8 +682,8 @@ func TestOverviewCascadeCoveringFloat(t *testing.T) {
553 // The cards behind fan to the left: coming to the front, the682 // The cards behind fan to the left: coming to the front, the
554 // columns move right by one peek step.683 // columns move right by one peek step.
555 front := previewOf(t, m.Layout(), 10)684 front := previewOf(t, m.Layout(), 10)
556- if front.Peek || front.Rect.X-behind.Rect.X != w.peekStep() {685+ if front.Peek || front.Rect.X-behind.Rect.X != w.peekStep(horizontalAxis) {
557- t.Fatalf("columns behind %v, in front %v, peek step %d", behind.Rect, front.Rect, w.peekStep())686+ t.Fatalf("columns behind %v, in front %v, peek step %d", behind.Rect, front.Rect, w.peekStep(horizontalAxis))
558 }687 }
559 d.move(m, 1, 0)688 d.move(m, 1, 0)
560 if f := m.stackFront(w); f.kind != stackFloat || f.id != 90 {689 if f := m.stackFront(w); f.kind != stackFloat || f.id != 90 {
@@ -617,9 +746,9 @@ func TestOverviewCascadeFocusBinds(t *testing.T) {
617 746
618 // A selected stash card keeps the pile cycle.747 // A selected stash card keeps the pile cycle.
619 withStash(m, 3)748 withStash(m, 3)
620- w.FocusID(7)749+ w.FocusID(10)
621 m.ToggleOverview()750 m.ToggleOverview()
622- m.OverviewMove(-1, 0)751+ m.OverviewMove(0, 1)
623 at := m.cardAt(w)752 at := m.cardAt(w)
624 m.overviewFocus(ActionFocusWorkspacePrev)753 m.overviewFocus(ActionFocusWorkspacePrev)
625 if m.Current() != w || m.cardAt(w) != at-1 {754 if m.Current() != w || m.cardAt(w) != at-1 {
M internal/core/overview_stack.go +1 -1
@@ -354,7 +354,7 @@ func (m *Monitor) stackRow(w *Workspace, axis layoutAxis, slot Rect, dim, lit bo
354354 at = slices.Index(items, m.stackFront(w))
355355 }
356356 cards := make([][]Placement, len(items))
357- step := w.peekStep()
357+ step := w.peekStep(axis)
358358 // along is the offset of d steps on the overview axis.
359359 along := func(d int) (int, int) {
360360 var r Rect
@@ -354,7 +354,7 @@ func (m *Monitor) stackRow(w *Workspace, axis layoutAxis, slot Rect, dim, lit bo
354 at = slices.Index(items, m.stackFront(w))354 at = slices.Index(items, m.stackFront(w))
355 }355 }
356 cards := make([][]Placement, len(items))356 cards := make([][]Placement, len(items))
357- step := w.peekStep()357+ step := w.peekStep(axis)
358 // along is the offset of d steps on the overview axis.358 // along is the offset of d steps on the overview axis.
359 along := func(d int) (int, int) {359 along := func(d int) (int, int) {
360 var r Rect360 var r Rect

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 23

Tags 10

↑↓ moveenter openesc close