fix(core): restore every remembered column across a fullscreen band
restoreBand looks past a single-column adjacent band, move-column dispatches fullscreen windows through one branch, and the fullscreen tests cover stacked, populated-destination and demoted-float cases.
3 files · +126 -65
M internal/core/action.go +30 -22
| @@ -388,32 +388,43 @@ func (c *Core) dispatchAction(a Action) Effect { | |||
| 388 | 388 | if a == ActionMoveColumnLeft { | |
| 389 | 389 | dir, d = -1, dirLeft | |
| 390 | 390 | } | |
| 391 | - | // A fullscreen float goes to the neighbor screen, fullscreen there. | |
| 392 | - | if fid, ok := w.Focused(); ok && w.fullscreen == fid && w.floatIndex(fid) >= 0 { | |
| 393 | - | if i := c.neighbor(d); i >= 0 { | |
| 394 | - | from := c.cur() | |
| 395 | - | c.focusScreen = i | |
| 396 | - | moveFloat(w, c.cur().mon.Current(), fid) | |
| 397 | - | w.reconcileFloats() | |
| 398 | - | from.mon.normalize() | |
| 399 | - | c.cur().mon.normalize() | |
| 400 | - | } | |
| 401 | - | return Effect{} | |
| 402 | - | } | |
| 403 | - | // A fullscreen window leaves for the neighbor screen at once, and | |
| 404 | - | // stays fullscreen there; with no screen, nothing moves. | |
| 405 | - | fid, _ := w.Focused() | |
| 406 | - | full := fid != 0 && !w.onFloat() && w.fullscreen == fid | |
| 391 | + | // A fullscreen window (tiled or floating) leaves for the neighbor | |
| 392 | + | // screen at once and stays fullscreen there; with no screen, | |
| 393 | + | // nothing moves. | |
| 407 | 394 | i := c.neighbor(d) | |
| 408 | - | if full && i < 0 { | |
| 395 | + | if fid, ok := w.Focused(); ok && w.fullscreen == fid && (w.floatIndex(fid) >= 0 || !w.onFloat()) { | |
| 396 | + | if i < 0 { | |
| 397 | + | return Effect{} | |
| 398 | + | } | |
| 399 | + | var col Column | |
| 400 | + | if w.floatIndex(fid) < 0 { | |
| 401 | + | if col, ok = w.takeColumn(); !ok { | |
| 402 | + | return Effect{} | |
| 403 | + | } | |
| 404 | + | } | |
| 405 | + | w.reconcileFloats() // floats the fullscreen kept demoted show again | |
| 406 | + | from := c.cur() | |
| 407 | + | c.focusScreen = i | |
| 408 | + | to := c.cur().mon.Current() | |
| 409 | + | if len(col.Windows) == 0 { | |
| 410 | + | moveFloat(w, to, fid) | |
| 411 | + | } else { | |
| 412 | + | at := 0 | |
| 413 | + | if d == dirLeft { | |
| 414 | + | at = len(to.Columns) | |
| 415 | + | } | |
| 416 | + | to.receive(col, at) | |
| 417 | + | to.SetFullscreen(fid, true) | |
| 418 | + | } | |
| 419 | + | from.mon.normalize() | |
| 420 | + | c.cur().mon.normalize() | |
| 409 | 421 | return Effect{} | |
| 410 | 422 | } | |
| 411 | - | if i >= 0 && (full || w.leavesBand(w.Focus+dir)) { | |
| 423 | + | if i >= 0 && w.leavesBand(w.Focus+dir) { | |
| 412 | 424 | col, ok := w.takeColumn() | |
| 413 | 425 | if !ok { | |
| 414 | 426 | return Effect{} | |
| 415 | 427 | } | |
| 416 | - | w.reconcileFloats() // floats the fullscreen kept demoted show again | |
| 417 | 428 | from := c.cur() | |
| 418 | 429 | c.focusScreen = i | |
| 419 | 430 | to := c.cur().mon.Current() | |
| @@ -422,9 +433,6 @@ func (c *Core) dispatchAction(a Action) Effect { | |||
| 422 | 433 | at = len(to.Columns) | |
| 423 | 434 | } | |
| 424 | 435 | to.receive(col, at) | |
| 425 | - | if full { | |
| 426 | - | to.SetFullscreen(fid, true) | |
| 427 | - | } | |
| 428 | 436 | from.mon.normalize() | |
| 429 | 437 | c.cur().mon.normalize() | |
| 430 | 438 | return Effect{} | |
| @@ -388,32 +388,43 @@ func (c *Core) dispatchAction(a Action) Effect { | |||||
| 388 | if a == ActionMoveColumnLeft { | 388 | if a == ActionMoveColumnLeft { | ||
| 389 | dir, d = -1, dirLeft | 389 | dir, d = -1, dirLeft | ||
| 390 | } | 390 | } | ||
| 391 | - | // A fullscreen float goes to the neighbor screen, fullscreen there. | 391 | + | // A fullscreen window (tiled or floating) leaves for the neighbor |
| 392 | - | if fid, ok := w.Focused(); ok && w.fullscreen == fid && w.floatIndex(fid) >= 0 { | 392 | + | // screen at once and stays fullscreen there; with no screen, |
| 393 | - | if i := c.neighbor(d); i >= 0 { | 393 | + | // nothing moves. |
| 394 | - | from := c.cur() | |||
| 395 | - | c.focusScreen = i | |||
| 396 | - | moveFloat(w, c.cur().mon.Current(), fid) | |||
| 397 | - | w.reconcileFloats() | |||
| 398 | - | from.mon.normalize() | |||
| 399 | - | c.cur().mon.normalize() | |||
| 400 | - | } | |||
| 401 | - | return Effect{} | |||
| 402 | - | } | |||
| 403 | - | // A fullscreen window leaves for the neighbor screen at once, and | |||
| 404 | - | // stays fullscreen there; with no screen, nothing moves. | |||
| 405 | - | fid, _ := w.Focused() | |||
| 406 | - | full := fid != 0 && !w.onFloat() && w.fullscreen == fid | |||
| 407 | i := c.neighbor(d) | 394 | i := c.neighbor(d) | ||
| 408 | - | if full && i < 0 { | 395 | + | if fid, ok := w.Focused(); ok && w.fullscreen == fid && (w.floatIndex(fid) >= 0 || !w.onFloat()) { |
| 396 | + | if i < 0 { | |||
| 397 | + | return Effect{} | |||
| 398 | + | } | |||
| 399 | + | var col Column | |||
| 400 | + | if w.floatIndex(fid) < 0 { | |||
| 401 | + | if col, ok = w.takeColumn(); !ok { | |||
| 402 | + | return Effect{} | |||
| 403 | + | } | |||
| 404 | + | } | |||
| 405 | + | w.reconcileFloats() // floats the fullscreen kept demoted show again | |||
| 406 | + | from := c.cur() | |||
| 407 | + | c.focusScreen = i | |||
| 408 | + | to := c.cur().mon.Current() | |||
| 409 | + | if len(col.Windows) == 0 { | |||
| 410 | + | moveFloat(w, to, fid) | |||
| 411 | + | } else { | |||
| 412 | + | at := 0 | |||
| 413 | + | if d == dirLeft { | |||
| 414 | + | at = len(to.Columns) | |||
| 415 | + | } | |||
| 416 | + | to.receive(col, at) | |||
| 417 | + | to.SetFullscreen(fid, true) | |||
| 418 | + | } | |||
| 419 | + | from.mon.normalize() | |||
| 420 | + | c.cur().mon.normalize() | |||
| 409 | return Effect{} | 421 | return Effect{} | ||
| 410 | } | 422 | } | ||
| 411 | - | if i >= 0 && (full || w.leavesBand(w.Focus+dir)) { | 423 | + | if i >= 0 && w.leavesBand(w.Focus+dir) { |
| 412 | col, ok := w.takeColumn() | 424 | col, ok := w.takeColumn() | ||
| 413 | if !ok { | 425 | if !ok { | ||
| 414 | return Effect{} | 426 | return Effect{} | ||
| 415 | } | 427 | } | ||
| 416 | - | w.reconcileFloats() // floats the fullscreen kept demoted show again | |||
| 417 | from := c.cur() | 428 | from := c.cur() | ||
| 418 | c.focusScreen = i | 429 | c.focusScreen = i | ||
| 419 | to := c.cur().mon.Current() | 430 | to := c.cur().mon.Current() | ||
| @@ -422,9 +433,6 @@ func (c *Core) dispatchAction(a Action) Effect { | |||||
| 422 | at = len(to.Columns) | 433 | at = len(to.Columns) | ||
| 423 | } | 434 | } | ||
| 424 | to.receive(col, at) | 435 | to.receive(col, at) | ||
| 425 | - | if full { | |||
| 426 | - | to.SetFullscreen(fid, true) | |||
| 427 | - | } | |||
| 428 | from.mon.normalize() | 436 | from.mon.normalize() | ||
| 429 | c.cur().mon.normalize() | 437 | c.cur().mon.normalize() | ||
| 430 | return Effect{} | 438 | return Effect{} | ||
M internal/core/cascade.go +26 -22
| @@ -503,13 +503,10 @@ func (w *Workspace) restoreBand() bool { | |||
| 503 | 503 | bands := w.bandList() | |
| 504 | 504 | b := w.band(w.Focus) | |
| 505 | 505 | room := max(max(w.MaxColumns, 1)-len(bands[b]), 0) | |
| 506 | - | // take removes from band nb the remembered columns found there, at | |
| 507 | - | // most room of them, in the order of ids walked from the focused | |
| 506 | + | // take removes from the bands nbs the remembered columns found there, | |
| 507 | + | // at most room of them, in the order of ids walked from the focused | |
| 508 | 508 | // column outward (rev: ids end nearest). | |
| 509 | - | take := func(nb int, ids [][]WindowID, rev bool) []Column { | |
| 510 | - | if nb < 0 || nb >= len(bands) { | |
| 511 | - | return nil | |
| 512 | - | } | |
| 509 | + | take := func(nbs []int, ids [][]WindowID, rev bool) []Column { | |
| 513 | 510 | var got []Column | |
| 514 | 511 | for k := range ids { | |
| 515 | 512 | if room == 0 { | |
| @@ -527,30 +524,37 @@ func (w *Workspace) restoreBand() bool { | |||
| 527 | 524 | if head < 0 { | |
| 528 | 525 | continue | |
| 529 | 526 | } | |
| 530 | - | i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] }) | |
| 531 | - | if i < 0 { | |
| 532 | - | continue | |
| 527 | + | for _, nb := range nbs { | |
| 528 | + | i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] }) | |
| 529 | + | if i < 0 { | |
| 530 | + | continue | |
| 531 | + | } | |
| 532 | + | got = append(got, bands[nb][i]) | |
| 533 | + | bands[nb] = slices.Delete(bands[nb], i, i+1) | |
| 534 | + | room-- | |
| 535 | + | break | |
| 533 | 536 | } | |
| 534 | - | got = append(got, bands[nb][i]) | |
| 535 | - | bands[nb] = slices.Delete(bands[nb], i, i+1) | |
| 536 | - | room-- | |
| 537 | 537 | } | |
| 538 | 538 | if rev { | |
| 539 | 539 | slices.Reverse(got) | |
| 540 | 540 | } | |
| 541 | 541 | return got | |
| 542 | 542 | } | |
| 543 | - | // A band the take left empty (a fullscreen column that split off its | |
| 544 | - | // own band meanwhile) is looked past: the remembered columns may lie | |
| 545 | - | // one band farther. | |
| 546 | - | above := take(b-1, up, true) | |
| 547 | - | if b >= 2 && len(bands[b-1]) == 0 { | |
| 548 | - | above = slices.Concat(take(b-2, up, true), above) | |
| 549 | - | } | |
| 550 | - | below := take(b+1, down, false) | |
| 551 | - | if b+2 < len(bands) && len(bands[b+1]) == 0 { | |
| 552 | - | below = slices.Concat(below, take(b+2, down, false)) | |
| 543 | + | // reach lists the bands dir away to look in: the adjacent one, and the | |
| 544 | + | // next when the adjacent holds a single column, as a fullscreen column | |
| 545 | + | // split off a band of its own meanwhile leaves. | |
| 546 | + | reach := func(dir int) []int { | |
| 547 | + | var nbs []int | |
| 548 | + | for n := b + dir; n >= 0 && n < len(bands); n += dir { | |
| 549 | + | nbs = append(nbs, n) | |
| 550 | + | if len(bands[n]) != 1 || len(nbs) == 2 { | |
| 551 | + | break | |
| 552 | + | } | |
| 553 | + | } | |
| 554 | + | return nbs | |
| 553 | 555 | } | |
| 556 | + | above := take(reach(-1), up, true) | |
| 557 | + | below := take(reach(1), down, false) | |
| 554 | 558 | bands[b] = slices.Concat(above, bands[b], below) | |
| 555 | 559 | w.setBands(bands, id) | |
| 556 | 560 | return true | |
| @@ -503,13 +503,10 @@ func (w *Workspace) restoreBand() bool { | |||||
| 503 | bands := w.bandList() | 503 | bands := w.bandList() | ||
| 504 | b := w.band(w.Focus) | 504 | b := w.band(w.Focus) | ||
| 505 | room := max(max(w.MaxColumns, 1)-len(bands[b]), 0) | 505 | room := max(max(w.MaxColumns, 1)-len(bands[b]), 0) | ||
| 506 | - | // take removes from band nb the remembered columns found there, at | 506 | + | // take removes from the bands nbs the remembered columns found there, |
| 507 | - | // most room of them, in the order of ids walked from the focused | 507 | + | // at most room of them, in the order of ids walked from the focused |
| 508 | // column outward (rev: ids end nearest). | 508 | // column outward (rev: ids end nearest). | ||
| 509 | - | take := func(nb int, ids [][]WindowID, rev bool) []Column { | 509 | + | take := func(nbs []int, ids [][]WindowID, rev bool) []Column { |
| 510 | - | if nb < 0 || nb >= len(bands) { | |||
| 511 | - | return nil | |||
| 512 | - | } | |||
| 513 | var got []Column | 510 | var got []Column | ||
| 514 | for k := range ids { | 511 | for k := range ids { | ||
| 515 | if room == 0 { | 512 | if room == 0 { | ||
| @@ -527,30 +524,37 @@ func (w *Workspace) restoreBand() bool { | |||||
| 527 | if head < 0 { | 524 | if head < 0 { | ||
| 528 | continue | 525 | continue | ||
| 529 | } | 526 | } | ||
| 530 | - | i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] }) | 527 | + | for _, nb := range nbs { |
| 531 | - | if i < 0 { | 528 | + | i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] }) |
| 532 | - | continue | 529 | + | if i < 0 { |
| 530 | + | continue | |||
| 531 | + | } | |||
| 532 | + | got = append(got, bands[nb][i]) | |||
| 533 | + | bands[nb] = slices.Delete(bands[nb], i, i+1) | |||
| 534 | + | room-- | |||
| 535 | + | break | |||
| 533 | } | 536 | } | ||
| 534 | - | got = append(got, bands[nb][i]) | |||
| 535 | - | bands[nb] = slices.Delete(bands[nb], i, i+1) | |||
| 536 | - | room-- | |||
| 537 | } | 537 | } | ||
| 538 | if rev { | 538 | if rev { | ||
| 539 | slices.Reverse(got) | 539 | slices.Reverse(got) | ||
| 540 | } | 540 | } | ||
| 541 | return got | 541 | return got | ||
| 542 | } | 542 | } | ||
| 543 | - | // A band the take left empty (a fullscreen column that split off its | 543 | + | // reach lists the bands dir away to look in: the adjacent one, and the |
| 544 | - | // own band meanwhile) is looked past: the remembered columns may lie | 544 | + | // next when the adjacent holds a single column, as a fullscreen column |
| 545 | - | // one band farther. | 545 | + | // split off a band of its own meanwhile leaves. |
| 546 | - | above := take(b-1, up, true) | 546 | + | reach := func(dir int) []int { |
| 547 | - | if b >= 2 && len(bands[b-1]) == 0 { | 547 | + | var nbs []int |
| 548 | - | above = slices.Concat(take(b-2, up, true), above) | 548 | + | for n := b + dir; n >= 0 && n < len(bands); n += dir { |
| 549 | - | } | 549 | + | nbs = append(nbs, n) |
| 550 | - | below := take(b+1, down, false) | 550 | + | if len(bands[n]) != 1 || len(nbs) == 2 { |
| 551 | - | if b+2 < len(bands) && len(bands[b+1]) == 0 { | 551 | + | break |
| 552 | - | below = slices.Concat(below, take(b+2, down, false)) | 552 | + | } |
| 553 | + | } | |||
| 554 | + | return nbs | |||
| 553 | } | 555 | } | ||
| 556 | + | above := take(reach(-1), up, true) | |||
| 557 | + | below := take(reach(1), down, false) | |||
| 554 | bands[b] = slices.Concat(above, bands[b], below) | 558 | bands[b] = slices.Concat(above, bands[b], below) | ||
| 555 | w.setBands(bands, id) | 559 | w.setBands(bands, id) | ||
| 556 | return true | 560 | return true | ||
M internal/core/cascade_fullscreen_band_test.go +70 -21
| @@ -175,13 +175,11 @@ func TestCascadeFullscreenAfterMaximizeRestores(t *testing.T) { | |||
| 175 | 175 | _, w := cascadeMovesCore(t, 3, 3, false) | |
| 176 | 176 | w.FocusID(1) | |
| 177 | 177 | w.ToggleFullWidth() | |
| 178 | - | before := columnOrder(w) | |
| 179 | 178 | w.FocusID(3) | |
| 180 | 179 | w.ToggleFullscreen() | |
| 181 | 180 | w.ToggleFullscreen() | |
| 182 | 181 | w.FocusID(1) | |
| 183 | 182 | w.ToggleFullWidth() | |
| 184 | - | t.Logf("before %v bands after %v", before, columnOrder(w)) | |
| 185 | 183 | wantBands(t, w, []WindowID{1, 2, 3}) | |
| 186 | 184 | } | |
| 187 | 185 | ||
| @@ -210,25 +208,76 @@ func TestFullscreenMoveEdges(t *testing.T) { | |||
| 210 | 208 | } | |
| 211 | 209 | }) | |
| 212 | 210 | t.Run("source floats promoted", func(t *testing.T) { | |
| 213 | - | for _, float := range []bool{false, true} { | |
| 214 | - | c, w := cascadeMovesCore(t, 2, 3, true) | |
| 215 | - | w.AddFloating(9, 100, 100) | |
| 216 | - | w.SetFullscreen(9, true) | |
| 217 | - | if !float { | |
| 218 | - | w.FocusID(2) | |
| 219 | - | w.ToggleFullscreen() | |
| 220 | - | } | |
| 221 | - | demoted := w.Floats[0].below | |
| 222 | - | c.applyAction(ActionMoveColumnRight) | |
| 223 | - | if float { | |
| 224 | - | demoted = false | |
| 225 | - | } | |
| 226 | - | _ = demoted | |
| 227 | - | for _, f := range w.Floats { | |
| 228 | - | if f.below { | |
| 229 | - | t.Fatalf("float=%v: float %d still demoted", float, f.ID) | |
| 230 | - | } | |
| 231 | - | } | |
| 211 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |
| 212 | + | w.AddFloating(9, 100, 100) | |
| 213 | + | w.FocusID(2) | |
| 214 | + | w.ToggleFullscreen() | |
| 215 | + | // A fullscreen keeps a float demoted without it covering the output. | |
| 216 | + | w.Floats[0].below = true | |
| 217 | + | if len(w.Floats) != 1 || !w.Floats[0].below || w.fullscreen != 2 { | |
| 218 | + | t.Fatalf("setup: float not demoted: %+v", w.Floats) | |
| 219 | + | } | |
| 220 | + | c.applyAction(ActionMoveColumnRight) | |
| 221 | + | if len(w.Floats) != 1 || w.Floats[0].below { | |
| 222 | + | t.Fatalf("float still demoted: %+v", w.Floats) | |
| 223 | + | } | |
| 224 | + | }) | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | func TestCascadeFullscreenRestoresAcrossBand(t *testing.T) { | |
| 228 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |
| 229 | + | w.FocusID(1) | |
| 230 | + | w.ToggleFullWidth() // X=1 alone; 2 and 3 pushed below | |
| 231 | + | wantBands(t, w, []WindowID{1}, []WindowID{2, 3}) | |
| 232 | + | w.FocusID(2) | |
| 233 | + | w.ToggleFullscreen() | |
| 234 | + | wantBands(t, w, []WindowID{1}, []WindowID{3}, []WindowID{2}) | |
| 235 | + | w.ToggleFullscreen() | |
| 236 | + | w.FocusID(1) | |
| 237 | + | w.ToggleFullWidth() | |
| 238 | + | wantBands(t, w, []WindowID{1, 2, 3}) | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | func TestFullscreenMoveDestinations(t *testing.T) { | |
| 242 | + | t.Run("mayCover refuses", func(t *testing.T) { | |
| 243 | + | // A fixed destination refuses a cover unless the window is focused; | |
| 244 | + | // the arrival is focused, so it ends fullscreen. | |
| 245 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |
| 246 | + | dst := screenWorkspace(c, "R") | |
| 247 | + | dst.Overflow = OverflowFixed | |
| 248 | + | dst.left = 2 | |
| 249 | + | w.ToggleFullscreen() | |
| 250 | + | c.applyAction(ActionMoveColumnRight) | |
| 251 | + | wantFocus(t, c, "R", 2) | |
| 252 | + | if dst.fullscreen != 2 { | |
| 253 | + | t.Fatalf("fullscreen %d, want 2", dst.fullscreen) | |
| 254 | + | } | |
| 255 | + | }) | |
| 256 | + | t.Run("stacked column in a band", func(t *testing.T) { | |
| 257 | + | c, w := cascadeMovesCore(t, 3, 3, true) | |
| 258 | + | w.FocusID(3) | |
| 259 | + | c.applyAction(ActionConsumeOrExpelLeft) // 3 under 2 | |
| 260 | + | w.ToggleFullscreen() // 3 expelled to its own band | |
| 261 | + | c.applyAction(ActionMoveColumnLeft) | |
| 262 | + | wantColumns(t, w, 1, 2) | |
| 263 | + | dst := screenWorkspace(c, "L") | |
| 264 | + | wantColumns(t, dst, 3) | |
| 265 | + | if dst.fullscreen != 3 || len(w.Columns[1].Windows) != 1 { | |
| 266 | + | t.Fatalf("fullscreen %d, source column %v", dst.fullscreen, w.Columns[1].Windows) | |
| 267 | + | } | |
| 268 | + | }) | |
| 269 | + | t.Run("destination with columns gets its own band", func(t *testing.T) { | |
| 270 | + | c, w := cascadeMovesCore(t, 1, 3, true) | |
| 271 | + | dst := screenWorkspace(c, "R") | |
| 272 | + | dst.Overflow = OverflowCascade | |
| 273 | + | dst.SetMaxColumns(3) | |
| 274 | + | dst.AddWindow(20) | |
| 275 | + | dst.AddWindow(21) | |
| 276 | + | w.ToggleFullscreen() | |
| 277 | + | c.applyAction(ActionMoveColumnRight) | |
| 278 | + | wantBands(t, dst, []WindowID{20, 21}, []WindowID{1}) | |
| 279 | + | if dst.fullscreen != 1 { | |
| 280 | + | t.Fatalf("fullscreen %d", dst.fullscreen) | |
| 232 | 281 | } | |
| 233 | 282 | }) | |
| 234 | 283 | } | |
| @@ -175,13 +175,11 @@ func TestCascadeFullscreenAfterMaximizeRestores(t *testing.T) { | |||||
| 175 | _, w := cascadeMovesCore(t, 3, 3, false) | 175 | _, w := cascadeMovesCore(t, 3, 3, false) | ||
| 176 | w.FocusID(1) | 176 | w.FocusID(1) | ||
| 177 | w.ToggleFullWidth() | 177 | w.ToggleFullWidth() | ||
| 178 | - | before := columnOrder(w) | |||
| 179 | w.FocusID(3) | 178 | w.FocusID(3) | ||
| 180 | w.ToggleFullscreen() | 179 | w.ToggleFullscreen() | ||
| 181 | w.ToggleFullscreen() | 180 | w.ToggleFullscreen() | ||
| 182 | w.FocusID(1) | 181 | w.FocusID(1) | ||
| 183 | w.ToggleFullWidth() | 182 | w.ToggleFullWidth() | ||
| 184 | - | t.Logf("before %v bands after %v", before, columnOrder(w)) | |||
| 185 | wantBands(t, w, []WindowID{1, 2, 3}) | 183 | wantBands(t, w, []WindowID{1, 2, 3}) | ||
| 186 | } | 184 | } | ||
| 187 | 185 | ||||
| @@ -210,25 +208,76 @@ func TestFullscreenMoveEdges(t *testing.T) { | |||||
| 210 | } | 208 | } | ||
| 211 | }) | 209 | }) | ||
| 212 | t.Run("source floats promoted", func(t *testing.T) { | 210 | t.Run("source floats promoted", func(t *testing.T) { | ||
| 213 | - | for _, float := range []bool{false, true} { | 211 | + | c, w := cascadeMovesCore(t, 2, 3, true) |
| 214 | - | c, w := cascadeMovesCore(t, 2, 3, true) | 212 | + | w.AddFloating(9, 100, 100) |
| 215 | - | w.AddFloating(9, 100, 100) | 213 | + | w.FocusID(2) |
| 216 | - | w.SetFullscreen(9, true) | 214 | + | w.ToggleFullscreen() |
| 217 | - | if !float { | 215 | + | // A fullscreen keeps a float demoted without it covering the output. |
| 218 | - | w.FocusID(2) | 216 | + | w.Floats[0].below = true |
| 219 | - | w.ToggleFullscreen() | 217 | + | if len(w.Floats) != 1 || !w.Floats[0].below || w.fullscreen != 2 { |
| 220 | - | } | 218 | + | t.Fatalf("setup: float not demoted: %+v", w.Floats) |
| 221 | - | demoted := w.Floats[0].below | 219 | + | } |
| 222 | - | c.applyAction(ActionMoveColumnRight) | 220 | + | c.applyAction(ActionMoveColumnRight) |
| 223 | - | if float { | 221 | + | if len(w.Floats) != 1 || w.Floats[0].below { |
| 224 | - | demoted = false | 222 | + | t.Fatalf("float still demoted: %+v", w.Floats) |
| 225 | - | } | 223 | + | } |
| 226 | - | _ = demoted | 224 | + | }) |
| 227 | - | for _, f := range w.Floats { | 225 | + | } |
| 228 | - | if f.below { | 226 | + | |
| 229 | - | t.Fatalf("float=%v: float %d still demoted", float, f.ID) | 227 | + | func TestCascadeFullscreenRestoresAcrossBand(t *testing.T) { |
| 230 | - | } | 228 | + | _, w := cascadeMovesCore(t, 3, 3, false) |
| 231 | - | } | 229 | + | w.FocusID(1) |
| 230 | + | w.ToggleFullWidth() // X=1 alone; 2 and 3 pushed below | |||
| 231 | + | wantBands(t, w, []WindowID{1}, []WindowID{2, 3}) | |||
| 232 | + | w.FocusID(2) | |||
| 233 | + | w.ToggleFullscreen() | |||
| 234 | + | wantBands(t, w, []WindowID{1}, []WindowID{3}, []WindowID{2}) | |||
| 235 | + | w.ToggleFullscreen() | |||
| 236 | + | w.FocusID(1) | |||
| 237 | + | w.ToggleFullWidth() | |||
| 238 | + | wantBands(t, w, []WindowID{1, 2, 3}) | |||
| 239 | + | } | |||
| 240 | + | ||||
| 241 | + | func TestFullscreenMoveDestinations(t *testing.T) { | |||
| 242 | + | t.Run("mayCover refuses", func(t *testing.T) { | |||
| 243 | + | // A fixed destination refuses a cover unless the window is focused; | |||
| 244 | + | // the arrival is focused, so it ends fullscreen. | |||
| 245 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |||
| 246 | + | dst := screenWorkspace(c, "R") | |||
| 247 | + | dst.Overflow = OverflowFixed | |||
| 248 | + | dst.left = 2 | |||
| 249 | + | w.ToggleFullscreen() | |||
| 250 | + | c.applyAction(ActionMoveColumnRight) | |||
| 251 | + | wantFocus(t, c, "R", 2) | |||
| 252 | + | if dst.fullscreen != 2 { | |||
| 253 | + | t.Fatalf("fullscreen %d, want 2", dst.fullscreen) | |||
| 254 | + | } | |||
| 255 | + | }) | |||
| 256 | + | t.Run("stacked column in a band", func(t *testing.T) { | |||
| 257 | + | c, w := cascadeMovesCore(t, 3, 3, true) | |||
| 258 | + | w.FocusID(3) | |||
| 259 | + | c.applyAction(ActionConsumeOrExpelLeft) // 3 under 2 | |||
| 260 | + | w.ToggleFullscreen() // 3 expelled to its own band | |||
| 261 | + | c.applyAction(ActionMoveColumnLeft) | |||
| 262 | + | wantColumns(t, w, 1, 2) | |||
| 263 | + | dst := screenWorkspace(c, "L") | |||
| 264 | + | wantColumns(t, dst, 3) | |||
| 265 | + | if dst.fullscreen != 3 || len(w.Columns[1].Windows) != 1 { | |||
| 266 | + | t.Fatalf("fullscreen %d, source column %v", dst.fullscreen, w.Columns[1].Windows) | |||
| 267 | + | } | |||
| 268 | + | }) | |||
| 269 | + | t.Run("destination with columns gets its own band", func(t *testing.T) { | |||
| 270 | + | c, w := cascadeMovesCore(t, 1, 3, true) | |||
| 271 | + | dst := screenWorkspace(c, "R") | |||
| 272 | + | dst.Overflow = OverflowCascade | |||
| 273 | + | dst.SetMaxColumns(3) | |||
| 274 | + | dst.AddWindow(20) | |||
| 275 | + | dst.AddWindow(21) | |||
| 276 | + | w.ToggleFullscreen() | |||
| 277 | + | c.applyAction(ActionMoveColumnRight) | |||
| 278 | + | wantBands(t, dst, []WindowID{20, 21}, []WindowID{1}) | |||
| 279 | + | if dst.fullscreen != 1 { | |||
| 280 | + | t.Fatalf("fullscreen %d", dst.fullscreen) | |||
| 232 | } | 281 | } | ||
| 233 | }) | 282 | }) | ||
| 234 | } | 283 | } | ||