detached 6648883

fix(overview): fade only the carousel cards that cross the seam and clamp long stash-only carousels

commit
6648883e76386bd3bef0a4be61d6d99f0c1d99f0
author
brice · · 2026-10-16 12:42 UTC
parent
a82be5b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +144 -76
M internal/core/overview.go +26 -23
@@ -402,9 +402,10 @@ func (m *Monitor) OverviewMove(dx, dy int) {
402402 }
403403
404404 // overviewWorkspaceStep is a focus-workspace bind. A vertical overview
405-// walks the stack cards before changing workspace (overviewVertical), not
406-// the unfolded stash, which wraps round. A side-by-side one always changes
407-// workspace, since its stack and unfolded stash run left and right.
405+// walks the stack cards before changing workspace (overviewVertical); from
406+// a selected stash card, which the move keys walk, it changes workspace
407+// directly. A side-by-side one always changes workspace, since its stack
408+// and unfolded stash run left and right.
408409 func (m *Monitor) overviewWorkspaceStep(d int) {
409410 if m.overviewAxis() == verticalAxis && m.cardAt(m.Current()) < 0 {
410411 m.overviewVertical(d)
@@ -414,16 +415,14 @@ func (m *Monitor) overviewWorkspaceStep(d int) {
414415 }
415416
416417 // cycleCard selects the stash card d entries from the selected one of w in
417-// the unfolded pile, wrapping round (wrapsCards), else stopping at either
418+// the unfolded pile, wrapping round (wrapsCards), else staying at either
418419 // end. It reports whether a card was selected.
419420 func (m *Monitor) cycleCard(w *Workspace, d int) bool {
420421 at := m.cardAt(w)
421422 if at < 0 {
422423 return false
423424 }
424- if !m.stepCard(w, at, d) {
425- m.selectCard(w, min(max(at+d, 0), len(w.Stash)-1))
426- }
425+ m.stepCard(w, at, d)
427426 return true
428427 }
429428
@@ -510,10 +509,8 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) {
510509 // side by side (cascade): a column inside the selected lane, or a card of
511510 // the unfolded stash, which wraps round (stepCard). Past the first column
512511 // or a card that does not wrap, it goes to the previous workspace; past
513-// the last, to the next workspace.
514-// Covering floats fan
515-// along this axis and are traversed before a workspace: the card on the
516-// left is the one behind.
512+// the last, to the next workspace. Covering floats fan along this axis and
513+// are traversed before a workspace: the card on the left is the one behind.
517514 func (m *Monitor) overviewAcross(dx int) {
518515 w := m.Current()
519516 if at := m.cardAt(w); at >= 0 {
@@ -949,9 +946,11 @@ func (w *Workspace) pile(dst []Placement, axis layoutAxis, slot Rect, dim bool,
949946 // vertical one. The selected card at sits on the pile, in the middle; the
950947 // cards before it come on the left (above), the ones after it on the right
951948 // (below), in stash order, wrapping round (wrapsCards) with the oldest
952-// following the newest (carouselSlot). A wider gap marks that seam. When the carousel fits the
953-// overview area it is shifted into it; else the cards past the area are
954-// hidden. Cards other than at are dimmed. The cards are appended to dst.
949+// following the newest (carouselSlot). A wider gap marks that seam. A
950+// carousel that fits the overview area is shifted into it. Longer, one
951+// that wraps keeps the selection in the middle; one that does not stops at
952+// the area's edges, like a wide row. The cards past the area are hidden,
953+// the ones other than at dimmed. The cards are appended to dst.
955954 func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement {
956955 n := len(w.Stash)
957956 u := w.overviewArea()
@@ -963,8 +962,7 @@ func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int
963962 // The extra room at the seam, on top of the stride.
964963 gap := size / 4
965964 wraps := w.wrapsCards()
966- pos := func(i int) int {
967- s := carouselSlot(i, at, n, wraps)
965+ pos := func(s int) int {
968966 p := origin + s*stride
969967 switch {
970968 case s > 0 && at+s >= n:
@@ -974,20 +972,25 @@ func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int
974972 }
975973 return p
976974 }
977- first, last := pos(0), pos(n-1)
975+ // The slots of the ends.
976+ from, to := -at, n-1-at
978977 if wraps {
979- first, last = pos((at+n-n/2)%n), pos((at+(n-1)/2)%n)
978+ from, to = -(n / 2), (n-1)/2
980979 }
981- shift := 0
982- if total := last + size - first; total <= room {
980+ first, total := pos(from), pos(to)+size-pos(from)
981+ var shift int
982+ switch {
983+ case total <= room:
983984 shift = min(max(first, lo), lo+room-total) - first
985+ case !wraps:
986+ shift = min(max(first, lo+room-total), lo) - first
984987 }
985988 for i := range w.Stash {
986989 r := folded
987- if axis == verticalAxis {
988- r.Y = pos(i) + shift
990+ if p := pos(carouselSlot(i, at, n, wraps)) + shift; axis == verticalAxis {
991+ r.Y = p
989992 } else {
990- r.X = pos(i) + shift
993+ r.X = p
991994 }
992995 dst = w.unfoldedCard(dst, i, r, at, u)
993996 }
@@ -402,9 +402,10 @@ func (m *Monitor) OverviewMove(dx, dy int) {
402 }402 }
403 403
404 // overviewWorkspaceStep is a focus-workspace bind. A vertical overview404 // overviewWorkspaceStep is a focus-workspace bind. A vertical overview
405-// walks the stack cards before changing workspace (overviewVertical), not405+// walks the stack cards before changing workspace (overviewVertical); from
406-// the unfolded stash, which wraps round. A side-by-side one always changes406+// a selected stash card, which the move keys walk, it changes workspace
407-// workspace, since its stack and unfolded stash run left and right.407+// directly. A side-by-side one always changes workspace, since its stack
408+// and unfolded stash run left and right.
408 func (m *Monitor) overviewWorkspaceStep(d int) {409 func (m *Monitor) overviewWorkspaceStep(d int) {
409 if m.overviewAxis() == verticalAxis && m.cardAt(m.Current()) < 0 {410 if m.overviewAxis() == verticalAxis && m.cardAt(m.Current()) < 0 {
410 m.overviewVertical(d)411 m.overviewVertical(d)
@@ -414,16 +415,14 @@ func (m *Monitor) overviewWorkspaceStep(d int) {
414 }415 }
415 416
416 // cycleCard selects the stash card d entries from the selected one of w in417 // cycleCard selects the stash card d entries from the selected one of w in
417-// the unfolded pile, wrapping round (wrapsCards), else stopping at either418+// the unfolded pile, wrapping round (wrapsCards), else staying at either
418 // end. It reports whether a card was selected.419 // end. It reports whether a card was selected.
419 func (m *Monitor) cycleCard(w *Workspace, d int) bool {420 func (m *Monitor) cycleCard(w *Workspace, d int) bool {
420 at := m.cardAt(w)421 at := m.cardAt(w)
421 if at < 0 {422 if at < 0 {
422 return false423 return false
423 }424 }
424- if !m.stepCard(w, at, d) {425+ m.stepCard(w, at, d)
425- m.selectCard(w, min(max(at+d, 0), len(w.Stash)-1))
426- }
427 return true426 return true
428 }427 }
429 428
@@ -510,10 +509,8 @@ func (m *Monitor) selectLaneColumn(w *Workspace, i int) {
510 // side by side (cascade): a column inside the selected lane, or a card of509 // side by side (cascade): a column inside the selected lane, or a card of
511 // the unfolded stash, which wraps round (stepCard). Past the first column510 // the unfolded stash, which wraps round (stepCard). Past the first column
512 // or a card that does not wrap, it goes to the previous workspace; past511 // or a card that does not wrap, it goes to the previous workspace; past
513-// the last, to the next workspace.512+// the last, to the next workspace. Covering floats fan along this axis and
514-// Covering floats fan513+// are traversed before a workspace: the card on the left is the one behind.
515-// along this axis and are traversed before a workspace: the card on the
516-// left is the one behind.
517 func (m *Monitor) overviewAcross(dx int) {514 func (m *Monitor) overviewAcross(dx int) {
518 w := m.Current()515 w := m.Current()
519 if at := m.cardAt(w); at >= 0 {516 if at := m.cardAt(w); at >= 0 {
@@ -949,9 +946,11 @@ func (w *Workspace) pile(dst []Placement, axis layoutAxis, slot Rect, dim bool,
949 // vertical one. The selected card at sits on the pile, in the middle; the946 // vertical one. The selected card at sits on the pile, in the middle; the
950 // cards before it come on the left (above), the ones after it on the right947 // cards before it come on the left (above), the ones after it on the right
951 // (below), in stash order, wrapping round (wrapsCards) with the oldest948 // (below), in stash order, wrapping round (wrapsCards) with the oldest
952-// following the newest (carouselSlot). A wider gap marks that seam. When the carousel fits the949+// following the newest (carouselSlot). A wider gap marks that seam. A
953-// overview area it is shifted into it; else the cards past the area are950+// carousel that fits the overview area is shifted into it. Longer, one
954-// hidden. Cards other than at are dimmed. The cards are appended to dst.951+// that wraps keeps the selection in the middle; one that does not stops at
952+// the area's edges, like a wide row. The cards past the area are hidden,
953+// the ones other than at dimmed. The cards are appended to dst.
955 func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement {954 func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int) []Placement {
956 n := len(w.Stash)955 n := len(w.Stash)
957 u := w.overviewArea()956 u := w.overviewArea()
@@ -963,8 +962,7 @@ func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int
963 // The extra room at the seam, on top of the stride.962 // The extra room at the seam, on top of the stride.
964 gap := size / 4963 gap := size / 4
965 wraps := w.wrapsCards()964 wraps := w.wrapsCards()
966- pos := func(i int) int {965+ pos := func(s int) int {
967- s := carouselSlot(i, at, n, wraps)
968 p := origin + s*stride966 p := origin + s*stride
969 switch {967 switch {
970 case s > 0 && at+s >= n:968 case s > 0 && at+s >= n:
@@ -974,20 +972,25 @@ func (w *Workspace) unfold(dst []Placement, axis layoutAxis, folded Rect, at int
974 }972 }
975 return p973 return p
976 }974 }
977- first, last := pos(0), pos(n-1)975+ // The slots of the ends.
976+ from, to := -at, n-1-at
978 if wraps {977 if wraps {
979- first, last = pos((at+n-n/2)%n), pos((at+(n-1)/2)%n)978+ from, to = -(n / 2), (n-1)/2
980 }979 }
981- shift := 0980+ first, total := pos(from), pos(to)+size-pos(from)
982- if total := last + size - first; total <= room {981+ var shift int
982+ switch {
983+ case total <= room:
983 shift = min(max(first, lo), lo+room-total) - first984 shift = min(max(first, lo), lo+room-total) - first
985+ case !wraps:
986+ shift = min(max(first, lo+room-total), lo) - first
984 }987 }
985 for i := range w.Stash {988 for i := range w.Stash {
986 r := folded989 r := folded
987- if axis == verticalAxis {990+ if p := pos(carouselSlot(i, at, n, wraps)) + shift; axis == verticalAxis {
988- r.Y = pos(i) + shift991+ r.Y = p
989 } else {992 } else {
990- r.X = pos(i) + shift993+ r.X = p
991 }994 }
992 dst = w.unfoldedCard(dst, i, r, at, u)995 dst = w.unfoldedCard(dst, i, r, at, u)
993 }996 }
M internal/core/overview_stash_input_test.go +23 -15
@@ -74,21 +74,29 @@ func TestOverviewStashInputNavigation(t *testing.T) {
7474 }
7575
7676 // In a vertical overview, a focus-workspace bind over the unfolded stash
77-// changes workspace: the carousel wraps round, so walking it would never
78-// get there.
77+// changes workspace, in its direction, wrapping round or not: the move
78+// keys walk the cards.
7979 func TestOverviewWorkspaceBindLeavesCarousel(t *testing.T) {
80- m := pileMonitor()
81- w := m.Current()
82- m.Focus(1)
83- m.AddWindow(30)
84- m.Focus(0)
85- m.ToggleOverview()
86- m.OverviewMove(-1, 0)
87- if m.overviewAxis() != verticalAxis || m.cardAt(w) < 0 {
88- t.Fatal("setup: no card selected in a vertical overview")
89- }
90- m.overviewFocus(ActionFocusWorkspaceNext)
91- if m.Current() == w {
92- t.Fatal("focus-workspace-next over a card stayed on the workspace")
80+ for _, n := range []int{2, 3} {
81+ m := overviewMonitor()
82+ w := m.Current()
83+ for id := WindowID(5); id < WindowID(5+n); id++ {
84+ w.AddWindow(id)
85+ w.FocusID(id)
86+ w.ToggleWindowStash()
87+ }
88+ w.FocusID(1)
89+ m.Focus(1)
90+ m.AddWindow(30)
91+ m.Focus(0)
92+ m.ToggleOverview()
93+ m.OverviewMove(-1, 0)
94+ if m.overviewAxis() != verticalAxis || m.cardAt(w) < 0 {
95+ t.Fatalf("%d cards, setup: no card selected in a vertical overview", n)
96+ }
97+ m.overviewFocus(ActionFocusWorkspaceNext)
98+ if m.Current() != m.Workspaces[1] || m.cardAt(w) >= 0 {
99+ t.Fatalf("%d cards: focus-workspace-next went to workspace %d, card %d", n, indexOf(m.Workspaces, m.Current()), m.cardAt(w))
100+ }
93101 }
94102 }
@@ -74,21 +74,29 @@ func TestOverviewStashInputNavigation(t *testing.T) {
74 }74 }
75 75
76 // In a vertical overview, a focus-workspace bind over the unfolded stash76 // In a vertical overview, a focus-workspace bind over the unfolded stash
77-// changes workspace: the carousel wraps round, so walking it would never77+// changes workspace, in its direction, wrapping round or not: the move
78-// get there.78+// keys walk the cards.
79 func TestOverviewWorkspaceBindLeavesCarousel(t *testing.T) {79 func TestOverviewWorkspaceBindLeavesCarousel(t *testing.T) {
80- m := pileMonitor()80+ for _, n := range []int{2, 3} {
81- w := m.Current()81+ m := overviewMonitor()
82- m.Focus(1)82+ w := m.Current()
83- m.AddWindow(30)83+ for id := WindowID(5); id < WindowID(5+n); id++ {
84- m.Focus(0)84+ w.AddWindow(id)
85- m.ToggleOverview()85+ w.FocusID(id)
86- m.OverviewMove(-1, 0)86+ w.ToggleWindowStash()
87- if m.overviewAxis() != verticalAxis || m.cardAt(w) < 0 {87+ }
88- t.Fatal("setup: no card selected in a vertical overview")88+ w.FocusID(1)
89- }89+ m.Focus(1)
90- m.overviewFocus(ActionFocusWorkspaceNext)90+ m.AddWindow(30)
91- if m.Current() == w {91+ m.Focus(0)
92- t.Fatal("focus-workspace-next over a card stayed on the workspace")92+ m.ToggleOverview()
93+ m.OverviewMove(-1, 0)
94+ if m.overviewAxis() != verticalAxis || m.cardAt(w) < 0 {
95+ t.Fatalf("%d cards, setup: no card selected in a vertical overview", n)
96+ }
97+ m.overviewFocus(ActionFocusWorkspaceNext)
98+ if m.Current() != m.Workspaces[1] || m.cardAt(w) >= 0 {
99+ t.Fatalf("%d cards: focus-workspace-next went to workspace %d, card %d", n, indexOf(m.Workspaces, m.Current()), m.cardAt(w))
100+ }
93 }101 }
94 }102 }
M internal/core/overview_unfold_test.go +31 -0
@@ -328,3 +328,34 @@ func TestOverviewStashOnlyCarousel(t *testing.T) {
328328 })
329329 }
330330 }
331+
332+// A carousel that does not wrap round and is longer than the area stops at
333+// its edges, like a wide row: the first card at the start of the area, the
334+// selection inside it.
335+func TestOverviewStashOnlyCarouselScrolls(t *testing.T) {
336+ m := overviewMonitor()
337+ m.Focus(1)
338+ w := m.Current()
339+ w.Overflow = OverflowCascade
340+ w.ToggleWindowStash() // its own window
341+ for id := WindowID(40); id < 52; id++ {
342+ w.AddWindow(id)
343+ w.FocusID(id)
344+ w.ToggleWindowStash()
345+ }
346+ m.ToggleOverview()
347+ if w.wrapsCards() || m.cardAt(w) < 0 {
348+ t.Fatalf("setup: wraps %v, card %d", w.wrapsCards(), m.cardAt(w))
349+ }
350+ u := w.overviewArea()
351+ m.selectCard(w, 0)
352+ ps := m.Layout()
353+ if r := previewOf(t, ps, w.Stash[0].ID).Rect; r.X != u.X {
354+ t.Fatalf("first card at %d, area starts at %d", r.X, u.X)
355+ }
356+ m.selectCard(w, len(w.Stash)-1)
357+ ps = m.Layout()
358+ if r := previewOf(t, ps, w.Stash[len(w.Stash)-1].ID).Rect; r.X+r.W != u.X+u.W {
359+ t.Fatalf("last card ends at %d, area at %d", r.X+r.W, u.X+u.W)
360+ }
361+}
@@ -328,3 +328,34 @@ func TestOverviewStashOnlyCarousel(t *testing.T) {
328 })328 })
329 }329 }
330 }330 }
331+
332+// A carousel that does not wrap round and is longer than the area stops at
333+// its edges, like a wide row: the first card at the start of the area, the
334+// selection inside it.
335+func TestOverviewStashOnlyCarouselScrolls(t *testing.T) {
336+ m := overviewMonitor()
337+ m.Focus(1)
338+ w := m.Current()
339+ w.Overflow = OverflowCascade
340+ w.ToggleWindowStash() // its own window
341+ for id := WindowID(40); id < 52; id++ {
342+ w.AddWindow(id)
343+ w.FocusID(id)
344+ w.ToggleWindowStash()
345+ }
346+ m.ToggleOverview()
347+ if w.wrapsCards() || m.cardAt(w) < 0 {
348+ t.Fatalf("setup: wraps %v, card %d", w.wrapsCards(), m.cardAt(w))
349+ }
350+ u := w.overviewArea()
351+ m.selectCard(w, 0)
352+ ps := m.Layout()
353+ if r := previewOf(t, ps, w.Stash[0].ID).Rect; r.X != u.X {
354+ t.Fatalf("first card at %d, area starts at %d", r.X, u.X)
355+ }
356+ m.selectCard(w, len(w.Stash)-1)
357+ ps = m.Layout()
358+ if r := previewOf(t, ps, w.Stash[len(w.Stash)-1].ID).Rect; r.X+r.W != u.X+u.W {
359+ t.Fatalf("last card ends at %d, area at %d", r.X+r.W, u.X+u.W)
360+ }
361+}
M internal/core/transition.go +14 -10
@@ -46,8 +46,10 @@ type viewShot struct {
4646 stashAt, stashLen int
4747 stashPos, stashV float64
4848 // card is the selected card of the unfolded stash carousel in the
49- // overview (Workspace.unfold), -1 without one.
50- card int
49+ // overview (Workspace.unfold), -1 without one; wraps is whether that
50+ // carousel wrapped round (Workspace.wrapsCards).
51+ card int
52+ wraps bool
5153 // list and pos are the numbered workspaces and the monitor's fractional
5254 // position in them (index of the current one plus switchView.off); ok is
5355 // false when there is none (a shown stash workspace).
@@ -640,7 +642,7 @@ func (c *Core) snapshot(now time.Time) []viewShot {
640642 _, s.switchV = m.switchView.motion.sampleAt(now)
641643 }
642644 s.stashShown, s.stashAt, s.stashLen, s.stashPos = w.stashShown(), w.stashAt, len(w.Stash), float64(w.stashAt)+w.stashView.off
643- s.card = m.cardAt(w)
645+ s.card, s.wraps = m.cardAt(w), w.wrapsCards()
644646 if w.stashView.motion.on {
645647 _, s.stashV = w.stashView.motion.sampleAt(now)
646648 }
@@ -912,13 +914,14 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) {
912914 at = carouselStep(b)
913915 }
914916 w := sc.mon.Current()
917+ n := len(w.Stash)
915918 for _, p := range sc.mon.Layout() {
916919 if p.Hidden {
917920 continue
918921 }
919922 rm := sc.rects[p.ID]
920923 i := slices.IndexFunc(b.rects, func(r rectShot) bool { return r.id == p.ID })
921- if i >= 0 && at >= 0 && wrapped(w.stashIndex(p.ID), b.card, at, len(w.Stash), w.wrapsCards()) {
924+ if i >= 0 && at >= 0 && wrapped(w.stashIndex(p.ID), b.card, at, n, b.wraps) {
922925 // The card went round the carousel to its other end: it
923926 // fades in there instead of crossing the row.
924927 rm, i = rectMotion{}, -1
@@ -949,22 +952,23 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) {
949952 }
950953
951954 // carouselStep is the selected card of the stash carousel b's screen shows
952-// now, when it showed the same carousel in b too (the same workspace and
953-// stash size); else -1.
955+// now, when it showed the same carousel in b too (the same workspace, stash
956+// size and wrapping); else -1.
954957 func carouselStep(b *viewShot) int {
955958 m := b.sc.mon
956959 w := m.Current()
957- if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen {
960+ if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen || w.wrapsCards() != b.wraps {
958961 return -1
959962 }
960963 return m.cardAt(w)
961964 }
962965
963966 // wrapped reports whether stash card i of a carousel of n cards went round
964-// it, from one end to the other: it moved more than one place
965-// (carouselSlot) when the selection went from card from to card to.
967+// it, from one end to the other, when the selection went from card from to
968+// card to: it moved other than the cards that stayed on their side, which
969+// all shift by the old selection's move (carouselSlot).
966970 func wrapped(i, from, to, n int, wraps bool) bool {
967- return i >= 0 && abs(carouselSlot(i, to, n, wraps)-carouselSlot(i, from, n, wraps)) > 1
971+ return i >= 0 && carouselSlot(i, to, n, wraps)-carouselSlot(i, from, n, wraps) != carouselSlot(from, to, n, wraps)
968972 }
969973
970974 // movedFrom is the shot of the other screen that showed the workspace b's
@@ -46,8 +46,10 @@ type viewShot struct {
46 stashAt, stashLen int46 stashAt, stashLen int
47 stashPos, stashV float6447 stashPos, stashV float64
48 // card is the selected card of the unfolded stash carousel in the48 // card is the selected card of the unfolded stash carousel in the
49- // overview (Workspace.unfold), -1 without one.49+ // overview (Workspace.unfold), -1 without one; wraps is whether that
50- card int50+ // carousel wrapped round (Workspace.wrapsCards).
51+ card int
52+ wraps bool
51 // list and pos are the numbered workspaces and the monitor's fractional53 // list and pos are the numbered workspaces and the monitor's fractional
52 // position in them (index of the current one plus switchView.off); ok is54 // position in them (index of the current one plus switchView.off); ok is
53 // false when there is none (a shown stash workspace).55 // false when there is none (a shown stash workspace).
@@ -640,7 +642,7 @@ func (c *Core) snapshot(now time.Time) []viewShot {
640 _, s.switchV = m.switchView.motion.sampleAt(now)642 _, s.switchV = m.switchView.motion.sampleAt(now)
641 }643 }
642 s.stashShown, s.stashAt, s.stashLen, s.stashPos = w.stashShown(), w.stashAt, len(w.Stash), float64(w.stashAt)+w.stashView.off644 s.stashShown, s.stashAt, s.stashLen, s.stashPos = w.stashShown(), w.stashAt, len(w.Stash), float64(w.stashAt)+w.stashView.off
643- s.card = m.cardAt(w)645+ s.card, s.wraps = m.cardAt(w), w.wrapsCards()
644 if w.stashView.motion.on {646 if w.stashView.motion.on {
645 _, s.stashV = w.stashView.motion.sampleAt(now)647 _, s.stashV = w.stashView.motion.sampleAt(now)
646 }648 }
@@ -912,13 +914,14 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) {
912 at = carouselStep(b)914 at = carouselStep(b)
913 }915 }
914 w := sc.mon.Current()916 w := sc.mon.Current()
917+ n := len(w.Stash)
915 for _, p := range sc.mon.Layout() {918 for _, p := range sc.mon.Layout() {
916 if p.Hidden {919 if p.Hidden {
917 continue920 continue
918 }921 }
919 rm := sc.rects[p.ID]922 rm := sc.rects[p.ID]
920 i := slices.IndexFunc(b.rects, func(r rectShot) bool { return r.id == p.ID })923 i := slices.IndexFunc(b.rects, func(r rectShot) bool { return r.id == p.ID })
921- if i >= 0 && at >= 0 && wrapped(w.stashIndex(p.ID), b.card, at, len(w.Stash), w.wrapsCards()) {924+ if i >= 0 && at >= 0 && wrapped(w.stashIndex(p.ID), b.card, at, n, b.wraps) {
922 // The card went round the carousel to its other end: it925 // The card went round the carousel to its other end: it
923 // fades in there instead of crossing the row.926 // fades in there instead of crossing the row.
924 rm, i = rectMotion{}, -1927 rm, i = rectMotion{}, -1
@@ -949,22 +952,23 @@ func (c *Core) transitionOverview(b *viewShot, now time.Time) {
949 }952 }
950 953
951 // carouselStep is the selected card of the stash carousel b's screen shows954 // carouselStep is the selected card of the stash carousel b's screen shows
952-// now, when it showed the same carousel in b too (the same workspace and955+// now, when it showed the same carousel in b too (the same workspace, stash
953-// stash size); else -1.956+// size and wrapping); else -1.
954 func carouselStep(b *viewShot) int {957 func carouselStep(b *viewShot) int {
955 m := b.sc.mon958 m := b.sc.mon
956 w := m.Current()959 w := m.Current()
957- if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen {960+ if b.card < 0 || w != b.ws || len(w.Stash) != b.stashLen || w.wrapsCards() != b.wraps {
958 return -1961 return -1
959 }962 }
960 return m.cardAt(w)963 return m.cardAt(w)
961 }964 }
962 965
963 // wrapped reports whether stash card i of a carousel of n cards went round966 // wrapped reports whether stash card i of a carousel of n cards went round
964-// it, from one end to the other: it moved more than one place967+// it, from one end to the other, when the selection went from card from to
965-// (carouselSlot) when the selection went from card from to card to.968+// card to: it moved other than the cards that stayed on their side, which
969+// all shift by the old selection's move (carouselSlot).
966 func wrapped(i, from, to, n int, wraps bool) bool {970 func wrapped(i, from, to, n int, wraps bool) bool {
967- return i >= 0 && abs(carouselSlot(i, to, n, wraps)-carouselSlot(i, from, n, wraps)) > 1971+ return i >= 0 && carouselSlot(i, to, n, wraps)-carouselSlot(i, from, n, wraps) != carouselSlot(from, to, n, wraps)
968 }972 }
969 973
970 // movedFrom is the shot of the other screen that showed the workspace b's974 // movedFrom is the shot of the other screen that showed the workspace b's
M internal/core/transition_stash_internal_test.go +50 -28
@@ -474,35 +474,57 @@ func TestWithLeavingOrderIsDeterministic(t *testing.T) {
474474 }
475475 }
476476
477-// Walking the stash carousel slides its cards one place; the card that goes
478-// round to the other end fades in there instead of crossing the row.
477+// Walking the stash carousel slides its cards; a card that goes round to
478+// the other end fades in there instead of crossing the row, whatever the
479+// direction, the count or the size of the step (a fast wheel). A carousel
480+// that does not wrap round slides every card.
479481 func TestCarouselWrapFadesIn(t *testing.T) {
480- c, ic, sc, _ := stashRig(t, true)
481- m := sc.mon
482- w := m.Current()
483- w.Overflow = OverflowCascade
484- c.applyAction(ActionToggleOverview)
485- settleShown(t, c)
486- m.selectCard(w, 0)
487- settleShown(t, c)
488- shots := c.snapshot(ic.now)
489- m.OverviewMove(1, 0)
490- c.transition(shots, ic.now)
491- if m.cardAt(w) != 1 {
492- t.Fatalf("setup: card %d", m.cardAt(w))
493- }
494- // Three cards: the selection moved from 0 to 1, card 2 went round
495- // from the right end to the left one.
496- for i, e := range w.Stash {
497- rm := sc.rects[e.ID]
498- if i == 2 {
499- if rm.dx != 0 || !rm.fade.on {
500- t.Fatalf("wrapped card: dx %v, fade %v", rm.dx, rm.fade.on)
482+ for _, tc := range []struct {
483+ name string
484+ n, from, to int
485+ wrapped []int
486+ }{
487+ {"right", 3, 0, 1, []int{2}},
488+ {"left", 3, 0, 2, []int{1}},
489+ {"even", 4, 3, 0, []int{1}},
490+ {"jump", 5, 0, 2, []int{3, 4}},
491+ {"two cards", 2, 0, 1, nil},
492+ } {
493+ t.Run(tc.name, func(t *testing.T) {
494+ c, ic, _ := fadeCore(t)
495+ c.cfg.Animations.On = true
496+ sc := c.cur()
497+ m := sc.mon
498+ w := m.Current()
499+ w.Overflow = OverflowCascade
500+ m.AddWindow(1)
501+ for id := WindowID(2); id < WindowID(2+tc.n); id++ {
502+ m.AddWindow(id)
503+ w.FocusID(id)
504+ c.applyAction(ActionToggleWindowStash)
501505 }
502- continue
503- }
504- if rm.dx == 0 || rm.fade.on {
505- t.Fatalf("card %d: dx %v, fade %v", i, rm.dx, rm.fade.on)
506- }
506+ w.FocusID(1)
507+ c.applyAction(ActionToggleOverview)
508+ m.selectCard(w, tc.from)
509+ settleShown(t, c)
510+ shots := c.snapshot(ic.now)
511+ m.selectCard(w, tc.to)
512+ c.transition(shots, ic.now)
513+ for i, e := range w.Stash {
514+ if previewOf(t, m.Layout(), e.ID).Hidden {
515+ continue
516+ }
517+ rm := sc.rects[e.ID]
518+ if slices.Contains(tc.wrapped, i) {
519+ if rm.dx != 0 || !rm.fade.on {
520+ t.Fatalf("wrapped card %d: dx %v, fade %v", i, rm.dx, rm.fade.on)
521+ }
522+ continue
523+ }
524+ if rm.dx == 0 || rm.fade.on {
525+ t.Fatalf("card %d: dx %v, fade %v", i, rm.dx, rm.fade.on)
526+ }
527+ }
528+ })
507529 }
508530 }
@@ -474,35 +474,57 @@ func TestWithLeavingOrderIsDeterministic(t *testing.T) {
474 }474 }
475 }475 }
476 476
477-// Walking the stash carousel slides its cards one place; the card that goes477+// Walking the stash carousel slides its cards; a card that goes round to
478-// round to the other end fades in there instead of crossing the row.478+// the other end fades in there instead of crossing the row, whatever the
479+// direction, the count or the size of the step (a fast wheel). A carousel
480+// that does not wrap round slides every card.
479 func TestCarouselWrapFadesIn(t *testing.T) {481 func TestCarouselWrapFadesIn(t *testing.T) {
480- c, ic, sc, _ := stashRig(t, true)482+ for _, tc := range []struct {
481- m := sc.mon483+ name string
482- w := m.Current()484+ n, from, to int
483- w.Overflow = OverflowCascade485+ wrapped []int
484- c.applyAction(ActionToggleOverview)486+ }{
485- settleShown(t, c)487+ {"right", 3, 0, 1, []int{2}},
486- m.selectCard(w, 0)488+ {"left", 3, 0, 2, []int{1}},
487- settleShown(t, c)489+ {"even", 4, 3, 0, []int{1}},
488- shots := c.snapshot(ic.now)490+ {"jump", 5, 0, 2, []int{3, 4}},
489- m.OverviewMove(1, 0)491+ {"two cards", 2, 0, 1, nil},
490- c.transition(shots, ic.now)492+ } {
491- if m.cardAt(w) != 1 {493+ t.Run(tc.name, func(t *testing.T) {
492- t.Fatalf("setup: card %d", m.cardAt(w))494+ c, ic, _ := fadeCore(t)
493- }495+ c.cfg.Animations.On = true
494- // Three cards: the selection moved from 0 to 1, card 2 went round496+ sc := c.cur()
495- // from the right end to the left one.497+ m := sc.mon
496- for i, e := range w.Stash {498+ w := m.Current()
497- rm := sc.rects[e.ID]499+ w.Overflow = OverflowCascade
498- if i == 2 {500+ m.AddWindow(1)
499- if rm.dx != 0 || !rm.fade.on {501+ for id := WindowID(2); id < WindowID(2+tc.n); id++ {
500- t.Fatalf("wrapped card: dx %v, fade %v", rm.dx, rm.fade.on)502+ m.AddWindow(id)
503+ w.FocusID(id)
504+ c.applyAction(ActionToggleWindowStash)
501 }505 }
502- continue506+ w.FocusID(1)
503- }507+ c.applyAction(ActionToggleOverview)
504- if rm.dx == 0 || rm.fade.on {508+ m.selectCard(w, tc.from)
505- t.Fatalf("card %d: dx %v, fade %v", i, rm.dx, rm.fade.on)509+ settleShown(t, c)
506- }510+ shots := c.snapshot(ic.now)
511+ m.selectCard(w, tc.to)
512+ c.transition(shots, ic.now)
513+ for i, e := range w.Stash {
514+ if previewOf(t, m.Layout(), e.ID).Hidden {
515+ continue
516+ }
517+ rm := sc.rects[e.ID]
518+ if slices.Contains(tc.wrapped, i) {
519+ if rm.dx != 0 || !rm.fade.on {
520+ t.Fatalf("wrapped card %d: dx %v, fade %v", i, rm.dx, rm.fade.on)
521+ }
522+ continue
523+ }
524+ if rm.dx == 0 || rm.fade.on {
525+ t.Fatalf("card %d: dx %v, fade %v", i, rm.dx, rm.fade.on)
526+ }
527+ }
528+ })
507 }529 }
508 }530 }

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