Merge branch 'fix/fullscreen-float-band' into main (!34)
fix(core): give fullscreen cascade floats their own band
7 files · +254 -11
M docs/config.md +2 -2
| @@ -302,14 +302,14 @@ With `keyboard.cmd = alt` or `ctrl`, the defaults that would combine `cmd` with | |||
| 302 | 302 | ||
| 303 | 303 | The default layout. Columns sit side by side in bands of up to `max-columns`, stacked one below the other. Each band fills the screen height and keeps its columns until you move them. | |
| 304 | 304 | ||
| 305 | - | - Left/right focus moves within a band, then to the neighbor monitor, then to the neighbor workspace. | |
| 305 | + | - Left/right focus moves within a band, then to an occupied neighbor workspace, then to the neighbor monitor, then to the trailing empty workspace. From a fullscreen float it crosses that edge without revealing the tiles underneath; only up/down reaches another band. | |
| 306 | 306 | - `move-column-left/right` stays in the band; at its edge the column goes to the neighbor monitor, if any. | |
| 307 | 307 | - Up/down moves through the windows of a column, then to the band above or below. | |
| 308 | 308 | - `move-window-up/down` past the column's top or bottom sends the whole column to the band above or below. It joins that band when it has room, else it gets a new band between the two. | |
| 309 | 309 | - A new window joins the focused band when it has room, else it gets a new band right below. | |
| 310 | 310 | - `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. | |
| 311 | 311 | - `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. | |
| 312 | - | - 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. | |
| 312 | + | - 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. A parentless, non-stashed floating window becomes a tiled column in its own band below the selected column's band on fullscreen entry, including `toggle-fullscreen`; dialogs with a parent and stash entries stay floating. | |
| 313 | 313 | - `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. | |
| 314 | 314 | - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it. | |
| 315 | 315 | ||
| @@ -302,14 +302,14 @@ With `keyboard.cmd = alt` or `ctrl`, the defaults that would combine `cmd` with | |||||
| 302 | 302 | ||||
| 303 | The default layout. Columns sit side by side in bands of up to `max-columns`, stacked one below the other. Each band fills the screen height and keeps its columns until you move them. | 303 | The default layout. Columns sit side by side in bands of up to `max-columns`, stacked one below the other. Each band fills the screen height and keeps its columns until you move them. | ||
| 304 | 304 | ||||
| 305 | - | - Left/right focus moves within a band, then to the neighbor monitor, then to the neighbor workspace. | 305 | + | - Left/right focus moves within a band, then to an occupied neighbor workspace, then to the neighbor monitor, then to the trailing empty workspace. From a fullscreen float it crosses that edge without revealing the tiles underneath; only up/down reaches another band. |
| 306 | - `move-column-left/right` stays in the band; at its edge the column goes to the neighbor monitor, if any. | 306 | - `move-column-left/right` stays in the band; at its edge the column goes to the neighbor monitor, if any. | ||
| 307 | - Up/down moves through the windows of a column, then to the band above or below. | 307 | - Up/down moves through the windows of a column, then to the band above or below. | ||
| 308 | - `move-window-up/down` past the column's top or bottom sends the whole column to the band above or below. It joins that band when it has room, else it gets a new band between the two. | 308 | - `move-window-up/down` past the column's top or bottom sends the whole column to the band above or below. It joins that band when it has room, else it gets a new band between the two. | ||
| 309 | - A new window joins the focused band when it has room, else it gets a new band right below. | 309 | - A new window joins the focused band when it has room, else it gets a new band right below. | ||
| 310 | - `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. | 310 | - `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. | ||
| 311 | - `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. | 311 | - `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. | ||
| 312 | - | - 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. | 312 | + | - 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. A parentless, non-stashed floating window becomes a tiled column in its own band below the selected column's band on fullscreen entry, including `toggle-fullscreen`; dialogs with a parent and stash entries stay floating. |
| 313 | - `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. | 313 | - `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. | ||
| 314 | - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it. | 314 | - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it. | ||
| 315 | 315 | ||||
M internal/adapters/wayland/xdg.go +2 -0
| @@ -400,11 +400,13 @@ func (t top) SetMinSize(r *xdgshell.Toplevel, w, h int32) { | |||
| 400 | 400 | func (top) SetMaximized(*xdgshell.Toplevel) {} | |
| 401 | 401 | func (top) UnsetMaximized(*xdgshell.Toplevel) {} | |
| 402 | 402 | func (t top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) { | |
| 403 | + | t.w.xdg.server.log.Info().Uint64("id", uint64(t.w.id)).Bool("on", true).Msg("client fullscreen request") | |
| 403 | 404 | if t.w.mapped { | |
| 404 | 405 | t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id, Fullscreen: true}) | |
| 405 | 406 | } | |
| 406 | 407 | } | |
| 407 | 408 | func (t top) UnsetFullscreen(*xdgshell.Toplevel) { | |
| 409 | + | t.w.xdg.server.log.Info().Uint64("id", uint64(t.w.id)).Bool("on", false).Msg("client fullscreen request") | |
| 408 | 410 | if t.w.mapped { | |
| 409 | 411 | t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id}) | |
| 410 | 412 | } | |
| @@ -400,11 +400,13 @@ func (t top) SetMinSize(r *xdgshell.Toplevel, w, h int32) { | |||||
| 400 | func (top) SetMaximized(*xdgshell.Toplevel) {} | 400 | func (top) SetMaximized(*xdgshell.Toplevel) {} | ||
| 401 | func (top) UnsetMaximized(*xdgshell.Toplevel) {} | 401 | func (top) UnsetMaximized(*xdgshell.Toplevel) {} | ||
| 402 | func (t top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) { | 402 | func (t top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) { | ||
| 403 | + | t.w.xdg.server.log.Info().Uint64("id", uint64(t.w.id)).Bool("on", true).Msg("client fullscreen request") | |||
| 403 | if t.w.mapped { | 404 | if t.w.mapped { | ||
| 404 | t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id, Fullscreen: true}) | 405 | t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id, Fullscreen: true}) | ||
| 405 | } | 406 | } | ||
| 406 | } | 407 | } | ||
| 407 | func (t top) UnsetFullscreen(*xdgshell.Toplevel) { | 408 | func (t top) UnsetFullscreen(*xdgshell.Toplevel) { | ||
| 409 | + | t.w.xdg.server.log.Info().Uint64("id", uint64(t.w.id)).Bool("on", false).Msg("client fullscreen request") | |||
| 408 | if t.w.mapped { | 410 | if t.w.mapped { | ||
| 409 | t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id}) | 411 | t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id}) | ||
| 410 | } | 412 | } | ||
M internal/core/action.go +7 -0
| @@ -323,6 +323,13 @@ func (c *Core) dispatchAction(a Action) Effect { | |||
| 323 | 323 | if a == ActionFocusColumnLeft { | |
| 324 | 324 | dir, d = -1, dirLeft | |
| 325 | 325 | } | |
| 326 | + | // A fullscreen float has no column in its band. Horizontal moves | |
| 327 | + | // use the existing workspace/monitor edge order; only up/down may | |
| 328 | + | // reveal the tiles below. | |
| 329 | + | if w.policy().wraps && w.cover() != 0 && w.isFloat(w.cover()) && !w.stashOverCover() && !w.coverDialogFocused() { | |
| 330 | + | c.edgeFocus(horizontalAxis, dir, d) | |
| 331 | + | return Effect{} | |
| 332 | + | } | |
| 326 | 333 | // A covering fullscreen window is left for the window on that side; | |
| 327 | 334 | // with none (the workspace, band or stash edge), the move goes past | |
| 328 | 335 | // the edge (see edgeFocus) and fullscreen stays. | |
| @@ -323,6 +323,13 @@ func (c *Core) dispatchAction(a Action) Effect { | |||||
| 323 | if a == ActionFocusColumnLeft { | 323 | if a == ActionFocusColumnLeft { | ||
| 324 | dir, d = -1, dirLeft | 324 | dir, d = -1, dirLeft | ||
| 325 | } | 325 | } | ||
| 326 | + | // A fullscreen float has no column in its band. Horizontal moves | |||
| 327 | + | // use the existing workspace/monitor edge order; only up/down may | |||
| 328 | + | // reveal the tiles below. | |||
| 329 | + | if w.policy().wraps && w.cover() != 0 && w.isFloat(w.cover()) && !w.stashOverCover() && !w.coverDialogFocused() { | |||
| 330 | + | c.edgeFocus(horizontalAxis, dir, d) | |||
| 331 | + | return Effect{} | |||
| 332 | + | } | |||
| 326 | // A covering fullscreen window is left for the window on that side; | 333 | // A covering fullscreen window is left for the window on that side; | ||
| 327 | // with none (the workspace, band or stash edge), the move goes past | 334 | // with none (the workspace, band or stash edge), the move goes past | ||
| 328 | // the edge (see edgeFocus) and fullscreen stays. | 335 | // the edge (see edgeFocus) and fullscreen stays. | ||
M internal/core/cascade.go +37 -7
| @@ -491,7 +491,8 @@ func (w *Workspace) pushNeighbor() { | |||
| 491 | 491 | } | |
| 492 | 492 | ||
| 493 | 493 | // restoreBand brings back the columns isolateBand or pushNeighbor sent | |
| 494 | - | // away, those still in the adjacent band, as many as the band has room | |
| 494 | + | // away, those still in the adjacent ordinary band (across automatic | |
| 495 | + | // own-band isolates), as many as the band has room | |
| 495 | 496 | // for, nearest to the focused column first: the ones sent up at the start | |
| 496 | 497 | // of the band, the ones sent down at its end, in their former order. The | |
| 497 | 498 | // others stay where they are. It reports whether the column remembered | |
| @@ -507,6 +508,13 @@ func (w *Workspace) restoreBand() bool { | |||
| 507 | 508 | bands := w.bandList() | |
| 508 | 509 | b := w.band(w.Focus) | |
| 509 | 510 | room := max(max(w.MaxColumns, 1)-len(bands[b]), 0) | |
| 511 | + | // Automatically isolated fullscreen bands do not separate a column | |
| 512 | + | // from its remembered neighbors. User-shaped bands still do: ordinary | |
| 513 | + | // ones and one whose column the user maximized. | |
| 514 | + | ownOnly := make([]bool, len(bands)) | |
| 515 | + | for i, band := range bands { | |
| 516 | + | ownOnly[i] = len(band) > 0 && !slices.ContainsFunc(band, func(c Column) bool { return !c.ownBand || c.FullWidth }) | |
| 517 | + | } | |
| 510 | 518 | // take removes from the bands nbs the remembered columns found there, | |
| 511 | 519 | // at most room of them, in the order of ids walked from the focused | |
| 512 | 520 | // column outward (rev: ids end nearest). | |
| @@ -529,10 +537,10 @@ func (w *Workspace) restoreBand() bool { | |||
| 529 | 537 | continue | |
| 530 | 538 | } | |
| 531 | 539 | 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. | |
| 540 | + | adjacent := !slices.ContainsFunc(nbs[:k], func(n int) bool { return !ownOnly[n] }) | |
| 534 | 541 | i := slices.IndexFunc(bands[nb], func(c Column) bool { | |
| 535 | - | return c.Windows[0] == e[head] && (k == 0 || c.ownBand) | |
| 542 | + | // A column the user maximized keeps its band. | |
| 543 | + | return c.Windows[0] == e[head] && !c.FullWidth && (adjacent || c.ownBand) | |
| 536 | 544 | }) | |
| 537 | 545 | if i < 0 { | |
| 538 | 546 | continue | |
| @@ -548,13 +556,13 @@ func (w *Workspace) restoreBand() bool { | |||
| 548 | 556 | } | |
| 549 | 557 | return got | |
| 550 | 558 | } | |
| 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. | |
| 559 | + | // reach crosses automatic own-band isolates, but looks only one band | |
| 560 | + | // beyond the first ordinary band, as before: user moves remain boundaries. | |
| 553 | 561 | reach := func(dir int) []int { | |
| 554 | 562 | var nbs []int | |
| 555 | 563 | for n := b + dir; n >= 0 && n < len(bands); n += dir { | |
| 556 | 564 | nbs = append(nbs, n) | |
| 557 | - | if len(nbs) == 2 { | |
| 565 | + | if len(nbs) > 1 && !ownOnly[n-dir] { | |
| 558 | 566 | break | |
| 559 | 567 | } | |
| 560 | 568 | } | |
| @@ -598,3 +606,25 @@ func (w *Workspace) forgetBands() { | |||
| 598 | 606 | c.BandStart, c.pushedUp, c.pushedDown = false, nil, nil | |
| 599 | 607 | } | |
| 600 | 608 | } | |
| 609 | + | ||
| 610 | + | // fullscreenFloatBand promotes a parentless float into a permanent cascade | |
| 611 | + | // column below the selected column's band. Dialogs and stash entries keep | |
| 612 | + | // their floating placement. Like fullscreenBand, this never changes focus. | |
| 613 | + | func (w *Workspace) fullscreenFloatBand(id WindowID) { | |
| 614 | + | i := w.floatIndex(id) | |
| 615 | + | if !w.policy().wraps || i < 0 || w.Floats[i].over != 0 || w.stashIndex(id) >= 0 { | |
| 616 | + | return | |
| 617 | + | } | |
| 618 | + | focus, _ := w.Focused() | |
| 619 | + | bands := w.bandList() | |
| 620 | + | at := 0 | |
| 621 | + | if len(w.Columns) > 0 { | |
| 622 | + | at = w.band(w.Focus) + 1 | |
| 623 | + | } | |
| 624 | + | w.Floats = slices.Delete(w.Floats, i, i+1) | |
| 625 | + | bands = slices.Insert(bands, at, []Column{{Windows: []WindowID{id}, ownBand: true}}) | |
| 626 | + | w.setBands(bands, focus) | |
| 627 | + | if focus == id { | |
| 628 | + | w.floatFocus, w.stashFocus = false, false | |
| 629 | + | } | |
| 630 | + | } | |
| @@ -491,7 +491,8 @@ func (w *Workspace) pushNeighbor() { | |||||
| 491 | } | 491 | } | ||
| 492 | 492 | ||||
| 493 | // restoreBand brings back the columns isolateBand or pushNeighbor sent | 493 | // restoreBand brings back the columns isolateBand or pushNeighbor sent | ||
| 494 | - | // away, those still in the adjacent band, as many as the band has room | 494 | + | // away, those still in the adjacent ordinary band (across automatic |
| 495 | + | // own-band isolates), as many as the band has room | |||
| 495 | // for, nearest to the focused column first: the ones sent up at the start | 496 | // for, nearest to the focused column first: the ones sent up at the start | ||
| 496 | // of the band, the ones sent down at its end, in their former order. The | 497 | // of the band, the ones sent down at its end, in their former order. The | ||
| 497 | // others stay where they are. It reports whether the column remembered | 498 | // others stay where they are. It reports whether the column remembered | ||
| @@ -507,6 +508,13 @@ func (w *Workspace) restoreBand() bool { | |||||
| 507 | bands := w.bandList() | 508 | bands := w.bandList() | ||
| 508 | b := w.band(w.Focus) | 509 | b := w.band(w.Focus) | ||
| 509 | room := max(max(w.MaxColumns, 1)-len(bands[b]), 0) | 510 | room := max(max(w.MaxColumns, 1)-len(bands[b]), 0) | ||
| 511 | + | // Automatically isolated fullscreen bands do not separate a column | |||
| 512 | + | // from its remembered neighbors. User-shaped bands still do: ordinary | |||
| 513 | + | // ones and one whose column the user maximized. | |||
| 514 | + | ownOnly := make([]bool, len(bands)) | |||
| 515 | + | for i, band := range bands { | |||
| 516 | + | ownOnly[i] = len(band) > 0 && !slices.ContainsFunc(band, func(c Column) bool { return !c.ownBand || c.FullWidth }) | |||
| 517 | + | } | |||
| 510 | // take removes from the bands nbs the remembered columns found there, | 518 | // 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 | 519 | // at most room of them, in the order of ids walked from the focused | ||
| 512 | // column outward (rev: ids end nearest). | 520 | // column outward (rev: ids end nearest). | ||
| @@ -529,10 +537,10 @@ func (w *Workspace) restoreBand() bool { | |||||
| 529 | continue | 537 | continue | ||
| 530 | } | 538 | } | ||
| 531 | for k, nb := range nbs { | 539 | for k, nb := range nbs { | ||
| 532 | - | // Past the adjacent band, only a column fullscreenBand put in | 540 | + | adjacent := !slices.ContainsFunc(nbs[:k], func(n int) bool { return !ownOnly[n] }) |
| 533 | - | // a band of its own is taken: one the user moved there stays. | |||
| 534 | i := slices.IndexFunc(bands[nb], func(c Column) bool { | 541 | i := slices.IndexFunc(bands[nb], func(c Column) bool { | ||
| 535 | - | return c.Windows[0] == e[head] && (k == 0 || c.ownBand) | 542 | + | // A column the user maximized keeps its band. |
| 543 | + | return c.Windows[0] == e[head] && !c.FullWidth && (adjacent || c.ownBand) | |||
| 536 | }) | 544 | }) | ||
| 537 | if i < 0 { | 545 | if i < 0 { | ||
| 538 | continue | 546 | continue | ||
| @@ -548,13 +556,13 @@ func (w *Workspace) restoreBand() bool { | |||||
| 548 | } | 556 | } | ||
| 549 | return got | 557 | return got | ||
| 550 | } | 558 | } | ||
| 551 | - | // reach lists the bands dir away to look in: the adjacent one, and the | 559 | + | // reach crosses automatic own-band isolates, but looks only one band |
| 552 | - | // next, where a column that fullscreenBand split off may lie. | 560 | + | // beyond the first ordinary band, as before: user moves remain boundaries. |
| 553 | reach := func(dir int) []int { | 561 | reach := func(dir int) []int { | ||
| 554 | var nbs []int | 562 | var nbs []int | ||
| 555 | for n := b + dir; n >= 0 && n < len(bands); n += dir { | 563 | for n := b + dir; n >= 0 && n < len(bands); n += dir { | ||
| 556 | nbs = append(nbs, n) | 564 | nbs = append(nbs, n) | ||
| 557 | - | if len(nbs) == 2 { | 565 | + | if len(nbs) > 1 && !ownOnly[n-dir] { |
| 558 | break | 566 | break | ||
| 559 | } | 567 | } | ||
| 560 | } | 568 | } | ||
| @@ -598,3 +606,25 @@ func (w *Workspace) forgetBands() { | |||||
| 598 | c.BandStart, c.pushedUp, c.pushedDown = false, nil, nil | 606 | c.BandStart, c.pushedUp, c.pushedDown = false, nil, nil | ||
| 599 | } | 607 | } | ||
| 600 | } | 608 | } | ||
| 609 | + | ||||
| 610 | + | // fullscreenFloatBand promotes a parentless float into a permanent cascade | |||
| 611 | + | // column below the selected column's band. Dialogs and stash entries keep | |||
| 612 | + | // their floating placement. Like fullscreenBand, this never changes focus. | |||
| 613 | + | func (w *Workspace) fullscreenFloatBand(id WindowID) { | |||
| 614 | + | i := w.floatIndex(id) | |||
| 615 | + | if !w.policy().wraps || i < 0 || w.Floats[i].over != 0 || w.stashIndex(id) >= 0 { | |||
| 616 | + | return | |||
| 617 | + | } | |||
| 618 | + | focus, _ := w.Focused() | |||
| 619 | + | bands := w.bandList() | |||
| 620 | + | at := 0 | |||
| 621 | + | if len(w.Columns) > 0 { | |||
| 622 | + | at = w.band(w.Focus) + 1 | |||
| 623 | + | } | |||
| 624 | + | w.Floats = slices.Delete(w.Floats, i, i+1) | |||
| 625 | + | bands = slices.Insert(bands, at, []Column{{Windows: []WindowID{id}, ownBand: true}}) | |||
| 626 | + | w.setBands(bands, focus) | |||
| 627 | + | if focus == id { | |||
| 628 | + | w.floatFocus, w.stashFocus = false, false | |||
| 629 | + | } | |||
| 630 | + | } | |||
M internal/core/cascade_fullscreen_band_test.go +148 -2
| @@ -100,7 +100,7 @@ func TestFullscreenFloatMovesToNeighborMonitor(t *testing.T) { | |||
| 100 | 100 | }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} { | |
| 101 | 101 | t.Run(tc.screen, func(t *testing.T) { | |
| 102 | 102 | c, w := cascadeMovesCore(t, 2, 3, true) | |
| 103 | - | w.AddFloating(9, 5120, 2160) | |
| 103 | + | w.AddDialog(9, 2, 5120, 2160) | |
| 104 | 104 | w.SetFullscreen(9, true) | |
| 105 | 105 | c.applyAction(tc.action) | |
| 106 | 106 | dst := screenWorkspace(c, tc.screen) | |
| @@ -115,7 +115,7 @@ func TestFullscreenFloatMovesToNeighborMonitor(t *testing.T) { | |||
| 115 | 115 | ||
| 116 | 116 | func TestFullscreenFloatNoNeighborStays(t *testing.T) { | |
| 117 | 117 | c, w := cascadeMovesCore(t, 2, 3, false) | |
| 118 | - | w.AddFloating(9, 800, 600) | |
| 118 | + | w.AddDialog(9, 2, 800, 600) | |
| 119 | 119 | w.SetFullscreen(9, true) | |
| 120 | 120 | c.applyAction(ActionMoveColumnLeft) | |
| 121 | 121 | if w.floatIndex(9) < 0 || w.fullscreen != 9 { | |
| @@ -295,3 +295,149 @@ func TestRestoreBandDoesNotLookPastUserBand(t *testing.T) { | |||
| 295 | 295 | w.ToggleFullWidth() | |
| 296 | 296 | wantBands(t, w, []WindowID{1, 3}, []WindowID{2}) | |
| 297 | 297 | } | |
| 298 | + | ||
| 299 | + | func TestCascadeFullscreenHorizontalKeepsCover(t *testing.T) { | |
| 300 | + | for _, floating := range []bool{false, true} { | |
| 301 | + | t.Run(map[bool]string{false: "tile", true: "dialog"}[floating], func(t *testing.T) { | |
| 302 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |
| 303 | + | id := WindowID(2) | |
| 304 | + | if floating { | |
| 305 | + | id = 9 | |
| 306 | + | w.AddDialog(id, 2, 800, 600) | |
| 307 | + | } | |
| 308 | + | w.SetFullscreen(id, true) | |
| 309 | + | c.applyAction(ActionFocusColumnLeft) | |
| 310 | + | if c.cur().name() != "L" || w.fullscreen != id { | |
| 311 | + | t.Fatalf("screen %+v fullscreen %d", c.cur(), w.fullscreen) | |
| 312 | + | } | |
| 313 | + | }) | |
| 314 | + | } | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | func TestCascadeFullscreenUpReachesPreviousBand(t *testing.T) { | |
| 318 | + | c, w := cascadeMovesCore(t, 2, 3, false) | |
| 319 | + | w.ToggleFullscreen() | |
| 320 | + | c.applyAction(ActionFocusWindowUp) | |
| 321 | + | if w.fullscreen != 0 { | |
| 322 | + | t.Fatalf("fullscreen %d", w.fullscreen) | |
| 323 | + | } | |
| 324 | + | wantFocus(t, c, "M", 1) | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | func TestCascadeFullscreenFloatEdgePrefersWorkspace(t *testing.T) { | |
| 328 | + | c, _ := cascadeMovesCore(t, 1, 3, true) | |
| 329 | + | m := c.cur().mon | |
| 330 | + | m.Focus(1) | |
| 331 | + | m.AddWindow(2) | |
| 332 | + | w := m.Current() | |
| 333 | + | w.Overflow = OverflowCascade | |
| 334 | + | w.AddDialog(9, 2, 800, 600) | |
| 335 | + | w.SetFullscreen(9, true) | |
| 336 | + | c.applyAction(ActionFocusColumnLeft) | |
| 337 | + | wantFocus(t, c, "M", 1) | |
| 338 | + | if w.fullscreen != 9 { | |
| 339 | + | t.Fatalf("fullscreen %d", w.fullscreen) | |
| 340 | + | } | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | func TestCascadeFullscreenFloatChildHorizontalReturnsToCover(t *testing.T) { | |
| 344 | + | for _, sides := range []bool{false, true} { | |
| 345 | + | for _, a := range []Action{ActionFocusColumnLeft, ActionFocusColumnRight} { | |
| 346 | + | t.Run(string(a)+map[bool]string{false: "/no-edge", true: "/edge"}[sides], func(t *testing.T) { | |
| 347 | + | c, w := cascadeMovesCore(t, 2, 3, sides) | |
| 348 | + | w.AddDialog(9, 2, 800, 600) | |
| 349 | + | w.SetFullscreen(9, true) | |
| 350 | + | w.AddDialog(10, 9, 100, 100) | |
| 351 | + | c.applyAction(a) | |
| 352 | + | wantFocus(t, c, "M", 9) | |
| 353 | + | if w.fullscreen != 9 { | |
| 354 | + | t.Fatalf("fullscreen %d", w.fullscreen) | |
| 355 | + | } | |
| 356 | + | }) | |
| 357 | + | } | |
| 358 | + | } | |
| 359 | + | } | |
| 360 | + | ||
| 361 | + | func TestRestoreBandAcrossPromotedFloatBand(t *testing.T) { | |
| 362 | + | for _, tc := range []struct { | |
| 363 | + | name string | |
| 364 | + | maximize bool // the user maximizes the promoted column before 1 comes back | |
| 365 | + | want [][]WindowID | |
| 366 | + | }{ | |
| 367 | + | {"an automatic band is crossed", false, [][]WindowID{{1, 2, 3}, {9}}}, | |
| 368 | + | {"a maximized band stops the restore", true, [][]WindowID{{1}, {9}, {2, 3}}}, | |
| 369 | + | } { | |
| 370 | + | t.Run(tc.name, func(t *testing.T) { | |
| 371 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |
| 372 | + | w.FocusID(1) | |
| 373 | + | w.ToggleFullWidth() | |
| 374 | + | w.AddFloating(9, 800, 600) | |
| 375 | + | w.SetFullscreen(9, true) | |
| 376 | + | wantBands(t, w, []WindowID{1}, []WindowID{9}, []WindowID{2, 3}) | |
| 377 | + | w.SetFullscreen(9, false) | |
| 378 | + | if tc.maximize { | |
| 379 | + | w.FocusID(9) | |
| 380 | + | w.ToggleFullWidth() | |
| 381 | + | } | |
| 382 | + | w.FocusID(1) | |
| 383 | + | w.ToggleFullWidth() | |
| 384 | + | wantBands(t, w, tc.want...) | |
| 385 | + | }) | |
| 386 | + | } | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | // A remembered neighbor the user maximized since keeps its band; the | |
| 390 | + | // ordinary neighbors still come back. | |
| 391 | + | func TestRestoreBandLeavesMaximizedNeighbor(t *testing.T) { | |
| 392 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |
| 393 | + | w.FocusID(1) | |
| 394 | + | w.ToggleFullWidth() | |
| 395 | + | wantBands(t, w, []WindowID{1}, []WindowID{2, 3}) | |
| 396 | + | w.FocusID(2) | |
| 397 | + | w.ToggleFullscreen() | |
| 398 | + | w.ToggleFullscreen() | |
| 399 | + | w.ToggleFullWidth() | |
| 400 | + | wantBands(t, w, []WindowID{1}, []WindowID{3}, []WindowID{2}) | |
| 401 | + | w.FocusID(1) | |
| 402 | + | w.ToggleFullWidth() | |
| 403 | + | wantBands(t, w, []WindowID{1, 3}, []WindowID{2}) | |
| 404 | + | if c := w.columnOf(2); c < 0 || !w.Columns[c].FullWidth { | |
| 405 | + | t.Fatal("column 2 lost its maximization") | |
| 406 | + | } | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | func TestPromotedFullscreenFloatMovesToNeighborMonitor(t *testing.T) { | |
| 410 | + | for _, tc := range []struct { | |
| 411 | + | a Action | |
| 412 | + | screen string | |
| 413 | + | }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} { | |
| 414 | + | t.Run(tc.screen, func(t *testing.T) { | |
| 415 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |
| 416 | + | w.AddFloating(9, 800, 600) | |
| 417 | + | w.SetFullscreen(9, true) | |
| 418 | + | c.applyAction(tc.a) | |
| 419 | + | dst := screenWorkspace(c, tc.screen) | |
| 420 | + | wantFocus(t, c, tc.screen, 9) | |
| 421 | + | wantColumns(t, w, 1, 2) | |
| 422 | + | if dst.isFloat(9) || dst.columnOf(9) < 0 || dst.fullscreen != 9 || w.fullscreen != 0 { | |
| 423 | + | t.Fatalf("destination %+v", dst) | |
| 424 | + | } | |
| 425 | + | }) | |
| 426 | + | } | |
| 427 | + | } | |
| 428 | + | ||
| 429 | + | func TestUserFloatedToggleFullscreenPromotes(t *testing.T) { | |
| 430 | + | _, w := cascadeMovesCore(t, 2, 3, false) | |
| 431 | + | w.ToggleFloating() | |
| 432 | + | if !w.isFloat(2) { | |
| 433 | + | t.Fatal("setup not floating") | |
| 434 | + | } | |
| 435 | + | w.ToggleFullscreen() | |
| 436 | + | if w.isFloat(2) || w.fullscreen != 2 { | |
| 437 | + | t.Fatal("user float not promoted") | |
| 438 | + | } | |
| 439 | + | wantBands(t, w, []WindowID{1}, []WindowID{2}) | |
| 440 | + | w.SetFullscreen(2, true) | |
| 441 | + | w.SetFullscreen(2, true) | |
| 442 | + | wantBands(t, w, []WindowID{1}, []WindowID{2}) | |
| 443 | + | } | |
| @@ -100,7 +100,7 @@ func TestFullscreenFloatMovesToNeighborMonitor(t *testing.T) { | |||||
| 100 | }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} { | 100 | }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} { | ||
| 101 | t.Run(tc.screen, func(t *testing.T) { | 101 | t.Run(tc.screen, func(t *testing.T) { | ||
| 102 | c, w := cascadeMovesCore(t, 2, 3, true) | 102 | c, w := cascadeMovesCore(t, 2, 3, true) | ||
| 103 | - | w.AddFloating(9, 5120, 2160) | 103 | + | w.AddDialog(9, 2, 5120, 2160) |
| 104 | w.SetFullscreen(9, true) | 104 | w.SetFullscreen(9, true) | ||
| 105 | c.applyAction(tc.action) | 105 | c.applyAction(tc.action) | ||
| 106 | dst := screenWorkspace(c, tc.screen) | 106 | dst := screenWorkspace(c, tc.screen) | ||
| @@ -115,7 +115,7 @@ func TestFullscreenFloatMovesToNeighborMonitor(t *testing.T) { | |||||
| 115 | 115 | ||||
| 116 | func TestFullscreenFloatNoNeighborStays(t *testing.T) { | 116 | func TestFullscreenFloatNoNeighborStays(t *testing.T) { | ||
| 117 | c, w := cascadeMovesCore(t, 2, 3, false) | 117 | c, w := cascadeMovesCore(t, 2, 3, false) | ||
| 118 | - | w.AddFloating(9, 800, 600) | 118 | + | w.AddDialog(9, 2, 800, 600) |
| 119 | w.SetFullscreen(9, true) | 119 | w.SetFullscreen(9, true) | ||
| 120 | c.applyAction(ActionMoveColumnLeft) | 120 | c.applyAction(ActionMoveColumnLeft) | ||
| 121 | if w.floatIndex(9) < 0 || w.fullscreen != 9 { | 121 | if w.floatIndex(9) < 0 || w.fullscreen != 9 { | ||
| @@ -295,3 +295,149 @@ func TestRestoreBandDoesNotLookPastUserBand(t *testing.T) { | |||||
| 295 | w.ToggleFullWidth() | 295 | w.ToggleFullWidth() | ||
| 296 | wantBands(t, w, []WindowID{1, 3}, []WindowID{2}) | 296 | wantBands(t, w, []WindowID{1, 3}, []WindowID{2}) | ||
| 297 | } | 297 | } | ||
| 298 | + | ||||
| 299 | + | func TestCascadeFullscreenHorizontalKeepsCover(t *testing.T) { | |||
| 300 | + | for _, floating := range []bool{false, true} { | |||
| 301 | + | t.Run(map[bool]string{false: "tile", true: "dialog"}[floating], func(t *testing.T) { | |||
| 302 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |||
| 303 | + | id := WindowID(2) | |||
| 304 | + | if floating { | |||
| 305 | + | id = 9 | |||
| 306 | + | w.AddDialog(id, 2, 800, 600) | |||
| 307 | + | } | |||
| 308 | + | w.SetFullscreen(id, true) | |||
| 309 | + | c.applyAction(ActionFocusColumnLeft) | |||
| 310 | + | if c.cur().name() != "L" || w.fullscreen != id { | |||
| 311 | + | t.Fatalf("screen %+v fullscreen %d", c.cur(), w.fullscreen) | |||
| 312 | + | } | |||
| 313 | + | }) | |||
| 314 | + | } | |||
| 315 | + | } | |||
| 316 | + | ||||
| 317 | + | func TestCascadeFullscreenUpReachesPreviousBand(t *testing.T) { | |||
| 318 | + | c, w := cascadeMovesCore(t, 2, 3, false) | |||
| 319 | + | w.ToggleFullscreen() | |||
| 320 | + | c.applyAction(ActionFocusWindowUp) | |||
| 321 | + | if w.fullscreen != 0 { | |||
| 322 | + | t.Fatalf("fullscreen %d", w.fullscreen) | |||
| 323 | + | } | |||
| 324 | + | wantFocus(t, c, "M", 1) | |||
| 325 | + | } | |||
| 326 | + | ||||
| 327 | + | func TestCascadeFullscreenFloatEdgePrefersWorkspace(t *testing.T) { | |||
| 328 | + | c, _ := cascadeMovesCore(t, 1, 3, true) | |||
| 329 | + | m := c.cur().mon | |||
| 330 | + | m.Focus(1) | |||
| 331 | + | m.AddWindow(2) | |||
| 332 | + | w := m.Current() | |||
| 333 | + | w.Overflow = OverflowCascade | |||
| 334 | + | w.AddDialog(9, 2, 800, 600) | |||
| 335 | + | w.SetFullscreen(9, true) | |||
| 336 | + | c.applyAction(ActionFocusColumnLeft) | |||
| 337 | + | wantFocus(t, c, "M", 1) | |||
| 338 | + | if w.fullscreen != 9 { | |||
| 339 | + | t.Fatalf("fullscreen %d", w.fullscreen) | |||
| 340 | + | } | |||
| 341 | + | } | |||
| 342 | + | ||||
| 343 | + | func TestCascadeFullscreenFloatChildHorizontalReturnsToCover(t *testing.T) { | |||
| 344 | + | for _, sides := range []bool{false, true} { | |||
| 345 | + | for _, a := range []Action{ActionFocusColumnLeft, ActionFocusColumnRight} { | |||
| 346 | + | t.Run(string(a)+map[bool]string{false: "/no-edge", true: "/edge"}[sides], func(t *testing.T) { | |||
| 347 | + | c, w := cascadeMovesCore(t, 2, 3, sides) | |||
| 348 | + | w.AddDialog(9, 2, 800, 600) | |||
| 349 | + | w.SetFullscreen(9, true) | |||
| 350 | + | w.AddDialog(10, 9, 100, 100) | |||
| 351 | + | c.applyAction(a) | |||
| 352 | + | wantFocus(t, c, "M", 9) | |||
| 353 | + | if w.fullscreen != 9 { | |||
| 354 | + | t.Fatalf("fullscreen %d", w.fullscreen) | |||
| 355 | + | } | |||
| 356 | + | }) | |||
| 357 | + | } | |||
| 358 | + | } | |||
| 359 | + | } | |||
| 360 | + | ||||
| 361 | + | func TestRestoreBandAcrossPromotedFloatBand(t *testing.T) { | |||
| 362 | + | for _, tc := range []struct { | |||
| 363 | + | name string | |||
| 364 | + | maximize bool // the user maximizes the promoted column before 1 comes back | |||
| 365 | + | want [][]WindowID | |||
| 366 | + | }{ | |||
| 367 | + | {"an automatic band is crossed", false, [][]WindowID{{1, 2, 3}, {9}}}, | |||
| 368 | + | {"a maximized band stops the restore", true, [][]WindowID{{1}, {9}, {2, 3}}}, | |||
| 369 | + | } { | |||
| 370 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 371 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |||
| 372 | + | w.FocusID(1) | |||
| 373 | + | w.ToggleFullWidth() | |||
| 374 | + | w.AddFloating(9, 800, 600) | |||
| 375 | + | w.SetFullscreen(9, true) | |||
| 376 | + | wantBands(t, w, []WindowID{1}, []WindowID{9}, []WindowID{2, 3}) | |||
| 377 | + | w.SetFullscreen(9, false) | |||
| 378 | + | if tc.maximize { | |||
| 379 | + | w.FocusID(9) | |||
| 380 | + | w.ToggleFullWidth() | |||
| 381 | + | } | |||
| 382 | + | w.FocusID(1) | |||
| 383 | + | w.ToggleFullWidth() | |||
| 384 | + | wantBands(t, w, tc.want...) | |||
| 385 | + | }) | |||
| 386 | + | } | |||
| 387 | + | } | |||
| 388 | + | ||||
| 389 | + | // A remembered neighbor the user maximized since keeps its band; the | |||
| 390 | + | // ordinary neighbors still come back. | |||
| 391 | + | func TestRestoreBandLeavesMaximizedNeighbor(t *testing.T) { | |||
| 392 | + | _, w := cascadeMovesCore(t, 3, 3, false) | |||
| 393 | + | w.FocusID(1) | |||
| 394 | + | w.ToggleFullWidth() | |||
| 395 | + | wantBands(t, w, []WindowID{1}, []WindowID{2, 3}) | |||
| 396 | + | w.FocusID(2) | |||
| 397 | + | w.ToggleFullscreen() | |||
| 398 | + | w.ToggleFullscreen() | |||
| 399 | + | w.ToggleFullWidth() | |||
| 400 | + | wantBands(t, w, []WindowID{1}, []WindowID{3}, []WindowID{2}) | |||
| 401 | + | w.FocusID(1) | |||
| 402 | + | w.ToggleFullWidth() | |||
| 403 | + | wantBands(t, w, []WindowID{1, 3}, []WindowID{2}) | |||
| 404 | + | if c := w.columnOf(2); c < 0 || !w.Columns[c].FullWidth { | |||
| 405 | + | t.Fatal("column 2 lost its maximization") | |||
| 406 | + | } | |||
| 407 | + | } | |||
| 408 | + | ||||
| 409 | + | func TestPromotedFullscreenFloatMovesToNeighborMonitor(t *testing.T) { | |||
| 410 | + | for _, tc := range []struct { | |||
| 411 | + | a Action | |||
| 412 | + | screen string | |||
| 413 | + | }{{ActionMoveColumnLeft, "L"}, {ActionMoveColumnRight, "R"}} { | |||
| 414 | + | t.Run(tc.screen, func(t *testing.T) { | |||
| 415 | + | c, w := cascadeMovesCore(t, 2, 3, true) | |||
| 416 | + | w.AddFloating(9, 800, 600) | |||
| 417 | + | w.SetFullscreen(9, true) | |||
| 418 | + | c.applyAction(tc.a) | |||
| 419 | + | dst := screenWorkspace(c, tc.screen) | |||
| 420 | + | wantFocus(t, c, tc.screen, 9) | |||
| 421 | + | wantColumns(t, w, 1, 2) | |||
| 422 | + | if dst.isFloat(9) || dst.columnOf(9) < 0 || dst.fullscreen != 9 || w.fullscreen != 0 { | |||
| 423 | + | t.Fatalf("destination %+v", dst) | |||
| 424 | + | } | |||
| 425 | + | }) | |||
| 426 | + | } | |||
| 427 | + | } | |||
| 428 | + | ||||
| 429 | + | func TestUserFloatedToggleFullscreenPromotes(t *testing.T) { | |||
| 430 | + | _, w := cascadeMovesCore(t, 2, 3, false) | |||
| 431 | + | w.ToggleFloating() | |||
| 432 | + | if !w.isFloat(2) { | |||
| 433 | + | t.Fatal("setup not floating") | |||
| 434 | + | } | |||
| 435 | + | w.ToggleFullscreen() | |||
| 436 | + | if w.isFloat(2) || w.fullscreen != 2 { | |||
| 437 | + | t.Fatal("user float not promoted") | |||
| 438 | + | } | |||
| 439 | + | wantBands(t, w, []WindowID{1}, []WindowID{2}) | |||
| 440 | + | w.SetFullscreen(2, true) | |||
| 441 | + | w.SetFullscreen(2, true) | |||
| 442 | + | wantBands(t, w, []WindowID{1}, []WindowID{2}) | |||
| 443 | + | } | |||
M internal/core/layout.go +2 -0
| @@ -1340,6 +1340,7 @@ func (w *Workspace) ToggleFullscreen() { | |||
| 1340 | 1340 | w.endFullscreen() | |
| 1341 | 1341 | return | |
| 1342 | 1342 | } | |
| 1343 | + | w.fullscreenFloatBand(id) | |
| 1343 | 1344 | w.fullscreenBand(id) | |
| 1344 | 1345 | w.fullscreen = id | |
| 1345 | 1346 | w.scroll() | |
| @@ -1450,6 +1451,7 @@ func (w *Workspace) SetFullscreen(id WindowID, on bool) { | |||
| 1450 | 1451 | // Another window covers: the override was for the old cover. | |
| 1451 | 1452 | w.dropStashOver() | |
| 1452 | 1453 | } | |
| 1454 | + | w.fullscreenFloatBand(id) | |
| 1453 | 1455 | if !w.isFloat(id) && w.fullscreen != id { | |
| 1454 | 1456 | w.fullscreenBand(id) | |
| 1455 | 1457 | } | |
| @@ -1340,6 +1340,7 @@ func (w *Workspace) ToggleFullscreen() { | |||||
| 1340 | w.endFullscreen() | 1340 | w.endFullscreen() | ||
| 1341 | return | 1341 | return | ||
| 1342 | } | 1342 | } | ||
| 1343 | + | w.fullscreenFloatBand(id) | |||
| 1343 | w.fullscreenBand(id) | 1344 | w.fullscreenBand(id) | ||
| 1344 | w.fullscreen = id | 1345 | w.fullscreen = id | ||
| 1345 | w.scroll() | 1346 | w.scroll() | ||
| @@ -1450,6 +1451,7 @@ func (w *Workspace) SetFullscreen(id WindowID, on bool) { | |||||
| 1450 | // Another window covers: the override was for the old cover. | 1451 | // Another window covers: the override was for the old cover. | ||
| 1451 | w.dropStashOver() | 1452 | w.dropStashOver() | ||
| 1452 | } | 1453 | } | ||
| 1454 | + | w.fullscreenFloatBand(id) | |||
| 1453 | if !w.isFloat(id) && w.fullscreen != id { | 1455 | if !w.isFloat(id) && w.fullscreen != id { | ||
| 1454 | w.fullscreenBand(id) | 1456 | w.fullscreenBand(id) | ||
| 1455 | } | 1457 | } | ||
M internal/core/legacy_fullscreen_internal_test.go +56 -0
| @@ -244,3 +244,59 @@ func TestLegacyFullscreenUnderBoundCover(t *testing.T) { | |||
| 244 | 244 | } | |
| 245 | 245 | } | |
| 246 | 246 | } | |
| 247 | + | ||
| 248 | + | func TestLegacyFullscreenCascadeFloatBand(t *testing.T) { | |
| 249 | + | for _, parent := range []WindowID{0, 1} { | |
| 250 | + | t.Run(map[bool]string{true: "dialog", false: "parentless"}[parent != 0], func(t *testing.T) { | |
| 251 | + | c, clock, _ := fadeCore(t) | |
| 252 | + | sc := c.cur() | |
| 253 | + | w := sc.mon.Current() | |
| 254 | + | w.Overflow, w.MaxColumns = OverflowCascade, 3 | |
| 255 | + | c.clientEvent(context.Background(), ports.WindowMapped{ID: 1}) | |
| 256 | + | c.clientEvent(context.Background(), ports.WindowMapped{ID: 2, Floating: true, Parent: parent}) | |
| 257 | + | o := sc.rect() | |
| 258 | + | c.clientEvent(context.Background(), ports.WindowResized{ID: 2, Width: o.W, Height: o.H}) | |
| 259 | + | if parent != 0 { | |
| 260 | + | if !w.isFloat(2) || w.fullscreen != 0 { | |
| 261 | + | t.Fatal("dialog was promoted or auto-fullscreened") | |
| 262 | + | } | |
| 263 | + | clock.now = clock.now.Add(fullscreenGrace) | |
| 264 | + | c.clientEvent(context.Background(), ports.WindowFullscreenRequest{ID: 2, Fullscreen: true}) | |
| 265 | + | if w.fullscreen != 2 || !w.isFloat(2) { | |
| 266 | + | t.Fatal("explicit fullscreen promoted a dialog") | |
| 267 | + | } | |
| 268 | + | return | |
| 269 | + | } | |
| 270 | + | if w.isFloat(2) || w.fullscreen != 2 || len(w.Columns) != 2 || w.band(w.columnOf(2)) != w.band(w.columnOf(1))+1 || !w.Columns[w.columnOf(2)].ownBand { | |
| 271 | + | t.Fatalf("fullscreen float placement: %+v, floats %+v fullscreen %d", w.Columns, w.Floats, w.fullscreen) | |
| 272 | + | } | |
| 273 | + | c.clientEvent(context.Background(), ports.WindowFullscreenRequest{ID: 2}) | |
| 274 | + | if id, _ := w.Focused(); id != 2 || w.fullscreen != 0 || w.isFloat(2) || w.band(w.columnOf(2)) != 1 { | |
| 275 | + | t.Fatalf("after unset: focus %d fullscreen %d columns %+v floats %+v", id, w.fullscreen, w.Columns, w.Floats) | |
| 276 | + | } | |
| 277 | + | if r, ok := w.placed(2); !ok || r.W <= 0 || r.H <= 0 { | |
| 278 | + | t.Fatalf("game not visible: %+v %v", r, ok) | |
| 279 | + | } | |
| 280 | + | }) | |
| 281 | + | } | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | func TestCascadeClientFullscreenPromotesFloatAfterGrace(t *testing.T) { | |
| 285 | + | for _, requests := range []int{1, 3} { | |
| 286 | + | t.Run(map[int]string{1: "once", 3: "repeated"}[requests], func(t *testing.T) { | |
| 287 | + | c, clock, _ := fadeCore(t) | |
| 288 | + | w := c.cur().mon.Current() | |
| 289 | + | w.Overflow, w.MaxColumns = OverflowCascade, 3 | |
| 290 | + | c.clientEvent(context.Background(), ports.WindowMapped{ID: 1}) | |
| 291 | + | c.clientEvent(context.Background(), ports.WindowMapped{ID: 2, Floating: true}) | |
| 292 | + | clock.now = clock.now.Add(fullscreenGrace) | |
| 293 | + | for range requests { | |
| 294 | + | c.clientEvent(context.Background(), ports.WindowFullscreenRequest{ID: 2, Fullscreen: true}) | |
| 295 | + | } | |
| 296 | + | if w.fullscreen != 2 || w.isFloat(2) { | |
| 297 | + | t.Fatalf("fullscreen %d floats %+v", w.fullscreen, w.Floats) | |
| 298 | + | } | |
| 299 | + | wantBands(t, w, []WindowID{1}, []WindowID{2}) | |
| 300 | + | }) | |
| 301 | + | } | |
| 302 | + | } | |
| @@ -244,3 +244,59 @@ func TestLegacyFullscreenUnderBoundCover(t *testing.T) { | |||||
| 244 | } | 244 | } | ||
| 245 | } | 245 | } | ||
| 246 | } | 246 | } | ||
| 247 | + | ||||
| 248 | + | func TestLegacyFullscreenCascadeFloatBand(t *testing.T) { | |||
| 249 | + | for _, parent := range []WindowID{0, 1} { | |||
| 250 | + | t.Run(map[bool]string{true: "dialog", false: "parentless"}[parent != 0], func(t *testing.T) { | |||
| 251 | + | c, clock, _ := fadeCore(t) | |||
| 252 | + | sc := c.cur() | |||
| 253 | + | w := sc.mon.Current() | |||
| 254 | + | w.Overflow, w.MaxColumns = OverflowCascade, 3 | |||
| 255 | + | c.clientEvent(context.Background(), ports.WindowMapped{ID: 1}) | |||
| 256 | + | c.clientEvent(context.Background(), ports.WindowMapped{ID: 2, Floating: true, Parent: parent}) | |||
| 257 | + | o := sc.rect() | |||
| 258 | + | c.clientEvent(context.Background(), ports.WindowResized{ID: 2, Width: o.W, Height: o.H}) | |||
| 259 | + | if parent != 0 { | |||
| 260 | + | if !w.isFloat(2) || w.fullscreen != 0 { | |||
| 261 | + | t.Fatal("dialog was promoted or auto-fullscreened") | |||
| 262 | + | } | |||
| 263 | + | clock.now = clock.now.Add(fullscreenGrace) | |||
| 264 | + | c.clientEvent(context.Background(), ports.WindowFullscreenRequest{ID: 2, Fullscreen: true}) | |||
| 265 | + | if w.fullscreen != 2 || !w.isFloat(2) { | |||
| 266 | + | t.Fatal("explicit fullscreen promoted a dialog") | |||
| 267 | + | } | |||
| 268 | + | return | |||
| 269 | + | } | |||
| 270 | + | if w.isFloat(2) || w.fullscreen != 2 || len(w.Columns) != 2 || w.band(w.columnOf(2)) != w.band(w.columnOf(1))+1 || !w.Columns[w.columnOf(2)].ownBand { | |||
| 271 | + | t.Fatalf("fullscreen float placement: %+v, floats %+v fullscreen %d", w.Columns, w.Floats, w.fullscreen) | |||
| 272 | + | } | |||
| 273 | + | c.clientEvent(context.Background(), ports.WindowFullscreenRequest{ID: 2}) | |||
| 274 | + | if id, _ := w.Focused(); id != 2 || w.fullscreen != 0 || w.isFloat(2) || w.band(w.columnOf(2)) != 1 { | |||
| 275 | + | t.Fatalf("after unset: focus %d fullscreen %d columns %+v floats %+v", id, w.fullscreen, w.Columns, w.Floats) | |||
| 276 | + | } | |||
| 277 | + | if r, ok := w.placed(2); !ok || r.W <= 0 || r.H <= 0 { | |||
| 278 | + | t.Fatalf("game not visible: %+v %v", r, ok) | |||
| 279 | + | } | |||
| 280 | + | }) | |||
| 281 | + | } | |||
| 282 | + | } | |||
| 283 | + | ||||
| 284 | + | func TestCascadeClientFullscreenPromotesFloatAfterGrace(t *testing.T) { | |||
| 285 | + | for _, requests := range []int{1, 3} { | |||
| 286 | + | t.Run(map[int]string{1: "once", 3: "repeated"}[requests], func(t *testing.T) { | |||
| 287 | + | c, clock, _ := fadeCore(t) | |||
| 288 | + | w := c.cur().mon.Current() | |||
| 289 | + | w.Overflow, w.MaxColumns = OverflowCascade, 3 | |||
| 290 | + | c.clientEvent(context.Background(), ports.WindowMapped{ID: 1}) | |||
| 291 | + | c.clientEvent(context.Background(), ports.WindowMapped{ID: 2, Floating: true}) | |||
| 292 | + | clock.now = clock.now.Add(fullscreenGrace) | |||
| 293 | + | for range requests { | |||
| 294 | + | c.clientEvent(context.Background(), ports.WindowFullscreenRequest{ID: 2, Fullscreen: true}) | |||
| 295 | + | } | |||
| 296 | + | if w.fullscreen != 2 || w.isFloat(2) { | |||
| 297 | + | t.Fatalf("fullscreen %d floats %+v", w.fullscreen, w.Floats) | |||
| 298 | + | } | |||
| 299 | + | wantBands(t, w, []WindowID{1}, []WindowID{2}) | |||
| 300 | + | }) | |||
| 301 | + | } | |||
| 302 | + | } | |||