Merge branch 'feat/cascade-fullscreen-band' into main (!29)
feat(core): fullscreen cascade band and monitor move
8 files · +442 -20
M docs/config.md +2 -0
| @@ -307,6 +307,8 @@ The default layout. Columns sit side by side in bands of up to `max-columns`, st | |||
| 307 | 307 | - A new window joins the focused band when it has room, else it gets a new band right below. | |
| 308 | 308 | - `cycle-column-width` in a full band makes room: the rightmost column goes to the band below (from the last column, the leftmost goes to the band above). The end of the cycle brings it back. | |
| 309 | 309 | - `maximize-column` gives the column a band of its own: the columns on its left go to the band above, the ones on its right to the band below. Maximizing again forms the band back. | |
| 310 | + | - A fullscreen window gets a band of its own right below the band it was in (expelled into its own column first when it shares one), so the other columns stay put and focus changes among them leave the fullscreen output alone. Leaving fullscreen keeps the column in that band. | |
| 311 | + | - `move-column-left/right` on a fullscreen window (tiled or floating) sends it to the neighbor monitor, where it stays fullscreen and focused; with no neighbor monitor nothing moves. | |
| 310 | 312 | - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it. | |
| 311 | 313 | ||
| 312 | 314 | ### Column widths | |
| @@ -307,6 +307,8 @@ The default layout. Columns sit side by side in bands of up to `max-columns`, st | |||||
| 307 | - A new window joins the focused band when it has room, else it gets a new band right below. | 307 | - A new window joins the focused band when it has room, else it gets a new band right below. | ||
| 308 | - `cycle-column-width` in a full band makes room: the rightmost column goes to the band below (from the last column, the leftmost goes to the band above). The end of the cycle brings it back. | 308 | - `cycle-column-width` in a full band makes room: the rightmost column goes to the band below (from the last column, the leftmost goes to the band above). The end of the cycle brings it back. | ||
| 309 | - `maximize-column` gives the column a band of its own: the columns on its left go to the band above, the ones on its right to the band below. Maximizing again forms the band back. | 309 | - `maximize-column` gives the column a band of its own: the columns on its left go to the band above, the ones on its right to the band below. Maximizing again forms the band back. | ||
| 310 | + | - A fullscreen window gets a band of its own right below the band it was in (expelled into its own column first when it shares one), so the other columns stay put and focus changes among them leave the fullscreen output alone. Leaving fullscreen keeps the column in that band. | |||
| 311 | + | - `move-column-left/right` on a fullscreen window (tiled or floating) sends it to the neighbor monitor, where it stays fullscreen and focused; with no neighbor monitor nothing moves. | |||
| 310 | - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it. | 312 | - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it. | ||
| 311 | 313 | ||||
| 312 | ### Column widths | 314 | ### Column widths | ||
M internal/core/action.go +47 -1
| @@ -388,7 +388,39 @@ func (c *Core) dispatchAction(a Action) Effect { | |||
| 388 | 388 | if a == ActionMoveColumnLeft { | |
| 389 | 389 | dir, d = -1, dirLeft | |
| 390 | 390 | } | |
| 391 | - | if i := c.neighbor(d); w.leavesBand(w.Focus+dir) && i >= 0 { | |
| 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. | |
| 394 | + | i := c.neighbor(d) | |
| 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() | |
| 421 | + | return Effect{} | |
| 422 | + | } | |
| 423 | + | if i >= 0 && w.leavesBand(w.Focus+dir) { | |
| 392 | 424 | col, ok := w.takeColumn() | |
| 393 | 425 | if !ok { | |
| 394 | 426 | return Effect{} | |
| @@ -608,3 +640,17 @@ func (w *Workspace) Apply(a Action) Effect { | |||
| 608 | 640 | } | |
| 609 | 641 | return Effect{} | |
| 610 | 642 | } | |
| 643 | + | ||
| 644 | + | // moveFloat moves float id from workspace cur to to, as MoveToWorkspace | |
| 645 | + | // does, focuses it there and keeps it fullscreen. | |
| 646 | + | func moveFloat(cur, to *Workspace, id WindowID) { | |
| 647 | + | fl := cur.Floats[cur.floatIndex(id)] | |
| 648 | + | if fl.free && fl.back != nil { | |
| 649 | + | fl = fl.rehome() | |
| 650 | + | } | |
| 651 | + | cur.RemoveWindow(id) | |
| 652 | + | to.AddFloating(id, fl.W, fl.H) | |
| 653 | + | to.Floats[len(to.Floats)-1] = fl | |
| 654 | + | to.Activate(id) | |
| 655 | + | to.SetFullscreen(id, true) | |
| 656 | + | } | |
| @@ -388,7 +388,39 @@ 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 | - | if i := c.neighbor(d); w.leavesBand(w.Focus+dir) && i >= 0 { | 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. | |||
| 394 | + | i := c.neighbor(d) | |||
| 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() | |||
| 421 | + | return Effect{} | |||
| 422 | + | } | |||
| 423 | + | if i >= 0 && w.leavesBand(w.Focus+dir) { | |||
| 392 | col, ok := w.takeColumn() | 424 | col, ok := w.takeColumn() | ||
| 393 | if !ok { | 425 | if !ok { | ||
| 394 | return Effect{} | 426 | return Effect{} | ||
| @@ -608,3 +640,17 @@ func (w *Workspace) Apply(a Action) Effect { | |||||
| 608 | } | 640 | } | ||
| 609 | return Effect{} | 641 | return Effect{} | ||
| 610 | } | 642 | } | ||
| 643 | + | ||||
| 644 | + | // moveFloat moves float id from workspace cur to to, as MoveToWorkspace | |||
| 645 | + | // does, focuses it there and keeps it fullscreen. | |||
| 646 | + | func moveFloat(cur, to *Workspace, id WindowID) { | |||
| 647 | + | fl := cur.Floats[cur.floatIndex(id)] | |||
| 648 | + | if fl.free && fl.back != nil { | |||
| 649 | + | fl = fl.rehome() | |||
| 650 | + | } | |||
| 651 | + | cur.RemoveWindow(id) | |||
| 652 | + | to.AddFloating(id, fl.W, fl.H) | |||
| 653 | + | to.Floats[len(to.Floats)-1] = fl | |||
| 654 | + | to.Activate(id) | |||
| 655 | + | to.SetFullscreen(id, true) | |||
| 656 | + | } | |||
M internal/core/cascade.go +80 -14
| @@ -371,6 +371,9 @@ func (w *Workspace) shiftBand(lo, hi, dir, at int) bool { | |||
| 371 | 371 | id, _ := w.Focused() | |
| 372 | 372 | bands := w.bandList() | |
| 373 | 373 | cols := slices.Clone(bands[b][lo-first : hi-first+1]) | |
| 374 | + | for k := range cols { | |
| 375 | + | cols[k].ownBand = false | |
| 376 | + | } | |
| 374 | 377 | bands[b] = slices.Delete(bands[b], lo-first, hi-first+1) | |
| 375 | 378 | w.setBands(w.sendBand(bands, b, dir, at, cols), id) | |
| 376 | 379 | return true | |
| @@ -419,6 +422,53 @@ func (w *Workspace) isolateBand() { | |||
| 419 | 422 | w.setBands(bands, id) | |
| 420 | 423 | } | |
| 421 | 424 | ||
| 425 | + | // fullscreenBand gives tiled window id, about to go fullscreen, a band of | |
| 426 | + | // its own right below the band it is in, so the other columns keep their | |
| 427 | + | // band and a focus change among them never touches the fullscreen output. | |
| 428 | + | // A window stacked in a column is expelled into a column of its own first. | |
| 429 | + | // Leaving fullscreen does not move it back: the column keeps its band, so | |
| 430 | + | // nothing jumps when the user leaves. Only cascade layouts do this; the | |
| 431 | + | // focus stays on the same window. | |
| 432 | + | func (w *Workspace) fullscreenBand(id WindowID) { | |
| 433 | + | i := w.columnOf(id) | |
| 434 | + | if !w.policy().wraps || i < 0 { | |
| 435 | + | return | |
| 436 | + | } | |
| 437 | + | b := w.band(i) | |
| 438 | + | first, last := w.bandBounds(b) | |
| 439 | + | if len(w.Columns[i].Windows) == 1 && first == last { | |
| 440 | + | return | |
| 441 | + | } | |
| 442 | + | fc := w.Columns[w.Focus] | |
| 443 | + | focus := fc.Windows[fc.Focus] | |
| 444 | + | bands := w.bandList() | |
| 445 | + | col := bands[b][i-first] | |
| 446 | + | nc := col | |
| 447 | + | if len(col.Windows) == 1 { | |
| 448 | + | // The column leaves its lane, as deleteColumn: manual widths of the | |
| 449 | + | // lane no longer add up. It keeps its slot and its own width. | |
| 450 | + | w.clearSpans(i) | |
| 451 | + | bands[b] = slices.Delete(bands[b], i-first, i-first+1) | |
| 452 | + | } else { | |
| 453 | + | j := slices.Index(col.Windows, id) | |
| 454 | + | // The expelled window gets a plain column: no Span (a manual width | |
| 455 | + | // is the stack's, and the new band is alone), no shares. | |
| 456 | + | nc = Column{Windows: []WindowID{id}, Cells: col.Cells} | |
| 457 | + | col.Windows = slices.Delete(slices.Clone(col.Windows), j, j+1) | |
| 458 | + | col.Shares = nil | |
| 459 | + | if j == 0 { | |
| 460 | + | col.Slot = 0 // the slot window left | |
| 461 | + | } | |
| 462 | + | if col.Focus > j || col.Focus >= len(col.Windows) { | |
| 463 | + | col.Focus = max(min(col.Focus-1, len(col.Windows)-1), 0) | |
| 464 | + | } | |
| 465 | + | bands[b][i-first] = col | |
| 466 | + | } | |
| 467 | + | nc.ownBand = true | |
| 468 | + | bands = slices.Insert(bands, b+1, []Column{nc}) | |
| 469 | + | w.setBands(bands, focus) | |
| 470 | + | } | |
| 471 | + | ||
| 422 | 472 | // pushNeighbor makes room in the focused column's full cascade band for a | |
| 423 | 473 | // wider column: the column at the far end of the band leaves it. The | |
| 424 | 474 | // rightmost goes to the band below; from the last column, the leftmost | |
| @@ -457,13 +507,10 @@ func (w *Workspace) restoreBand() bool { | |||
| 457 | 507 | bands := w.bandList() | |
| 458 | 508 | b := w.band(w.Focus) | |
| 459 | 509 | room := max(max(w.MaxColumns, 1)-len(bands[b]), 0) | |
| 460 | - | // take removes from band nb the remembered columns found there, at | |
| 461 | - | // most room of them, in the order of ids walked from the focused | |
| 510 | + | // take removes from the bands nbs the remembered columns found there, | |
| 511 | + | // at most room of them, in the order of ids walked from the focused | |
| 462 | 512 | // column outward (rev: ids end nearest). | |
| 463 | - | take := func(nb int, ids [][]WindowID, rev bool) []Column { | |
| 464 | - | if nb < 0 || nb >= len(bands) { | |
| 465 | - | return nil | |
| 466 | - | } | |
| 513 | + | take := func(nbs []int, ids [][]WindowID, rev bool) []Column { | |
| 467 | 514 | var got []Column | |
| 468 | 515 | for k := range ids { | |
| 469 | 516 | if room == 0 { | |
| @@ -481,21 +528,40 @@ func (w *Workspace) restoreBand() bool { | |||
| 481 | 528 | if head < 0 { | |
| 482 | 529 | continue | |
| 483 | 530 | } | |
| 484 | - | i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] }) | |
| 485 | - | if i < 0 { | |
| 486 | - | continue | |
| 531 | + | for k, nb := range nbs { | |
| 532 | + | // Past the adjacent band, only a column fullscreenBand put in | |
| 533 | + | // a band of its own is taken: one the user moved there stays. | |
| 534 | + | i := slices.IndexFunc(bands[nb], func(c Column) bool { | |
| 535 | + | return c.Windows[0] == e[head] && (k == 0 || c.ownBand) | |
| 536 | + | }) | |
| 537 | + | if i < 0 { | |
| 538 | + | continue | |
| 539 | + | } | |
| 540 | + | got = append(got, bands[nb][i]) | |
| 541 | + | bands[nb] = slices.Delete(bands[nb], i, i+1) | |
| 542 | + | room-- | |
| 543 | + | break | |
| 487 | 544 | } | |
| 488 | - | got = append(got, bands[nb][i]) | |
| 489 | - | bands[nb] = slices.Delete(bands[nb], i, i+1) | |
| 490 | - | room-- | |
| 491 | 545 | } | |
| 492 | 546 | if rev { | |
| 493 | 547 | slices.Reverse(got) | |
| 494 | 548 | } | |
| 495 | 549 | return got | |
| 496 | 550 | } | |
| 497 | - | above := take(b-1, up, true) | |
| 498 | - | below := take(b+1, down, false) | |
| 551 | + | // reach lists the bands dir away to look in: the adjacent one, and the | |
| 552 | + | // next, where a column that fullscreenBand split off may lie. | |
| 553 | + | reach := func(dir int) []int { | |
| 554 | + | var nbs []int | |
| 555 | + | for n := b + dir; n >= 0 && n < len(bands); n += dir { | |
| 556 | + | nbs = append(nbs, n) | |
| 557 | + | if len(nbs) == 2 { | |
| 558 | + | break | |
| 559 | + | } | |
| 560 | + | } | |
| 561 | + | return nbs | |
| 562 | + | } | |
| 563 | + | above := take(reach(-1), up, true) | |
| 564 | + | below := take(reach(1), down, false) | |
| 499 | 565 | bands[b] = slices.Concat(above, bands[b], below) | |
| 500 | 566 | w.setBands(bands, id) | |
| 501 | 567 | return true | |
| @@ -371,6 +371,9 @@ func (w *Workspace) shiftBand(lo, hi, dir, at int) bool { | |||||
| 371 | id, _ := w.Focused() | 371 | id, _ := w.Focused() | ||
| 372 | bands := w.bandList() | 372 | bands := w.bandList() | ||
| 373 | cols := slices.Clone(bands[b][lo-first : hi-first+1]) | 373 | cols := slices.Clone(bands[b][lo-first : hi-first+1]) | ||
| 374 | + | for k := range cols { | |||
| 375 | + | cols[k].ownBand = false | |||
| 376 | + | } | |||
| 374 | bands[b] = slices.Delete(bands[b], lo-first, hi-first+1) | 377 | bands[b] = slices.Delete(bands[b], lo-first, hi-first+1) | ||
| 375 | w.setBands(w.sendBand(bands, b, dir, at, cols), id) | 378 | w.setBands(w.sendBand(bands, b, dir, at, cols), id) | ||
| 376 | return true | 379 | return true | ||
| @@ -419,6 +422,53 @@ func (w *Workspace) isolateBand() { | |||||
| 419 | w.setBands(bands, id) | 422 | w.setBands(bands, id) | ||
| 420 | } | 423 | } | ||
| 421 | 424 | ||||
| 425 | + | // fullscreenBand gives tiled window id, about to go fullscreen, a band of | |||
| 426 | + | // its own right below the band it is in, so the other columns keep their | |||
| 427 | + | // band and a focus change among them never touches the fullscreen output. | |||
| 428 | + | // A window stacked in a column is expelled into a column of its own first. | |||
| 429 | + | // Leaving fullscreen does not move it back: the column keeps its band, so | |||
| 430 | + | // nothing jumps when the user leaves. Only cascade layouts do this; the | |||
| 431 | + | // focus stays on the same window. | |||
| 432 | + | func (w *Workspace) fullscreenBand(id WindowID) { | |||
| 433 | + | i := w.columnOf(id) | |||
| 434 | + | if !w.policy().wraps || i < 0 { | |||
| 435 | + | return | |||
| 436 | + | } | |||
| 437 | + | b := w.band(i) | |||
| 438 | + | first, last := w.bandBounds(b) | |||
| 439 | + | if len(w.Columns[i].Windows) == 1 && first == last { | |||
| 440 | + | return | |||
| 441 | + | } | |||
| 442 | + | fc := w.Columns[w.Focus] | |||
| 443 | + | focus := fc.Windows[fc.Focus] | |||
| 444 | + | bands := w.bandList() | |||
| 445 | + | col := bands[b][i-first] | |||
| 446 | + | nc := col | |||
| 447 | + | if len(col.Windows) == 1 { | |||
| 448 | + | // The column leaves its lane, as deleteColumn: manual widths of the | |||
| 449 | + | // lane no longer add up. It keeps its slot and its own width. | |||
| 450 | + | w.clearSpans(i) | |||
| 451 | + | bands[b] = slices.Delete(bands[b], i-first, i-first+1) | |||
| 452 | + | } else { | |||
| 453 | + | j := slices.Index(col.Windows, id) | |||
| 454 | + | // The expelled window gets a plain column: no Span (a manual width | |||
| 455 | + | // is the stack's, and the new band is alone), no shares. | |||
| 456 | + | nc = Column{Windows: []WindowID{id}, Cells: col.Cells} | |||
| 457 | + | col.Windows = slices.Delete(slices.Clone(col.Windows), j, j+1) | |||
| 458 | + | col.Shares = nil | |||
| 459 | + | if j == 0 { | |||
| 460 | + | col.Slot = 0 // the slot window left | |||
| 461 | + | } | |||
| 462 | + | if col.Focus > j || col.Focus >= len(col.Windows) { | |||
| 463 | + | col.Focus = max(min(col.Focus-1, len(col.Windows)-1), 0) | |||
| 464 | + | } | |||
| 465 | + | bands[b][i-first] = col | |||
| 466 | + | } | |||
| 467 | + | nc.ownBand = true | |||
| 468 | + | bands = slices.Insert(bands, b+1, []Column{nc}) | |||
| 469 | + | w.setBands(bands, focus) | |||
| 470 | + | } | |||
| 471 | + | ||||
| 422 | // pushNeighbor makes room in the focused column's full cascade band for a | 472 | // pushNeighbor makes room in the focused column's full cascade band for a | ||
| 423 | // wider column: the column at the far end of the band leaves it. The | 473 | // wider column: the column at the far end of the band leaves it. The | ||
| 424 | // rightmost goes to the band below; from the last column, the leftmost | 474 | // rightmost goes to the band below; from the last column, the leftmost | ||
| @@ -457,13 +507,10 @@ func (w *Workspace) restoreBand() bool { | |||||
| 457 | bands := w.bandList() | 507 | bands := w.bandList() | ||
| 458 | b := w.band(w.Focus) | 508 | b := w.band(w.Focus) | ||
| 459 | room := max(max(w.MaxColumns, 1)-len(bands[b]), 0) | 509 | room := max(max(w.MaxColumns, 1)-len(bands[b]), 0) | ||
| 460 | - | // take removes from band nb the remembered columns found there, at | 510 | + | // take removes from the bands nbs the remembered columns found there, |
| 461 | - | // most room of them, in the order of ids walked from the focused | 511 | + | // at most room of them, in the order of ids walked from the focused |
| 462 | // column outward (rev: ids end nearest). | 512 | // column outward (rev: ids end nearest). | ||
| 463 | - | take := func(nb int, ids [][]WindowID, rev bool) []Column { | 513 | + | take := func(nbs []int, ids [][]WindowID, rev bool) []Column { |
| 464 | - | if nb < 0 || nb >= len(bands) { | |||
| 465 | - | return nil | |||
| 466 | - | } | |||
| 467 | var got []Column | 514 | var got []Column | ||
| 468 | for k := range ids { | 515 | for k := range ids { | ||
| 469 | if room == 0 { | 516 | if room == 0 { | ||
| @@ -481,21 +528,40 @@ func (w *Workspace) restoreBand() bool { | |||||
| 481 | if head < 0 { | 528 | if head < 0 { | ||
| 482 | continue | 529 | continue | ||
| 483 | } | 530 | } | ||
| 484 | - | i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] }) | 531 | + | for k, nb := range nbs { |
| 485 | - | if i < 0 { | 532 | + | // Past the adjacent band, only a column fullscreenBand put in |
| 486 | - | continue | 533 | + | // a band of its own is taken: one the user moved there stays. |
| 534 | + | i := slices.IndexFunc(bands[nb], func(c Column) bool { | |||
| 535 | + | return c.Windows[0] == e[head] && (k == 0 || c.ownBand) | |||
| 536 | + | }) | |||
| 537 | + | if i < 0 { | |||
| 538 | + | continue | |||
| 539 | + | } | |||
| 540 | + | got = append(got, bands[nb][i]) | |||
| 541 | + | bands[nb] = slices.Delete(bands[nb], i, i+1) | |||
| 542 | + | room-- | |||
| 543 | + | break | |||
| 487 | } | 544 | } | ||
| 488 | - | got = append(got, bands[nb][i]) | |||
| 489 | - | bands[nb] = slices.Delete(bands[nb], i, i+1) | |||
| 490 | - | room-- | |||
| 491 | } | 545 | } | ||
| 492 | if rev { | 546 | if rev { | ||
| 493 | slices.Reverse(got) | 547 | slices.Reverse(got) | ||
| 494 | } | 548 | } | ||
| 495 | return got | 549 | return got | ||
| 496 | } | 550 | } | ||
| 497 | - | above := take(b-1, up, true) | 551 | + | // reach lists the bands dir away to look in: the adjacent one, and the |
| 498 | - | below := take(b+1, down, false) | 552 | + | // next, where a column that fullscreenBand split off may lie. |
| 553 | + | reach := func(dir int) []int { | |||
| 554 | + | var nbs []int | |||
| 555 | + | for n := b + dir; n >= 0 && n < len(bands); n += dir { | |||
| 556 | + | nbs = append(nbs, n) | |||
| 557 | + | if len(nbs) == 2 { | |||
| 558 | + | break | |||
| 559 | + | } | |||
| 560 | + | } | |||
| 561 | + | return nbs | |||
| 562 | + | } | |||
| 563 | + | above := take(reach(-1), up, true) | |||
| 564 | + | below := take(reach(1), down, false) | |||
| 499 | bands[b] = slices.Concat(above, bands[b], below) | 565 | bands[b] = slices.Concat(above, bands[b], below) | ||
| 500 | w.setBands(bands, id) | 566 | w.setBands(bands, id) | ||
| 501 | return true | 567 | return true | ||
A internal/core/cascade_fullscreen_band_test.go +297 -0
| @@ -0,0 +1,297 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | // Entering fullscreen gives a cascade column a band below its own; the | |
| 6 | + | // others stay. Leaving keeps it there. | |
| 7 | + | func TestCascadeFullscreenOwnBand(t *testing.T) { | |
| 8 | + | for _, tc := range []struct { | |
| 9 | + | name string | |
| 10 | + | do func(w *Workspace) | |
| 11 | + | }{ | |
| 12 | + | {"toggle", func(w *Workspace) { w.ToggleFullscreen() }}, | |
| 13 | + | {"client request", func(w *Workspace) { w.SetFullscreen(3, true) }}, | |
| 14 | + | } { | |
| 15 | + | t.Run(tc.name, func(t *testing.T) { | |
| 16 | + | c, w := cascadeMovesCore(t, 3, 3, false) | |
| 17 | + | wantBands(t, w, []WindowID{1, 2, 3}) | |
| 18 | + | tc.do(w) | |
| 19 | + | wantBands(t, w, []WindowID{1, 2}, []WindowID{3}) | |
| 20 | + | wantFocus(t, c, "M", 3) | |
| 21 | + | if w.fullscreen != 3 { | |
| 22 | + | t.Fatalf("fullscreen %d, want 3", w.fullscreen) | |
| 23 | + | } | |
| 24 | + | w.ToggleFullscreen() | |
| 25 | + | wantBands(t, w, []WindowID{1, 2}, []WindowID{3}) | |
| 26 | + | }) | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | func TestCascadeFullscreenMiddleAndAlone(t *testing.T) { | |
| 31 | + | c, w := cascadeMovesCore(t, 3, 3, false) | |
| 32 | + | w.FocusID(2) | |
| 33 | + | w.ToggleFullscreen() | |
| 34 | + | wantBands(t, w, []WindowID{1, 3}, []WindowID{2}) | |
| 35 | + | wantFocus(t, c, "M", 2) | |
| 36 | + | ||
| 37 | + | _, w = cascadeMovesCore(t, 1, 3, false) | |
| 38 | + | w.ToggleFullscreen() | |
| 39 | + | wantBands(t, w, []WindowID{1}) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func TestCascadeFullscreenExpelsFromStack(t *testing.T) { | |
| 43 | + | c, w := cascadeMovesCore(t, 2, 3, false) | |
| 44 | + | w.FocusID(2) | |
| 45 | + | c.applyAction(ActionConsumeOrExpelLeft) // 2 stacks under 1 | |
| 46 | + | wantColumns(t, w, 1) | |
| 47 | + | w.ToggleFullscreen() | |
| 48 | + | wantBands(t, w, []WindowID{1}, []WindowID{2}) | |
| 49 | + | if len(w.Columns[0].Windows) != 1 || w.fullscreen != 2 { | |
| 50 | + | t.Fatalf("columns %v fullscreen %d", w.Columns, w.fullscreen) | |
| 51 | + | } | |
| 52 | + | wantFocus(t, c, "M", 2) | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | func TestScrollFullscreenKeepsColumns(t *testing.T) { | |
| 56 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |
| 57 | + | w.Overflow = OverflowScroll | |
| 58 | + | w.forgetBands() | |
| 59 | + | w.ToggleFullscreen() | |
| 60 | + | if len(w.Columns) != 3 { | |
| 61 | + | t.Fatalf("columns %d", len(w.Columns)) | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | func TestFullscreenColumnMovesToNeighborMonitor(t *testing.T) { | |
| 66 | + | for _, tc := range []struct { | |
| 67 | + | action Action | |
| 68 | + | screen string | |
| 69 | + | }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} { | |
| 70 | + | t.Run(tc.screen, func(t *testing.T) { | |
| 71 | + | c, w := cascadeMovesCore(t, 3, 3, true) | |
| 72 | + | w.ToggleFullscreen() | |
| 73 | + | c.applyAction(tc.action) | |
| 74 | + | wantColumns(t, w, 1, 2) | |
| 75 | + | dst := screenWorkspace(c, tc.screen) | |
| 76 | + | wantColumns(t, dst, 3) | |
| 77 | + | wantFocus(t, c, tc.screen, 3) | |
| 78 | + | if dst.fullscreen != 3 || w.fullscreen != 0 { | |
| 79 | + | t.Fatalf("fullscreen dst %d src %d", dst.fullscreen, w.fullscreen) | |
| 80 | + | } | |
| 81 | + | }) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | func TestFullscreenColumnNoNeighborStays(t *testing.T) { | |
| 86 | + | c, w := cascadeMovesCore(t, 3, 3, false) | |
| 87 | + | w.ToggleFullscreen() | |
| 88 | + | c.applyAction(ActionMoveColumnLeft) | |
| 89 | + | c.applyAction(ActionMoveColumnRight) | |
| 90 | + | wantBands(t, w, []WindowID{1, 2}, []WindowID{3}) | |
| 91 | + | if w.fullscreen != 3 { | |
| 92 | + | t.Fatalf("fullscreen %d", w.fullscreen) | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | func TestFullscreenFloatMovesToNeighborMonitor(t *testing.T) { | |
| 97 | + | for _, tc := range []struct { | |
| 98 | + | action Action | |
| 99 | + | screen string | |
| 100 | + | }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} { | |
| 101 | + | t.Run(tc.screen, func(t *testing.T) { | |
| 102 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |
| 103 | + | w.AddFloating(9, 5120, 2160) | |
| 104 | + | w.SetFullscreen(9, true) | |
| 105 | + | c.applyAction(tc.action) | |
| 106 | + | dst := screenWorkspace(c, tc.screen) | |
| 107 | + | if w.floatIndex(9) >= 0 || dst.floatIndex(9) < 0 || w.fullscreen != 0 || dst.fullscreen != 9 { | |
| 108 | + | t.Fatalf("float on src %v dst %v, fullscreen src %d dst %d", w.Floats, dst.Floats, w.fullscreen, dst.fullscreen) | |
| 109 | + | } | |
| 110 | + | wantFocus(t, c, tc.screen, 9) | |
| 111 | + | wantColumns(t, w, 1, 2) | |
| 112 | + | }) | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | func TestFullscreenFloatNoNeighborStays(t *testing.T) { | |
| 117 | + | c, w := cascadeMovesCore(t, 2, 3, false) | |
| 118 | + | w.AddFloating(9, 800, 600) | |
| 119 | + | w.SetFullscreen(9, true) | |
| 120 | + | c.applyAction(ActionMoveColumnLeft) | |
| 121 | + | if w.floatIndex(9) < 0 || w.fullscreen != 9 { | |
| 122 | + | t.Fatalf("float moved: %v fullscreen %d", w.Floats, w.fullscreen) | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | func TestCascadeFullscreenRepeatedRequestKeepsBands(t *testing.T) { | |
| 127 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |
| 128 | + | w.SetFullscreen(3, true) | |
| 129 | + | w.SetFullscreen(3, true) | |
| 130 | + | w.SetFullscreen(3, true) | |
| 131 | + | wantBands(t, w, []WindowID{1, 2}, []WindowID{3}) | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | func TestCascadeFullscreenStackedFixups(t *testing.T) { | |
| 135 | + | for _, tc := range []struct { | |
| 136 | + | name string | |
| 137 | + | focus WindowID | |
| 138 | + | rest WindowID | |
| 139 | + | slotKept bool | |
| 140 | + | }{{"last row", 3, 2, true}, {"first row", 2, 3, false}} { | |
| 141 | + | t.Run(tc.name, func(t *testing.T) { | |
| 142 | + | c, w := cascadeMovesCore(t, 3, 3, false) | |
| 143 | + | w.FocusID(3) | |
| 144 | + | c.applyAction(ActionConsumeOrExpelLeft) // 3 stacks under 2 | |
| 145 | + | w.FocusID(tc.focus) | |
| 146 | + | w.Columns[1].Shares = []int{5000, 5000} | |
| 147 | + | w.Columns[1].Span = 3000 | |
| 148 | + | w.Columns[1].Slot = 4 | |
| 149 | + | w.SetFullscreen(tc.focus, true) | |
| 150 | + | wantBands(t, w, []WindowID{1, tc.rest}, []WindowID{tc.focus}) | |
| 151 | + | col := w.Columns[1] | |
| 152 | + | if len(col.Windows) != 1 || col.Windows[0] != tc.rest || col.Shares != nil || col.Focus != 0 || (col.Slot == 4) != tc.slotKept { | |
| 153 | + | t.Fatalf("remaining column %+v", col) | |
| 154 | + | } | |
| 155 | + | if got, _ := w.Focused(); got != tc.focus { | |
| 156 | + | t.Fatalf("focus %d, want %d", got, tc.focus) | |
| 157 | + | } | |
| 158 | + | if w.Columns[2].Span != 0 || w.Columns[2].Slot != 0 { | |
| 159 | + | t.Fatalf("expelled column %+v", w.Columns[2]) | |
| 160 | + | } | |
| 161 | + | }) | |
| 162 | + | } | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | func TestCascadeFullscreenKeepsSlotOfIntactColumn(t *testing.T) { | |
| 166 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |
| 167 | + | w.Columns[2].Slot = 4 | |
| 168 | + | w.ToggleFullscreen() | |
| 169 | + | if i := w.columnOf(3); w.Columns[i].Slot != 4 { | |
| 170 | + | t.Fatalf("slot %d, want 4", w.Columns[i].Slot) | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | func TestCascadeFullscreenAfterMaximizeRestores(t *testing.T) { | |
| 175 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |
| 176 | + | w.FocusID(1) | |
| 177 | + | w.ToggleFullWidth() | |
| 178 | + | w.FocusID(3) | |
| 179 | + | w.ToggleFullscreen() | |
| 180 | + | w.ToggleFullscreen() | |
| 181 | + | w.FocusID(1) | |
| 182 | + | w.ToggleFullWidth() | |
| 183 | + | wantBands(t, w, []WindowID{1, 2, 3}) | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | func TestFullscreenMoveEdges(t *testing.T) { | |
| 187 | + | t.Run("destination fullscreen elsewhere", func(t *testing.T) { | |
| 188 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |
| 189 | + | dst := screenWorkspace(c, "R") | |
| 190 | + | dst.AddWindow(20) | |
| 191 | + | dst.SetFullscreen(20, true) | |
| 192 | + | w.ToggleFullscreen() | |
| 193 | + | c.applyAction(ActionMoveColumnRight) | |
| 194 | + | if dst.fullscreen != 2 { | |
| 195 | + | t.Fatalf("fullscreen %d, want 2", dst.fullscreen) | |
| 196 | + | } | |
| 197 | + | wantFocus(t, c, "R", 2) | |
| 198 | + | }) | |
| 199 | + | t.Run("scroll layout", func(t *testing.T) { | |
| 200 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |
| 201 | + | w.Overflow = OverflowScroll | |
| 202 | + | w.ToggleFullscreen() | |
| 203 | + | c.applyAction(ActionMoveColumnLeft) | |
| 204 | + | dst := screenWorkspace(c, "L") | |
| 205 | + | wantColumns(t, dst, 2) | |
| 206 | + | if dst.fullscreen != 2 || w.fullscreen != 0 { | |
| 207 | + | t.Fatalf("fullscreen dst %d src %d", dst.fullscreen, w.fullscreen) | |
| 208 | + | } | |
| 209 | + | }) | |
| 210 | + | t.Run("source floats promoted", func(t *testing.T) { | |
| 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("arrival focused despite left mark", func(t *testing.T) { | |
| 243 | + | // The destination has a pending "left" mark on the window, which | |
| 244 | + | // mayCover would refuse unless it is focused; the arrival is | |
| 245 | + | // focused, so it ends fullscreen. | |
| 246 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |
| 247 | + | dst := screenWorkspace(c, "R") | |
| 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("fullscreen window expelled from a stack then moved", 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) | |
| 281 | + | } | |
| 282 | + | }) | |
| 283 | + | } | |
| 284 | + | ||
| 285 | + | // A band the user shaped is not looked past: P, pushed down from F, then | |
| 286 | + | // moved past Y on purpose, stays there. | |
| 287 | + | func TestRestoreBandDoesNotLookPastUserBand(t *testing.T) { | |
| 288 | + | c, w := cascadeMovesCore(t, 3, 3, false) | |
| 289 | + | w.FocusID(1) | |
| 290 | + | w.ToggleFullWidth() // {1} {2 3} | |
| 291 | + | w.FocusID(2) | |
| 292 | + | c.applyAction(ActionMoveWindowDown) // {1} {3} {2} | |
| 293 | + | wantBands(t, w, []WindowID{1}, []WindowID{3}, []WindowID{2}) | |
| 294 | + | w.FocusID(1) | |
| 295 | + | w.ToggleFullWidth() | |
| 296 | + | wantBands(t, w, []WindowID{1, 3}, []WindowID{2}) | |
| 297 | + | } | |
| @@ -0,0 +1,297 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | // Entering fullscreen gives a cascade column a band below its own; the | |||
| 6 | + | // others stay. Leaving keeps it there. | |||
| 7 | + | func TestCascadeFullscreenOwnBand(t *testing.T) { | |||
| 8 | + | for _, tc := range []struct { | |||
| 9 | + | name string | |||
| 10 | + | do func(w *Workspace) | |||
| 11 | + | }{ | |||
| 12 | + | {"toggle", func(w *Workspace) { w.ToggleFullscreen() }}, | |||
| 13 | + | {"client request", func(w *Workspace) { w.SetFullscreen(3, true) }}, | |||
| 14 | + | } { | |||
| 15 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 16 | + | c, w := cascadeMovesCore(t, 3, 3, false) | |||
| 17 | + | wantBands(t, w, []WindowID{1, 2, 3}) | |||
| 18 | + | tc.do(w) | |||
| 19 | + | wantBands(t, w, []WindowID{1, 2}, []WindowID{3}) | |||
| 20 | + | wantFocus(t, c, "M", 3) | |||
| 21 | + | if w.fullscreen != 3 { | |||
| 22 | + | t.Fatalf("fullscreen %d, want 3", w.fullscreen) | |||
| 23 | + | } | |||
| 24 | + | w.ToggleFullscreen() | |||
| 25 | + | wantBands(t, w, []WindowID{1, 2}, []WindowID{3}) | |||
| 26 | + | }) | |||
| 27 | + | } | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | func TestCascadeFullscreenMiddleAndAlone(t *testing.T) { | |||
| 31 | + | c, w := cascadeMovesCore(t, 3, 3, false) | |||
| 32 | + | w.FocusID(2) | |||
| 33 | + | w.ToggleFullscreen() | |||
| 34 | + | wantBands(t, w, []WindowID{1, 3}, []WindowID{2}) | |||
| 35 | + | wantFocus(t, c, "M", 2) | |||
| 36 | + | ||||
| 37 | + | _, w = cascadeMovesCore(t, 1, 3, false) | |||
| 38 | + | w.ToggleFullscreen() | |||
| 39 | + | wantBands(t, w, []WindowID{1}) | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func TestCascadeFullscreenExpelsFromStack(t *testing.T) { | |||
| 43 | + | c, w := cascadeMovesCore(t, 2, 3, false) | |||
| 44 | + | w.FocusID(2) | |||
| 45 | + | c.applyAction(ActionConsumeOrExpelLeft) // 2 stacks under 1 | |||
| 46 | + | wantColumns(t, w, 1) | |||
| 47 | + | w.ToggleFullscreen() | |||
| 48 | + | wantBands(t, w, []WindowID{1}, []WindowID{2}) | |||
| 49 | + | if len(w.Columns[0].Windows) != 1 || w.fullscreen != 2 { | |||
| 50 | + | t.Fatalf("columns %v fullscreen %d", w.Columns, w.fullscreen) | |||
| 51 | + | } | |||
| 52 | + | wantFocus(t, c, "M", 2) | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | func TestScrollFullscreenKeepsColumns(t *testing.T) { | |||
| 56 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |||
| 57 | + | w.Overflow = OverflowScroll | |||
| 58 | + | w.forgetBands() | |||
| 59 | + | w.ToggleFullscreen() | |||
| 60 | + | if len(w.Columns) != 3 { | |||
| 61 | + | t.Fatalf("columns %d", len(w.Columns)) | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | func TestFullscreenColumnMovesToNeighborMonitor(t *testing.T) { | |||
| 66 | + | for _, tc := range []struct { | |||
| 67 | + | action Action | |||
| 68 | + | screen string | |||
| 69 | + | }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} { | |||
| 70 | + | t.Run(tc.screen, func(t *testing.T) { | |||
| 71 | + | c, w := cascadeMovesCore(t, 3, 3, true) | |||
| 72 | + | w.ToggleFullscreen() | |||
| 73 | + | c.applyAction(tc.action) | |||
| 74 | + | wantColumns(t, w, 1, 2) | |||
| 75 | + | dst := screenWorkspace(c, tc.screen) | |||
| 76 | + | wantColumns(t, dst, 3) | |||
| 77 | + | wantFocus(t, c, tc.screen, 3) | |||
| 78 | + | if dst.fullscreen != 3 || w.fullscreen != 0 { | |||
| 79 | + | t.Fatalf("fullscreen dst %d src %d", dst.fullscreen, w.fullscreen) | |||
| 80 | + | } | |||
| 81 | + | }) | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | func TestFullscreenColumnNoNeighborStays(t *testing.T) { | |||
| 86 | + | c, w := cascadeMovesCore(t, 3, 3, false) | |||
| 87 | + | w.ToggleFullscreen() | |||
| 88 | + | c.applyAction(ActionMoveColumnLeft) | |||
| 89 | + | c.applyAction(ActionMoveColumnRight) | |||
| 90 | + | wantBands(t, w, []WindowID{1, 2}, []WindowID{3}) | |||
| 91 | + | if w.fullscreen != 3 { | |||
| 92 | + | t.Fatalf("fullscreen %d", w.fullscreen) | |||
| 93 | + | } | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | func TestFullscreenFloatMovesToNeighborMonitor(t *testing.T) { | |||
| 97 | + | for _, tc := range []struct { | |||
| 98 | + | action Action | |||
| 99 | + | screen string | |||
| 100 | + | }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} { | |||
| 101 | + | t.Run(tc.screen, func(t *testing.T) { | |||
| 102 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |||
| 103 | + | w.AddFloating(9, 5120, 2160) | |||
| 104 | + | w.SetFullscreen(9, true) | |||
| 105 | + | c.applyAction(tc.action) | |||
| 106 | + | dst := screenWorkspace(c, tc.screen) | |||
| 107 | + | if w.floatIndex(9) >= 0 || dst.floatIndex(9) < 0 || w.fullscreen != 0 || dst.fullscreen != 9 { | |||
| 108 | + | t.Fatalf("float on src %v dst %v, fullscreen src %d dst %d", w.Floats, dst.Floats, w.fullscreen, dst.fullscreen) | |||
| 109 | + | } | |||
| 110 | + | wantFocus(t, c, tc.screen, 9) | |||
| 111 | + | wantColumns(t, w, 1, 2) | |||
| 112 | + | }) | |||
| 113 | + | } | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | func TestFullscreenFloatNoNeighborStays(t *testing.T) { | |||
| 117 | + | c, w := cascadeMovesCore(t, 2, 3, false) | |||
| 118 | + | w.AddFloating(9, 800, 600) | |||
| 119 | + | w.SetFullscreen(9, true) | |||
| 120 | + | c.applyAction(ActionMoveColumnLeft) | |||
| 121 | + | if w.floatIndex(9) < 0 || w.fullscreen != 9 { | |||
| 122 | + | t.Fatalf("float moved: %v fullscreen %d", w.Floats, w.fullscreen) | |||
| 123 | + | } | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | func TestCascadeFullscreenRepeatedRequestKeepsBands(t *testing.T) { | |||
| 127 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |||
| 128 | + | w.SetFullscreen(3, true) | |||
| 129 | + | w.SetFullscreen(3, true) | |||
| 130 | + | w.SetFullscreen(3, true) | |||
| 131 | + | wantBands(t, w, []WindowID{1, 2}, []WindowID{3}) | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | func TestCascadeFullscreenStackedFixups(t *testing.T) { | |||
| 135 | + | for _, tc := range []struct { | |||
| 136 | + | name string | |||
| 137 | + | focus WindowID | |||
| 138 | + | rest WindowID | |||
| 139 | + | slotKept bool | |||
| 140 | + | }{{"last row", 3, 2, true}, {"first row", 2, 3, false}} { | |||
| 141 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 142 | + | c, w := cascadeMovesCore(t, 3, 3, false) | |||
| 143 | + | w.FocusID(3) | |||
| 144 | + | c.applyAction(ActionConsumeOrExpelLeft) // 3 stacks under 2 | |||
| 145 | + | w.FocusID(tc.focus) | |||
| 146 | + | w.Columns[1].Shares = []int{5000, 5000} | |||
| 147 | + | w.Columns[1].Span = 3000 | |||
| 148 | + | w.Columns[1].Slot = 4 | |||
| 149 | + | w.SetFullscreen(tc.focus, true) | |||
| 150 | + | wantBands(t, w, []WindowID{1, tc.rest}, []WindowID{tc.focus}) | |||
| 151 | + | col := w.Columns[1] | |||
| 152 | + | if len(col.Windows) != 1 || col.Windows[0] != tc.rest || col.Shares != nil || col.Focus != 0 || (col.Slot == 4) != tc.slotKept { | |||
| 153 | + | t.Fatalf("remaining column %+v", col) | |||
| 154 | + | } | |||
| 155 | + | if got, _ := w.Focused(); got != tc.focus { | |||
| 156 | + | t.Fatalf("focus %d, want %d", got, tc.focus) | |||
| 157 | + | } | |||
| 158 | + | if w.Columns[2].Span != 0 || w.Columns[2].Slot != 0 { | |||
| 159 | + | t.Fatalf("expelled column %+v", w.Columns[2]) | |||
| 160 | + | } | |||
| 161 | + | }) | |||
| 162 | + | } | |||
| 163 | + | } | |||
| 164 | + | ||||
| 165 | + | func TestCascadeFullscreenKeepsSlotOfIntactColumn(t *testing.T) { | |||
| 166 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |||
| 167 | + | w.Columns[2].Slot = 4 | |||
| 168 | + | w.ToggleFullscreen() | |||
| 169 | + | if i := w.columnOf(3); w.Columns[i].Slot != 4 { | |||
| 170 | + | t.Fatalf("slot %d, want 4", w.Columns[i].Slot) | |||
| 171 | + | } | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | func TestCascadeFullscreenAfterMaximizeRestores(t *testing.T) { | |||
| 175 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |||
| 176 | + | w.FocusID(1) | |||
| 177 | + | w.ToggleFullWidth() | |||
| 178 | + | w.FocusID(3) | |||
| 179 | + | w.ToggleFullscreen() | |||
| 180 | + | w.ToggleFullscreen() | |||
| 181 | + | w.FocusID(1) | |||
| 182 | + | w.ToggleFullWidth() | |||
| 183 | + | wantBands(t, w, []WindowID{1, 2, 3}) | |||
| 184 | + | } | |||
| 185 | + | ||||
| 186 | + | func TestFullscreenMoveEdges(t *testing.T) { | |||
| 187 | + | t.Run("destination fullscreen elsewhere", func(t *testing.T) { | |||
| 188 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |||
| 189 | + | dst := screenWorkspace(c, "R") | |||
| 190 | + | dst.AddWindow(20) | |||
| 191 | + | dst.SetFullscreen(20, true) | |||
| 192 | + | w.ToggleFullscreen() | |||
| 193 | + | c.applyAction(ActionMoveColumnRight) | |||
| 194 | + | if dst.fullscreen != 2 { | |||
| 195 | + | t.Fatalf("fullscreen %d, want 2", dst.fullscreen) | |||
| 196 | + | } | |||
| 197 | + | wantFocus(t, c, "R", 2) | |||
| 198 | + | }) | |||
| 199 | + | t.Run("scroll layout", func(t *testing.T) { | |||
| 200 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |||
| 201 | + | w.Overflow = OverflowScroll | |||
| 202 | + | w.ToggleFullscreen() | |||
| 203 | + | c.applyAction(ActionMoveColumnLeft) | |||
| 204 | + | dst := screenWorkspace(c, "L") | |||
| 205 | + | wantColumns(t, dst, 2) | |||
| 206 | + | if dst.fullscreen != 2 || w.fullscreen != 0 { | |||
| 207 | + | t.Fatalf("fullscreen dst %d src %d", dst.fullscreen, w.fullscreen) | |||
| 208 | + | } | |||
| 209 | + | }) | |||
| 210 | + | t.Run("source floats promoted", func(t *testing.T) { | |||
| 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("arrival focused despite left mark", func(t *testing.T) { | |||
| 243 | + | // The destination has a pending "left" mark on the window, which | |||
| 244 | + | // mayCover would refuse unless it is focused; the arrival is | |||
| 245 | + | // focused, so it ends fullscreen. | |||
| 246 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |||
| 247 | + | dst := screenWorkspace(c, "R") | |||
| 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("fullscreen window expelled from a stack then moved", 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) | |||
| 281 | + | } | |||
| 282 | + | }) | |||
| 283 | + | } | |||
| 284 | + | ||||
| 285 | + | // A band the user shaped is not looked past: P, pushed down from F, then | |||
| 286 | + | // moved past Y on purpose, stays there. | |||
| 287 | + | func TestRestoreBandDoesNotLookPastUserBand(t *testing.T) { | |||
| 288 | + | c, w := cascadeMovesCore(t, 3, 3, false) | |||
| 289 | + | w.FocusID(1) | |||
| 290 | + | w.ToggleFullWidth() // {1} {2 3} | |||
| 291 | + | w.FocusID(2) | |||
| 292 | + | c.applyAction(ActionMoveWindowDown) // {1} {3} {2} | |||
| 293 | + | wantBands(t, w, []WindowID{1}, []WindowID{3}, []WindowID{2}) | |||
| 294 | + | w.FocusID(1) | |||
| 295 | + | w.ToggleFullWidth() | |||
| 296 | + | wantBands(t, w, []WindowID{1, 3}, []WindowID{2}) | |||
| 297 | + | } | |||
M internal/core/cascade_moves_test.go +3 -2
| @@ -299,13 +299,14 @@ func TestCascadeMoveWindowStackedFirst(t *testing.T) { | |||
| 299 | 299 | wantFocus(t, c, "M", 5) | |
| 300 | 300 | } | |
| 301 | 301 | ||
| 302 | - | // A fullscreen window keeps its band. | |
| 302 | + | // A fullscreen window keeps its own band (it left {4 5} when it went | |
| 303 | + | // fullscreen). | |
| 303 | 304 | func TestCascadeMoveWindowFullscreenStays(t *testing.T) { | |
| 304 | 305 | c, w := cascadeMovesCore(t, 5, 3, false) | |
| 305 | 306 | w.FocusID(4) | |
| 306 | 307 | w.ToggleFullscreen() | |
| 307 | 308 | c.applyAction(ActionMoveWindowUp) | |
| 308 | - | wantColumns(t, w, 1, 2, 3, 4, 5) | |
| 309 | + | wantColumns(t, w, 1, 2, 3, 5, 4) | |
| 309 | 310 | } | |
| 310 | 311 | ||
| 311 | 312 | // Scroll overflow has no bands: up/down stop at the column edge. | |
| @@ -299,13 +299,14 @@ func TestCascadeMoveWindowStackedFirst(t *testing.T) { | |||||
| 299 | wantFocus(t, c, "M", 5) | 299 | wantFocus(t, c, "M", 5) | ||
| 300 | } | 300 | } | ||
| 301 | 301 | ||||
| 302 | - | // A fullscreen window keeps its band. | 302 | + | // A fullscreen window keeps its own band (it left {4 5} when it went |
| 303 | + | // fullscreen). | |||
| 303 | func TestCascadeMoveWindowFullscreenStays(t *testing.T) { | 304 | func TestCascadeMoveWindowFullscreenStays(t *testing.T) { | ||
| 304 | c, w := cascadeMovesCore(t, 5, 3, false) | 305 | c, w := cascadeMovesCore(t, 5, 3, false) | ||
| 305 | w.FocusID(4) | 306 | w.FocusID(4) | ||
| 306 | w.ToggleFullscreen() | 307 | w.ToggleFullscreen() | ||
| 307 | c.applyAction(ActionMoveWindowUp) | 308 | c.applyAction(ActionMoveWindowUp) | ||
| 308 | - | wantColumns(t, w, 1, 2, 3, 4, 5) | 309 | + | wantColumns(t, w, 1, 2, 3, 5, 4) |
| 309 | } | 310 | } | ||
| 310 | 311 | ||||
| 311 | // Scroll overflow has no bands: up/down stop at the column edge. | 312 | // Scroll overflow has no bands: up/down stop at the column edge. | ||
M internal/core/cascade_test.go +1 -1
| @@ -195,7 +195,7 @@ func TestCascadeCovers(t *testing.T) { | |||
| 195 | 195 | } | |
| 196 | 196 | w.ToggleFullscreen() | |
| 197 | 197 | w.FocusID(4) | |
| 198 | - | if w.View != 600 { | |
| 198 | + | if w.View != 1200 { | |
| 199 | 199 | t.Fatal("fullscreen restore lost band reveal") | |
| 200 | 200 | } | |
| 201 | 201 | } | |
| @@ -195,7 +195,7 @@ func TestCascadeCovers(t *testing.T) { | |||||
| 195 | } | 195 | } | ||
| 196 | w.ToggleFullscreen() | 196 | w.ToggleFullscreen() | ||
| 197 | w.FocusID(4) | 197 | w.FocusID(4) | ||
| 198 | - | if w.View != 600 { | 198 | + | if w.View != 1200 { |
| 199 | t.Fatal("fullscreen restore lost band reveal") | 199 | t.Fatal("fullscreen restore lost band reveal") | ||
| 200 | } | 200 | } | ||
| 201 | } | 201 | } | ||
M internal/core/layout.go +9 -0
| @@ -40,6 +40,11 @@ type Column struct { | |||
| 40 | 40 | // maximize-column or cycle-column-width sent to the band above or | |
| 41 | 41 | // below; they come back when the column is restored. | |
| 42 | 42 | pushedUp, pushedDown [][]WindowID | |
| 43 | + | // ownBand marks a column fullscreenBand put in a band of its own: | |
| 44 | + | // restoreBand brings it back from past the adjacent band, unlike a | |
| 45 | + | // column the user moved there. shiftBand clears it when the column | |
| 46 | + | // moves to another band on purpose. | |
| 47 | + | ownBand bool | |
| 43 | 48 | } | |
| 44 | 49 | type Placement struct { | |
| 45 | 50 | ID WindowID | |
| @@ -1335,6 +1340,7 @@ func (w *Workspace) ToggleFullscreen() { | |||
| 1335 | 1340 | w.endFullscreen() | |
| 1336 | 1341 | return | |
| 1337 | 1342 | } | |
| 1343 | + | w.fullscreenBand(id) | |
| 1338 | 1344 | w.fullscreen = id | |
| 1339 | 1345 | w.scroll() | |
| 1340 | 1346 | } | |
| @@ -1444,6 +1450,9 @@ func (w *Workspace) SetFullscreen(id WindowID, on bool) { | |||
| 1444 | 1450 | // Another window covers: the override was for the old cover. | |
| 1445 | 1451 | w.dropStashOver() | |
| 1446 | 1452 | } | |
| 1453 | + | if !w.isFloat(id) && w.fullscreen != id { | |
| 1454 | + | w.fullscreenBand(id) | |
| 1455 | + | } | |
| 1447 | 1456 | w.fullscreen = id | |
| 1448 | 1457 | if !w.isFloat(id) { | |
| 1449 | 1458 | w.scroll() | |
| @@ -40,6 +40,11 @@ type Column struct { | |||||
| 40 | // maximize-column or cycle-column-width sent to the band above or | 40 | // maximize-column or cycle-column-width sent to the band above or | ||
| 41 | // below; they come back when the column is restored. | 41 | // below; they come back when the column is restored. | ||
| 42 | pushedUp, pushedDown [][]WindowID | 42 | pushedUp, pushedDown [][]WindowID | ||
| 43 | + | // ownBand marks a column fullscreenBand put in a band of its own: | |||
| 44 | + | // restoreBand brings it back from past the adjacent band, unlike a | |||
| 45 | + | // column the user moved there. shiftBand clears it when the column | |||
| 46 | + | // moves to another band on purpose. | |||
| 47 | + | ownBand bool | |||
| 43 | } | 48 | } | ||
| 44 | type Placement struct { | 49 | type Placement struct { | ||
| 45 | ID WindowID | 50 | ID WindowID | ||
| @@ -1335,6 +1340,7 @@ func (w *Workspace) ToggleFullscreen() { | |||||
| 1335 | w.endFullscreen() | 1340 | w.endFullscreen() | ||
| 1336 | return | 1341 | return | ||
| 1337 | } | 1342 | } | ||
| 1343 | + | w.fullscreenBand(id) | |||
| 1338 | w.fullscreen = id | 1344 | w.fullscreen = id | ||
| 1339 | w.scroll() | 1345 | w.scroll() | ||
| 1340 | } | 1346 | } | ||
| @@ -1444,6 +1450,9 @@ func (w *Workspace) SetFullscreen(id WindowID, on bool) { | |||||
| 1444 | // Another window covers: the override was for the old cover. | 1450 | // Another window covers: the override was for the old cover. | ||
| 1445 | w.dropStashOver() | 1451 | w.dropStashOver() | ||
| 1446 | } | 1452 | } | ||
| 1453 | + | if !w.isFloat(id) && w.fullscreen != id { | |||
| 1454 | + | w.fullscreenBand(id) | |||
| 1455 | + | } | |||
| 1447 | w.fullscreen = id | 1456 | w.fullscreen = id | ||
| 1448 | if !w.isFloat(id) { | 1457 | if !w.isFloat(id) { | ||
| 1449 | w.scroll() | 1458 | w.scroll() | ||
M internal/core/overview_cascade_test.go +3 -2
| @@ -728,7 +728,8 @@ func TestOverviewCascadeCoveringFloat(t *testing.T) { | |||
| 728 | 728 | } | |
| 729 | 729 | } | |
| 730 | 730 | ||
| 731 | - | // T10b: a pinned fullscreen column keeps its Fullscreen tile in the band. | |
| 731 | + | // T10b: a pinned fullscreen column keeps its Fullscreen tile, and the | |
| 732 | + | // columns of the other bands stay shown. | |
| 732 | 733 | func TestOverviewCascadePinnedFullscreenTile(t *testing.T) { | |
| 733 | 734 | m := cascadeOverview() | |
| 734 | 735 | w := m.Current() | |
| @@ -744,7 +745,7 @@ func TestOverviewCascadePinnedFullscreenTile(t *testing.T) { | |||
| 744 | 745 | t.Fatalf("fullscreen tile: %+v", fs) | |
| 745 | 746 | } | |
| 746 | 747 | for _, id := range []WindowID{7, 9} { | |
| 747 | - | if p := previewOf(t, ps, id); p.Hidden || p.Fullscreen || p.Peek != (fs.Peek) { | |
| 748 | + | if p := previewOf(t, ps, id); p.Hidden || p.Fullscreen { | |
| 748 | 749 | t.Fatalf("band tile %d: %+v", id, p) | |
| 749 | 750 | } | |
| 750 | 751 | } | |
| @@ -728,7 +728,8 @@ func TestOverviewCascadeCoveringFloat(t *testing.T) { | |||||
| 728 | } | 728 | } | ||
| 729 | } | 729 | } | ||
| 730 | 730 | ||||
| 731 | - | // T10b: a pinned fullscreen column keeps its Fullscreen tile in the band. | 731 | + | // T10b: a pinned fullscreen column keeps its Fullscreen tile, and the |
| 732 | + | // columns of the other bands stay shown. | |||
| 732 | func TestOverviewCascadePinnedFullscreenTile(t *testing.T) { | 733 | func TestOverviewCascadePinnedFullscreenTile(t *testing.T) { | ||
| 733 | m := cascadeOverview() | 734 | m := cascadeOverview() | ||
| 734 | w := m.Current() | 735 | w := m.Current() | ||
| @@ -744,7 +745,7 @@ func TestOverviewCascadePinnedFullscreenTile(t *testing.T) { | |||||
| 744 | t.Fatalf("fullscreen tile: %+v", fs) | 745 | t.Fatalf("fullscreen tile: %+v", fs) | ||
| 745 | } | 746 | } | ||
| 746 | for _, id := range []WindowID{7, 9} { | 747 | for _, id := range []WindowID{7, 9} { | ||
| 747 | - | if p := previewOf(t, ps, id); p.Hidden || p.Fullscreen || p.Peek != (fs.Peek) { | 748 | + | if p := previewOf(t, ps, id); p.Hidden || p.Fullscreen { |
| 748 | t.Fatalf("band tile %d: %+v", id, p) | 749 | t.Fatalf("band tile %d: %+v", id, p) | ||
| 749 | } | 750 | } | ||
| 750 | } | 751 | } | ||