detached 553cc10

fix(overview): keep a maximized cascade column at full size beside its cards

The cards a maximized cascade column hides now peek out past the band, as in
a fixed workspace, instead of narrowing the column: its preview keeps the
workspace's aspect ratio and its buffer is no longer cropped.
commit
553cc10919e33244450d3faabce7b68b798a7da6
author
brice · · 2026-10-08 12:14 UTC
parent
4fd770d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +24 -23
M internal/core/overview.go +14 -15
@@ -978,7 +978,11 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
978978 rects = w.colBuf
979979 span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile.
980980 }
981+ // off is the room, left of the band, for the cards a maximized
982+ // cascade column hides (the same room is kept on its right).
983+ off := 0
981984 if w.hasBandCards() {
985+ off = 2 * w.bandCardStep()
982986 // The columns the maximized one hides are cards behind it,
983987 // farthest first so the nearest paint over them.
984988 first, last := w.laneBounds(w.laneOf(w.Focus))
@@ -998,18 +1002,13 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
9981002 continue // A card, above.
9991003 }
10001004 // The band is the lane: it is encoded in Y, one screen apart.
1001- // A maximized column fills its band as on screen, inset for
1002- // the cards it hides.
1003- r.X = w.cellX(i)
1005+ // A maximized column fills its band as on screen; the cards
1006+ // it hides peek out on either side, past the band.
1007+ r.X = off + w.cellX(i)
10041008 r.Y = g + w.band(i)*w.Usable.H
10051009 r.W = w.cellWidth(i)
10061010 if c.FullWidth {
1007- in := 0
1008- if i == w.Focus && w.hasBandCards() {
1009- // Only the focused column has cards behind it.
1010- in = 2 * w.bandCardStep()
1011- }
1012- r.X, r.W = g+in, max(w.Usable.W-2*g-2*in, 0)
1011+ r.X, r.W = off+g, max(w.Usable.W-2*g, 0)
10131012 }
10141013 } else if w.Overflow == OverflowFixed {
10151014 r = rects[i]
@@ -1042,7 +1041,7 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
10421041 }
10431042 }
10441043 if w.policy().wraps && len(w.Columns) > 0 {
1045- span = w.Usable.W
1044+ span = w.Usable.W + 2*off
10461045 }
10471046 return tiles, span, sel
10481047 }
@@ -1055,8 +1054,7 @@ func (w *Workspace) bandCardStep() int {
10551054
10561055 // hasBandCards reports whether the focused column of a cascade is
10571056 // maximized and hides other columns of its band: the overview shows them
1058-// as cards behind it, the maximized column inset by two card steps on
1059-// each side.
1057+// as cards behind it, peeking out up to two card steps on each side.
10601058 func (w *Workspace) hasBandCards() bool {
10611059 if !w.policy().wraps || w.Focus < 0 || w.Focus >= len(w.Columns) || !w.Columns[w.Focus].FullWidth {
10621060 return false
@@ -1067,8 +1065,9 @@ func (w *Workspace) hasBandCards() bool {
10671065
10681066 // appendBandCard appends hidden column i of a cascade band as a dimmed card
10691067 // at its cell size behind the maximized column, on its side: the nearest
1070-// column one step out from the maximized one, the next ones two steps out
1071-// (on the band's edge), like the cards of a stack.
1068+// column one step out from the maximized one, the next ones two steps out,
1069+// like the cards of a stack. The maximized column keeps its full size two
1070+// steps right of the row's start (see previewTilesInto).
10721071 func (w *Workspace) appendBandCard(tiles []Placement, i int) []Placement {
10731072 g, s, c := w.gap(), w.bandCardStep(), w.Columns[i]
10741073 r := Rect{Y: g + w.band(i)*w.Usable.H, W: w.cellWidth(i), H: max(w.Usable.H-2*g, 0)}
@@ -1076,7 +1075,7 @@ func (w *Workspace) appendBandCard(tiles []Placement, i int) []Placement {
10761075 if i < w.Focus {
10771076 r.X = g + (2-k)*s
10781077 } else {
1079- r.X = w.Usable.W - g - (2-k)*s - r.W
1078+ r.X = w.Usable.W - g + (2+k)*s - r.W
10801079 }
10811080 w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g)
10821081 for j, t := range w.rowBuf {
@@ -978,7 +978,11 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
978 rects = w.colBuf978 rects = w.colBuf
979 span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile.979 span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile.
980 }980 }
981+ // off is the room, left of the band, for the cards a maximized
982+ // cascade column hides (the same room is kept on its right).
983+ off := 0
981 if w.hasBandCards() {984 if w.hasBandCards() {
985+ off = 2 * w.bandCardStep()
982 // The columns the maximized one hides are cards behind it,986 // The columns the maximized one hides are cards behind it,
983 // farthest first so the nearest paint over them.987 // farthest first so the nearest paint over them.
984 first, last := w.laneBounds(w.laneOf(w.Focus))988 first, last := w.laneBounds(w.laneOf(w.Focus))
@@ -998,18 +1002,13 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
998 continue // A card, above.1002 continue // A card, above.
999 }1003 }
1000 // The band is the lane: it is encoded in Y, one screen apart.1004 // The band is the lane: it is encoded in Y, one screen apart.
1001- // A maximized column fills its band as on screen, inset for1005+ // A maximized column fills its band as on screen; the cards
1002- // the cards it hides.1006+ // it hides peek out on either side, past the band.
1003- r.X = w.cellX(i)1007+ r.X = off + w.cellX(i)
1004 r.Y = g + w.band(i)*w.Usable.H1008 r.Y = g + w.band(i)*w.Usable.H
1005 r.W = w.cellWidth(i)1009 r.W = w.cellWidth(i)
1006 if c.FullWidth {1010 if c.FullWidth {
1007- in := 01011+ r.X, r.W = off+g, max(w.Usable.W-2*g, 0)
1008- if i == w.Focus && w.hasBandCards() {
1009- // Only the focused column has cards behind it.
1010- in = 2 * w.bandCardStep()
1011- }
1012- r.X, r.W = g+in, max(w.Usable.W-2*g-2*in, 0)
1013 }1012 }
1014 } else if w.Overflow == OverflowFixed {1013 } else if w.Overflow == OverflowFixed {
1015 r = rects[i]1014 r = rects[i]
@@ -1042,7 +1041,7 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect)
1042 }1041 }
1043 }1042 }
1044 if w.policy().wraps && len(w.Columns) > 0 {1043 if w.policy().wraps && len(w.Columns) > 0 {
1045- span = w.Usable.W1044+ span = w.Usable.W + 2*off
1046 }1045 }
1047 return tiles, span, sel1046 return tiles, span, sel
1048 }1047 }
@@ -1055,8 +1054,7 @@ func (w *Workspace) bandCardStep() int {
1055 1054
1056 // hasBandCards reports whether the focused column of a cascade is1055 // hasBandCards reports whether the focused column of a cascade is
1057 // maximized and hides other columns of its band: the overview shows them1056 // maximized and hides other columns of its band: the overview shows them
1058-// as cards behind it, the maximized column inset by two card steps on1057+// as cards behind it, peeking out up to two card steps on each side.
1059-// each side.
1060 func (w *Workspace) hasBandCards() bool {1058 func (w *Workspace) hasBandCards() bool {
1061 if !w.policy().wraps || w.Focus < 0 || w.Focus >= len(w.Columns) || !w.Columns[w.Focus].FullWidth {1059 if !w.policy().wraps || w.Focus < 0 || w.Focus >= len(w.Columns) || !w.Columns[w.Focus].FullWidth {
1062 return false1060 return false
@@ -1067,8 +1065,9 @@ func (w *Workspace) hasBandCards() bool {
1067 1065
1068 // appendBandCard appends hidden column i of a cascade band as a dimmed card1066 // appendBandCard appends hidden column i of a cascade band as a dimmed card
1069 // at its cell size behind the maximized column, on its side: the nearest1067 // at its cell size behind the maximized column, on its side: the nearest
1070-// column one step out from the maximized one, the next ones two steps out1068+// column one step out from the maximized one, the next ones two steps out,
1071-// (on the band's edge), like the cards of a stack.1069+// like the cards of a stack. The maximized column keeps its full size two
1070+// steps right of the row's start (see previewTilesInto).
1072 func (w *Workspace) appendBandCard(tiles []Placement, i int) []Placement {1071 func (w *Workspace) appendBandCard(tiles []Placement, i int) []Placement {
1073 g, s, c := w.gap(), w.bandCardStep(), w.Columns[i]1072 g, s, c := w.gap(), w.bandCardStep(), w.Columns[i]
1074 r := Rect{Y: g + w.band(i)*w.Usable.H, W: w.cellWidth(i), H: max(w.Usable.H-2*g, 0)}1073 r := Rect{Y: g + w.band(i)*w.Usable.H, W: w.cellWidth(i), H: max(w.Usable.H-2*g, 0)}
@@ -1076,7 +1075,7 @@ func (w *Workspace) appendBandCard(tiles []Placement, i int) []Placement {
1076 if i < w.Focus {1075 if i < w.Focus {
1077 r.X = g + (2-k)*s1076 r.X = g + (2-k)*s
1078 } else {1077 } else {
1079- r.X = w.Usable.W - g - (2-k)*s - r.W1078+ r.X = w.Usable.W - g + (2+k)*s - r.W
1080 }1079 }
1081 w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g)1080 w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g)
1082 for j, t := range w.rowBuf {1081 for j, t := range w.rowBuf {
M internal/core/overview_cascade_test.go +10 -8
@@ -99,9 +99,9 @@ func TestOverviewCascadeMaximizedColumnCards(t *testing.T) {
9999 w.maximize(tc.max)
100100 tiles, _, sel := w.previewTiles()
101101 g, h, s := w.gap(), w.Usable.H-2*w.gap(), w.bandCardStep()
102- in := 0
102+ off := 0
103103 if slices.ContainsFunc(tc.cards, func(k int) bool { return k != 0 }) {
104- in = 2 * s
104+ off = 2 * s
105105 }
106106 var order []int
107107 for _, p := range tiles {
@@ -110,14 +110,15 @@ func TestOverviewCascadeMaximizedColumnCards(t *testing.T) {
110110 order = append(order, i)
111111 }
112112 band := i / 3
113- want := Rect{X: w.cellX(i), Y: g + band*w.Usable.H, W: w.cellWidth(i), H: h}
113+ want := Rect{X: off + w.cellX(i), Y: g + band*w.Usable.H, W: w.cellWidth(i), H: h}
114114 switch k := tc.cards[i]; {
115115 case i == tc.max:
116- want.X, want.W = g+in, w.Usable.W-2*g-2*in
116+ // Full size, as on screen: the workspace's ratio.
117+ want.X, want.W = off+g, w.Usable.W-2*g
117118 case k < 0:
118119 want.X = g + (2+k)*s
119120 case k > 0:
120- want.X = w.Usable.W - g - (2-k)*s - want.W
121+ want.X = w.Usable.W - g + (2+k)*s - want.W
121122 }
122123 if p.Hidden || p.Peek != (tc.cards[i] != 0) || p.Focused != (i == tc.max) || p.Rect != want {
123124 t.Fatalf("n=%d max=%d column %d: %+v, want rect %+v card %v", tc.n, tc.max, i, p, want, tc.cards[i] != 0)
@@ -206,10 +207,11 @@ func TestOverviewCascadeUnfocusedMaximizedNotInset(t *testing.T) {
206207 w.maximize(4)
207208 w.Columns[0].FullWidth = true
208209 tiles, _, _ := w.previewTiles()
209- g := w.gap()
210+ // The row keeps room for the focused column's cards on its left.
211+ g, off := w.gap(), 2*w.bandCardStep()
210212 p := previewOf(t, tiles, 1)
211- if p.Hidden || p.Rect.X != g || p.Rect.W != w.Usable.W-2*g {
212- t.Fatalf("unfocused maximized column %+v, want X %d W %d", p, g, w.Usable.W-2*g)
213+ if p.Hidden || p.Rect.X != off+g || p.Rect.W != w.Usable.W-2*g {
214+ t.Fatalf("unfocused maximized column %+v, want X %d W %d", p, off+g, w.Usable.W-2*g)
213215 }
214216 }
215217
@@ -99,9 +99,9 @@ func TestOverviewCascadeMaximizedColumnCards(t *testing.T) {
99 w.maximize(tc.max)99 w.maximize(tc.max)
100 tiles, _, sel := w.previewTiles()100 tiles, _, sel := w.previewTiles()
101 g, h, s := w.gap(), w.Usable.H-2*w.gap(), w.bandCardStep()101 g, h, s := w.gap(), w.Usable.H-2*w.gap(), w.bandCardStep()
102- in := 0102+ off := 0
103 if slices.ContainsFunc(tc.cards, func(k int) bool { return k != 0 }) {103 if slices.ContainsFunc(tc.cards, func(k int) bool { return k != 0 }) {
104- in = 2 * s104+ off = 2 * s
105 }105 }
106 var order []int106 var order []int
107 for _, p := range tiles {107 for _, p := range tiles {
@@ -110,14 +110,15 @@ func TestOverviewCascadeMaximizedColumnCards(t *testing.T) {
110 order = append(order, i)110 order = append(order, i)
111 }111 }
112 band := i / 3112 band := i / 3
113- want := Rect{X: w.cellX(i), Y: g + band*w.Usable.H, W: w.cellWidth(i), H: h}113+ want := Rect{X: off + w.cellX(i), Y: g + band*w.Usable.H, W: w.cellWidth(i), H: h}
114 switch k := tc.cards[i]; {114 switch k := tc.cards[i]; {
115 case i == tc.max:115 case i == tc.max:
116- want.X, want.W = g+in, w.Usable.W-2*g-2*in116+ // Full size, as on screen: the workspace's ratio.
117+ want.X, want.W = off+g, w.Usable.W-2*g
117 case k < 0:118 case k < 0:
118 want.X = g + (2+k)*s119 want.X = g + (2+k)*s
119 case k > 0:120 case k > 0:
120- want.X = w.Usable.W - g - (2-k)*s - want.W121+ want.X = w.Usable.W - g + (2+k)*s - want.W
121 }122 }
122 if p.Hidden || p.Peek != (tc.cards[i] != 0) || p.Focused != (i == tc.max) || p.Rect != want {123 if p.Hidden || p.Peek != (tc.cards[i] != 0) || p.Focused != (i == tc.max) || p.Rect != want {
123 t.Fatalf("n=%d max=%d column %d: %+v, want rect %+v card %v", tc.n, tc.max, i, p, want, tc.cards[i] != 0)124 t.Fatalf("n=%d max=%d column %d: %+v, want rect %+v card %v", tc.n, tc.max, i, p, want, tc.cards[i] != 0)
@@ -206,10 +207,11 @@ func TestOverviewCascadeUnfocusedMaximizedNotInset(t *testing.T) {
206 w.maximize(4)207 w.maximize(4)
207 w.Columns[0].FullWidth = true208 w.Columns[0].FullWidth = true
208 tiles, _, _ := w.previewTiles()209 tiles, _, _ := w.previewTiles()
209- g := w.gap()210+ // The row keeps room for the focused column's cards on its left.
211+ g, off := w.gap(), 2*w.bandCardStep()
210 p := previewOf(t, tiles, 1)212 p := previewOf(t, tiles, 1)
211- if p.Hidden || p.Rect.X != g || p.Rect.W != w.Usable.W-2*g {213+ if p.Hidden || p.Rect.X != off+g || p.Rect.W != w.Usable.W-2*g {
212- t.Fatalf("unfocused maximized column %+v, want X %d W %d", p, g, w.Usable.W-2*g)214+ t.Fatalf("unfocused maximized column %+v, want X %d W %d", p, off+g, w.Usable.W-2*g)
213 }215 }
214 }216 }
215 217

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