detached d568f15

fix(core): keep a cascade fullscreen tile on its band edge

focus-column-left/right from a fullscreen tile at the edge of its cascade band goes to the neighbor monitor and keeps fullscreen, as from any tile there, instead of crossing bands. Cover maximization, floats and outer bands in the band-move tests.
commit
d568f15f67f3952e46f8288cb79f5564e6a1c27c
author
brice · · 2026-10-10 14:15 UTC
parent
79cb2cc
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +56 -7
M internal/core/action.go +6 -0
@@ -222,6 +222,12 @@ func (c *Core) applyAction(a Action) Effect {
222222 // with none (the workspace or stash edge), the move goes to the
223223 // neighbor monitor and fullscreen stays.
224224 if w.pinned() && !w.stashOverCover() {
225+ // A cascade fullscreen tile at its band edge leaves for the
226+ // neighbor monitor, like any tile there, and stays fullscreen.
227+ if i := c.neighbor(d); i >= 0 && !w.onFloat() && w.policy().wraps && w.leavesBand(w.Focus+dir) {
228+ c.focusScreen = i
229+ return Effect{}
230+ }
225231 if !w.FocusColumn(dir) {
226232 if i := c.neighbor(d); i >= 0 {
227233 c.focusScreen = i
@@ -222,6 +222,12 @@ func (c *Core) applyAction(a Action) Effect {
222 // with none (the workspace or stash edge), the move goes to the222 // with none (the workspace or stash edge), the move goes to the
223 // neighbor monitor and fullscreen stays.223 // neighbor monitor and fullscreen stays.
224 if w.pinned() && !w.stashOverCover() {224 if w.pinned() && !w.stashOverCover() {
225+ // A cascade fullscreen tile at its band edge leaves for the
226+ // neighbor monitor, like any tile there, and stays fullscreen.
227+ if i := c.neighbor(d); i >= 0 && !w.onFloat() && w.policy().wraps && w.leavesBand(w.Focus+dir) {
228+ c.focusScreen = i
229+ return Effect{}
230+ }
225 if !w.FocusColumn(dir) {231 if !w.FocusColumn(dir) {
226 if i := c.neighbor(d); i >= 0 {232 if i := c.neighbor(d); i >= 0 {
227 c.focusScreen = i233 c.focusScreen = i
M internal/core/cascade.go +5 -6
@@ -110,18 +110,17 @@ func (w *Workspace) leavesBand(i int) bool {
110110 }
111111
112112 // moveBand swaps the focused cascade column with the column in the band dir
113-// away where focus-window-up/down goes; focus follows it. It reports
114-// whether the column moved.
115-func (w *Workspace) moveBand(dir int) bool {
113+// away where focus-window-up/down goes; focus follows it. Past the first
114+// or last band nothing moves.
115+func (w *Workspace) moveBand(dir int) {
116116 i := w.laneNeighbor(dir)
117- if !w.policy().wraps || i < 0 {
118- return false
117+ if i < 0 {
118+ return
119119 }
120120 w.unmaximize()
121121 w.Columns[w.Focus], w.Columns[i] = w.Columns[i], w.Columns[w.Focus]
122122 w.Focus = i
123123 w.scroll()
124- return true
125124 }
126125
127126 // laneNeighbor is the closest column to the focus in the lane d lanes away,
@@ -110,18 +110,17 @@ func (w *Workspace) leavesBand(i int) bool {
110 }110 }
111 111
112 // moveBand swaps the focused cascade column with the column in the band dir112 // moveBand swaps the focused cascade column with the column in the band dir
113-// away where focus-window-up/down goes; focus follows it. It reports113+// away where focus-window-up/down goes; focus follows it. Past the first
114-// whether the column moved.114+// or last band nothing moves.
115-func (w *Workspace) moveBand(dir int) bool {115+func (w *Workspace) moveBand(dir int) {
116 i := w.laneNeighbor(dir)116 i := w.laneNeighbor(dir)
117- if !w.policy().wraps || i < 0 {117+ if i < 0 {
118- return false118+ return
119 }119 }
120 w.unmaximize()120 w.unmaximize()
121 w.Columns[w.Focus], w.Columns[i] = w.Columns[i], w.Columns[w.Focus]121 w.Columns[w.Focus], w.Columns[i] = w.Columns[i], w.Columns[w.Focus]
122 w.Focus = i122 w.Focus = i
123 w.scroll()123 w.scroll()
124- return true
125 }124 }
126 125
127 // laneNeighbor is the closest column to the focus in the lane d lanes away,126 // laneNeighbor is the closest column to the focus in the lane d lanes away,
M internal/core/cascade_moves_test.go +45 -0
@@ -163,9 +163,54 @@ func TestCascadeMoveWindowStopsAtOuterBands(t *testing.T) {
163163 c, w := cascadeMovesCore(t, 5, 3, false)
164164 w.FocusID(1)
165165 c.applyAction(ActionMoveWindowUp)
166+ wantFocus(t, c, "M", 1)
166167 w.FocusID(4)
167168 c.applyAction(ActionMoveWindowDown)
168169 wantColumns(t, w, 1, 2, 3, 4, 5)
170+ wantFocus(t, c, "M", 4)
171+}
172+
173+// A maximized column leaving its band gives up maximization: the band it
174+// joins shows all its columns.
175+func TestCascadeMoveWindowEndsMaximization(t *testing.T) {
176+ c, w := cascadeMovesCore(t, 5, 3, false)
177+ w.FocusID(5)
178+ w.ToggleFullWidth()
179+ c.applyAction(ActionMoveWindowUp)
180+ wantColumns(t, w, 1, 5, 3, 4, 2)
181+ if w.Columns[1].FullWidth || len(w.maximized) != 0 {
182+ t.Fatalf("still maximized: full width %t, MRU %v", w.Columns[1].FullWidth, w.maximized)
183+ }
184+}
185+
186+// A fullscreen tile at its band edge leaves for the neighbor monitor and
187+// stays fullscreen, as a tile there does.
188+func TestCascadeFullscreenBandEdgeFocusesMonitor(t *testing.T) {
189+ c, w := cascadeMovesCore(t, 5, 3, true)
190+ w.FocusID(3)
191+ w.ToggleFullscreen()
192+ c.applyAction(ActionFocusColumnRight)
193+ if got := c.cur().name(); got != "R" {
194+ t.Fatalf("focused screen %q, want R", got)
195+ }
196+ if w.fullscreen != 3 {
197+ t.Fatalf("fullscreen %d, want 3", w.fullscreen)
198+ }
199+}
200+
201+// From a native float over the columns, the first move leaves the float
202+// on this monitor, even with the focused column at its band edge.
203+func TestCascadeFloatAtBandEdgeStaysOnMonitor(t *testing.T) {
204+ c, w := cascadeMovesCore(t, 5, 3, true)
205+ w.FocusID(3)
206+ w.AddFloating(9, 100, 100)
207+ c.applyAction(ActionFocusColumnRight)
208+ if got := c.cur().name(); got != "M" {
209+ t.Fatalf("focused screen %q, want M", got)
210+ }
211+ if w.floatFocus {
212+ t.Fatal("focus still on the float")
213+ }
169214 }
170215
171216 // A stacked window moves inside its column first, then the column changes
@@ -163,9 +163,54 @@ func TestCascadeMoveWindowStopsAtOuterBands(t *testing.T) {
163 c, w := cascadeMovesCore(t, 5, 3, false)163 c, w := cascadeMovesCore(t, 5, 3, false)
164 w.FocusID(1)164 w.FocusID(1)
165 c.applyAction(ActionMoveWindowUp)165 c.applyAction(ActionMoveWindowUp)
166+ wantFocus(t, c, "M", 1)
166 w.FocusID(4)167 w.FocusID(4)
167 c.applyAction(ActionMoveWindowDown)168 c.applyAction(ActionMoveWindowDown)
168 wantColumns(t, w, 1, 2, 3, 4, 5)169 wantColumns(t, w, 1, 2, 3, 4, 5)
170+ wantFocus(t, c, "M", 4)
171+}
172+
173+// A maximized column leaving its band gives up maximization: the band it
174+// joins shows all its columns.
175+func TestCascadeMoveWindowEndsMaximization(t *testing.T) {
176+ c, w := cascadeMovesCore(t, 5, 3, false)
177+ w.FocusID(5)
178+ w.ToggleFullWidth()
179+ c.applyAction(ActionMoveWindowUp)
180+ wantColumns(t, w, 1, 5, 3, 4, 2)
181+ if w.Columns[1].FullWidth || len(w.maximized) != 0 {
182+ t.Fatalf("still maximized: full width %t, MRU %v", w.Columns[1].FullWidth, w.maximized)
183+ }
184+}
185+
186+// A fullscreen tile at its band edge leaves for the neighbor monitor and
187+// stays fullscreen, as a tile there does.
188+func TestCascadeFullscreenBandEdgeFocusesMonitor(t *testing.T) {
189+ c, w := cascadeMovesCore(t, 5, 3, true)
190+ w.FocusID(3)
191+ w.ToggleFullscreen()
192+ c.applyAction(ActionFocusColumnRight)
193+ if got := c.cur().name(); got != "R" {
194+ t.Fatalf("focused screen %q, want R", got)
195+ }
196+ if w.fullscreen != 3 {
197+ t.Fatalf("fullscreen %d, want 3", w.fullscreen)
198+ }
199+}
200+
201+// From a native float over the columns, the first move leaves the float
202+// on this monitor, even with the focused column at its band edge.
203+func TestCascadeFloatAtBandEdgeStaysOnMonitor(t *testing.T) {
204+ c, w := cascadeMovesCore(t, 5, 3, true)
205+ w.FocusID(3)
206+ w.AddFloating(9, 100, 100)
207+ c.applyAction(ActionFocusColumnRight)
208+ if got := c.cur().name(); got != "M" {
209+ t.Fatalf("focused screen %q, want M", got)
210+ }
211+ if w.floatFocus {
212+ t.Fatal("focus still on the float")
213+ }
169 }214 }
170 215
171 // A stacked window moves inside its column first, then the column changes216 // A stacked window moves inside its column first, then the column changes
M internal/core/layout.go +0 -1
@@ -925,7 +925,6 @@ func (w *Workspace) MoveWindow(dir int) {
925925 c := &w.Columns[w.Focus]
926926 to := c.Focus + dir
927927 if to < 0 || to >= len(c.Windows) {
928- // Past the column, a cascade column changes band.
929928 w.moveBand(dir)
930929 return
931930 }
@@ -925,7 +925,6 @@ func (w *Workspace) MoveWindow(dir int) {
925 c := &w.Columns[w.Focus]925 c := &w.Columns[w.Focus]
926 to := c.Focus + dir926 to := c.Focus + dir
927 if to < 0 || to >= len(c.Windows) {927 if to < 0 || to >= len(c.Windows) {
928- // Past the column, a cascade column changes band.
929 w.moveBand(dir)928 w.moveBand(dir)
930 return929 return
931 }930 }

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 25

Tags 10

↑↓ moveenter openesc close