detached 06140d8

fix(core): show windows hidden by a fullscreen window in the overview

A pinned fullscreen window (fixed overflow, or a fullscreen float) is now
the front card of its overview row, with the windows it hides behind it.
Choosing one of them leaves fullscreen; confirming or cancelling to the
fullscreen window keeps it.

Updated tests asserting the former single-preview row:
TestOverviewOpensOverFullscreenTile, TestOverviewFullscreenFloatRow.
commit
06140d8c37f9baf41f3e6b55fb46fbb2d1e50344
author
brice · · 2026-10-02 15:24 UTC
parent
b87e882
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +281 -62
M internal/core/layout.go +3 -2
@@ -1078,8 +1078,9 @@ func (w *Workspace) mayCover(id WindowID) bool {
10781078 }
10791079
10801080 // pinned reports whether the covering fullscreen window hides every other
1081-// window: focus moves leave it (leaveCover). Only in scroll overflow does
1082-// moving to a tiled neighbor scroll it off and show the target instead.
1081+// window: focus moves leave it (leaveCover), and the overview shows it as
1082+// the front card of its row. Only in scroll overflow does moving to a
1083+// tiled neighbor scroll it off and show the target instead.
10831084 func (w *Workspace) pinned() bool {
10841085 c := w.cover()
10851086 return c != 0 && (w.Overflow == OverflowFixed || w.isFloat(c))
@@ -1078,8 +1078,9 @@ func (w *Workspace) mayCover(id WindowID) bool {
1078 }1078 }
1079 1079
1080 // pinned reports whether the covering fullscreen window hides every other1080 // pinned reports whether the covering fullscreen window hides every other
1081-// window: focus moves leave it (leaveCover). Only in scroll overflow does1081+// window: focus moves leave it (leaveCover), and the overview shows it as
1082-// moving to a tiled neighbor scroll it off and show the target instead.1082+// the front card of its row. Only in scroll overflow does moving to a
1083+// tiled neighbor scroll it off and show the target instead.
1083 func (w *Workspace) pinned() bool {1084 func (w *Workspace) pinned() bool {
1084 c := w.cover()1085 c := w.cover()
1085 return c != 0 && (w.Overflow == OverflowFixed || w.isFloat(c))1086 return c != 0 && (w.Overflow == OverflowFixed || w.isFloat(c))
M internal/core/overview.go +25 -34
@@ -44,7 +44,8 @@ type rowSnapshot struct {
4444 // The overview scales workspace rows without resizing clients. Covering
4545 // floats and the columns form a linear stack of cards; small floats stay
4646 // hidden. Up/down traverse the stack before changing workspace. A pinned
47-// fullscreen window remains the only preview on its row.
47+// fullscreen window is the front card of its row, the windows it hides
48+// behind it; choosing one of them leaves fullscreen.
4849
4950 // A workspace's stash shows as a pile of cards pinned to the left edge of
5051 // its row: the selected stashed window in front at overviewCardZoom, up to
@@ -108,9 +109,8 @@ func (m *Monitor) selectRow() {
108109 m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0
109110 m.ov.selected, m.ov.selectedAt = 0, -1
110111 w := m.Current()
111- if w.pinned() {
112- return
113- }
112+ // A fullscreen window has the focus: its card is in front.
113+ w.focusCover()
114114 if w.floatFocus && len(w.Columns) > 0 && (len(w.stack()) < 2 || m.stackFront(w).kind == stackColumns) {
115115 w.floatFocus = false
116116 }
@@ -150,9 +150,6 @@ func (m *Monitor) overviewTarget() WindowID {
150150 return id
151151 }
152152 w := m.Current()
153- if w.pinned() {
154- return w.cover()
155- }
156153 item := m.stackFront(w)
157154 switch item.kind {
158155 case stackFloat:
@@ -222,17 +219,22 @@ func (m *Monitor) overviewMoveTo(a Action) {
222219
223220 // closeOverview closes the overview on the selection: a card shows the
224221 // stash on it. A fullscreen request another stashed window made while
225-// the stash was hidden is dropped: the picked card stays in front.
222+// the stash was hidden is dropped: the picked card stays in front. A
223+// window a pinned fullscreen window hides leaves that fullscreen.
226224 func (m *Monitor) closeOverview() {
227225 w := m.Current()
226+ front, target := m.stackFront(w), m.overviewTarget()
227+ if w.pinned() && target != w.cover() {
228+ w.endFullscreen()
229+ }
228230 if i := m.cardAt(w); i >= 0 {
229231 if w.hiddenFullscreen != w.Stash[i].ID {
230232 w.hiddenFullscreen = 0
231233 }
232234 w.stashAt = i
233235 w.showStash()
234- } else if !w.pinned() && len(w.stack()) > 0 {
235- w.apply(m.stackFront(w), m.overviewTarget())
236+ } else if front != (stackItem{}) {
237+ w.apply(front, target)
236238 }
237239 for r, s := range m.ov.rows {
238240 if r != w && m.has(r) {
@@ -373,9 +375,6 @@ func (m *Monitor) OverviewMove(dx, dy int) {
373375 m.showOverview(rows[i])
374376 return
375377 }
376- if w.pinned() {
377- return
378- }
379378 if at := m.cardAt(w); at >= 0 {
380379 switch {
381380 case at+dx >= 0 && at+dx < len(w.Stash):
@@ -447,7 +446,7 @@ func (m *Monitor) OverviewPick(id WindowID) {
447446 }
448447 if w.floatIndex(id) >= 0 && w.stashIndex(id) < 0 {
449448 i := w.floatIndex(id)
450- if !w.coversFloat(w.Floats[i]) && !w.pinned() {
449+ if !w.coversFloat(w.Floats[i]) && !(id == w.cover() && w.pinned()) {
451450 return
452451 }
453452 }
@@ -455,8 +454,6 @@ func (m *Monitor) OverviewPick(id WindowID) {
455454 m.ov.card, m.ov.cardOf = 0, nil
456455 if i := w.stashIndex(id); i >= 0 {
457456 m.selectCard(w, i)
458- } else if w.pinned() {
459- w.FocusID(id)
460457 } else if item, ok := w.itemOf(id); ok {
461458 m.setFront(w, item)
462459 if i := w.columnOf(id); i >= 0 {
@@ -522,16 +519,6 @@ func (m *Monitor) overviewLayout() []Placement {
522519 var result []Placement
523520 for w := range m.all() {
524521 ry, shown := rows[w]
525- if shown && w.pinned() {
526- // Only the covering window shows, as on screen.
527- result = append(result, w.previewRow(ry, w != cur, true)...)
528- for _, id := range w.windows() {
529- if id != w.fullscreen {
530- result = append(result, Placement{ID: id, Hidden: true})
531- }
532- }
533- continue
534- }
535522 if shown {
536523 at := m.cardAt(w)
537524 result = append(result, m.stackRow(w, ry, w != cur, at < 0)...)
@@ -622,7 +609,7 @@ func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool,
622609 // pileWidth is the room the stash pile of w takes on the left of its row,
623610 // its margin included; 0 without a stash.
624611 func (w *Workspace) pileWidth() int {
625- if len(w.Stash) == 0 || w.pinned() {
612+ if len(w.Stash) == 0 {
626613 return 0
627614 }
628615 card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom))
@@ -713,15 +700,19 @@ func (w *Workspace) rowHeight() int {
713700 // corner. Fixed overflow uses its on-screen geometry; scroll overflow uses
714701 // an unscrolled row. span is the row's width; sel is the focused column.
715702 func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) {
703+ if c := w.cover(); c != 0 && w.pinned() {
704+ // The row shows the columns a pinned fullscreen window hides,
705+ // laid out as without it; its own tile stays marked fullscreen.
706+ v := *w
707+ v.fullscreen = 0
708+ tiles, span, sel = v.previewTiles()
709+ for i := range tiles {
710+ tiles[i].Fullscreen = tiles[i].ID == c
711+ }
712+ return tiles, span, sel
713+ }
716714 g := w.gap()
717715 h := max(w.Usable.H-2*g, 0)
718- if w.pinned() {
719- // A covering window nothing else is reachable under (a game) is
720- // the row's only tile, at the size of a fullscreen column. It
721- // stays fullscreen.
722- r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: h}
723- return []Placement{{ID: w.cover(), Rect: r, Focused: true, Fullscreen: true}}, r.X + r.W + g, r
724- }
725716 var rects []Rect
726717 if w.Overflow == OverflowFixed {
727718 rects = w.columnRects()
@@ -44,7 +44,8 @@ type rowSnapshot struct {
44 // The overview scales workspace rows without resizing clients. Covering44 // The overview scales workspace rows without resizing clients. Covering
45 // floats and the columns form a linear stack of cards; small floats stay45 // floats and the columns form a linear stack of cards; small floats stay
46 // hidden. Up/down traverse the stack before changing workspace. A pinned46 // hidden. Up/down traverse the stack before changing workspace. A pinned
47-// fullscreen window remains the only preview on its row.47+// fullscreen window is the front card of its row, the windows it hides
48+// behind it; choosing one of them leaves fullscreen.
48 49
49 // A workspace's stash shows as a pile of cards pinned to the left edge of50 // A workspace's stash shows as a pile of cards pinned to the left edge of
50 // its row: the selected stashed window in front at overviewCardZoom, up to51 // its row: the selected stashed window in front at overviewCardZoom, up to
@@ -108,9 +109,8 @@ func (m *Monitor) selectRow() {
108 m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0109 m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0
109 m.ov.selected, m.ov.selectedAt = 0, -1110 m.ov.selected, m.ov.selectedAt = 0, -1
110 w := m.Current()111 w := m.Current()
111- if w.pinned() {112+ // A fullscreen window has the focus: its card is in front.
112- return113+ w.focusCover()
113- }
114 if w.floatFocus && len(w.Columns) > 0 && (len(w.stack()) < 2 || m.stackFront(w).kind == stackColumns) {114 if w.floatFocus && len(w.Columns) > 0 && (len(w.stack()) < 2 || m.stackFront(w).kind == stackColumns) {
115 w.floatFocus = false115 w.floatFocus = false
116 }116 }
@@ -150,9 +150,6 @@ func (m *Monitor) overviewTarget() WindowID {
150 return id150 return id
151 }151 }
152 w := m.Current()152 w := m.Current()
153- if w.pinned() {
154- return w.cover()
155- }
156 item := m.stackFront(w)153 item := m.stackFront(w)
157 switch item.kind {154 switch item.kind {
158 case stackFloat:155 case stackFloat:
@@ -222,17 +219,22 @@ func (m *Monitor) overviewMoveTo(a Action) {
222 219
223 // closeOverview closes the overview on the selection: a card shows the220 // closeOverview closes the overview on the selection: a card shows the
224 // stash on it. A fullscreen request another stashed window made while221 // stash on it. A fullscreen request another stashed window made while
225-// the stash was hidden is dropped: the picked card stays in front.222+// the stash was hidden is dropped: the picked card stays in front. A
223+// window a pinned fullscreen window hides leaves that fullscreen.
226 func (m *Monitor) closeOverview() {224 func (m *Monitor) closeOverview() {
227 w := m.Current()225 w := m.Current()
226+ front, target := m.stackFront(w), m.overviewTarget()
227+ if w.pinned() && target != w.cover() {
228+ w.endFullscreen()
229+ }
228 if i := m.cardAt(w); i >= 0 {230 if i := m.cardAt(w); i >= 0 {
229 if w.hiddenFullscreen != w.Stash[i].ID {231 if w.hiddenFullscreen != w.Stash[i].ID {
230 w.hiddenFullscreen = 0232 w.hiddenFullscreen = 0
231 }233 }
232 w.stashAt = i234 w.stashAt = i
233 w.showStash()235 w.showStash()
234- } else if !w.pinned() && len(w.stack()) > 0 {236+ } else if front != (stackItem{}) {
235- w.apply(m.stackFront(w), m.overviewTarget())237+ w.apply(front, target)
236 }238 }
237 for r, s := range m.ov.rows {239 for r, s := range m.ov.rows {
238 if r != w && m.has(r) {240 if r != w && m.has(r) {
@@ -373,9 +375,6 @@ func (m *Monitor) OverviewMove(dx, dy int) {
373 m.showOverview(rows[i])375 m.showOverview(rows[i])
374 return376 return
375 }377 }
376- if w.pinned() {
377- return
378- }
379 if at := m.cardAt(w); at >= 0 {378 if at := m.cardAt(w); at >= 0 {
380 switch {379 switch {
381 case at+dx >= 0 && at+dx < len(w.Stash):380 case at+dx >= 0 && at+dx < len(w.Stash):
@@ -447,7 +446,7 @@ func (m *Monitor) OverviewPick(id WindowID) {
447 }446 }
448 if w.floatIndex(id) >= 0 && w.stashIndex(id) < 0 {447 if w.floatIndex(id) >= 0 && w.stashIndex(id) < 0 {
449 i := w.floatIndex(id)448 i := w.floatIndex(id)
450- if !w.coversFloat(w.Floats[i]) && !w.pinned() {449+ if !w.coversFloat(w.Floats[i]) && !(id == w.cover() && w.pinned()) {
451 return450 return
452 }451 }
453 }452 }
@@ -455,8 +454,6 @@ func (m *Monitor) OverviewPick(id WindowID) {
455 m.ov.card, m.ov.cardOf = 0, nil454 m.ov.card, m.ov.cardOf = 0, nil
456 if i := w.stashIndex(id); i >= 0 {455 if i := w.stashIndex(id); i >= 0 {
457 m.selectCard(w, i)456 m.selectCard(w, i)
458- } else if w.pinned() {
459- w.FocusID(id)
460 } else if item, ok := w.itemOf(id); ok {457 } else if item, ok := w.itemOf(id); ok {
461 m.setFront(w, item)458 m.setFront(w, item)
462 if i := w.columnOf(id); i >= 0 {459 if i := w.columnOf(id); i >= 0 {
@@ -522,16 +519,6 @@ func (m *Monitor) overviewLayout() []Placement {
522 var result []Placement519 var result []Placement
523 for w := range m.all() {520 for w := range m.all() {
524 ry, shown := rows[w]521 ry, shown := rows[w]
525- if shown && w.pinned() {
526- // Only the covering window shows, as on screen.
527- result = append(result, w.previewRow(ry, w != cur, true)...)
528- for _, id := range w.windows() {
529- if id != w.fullscreen {
530- result = append(result, Placement{ID: id, Hidden: true})
531- }
532- }
533- continue
534- }
535 if shown {522 if shown {
536 at := m.cardAt(w)523 at := m.cardAt(w)
537 result = append(result, m.stackRow(w, ry, w != cur, at < 0)...)524 result = append(result, m.stackRow(w, ry, w != cur, at < 0)...)
@@ -622,7 +609,7 @@ func fan(cards [][]Placement, front, maxBehind, maxBefore int, circular bool,
622 // pileWidth is the room the stash pile of w takes on the left of its row,609 // pileWidth is the room the stash pile of w takes on the left of its row,
623 // its margin included; 0 without a stash.610 // its margin included; 0 without a stash.
624 func (w *Workspace) pileWidth() int {611 func (w *Workspace) pileWidth() int {
625- if len(w.Stash) == 0 || w.pinned() {612+ if len(w.Stash) == 0 {
626 return 0613 return 0
627 }614 }
628 card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom))615 card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom))
@@ -713,15 +700,19 @@ func (w *Workspace) rowHeight() int {
713 // corner. Fixed overflow uses its on-screen geometry; scroll overflow uses700 // corner. Fixed overflow uses its on-screen geometry; scroll overflow uses
714 // an unscrolled row. span is the row's width; sel is the focused column.701 // an unscrolled row. span is the row's width; sel is the focused column.
715 func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) {702 func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) {
703+ if c := w.cover(); c != 0 && w.pinned() {
704+ // The row shows the columns a pinned fullscreen window hides,
705+ // laid out as without it; its own tile stays marked fullscreen.
706+ v := *w
707+ v.fullscreen = 0
708+ tiles, span, sel = v.previewTiles()
709+ for i := range tiles {
710+ tiles[i].Fullscreen = tiles[i].ID == c
711+ }
712+ return tiles, span, sel
713+ }
716 g := w.gap()714 g := w.gap()
717 h := max(w.Usable.H-2*g, 0)715 h := max(w.Usable.H-2*g, 0)
718- if w.pinned() {
719- // A covering window nothing else is reachable under (a game) is
720- // the row's only tile, at the size of a fullscreen column. It
721- // stays fullscreen.
722- r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: h}
723- return []Placement{{ID: w.cover(), Rect: r, Focused: true, Fullscreen: true}}, r.X + r.W + g, r
724- }
725 var rects []Rect716 var rects []Rect
726 if w.Overflow == OverflowFixed {717 if w.Overflow == OverflowFixed {
727 rects = w.columnRects()718 rects = w.columnRects()
A internal/core/overview_cover_test.go +186 -0
@@ -0,0 +1,186 @@
1+package core
2+
3+import "testing"
4+
5+// coverMonitor has, on a 300x200 output, tiles 1 and 2 on one workspace,
6+// 2 fullscreen. With float, 2 is a floating game over tile 1 instead.
7+func coverMonitor(o Overflow, float bool) *Monitor {
8+ m := newMonitor("", "")
9+ m.SetOutput(300, 200)
10+ m.SetOverflow(o)
11+ m.AddWindow(1)
12+ if float {
13+ m.AddFloating(2, 300, 200)
14+ m.SetFullscreen(2, true)
15+ } else {
16+ m.AddWindow(2)
17+ m.ToggleFullscreen()
18+ }
19+ return m
20+}
21+
22+// pinnedCases are the setups where the fullscreen window pins its row.
23+var pinnedCases = []struct {
24+ name string
25+ overflow Overflow
26+ float bool
27+}{
28+ {"fixed tile", OverflowFixed, false},
29+ {"fixed float", OverflowFixed, true},
30+ {"scroll float", OverflowScroll, true},
31+}
32+
33+// uniquePlacements fails when a window is placed twice.
34+func uniquePlacements(t *testing.T, ps []Placement) {
35+ t.Helper()
36+ seen := map[WindowID]bool{}
37+ for _, p := range ps {
38+ if seen[p.ID] {
39+ t.Fatalf("window %d placed twice: %+v", p.ID, ps)
40+ }
41+ seen[p.ID] = true
42+ }
43+}
44+
45+// The overview over a pinned fullscreen window shows it in front, still
46+// fullscreen and selected, and the window it hides behind it.
47+func TestOverviewCoverShowsWorkspace(t *testing.T) {
48+ for _, tc := range pinnedCases {
49+ t.Run(tc.name, func(t *testing.T) {
50+ m := coverMonitor(tc.overflow, tc.float)
51+ if !m.Current().pinned() {
52+ t.Fatal("not pinned")
53+ }
54+ m.ToggleOverview()
55+ ps := m.Layout()
56+ uniquePlacements(t, ps)
57+ game, behind := previewOf(t, ps, 2), previewOf(t, ps, 1)
58+ if game.Hidden || !game.Focused || game.Peek || !game.Fullscreen {
59+ t.Fatalf("game %+v", game)
60+ }
61+ if behind.Hidden || !behind.Peek || behind.Focused {
62+ t.Fatalf("behind %+v", behind)
63+ }
64+ })
65+ }
66+}
67+
68+// Return or a click on the fullscreen window keeps it fullscreen.
69+func TestOverviewCoverConfirmKeeps(t *testing.T) {
70+ for _, tc := range pinnedCases {
71+ t.Run(tc.name, func(t *testing.T) {
72+ for _, pick := range []bool{false, true} {
73+ m := coverMonitor(tc.overflow, tc.float)
74+ m.ToggleOverview()
75+ if pick {
76+ m.OverviewPick(2)
77+ } else {
78+ m.ToggleOverview()
79+ }
80+ if f, _ := m.Focused(); f != 2 || m.ov.open || m.Current().fullscreen != 2 || !previewOf(t, m.Layout(), 2).Fullscreen {
81+ t.Fatalf("pick %v: focused %d, fullscreen %d", pick, f, m.Current().fullscreen)
82+ }
83+ }
84+ })
85+ }
86+}
87+
88+// Choosing the window behind leaves fullscreen and focuses it; the left
89+// window shows again, a float behind the tiles.
90+func TestOverviewCoverPickOther(t *testing.T) {
91+ for _, tc := range pinnedCases {
92+ t.Run(tc.name, func(t *testing.T) {
93+ for _, pick := range []bool{false, true} {
94+ m := coverMonitor(tc.overflow, tc.float)
95+ m.ToggleOverview()
96+ if pick {
97+ m.OverviewPick(1)
98+ } else {
99+ m.OverviewMove(-1, 0)
100+ m.ToggleOverview()
101+ }
102+ w := m.Current()
103+ if f, _ := m.Focused(); f != 1 || m.ov.open || w.fullscreen != 0 {
104+ t.Fatalf("pick %v: focused %d, fullscreen %d", pick, f, w.fullscreen)
105+ }
106+ if p := placement(w, 2); p.Fullscreen || (p.Hidden && !tc.float) {
107+ t.Fatalf("pick %v: left window %+v", pick, p)
108+ }
109+ if i := w.floatIndex(2); tc.float && (i < 0 || !w.Floats[i].below) {
110+ t.Fatalf("pick %v: float %+v", pick, w.Floats)
111+ }
112+ }
113+ })
114+ }
115+}
116+
117+// Escape returns to the fullscreen window, still fullscreen.
118+func TestOverviewCoverCancel(t *testing.T) {
119+ for _, tc := range pinnedCases {
120+ t.Run(tc.name, func(t *testing.T) {
121+ m := coverMonitor(tc.overflow, tc.float)
122+ m.ToggleOverview()
123+ m.OverviewMove(-1, 0)
124+ m.CancelOverview()
125+ if f, _ := m.Focused(); f != 2 || m.Current().fullscreen != 2 {
126+ t.Fatalf("focused %d, fullscreen %d", f, m.Current().fullscreen)
127+ }
128+ })
129+ }
130+}
131+
132+// A stash card is reachable under a fullscreen tile; choosing it leaves
133+// fullscreen.
134+func TestOverviewCoverPickStashCard(t *testing.T) {
135+ m := newMonitor("", "")
136+ m.SetOutput(300, 200)
137+ m.SetOverflow(OverflowFixed)
138+ w := m.Current()
139+ w.AddWindow(1)
140+ w.AddWindow(5)
141+ w.FocusID(5)
142+ w.ToggleWindowStash()
143+ w.FocusID(1)
144+ m.ToggleFullscreen()
145+ if !w.pinned() {
146+ t.Fatal("not pinned")
147+ }
148+ m.ToggleOverview()
149+ uniquePlacements(t, m.Layout())
150+ m.OverviewMove(-1, 0)
151+ if m.card() != 5 {
152+ t.Fatalf("card %d", m.card())
153+ }
154+ m.ToggleOverview()
155+ if f, _ := m.Focused(); f != 5 || w.fullscreen != 0 {
156+ t.Fatalf("focused %d, fullscreen %d", f, w.fullscreen)
157+ }
158+}
159+
160+// A fixed fullscreen tile among several columns is the front card, the
161+// other columns behind it, as for a maximized column.
162+func TestOverviewCoverTileAmongColumns(t *testing.T) {
163+ m := newMonitor("", "")
164+ m.SetOutput(300, 200)
165+ m.SetOverflow(OverflowFixed)
166+ m.AddWindow(1)
167+ m.AddWindow(2)
168+ m.AddWindow(3)
169+ m.Current().FocusID(2)
170+ m.ToggleFullscreen()
171+ m.ToggleOverview()
172+ ps := m.Layout()
173+ uniquePlacements(t, ps)
174+ if g := previewOf(t, ps, 2); g.Hidden || g.Peek || !g.Fullscreen {
175+ t.Fatalf("game %+v", g)
176+ }
177+ for _, id := range []WindowID{1, 3} {
178+ if p := previewOf(t, ps, id); p.Hidden || !p.Peek {
179+ t.Fatalf("%d: %+v", id, p)
180+ }
181+ }
182+ m.OverviewPick(3)
183+ if f, _ := m.Focused(); f != 3 || m.Current().fullscreen != 0 {
184+ t.Fatalf("focused %d, fullscreen %d", f, m.Current().fullscreen)
185+ }
186+}
@@ -0,0 +1,186 @@
1+package core
2+
3+import "testing"
4+
5+// coverMonitor has, on a 300x200 output, tiles 1 and 2 on one workspace,
6+// 2 fullscreen. With float, 2 is a floating game over tile 1 instead.
7+func coverMonitor(o Overflow, float bool) *Monitor {
8+ m := newMonitor("", "")
9+ m.SetOutput(300, 200)
10+ m.SetOverflow(o)
11+ m.AddWindow(1)
12+ if float {
13+ m.AddFloating(2, 300, 200)
14+ m.SetFullscreen(2, true)
15+ } else {
16+ m.AddWindow(2)
17+ m.ToggleFullscreen()
18+ }
19+ return m
20+}
21+
22+// pinnedCases are the setups where the fullscreen window pins its row.
23+var pinnedCases = []struct {
24+ name string
25+ overflow Overflow
26+ float bool
27+}{
28+ {"fixed tile", OverflowFixed, false},
29+ {"fixed float", OverflowFixed, true},
30+ {"scroll float", OverflowScroll, true},
31+}
32+
33+// uniquePlacements fails when a window is placed twice.
34+func uniquePlacements(t *testing.T, ps []Placement) {
35+ t.Helper()
36+ seen := map[WindowID]bool{}
37+ for _, p := range ps {
38+ if seen[p.ID] {
39+ t.Fatalf("window %d placed twice: %+v", p.ID, ps)
40+ }
41+ seen[p.ID] = true
42+ }
43+}
44+
45+// The overview over a pinned fullscreen window shows it in front, still
46+// fullscreen and selected, and the window it hides behind it.
47+func TestOverviewCoverShowsWorkspace(t *testing.T) {
48+ for _, tc := range pinnedCases {
49+ t.Run(tc.name, func(t *testing.T) {
50+ m := coverMonitor(tc.overflow, tc.float)
51+ if !m.Current().pinned() {
52+ t.Fatal("not pinned")
53+ }
54+ m.ToggleOverview()
55+ ps := m.Layout()
56+ uniquePlacements(t, ps)
57+ game, behind := previewOf(t, ps, 2), previewOf(t, ps, 1)
58+ if game.Hidden || !game.Focused || game.Peek || !game.Fullscreen {
59+ t.Fatalf("game %+v", game)
60+ }
61+ if behind.Hidden || !behind.Peek || behind.Focused {
62+ t.Fatalf("behind %+v", behind)
63+ }
64+ })
65+ }
66+}
67+
68+// Return or a click on the fullscreen window keeps it fullscreen.
69+func TestOverviewCoverConfirmKeeps(t *testing.T) {
70+ for _, tc := range pinnedCases {
71+ t.Run(tc.name, func(t *testing.T) {
72+ for _, pick := range []bool{false, true} {
73+ m := coverMonitor(tc.overflow, tc.float)
74+ m.ToggleOverview()
75+ if pick {
76+ m.OverviewPick(2)
77+ } else {
78+ m.ToggleOverview()
79+ }
80+ if f, _ := m.Focused(); f != 2 || m.ov.open || m.Current().fullscreen != 2 || !previewOf(t, m.Layout(), 2).Fullscreen {
81+ t.Fatalf("pick %v: focused %d, fullscreen %d", pick, f, m.Current().fullscreen)
82+ }
83+ }
84+ })
85+ }
86+}
87+
88+// Choosing the window behind leaves fullscreen and focuses it; the left
89+// window shows again, a float behind the tiles.
90+func TestOverviewCoverPickOther(t *testing.T) {
91+ for _, tc := range pinnedCases {
92+ t.Run(tc.name, func(t *testing.T) {
93+ for _, pick := range []bool{false, true} {
94+ m := coverMonitor(tc.overflow, tc.float)
95+ m.ToggleOverview()
96+ if pick {
97+ m.OverviewPick(1)
98+ } else {
99+ m.OverviewMove(-1, 0)
100+ m.ToggleOverview()
101+ }
102+ w := m.Current()
103+ if f, _ := m.Focused(); f != 1 || m.ov.open || w.fullscreen != 0 {
104+ t.Fatalf("pick %v: focused %d, fullscreen %d", pick, f, w.fullscreen)
105+ }
106+ if p := placement(w, 2); p.Fullscreen || (p.Hidden && !tc.float) {
107+ t.Fatalf("pick %v: left window %+v", pick, p)
108+ }
109+ if i := w.floatIndex(2); tc.float && (i < 0 || !w.Floats[i].below) {
110+ t.Fatalf("pick %v: float %+v", pick, w.Floats)
111+ }
112+ }
113+ })
114+ }
115+}
116+
117+// Escape returns to the fullscreen window, still fullscreen.
118+func TestOverviewCoverCancel(t *testing.T) {
119+ for _, tc := range pinnedCases {
120+ t.Run(tc.name, func(t *testing.T) {
121+ m := coverMonitor(tc.overflow, tc.float)
122+ m.ToggleOverview()
123+ m.OverviewMove(-1, 0)
124+ m.CancelOverview()
125+ if f, _ := m.Focused(); f != 2 || m.Current().fullscreen != 2 {
126+ t.Fatalf("focused %d, fullscreen %d", f, m.Current().fullscreen)
127+ }
128+ })
129+ }
130+}
131+
132+// A stash card is reachable under a fullscreen tile; choosing it leaves
133+// fullscreen.
134+func TestOverviewCoverPickStashCard(t *testing.T) {
135+ m := newMonitor("", "")
136+ m.SetOutput(300, 200)
137+ m.SetOverflow(OverflowFixed)
138+ w := m.Current()
139+ w.AddWindow(1)
140+ w.AddWindow(5)
141+ w.FocusID(5)
142+ w.ToggleWindowStash()
143+ w.FocusID(1)
144+ m.ToggleFullscreen()
145+ if !w.pinned() {
146+ t.Fatal("not pinned")
147+ }
148+ m.ToggleOverview()
149+ uniquePlacements(t, m.Layout())
150+ m.OverviewMove(-1, 0)
151+ if m.card() != 5 {
152+ t.Fatalf("card %d", m.card())
153+ }
154+ m.ToggleOverview()
155+ if f, _ := m.Focused(); f != 5 || w.fullscreen != 0 {
156+ t.Fatalf("focused %d, fullscreen %d", f, w.fullscreen)
157+ }
158+}
159+
160+// A fixed fullscreen tile among several columns is the front card, the
161+// other columns behind it, as for a maximized column.
162+func TestOverviewCoverTileAmongColumns(t *testing.T) {
163+ m := newMonitor("", "")
164+ m.SetOutput(300, 200)
165+ m.SetOverflow(OverflowFixed)
166+ m.AddWindow(1)
167+ m.AddWindow(2)
168+ m.AddWindow(3)
169+ m.Current().FocusID(2)
170+ m.ToggleFullscreen()
171+ m.ToggleOverview()
172+ ps := m.Layout()
173+ uniquePlacements(t, ps)
174+ if g := previewOf(t, ps, 2); g.Hidden || g.Peek || !g.Fullscreen {
175+ t.Fatalf("game %+v", g)
176+ }
177+ for _, id := range []WindowID{1, 3} {
178+ if p := previewOf(t, ps, id); p.Hidden || !p.Peek {
179+ t.Fatalf("%d: %+v", id, p)
180+ }
181+ }
182+ m.OverviewPick(3)
183+ if f, _ := m.Focused(); f != 3 || m.Current().fullscreen != 0 {
184+ t.Fatalf("focused %d, fullscreen %d", f, m.Current().fullscreen)
185+ }
186+}
M internal/core/overview_stack.go +35 -6
@@ -18,8 +18,17 @@ type stackItem struct {
1818 id WindowID
1919 }
2020
21+// overviewMaximized reports a fixed row whose focused column hides the
22+// others: maximized, or holding the covering fullscreen tile. Its card
23+// shows in front of the hidden columns' card.
2124 func (w *Workspace) overviewMaximized() bool {
22- return w.Overflow == OverflowFixed && len(w.Columns) > 1 && w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth && !w.pinned()
25+ if w.Overflow != OverflowFixed || len(w.Columns) < 2 || w.Focus < 0 || w.Focus >= len(w.Columns) {
26+ return false
27+ }
28+ if c := w.cover(); c != 0 && !w.isFloat(c) {
29+ return w.columnOf(c) == w.Focus
30+ }
31+ return w.Columns[w.Focus].FullWidth
2332 }
2433
2534 func (w *Workspace) columnOf(id WindowID) int {
@@ -32,21 +41,36 @@ func (w *Workspace) columnOf(id WindowID) int {
3241 }
3342
3443 // stack lists what is on screen followed by what it hides, top to bottom.
44+// A pinned fullscreen window is the front card, a float on its own, a
45+// fixed tile with its column, so every window it hides stays reachable; a
46+// stashed one is its card in the pile.
3547 func (w *Workspace) stack() []stackItem {
48+ var items []stackItem
49+ c, columns := WindowID(0), len(w.Columns) > 0
3650 if w.pinned() {
37- return nil
51+ c = w.cover()
52+ }
53+ switch {
54+ case c == 0, w.stashIndex(c) >= 0:
55+ case w.floatIndex(c) >= 0:
56+ items = append(items, stackItem{stackFloat, c})
57+ case w.overviewMaximized():
58+ items = append(items, stackItem{stackColumn, c})
59+ default:
60+ // Its column is the only one.
61+ items = append(items, stackItem{kind: stackColumns})
62+ columns = false
3863 }
39- var items []stackItem
4064 for i := len(w.Floats) - 1; i >= 0; i-- {
4165 f := w.Floats[i]
4266 if !f.below && w.coversFloat(f) {
4367 items = append(items, stackItem{stackFloat, f.ID})
4468 }
4569 }
46- if w.overviewMaximized() {
70+ if (c == 0 || w.isFloat(c)) && w.overviewMaximized() {
4771 items = append(items, stackItem{stackColumn, w.Columns[w.Focus].Windows[0]})
4872 }
49- if len(w.Columns) > 0 {
73+ if columns {
5074 items = append(items, stackItem{kind: stackColumns})
5175 }
5276 for i := len(w.Floats) - 1; i >= 0; i-- {
@@ -242,6 +266,11 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool
242266 return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, sel)
243267 }
244268 g := w.gap()
269+ if item.kind == stackFloat && item.id == w.cover() {
270+ // A fullscreen float shows at the size of a fullscreen column.
271+ r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
272+ return w.previewRowTiles(y, dim, lit, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r)
273+ }
245274 if item.kind == stackFloat {
246275 f := w.Floats[w.floatIndex(item.id)]
247276 r := w.floatRect(f)
@@ -259,7 +288,7 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool
259288 r.Y = (w.Usable.H - r.H) / 2
260289 tiles := make([]Placement, 0, len(c.Windows))
261290 for j, t := range rowRects(r, c, g) {
262- tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus})
291+ tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus, Fullscreen: c.Windows[j] == w.cover()})
263292 }
264293 return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r)
265294 }
@@ -18,8 +18,17 @@ type stackItem struct {
18 id WindowID18 id WindowID
19 }19 }
20 20
21+// overviewMaximized reports a fixed row whose focused column hides the
22+// others: maximized, or holding the covering fullscreen tile. Its card
23+// shows in front of the hidden columns' card.
21 func (w *Workspace) overviewMaximized() bool {24 func (w *Workspace) overviewMaximized() bool {
22- return w.Overflow == OverflowFixed && len(w.Columns) > 1 && w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth && !w.pinned()25+ if w.Overflow != OverflowFixed || len(w.Columns) < 2 || w.Focus < 0 || w.Focus >= len(w.Columns) {
26+ return false
27+ }
28+ if c := w.cover(); c != 0 && !w.isFloat(c) {
29+ return w.columnOf(c) == w.Focus
30+ }
31+ return w.Columns[w.Focus].FullWidth
23 }32 }
24 33
25 func (w *Workspace) columnOf(id WindowID) int {34 func (w *Workspace) columnOf(id WindowID) int {
@@ -32,21 +41,36 @@ func (w *Workspace) columnOf(id WindowID) int {
32 }41 }
33 42
34 // stack lists what is on screen followed by what it hides, top to bottom.43 // stack lists what is on screen followed by what it hides, top to bottom.
44+// A pinned fullscreen window is the front card, a float on its own, a
45+// fixed tile with its column, so every window it hides stays reachable; a
46+// stashed one is its card in the pile.
35 func (w *Workspace) stack() []stackItem {47 func (w *Workspace) stack() []stackItem {
48+ var items []stackItem
49+ c, columns := WindowID(0), len(w.Columns) > 0
36 if w.pinned() {50 if w.pinned() {
37- return nil51+ c = w.cover()
52+ }
53+ switch {
54+ case c == 0, w.stashIndex(c) >= 0:
55+ case w.floatIndex(c) >= 0:
56+ items = append(items, stackItem{stackFloat, c})
57+ case w.overviewMaximized():
58+ items = append(items, stackItem{stackColumn, c})
59+ default:
60+ // Its column is the only one.
61+ items = append(items, stackItem{kind: stackColumns})
62+ columns = false
38 }63 }
39- var items []stackItem
40 for i := len(w.Floats) - 1; i >= 0; i-- {64 for i := len(w.Floats) - 1; i >= 0; i-- {
41 f := w.Floats[i]65 f := w.Floats[i]
42 if !f.below && w.coversFloat(f) {66 if !f.below && w.coversFloat(f) {
43 items = append(items, stackItem{stackFloat, f.ID})67 items = append(items, stackItem{stackFloat, f.ID})
44 }68 }
45 }69 }
46- if w.overviewMaximized() {70+ if (c == 0 || w.isFloat(c)) && w.overviewMaximized() {
47 items = append(items, stackItem{stackColumn, w.Columns[w.Focus].Windows[0]})71 items = append(items, stackItem{stackColumn, w.Columns[w.Focus].Windows[0]})
48 }72 }
49- if len(w.Columns) > 0 {73+ if columns {
50 items = append(items, stackItem{kind: stackColumns})74 items = append(items, stackItem{kind: stackColumns})
51 }75 }
52 for i := len(w.Floats) - 1; i >= 0; i-- {76 for i := len(w.Floats) - 1; i >= 0; i-- {
@@ -242,6 +266,11 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool
242 return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, sel)266 return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, sel)
243 }267 }
244 g := w.gap()268 g := w.gap()
269+ if item.kind == stackFloat && item.id == w.cover() {
270+ // A fullscreen float shows at the size of a fullscreen column.
271+ r := Rect{X: g, Y: g, W: max(w.Usable.W-2*g, 0), H: max(w.Usable.H-2*g, 0)}
272+ return w.previewRowTiles(y, dim, lit, []Placement{{ID: item.id, Rect: r, Focused: true, Fullscreen: true}}, w.Usable.W, r)
273+ }
245 if item.kind == stackFloat {274 if item.kind == stackFloat {
246 f := w.Floats[w.floatIndex(item.id)]275 f := w.Floats[w.floatIndex(item.id)]
247 r := w.floatRect(f)276 r := w.floatRect(f)
@@ -259,7 +288,7 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool
259 r.Y = (w.Usable.H - r.H) / 2288 r.Y = (w.Usable.H - r.H) / 2
260 tiles := make([]Placement, 0, len(c.Windows))289 tiles := make([]Placement, 0, len(c.Windows))
261 for j, t := range rowRects(r, c, g) {290 for j, t := range rowRects(r, c, g) {
262- tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus})291+ tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus, Fullscreen: c.Windows[j] == w.cover()})
263 }292 }
264 return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r)293 return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r)
265 }294 }
M internal/core/overview_test.go +32 -20
@@ -208,26 +208,35 @@ func TestOverviewOpensOverFullscreenFloat(t *testing.T) {
208208 }
209209 }
210210
211-// A tiled fullscreen window (fixed overflow) is its row's only preview
212-// too: the selection cannot leave it for a hidden column, and Return keeps
213-// it fullscreen.
211+// A tiled fullscreen window (fixed overflow) is its row's front card:
212+// Return keeps it fullscreen; the column it hides is behind, and picking
213+// it leaves fullscreen.
214214 func TestOverviewOpensOverFullscreenTile(t *testing.T) {
215- m := newMonitor("", "")
216- m.SetOutput(300, 200)
217- m.SetOverflow(OverflowFixed)
218- m.AddWindow(1)
219- m.AddWindow(2)
220- m.ToggleFullscreen()
221- m.ToggleOverview()
215+ open := func() *Monitor {
216+ m := newMonitor("", "")
217+ m.SetOutput(300, 200)
218+ m.SetOverflow(OverflowFixed)
219+ m.AddWindow(1)
220+ m.AddWindow(2)
221+ m.ToggleFullscreen()
222+ m.ToggleOverview()
223+ return m
224+ }
225+ m := open()
222226 game := previewOf(t, m.Layout(), 2)
223227 if !m.ov.open || !game.Focused || game.Peek || !game.Fullscreen {
224228 t.Fatalf("game %+v", game)
225229 }
226- m.OverviewMove(-1, 0)
227230 m.ToggleOverview()
228231 if f, _ := m.Focused(); f != 2 || m.Active != 0 || !previewOf(t, m.Layout(), 2).Fullscreen {
229232 t.Fatalf("confirm: %d on %d", f, m.Active)
230233 }
234+ m = open()
235+ m.OverviewMove(-1, 0)
236+ m.ToggleOverview()
237+ if f, _ := m.Focused(); f != 1 || m.Current().fullscreen != 0 {
238+ t.Fatalf("behind: focused %d, fullscreen %d", f, m.Current().fullscreen)
239+ }
231240 }
232241
233242 // Under scroll overflow a fullscreen column does not pin the row: another
@@ -250,9 +259,9 @@ func TestOverviewOverScrollFullscreenPicksColumn(t *testing.T) {
250259 }
251260 }
252261
253-// Only the game shows on its row: its dialogs and the columns under it
254-// stay hidden, as on screen.
255-func TestOverviewFullscreenFloatHidesOthers(t *testing.T) {
262+// A neighbor row with a fullscreen game shows it in front, the columns it
263+// hides behind; a dialog under it stays hidden, as for any small float.
264+func TestOverviewFullscreenFloatRow(t *testing.T) {
256265 m := gameMonitor(OverflowScroll)
257266 m.Focus(1)
258267 m.AddWindow(5)
@@ -262,14 +271,17 @@ func TestOverviewFullscreenFloatHidesOthers(t *testing.T) {
262271 m.Focus(0)
263272 m.ToggleOverview()
264273 ps := m.Layout()
265- if previewOf(t, ps, 2).Hidden {
266- t.Fatal("game hidden")
267- }
268- for _, id := range []WindowID{3, 4, 5} {
269- if p := previewOf(t, ps, id); !p.Hidden {
270- t.Fatalf("%d shown: %+v", id, p)
274+ for _, id := range []WindowID{2, 3, 5} {
275+ if p := previewOf(t, ps, id); p.Hidden || !p.Peek {
276+ t.Fatalf("%d: %+v", id, p)
271277 }
272278 }
279+ if p := previewOf(t, ps, 4); !p.Hidden {
280+ t.Fatalf("dialog shown: %+v", p)
281+ }
282+ if g := previewOf(t, ps, 2); !g.Fullscreen {
283+ t.Fatalf("game %+v", g)
284+ }
273285 }
274286
275287 // pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1
@@ -208,26 +208,35 @@ func TestOverviewOpensOverFullscreenFloat(t *testing.T) {
208 }208 }
209 }209 }
210 210
211-// A tiled fullscreen window (fixed overflow) is its row's only preview211+// A tiled fullscreen window (fixed overflow) is its row's front card:
212-// too: the selection cannot leave it for a hidden column, and Return keeps212+// Return keeps it fullscreen; the column it hides is behind, and picking
213-// it fullscreen.213+// it leaves fullscreen.
214 func TestOverviewOpensOverFullscreenTile(t *testing.T) {214 func TestOverviewOpensOverFullscreenTile(t *testing.T) {
215- m := newMonitor("", "")215+ open := func() *Monitor {
216- m.SetOutput(300, 200)216+ m := newMonitor("", "")
217- m.SetOverflow(OverflowFixed)217+ m.SetOutput(300, 200)
218- m.AddWindow(1)218+ m.SetOverflow(OverflowFixed)
219- m.AddWindow(2)219+ m.AddWindow(1)
220- m.ToggleFullscreen()220+ m.AddWindow(2)
221- m.ToggleOverview()221+ m.ToggleFullscreen()
222+ m.ToggleOverview()
223+ return m
224+ }
225+ m := open()
222 game := previewOf(t, m.Layout(), 2)226 game := previewOf(t, m.Layout(), 2)
223 if !m.ov.open || !game.Focused || game.Peek || !game.Fullscreen {227 if !m.ov.open || !game.Focused || game.Peek || !game.Fullscreen {
224 t.Fatalf("game %+v", game)228 t.Fatalf("game %+v", game)
225 }229 }
226- m.OverviewMove(-1, 0)
227 m.ToggleOverview()230 m.ToggleOverview()
228 if f, _ := m.Focused(); f != 2 || m.Active != 0 || !previewOf(t, m.Layout(), 2).Fullscreen {231 if f, _ := m.Focused(); f != 2 || m.Active != 0 || !previewOf(t, m.Layout(), 2).Fullscreen {
229 t.Fatalf("confirm: %d on %d", f, m.Active)232 t.Fatalf("confirm: %d on %d", f, m.Active)
230 }233 }
234+ m = open()
235+ m.OverviewMove(-1, 0)
236+ m.ToggleOverview()
237+ if f, _ := m.Focused(); f != 1 || m.Current().fullscreen != 0 {
238+ t.Fatalf("behind: focused %d, fullscreen %d", f, m.Current().fullscreen)
239+ }
231 }240 }
232 241
233 // Under scroll overflow a fullscreen column does not pin the row: another242 // Under scroll overflow a fullscreen column does not pin the row: another
@@ -250,9 +259,9 @@ func TestOverviewOverScrollFullscreenPicksColumn(t *testing.T) {
250 }259 }
251 }260 }
252 261
253-// Only the game shows on its row: its dialogs and the columns under it262+// A neighbor row with a fullscreen game shows it in front, the columns it
254-// stay hidden, as on screen.263+// hides behind; a dialog under it stays hidden, as for any small float.
255-func TestOverviewFullscreenFloatHidesOthers(t *testing.T) {264+func TestOverviewFullscreenFloatRow(t *testing.T) {
256 m := gameMonitor(OverflowScroll)265 m := gameMonitor(OverflowScroll)
257 m.Focus(1)266 m.Focus(1)
258 m.AddWindow(5)267 m.AddWindow(5)
@@ -262,14 +271,17 @@ func TestOverviewFullscreenFloatHidesOthers(t *testing.T) {
262 m.Focus(0)271 m.Focus(0)
263 m.ToggleOverview()272 m.ToggleOverview()
264 ps := m.Layout()273 ps := m.Layout()
265- if previewOf(t, ps, 2).Hidden {274+ for _, id := range []WindowID{2, 3, 5} {
266- t.Fatal("game hidden")275+ if p := previewOf(t, ps, id); p.Hidden || !p.Peek {
267- }276+ t.Fatalf("%d: %+v", id, p)
268- for _, id := range []WindowID{3, 4, 5} {
269- if p := previewOf(t, ps, id); !p.Hidden {
270- t.Fatalf("%d shown: %+v", id, p)
271 }277 }
272 }278 }
279+ if p := previewOf(t, ps, 4); !p.Hidden {
280+ t.Fatalf("dialog shown: %+v", p)
281+ }
282+ if g := previewOf(t, ps, 2); !g.Fullscreen {
283+ t.Fatalf("game %+v", g)
284+ }
273 }285 }
274 286
275 // pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1287 // pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1

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 26

Tags 10

↑↓ moveenter openesc close