detached 09abb12

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 UnverifiedB5690EEEBB952194 The 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
136136
137137 ### Overview
138138
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.
140140
141141 | Key | Action |
142142 | --- | --- |
@@ -136,7 +136,7 @@ Native floating windows (dialogs, file pickers) are not in the stash: they stay
136 136
137 ### Overview137 ### 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
4646 if !ok {
4747 v = ports.ConfigureWindow{ID: p.ID, Width: int(float64(p.Rect.W) / p.Preview), Height: int(float64(p.Rect.H) / p.Preview)}
4848 }
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+ }
4954 v.Activated, v.Visible, v.Output = t.focused, onScreen(p, t.area), t.output
5055 default:
5156 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.output54 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) {
2727 {name: "hidden new", p: Placement{ID: 1, Hidden: true, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Output: "A"}, send: true},
2828 {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},
2929 {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},
3032 {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},
3133 }
3234 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) {
212212 }
213213 }
214214
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+
215230 // Under a covering fullscreen float, or any fixed-overflow fullscreen,
216231 // focus moves inside the workspace are off and binds act on the window on
217232 // 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 on231 // 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) {
8787 }
8888
8989 // 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.
9192 func hasFullscreen(sc *screen) bool {
92- return sc.mon.Current().cover() != 0
93+ return !sc.mon.overview && sc.mon.Current().cover() != 0
9394 }
9495
9596 // 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. In89 // 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() != 093+ 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 is96 // 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) {
142142 }
143143 }
144144
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+
145163 // A fullscreen window is exclusive: the scene carries only the background
146164 // and a layer taking the keyboard exclusively (a locker), so the output
147165 // 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 background163 // A fullscreen window is exclusive: the scene carries only the background
146 // and a layer taking the keyboard exclusively (a locker), so the output164 // 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) {
287287 // window (ADR 011): it waits, hidden, after the focused column.
288288 at := len(w.Columns)
289289 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.
291291 at = min(w.Focus+1, len(w.Columns))
292292 }
293293 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 (
99
1010 // The overview shows every column of the workspace on screen in one row,
1111 // 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
1416 // column, j/k (or down/up) a workspace, Return keeps the selection and
1517 // Escape returns to where the overview opened.
1618
@@ -29,17 +31,15 @@ const (
2931 // scroll to keep the selected column in view.
3032 const overviewMaxZoom, overviewMinZoom = 0.6, 0.25
3133
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.
3437 func (m *Monitor) ToggleOverview() {
3538 if m.overview {
3639 m.closeOverview()
3740 return
3841 }
3942 w := m.Current()
40- if w.cover() != 0 {
41- return
42- }
4343 // A slide in progress lands at once: the overview lays out the
4444 // settled state.
4545 m.stopSwitch()
@@ -54,10 +54,15 @@ func (m *Monitor) ToggleOverview() {
5454 // selectRow selects the stash card of a workspace whose stash has the
5555 // focus (or only stashed windows), else its focused column. The overview
5656 // 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.
5859 func (m *Monitor) selectRow() {
5960 m.overviewCard, m.overviewCardOf = 0, nil
6061 w := m.Current()
62+ if w.pinned() {
63+ // The covering window is the row's only preview: it keeps the focus.
64+ return
65+ }
6166 w.floatFocus = false
6267 if len(w.Stash) > 0 && (len(w.Columns) == 0 || w.stashFocused()) {
6368 m.selectCard(w, w.stashAt)
@@ -134,6 +139,9 @@ func (m *Monitor) OverviewMove(dx, dy int) {
134139 return
135140 }
136141 w := m.Current()
142+ if w.pinned() {
143+ return
144+ }
137145 if at := m.cardAt(w); at >= 0 {
138146 switch {
139147 case at+dx >= 0 && at+dx < len(w.Stash):
@@ -195,6 +203,16 @@ func (m *Monitor) overviewLayout() []Placement {
195203 }
196204 for _, w := range m.all() {
197205 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+ }
198216 if shown {
199217 at := m.cardAt(w)
200218 result = append(result, w.previewRow(ry, w != cur, at < 0)...)
@@ -217,7 +235,7 @@ func (m *Monitor) overviewLayout() []Placement {
217235 // pileWidth is the room the stash pile of w takes on the left of its row,
218236 // its margin included; 0 without a stash.
219237 func (w *Workspace) pileWidth() int {
220- if len(w.Stash) == 0 {
238+ if len(w.Stash) == 0 || w.pinned() {
221239 return 0
222240 }
223241 card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom))
@@ -282,6 +300,13 @@ func (w *Workspace) overviewZoom() float64 {
282300 func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) {
283301 g := w.gap()
284302 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+ }
285310 for i, c := range w.Columns {
286311 r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h}
287312 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, with11 // 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 a13+// 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 and16 // 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.2532 const overviewMaxZoom, overviewMinZoom = 0.6, 0.25
31 33
32-// ToggleOverview opens the overview, or closes it on the selection. It34+// 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 return40 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 the43 // 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 the54 // selectRow selects the stash card of a workspace whose stash has the
55 // focus (or only stashed windows), else its focused column. The overview55 // focus (or only stashed windows), else its focused column. The overview
56 // hides native floats, so a float's focus moves to its column; Escape56 // 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, nil60 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 = false66 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 return139 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 0239 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) {
126126 }
127127 }
128128
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+
129286 // pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1
130287 // (7 selected) and the stash hidden.
131288 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 1286 // 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 {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close