Merge c55955b353be20c11e800406ae8f1b964b88a0cf into f2d166b7fd4fa830ec06f487fcdcc64f1eacb4c9
8 files · +416 -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 +73 -14
| @@ -419,6 +419,52 @@ func (w *Workspace) isolateBand() { | |||
| 419 | 419 | w.setBands(bands, id) | |
| 420 | 420 | } | |
| 421 | 421 | ||
| 422 | + | // fullscreenBand gives tiled window id, about to go fullscreen, a band of | |
| 423 | + | // its own right below the band it is in, so the other columns keep their | |
| 424 | + | // band and a focus change among them never touches the fullscreen output. | |
| 425 | + | // A window stacked in a column is expelled into a column of its own first. | |
| 426 | + | // Leaving fullscreen does not move it back: the column keeps its band, so | |
| 427 | + | // nothing jumps when the user leaves. Only cascade layouts do this; the | |
| 428 | + | // focus stays on the same window. | |
| 429 | + | func (w *Workspace) fullscreenBand(id WindowID) { | |
| 430 | + | i := w.columnOf(id) | |
| 431 | + | if !w.policy().wraps || i < 0 { | |
| 432 | + | return | |
| 433 | + | } | |
| 434 | + | b := w.band(i) | |
| 435 | + | first, last := w.bandBounds(b) | |
| 436 | + | if len(w.Columns[i].Windows) == 1 && first == last { | |
| 437 | + | return | |
| 438 | + | } | |
| 439 | + | fc := w.Columns[w.Focus] | |
| 440 | + | focus := fc.Windows[fc.Focus] | |
| 441 | + | bands := w.bandList() | |
| 442 | + | col := bands[b][i-first] | |
| 443 | + | nc := col | |
| 444 | + | if len(col.Windows) == 1 { | |
| 445 | + | // The column leaves its lane, as deleteColumn: manual widths of the | |
| 446 | + | // lane no longer add up. It keeps its slot and its own width. | |
| 447 | + | w.clearSpans(i) | |
| 448 | + | bands[b] = slices.Delete(bands[b], i-first, i-first+1) | |
| 449 | + | } else { | |
| 450 | + | j := slices.Index(col.Windows, id) | |
| 451 | + | // The expelled window gets a plain column: no Span (a manual width | |
| 452 | + | // is the stack's, and the new band is alone), no shares. | |
| 453 | + | nc = Column{Windows: []WindowID{id}, Cells: col.Cells} | |
| 454 | + | col.Windows = slices.Delete(slices.Clone(col.Windows), j, j+1) | |
| 455 | + | col.Shares = nil | |
| 456 | + | if j == 0 { | |
| 457 | + | col.Slot = 0 // the slot window left | |
| 458 | + | } | |
| 459 | + | if col.Focus > j || col.Focus >= len(col.Windows) { | |
| 460 | + | col.Focus = max(min(col.Focus-1, len(col.Windows)-1), 0) | |
| 461 | + | } | |
| 462 | + | bands[b][i-first] = col | |
| 463 | + | } | |
| 464 | + | bands = slices.Insert(bands, b+1, []Column{nc}) | |
| 465 | + | w.setBands(bands, focus) | |
| 466 | + | } | |
| 467 | + | ||
| 422 | 468 | // pushNeighbor makes room in the focused column's full cascade band for a | |
| 423 | 469 | // wider column: the column at the far end of the band leaves it. The | |
| 424 | 470 | // rightmost goes to the band below; from the last column, the leftmost | |
| @@ -457,13 +503,10 @@ func (w *Workspace) restoreBand() bool { | |||
| 457 | 503 | bands := w.bandList() | |
| 458 | 504 | b := w.band(w.Focus) | |
| 459 | 505 | 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 | |
| 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 | |
| 462 | 508 | // 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 | - | } | |
| 509 | + | take := func(nbs []int, ids [][]WindowID, rev bool) []Column { | |
| 467 | 510 | var got []Column | |
| 468 | 511 | for k := range ids { | |
| 469 | 512 | if room == 0 { | |
| @@ -481,21 +524,37 @@ func (w *Workspace) restoreBand() bool { | |||
| 481 | 524 | if head < 0 { | |
| 482 | 525 | continue | |
| 483 | 526 | } | |
| 484 | - | i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] }) | |
| 485 | - | if i < 0 { | |
| 486 | - | 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 | |
| 487 | 536 | } | |
| 488 | - | got = append(got, bands[nb][i]) | |
| 489 | - | bands[nb] = slices.Delete(bands[nb], i, i+1) | |
| 490 | - | room-- | |
| 491 | 537 | } | |
| 492 | 538 | if rev { | |
| 493 | 539 | slices.Reverse(got) | |
| 494 | 540 | } | |
| 495 | 541 | return got | |
| 496 | 542 | } | |
| 497 | - | above := take(b-1, up, true) | |
| 498 | - | below := take(b+1, 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 | |
| 555 | + | } | |
| 556 | + | above := take(reach(-1), up, true) | |
| 557 | + | below := take(reach(1), down, false) | |
| 499 | 558 | bands[b] = slices.Concat(above, bands[b], below) | |
| 500 | 559 | w.setBands(bands, id) | |
| 501 | 560 | return true | |
| @@ -419,6 +419,52 @@ func (w *Workspace) isolateBand() { | |||||
| 419 | w.setBands(bands, id) | 419 | w.setBands(bands, id) | ||
| 420 | } | 420 | } | ||
| 421 | 421 | ||||
| 422 | + | // fullscreenBand gives tiled window id, about to go fullscreen, a band of | |||
| 423 | + | // its own right below the band it is in, so the other columns keep their | |||
| 424 | + | // band and a focus change among them never touches the fullscreen output. | |||
| 425 | + | // A window stacked in a column is expelled into a column of its own first. | |||
| 426 | + | // Leaving fullscreen does not move it back: the column keeps its band, so | |||
| 427 | + | // nothing jumps when the user leaves. Only cascade layouts do this; the | |||
| 428 | + | // focus stays on the same window. | |||
| 429 | + | func (w *Workspace) fullscreenBand(id WindowID) { | |||
| 430 | + | i := w.columnOf(id) | |||
| 431 | + | if !w.policy().wraps || i < 0 { | |||
| 432 | + | return | |||
| 433 | + | } | |||
| 434 | + | b := w.band(i) | |||
| 435 | + | first, last := w.bandBounds(b) | |||
| 436 | + | if len(w.Columns[i].Windows) == 1 && first == last { | |||
| 437 | + | return | |||
| 438 | + | } | |||
| 439 | + | fc := w.Columns[w.Focus] | |||
| 440 | + | focus := fc.Windows[fc.Focus] | |||
| 441 | + | bands := w.bandList() | |||
| 442 | + | col := bands[b][i-first] | |||
| 443 | + | nc := col | |||
| 444 | + | if len(col.Windows) == 1 { | |||
| 445 | + | // The column leaves its lane, as deleteColumn: manual widths of the | |||
| 446 | + | // lane no longer add up. It keeps its slot and its own width. | |||
| 447 | + | w.clearSpans(i) | |||
| 448 | + | bands[b] = slices.Delete(bands[b], i-first, i-first+1) | |||
| 449 | + | } else { | |||
| 450 | + | j := slices.Index(col.Windows, id) | |||
| 451 | + | // The expelled window gets a plain column: no Span (a manual width | |||
| 452 | + | // is the stack's, and the new band is alone), no shares. | |||
| 453 | + | nc = Column{Windows: []WindowID{id}, Cells: col.Cells} | |||
| 454 | + | col.Windows = slices.Delete(slices.Clone(col.Windows), j, j+1) | |||
| 455 | + | col.Shares = nil | |||
| 456 | + | if j == 0 { | |||
| 457 | + | col.Slot = 0 // the slot window left | |||
| 458 | + | } | |||
| 459 | + | if col.Focus > j || col.Focus >= len(col.Windows) { | |||
| 460 | + | col.Focus = max(min(col.Focus-1, len(col.Windows)-1), 0) | |||
| 461 | + | } | |||
| 462 | + | bands[b][i-first] = col | |||
| 463 | + | } | |||
| 464 | + | bands = slices.Insert(bands, b+1, []Column{nc}) | |||
| 465 | + | w.setBands(bands, focus) | |||
| 466 | + | } | |||
| 467 | + | ||||
| 422 | // pushNeighbor makes room in the focused column's full cascade band for a | 468 | // 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 | 469 | // 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 | 470 | // rightmost goes to the band below; from the last column, the leftmost | ||
| @@ -457,13 +503,10 @@ func (w *Workspace) restoreBand() bool { | |||||
| 457 | bands := w.bandList() | 503 | bands := w.bandList() | ||
| 458 | b := w.band(w.Focus) | 504 | b := w.band(w.Focus) | ||
| 459 | room := max(max(w.MaxColumns, 1)-len(bands[b]), 0) | 505 | room := max(max(w.MaxColumns, 1)-len(bands[b]), 0) | ||
| 460 | - | // take removes from band nb the remembered columns found there, at | 506 | + | // 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 | 507 | + | // at most room of them, in the order of ids walked from the focused |
| 462 | // column outward (rev: ids end nearest). | 508 | // column outward (rev: ids end nearest). | ||
| 463 | - | take := func(nb int, ids [][]WindowID, rev bool) []Column { | 509 | + | take := func(nbs []int, ids [][]WindowID, rev bool) []Column { |
| 464 | - | if nb < 0 || nb >= len(bands) { | |||
| 465 | - | return nil | |||
| 466 | - | } | |||
| 467 | var got []Column | 510 | var got []Column | ||
| 468 | for k := range ids { | 511 | for k := range ids { | ||
| 469 | if room == 0 { | 512 | if room == 0 { | ||
| @@ -481,21 +524,37 @@ func (w *Workspace) restoreBand() bool { | |||||
| 481 | if head < 0 { | 524 | if head < 0 { | ||
| 482 | continue | 525 | continue | ||
| 483 | } | 526 | } | ||
| 484 | - | i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] }) | 527 | + | for _, nb := range nbs { |
| 485 | - | if i < 0 { | 528 | + | i := slices.IndexFunc(bands[nb], func(c Column) bool { return c.Windows[0] == e[head] }) |
| 486 | - | 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 | |||
| 487 | } | 536 | } | ||
| 488 | - | got = append(got, bands[nb][i]) | |||
| 489 | - | bands[nb] = slices.Delete(bands[nb], i, i+1) | |||
| 490 | - | room-- | |||
| 491 | } | 537 | } | ||
| 492 | if rev { | 538 | if rev { | ||
| 493 | slices.Reverse(got) | 539 | slices.Reverse(got) | ||
| 494 | } | 540 | } | ||
| 495 | return got | 541 | return got | ||
| 496 | } | 542 | } | ||
| 497 | - | above := take(b-1, up, true) | 543 | + | // reach lists the bands dir away to look in: the adjacent one, and the |
| 498 | - | below := take(b+1, down, false) | 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 | |||
| 555 | + | } | |||
| 556 | + | above := take(reach(-1), up, true) | |||
| 557 | + | below := take(reach(1), down, false) | |||
| 499 | bands[b] = slices.Concat(above, bands[b], below) | 558 | bands[b] = slices.Concat(above, bands[b], below) | ||
| 500 | w.setBands(bands, id) | 559 | w.setBands(bands, id) | ||
| 501 | return true | 560 | return true | ||
A internal/core/cascade_fullscreen_band_test.go +283 -0
| @@ -0,0 +1,283 @@ | |||
| 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("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) | |
| 281 | + | } | |
| 282 | + | }) | |
| 283 | + | } | |
| @@ -0,0 +1,283 @@ | |||||
| 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("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) | |||
| 281 | + | } | |||
| 282 | + | }) | |||
| 283 | + | } | |||
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 +4 -0
| @@ -1335,6 +1335,7 @@ func (w *Workspace) ToggleFullscreen() { | |||
| 1335 | 1335 | w.endFullscreen() | |
| 1336 | 1336 | return | |
| 1337 | 1337 | } | |
| 1338 | + | w.fullscreenBand(id) | |
| 1338 | 1339 | w.fullscreen = id | |
| 1339 | 1340 | w.scroll() | |
| 1340 | 1341 | } | |
| @@ -1444,6 +1445,9 @@ func (w *Workspace) SetFullscreen(id WindowID, on bool) { | |||
| 1444 | 1445 | // Another window covers: the override was for the old cover. | |
| 1445 | 1446 | w.dropStashOver() | |
| 1446 | 1447 | } | |
| 1448 | + | if !w.isFloat(id) && w.fullscreen != id { | |
| 1449 | + | w.fullscreenBand(id) | |
| 1450 | + | } | |
| 1447 | 1451 | w.fullscreen = id | |
| 1448 | 1452 | if !w.isFloat(id) { | |
| 1449 | 1453 | w.scroll() | |
| @@ -1335,6 +1335,7 @@ func (w *Workspace) ToggleFullscreen() { | |||||
| 1335 | w.endFullscreen() | 1335 | w.endFullscreen() | ||
| 1336 | return | 1336 | return | ||
| 1337 | } | 1337 | } | ||
| 1338 | + | w.fullscreenBand(id) | |||
| 1338 | w.fullscreen = id | 1339 | w.fullscreen = id | ||
| 1339 | w.scroll() | 1340 | w.scroll() | ||
| 1340 | } | 1341 | } | ||
| @@ -1444,6 +1445,9 @@ func (w *Workspace) SetFullscreen(id WindowID, on bool) { | |||||
| 1444 | // Another window covers: the override was for the old cover. | 1445 | // Another window covers: the override was for the old cover. | ||
| 1445 | w.dropStashOver() | 1446 | w.dropStashOver() | ||
| 1446 | } | 1447 | } | ||
| 1448 | + | if !w.isFloat(id) && w.fullscreen != id { | |||
| 1449 | + | w.fullscreenBand(id) | |||
| 1450 | + | } | |||
| 1447 | w.fullscreen = id | 1451 | w.fullscreen = id | ||
| 1448 | if !w.isFloat(id) { | 1452 | if !w.isFloat(id) { | ||
| 1449 | w.scroll() | 1453 | 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 | } | ||