fix(core): show and open the overview over fullscreen windows (#65)
* fix(core): show a covering fullscreen float in the overview A fixed-size game maps as a native float; the overview hid every float, so a workspace covered by a fullscreen game showed an empty row. The covering float is now that row's only preview, clickable and selectable; its dialogs and the columns under it stay hidden. Also fixes a panic when a window maps under a covering float with no column (scroll overflow). * fix(core): configure a covering float fullscreen in its overview preview A float made fullscreen while hidden kept its non-fullscreen configure in the overview preview. The preview placement now carries Fullscreen, and configures sizes such a window for the output, never the preview. Documents that the overview shows a fullscreen float. * fix(core): open the overview over a covering fullscreen window Cmd+o did nothing on a workspace covered by a fullscreen window (a game on its own workspace). The overview now opens there too: the game's row is selected, composed while the overview is open, and Escape returns to it. * fix(core): show bars and pin selection in the overview over fullscreen Opened over a covering fullscreen window, the overview kept bars and notifications hidden, and in fixed overflow a tiled fullscreen window's hidden columns stayed selectable while Return kept the fullscreen window in front. The overview now shows the layers, and any pinned fullscreen window (tiled or floating) is its row's only preview, as on screen. * docs(config): describe the overview over fixed and scroll fullscreen Documents that only pinned fullscreen windows (floating, or fixed overflow) are their row's only preview, and tests picking another column over a scroll-overflow fullscreen column.
- commit
- 09abb12d4b2580f5a4bea6a027d4dfe08a3233a5
- author
- bnema · · 2026-09-29 16:20 UTC
- committer
- GitHub · · 2026-09-29 16:20 UTC
- parent
- fa4ed84
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
9 files · +236 -13
M docs/config.md +1 -1
| @@ -136,7 +136,7 @@ Native floating windows (dialogs, file pickers) are not in the stash: they stay | |||
| 136 | 136 | ||
| 137 | 137 | ### Overview | |
| 138 | 138 | ||
| 139 | - | `toggle-overview` shows every column of the current workspace in one row, shrunk by the same factor so each window keeps its shape, as small as needed to fit (down to a quarter, then the row scrolls). The workspaces above and below show dimmed. Windows are not resized: the previews are their last frames. | |
| 139 | + | `toggle-overview` shows every column of the current workspace in one row, shrunk by the same factor so each window keeps its shape, as small as needed to fit (down to a quarter, then the row scrolls). The workspaces above and below show dimmed. Windows are not resized: the previews are their last frames. Native floating windows are hidden. The overview opens over a fullscreen window too. A fullscreen floating window, or any fullscreen window with `fixed` overflow, is its workspace's only preview; with `scroll` overflow the other columns stay selectable. | |
| 140 | 140 | ||
| 141 | 141 | | Key | Action | | |
| 142 | 142 | | --- | --- | | |
| @@ -136,7 +136,7 @@ Native floating windows (dialogs, file pickers) are not in the stash: they stay | |||||
| 136 | 136 | ||||
| 137 | ### Overview | 137 | ### Overview | ||
| 138 | 138 | ||||
| 139 | - | `toggle-overview` shows every column of the current workspace in one row, shrunk by the same factor so each window keeps its shape, as small as needed to fit (down to a quarter, then the row scrolls). The workspaces above and below show dimmed. Windows are not resized: the previews are their last frames. | 139 | + | `toggle-overview` shows every column of the current workspace in one row, shrunk by the same factor so each window keeps its shape, as small as needed to fit (down to a quarter, then the row scrolls). The workspaces above and below show dimmed. Windows are not resized: the previews are their last frames. Native floating windows are hidden. The overview opens over a fullscreen window too. A fullscreen floating window, or any fullscreen window with `fixed` overflow, is its workspace's only preview; with `scroll` overflow the other columns stay selectable. |
| 140 | 140 | ||||
| 141 | | Key | Action | | 141 | | Key | Action | | ||
| 142 | | --- | --- | | 142 | | --- | --- | | ||
M internal/core/configures.go +5 -0
| @@ -46,6 +46,11 @@ func (s *configures) next(p Placement, t configureTarget) (ports.ConfigureWindow | |||
| 46 | 46 | if !ok { | |
| 47 | 47 | v = ports.ConfigureWindow{ID: p.ID, Width: int(float64(p.Rect.W) / p.Preview), Height: int(float64(p.Rect.H) / p.Preview)} | |
| 48 | 48 | } | |
| 49 | + | if p.Fullscreen && !v.Fullscreen { | |
| 50 | + | // A float made fullscreen off screen: its preview row shows | |
| 51 | + | // it fullscreen, sized for the output, never the preview. | |
| 52 | + | v.Width, v.Height, v.Fullscreen, v.Floating = t.area.W, t.area.H, true, false | |
| 53 | + | } | |
| 49 | 54 | v.Activated, v.Visible, v.Output = t.focused, onScreen(p, t.area), t.output | |
| 50 | 55 | default: | |
| 51 | 56 | v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: onScreen(p, t.area)} | |
| @@ -46,6 +46,11 @@ func (s *configures) next(p Placement, t configureTarget) (ports.ConfigureWindow | |||||
| 46 | if !ok { | 46 | if !ok { | ||
| 47 | v = ports.ConfigureWindow{ID: p.ID, Width: int(float64(p.Rect.W) / p.Preview), Height: int(float64(p.Rect.H) / p.Preview)} | 47 | v = ports.ConfigureWindow{ID: p.ID, Width: int(float64(p.Rect.W) / p.Preview), Height: int(float64(p.Rect.H) / p.Preview)} | ||
| 48 | } | 48 | } | ||
| 49 | + | if p.Fullscreen && !v.Fullscreen { | |||
| 50 | + | // A float made fullscreen off screen: its preview row shows | |||
| 51 | + | // it fullscreen, sized for the output, never the preview. | |||
| 52 | + | v.Width, v.Height, v.Fullscreen, v.Floating = t.area.W, t.area.H, true, false | |||
| 53 | + | } | |||
| 49 | v.Activated, v.Visible, v.Output = t.focused, onScreen(p, t.area), t.output | 54 | v.Activated, v.Visible, v.Output = t.focused, onScreen(p, t.area), t.output | ||
| 50 | default: | 55 | default: | ||
| 51 | v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: onScreen(p, t.area)} | 56 | v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: onScreen(p, t.area)} | ||
M internal/core/configures_test.go +2 -0
| @@ -27,6 +27,8 @@ func TestConfiguresNext(t *testing.T) { | |||
| 27 | 27 | {name: "hidden new", p: Placement{ID: 1, Hidden: true, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Output: "A"}, send: true}, | |
| 28 | 28 | {name: "hidden keeps size", first: &tiled, p: Placement{ID: 1, Hidden: true}, t: configureTarget{output: "B"}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "B"}, send: true}, | |
| 29 | 29 | {name: "preview new", p: Placement{ID: 1, Rect: Rect{W: 250, H: 200}, Preview: 0.5}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 500, Height: 400, Activated: true, Output: "A", Visible: true}, send: true}, | |
| 30 | + | {name: "fullscreen preview keeps size and state", first: &Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Fullscreen: true, Output: "A", Visible: true}, send: true}, | |
| 31 | + | {name: "hidden float made fullscreen previews fullscreen", first: &Placement{ID: 1, Hidden: true, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1, Fullscreen: true}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true}, | |
| 30 | 32 | {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true}, | |
| 31 | 33 | } | |
| 32 | 34 | for _, tt := range tests { | |
| @@ -27,6 +27,8 @@ func TestConfiguresNext(t *testing.T) { | |||||
| 27 | {name: "hidden new", p: Placement{ID: 1, Hidden: true, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Output: "A"}, send: true}, | 27 | {name: "hidden new", p: Placement{ID: 1, Hidden: true, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Output: "A"}, send: true}, | ||
| 28 | {name: "hidden keeps size", first: &tiled, p: Placement{ID: 1, Hidden: true}, t: configureTarget{output: "B"}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "B"}, send: true}, | 28 | {name: "hidden keeps size", first: &tiled, p: Placement{ID: 1, Hidden: true}, t: configureTarget{output: "B"}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "B"}, send: true}, | ||
| 29 | {name: "preview new", p: Placement{ID: 1, Rect: Rect{W: 250, H: 200}, Preview: 0.5}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 500, Height: 400, Activated: true, Output: "A", Visible: true}, send: true}, | 29 | {name: "preview new", p: Placement{ID: 1, Rect: Rect{W: 250, H: 200}, Preview: 0.5}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 500, Height: 400, Activated: true, Output: "A", Visible: true}, send: true}, | ||
| 30 | + | {name: "fullscreen preview keeps size and state", first: &Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Fullscreen: true, Output: "A", Visible: true}, send: true}, | |||
| 31 | + | {name: "hidden float made fullscreen previews fullscreen", first: &Placement{ID: 1, Hidden: true, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1, Fullscreen: true}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true}, | |||
| 30 | {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true}, | 32 | {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true}, | ||
| 31 | } | 33 | } | ||
| 32 | for _, tt := range tests { | 34 | for _, tt := range tests { | ||
M internal/core/floating_test.go +15 -0
| @@ -212,6 +212,21 @@ func TestExclusiveFullscreenNewWindow(t *testing.T) { | |||
| 212 | 212 | } | |
| 213 | 213 | } | |
| 214 | 214 | ||
| 215 | + | // A window mapped under a covering fullscreen float with no column waits | |
| 216 | + | // hidden as the first column. | |
| 217 | + | func TestFullscreenFloatNewWindowNoColumns(t *testing.T) { | |
| 218 | + | w := workspace() | |
| 219 | + | w.AddFloating(1, 300, 200) | |
| 220 | + | w.SetFullscreen(1, true) | |
| 221 | + | w.AddWindow(2) | |
| 222 | + | if len(w.Columns) != 1 || w.Columns[0].Windows[0] != 2 { | |
| 223 | + | t.Fatalf("columns %+v", w.Columns) | |
| 224 | + | } | |
| 225 | + | if id, _ := w.Focused(); id != 1 { | |
| 226 | + | t.Fatalf("focus %d", id) | |
| 227 | + | } | |
| 228 | + | } | |
| 229 | + | ||
| 215 | 230 | // Under a covering fullscreen float, or any fixed-overflow fullscreen, | |
| 216 | 231 | // focus moves inside the workspace are off and binds act on the window on | |
| 217 | 232 | // screen, never on a column it hides. | |
| @@ -212,6 +212,21 @@ func TestExclusiveFullscreenNewWindow(t *testing.T) { | |||||
| 212 | } | 212 | } | ||
| 213 | } | 213 | } | ||
| 214 | 214 | ||||
| 215 | + | // A window mapped under a covering fullscreen float with no column waits | |||
| 216 | + | // hidden as the first column. | |||
| 217 | + | func TestFullscreenFloatNewWindowNoColumns(t *testing.T) { | |||
| 218 | + | w := workspace() | |||
| 219 | + | w.AddFloating(1, 300, 200) | |||
| 220 | + | w.SetFullscreen(1, true) | |||
| 221 | + | w.AddWindow(2) | |||
| 222 | + | if len(w.Columns) != 1 || w.Columns[0].Windows[0] != 2 { | |||
| 223 | + | t.Fatalf("columns %+v", w.Columns) | |||
| 224 | + | } | |||
| 225 | + | if id, _ := w.Focused(); id != 1 { | |||
| 226 | + | t.Fatalf("focus %d", id) | |||
| 227 | + | } | |||
| 228 | + | } | |||
| 229 | + | ||||
| 215 | // Under a covering fullscreen float, or any fixed-overflow fullscreen, | 230 | // Under a covering fullscreen float, or any fixed-overflow fullscreen, | ||
| 216 | // focus moves inside the workspace are off and binds act on the window on | 231 | // focus moves inside the workspace are off and binds act on the window on | ||
| 217 | // screen, never on a column it hides. | 232 | // screen, never on a column it hides. | ||
M internal/core/layers.go +3 -2
| @@ -87,9 +87,10 @@ func (c *Core) layerOf(id WindowID) (*screen, ports.SceneLayer, bool) { | |||
| 87 | 87 | } | |
| 88 | 88 | ||
| 89 | 89 | // hasFullscreen reports whether a fullscreen window covers the screen. In | |
| 90 | - | // scroll overflow, one scrolled off by the user no longer does. | |
| 90 | + | // scroll overflow, one scrolled off by the user no longer does; nor does | |
| 91 | + | // one shown as an overview preview. | |
| 91 | 92 | func hasFullscreen(sc *screen) bool { | |
| 92 | - | return sc.mon.Current().cover() != 0 | |
| 93 | + | return !sc.mon.overview && sc.mon.Current().cover() != 0 | |
| 93 | 94 | } | |
| 94 | 95 | ||
| 95 | 96 | // shown reports whether a layer surface is drawn. A fullscreen window is | |
| @@ -87,9 +87,10 @@ func (c *Core) layerOf(id WindowID) (*screen, ports.SceneLayer, bool) { | |||||
| 87 | } | 87 | } | ||
| 88 | 88 | ||||
| 89 | // hasFullscreen reports whether a fullscreen window covers the screen. In | 89 | // hasFullscreen reports whether a fullscreen window covers the screen. In | ||
| 90 | - | // scroll overflow, one scrolled off by the user no longer does. | 90 | + | // scroll overflow, one scrolled off by the user no longer does; nor does |
| 91 | + | // one shown as an overview preview. | |||
| 91 | func hasFullscreen(sc *screen) bool { | 92 | func hasFullscreen(sc *screen) bool { | ||
| 92 | - | return sc.mon.Current().cover() != 0 | 93 | + | return !sc.mon.overview && sc.mon.Current().cover() != 0 |
| 93 | } | 94 | } | ||
| 94 | 95 | ||||
| 95 | // shown reports whether a layer surface is drawn. A fullscreen window is | 96 | // shown reports whether a layer surface is drawn. A fullscreen window is | ||
M internal/core/layers_test.go +18 -0
| @@ -142,6 +142,24 @@ func TestScrolledOffFullscreenShowsLayers(t *testing.T) { | |||
| 142 | 142 | } | |
| 143 | 143 | } | |
| 144 | 144 | ||
| 145 | + | // In the overview a fullscreen window is a preview: the layers show. | |
| 146 | + | func TestOverviewOverFullscreenShowsLayers(t *testing.T) { | |
| 147 | + | m := newMonitor("", "") | |
| 148 | + | m.SetOutput(100, 80) | |
| 149 | + | m.AddWindow(1) | |
| 150 | + | m.SetFullscreen(1, true) | |
| 151 | + | sc := &screen{mon: m} | |
| 152 | + | bar := ports.SceneLayer{ID: 6, Layer: ports.LayerTop} | |
| 153 | + | m.ToggleOverview() | |
| 154 | + | if !shown(sc, bar) { | |
| 155 | + | t.Fatal("bar hidden in the overview") | |
| 156 | + | } | |
| 157 | + | m.ToggleOverview() | |
| 158 | + | if shown(sc, bar) { | |
| 159 | + | t.Fatal("bar shown over fullscreen after the overview") | |
| 160 | + | } | |
| 161 | + | } | |
| 162 | + | ||
| 145 | 163 | // A fullscreen window is exclusive: the scene carries only the background | |
| 146 | 164 | // and a layer taking the keyboard exclusively (a locker), so the output | |
| 147 | 165 | // can scan the window out. | |
| @@ -142,6 +142,24 @@ func TestScrolledOffFullscreenShowsLayers(t *testing.T) { | |||||
| 142 | } | 142 | } | ||
| 143 | } | 143 | } | ||
| 144 | 144 | ||||
| 145 | + | // In the overview a fullscreen window is a preview: the layers show. | |||
| 146 | + | func TestOverviewOverFullscreenShowsLayers(t *testing.T) { | |||
| 147 | + | m := newMonitor("", "") | |||
| 148 | + | m.SetOutput(100, 80) | |||
| 149 | + | m.AddWindow(1) | |||
| 150 | + | m.SetFullscreen(1, true) | |||
| 151 | + | sc := &screen{mon: m} | |||
| 152 | + | bar := ports.SceneLayer{ID: 6, Layer: ports.LayerTop} | |||
| 153 | + | m.ToggleOverview() | |||
| 154 | + | if !shown(sc, bar) { | |||
| 155 | + | t.Fatal("bar hidden in the overview") | |||
| 156 | + | } | |||
| 157 | + | m.ToggleOverview() | |||
| 158 | + | if shown(sc, bar) { | |||
| 159 | + | t.Fatal("bar shown over fullscreen after the overview") | |||
| 160 | + | } | |||
| 161 | + | } | |||
| 162 | + | ||||
| 145 | // A fullscreen window is exclusive: the scene carries only the background | 163 | // A fullscreen window is exclusive: the scene carries only the background | ||
| 146 | // and a layer taking the keyboard exclusively (a locker), so the output | 164 | // and a layer taking the keyboard exclusively (a locker), so the output | ||
| 147 | // can scan the window out. | 165 | // can scan the window out. | ||
M internal/core/layout.go +1 -1
| @@ -287,7 +287,7 @@ func (w *Workspace) AddWindow(id WindowID) { | |||
| 287 | 287 | // window (ADR 011): it waits, hidden, after the focused column. | |
| 288 | 288 | at := len(w.Columns) | |
| 289 | 289 | if w.Overflow != OverflowFixed { | |
| 290 | - | // No tiles when the covering window is a stashed one. | |
| 290 | + | // A covering float or stashed window may have no column under it. | |
| 291 | 291 | at = min(w.Focus+1, len(w.Columns)) | |
| 292 | 292 | } | |
| 293 | 293 | w.Columns = slices.Insert(w.Columns, at, Column{Windows: []WindowID{id}}) | |
| @@ -287,7 +287,7 @@ func (w *Workspace) AddWindow(id WindowID) { | |||||
| 287 | // window (ADR 011): it waits, hidden, after the focused column. | 287 | // window (ADR 011): it waits, hidden, after the focused column. | ||
| 288 | at := len(w.Columns) | 288 | at := len(w.Columns) | ||
| 289 | if w.Overflow != OverflowFixed { | 289 | if w.Overflow != OverflowFixed { | ||
| 290 | - | // No tiles when the covering window is a stashed one. | 290 | + | // A covering float or stashed window may have no column under it. |
| 291 | at = min(w.Focus+1, len(w.Columns)) | 291 | at = min(w.Focus+1, len(w.Columns)) | ||
| 292 | } | 292 | } | ||
| 293 | w.Columns = slices.Insert(w.Columns, at, Column{Windows: []WindowID{id}}) | 293 | w.Columns = slices.Insert(w.Columns, at, Column{Windows: []WindowID{id}}) | ||
M internal/core/overview.go +34 -9
| @@ -9,8 +9,10 @@ import ( | |||
| 9 | 9 | ||
| 10 | 10 | // The overview shows every column of the workspace on screen in one row, | |
| 11 | 11 | // each tile scaled by the same factor so windows keep their shape, with | |
| 12 | - | // the workspaces above and below it dimmed. The previews are the windows' | |
| 13 | - | // last buffers: clients are not resized. h/l (or left/right) select a | |
| 12 | + | // the workspaces above and below it dimmed. A workspace pinned under a | |
| 13 | + | // covering fullscreen window (a game) shows only that window, as on | |
| 14 | + | // screen. The previews | |
| 15 | + | // are the windows' last buffers: clients are not resized. h/l (or left/right) select a | |
| 14 | 16 | // column, j/k (or down/up) a workspace, Return keeps the selection and | |
| 15 | 17 | // Escape returns to where the overview opened. | |
| 16 | 18 | ||
| @@ -29,17 +31,15 @@ const ( | |||
| 29 | 31 | // scroll to keep the selected column in view. | |
| 30 | 32 | const overviewMaxZoom, overviewMinZoom = 0.6, 0.25 | |
| 31 | 33 | ||
| 32 | - | // ToggleOverview opens the overview, or closes it on the selection. It | |
| 33 | - | // does not open under a covering fullscreen window (a game in scanout). | |
| 34 | + | // ToggleOverview opens the overview, or closes it on the selection. Over | |
| 35 | + | // a covering fullscreen window (a game) too: its row shows it, composed | |
| 36 | + | // rather than scanned out while the overview is open. | |
| 34 | 37 | func (m *Monitor) ToggleOverview() { | |
| 35 | 38 | if m.overview { | |
| 36 | 39 | m.closeOverview() | |
| 37 | 40 | return | |
| 38 | 41 | } | |
| 39 | 42 | w := m.Current() | |
| 40 | - | if w.cover() != 0 { | |
| 41 | - | return | |
| 42 | - | } | |
| 43 | 43 | // A slide in progress lands at once: the overview lays out the | |
| 44 | 44 | // settled state. | |
| 45 | 45 | m.stopSwitch() | |
| @@ -54,10 +54,15 @@ func (m *Monitor) ToggleOverview() { | |||
| 54 | 54 | // selectRow selects the stash card of a workspace whose stash has the | |
| 55 | 55 | // focus (or only stashed windows), else its focused column. The overview | |
| 56 | 56 | // hides native floats, so a float's focus moves to its column; Escape | |
| 57 | - | // gives it back. | |
| 57 | + | // gives it back. A pinning fullscreen window is the exception: it is the | |
| 58 | + | // row. | |
| 58 | 59 | func (m *Monitor) selectRow() { | |
| 59 | 60 | m.overviewCard, m.overviewCardOf = 0, nil | |
| 60 | 61 | w := m.Current() | |
| 62 | + | if w.pinned() { | |
| 63 | + | // The covering window is the row's only preview: it keeps the focus. | |
| 64 | + | return | |
| 65 | + | } | |
| 61 | 66 | w.floatFocus = false | |
| 62 | 67 | if len(w.Stash) > 0 && (len(w.Columns) == 0 || w.stashFocused()) { | |
| 63 | 68 | m.selectCard(w, w.stashAt) | |
| @@ -134,6 +139,9 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||
| 134 | 139 | return | |
| 135 | 140 | } | |
| 136 | 141 | w := m.Current() | |
| 142 | + | if w.pinned() { | |
| 143 | + | return | |
| 144 | + | } | |
| 137 | 145 | if at := m.cardAt(w); at >= 0 { | |
| 138 | 146 | switch { | |
| 139 | 147 | case at+dx >= 0 && at+dx < len(w.Stash): | |
| @@ -195,6 +203,16 @@ func (m *Monitor) overviewLayout() []Placement { | |||
| 195 | 203 | } | |
| 196 | 204 | for _, w := range m.all() { | |
| 197 | 205 | ry, shown := rows[w] | |
| 206 | + | if shown && w.pinned() { | |
| 207 | + | // Only the covering window shows, as on screen. | |
| 208 | + | result = append(result, w.previewRow(ry, w != cur, true)...) | |
| 209 | + | for _, id := range w.windows() { | |
| 210 | + | if id != w.fullscreen { | |
| 211 | + | result = append(result, Placement{ID: id, Hidden: true}) | |
| 212 | + | } | |
| 213 | + | } | |
| 214 | + | continue | |
| 215 | + | } | |
| 198 | 216 | if shown { | |
| 199 | 217 | at := m.cardAt(w) | |
| 200 | 218 | result = append(result, w.previewRow(ry, w != cur, at < 0)...) | |
| @@ -217,7 +235,7 @@ func (m *Monitor) overviewLayout() []Placement { | |||
| 217 | 235 | // pileWidth is the room the stash pile of w takes on the left of its row, | |
| 218 | 236 | // its margin included; 0 without a stash. | |
| 219 | 237 | func (w *Workspace) pileWidth() int { | |
| 220 | - | if len(w.Stash) == 0 { | |
| 238 | + | if len(w.Stash) == 0 || w.pinned() { | |
| 221 | 239 | return 0 | |
| 222 | 240 | } | |
| 223 | 241 | card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom)) | |
| @@ -282,6 +300,13 @@ func (w *Workspace) overviewZoom() float64 { | |||
| 282 | 300 | func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) { | |
| 283 | 301 | g := w.gap() | |
| 284 | 302 | h := max(w.Usable.H-2*g, 0) | |
| 303 | + | if w.pinned() { | |
| 304 | + | // A covering window nothing else is reachable under (a game) is | |
| 305 | + | // the row's only tile, at the size of a fullscreen column. It | |
| 306 | + | // stays fullscreen. | |
| 307 | + | r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: h} | |
| 308 | + | return []Placement{{ID: w.cover(), Rect: r, Focused: true, Fullscreen: true}}, r.X + r.W + g, r | |
| 309 | + | } | |
| 285 | 310 | for i, c := range w.Columns { | |
| 286 | 311 | r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h} | |
| 287 | 312 | if w.fullscreenColumn(i) { | |
| @@ -9,8 +9,10 @@ import ( | |||||
| 9 | 9 | ||||
| 10 | // The overview shows every column of the workspace on screen in one row, | 10 | // The overview shows every column of the workspace on screen in one row, | ||
| 11 | // each tile scaled by the same factor so windows keep their shape, with | 11 | // each tile scaled by the same factor so windows keep their shape, with | ||
| 12 | - | // the workspaces above and below it dimmed. The previews are the windows' | 12 | + | // the workspaces above and below it dimmed. A workspace pinned under a |
| 13 | - | // last buffers: clients are not resized. h/l (or left/right) select a | 13 | + | // covering fullscreen window (a game) shows only that window, as on |
| 14 | + | // screen. The previews | |||
| 15 | + | // are the windows' last buffers: clients are not resized. h/l (or left/right) select a | |||
| 14 | // column, j/k (or down/up) a workspace, Return keeps the selection and | 16 | // column, j/k (or down/up) a workspace, Return keeps the selection and | ||
| 15 | // Escape returns to where the overview opened. | 17 | // Escape returns to where the overview opened. | ||
| 16 | 18 | ||||
| @@ -29,17 +31,15 @@ const ( | |||||
| 29 | // scroll to keep the selected column in view. | 31 | // scroll to keep the selected column in view. | ||
| 30 | const overviewMaxZoom, overviewMinZoom = 0.6, 0.25 | 32 | const overviewMaxZoom, overviewMinZoom = 0.6, 0.25 | ||
| 31 | 33 | ||||
| 32 | - | // ToggleOverview opens the overview, or closes it on the selection. It | 34 | + | // ToggleOverview opens the overview, or closes it on the selection. Over |
| 33 | - | // does not open under a covering fullscreen window (a game in scanout). | 35 | + | // a covering fullscreen window (a game) too: its row shows it, composed |
| 36 | + | // rather than scanned out while the overview is open. | |||
| 34 | func (m *Monitor) ToggleOverview() { | 37 | func (m *Monitor) ToggleOverview() { | ||
| 35 | if m.overview { | 38 | if m.overview { | ||
| 36 | m.closeOverview() | 39 | m.closeOverview() | ||
| 37 | return | 40 | return | ||
| 38 | } | 41 | } | ||
| 39 | w := m.Current() | 42 | w := m.Current() | ||
| 40 | - | if w.cover() != 0 { | |||
| 41 | - | return | |||
| 42 | - | } | |||
| 43 | // A slide in progress lands at once: the overview lays out the | 43 | // A slide in progress lands at once: the overview lays out the | ||
| 44 | // settled state. | 44 | // settled state. | ||
| 45 | m.stopSwitch() | 45 | m.stopSwitch() | ||
| @@ -54,10 +54,15 @@ func (m *Monitor) ToggleOverview() { | |||||
| 54 | // selectRow selects the stash card of a workspace whose stash has the | 54 | // selectRow selects the stash card of a workspace whose stash has the | ||
| 55 | // focus (or only stashed windows), else its focused column. The overview | 55 | // focus (or only stashed windows), else its focused column. The overview | ||
| 56 | // hides native floats, so a float's focus moves to its column; Escape | 56 | // hides native floats, so a float's focus moves to its column; Escape | ||
| 57 | - | // gives it back. | 57 | + | // gives it back. A pinning fullscreen window is the exception: it is the |
| 58 | + | // row. | |||
| 58 | func (m *Monitor) selectRow() { | 59 | func (m *Monitor) selectRow() { | ||
| 59 | m.overviewCard, m.overviewCardOf = 0, nil | 60 | m.overviewCard, m.overviewCardOf = 0, nil | ||
| 60 | w := m.Current() | 61 | w := m.Current() | ||
| 62 | + | if w.pinned() { | |||
| 63 | + | // The covering window is the row's only preview: it keeps the focus. | |||
| 64 | + | return | |||
| 65 | + | } | |||
| 61 | w.floatFocus = false | 66 | w.floatFocus = false | ||
| 62 | if len(w.Stash) > 0 && (len(w.Columns) == 0 || w.stashFocused()) { | 67 | if len(w.Stash) > 0 && (len(w.Columns) == 0 || w.stashFocused()) { | ||
| 63 | m.selectCard(w, w.stashAt) | 68 | m.selectCard(w, w.stashAt) | ||
| @@ -134,6 +139,9 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||||
| 134 | return | 139 | return | ||
| 135 | } | 140 | } | ||
| 136 | w := m.Current() | 141 | w := m.Current() | ||
| 142 | + | if w.pinned() { | |||
| 143 | + | return | |||
| 144 | + | } | |||
| 137 | if at := m.cardAt(w); at >= 0 { | 145 | if at := m.cardAt(w); at >= 0 { | ||
| 138 | switch { | 146 | switch { | ||
| 139 | case at+dx >= 0 && at+dx < len(w.Stash): | 147 | case at+dx >= 0 && at+dx < len(w.Stash): | ||
| @@ -195,6 +203,16 @@ func (m *Monitor) overviewLayout() []Placement { | |||||
| 195 | } | 203 | } | ||
| 196 | for _, w := range m.all() { | 204 | for _, w := range m.all() { | ||
| 197 | ry, shown := rows[w] | 205 | ry, shown := rows[w] | ||
| 206 | + | if shown && w.pinned() { | |||
| 207 | + | // Only the covering window shows, as on screen. | |||
| 208 | + | result = append(result, w.previewRow(ry, w != cur, true)...) | |||
| 209 | + | for _, id := range w.windows() { | |||
| 210 | + | if id != w.fullscreen { | |||
| 211 | + | result = append(result, Placement{ID: id, Hidden: true}) | |||
| 212 | + | } | |||
| 213 | + | } | |||
| 214 | + | continue | |||
| 215 | + | } | |||
| 198 | if shown { | 216 | if shown { | ||
| 199 | at := m.cardAt(w) | 217 | at := m.cardAt(w) | ||
| 200 | result = append(result, w.previewRow(ry, w != cur, at < 0)...) | 218 | result = append(result, w.previewRow(ry, w != cur, at < 0)...) | ||
| @@ -217,7 +235,7 @@ func (m *Monitor) overviewLayout() []Placement { | |||||
| 217 | // pileWidth is the room the stash pile of w takes on the left of its row, | 235 | // pileWidth is the room the stash pile of w takes on the left of its row, | ||
| 218 | // its margin included; 0 without a stash. | 236 | // its margin included; 0 without a stash. | ||
| 219 | func (w *Workspace) pileWidth() int { | 237 | func (w *Workspace) pileWidth() int { | ||
| 220 | - | if len(w.Stash) == 0 { | 238 | + | if len(w.Stash) == 0 || w.pinned() { |
| 221 | return 0 | 239 | return 0 | ||
| 222 | } | 240 | } | ||
| 223 | card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom)) | 241 | card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom)) | ||
| @@ -282,6 +300,13 @@ func (w *Workspace) overviewZoom() float64 { | |||||
| 282 | func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) { | 300 | func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) { | ||
| 283 | g := w.gap() | 301 | g := w.gap() | ||
| 284 | h := max(w.Usable.H-2*g, 0) | 302 | h := max(w.Usable.H-2*g, 0) | ||
| 303 | + | if w.pinned() { | |||
| 304 | + | // A covering window nothing else is reachable under (a game) is | |||
| 305 | + | // the row's only tile, at the size of a fullscreen column. It | |||
| 306 | + | // stays fullscreen. | |||
| 307 | + | r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: h} | |||
| 308 | + | return []Placement{{ID: w.cover(), Rect: r, Focused: true, Fullscreen: true}}, r.X + r.W + g, r | |||
| 309 | + | } | |||
| 285 | for i, c := range w.Columns { | 310 | for i, c := range w.Columns { | ||
| 286 | r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h} | 311 | r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h} | ||
| 287 | if w.fullscreenColumn(i) { | 312 | if w.fullscreenColumn(i) { | ||
M internal/core/overview_test.go +157 -0
| @@ -126,6 +126,163 @@ func TestOverviewPick(t *testing.T) { | |||
| 126 | 126 | } | |
| 127 | 127 | } | |
| 128 | 128 | ||
| 129 | + | // gameMonitor has workspace 1 with column 1 on a 300x200 output and a | |
| 130 | + | // floating game 2 (fixed size) fullscreen on its own workspace below, or | |
| 131 | + | // alone in place on workspace 2 when inPlace is set. | |
| 132 | + | func gameMonitor(o Overflow, inPlace bool) *Monitor { | |
| 133 | + | m := newMonitor("", "") | |
| 134 | + | m.SetOutput(300, 200) | |
| 135 | + | m.SetOverflow(o) | |
| 136 | + | m.AddWindow(1) | |
| 137 | + | if inPlace { | |
| 138 | + | m.Focus(1) | |
| 139 | + | } | |
| 140 | + | m.AddFloating(2, 300, 200) | |
| 141 | + | m.SetFullscreen(2, true) | |
| 142 | + | m.Focus(0) | |
| 143 | + | return m | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | // A floating game covering its workspace shows as that row's preview, | |
| 147 | + | // dedicated or in place: the row is not empty. | |
| 148 | + | func TestOverviewFullscreenFloat(t *testing.T) { | |
| 149 | + | for _, tt := range []struct { | |
| 150 | + | name string | |
| 151 | + | o Overflow | |
| 152 | + | inPlace bool | |
| 153 | + | }{ | |
| 154 | + | {"own workspace", OverflowFixed, false}, | |
| 155 | + | {"in place fixed", OverflowFixed, true}, | |
| 156 | + | {"in place scroll", OverflowScroll, true}, | |
| 157 | + | } { | |
| 158 | + | t.Run(tt.name, func(t *testing.T) { | |
| 159 | + | m := gameMonitor(tt.o, tt.inPlace) | |
| 160 | + | m.ToggleOverview() | |
| 161 | + | ps := m.Layout() | |
| 162 | + | cur, game := previewOf(t, ps, 1), previewOf(t, ps, 2) | |
| 163 | + | if game.Hidden || !game.Peek || game.Focused || game.Preview <= 0 || game.Rect.Y <= cur.Rect.Y { | |
| 164 | + | t.Fatalf("game %+v under row %+v", game, cur) | |
| 165 | + | } | |
| 166 | + | // Selected below, then picked: fullscreen and focused. | |
| 167 | + | m.OverviewMove(0, 1) | |
| 168 | + | if g := previewOf(t, m.Layout(), 2); !g.Focused || g.Peek { | |
| 169 | + | t.Fatalf("selected game %+v", g) | |
| 170 | + | } | |
| 171 | + | m.OverviewMove(-1, 0) // nothing else on its row | |
| 172 | + | m.ToggleOverview() | |
| 173 | + | if f, _ := m.Focused(); f != 2 || m.overview || m.Active != 1 || !previewOf(t, m.Layout(), 2).Fullscreen { | |
| 174 | + | t.Fatalf("confirm: %d on %d", f, m.Active) | |
| 175 | + | } | |
| 176 | + | }) | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // A click picks the game; Escape from its row returns where the overview | |
| 181 | + | // opened. | |
| 182 | + | func TestOverviewFullscreenFloatPickAndCancel(t *testing.T) { | |
| 183 | + | m := gameMonitor(OverflowFixed, false) | |
| 184 | + | m.ToggleOverview() | |
| 185 | + | r := previewOf(t, m.Layout(), 2).Rect | |
| 186 | + | if id := m.overviewAt(float64(r.X+1), float64(r.Y+1)); id != 2 { | |
| 187 | + | t.Fatalf("hit %d", id) | |
| 188 | + | } | |
| 189 | + | m.OverviewMove(0, 1) | |
| 190 | + | m.CancelOverview() | |
| 191 | + | if f, _ := m.Focused(); f != 1 || m.Active != 0 { | |
| 192 | + | t.Fatalf("cancel: %d on %d", f, m.Active) | |
| 193 | + | } | |
| 194 | + | m.ToggleOverview() | |
| 195 | + | m.OverviewPick(2) | |
| 196 | + | if f, _ := m.Focused(); f != 2 || m.overview { | |
| 197 | + | t.Fatalf("pick: %d", f) | |
| 198 | + | } | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | // The overview opens over the game too: the game is the selected row, | |
| 202 | + | // the workspace above shows dimmed, and Escape returns to the game. | |
| 203 | + | func TestOverviewOpensOverFullscreenFloat(t *testing.T) { | |
| 204 | + | m := gameMonitor(OverflowFixed, false) | |
| 205 | + | m.Focus(1) | |
| 206 | + | m.ToggleOverview() | |
| 207 | + | if !m.overview { | |
| 208 | + | t.Fatal("overview did not open") | |
| 209 | + | } | |
| 210 | + | ps := m.Layout() | |
| 211 | + | game, above := previewOf(t, ps, 2), previewOf(t, ps, 1) | |
| 212 | + | if !game.Focused || game.Peek || game.Preview <= 0 || !above.Peek || above.Rect.Y >= game.Rect.Y { | |
| 213 | + | t.Fatalf("game %+v, above %+v", game, above) | |
| 214 | + | } | |
| 215 | + | m.CancelOverview() | |
| 216 | + | if f, _ := m.Focused(); f != 2 || m.Active != 1 || m.overview || !previewOf(t, m.Layout(), 2).Fullscreen { | |
| 217 | + | t.Fatalf("cancel: %d on %d", f, m.Active) | |
| 218 | + | } | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | // A tiled fullscreen window on its own workspace (fixed overflow) is its | |
| 222 | + | // row's only preview too: the selection cannot leave it for a hidden | |
| 223 | + | // column, and Return keeps it fullscreen. | |
| 224 | + | func TestOverviewOpensOverFullscreenTile(t *testing.T) { | |
| 225 | + | m := newMonitor("", "") | |
| 226 | + | m.SetOutput(300, 200) | |
| 227 | + | m.SetOverflow(OverflowFixed) | |
| 228 | + | m.AddWindow(1) | |
| 229 | + | m.AddWindow(2) | |
| 230 | + | m.ToggleFullscreen() // 2 to its own workspace | |
| 231 | + | m.ToggleOverview() | |
| 232 | + | game := previewOf(t, m.Layout(), 2) | |
| 233 | + | if !m.overview || !game.Focused || game.Peek || !game.Fullscreen { | |
| 234 | + | t.Fatalf("game %+v", game) | |
| 235 | + | } | |
| 236 | + | m.OverviewMove(-1, 0) | |
| 237 | + | m.ToggleOverview() | |
| 238 | + | if f, _ := m.Focused(); f != 2 || m.Active != 1 || !previewOf(t, m.Layout(), 2).Fullscreen { | |
| 239 | + | t.Fatalf("confirm: %d on %d", f, m.Active) | |
| 240 | + | } | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | // Under scroll overflow a fullscreen column does not pin the row: another | |
| 244 | + | // column can be selected and picked, as scrolling would on screen. | |
| 245 | + | func TestOverviewOverScrollFullscreenPicksColumn(t *testing.T) { | |
| 246 | + | m := newMonitor("", "") | |
| 247 | + | m.SetOutput(300, 200) | |
| 248 | + | m.SetOverflow(OverflowScroll) | |
| 249 | + | m.AddWindow(1) | |
| 250 | + | m.AddWindow(2) | |
| 251 | + | m.SetFullscreen(2, true) | |
| 252 | + | m.ToggleOverview() | |
| 253 | + | if p := previewOf(t, m.Layout(), 1); p.Hidden { | |
| 254 | + | t.Fatalf("column 1 hidden: %+v", p) | |
| 255 | + | } | |
| 256 | + | m.OverviewMove(-1, 0) | |
| 257 | + | m.ToggleOverview() | |
| 258 | + | if f, _ := m.Focused(); f != 1 { | |
| 259 | + | t.Fatalf("focused %d", f) | |
| 260 | + | } | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | // Only the game shows on its row: its dialogs and the columns under it | |
| 264 | + | // stay hidden, as on screen. (Under fixed overflow a new column sends | |
| 265 | + | // the game to its own workspace instead.) | |
| 266 | + | func TestOverviewFullscreenFloatHidesOthers(t *testing.T) { | |
| 267 | + | m := gameMonitor(OverflowScroll, true) | |
| 268 | + | m.Focus(1) | |
| 269 | + | m.AddWindow(5) | |
| 270 | + | m.Current().FocusID(2) // back on the game, over column 5 | |
| 271 | + | m.AddWindow(3) | |
| 272 | + | m.AddFloating(4, 50, 50) | |
| 273 | + | m.Focus(0) | |
| 274 | + | m.ToggleOverview() | |
| 275 | + | ps := m.Layout() | |
| 276 | + | if previewOf(t, ps, 2).Hidden { | |
| 277 | + | t.Fatal("game hidden") | |
| 278 | + | } | |
| 279 | + | for _, id := range []WindowID{3, 4, 5} { | |
| 280 | + | if p := previewOf(t, ps, id); !p.Hidden { | |
| 281 | + | t.Fatalf("%d shown: %+v", id, p) | |
| 282 | + | } | |
| 283 | + | } | |
| 284 | + | } | |
| 285 | + | ||
| 129 | 286 | // pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1 | |
| 130 | 287 | // (7 selected) and the stash hidden. | |
| 131 | 288 | func pileMonitor() *Monitor { | |
| @@ -126,6 +126,163 @@ func TestOverviewPick(t *testing.T) { | |||||
| 126 | } | 126 | } | ||
| 127 | } | 127 | } | ||
| 128 | 128 | ||||
| 129 | + | // gameMonitor has workspace 1 with column 1 on a 300x200 output and a | |||
| 130 | + | // floating game 2 (fixed size) fullscreen on its own workspace below, or | |||
| 131 | + | // alone in place on workspace 2 when inPlace is set. | |||
| 132 | + | func gameMonitor(o Overflow, inPlace bool) *Monitor { | |||
| 133 | + | m := newMonitor("", "") | |||
| 134 | + | m.SetOutput(300, 200) | |||
| 135 | + | m.SetOverflow(o) | |||
| 136 | + | m.AddWindow(1) | |||
| 137 | + | if inPlace { | |||
| 138 | + | m.Focus(1) | |||
| 139 | + | } | |||
| 140 | + | m.AddFloating(2, 300, 200) | |||
| 141 | + | m.SetFullscreen(2, true) | |||
| 142 | + | m.Focus(0) | |||
| 143 | + | return m | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | // A floating game covering its workspace shows as that row's preview, | |||
| 147 | + | // dedicated or in place: the row is not empty. | |||
| 148 | + | func TestOverviewFullscreenFloat(t *testing.T) { | |||
| 149 | + | for _, tt := range []struct { | |||
| 150 | + | name string | |||
| 151 | + | o Overflow | |||
| 152 | + | inPlace bool | |||
| 153 | + | }{ | |||
| 154 | + | {"own workspace", OverflowFixed, false}, | |||
| 155 | + | {"in place fixed", OverflowFixed, true}, | |||
| 156 | + | {"in place scroll", OverflowScroll, true}, | |||
| 157 | + | } { | |||
| 158 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 159 | + | m := gameMonitor(tt.o, tt.inPlace) | |||
| 160 | + | m.ToggleOverview() | |||
| 161 | + | ps := m.Layout() | |||
| 162 | + | cur, game := previewOf(t, ps, 1), previewOf(t, ps, 2) | |||
| 163 | + | if game.Hidden || !game.Peek || game.Focused || game.Preview <= 0 || game.Rect.Y <= cur.Rect.Y { | |||
| 164 | + | t.Fatalf("game %+v under row %+v", game, cur) | |||
| 165 | + | } | |||
| 166 | + | // Selected below, then picked: fullscreen and focused. | |||
| 167 | + | m.OverviewMove(0, 1) | |||
| 168 | + | if g := previewOf(t, m.Layout(), 2); !g.Focused || g.Peek { | |||
| 169 | + | t.Fatalf("selected game %+v", g) | |||
| 170 | + | } | |||
| 171 | + | m.OverviewMove(-1, 0) // nothing else on its row | |||
| 172 | + | m.ToggleOverview() | |||
| 173 | + | if f, _ := m.Focused(); f != 2 || m.overview || m.Active != 1 || !previewOf(t, m.Layout(), 2).Fullscreen { | |||
| 174 | + | t.Fatalf("confirm: %d on %d", f, m.Active) | |||
| 175 | + | } | |||
| 176 | + | }) | |||
| 177 | + | } | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // A click picks the game; Escape from its row returns where the overview | |||
| 181 | + | // opened. | |||
| 182 | + | func TestOverviewFullscreenFloatPickAndCancel(t *testing.T) { | |||
| 183 | + | m := gameMonitor(OverflowFixed, false) | |||
| 184 | + | m.ToggleOverview() | |||
| 185 | + | r := previewOf(t, m.Layout(), 2).Rect | |||
| 186 | + | if id := m.overviewAt(float64(r.X+1), float64(r.Y+1)); id != 2 { | |||
| 187 | + | t.Fatalf("hit %d", id) | |||
| 188 | + | } | |||
| 189 | + | m.OverviewMove(0, 1) | |||
| 190 | + | m.CancelOverview() | |||
| 191 | + | if f, _ := m.Focused(); f != 1 || m.Active != 0 { | |||
| 192 | + | t.Fatalf("cancel: %d on %d", f, m.Active) | |||
| 193 | + | } | |||
| 194 | + | m.ToggleOverview() | |||
| 195 | + | m.OverviewPick(2) | |||
| 196 | + | if f, _ := m.Focused(); f != 2 || m.overview { | |||
| 197 | + | t.Fatalf("pick: %d", f) | |||
| 198 | + | } | |||
| 199 | + | } | |||
| 200 | + | ||||
| 201 | + | // The overview opens over the game too: the game is the selected row, | |||
| 202 | + | // the workspace above shows dimmed, and Escape returns to the game. | |||
| 203 | + | func TestOverviewOpensOverFullscreenFloat(t *testing.T) { | |||
| 204 | + | m := gameMonitor(OverflowFixed, false) | |||
| 205 | + | m.Focus(1) | |||
| 206 | + | m.ToggleOverview() | |||
| 207 | + | if !m.overview { | |||
| 208 | + | t.Fatal("overview did not open") | |||
| 209 | + | } | |||
| 210 | + | ps := m.Layout() | |||
| 211 | + | game, above := previewOf(t, ps, 2), previewOf(t, ps, 1) | |||
| 212 | + | if !game.Focused || game.Peek || game.Preview <= 0 || !above.Peek || above.Rect.Y >= game.Rect.Y { | |||
| 213 | + | t.Fatalf("game %+v, above %+v", game, above) | |||
| 214 | + | } | |||
| 215 | + | m.CancelOverview() | |||
| 216 | + | if f, _ := m.Focused(); f != 2 || m.Active != 1 || m.overview || !previewOf(t, m.Layout(), 2).Fullscreen { | |||
| 217 | + | t.Fatalf("cancel: %d on %d", f, m.Active) | |||
| 218 | + | } | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | // A tiled fullscreen window on its own workspace (fixed overflow) is its | |||
| 222 | + | // row's only preview too: the selection cannot leave it for a hidden | |||
| 223 | + | // column, and Return keeps it fullscreen. | |||
| 224 | + | func TestOverviewOpensOverFullscreenTile(t *testing.T) { | |||
| 225 | + | m := newMonitor("", "") | |||
| 226 | + | m.SetOutput(300, 200) | |||
| 227 | + | m.SetOverflow(OverflowFixed) | |||
| 228 | + | m.AddWindow(1) | |||
| 229 | + | m.AddWindow(2) | |||
| 230 | + | m.ToggleFullscreen() // 2 to its own workspace | |||
| 231 | + | m.ToggleOverview() | |||
| 232 | + | game := previewOf(t, m.Layout(), 2) | |||
| 233 | + | if !m.overview || !game.Focused || game.Peek || !game.Fullscreen { | |||
| 234 | + | t.Fatalf("game %+v", game) | |||
| 235 | + | } | |||
| 236 | + | m.OverviewMove(-1, 0) | |||
| 237 | + | m.ToggleOverview() | |||
| 238 | + | if f, _ := m.Focused(); f != 2 || m.Active != 1 || !previewOf(t, m.Layout(), 2).Fullscreen { | |||
| 239 | + | t.Fatalf("confirm: %d on %d", f, m.Active) | |||
| 240 | + | } | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | // Under scroll overflow a fullscreen column does not pin the row: another | |||
| 244 | + | // column can be selected and picked, as scrolling would on screen. | |||
| 245 | + | func TestOverviewOverScrollFullscreenPicksColumn(t *testing.T) { | |||
| 246 | + | m := newMonitor("", "") | |||
| 247 | + | m.SetOutput(300, 200) | |||
| 248 | + | m.SetOverflow(OverflowScroll) | |||
| 249 | + | m.AddWindow(1) | |||
| 250 | + | m.AddWindow(2) | |||
| 251 | + | m.SetFullscreen(2, true) | |||
| 252 | + | m.ToggleOverview() | |||
| 253 | + | if p := previewOf(t, m.Layout(), 1); p.Hidden { | |||
| 254 | + | t.Fatalf("column 1 hidden: %+v", p) | |||
| 255 | + | } | |||
| 256 | + | m.OverviewMove(-1, 0) | |||
| 257 | + | m.ToggleOverview() | |||
| 258 | + | if f, _ := m.Focused(); f != 1 { | |||
| 259 | + | t.Fatalf("focused %d", f) | |||
| 260 | + | } | |||
| 261 | + | } | |||
| 262 | + | ||||
| 263 | + | // Only the game shows on its row: its dialogs and the columns under it | |||
| 264 | + | // stay hidden, as on screen. (Under fixed overflow a new column sends | |||
| 265 | + | // the game to its own workspace instead.) | |||
| 266 | + | func TestOverviewFullscreenFloatHidesOthers(t *testing.T) { | |||
| 267 | + | m := gameMonitor(OverflowScroll, true) | |||
| 268 | + | m.Focus(1) | |||
| 269 | + | m.AddWindow(5) | |||
| 270 | + | m.Current().FocusID(2) // back on the game, over column 5 | |||
| 271 | + | m.AddWindow(3) | |||
| 272 | + | m.AddFloating(4, 50, 50) | |||
| 273 | + | m.Focus(0) | |||
| 274 | + | m.ToggleOverview() | |||
| 275 | + | ps := m.Layout() | |||
| 276 | + | if previewOf(t, ps, 2).Hidden { | |||
| 277 | + | t.Fatal("game hidden") | |||
| 278 | + | } | |||
| 279 | + | for _, id := range []WindowID{3, 4, 5} { | |||
| 280 | + | if p := previewOf(t, ps, id); !p.Hidden { | |||
| 281 | + | t.Fatalf("%d shown: %+v", id, p) | |||
| 282 | + | } | |||
| 283 | + | } | |||
| 284 | + | } | |||
| 285 | + | ||||
| 129 | // pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1 | 286 | // pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1 | ||
| 130 | // (7 selected) and the stash hidden. | 287 | // (7 selected) and the stash hidden. | ||
| 131 | func pileMonitor() *Monitor { | 288 | func pileMonitor() *Monitor { | ||