feat(core): move a fullscreen float to the neighbor monitor
move-column-left/right on a fullscreen floating window sends it to the neighbor monitor, focused and still fullscreen; with no neighbor it stays.
- commit
- c73000c5d9f80cb5f1b62302a6631e6e95e64ddb
- author
- brice · · 2026-10-22 19:45 UTC
- parent
- dbd9830
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +56 -1
M docs/config.md +1 -1
| @@ -308,7 +308,7 @@ The default layout. Columns sit side by side in bands of up to `max-columns`, st | |||
| 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 | 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 sends it to the neighbor monitor, where it stays fullscreen and focused; with no neighbor monitor nothing moves. | |
| 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. | |
| 312 | 312 | - A band left empty goes away. A smaller `max-columns` moves the extra columns of a band to a new band below it. | |
| 313 | 313 | ||
| 314 | 314 | ### Column widths | |
| @@ -308,7 +308,7 @@ The default layout. Columns sit side by side in bands of up to `max-columns`, st | |||||
| 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. | 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 sends it to the neighbor monitor, where it stays fullscreen and focused; with no neighbor monitor nothing moves. | 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. |
| 312 | - 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. | ||
| 313 | 313 | ||||
| 314 | ### Column widths | 314 | ### Column widths | ||
M internal/core/action.go +25 -0
| @@ -388,6 +388,17 @@ func (c *Core) dispatchAction(a Action) Effect { | |||
| 388 | 388 | if a == ActionMoveColumnLeft { | |
| 389 | 389 | dir, d = -1, dirLeft | |
| 390 | 390 | } | |
| 391 | + | // A fullscreen float goes to the neighbor screen, fullscreen there. | |
| 392 | + | if fid, ok := w.Focused(); ok && w.fullscreen == fid && w.floatIndex(fid) >= 0 { | |
| 393 | + | if i := c.neighbor(d); i >= 0 { | |
| 394 | + | from := c.cur() | |
| 395 | + | c.focusScreen = i | |
| 396 | + | moveFloat(w, c.cur().mon.Current(), fid) | |
| 397 | + | from.mon.normalize() | |
| 398 | + | c.cur().mon.normalize() | |
| 399 | + | } | |
| 400 | + | return Effect{} | |
| 401 | + | } | |
| 391 | 402 | // A fullscreen window leaves for the neighbor screen at once, and | |
| 392 | 403 | // stays fullscreen there; with no screen, nothing moves. | |
| 393 | 404 | fid, _ := w.Focused() | |
| @@ -619,3 +630,17 @@ func (w *Workspace) Apply(a Action) Effect { | |||
| 619 | 630 | } | |
| 620 | 631 | return Effect{} | |
| 621 | 632 | } | |
| 633 | + | ||
| 634 | + | // moveFloat moves float id from workspace cur to to, as MoveToWorkspace | |
| 635 | + | // does, focuses it there and keeps it fullscreen. | |
| 636 | + | func moveFloat(cur, to *Workspace, id WindowID) { | |
| 637 | + | fl := cur.Floats[cur.floatIndex(id)] | |
| 638 | + | if fl.free && fl.back != nil { | |
| 639 | + | fl = fl.rehome() | |
| 640 | + | } | |
| 641 | + | cur.RemoveWindow(id) | |
| 642 | + | to.AddFloating(id, fl.W, fl.H) | |
| 643 | + | to.Floats[len(to.Floats)-1] = fl | |
| 644 | + | to.Activate(id) | |
| 645 | + | to.SetFullscreen(id, true) | |
| 646 | + | } | |
| @@ -388,6 +388,17 @@ func (c *Core) dispatchAction(a Action) Effect { | |||||
| 388 | if a == ActionMoveColumnLeft { | 388 | if a == ActionMoveColumnLeft { | ||
| 389 | dir, d = -1, dirLeft | 389 | dir, d = -1, dirLeft | ||
| 390 | } | 390 | } | ||
| 391 | + | // A fullscreen float goes to the neighbor screen, fullscreen there. | |||
| 392 | + | if fid, ok := w.Focused(); ok && w.fullscreen == fid && w.floatIndex(fid) >= 0 { | |||
| 393 | + | if i := c.neighbor(d); i >= 0 { | |||
| 394 | + | from := c.cur() | |||
| 395 | + | c.focusScreen = i | |||
| 396 | + | moveFloat(w, c.cur().mon.Current(), fid) | |||
| 397 | + | from.mon.normalize() | |||
| 398 | + | c.cur().mon.normalize() | |||
| 399 | + | } | |||
| 400 | + | return Effect{} | |||
| 401 | + | } | |||
| 391 | // A fullscreen window leaves for the neighbor screen at once, and | 402 | // A fullscreen window leaves for the neighbor screen at once, and | ||
| 392 | // stays fullscreen there; with no screen, nothing moves. | 403 | // stays fullscreen there; with no screen, nothing moves. | ||
| 393 | fid, _ := w.Focused() | 404 | fid, _ := w.Focused() | ||
| @@ -619,3 +630,17 @@ func (w *Workspace) Apply(a Action) Effect { | |||||
| 619 | } | 630 | } | ||
| 620 | return Effect{} | 631 | return Effect{} | ||
| 621 | } | 632 | } | ||
| 633 | + | ||||
| 634 | + | // moveFloat moves float id from workspace cur to to, as MoveToWorkspace | |||
| 635 | + | // does, focuses it there and keeps it fullscreen. | |||
| 636 | + | func moveFloat(cur, to *Workspace, id WindowID) { | |||
| 637 | + | fl := cur.Floats[cur.floatIndex(id)] | |||
| 638 | + | if fl.free && fl.back != nil { | |||
| 639 | + | fl = fl.rehome() | |||
| 640 | + | } | |||
| 641 | + | cur.RemoveWindow(id) | |||
| 642 | + | to.AddFloating(id, fl.W, fl.H) | |||
| 643 | + | to.Floats[len(to.Floats)-1] = fl | |||
| 644 | + | to.Activate(id) | |||
| 645 | + | to.SetFullscreen(id, true) | |||
| 646 | + | } | |||
M internal/core/cascade_fullscreen_band_test.go +30 -0
| @@ -92,3 +92,33 @@ func TestFullscreenColumnNoNeighborStays(t *testing.T) { | |||
| 92 | 92 | t.Fatalf("fullscreen %d", w.fullscreen) | |
| 93 | 93 | } | |
| 94 | 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 | + | } | |
| @@ -92,3 +92,33 @@ func TestFullscreenColumnNoNeighborStays(t *testing.T) { | |||||
| 92 | t.Fatalf("fullscreen %d", w.fullscreen) | 92 | t.Fatalf("fullscreen %d", w.fullscreen) | ||
| 93 | } | 93 | } | ||
| 94 | } | 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 | + | } | |||