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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 1078 | 1078 | } | |
| 1079 | 1079 | ||
| 1080 | 1080 | // 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. | |
| 1083 | 1084 | func (w *Workspace) pinned() bool { | |
| 1084 | 1085 | c := w.cover() | |
| 1085 | 1086 | 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 other | 1080 | // pinned reports whether the covering fullscreen window hides every other | ||
| 1081 | - | // window: focus moves leave it (leaveCover). Only in scroll overflow does | 1081 | + | // 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 { | |||
| 44 | 44 | // The overview scales workspace rows without resizing clients. Covering | |
| 45 | 45 | // floats and the columns form a linear stack of cards; small floats stay | |
| 46 | 46 | // 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 | 50 | // A workspace's stash shows as a pile of cards pinned to the left edge of | |
| 50 | 51 | // its row: the selected stashed window in front at overviewCardZoom, up to | |
| @@ -108,9 +109,8 @@ func (m *Monitor) selectRow() { | |||
| 108 | 109 | m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0 | |
| 109 | 110 | m.ov.selected, m.ov.selectedAt = 0, -1 | |
| 110 | 111 | 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() | |
| 114 | 114 | if w.floatFocus && len(w.Columns) > 0 && (len(w.stack()) < 2 || m.stackFront(w).kind == stackColumns) { | |
| 115 | 115 | w.floatFocus = false | |
| 116 | 116 | } | |
| @@ -150,9 +150,6 @@ func (m *Monitor) overviewTarget() WindowID { | |||
| 150 | 150 | return id | |
| 151 | 151 | } | |
| 152 | 152 | w := m.Current() | |
| 153 | - | if w.pinned() { | |
| 154 | - | return w.cover() | |
| 155 | - | } | |
| 156 | 153 | item := m.stackFront(w) | |
| 157 | 154 | switch item.kind { | |
| 158 | 155 | case stackFloat: | |
| @@ -222,17 +219,22 @@ func (m *Monitor) overviewMoveTo(a Action) { | |||
| 222 | 219 | ||
| 223 | 220 | // closeOverview closes the overview on the selection: a card shows the | |
| 224 | 221 | // 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 | 224 | func (m *Monitor) closeOverview() { | |
| 227 | 225 | w := m.Current() | |
| 226 | + | front, target := m.stackFront(w), m.overviewTarget() | |
| 227 | + | if w.pinned() && target != w.cover() { | |
| 228 | + | w.endFullscreen() | |
| 229 | + | } | |
| 228 | 230 | if i := m.cardAt(w); i >= 0 { | |
| 229 | 231 | if w.hiddenFullscreen != w.Stash[i].ID { | |
| 230 | 232 | w.hiddenFullscreen = 0 | |
| 231 | 233 | } | |
| 232 | 234 | w.stashAt = i | |
| 233 | 235 | 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) | |
| 236 | 238 | } | |
| 237 | 239 | for r, s := range m.ov.rows { | |
| 238 | 240 | if r != w && m.has(r) { | |
| @@ -373,9 +375,6 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||
| 373 | 375 | m.showOverview(rows[i]) | |
| 374 | 376 | return | |
| 375 | 377 | } | |
| 376 | - | if w.pinned() { | |
| 377 | - | return | |
| 378 | - | } | |
| 379 | 378 | if at := m.cardAt(w); at >= 0 { | |
| 380 | 379 | switch { | |
| 381 | 380 | case at+dx >= 0 && at+dx < len(w.Stash): | |
| @@ -447,7 +446,7 @@ func (m *Monitor) OverviewPick(id WindowID) { | |||
| 447 | 446 | } | |
| 448 | 447 | if w.floatIndex(id) >= 0 && w.stashIndex(id) < 0 { | |
| 449 | 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 | 450 | return | |
| 452 | 451 | } | |
| 453 | 452 | } | |
| @@ -455,8 +454,6 @@ func (m *Monitor) OverviewPick(id WindowID) { | |||
| 455 | 454 | m.ov.card, m.ov.cardOf = 0, nil | |
| 456 | 455 | if i := w.stashIndex(id); i >= 0 { | |
| 457 | 456 | m.selectCard(w, i) | |
| 458 | - | } else if w.pinned() { | |
| 459 | - | w.FocusID(id) | |
| 460 | 457 | } else if item, ok := w.itemOf(id); ok { | |
| 461 | 458 | m.setFront(w, item) | |
| 462 | 459 | if i := w.columnOf(id); i >= 0 { | |
| @@ -522,16 +519,6 @@ func (m *Monitor) overviewLayout() []Placement { | |||
| 522 | 519 | var result []Placement | |
| 523 | 520 | for w := range m.all() { | |
| 524 | 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 | 522 | if shown { | |
| 536 | 523 | at := m.cardAt(w) | |
| 537 | 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 | 609 | // pileWidth is the room the stash pile of w takes on the left of its row, | |
| 623 | 610 | // its margin included; 0 without a stash. | |
| 624 | 611 | func (w *Workspace) pileWidth() int { | |
| 625 | - | if len(w.Stash) == 0 || w.pinned() { | |
| 612 | + | if len(w.Stash) == 0 { | |
| 626 | 613 | return 0 | |
| 627 | 614 | } | |
| 628 | 615 | card := int(math.Round(float64(w.stashRect().W) * overviewCardZoom)) | |
| @@ -713,15 +700,19 @@ func (w *Workspace) rowHeight() int { | |||
| 713 | 700 | // corner. Fixed overflow uses its on-screen geometry; scroll overflow uses | |
| 714 | 701 | // an unscrolled row. span is the row's width; sel is the focused column. | |
| 715 | 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 | 714 | g := w.gap() | |
| 717 | 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 | 716 | var rects []Rect | |
| 726 | 717 | if w.Overflow == OverflowFixed { | |
| 727 | 718 | rects = w.columnRects() | |
| @@ -44,7 +44,8 @@ type rowSnapshot struct { | |||||
| 44 | // The overview scales workspace rows without resizing clients. Covering | 44 | // The overview scales workspace rows without resizing clients. Covering | ||
| 45 | // floats and the columns form a linear stack of cards; small floats stay | 45 | // floats and the columns form a linear stack of cards; small floats stay | ||
| 46 | // hidden. Up/down traverse the stack before changing workspace. A pinned | 46 | // 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 of | 50 | // 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 to | 51 | // 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, 0 | 109 | m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0 | ||
| 109 | m.ov.selected, m.ov.selectedAt = 0, -1 | 110 | 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 | - | return | 113 | + | 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 = false | 115 | w.floatFocus = false | ||
| 116 | } | 116 | } | ||
| @@ -150,9 +150,6 @@ func (m *Monitor) overviewTarget() WindowID { | |||||
| 150 | return id | 150 | 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 the | 220 | // closeOverview closes the overview on the selection: a card shows the | ||
| 224 | // stash on it. A fullscreen request another stashed window made while | 221 | // 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 = 0 | 232 | w.hiddenFullscreen = 0 | ||
| 231 | } | 233 | } | ||
| 232 | w.stashAt = i | 234 | 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 | return | 376 | 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 | return | 450 | return | ||
| 452 | } | 451 | } | ||
| 453 | } | 452 | } | ||
| @@ -455,8 +454,6 @@ func (m *Monitor) OverviewPick(id WindowID) { | |||||
| 455 | m.ov.card, m.ov.cardOf = 0, nil | 454 | 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 []Placement | 519 | 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 0 | 613 | 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 uses | 700 | // 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 []Rect | 716 | 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 { | |||
| 18 | 18 | 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 | 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 | 34 | func (w *Workspace) columnOf(id WindowID) int { | |
| @@ -32,21 +41,36 @@ func (w *Workspace) columnOf(id WindowID) int { | |||
| 32 | 41 | } | |
| 33 | 42 | ||
| 34 | 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 | 47 | func (w *Workspace) stack() []stackItem { | |
| 48 | + | var items []stackItem | |
| 49 | + | c, columns := WindowID(0), len(w.Columns) > 0 | |
| 36 | 50 | 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 | |
| 38 | 63 | } | |
| 39 | - | var items []stackItem | |
| 40 | 64 | for i := len(w.Floats) - 1; i >= 0; i-- { | |
| 41 | 65 | f := w.Floats[i] | |
| 42 | 66 | if !f.below && w.coversFloat(f) { | |
| 43 | 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 | 71 | items = append(items, stackItem{stackColumn, w.Columns[w.Focus].Windows[0]}) | |
| 48 | 72 | } | |
| 49 | - | if len(w.Columns) > 0 { | |
| 73 | + | if columns { | |
| 50 | 74 | items = append(items, stackItem{kind: stackColumns}) | |
| 51 | 75 | } | |
| 52 | 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 | 266 | return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, sel) | |
| 243 | 267 | } | |
| 244 | 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 | 274 | if item.kind == stackFloat { | |
| 246 | 275 | f := w.Floats[w.floatIndex(item.id)] | |
| 247 | 276 | r := w.floatRect(f) | |
| @@ -259,7 +288,7 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool | |||
| 259 | 288 | r.Y = (w.Usable.H - r.H) / 2 | |
| 260 | 289 | tiles := make([]Placement, 0, len(c.Windows)) | |
| 261 | 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 | 293 | return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r) | |
| 265 | 294 | } | |
| @@ -18,8 +18,17 @@ type stackItem struct { | |||||
| 18 | id WindowID | 18 | 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 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 | |||
| 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) / 2 | 288 | 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) { | |||
| 208 | 208 | } | |
| 209 | 209 | } | |
| 210 | 210 | ||
| 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. | |
| 214 | 214 | 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() | |
| 222 | 226 | game := previewOf(t, m.Layout(), 2) | |
| 223 | 227 | if !m.ov.open || !game.Focused || game.Peek || !game.Fullscreen { | |
| 224 | 228 | t.Fatalf("game %+v", game) | |
| 225 | 229 | } | |
| 226 | - | m.OverviewMove(-1, 0) | |
| 227 | 230 | m.ToggleOverview() | |
| 228 | 231 | if f, _ := m.Focused(); f != 2 || m.Active != 0 || !previewOf(t, m.Layout(), 2).Fullscreen { | |
| 229 | 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 | 242 | // 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 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) { | |
| 256 | 265 | m := gameMonitor(OverflowScroll) | |
| 257 | 266 | m.Focus(1) | |
| 258 | 267 | m.AddWindow(5) | |
| @@ -262,14 +271,17 @@ func TestOverviewFullscreenFloatHidesOthers(t *testing.T) { | |||
| 262 | 271 | m.Focus(0) | |
| 263 | 272 | m.ToggleOverview() | |
| 264 | 273 | 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) | |
| 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 | 287 | // 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 preview | 211 | + | // 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 keeps | 212 | + | // 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: another | 242 | // 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 it | 262 | + | // 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 1 | 287 | // pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1 | ||