detached 7321c7b

fix(overview): keep cascade card navigation off floats and on the maximized column

commit
7321c7bb5e20fe375b31134f895d4f8cea9a7aff
author
brice · · 2026-10-07 15:54 UTC
parent
90ccc85
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +52 -19
M internal/core/overview.go +20 -19
@@ -450,8 +450,12 @@ func (m *Monitor) overviewAcross(dx int) {
450450 m.ov.card, m.ov.cardOf = 0, nil
451451 m.ov.selected = 0
452452 if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 {
453- first, _ := w.laneBounds(w.laneOf(w.Focus))
454- m.selectLaneColumn(w, first)
453+ // The front column: a maximized one keeps its cards behind.
454+ i := w.Focus
455+ if !w.hasBandCards() {
456+ i, _ = w.laneBounds(w.laneOf(w.Focus))
457+ }
458+ m.selectLaneColumn(w, i)
455459 }
456460 return
457461 }
@@ -972,7 +976,7 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
972976 rects = w.colBuf
973977 span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile.
974978 }
975- if w.policy().wraps && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth {
979+ if w.hasBandCards() {
976980 // The columns the maximized one hides are cards behind it,
977981 // farthest first so the nearest paint over them.
978982 first, last := w.laneBounds(w.laneOf(w.Focus))
@@ -999,9 +1003,9 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
9991003 r.W = w.cellWidth(i)
10001004 if c.FullWidth {
10011005 in := 0
1002- if i == w.Focus {
1006+ if i == w.Focus && w.hasBandCards() {
10031007 // Only the focused column has cards behind it.
1004- in = w.bandCardsInset(i)
1008+ in = 2 * w.bandCardStep()
10051009 }
10061010 r.X, r.W = g+in, max(w.Usable.W-2*g-2*in, 0)
10071011 }
@@ -1044,15 +1048,16 @@ func (w *Workspace) bandCardStep() int {
10441048 return max(1, int(math.Round(float64(w.Usable.H)*overviewPeekStep)))
10451049 }
10461050
1047-// bandCardsInset is how far the maximized column i is inset on each side
1048-// of its band in the overview, leaving room for up to two cards peeking
1049-// out on that side; 0 when it hides nothing.
1050-func (w *Workspace) bandCardsInset(i int) int {
1051- first, last := w.laneBounds(w.laneOf(i))
1052- if first >= last {
1053- return 0
1051+// hasBandCards reports whether the focused column of a cascade is
1052+// maximized and hides other columns of its band: the overview shows them
1053+// as cards behind it, the maximized column inset by two card steps on
1054+// each side.
1055+func (w *Workspace) hasBandCards() bool {
1056+ if !w.policy().wraps || w.Focus < 0 || w.Focus >= len(w.Columns) || !w.Columns[w.Focus].FullWidth {
1057+ return false
10541058 }
1055- return 2 * w.bandCardStep()
1059+ first, last := w.laneBounds(w.laneOf(w.Focus))
1060+ return first < last
10561061 }
10571062
10581063 // appendBandCard appends hidden column i of a cascade band as a dimmed card
@@ -1374,13 +1379,9 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) {
13741379 }
13751380
13761381 // bandCardsAt reports whether the selection of w is a maximized cascade
1377-// column hiding cards of its band, not a stash card.
1382+// column hiding cards of its band: not a stash card nor a covering float.
13781383 func (m *Monitor) bandCardsAt(w *Workspace) bool {
1379- if m.cardAt(w) >= 0 || len(w.Columns) == 0 || !w.Columns[w.Focus].FullWidth {
1380- return false
1381- }
1382- first, last := w.laneBounds(w.laneOf(w.Focus))
1383- return first < last
1384+ return m.cardAt(w) < 0 && w.hasBandCards() && m.stackFront(w).kind == stackColumns
13841385 }
13851386
13861387 // overviewTurned reports whether a scroll step turned the overview from the
@@ -450,8 +450,12 @@ func (m *Monitor) overviewAcross(dx int) {
450 m.ov.card, m.ov.cardOf = 0, nil450 m.ov.card, m.ov.cardOf = 0, nil
451 m.ov.selected = 0451 m.ov.selected = 0
452 if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 {452 if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 {
453- first, _ := w.laneBounds(w.laneOf(w.Focus))453+ // The front column: a maximized one keeps its cards behind.
454- m.selectLaneColumn(w, first)454+ i := w.Focus
455+ if !w.hasBandCards() {
456+ i, _ = w.laneBounds(w.laneOf(w.Focus))
457+ }
458+ m.selectLaneColumn(w, i)
455 }459 }
456 return460 return
457 }461 }
@@ -972,7 +976,7 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
972 rects = w.colBuf976 rects = w.colBuf
973 span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile.977 span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile.
974 }978 }
975- if w.policy().wraps && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth {979+ if w.hasBandCards() {
976 // The columns the maximized one hides are cards behind it,980 // The columns the maximized one hides are cards behind it,
977 // farthest first so the nearest paint over them.981 // farthest first so the nearest paint over them.
978 first, last := w.laneBounds(w.laneOf(w.Focus))982 first, last := w.laneBounds(w.laneOf(w.Focus))
@@ -999,9 +1003,9 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
999 r.W = w.cellWidth(i)1003 r.W = w.cellWidth(i)
1000 if c.FullWidth {1004 if c.FullWidth {
1001 in := 01005 in := 0
1002- if i == w.Focus {1006+ if i == w.Focus && w.hasBandCards() {
1003 // Only the focused column has cards behind it.1007 // Only the focused column has cards behind it.
1004- in = w.bandCardsInset(i)1008+ in = 2 * w.bandCardStep()
1005 }1009 }
1006 r.X, r.W = g+in, max(w.Usable.W-2*g-2*in, 0)1010 r.X, r.W = g+in, max(w.Usable.W-2*g-2*in, 0)
1007 }1011 }
@@ -1044,15 +1048,16 @@ func (w *Workspace) bandCardStep() int {
1044 return max(1, int(math.Round(float64(w.Usable.H)*overviewPeekStep)))1048 return max(1, int(math.Round(float64(w.Usable.H)*overviewPeekStep)))
1045 }1049 }
1046 1050
1047-// bandCardsInset is how far the maximized column i is inset on each side1051+// hasBandCards reports whether the focused column of a cascade is
1048-// of its band in the overview, leaving room for up to two cards peeking1052+// maximized and hides other columns of its band: the overview shows them
1049-// out on that side; 0 when it hides nothing.1053+// as cards behind it, the maximized column inset by two card steps on
1050-func (w *Workspace) bandCardsInset(i int) int {1054+// each side.
1051- first, last := w.laneBounds(w.laneOf(i))1055+func (w *Workspace) hasBandCards() bool {
1052- if first >= last {1056+ if !w.policy().wraps || w.Focus < 0 || w.Focus >= len(w.Columns) || !w.Columns[w.Focus].FullWidth {
1053- return 01057+ return false
1054 }1058 }
1055- return 2 * w.bandCardStep()1059+ first, last := w.laneBounds(w.laneOf(w.Focus))
1060+ return first < last
1056 }1061 }
1057 1062
1058 // appendBandCard appends hidden column i of a cascade band as a dimmed card1063 // appendBandCard appends hidden column i of a cascade band as a dimmed card
@@ -1374,13 +1379,9 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) {
1374 }1379 }
1375 1380
1376 // bandCardsAt reports whether the selection of w is a maximized cascade1381 // bandCardsAt reports whether the selection of w is a maximized cascade
1377-// column hiding cards of its band, not a stash card.1382+// column hiding cards of its band: not a stash card nor a covering float.
1378 func (m *Monitor) bandCardsAt(w *Workspace) bool {1383 func (m *Monitor) bandCardsAt(w *Workspace) bool {
1379- if m.cardAt(w) >= 0 || len(w.Columns) == 0 || !w.Columns[w.Focus].FullWidth {1384+ return m.cardAt(w) < 0 && w.hasBandCards() && m.stackFront(w).kind == stackColumns
1380- return false
1381- }
1382- first, last := w.laneBounds(w.laneOf(w.Focus))
1383- return first < last
1384 }1385 }
1385 1386
1386 // overviewTurned reports whether a scroll step turned the overview from the1387 // overviewTurned reports whether a scroll step turned the overview from the
M internal/core/overview_cascade_test.go +32 -0
@@ -663,6 +663,38 @@ func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) {
663663 if m.Current() != m.Workspaces[2] {
664664 t.Fatalf("swipe right past the band: workspace %d", indexOf(m.Workspaces, m.Current()))
665665 }
666+
667+ // A covering float in front: the swipe changes workspace, as without
668+ // a maximized column.
669+ m.CancelOverview()
670+ w.AddFloating(90, 300, 200)
671+ m.ToggleOverview()
672+ if f := m.stackFront(w); f.kind != stackFloat || !w.Columns[w.Focus].FullWidth {
673+ t.Fatalf("setup: front %+v", f)
674+ }
675+ swipe(-40)
676+ if m.Current() != m.Workspaces[0] {
677+ t.Fatalf("swipe left over a float: workspace %d", indexOf(m.Workspaces, m.Current()))
678+ }
679+}
680+
681+// Right from a stash card lands on the maximized column, which keeps its
682+// maximization, not on the first card of its band.
683+func TestOverviewCascadeStashToMaximized(t *testing.T) {
684+ m := cascadeOverview()
685+ withStash(m, 1)
686+ w := m.Current()
687+ w.FocusID(9)
688+ w.ToggleFullWidth()
689+ m.ToggleOverview()
690+ m.selectCard(w, w.stashAt) // As a click on the pile does.
691+ if m.cardAt(w) < 0 {
692+ t.Fatalf("setup: no stash card, target %d", m.overviewTarget())
693+ }
694+ m.OverviewMove(1, 0)
695+ if m.overviewTarget() != 9 || !w.Columns[w.Focus].FullWidth || len(w.maximized) == 0 || w.maximized[0] != 9 {
696+ t.Fatalf("target %d, maximized %v; want 9", m.overviewTarget(), w.maximized)
697+ }
666698 }
667699
668700 // T8: a named boundary of a cascade overview is a vertical rule strictly
@@ -663,6 +663,38 @@ func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) {
663 if m.Current() != m.Workspaces[2] {663 if m.Current() != m.Workspaces[2] {
664 t.Fatalf("swipe right past the band: workspace %d", indexOf(m.Workspaces, m.Current()))664 t.Fatalf("swipe right past the band: workspace %d", indexOf(m.Workspaces, m.Current()))
665 }665 }
666+
667+ // A covering float in front: the swipe changes workspace, as without
668+ // a maximized column.
669+ m.CancelOverview()
670+ w.AddFloating(90, 300, 200)
671+ m.ToggleOverview()
672+ if f := m.stackFront(w); f.kind != stackFloat || !w.Columns[w.Focus].FullWidth {
673+ t.Fatalf("setup: front %+v", f)
674+ }
675+ swipe(-40)
676+ if m.Current() != m.Workspaces[0] {
677+ t.Fatalf("swipe left over a float: workspace %d", indexOf(m.Workspaces, m.Current()))
678+ }
679+}
680+
681+// Right from a stash card lands on the maximized column, which keeps its
682+// maximization, not on the first card of its band.
683+func TestOverviewCascadeStashToMaximized(t *testing.T) {
684+ m := cascadeOverview()
685+ withStash(m, 1)
686+ w := m.Current()
687+ w.FocusID(9)
688+ w.ToggleFullWidth()
689+ m.ToggleOverview()
690+ m.selectCard(w, w.stashAt) // As a click on the pile does.
691+ if m.cardAt(w) < 0 {
692+ t.Fatalf("setup: no stash card, target %d", m.overviewTarget())
693+ }
694+ m.OverviewMove(1, 0)
695+ if m.overviewTarget() != 9 || !w.Columns[w.Focus].FullWidth || len(w.maximized) == 0 || w.maximized[0] != 9 {
696+ t.Fatalf("target %d, maximized %v; want 9", m.overviewTarget(), w.maximized)
697+ }
666 }698 }
667 699
668 // T8: a named boundary of a cascade overview is a vertical rule strictly700 // T8: a named boundary of a cascade overview is a vertical rule strictly

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