fix(core): restore pushed cascade columns by first window, in order
A pushed column is found by its first window being one of its remembered windows, so a window that left it no longer brings its new column back. Mixed width-cycle and maximize pushes keep the original neighbor order.
- commit
- efa1a9f404336dc913cc711e6f2027c44c983d8b
- author
- brice · · 2026-10-16 16:57 UTC
- parent
- 0e73719
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +33 -10
M internal/core/cascade.go +10 -10
| @@ -351,7 +351,8 @@ func (w *Workspace) shiftBand(lo, hi, dir, at int) bool { | |||
| 351 | 351 | } | |
| 352 | 352 | ||
| 353 | 353 | // columnIDs lists the windows of each column, its identity for | |
| 354 | - | // restoreBand: any of them still together finds the column. | |
| 354 | + | // restoreBand: a column whose first window is one of them is that column, | |
| 355 | + | // even after its first window closed or the stack changed order. | |
| 355 | 356 | func columnIDs(cols []Column) [][]WindowID { | |
| 356 | 357 | ids := make([][]WindowID, len(cols)) | |
| 357 | 358 | for i, c := range cols { | |
| @@ -375,10 +376,11 @@ func (w *Workspace) isolateBand() { | |||
| 375 | 376 | left := slices.Clone(bands[b][:p]) | |
| 376 | 377 | right := slices.Clone(bands[b][p+1:]) | |
| 377 | 378 | c := bands[b][p] | |
| 378 | - | // Earlier pushes (cycle-column-width) stay remembered, farther out: | |
| 379 | - | // pushedUp ends nearest, pushedDown starts nearest. | |
| 380 | - | c.pushedUp = append(c.pushedUp, columnIDs(left)...) | |
| 381 | - | c.pushedDown = append(columnIDs(right), c.pushedDown...) | |
| 379 | + | // Earlier pushes (cycle-column-width) stay remembered and were nearer | |
| 380 | + | // at first, so the new ones go past them: pushedUp ends nearest, | |
| 381 | + | // pushedDown starts nearest. | |
| 382 | + | c.pushedUp = append(columnIDs(left), c.pushedUp...) | |
| 383 | + | c.pushedDown = append(c.pushedDown, columnIDs(right)...) | |
| 382 | 384 | bands[b] = []Column{c} | |
| 383 | 385 | if len(right) > 0 { | |
| 384 | 386 | bands = w.sendBand(bands, b, 1, 0, right) | |
| @@ -400,10 +402,10 @@ func (w *Workspace) pushNeighbor() { | |||
| 400 | 402 | id, _ := w.Focused() | |
| 401 | 403 | c := &w.Columns[w.Focus] | |
| 402 | 404 | if w.Focus < last { | |
| 403 | - | c.pushedDown = append(columnIDs(w.Columns[w.Focus+1:w.Focus+2]), c.pushedDown...) | |
| 405 | + | c.pushedDown = append(c.pushedDown, columnIDs(w.Columns[w.Focus+1:w.Focus+2])...) | |
| 404 | 406 | w.shiftBand(w.Focus+1, w.Focus+1, 1, 0) | |
| 405 | 407 | } else { | |
| 406 | - | c.pushedUp = append(c.pushedUp, columnIDs(w.Columns[w.Focus-1:w.Focus])...) | |
| 408 | + | c.pushedUp = append(columnIDs(w.Columns[w.Focus-1:w.Focus]), c.pushedUp...) | |
| 407 | 409 | w.shiftBand(w.Focus-1, w.Focus-1, -1, max(w.MaxColumns, 1)) | |
| 408 | 410 | } | |
| 409 | 411 | w.Focus = w.columnOf(id) | |
| @@ -441,9 +443,7 @@ func (w *Workspace) restoreBand() bool { | |||
| 441 | 443 | if rev { | |
| 442 | 444 | e = ids[len(ids)-1-k] | |
| 443 | 445 | } | |
| 444 | - | i := slices.IndexFunc(bands[nb], func(c Column) bool { | |
| 445 | - | return slices.ContainsFunc(c.Windows, func(v WindowID) bool { return slices.Contains(e, v) }) | |
| 446 | - | }) | |
| 446 | + | i := slices.IndexFunc(bands[nb], func(c Column) bool { return slices.Contains(e, c.Windows[0]) }) | |
| 447 | 447 | if i < 0 { | |
| 448 | 448 | continue | |
| 449 | 449 | } | |
| @@ -351,7 +351,8 @@ func (w *Workspace) shiftBand(lo, hi, dir, at int) bool { | |||||
| 351 | } | 351 | } | ||
| 352 | 352 | ||||
| 353 | // columnIDs lists the windows of each column, its identity for | 353 | // columnIDs lists the windows of each column, its identity for | ||
| 354 | - | // restoreBand: any of them still together finds the column. | 354 | + | // restoreBand: a column whose first window is one of them is that column, |
| 355 | + | // even after its first window closed or the stack changed order. | |||
| 355 | func columnIDs(cols []Column) [][]WindowID { | 356 | func columnIDs(cols []Column) [][]WindowID { | ||
| 356 | ids := make([][]WindowID, len(cols)) | 357 | ids := make([][]WindowID, len(cols)) | ||
| 357 | for i, c := range cols { | 358 | for i, c := range cols { | ||
| @@ -375,10 +376,11 @@ func (w *Workspace) isolateBand() { | |||||
| 375 | left := slices.Clone(bands[b][:p]) | 376 | left := slices.Clone(bands[b][:p]) | ||
| 376 | right := slices.Clone(bands[b][p+1:]) | 377 | right := slices.Clone(bands[b][p+1:]) | ||
| 377 | c := bands[b][p] | 378 | c := bands[b][p] | ||
| 378 | - | // Earlier pushes (cycle-column-width) stay remembered, farther out: | 379 | + | // Earlier pushes (cycle-column-width) stay remembered and were nearer |
| 379 | - | // pushedUp ends nearest, pushedDown starts nearest. | 380 | + | // at first, so the new ones go past them: pushedUp ends nearest, |
| 380 | - | c.pushedUp = append(c.pushedUp, columnIDs(left)...) | 381 | + | // pushedDown starts nearest. |
| 381 | - | c.pushedDown = append(columnIDs(right), c.pushedDown...) | 382 | + | c.pushedUp = append(columnIDs(left), c.pushedUp...) |
| 383 | + | c.pushedDown = append(c.pushedDown, columnIDs(right)...) | |||
| 382 | bands[b] = []Column{c} | 384 | bands[b] = []Column{c} | ||
| 383 | if len(right) > 0 { | 385 | if len(right) > 0 { | ||
| 384 | bands = w.sendBand(bands, b, 1, 0, right) | 386 | bands = w.sendBand(bands, b, 1, 0, right) | ||
| @@ -400,10 +402,10 @@ func (w *Workspace) pushNeighbor() { | |||||
| 400 | id, _ := w.Focused() | 402 | id, _ := w.Focused() | ||
| 401 | c := &w.Columns[w.Focus] | 403 | c := &w.Columns[w.Focus] | ||
| 402 | if w.Focus < last { | 404 | if w.Focus < last { | ||
| 403 | - | c.pushedDown = append(columnIDs(w.Columns[w.Focus+1:w.Focus+2]), c.pushedDown...) | 405 | + | c.pushedDown = append(c.pushedDown, columnIDs(w.Columns[w.Focus+1:w.Focus+2])...) |
| 404 | w.shiftBand(w.Focus+1, w.Focus+1, 1, 0) | 406 | w.shiftBand(w.Focus+1, w.Focus+1, 1, 0) | ||
| 405 | } else { | 407 | } else { | ||
| 406 | - | c.pushedUp = append(c.pushedUp, columnIDs(w.Columns[w.Focus-1:w.Focus])...) | 408 | + | c.pushedUp = append(columnIDs(w.Columns[w.Focus-1:w.Focus]), c.pushedUp...) |
| 407 | w.shiftBand(w.Focus-1, w.Focus-1, -1, max(w.MaxColumns, 1)) | 409 | w.shiftBand(w.Focus-1, w.Focus-1, -1, max(w.MaxColumns, 1)) | ||
| 408 | } | 410 | } | ||
| 409 | w.Focus = w.columnOf(id) | 411 | w.Focus = w.columnOf(id) | ||
| @@ -441,9 +443,7 @@ func (w *Workspace) restoreBand() bool { | |||||
| 441 | if rev { | 443 | if rev { | ||
| 442 | e = ids[len(ids)-1-k] | 444 | e = ids[len(ids)-1-k] | ||
| 443 | } | 445 | } | ||
| 444 | - | i := slices.IndexFunc(bands[nb], func(c Column) bool { | 446 | + | i := slices.IndexFunc(bands[nb], func(c Column) bool { return slices.Contains(e, c.Windows[0]) }) |
| 445 | - | return slices.ContainsFunc(c.Windows, func(v WindowID) bool { return slices.Contains(e, v) }) | |||
| 446 | - | }) | |||
| 447 | if i < 0 { | 447 | if i < 0 { | ||
| 448 | continue | 448 | continue | ||
| 449 | } | 449 | } | ||
M internal/core/cascade_bands_test.go +23 -0
| @@ -201,6 +201,29 @@ func TestCascadeRestoreBandMemory(t *testing.T) { | |||
| 201 | 201 | w.ToggleFullWidth() // {1 2 3} | |
| 202 | 202 | wantBands(t, w, []WindowID{1, 2, 3}) | |
| 203 | 203 | }) | |
| 204 | + | t.Run("cycle then maximize from either end keeps the order", func(t *testing.T) { | |
| 205 | + | for _, focus := range []WindowID{1, 3} { | |
| 206 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |
| 207 | + | w.FocusID(focus) | |
| 208 | + | w.Apply(ActionCycleColumnWidth) | |
| 209 | + | w.ToggleFullWidth() | |
| 210 | + | w.ToggleFullWidth() | |
| 211 | + | wantBands(t, w, []WindowID{1, 2, 3}) | |
| 212 | + | } | |
| 213 | + | }) | |
| 214 | + | t.Run("a window that left the pushed column does not bring its new column", func(t *testing.T) { | |
| 215 | + | _, w := cascadeMovesCore(t, 5, 4, false) // {1 2 3 4} {5} | |
| 216 | + | w.FocusID(4) | |
| 217 | + | w.ConsumeOrExpel(-1) // {1 2 3/4} {5} | |
| 218 | + | w.FocusID(2) | |
| 219 | + | w.ToggleFullWidth() // {1} {2*} {3/4 5} | |
| 220 | + | w.stackWindow(4, 5) // {1} {2*} {3 5/4} | |
| 221 | + | w.FocusID(5) | |
| 222 | + | w.MoveColumn(-1) // {1} {2*} {5/4 3} | |
| 223 | + | w.FocusID(2) | |
| 224 | + | w.ToggleFullWidth() | |
| 225 | + | wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{5}) | |
| 226 | + | }) | |
| 204 | 227 | t.Run("first window of a pushed column closed", func(t *testing.T) { | |
| 205 | 228 | _, w := cascadeMovesCore(t, 4, 3, false) // {1 2 3} {4} | |
| 206 | 229 | w.FocusID(4) | |
| @@ -201,6 +201,29 @@ func TestCascadeRestoreBandMemory(t *testing.T) { | |||||
| 201 | w.ToggleFullWidth() // {1 2 3} | 201 | w.ToggleFullWidth() // {1 2 3} | ||
| 202 | wantBands(t, w, []WindowID{1, 2, 3}) | 202 | wantBands(t, w, []WindowID{1, 2, 3}) | ||
| 203 | }) | 203 | }) | ||
| 204 | + | t.Run("cycle then maximize from either end keeps the order", func(t *testing.T) { | |||
| 205 | + | for _, focus := range []WindowID{1, 3} { | |||
| 206 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |||
| 207 | + | w.FocusID(focus) | |||
| 208 | + | w.Apply(ActionCycleColumnWidth) | |||
| 209 | + | w.ToggleFullWidth() | |||
| 210 | + | w.ToggleFullWidth() | |||
| 211 | + | wantBands(t, w, []WindowID{1, 2, 3}) | |||
| 212 | + | } | |||
| 213 | + | }) | |||
| 214 | + | t.Run("a window that left the pushed column does not bring its new column", func(t *testing.T) { | |||
| 215 | + | _, w := cascadeMovesCore(t, 5, 4, false) // {1 2 3 4} {5} | |||
| 216 | + | w.FocusID(4) | |||
| 217 | + | w.ConsumeOrExpel(-1) // {1 2 3/4} {5} | |||
| 218 | + | w.FocusID(2) | |||
| 219 | + | w.ToggleFullWidth() // {1} {2*} {3/4 5} | |||
| 220 | + | w.stackWindow(4, 5) // {1} {2*} {3 5/4} | |||
| 221 | + | w.FocusID(5) | |||
| 222 | + | w.MoveColumn(-1) // {1} {2*} {5/4 3} | |||
| 223 | + | w.FocusID(2) | |||
| 224 | + | w.ToggleFullWidth() | |||
| 225 | + | wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{5}) | |||
| 226 | + | }) | |||
| 204 | t.Run("first window of a pushed column closed", func(t *testing.T) { | 227 | t.Run("first window of a pushed column closed", func(t *testing.T) { | ||
| 205 | _, w := cascadeMovesCore(t, 4, 3, false) // {1 2 3} {4} | 228 | _, w := cascadeMovesCore(t, 4, 3, false) // {1 2 3} {4} | ||
| 206 | w.FocusID(4) | 229 | w.FocusID(4) | ||