fix(core): keep the switcher in sync with the workspace on screen
- The cards count as shown only while their workspace is on screen (Monitor.switcherShown, used by previewing and layoutInto). An activation, a taskbar click or an output coming back no longer leaves the screen dimmed and unclickable; the next step or publish drops the stale switcher and the next Cmd+Tab opens fresh. - Ignore switch-column-* during a window drag. - Snapshot before another bind closes the cards so they fly back. - Closing the selected card's window keeps its column's card, standing for the column's new focused window; the card goes with the column. - Committing focuses the tile even when a float or the stash holds the focus, and moves the keyboard to the switcher's output. - security.go: drop the per-screen reset cancelSwitcher already covers. - Tests: no package-level command map, checked runBind errors, F9 bind through keyEvent, doc comment dedup.
- commit
- 17b575a1546e5e722ffada606487178d2b22e72d
- author
- brice · · 2026-10-08 18:10 UTC
- parent
- c947263
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +369 -88
M internal/core/core.go +4 -0
| @@ -598,6 +598,10 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||
| 598 | 598 | if c.security.Protected { | |
| 599 | 599 | return c.publishProtected(ctx) | |
| 600 | 600 | } | |
| 601 | + | if c.switching() && c.switcherStale() { | |
| 602 | + | // Another workspace came on screen under the switcher: it is over. | |
| 603 | + | c.cancelSwitcher() | |
| 604 | + | } | |
| 601 | 605 | for _, sc := range c.screens { | |
| 602 | 606 | // The overview's selection is provisional: it focuses nothing yet. | |
| 603 | 607 | if !sc.mon.ov.open { | |
| @@ -598,6 +598,10 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||||
| 598 | if c.security.Protected { | 598 | if c.security.Protected { | ||
| 599 | return c.publishProtected(ctx) | 599 | return c.publishProtected(ctx) | ||
| 600 | } | 600 | } | ||
| 601 | + | if c.switching() && c.switcherStale() { | |||
| 602 | + | // Another workspace came on screen under the switcher: it is over. | |||
| 603 | + | c.cancelSwitcher() | |||
| 604 | + | } | |||
| 601 | for _, sc := range c.screens { | 605 | for _, sc := range c.screens { | ||
| 602 | // The overview's selection is provisional: it focuses nothing yet. | 606 | // The overview's selection is provisional: it focuses nothing yet. | ||
| 603 | if !sc.mon.ov.open { | 607 | if !sc.mon.ov.open { | ||
M internal/core/input.go +4 -3
| @@ -328,13 +328,14 @@ func (c *Core) runBind(ctx context.Context, action Action) error { | |||
| 328 | 328 | if dir := switchDir(action); dir != 0 { | |
| 329 | 329 | return c.switchBind(ctx, dir) | |
| 330 | 330 | } | |
| 331 | - | if c.switching() { | |
| 332 | - | c.cancelSwitcher() | |
| 333 | - | } | |
| 334 | 331 | before := c.cur().mon.Current() | |
| 335 | 332 | swiped := c.swipedWorkspace() | |
| 336 | 333 | now := c.now() | |
| 334 | + | // Snapshot before the switcher closes: its cards fly back to the layout. | |
| 337 | 335 | shots := c.snapshot(now) | |
| 336 | + | if c.switching() { | |
| 337 | + | c.cancelSwitcher() | |
| 338 | + | } | |
| 338 | 339 | c.keyboard.takeBack() // a bind acts on the windows | |
| 339 | 340 | effect := c.applyAction(action) | |
| 340 | 341 | if effect.Quit { | |
| @@ -328,13 +328,14 @@ func (c *Core) runBind(ctx context.Context, action Action) error { | |||||
| 328 | if dir := switchDir(action); dir != 0 { | 328 | if dir := switchDir(action); dir != 0 { | ||
| 329 | return c.switchBind(ctx, dir) | 329 | return c.switchBind(ctx, dir) | ||
| 330 | } | 330 | } | ||
| 331 | - | if c.switching() { | |||
| 332 | - | c.cancelSwitcher() | |||
| 333 | - | } | |||
| 334 | before := c.cur().mon.Current() | 331 | before := c.cur().mon.Current() | ||
| 335 | swiped := c.swipedWorkspace() | 332 | swiped := c.swipedWorkspace() | ||
| 336 | now := c.now() | 333 | now := c.now() | ||
| 334 | + | // Snapshot before the switcher closes: its cards fly back to the layout. | |||
| 337 | shots := c.snapshot(now) | 335 | shots := c.snapshot(now) | ||
| 336 | + | if c.switching() { | |||
| 337 | + | c.cancelSwitcher() | |||
| 338 | + | } | |||
| 338 | c.keyboard.takeBack() // a bind acts on the windows | 339 | c.keyboard.takeBack() // a bind acts on the windows | ||
| 339 | effect := c.applyAction(action) | 340 | effect := c.applyAction(action) | ||
| 340 | if effect.Quit { | 341 | if effect.Quit { | ||
M internal/core/monitor.go +3 -2
| @@ -329,11 +329,12 @@ func (m *Monitor) AddFloating(id WindowID, width, height int) { | |||
| 329 | 329 | // RemoveWindow drops the window wherever it is; focus stays on the active | |
| 330 | 330 | // workspace. | |
| 331 | 331 | func (m *Monitor) RemoveWindow(id WindowID) { | |
| 332 | + | heir := m.sw.heir(id) | |
| 332 | 333 | if w, _ := m.find(id); w != nil { | |
| 333 | 334 | w.RemoveWindow(id) | |
| 334 | 335 | m.normalize() | |
| 335 | 336 | } | |
| 336 | - | m.sw.remove(id) | |
| 337 | + | m.sw.remove(id, heir) | |
| 337 | 338 | } | |
| 338 | 339 | ||
| 339 | 340 | // SetFullscreen applies a client request, in place. A client request never | |
| @@ -426,7 +427,7 @@ func (m *Monitor) Layout() []Placement { return m.layoutInto(nil) } | |||
| 426 | 427 | // so a reused dst's previous result is overwritten and only a caller done | |
| 427 | 428 | // with it may pass it. It is never the monitor's own scratch. | |
| 428 | 429 | func (m *Monitor) layoutInto(dst []Placement) []Placement { | |
| 429 | - | if m.sw.shown && m.sw.ws == m.Current() { | |
| 430 | + | if m.switcherShown() { | |
| 430 | 431 | return m.switcherLayout(dst[:0]) | |
| 431 | 432 | } | |
| 432 | 433 | if m.ov.open { | |
| @@ -329,11 +329,12 @@ func (m *Monitor) AddFloating(id WindowID, width, height int) { | |||||
| 329 | // RemoveWindow drops the window wherever it is; focus stays on the active | 329 | // RemoveWindow drops the window wherever it is; focus stays on the active | ||
| 330 | // workspace. | 330 | // workspace. | ||
| 331 | func (m *Monitor) RemoveWindow(id WindowID) { | 331 | func (m *Monitor) RemoveWindow(id WindowID) { | ||
| 332 | + | heir := m.sw.heir(id) | |||
| 332 | if w, _ := m.find(id); w != nil { | 333 | if w, _ := m.find(id); w != nil { | ||
| 333 | w.RemoveWindow(id) | 334 | w.RemoveWindow(id) | ||
| 334 | m.normalize() | 335 | m.normalize() | ||
| 335 | } | 336 | } | ||
| 336 | - | m.sw.remove(id) | 337 | + | m.sw.remove(id, heir) |
| 337 | } | 338 | } | ||
| 338 | 339 | ||||
| 339 | // SetFullscreen applies a client request, in place. A client request never | 340 | // SetFullscreen applies a client request, in place. A client request never | ||
| @@ -426,7 +427,7 @@ func (m *Monitor) Layout() []Placement { return m.layoutInto(nil) } | |||||
| 426 | // so a reused dst's previous result is overwritten and only a caller done | 427 | // so a reused dst's previous result is overwritten and only a caller done | ||
| 427 | // with it may pass it. It is never the monitor's own scratch. | 428 | // with it may pass it. It is never the monitor's own scratch. | ||
| 428 | func (m *Monitor) layoutInto(dst []Placement) []Placement { | 429 | func (m *Monitor) layoutInto(dst []Placement) []Placement { | ||
| 429 | - | if m.sw.shown && m.sw.ws == m.Current() { | 430 | + | if m.switcherShown() { |
| 430 | return m.switcherLayout(dst[:0]) | 431 | return m.switcherLayout(dst[:0]) | ||
| 431 | } | 432 | } | ||
| 432 | if m.ov.open { | 433 | if m.ov.open { | ||
M internal/core/scene.go +1 -1
| @@ -56,7 +56,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 56 | 56 | // Frame the selection and separate numbered and named row groups. | |
| 57 | 57 | _, scene.Separators = sc.mon.overviewRows() | |
| 58 | 58 | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | |
| 59 | - | } else if sc.mon.sw.shown { | |
| 59 | + | } else if sc.mon.switcherShown() { | |
| 60 | 60 | // The switcher's cards: only the selection is framed. | |
| 61 | 61 | scene.Separators = overviewOutline(layout, max(c.cfg.Border.Width, 2)) | |
| 62 | 62 | } else { | |
| @@ -56,7 +56,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 56 | // Frame the selection and separate numbered and named row groups. | 56 | // Frame the selection and separate numbered and named row groups. | ||
| 57 | _, scene.Separators = sc.mon.overviewRows() | 57 | _, scene.Separators = sc.mon.overviewRows() | ||
| 58 | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | 58 | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | ||
| 59 | - | } else if sc.mon.sw.shown { | 59 | + | } else if sc.mon.switcherShown() { |
| 60 | // The switcher's cards: only the selection is framed. | 60 | // The switcher's cards: only the selection is framed. | ||
| 61 | scene.Separators = overviewOutline(layout, max(c.cfg.Border.Width, 2)) | 61 | scene.Separators = overviewOutline(layout, max(c.cfg.Border.Width, 2)) | ||
| 62 | } else { | 62 | } else { | ||
M internal/core/security.go +0 -3
| @@ -47,9 +47,6 @@ func (c *Core) syncSecurity() bool { | |||
| 47 | 47 | c.stopAnimations() | |
| 48 | 48 | c.stopPulse() | |
| 49 | 49 | c.cancelSwitcher() | |
| 50 | - | for _, sc := range c.screens { | |
| 51 | - | sc.mon.sw = switcherState{} | |
| 52 | - | } | |
| 53 | 50 | if state.Protected { | |
| 54 | 51 | c.dropCaptureSessions() | |
| 55 | 52 | } | |
| @@ -47,9 +47,6 @@ func (c *Core) syncSecurity() bool { | |||||
| 47 | c.stopAnimations() | 47 | c.stopAnimations() | ||
| 48 | c.stopPulse() | 48 | c.stopPulse() | ||
| 49 | c.cancelSwitcher() | 49 | c.cancelSwitcher() | ||
| 50 | - | for _, sc := range c.screens { | |||
| 51 | - | sc.mon.sw = switcherState{} | |||
| 52 | - | } | |||
| 53 | if state.Protected { | 50 | if state.Protected { | ||
| 54 | c.dropCaptureSessions() | 51 | c.dropCaptureSessions() | ||
| 55 | } | 52 | } | ||
M internal/core/switcher.go +75 -21
| @@ -31,9 +31,15 @@ type switcherState struct { | |||
| 31 | 31 | sizes map[WindowID]Rect | |
| 32 | 32 | } | |
| 33 | 33 | ||
| 34 | + | // switcherShown reports whether the switcher's cards are drawn. They belong | |
| 35 | + | // to the workspace that was on screen when they showed: if another one is on | |
| 36 | + | // screen now (an activation, the taskbar, an output coming back) the cards | |
| 37 | + | // are stale and the monitor draws its workspace again. | |
| 38 | + | func (m *Monitor) switcherShown() bool { return m.sw.shown && m.sw.ws == m.Current() } | |
| 39 | + | ||
| 34 | 40 | // previewing reports whether the monitor draws previews instead of its | |
| 35 | 41 | // workspace: the overview, or the switcher's cards. | |
| 36 | - | func (m *Monitor) previewing() bool { return m.ov.open || m.sw.shown } | |
| 42 | + | func (m *Monitor) previewing() bool { return m.ov.open || m.switcherShown() } | |
| 37 | 43 | ||
| 38 | 44 | // noteFocus records the focused column as the most recently used. It does | |
| 39 | 45 | // nothing while a float or the stash has the focus, and allocates nothing | |
| @@ -85,10 +91,28 @@ func (w *Workspace) switchOrder() []WindowID { | |||
| 85 | 91 | return order | |
| 86 | 92 | } | |
| 87 | 93 | ||
| 88 | - | // remove drops a closed window from the open switcher: its card goes, the | |
| 89 | - | // selection stays on the same card (or the one that took its place). With | |
| 90 | - | // fewer than two columns left there is nothing to switch. | |
| 91 | - | func (s *switcherState) remove(id WindowID) { | |
| 94 | + | // heir is the window of id's column that is not id, or 0: call it before id | |
| 95 | + | // is removed, then hand it to remove. | |
| 96 | + | func (s *switcherState) heir(id WindowID) WindowID { | |
| 97 | + | if !s.open || s.ws == nil || !slices.Contains(s.order, id) { | |
| 98 | + | return 0 | |
| 99 | + | } | |
| 100 | + | if i := s.ws.columnOf(id); i >= 0 { | |
| 101 | + | for _, v := range s.ws.Columns[i].Windows { | |
| 102 | + | if v != id { | |
| 103 | + | return v | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | return 0 | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | // remove drops a closed window from the open switcher. When its column | |
| 111 | + | // still has windows (heir, from before the removal) the card stands for the | |
| 112 | + | // column's new focused window and the selection stays; otherwise the card | |
| 113 | + | // goes, and the selection stays on the same card (or the one that took its | |
| 114 | + | // place). With fewer than two columns left there is nothing to switch. | |
| 115 | + | func (s *switcherState) remove(id, heir WindowID) { | |
| 92 | 116 | if !s.open { | |
| 93 | 117 | return | |
| 94 | 118 | } | |
| @@ -96,6 +120,17 @@ func (s *switcherState) remove(id WindowID) { | |||
| 96 | 120 | if i < 0 { | |
| 97 | 121 | return | |
| 98 | 122 | } | |
| 123 | + | if j := s.ws.columnOf(heir); heir != 0 && j >= 0 { | |
| 124 | + | col := s.ws.Columns[j] | |
| 125 | + | next := col.Windows[col.Focus] | |
| 126 | + | if size, ok := s.sizes[id]; ok { | |
| 127 | + | if _, has := s.sizes[next]; !has { | |
| 128 | + | s.sizes[next] = size | |
| 129 | + | } | |
| 130 | + | } | |
| 131 | + | s.order[i] = next | |
| 132 | + | return | |
| 133 | + | } | |
| 99 | 134 | s.order = slices.Delete(s.order, i, i+1) | |
| 100 | 135 | if i < s.at { | |
| 101 | 136 | s.at-- | |
| @@ -109,26 +144,32 @@ func (s *switcherState) remove(id WindowID) { | |||
| 109 | 144 | // commitSwitcher closes the switcher and focuses the selected column. The | |
| 110 | 145 | // column left hands its maximization to the target. It does nothing when the | |
| 111 | 146 | // workspace is not on screen any more, the window went, or the selection is | |
| 112 | - | // the column already focused. | |
| 113 | - | func (m *Monitor) commitSwitcher() { | |
| 147 | + | // the tile already focused (a float or the stash holding the focus does not | |
| 148 | + | // count: the tile takes it back). It reports whether the selection was still | |
| 149 | + | // valid on the workspace on screen. | |
| 150 | + | func (m *Monitor) commitSwitcher() bool { | |
| 114 | 151 | s := m.sw | |
| 115 | 152 | m.sw = switcherState{} | |
| 116 | 153 | if !s.open || s.ws == nil || s.at < 0 || s.at >= len(s.order) { | |
| 117 | - | return | |
| 154 | + | return false | |
| 118 | 155 | } | |
| 119 | 156 | id, w := s.order[s.at], s.ws | |
| 120 | 157 | if w != m.Current() { | |
| 121 | - | return | |
| 158 | + | return false | |
| 122 | 159 | } | |
| 123 | 160 | i := w.columnOf(id) | |
| 124 | - | if i < 0 || i == w.Focus { | |
| 125 | - | return | |
| 161 | + | if i < 0 { | |
| 162 | + | return false | |
| 163 | + | } | |
| 164 | + | if i == w.Focus && !w.floatFocus && !w.stashFocused() { | |
| 165 | + | return true | |
| 126 | 166 | } | |
| 127 | - | if w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth { | |
| 167 | + | if i != w.Focus && w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth { | |
| 128 | 168 | w.transferMaximization(i) | |
| 129 | 169 | } | |
| 130 | 170 | // Activate leaves another window's fullscreen and handles dialogs. | |
| 131 | 171 | w.Activate(id) | |
| 172 | + | return true | |
| 132 | 173 | } | |
| 133 | 174 | ||
| 134 | 175 | // switchDir is the direction of a switch action, 0 for any other action. | |
| @@ -147,6 +188,13 @@ func (c *Core) switching() bool { | |||
| 147 | 188 | return c.switcher.sc != nil && c.switcher.sc.mon.sw.open | |
| 148 | 189 | } | |
| 149 | 190 | ||
| 191 | + | // switcherStale reports whether the open switcher belongs to a workspace | |
| 192 | + | // that is not on its screen any more. | |
| 193 | + | func (c *Core) switcherStale() bool { | |
| 194 | + | m := c.switcher.sc.mon | |
| 195 | + | return m.sw.open && m.sw.ws != m.Current() | |
| 196 | + | } | |
| 197 | + | ||
| 150 | 198 | func (c *Core) stopSwitcherTimer() { | |
| 151 | 199 | if c.switcher.timerStop != nil { | |
| 152 | 200 | c.switcher.timerStop() | |
| @@ -164,11 +212,17 @@ func (c *Core) cancelSwitcher() { | |||
| 164 | 212 | c.switcher.sc = nil | |
| 165 | 213 | } | |
| 166 | 214 | ||
| 167 | - | // commitSwitcher commits the switcher on its own screen and ends it. | |
| 215 | + | // commitSwitcher commits the switcher on its own screen and ends it. The | |
| 216 | + | // keyboard follows the committed window, even if the pointer moved the focus | |
| 217 | + | // to another output meanwhile. | |
| 168 | 218 | func (c *Core) commitSwitcher() { | |
| 169 | 219 | c.stopSwitcherTimer() | |
| 170 | 220 | if sc := c.switcher.sc; sc != nil { | |
| 171 | - | sc.mon.commitSwitcher() | |
| 221 | + | if sc.mon.commitSwitcher() { | |
| 222 | + | if i := slices.Index(c.screens, sc); i >= 0 { | |
| 223 | + | c.focusScreen = i | |
| 224 | + | } | |
| 225 | + | } | |
| 172 | 226 | } | |
| 173 | 227 | c.switcher.sc = nil | |
| 174 | 228 | } | |
| @@ -179,11 +233,12 @@ func (c *Core) commitSwitcher() { | |||
| 179 | 233 | // at once. | |
| 180 | 234 | func (c *Core) switchStep(dir int) { | |
| 181 | 235 | sc := c.cur() | |
| 182 | - | if c.switching() && c.switcher.sc != sc { | |
| 183 | - | // The focus moved to another output: start over there. | |
| 236 | + | if c.switching() && (c.switcher.sc != sc || c.switcherStale()) { | |
| 237 | + | // The focus moved to another output, or another workspace came on | |
| 238 | + | // screen: start over. | |
| 184 | 239 | c.cancelSwitcher() | |
| 185 | 240 | } | |
| 186 | - | if sc.mon.ov.open { | |
| 241 | + | if sc.mon.ov.open || c.drag != nil { | |
| 187 | 242 | return | |
| 188 | 243 | } | |
| 189 | 244 | m := sc.mon | |
| @@ -314,10 +369,9 @@ func (c *Core) switcherClick(ctx context.Context, button uint32) (handled bool, | |||
| 314 | 369 | ||
| 315 | 370 | // switcherLayout lays out the shown switcher: one card per column in | |
| 316 | 371 | // switchOrder, equal boxes in a row centered in the overview area, every | |
| 317 | - | // window scaled into its box (never up) and the selected card Focused. | |
| 318 | - | // Like the overview, the selected card gets Focused (so its configure is | |
| 319 | - | // Activated) while the keyboard stays on the real focus. Every other window | |
| 320 | - | // is hidden. | |
| 372 | + | // window scaled into its box (never up). Like the overview, the selected | |
| 373 | + | // card gets Focused (so its configure is Activated) while the keyboard stays | |
| 374 | + | // on the real focus. Every other window is hidden. | |
| 321 | 375 | func (m *Monitor) switcherLayout(dst []Placement) []Placement { | |
| 322 | 376 | sw := &m.sw | |
| 323 | 377 | w := sw.ws | |
| @@ -31,9 +31,15 @@ type switcherState struct { | |||||
| 31 | sizes map[WindowID]Rect | 31 | sizes map[WindowID]Rect | ||
| 32 | } | 32 | } | ||
| 33 | 33 | ||||
| 34 | + | // switcherShown reports whether the switcher's cards are drawn. They belong | |||
| 35 | + | // to the workspace that was on screen when they showed: if another one is on | |||
| 36 | + | // screen now (an activation, the taskbar, an output coming back) the cards | |||
| 37 | + | // are stale and the monitor draws its workspace again. | |||
| 38 | + | func (m *Monitor) switcherShown() bool { return m.sw.shown && m.sw.ws == m.Current() } | |||
| 39 | + | ||||
| 34 | // previewing reports whether the monitor draws previews instead of its | 40 | // previewing reports whether the monitor draws previews instead of its | ||
| 35 | // workspace: the overview, or the switcher's cards. | 41 | // workspace: the overview, or the switcher's cards. | ||
| 36 | - | func (m *Monitor) previewing() bool { return m.ov.open || m.sw.shown } | 42 | + | func (m *Monitor) previewing() bool { return m.ov.open || m.switcherShown() } |
| 37 | 43 | ||||
| 38 | // noteFocus records the focused column as the most recently used. It does | 44 | // noteFocus records the focused column as the most recently used. It does | ||
| 39 | // nothing while a float or the stash has the focus, and allocates nothing | 45 | // nothing while a float or the stash has the focus, and allocates nothing | ||
| @@ -85,10 +91,28 @@ func (w *Workspace) switchOrder() []WindowID { | |||||
| 85 | return order | 91 | return order | ||
| 86 | } | 92 | } | ||
| 87 | 93 | ||||
| 88 | - | // remove drops a closed window from the open switcher: its card goes, the | 94 | + | // heir is the window of id's column that is not id, or 0: call it before id |
| 89 | - | // selection stays on the same card (or the one that took its place). With | 95 | + | // is removed, then hand it to remove. |
| 90 | - | // fewer than two columns left there is nothing to switch. | 96 | + | func (s *switcherState) heir(id WindowID) WindowID { |
| 91 | - | func (s *switcherState) remove(id WindowID) { | 97 | + | if !s.open || s.ws == nil || !slices.Contains(s.order, id) { |
| 98 | + | return 0 | |||
| 99 | + | } | |||
| 100 | + | if i := s.ws.columnOf(id); i >= 0 { | |||
| 101 | + | for _, v := range s.ws.Columns[i].Windows { | |||
| 102 | + | if v != id { | |||
| 103 | + | return v | |||
| 104 | + | } | |||
| 105 | + | } | |||
| 106 | + | } | |||
| 107 | + | return 0 | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | // remove drops a closed window from the open switcher. When its column | |||
| 111 | + | // still has windows (heir, from before the removal) the card stands for the | |||
| 112 | + | // column's new focused window and the selection stays; otherwise the card | |||
| 113 | + | // goes, and the selection stays on the same card (or the one that took its | |||
| 114 | + | // place). With fewer than two columns left there is nothing to switch. | |||
| 115 | + | func (s *switcherState) remove(id, heir WindowID) { | |||
| 92 | if !s.open { | 116 | if !s.open { | ||
| 93 | return | 117 | return | ||
| 94 | } | 118 | } | ||
| @@ -96,6 +120,17 @@ func (s *switcherState) remove(id WindowID) { | |||||
| 96 | if i < 0 { | 120 | if i < 0 { | ||
| 97 | return | 121 | return | ||
| 98 | } | 122 | } | ||
| 123 | + | if j := s.ws.columnOf(heir); heir != 0 && j >= 0 { | |||
| 124 | + | col := s.ws.Columns[j] | |||
| 125 | + | next := col.Windows[col.Focus] | |||
| 126 | + | if size, ok := s.sizes[id]; ok { | |||
| 127 | + | if _, has := s.sizes[next]; !has { | |||
| 128 | + | s.sizes[next] = size | |||
| 129 | + | } | |||
| 130 | + | } | |||
| 131 | + | s.order[i] = next | |||
| 132 | + | return | |||
| 133 | + | } | |||
| 99 | s.order = slices.Delete(s.order, i, i+1) | 134 | s.order = slices.Delete(s.order, i, i+1) | ||
| 100 | if i < s.at { | 135 | if i < s.at { | ||
| 101 | s.at-- | 136 | s.at-- | ||
| @@ -109,26 +144,32 @@ func (s *switcherState) remove(id WindowID) { | |||||
| 109 | // commitSwitcher closes the switcher and focuses the selected column. The | 144 | // commitSwitcher closes the switcher and focuses the selected column. The | ||
| 110 | // column left hands its maximization to the target. It does nothing when the | 145 | // column left hands its maximization to the target. It does nothing when the | ||
| 111 | // workspace is not on screen any more, the window went, or the selection is | 146 | // workspace is not on screen any more, the window went, or the selection is | ||
| 112 | - | // the column already focused. | 147 | + | // the tile already focused (a float or the stash holding the focus does not |
| 113 | - | func (m *Monitor) commitSwitcher() { | 148 | + | // count: the tile takes it back). It reports whether the selection was still |
| 149 | + | // valid on the workspace on screen. | |||
| 150 | + | func (m *Monitor) commitSwitcher() bool { | |||
| 114 | s := m.sw | 151 | s := m.sw | ||
| 115 | m.sw = switcherState{} | 152 | m.sw = switcherState{} | ||
| 116 | if !s.open || s.ws == nil || s.at < 0 || s.at >= len(s.order) { | 153 | if !s.open || s.ws == nil || s.at < 0 || s.at >= len(s.order) { | ||
| 117 | - | return | 154 | + | return false |
| 118 | } | 155 | } | ||
| 119 | id, w := s.order[s.at], s.ws | 156 | id, w := s.order[s.at], s.ws | ||
| 120 | if w != m.Current() { | 157 | if w != m.Current() { | ||
| 121 | - | return | 158 | + | return false |
| 122 | } | 159 | } | ||
| 123 | i := w.columnOf(id) | 160 | i := w.columnOf(id) | ||
| 124 | - | if i < 0 || i == w.Focus { | 161 | + | if i < 0 { |
| 125 | - | return | 162 | + | return false |
| 163 | + | } | |||
| 164 | + | if i == w.Focus && !w.floatFocus && !w.stashFocused() { | |||
| 165 | + | return true | |||
| 126 | } | 166 | } | ||
| 127 | - | if w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth { | 167 | + | if i != w.Focus && w.Focus >= 0 && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth { |
| 128 | w.transferMaximization(i) | 168 | w.transferMaximization(i) | ||
| 129 | } | 169 | } | ||
| 130 | // Activate leaves another window's fullscreen and handles dialogs. | 170 | // Activate leaves another window's fullscreen and handles dialogs. | ||
| 131 | w.Activate(id) | 171 | w.Activate(id) | ||
| 172 | + | return true | |||
| 132 | } | 173 | } | ||
| 133 | 174 | ||||
| 134 | // switchDir is the direction of a switch action, 0 for any other action. | 175 | // switchDir is the direction of a switch action, 0 for any other action. | ||
| @@ -147,6 +188,13 @@ func (c *Core) switching() bool { | |||||
| 147 | return c.switcher.sc != nil && c.switcher.sc.mon.sw.open | 188 | return c.switcher.sc != nil && c.switcher.sc.mon.sw.open | ||
| 148 | } | 189 | } | ||
| 149 | 190 | ||||
| 191 | + | // switcherStale reports whether the open switcher belongs to a workspace | |||
| 192 | + | // that is not on its screen any more. | |||
| 193 | + | func (c *Core) switcherStale() bool { | |||
| 194 | + | m := c.switcher.sc.mon | |||
| 195 | + | return m.sw.open && m.sw.ws != m.Current() | |||
| 196 | + | } | |||
| 197 | + | ||||
| 150 | func (c *Core) stopSwitcherTimer() { | 198 | func (c *Core) stopSwitcherTimer() { | ||
| 151 | if c.switcher.timerStop != nil { | 199 | if c.switcher.timerStop != nil { | ||
| 152 | c.switcher.timerStop() | 200 | c.switcher.timerStop() | ||
| @@ -164,11 +212,17 @@ func (c *Core) cancelSwitcher() { | |||||
| 164 | c.switcher.sc = nil | 212 | c.switcher.sc = nil | ||
| 165 | } | 213 | } | ||
| 166 | 214 | ||||
| 167 | - | // commitSwitcher commits the switcher on its own screen and ends it. | 215 | + | // commitSwitcher commits the switcher on its own screen and ends it. The |
| 216 | + | // keyboard follows the committed window, even if the pointer moved the focus | |||
| 217 | + | // to another output meanwhile. | |||
| 168 | func (c *Core) commitSwitcher() { | 218 | func (c *Core) commitSwitcher() { | ||
| 169 | c.stopSwitcherTimer() | 219 | c.stopSwitcherTimer() | ||
| 170 | if sc := c.switcher.sc; sc != nil { | 220 | if sc := c.switcher.sc; sc != nil { | ||
| 171 | - | sc.mon.commitSwitcher() | 221 | + | if sc.mon.commitSwitcher() { |
| 222 | + | if i := slices.Index(c.screens, sc); i >= 0 { | |||
| 223 | + | c.focusScreen = i | |||
| 224 | + | } | |||
| 225 | + | } | |||
| 172 | } | 226 | } | ||
| 173 | c.switcher.sc = nil | 227 | c.switcher.sc = nil | ||
| 174 | } | 228 | } | ||
| @@ -179,11 +233,12 @@ func (c *Core) commitSwitcher() { | |||||
| 179 | // at once. | 233 | // at once. | ||
| 180 | func (c *Core) switchStep(dir int) { | 234 | func (c *Core) switchStep(dir int) { | ||
| 181 | sc := c.cur() | 235 | sc := c.cur() | ||
| 182 | - | if c.switching() && c.switcher.sc != sc { | 236 | + | if c.switching() && (c.switcher.sc != sc || c.switcherStale()) { |
| 183 | - | // The focus moved to another output: start over there. | 237 | + | // The focus moved to another output, or another workspace came on |
| 238 | + | // screen: start over. | |||
| 184 | c.cancelSwitcher() | 239 | c.cancelSwitcher() | ||
| 185 | } | 240 | } | ||
| 186 | - | if sc.mon.ov.open { | 241 | + | if sc.mon.ov.open || c.drag != nil { |
| 187 | return | 242 | return | ||
| 188 | } | 243 | } | ||
| 189 | m := sc.mon | 244 | m := sc.mon | ||
| @@ -314,10 +369,9 @@ func (c *Core) switcherClick(ctx context.Context, button uint32) (handled bool, | |||||
| 314 | 369 | ||||
| 315 | // switcherLayout lays out the shown switcher: one card per column in | 370 | // switcherLayout lays out the shown switcher: one card per column in | ||
| 316 | // switchOrder, equal boxes in a row centered in the overview area, every | 371 | // switchOrder, equal boxes in a row centered in the overview area, every | ||
| 317 | - | // window scaled into its box (never up) and the selected card Focused. | 372 | + | // window scaled into its box (never up). Like the overview, the selected |
| 318 | - | // Like the overview, the selected card gets Focused (so its configure is | 373 | + | // card gets Focused (so its configure is Activated) while the keyboard stays |
| 319 | - | // Activated) while the keyboard stays on the real focus. Every other window | 374 | + | // on the real focus. Every other window is hidden. |
| 320 | - | // is hidden. | |||
| 321 | func (m *Monitor) switcherLayout(dst []Placement) []Placement { | 375 | func (m *Monitor) switcherLayout(dst []Placement) []Placement { | ||
| 322 | sw := &m.sw | 376 | sw := &m.sw | ||
| 323 | w := sw.ws | 377 | w := sw.ws | ||
M internal/core/switcher_test.go +282 -58
| @@ -149,12 +149,10 @@ const ( | |||
| 149 | 149 | // switcherCore is indicatorCore with the switcher binds, up to three | |
| 150 | 150 | // columns on screen and n one-window columns (windows 1..n, n focused), | |
| 151 | 151 | // published once so the configures carry the windows' sizes. | |
| 152 | - | func switcherCore(t *testing.T, n int) (*Core, *indicatorClock) { | |
| 152 | + | func switcherCore(t *testing.T, n int) (*Core, *indicatorClock, <-chan ports.ClientCommand) { | |
| 153 | 153 | t.Helper() | |
| 154 | 154 | commands := make(chan ports.ClientCommand, 64) | |
| 155 | 155 | c, ic := indicatorCoreWith(t, commands) | |
| 156 | - | switcherCommands[c] = commands | |
| 157 | - | t.Cleanup(func() { delete(switcherCommands, c) }) | |
| 158 | 156 | cfg := c.cfg | |
| 159 | 157 | cfg.Layout.MaxColumns = 3 | |
| 160 | 158 | cfg.Binds = switcherBinds | |
| @@ -165,7 +163,7 @@ func switcherCore(t *testing.T, n int) (*Core, *indicatorClock) { | |||
| 165 | 163 | c.cur().mon.AddWindow(id) | |
| 166 | 164 | } | |
| 167 | 165 | indicatorScene(t, c) | |
| 168 | - | return c, ic | |
| 166 | + | return c, ic, commands | |
| 169 | 167 | } | |
| 170 | 168 | ||
| 171 | 169 | func tab(mods ports.Mods, pressed bool) ports.KeyEvent { | |
| @@ -180,6 +178,13 @@ func superKey(mods ports.Mods, pressed bool) ports.KeyEvent { | |||
| 180 | 178 | return ports.KeyEvent{Keysym: "Super_L", Keycode: keySuper, Mods: mods, Pressed: pressed} | |
| 181 | 179 | } | |
| 182 | 180 | ||
| 181 | + | func bind(t *testing.T, c *Core, a Action) { | |
| 182 | + | t.Helper() | |
| 183 | + | if err := c.runBind(context.Background(), a); err != nil { | |
| 184 | + | t.Fatal(err) | |
| 185 | + | } | |
| 186 | + | } | |
| 187 | + | ||
| 183 | 188 | func sendKeys(t *testing.T, c *Core, keys ...ports.KeyEvent) { | |
| 184 | 189 | t.Helper() | |
| 185 | 190 | for _, k := range keys { | |
| @@ -207,13 +212,9 @@ func switcherFocus(c *Core) WindowID { | |||
| 207 | 212 | return id | |
| 208 | 213 | } | |
| 209 | 214 | ||
| 210 | - | // switcherCommands is the receiving end of each switcherCore's commands. | |
| 211 | - | var switcherCommands = map[*Core]chan ports.ClientCommand{} | |
| 212 | - | ||
| 213 | 215 | // forwarded drains the commands and returns the forwarded keys. | |
| 214 | - | func forwarded(c *Core) []ports.ForwardKey { | |
| 216 | + | func forwarded(ch <-chan ports.ClientCommand) []ports.ForwardKey { | |
| 215 | 217 | var out []ports.ForwardKey | |
| 216 | - | ch := switcherCommands[c] | |
| 217 | 218 | for len(ch) > 0 { | |
| 218 | 219 | if v, ok := (<-ch).(ports.ForwardKey); ok { | |
| 219 | 220 | out = append(out, v) | |
| @@ -321,7 +322,7 @@ func TestSwitchCommitIgnoresGoneWindowAndOtherWorkspace(t *testing.T) { | |||
| 321 | 322 | // --- P2.2: actions and step --- | |
| 322 | 323 | ||
| 323 | 324 | func TestSwitchStepOpensOnPreviousColumnAndCycles(t *testing.T) { | |
| 324 | - | c, ic := switcherCore(t, 3) | |
| 325 | + | c, ic, _ := switcherCore(t, 3) | |
| 325 | 326 | c.mods = ports.ModSuper | |
| 326 | 327 | before := switcherFocus(c) | |
| 327 | 328 | if err := c.runBind(context.Background(), ActionSwitchColumnNext); err != nil { | |
| @@ -334,19 +335,19 @@ func TestSwitchStepOpensOnPreviousColumnAndCycles(t *testing.T) { | |||
| 334 | 335 | if len(ic.timers) != 1 || ic.timers[0] != switcherDelay { | |
| 335 | 336 | t.Fatalf("timers %v, want one of %v", ic.timers, switcherDelay) | |
| 336 | 337 | } | |
| 337 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | |
| 338 | + | bind(t, c, ActionSwitchColumnNext) | |
| 338 | 339 | if sw.at != 2 { | |
| 339 | 340 | t.Fatalf("at %d", sw.at) | |
| 340 | 341 | } | |
| 341 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | |
| 342 | + | bind(t, c, ActionSwitchColumnNext) | |
| 342 | 343 | if sw.at != 0 { | |
| 343 | 344 | t.Fatalf("at %d, want wrap to 0", sw.at) | |
| 344 | 345 | } | |
| 345 | - | c.runBind(context.Background(), ActionSwitchColumnPrev) | |
| 346 | + | bind(t, c, ActionSwitchColumnPrev) | |
| 346 | 347 | if sw.at != 2 { | |
| 347 | 348 | t.Fatalf("at %d, want wrap to 2", sw.at) | |
| 348 | 349 | } | |
| 349 | - | c.runBind(context.Background(), ActionSwitchColumnPrev) | |
| 350 | + | bind(t, c, ActionSwitchColumnPrev) | |
| 350 | 351 | if sw.at != 1 { | |
| 351 | 352 | t.Fatalf("at %d", sw.at) | |
| 352 | 353 | } | |
| @@ -356,16 +357,16 @@ func TestSwitchStepOpensOnPreviousColumnAndCycles(t *testing.T) { | |||
| 356 | 357 | } | |
| 357 | 358 | ||
| 358 | 359 | func TestSwitchPrevOpensOnOldestColumn(t *testing.T) { | |
| 359 | - | c, _ := switcherCore(t, 3) | |
| 360 | + | c, _, _ := switcherCore(t, 3) | |
| 360 | 361 | c.mods = ports.ModSuper | |
| 361 | - | c.runBind(context.Background(), ActionSwitchColumnPrev) | |
| 362 | + | bind(t, c, ActionSwitchColumnPrev) | |
| 362 | 363 | if sw := c.cur().mon.sw; !sw.open || sw.at != len(sw.order)-1 { | |
| 363 | 364 | t.Fatalf("at %d of %d", sw.at, len(sw.order)) | |
| 364 | 365 | } | |
| 365 | 366 | } | |
| 366 | 367 | ||
| 367 | 368 | func TestSwitchWithoutCmdCommitsAtOnce(t *testing.T) { | |
| 368 | - | c, ic := switcherCore(t, 3) | |
| 369 | + | c, ic, _ := switcherCore(t, 3) | |
| 369 | 370 | c.mods = 0 | |
| 370 | 371 | start := switcherFocus(c) | |
| 371 | 372 | if err := c.runBind(context.Background(), ActionSwitchColumnNext); err != nil { | |
| @@ -377,23 +378,23 @@ func TestSwitchWithoutCmdCommitsAtOnce(t *testing.T) { | |||
| 377 | 378 | if switcherFocus(c) == start { | |
| 378 | 379 | t.Fatal("focus did not move") | |
| 379 | 380 | } | |
| 380 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | |
| 381 | + | bind(t, c, ActionSwitchColumnNext) | |
| 381 | 382 | if switcherFocus(c) != start { | |
| 382 | 383 | t.Fatalf("focus %d, want back on %d", switcherFocus(c), start) | |
| 383 | 384 | } | |
| 384 | 385 | } | |
| 385 | 386 | ||
| 386 | 387 | func TestSwitchIgnoredWithOneColumnOrOverview(t *testing.T) { | |
| 387 | - | c, _ := switcherCore(t, 1) | |
| 388 | + | c, _, _ := switcherCore(t, 1) | |
| 388 | 389 | c.mods = ports.ModSuper | |
| 389 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | |
| 390 | + | bind(t, c, ActionSwitchColumnNext) | |
| 390 | 391 | if c.cur().mon.sw.open || c.switcher.sc != nil { | |
| 391 | 392 | t.Fatal("switcher opened with one column") | |
| 392 | 393 | } | |
| 393 | - | c, _ = switcherCore(t, 3) | |
| 394 | + | c, _, _ = switcherCore(t, 3) | |
| 394 | 395 | c.cur().mon.ToggleOverview() | |
| 395 | 396 | before := switcherFocus(c) | |
| 396 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | |
| 397 | + | bind(t, c, ActionSwitchColumnNext) | |
| 397 | 398 | if c.cur().mon.sw.open || c.switcher.sc != nil || switcherFocus(c) != before { | |
| 398 | 399 | t.Fatal("switcher acted in the overview") | |
| 399 | 400 | } | |
| @@ -402,7 +403,7 @@ func TestSwitchIgnoredWithOneColumnOrOverview(t *testing.T) { | |||
| 402 | 403 | // --- P2.3: release commit and timer --- | |
| 403 | 404 | ||
| 404 | 405 | func TestSwitchQuickTapSwapsWithoutCards(t *testing.T) { | |
| 405 | - | c, _ := switcherCore(t, 3) | |
| 406 | + | c, _, _ := switcherCore(t, 3) | |
| 406 | 407 | start := switcherFocus(c) | |
| 407 | 408 | for len(c.ch.Scenes) > 0 { | |
| 408 | 409 | <-c.ch.Scenes | |
| @@ -435,26 +436,26 @@ func TestSwitchQuickTapSwapsWithoutCards(t *testing.T) { | |||
| 435 | 436 | } | |
| 436 | 437 | ||
| 437 | 438 | func TestSwitchHoldCommitsOnReleaseAndForwardsIt(t *testing.T) { | |
| 438 | - | c, _ := switcherCore(t, 3) | |
| 439 | + | c, _, cmds := switcherCore(t, 3) | |
| 439 | 440 | start := switcherFocus(c) | |
| 440 | 441 | order := c.cur().mon.Current().switchOrder() | |
| 441 | 442 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true), tab(ports.ModSuper, false)) | |
| 442 | 443 | if switcherFocus(c) != start || !c.cur().mon.sw.open { | |
| 443 | 444 | t.Fatal("focus moved or switcher closed before the release") | |
| 444 | 445 | } | |
| 445 | - | forwarded(c) | |
| 446 | + | forwarded(cmds) | |
| 446 | 447 | sendKeys(t, c, superKey(0, false)) | |
| 447 | 448 | if switcherFocus(c) != order[1] || c.cur().mon.sw.open || c.switcher.timerC != nil { | |
| 448 | 449 | t.Fatalf("focus %d (want %d), open %v", switcherFocus(c), order[1], c.cur().mon.sw.open) | |
| 449 | 450 | } | |
| 450 | - | fk := forwarded(c) | |
| 451 | + | fk := forwarded(cmds) | |
| 451 | 452 | if len(fk) != 1 || fk[0].Key.Keysym != "Super_L" || fk[0].Key.Pressed || fk[0].ID != order[1] { | |
| 452 | 453 | t.Fatalf("release not forwarded to the new focus: %+v", fk) | |
| 453 | 454 | } | |
| 454 | 455 | } | |
| 455 | 456 | ||
| 456 | 457 | func TestSwitcherTickShowsCards(t *testing.T) { | |
| 457 | - | c, _ := switcherCore(t, 3) | |
| 458 | + | c, _, _ := switcherCore(t, 3) | |
| 458 | 459 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 459 | 460 | if c.cur().mon.sw.shown { | |
| 460 | 461 | t.Fatal("shown before the delay") | |
| @@ -468,9 +469,9 @@ func TestSwitcherTickShowsCards(t *testing.T) { | |||
| 468 | 469 | } | |
| 469 | 470 | ||
| 470 | 471 | func TestSwitcherTickAfterCloseIsHarmless(t *testing.T) { | |
| 471 | - | c, _ := switcherCore(t, 3) | |
| 472 | + | c, _, _ := switcherCore(t, 3) | |
| 472 | 473 | c.mods = ports.ModSuper | |
| 473 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | |
| 474 | + | bind(t, c, ActionSwitchColumnNext) | |
| 474 | 475 | c.cancelSwitcher() | |
| 475 | 476 | if c.switcherTick() { | |
| 476 | 477 | t.Fatal("tick on a closed switcher asks for a scene") | |
| @@ -480,10 +481,10 @@ func TestSwitcherTickAfterCloseIsHarmless(t *testing.T) { | |||
| 480 | 481 | // --- P3.1: keys and gestures --- | |
| 481 | 482 | ||
| 482 | 483 | func TestSwitchEscapeCancelsAndIsNotForwarded(t *testing.T) { | |
| 483 | - | c, _ := switcherCore(t, 3) | |
| 484 | + | c, _, cmds := switcherCore(t, 3) | |
| 484 | 485 | start := switcherFocus(c) | |
| 485 | 486 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true), tab(ports.ModSuper, false)) | |
| 486 | - | forwarded(c) | |
| 487 | + | forwarded(cmds) | |
| 487 | 488 | esc := ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true} | |
| 488 | 489 | sendKeys(t, c, esc) | |
| 489 | 490 | esc.Pressed = false | |
| @@ -491,7 +492,7 @@ func TestSwitchEscapeCancelsAndIsNotForwarded(t *testing.T) { | |||
| 491 | 492 | if c.cur().mon.sw.open || c.switcher.sc != nil || switcherFocus(c) != start { | |
| 492 | 493 | t.Fatalf("open %v focus %d (want %d)", c.cur().mon.sw.open, switcherFocus(c), start) | |
| 493 | 494 | } | |
| 494 | - | for _, k := range forwarded(c) { | |
| 495 | + | for _, k := range forwarded(cmds) { | |
| 495 | 496 | if k.Key.Keysym == "Escape" { | |
| 496 | 497 | t.Fatalf("Escape forwarded: %+v", k) | |
| 497 | 498 | } | |
| @@ -499,7 +500,7 @@ func TestSwitchEscapeCancelsAndIsNotForwarded(t *testing.T) { | |||
| 499 | 500 | } | |
| 500 | 501 | ||
| 501 | 502 | func TestSwitchAnotherBindCancelsThenRuns(t *testing.T) { | |
| 502 | - | c, _ := switcherCore(t, 3) | |
| 503 | + | c, _, _ := switcherCore(t, 3) | |
| 503 | 504 | start := switcherFocus(c) | |
| 504 | 505 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 505 | 506 | sendKeys(t, c, ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper, Pressed: true}) | |
| @@ -515,7 +516,7 @@ func TestSwitchAnotherBindCancelsThenRuns(t *testing.T) { | |||
| 515 | 516 | } | |
| 516 | 517 | ||
| 517 | 518 | func TestSwitchShiftTabGoesBackward(t *testing.T) { | |
| 518 | - | c, _ := switcherCore(t, 3) | |
| 519 | + | c, _, _ := switcherCore(t, 3) | |
| 519 | 520 | sendKeys(t, c, superKey(ports.ModSuper, true), shiftTab(true)) | |
| 520 | 521 | sw := c.cur().mon.sw | |
| 521 | 522 | if !sw.open || sw.at != len(sw.order)-1 { | |
| @@ -528,7 +529,7 @@ func TestSwitchShiftTabGoesBackward(t *testing.T) { | |||
| 528 | 529 | } | |
| 529 | 530 | ||
| 530 | 531 | func TestSwitchSwipeBeginCancels(t *testing.T) { | |
| 531 | - | c, _ := switcherCore(t, 3) | |
| 532 | + | c, _, _ := switcherCore(t, 3) | |
| 532 | 533 | start := switcherFocus(c) | |
| 533 | 534 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 534 | 535 | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | |
| @@ -538,7 +539,7 @@ func TestSwitchSwipeBeginCancels(t *testing.T) { | |||
| 538 | 539 | } | |
| 539 | 540 | ||
| 540 | 541 | func TestSwitchSecurityChangeCancels(t *testing.T) { | |
| 541 | - | c, _ := switcherCore(t, 3) | |
| 542 | + | c, _, _ := switcherCore(t, 3) | |
| 542 | 543 | state := ports.SecurityState{Generation: 1} | |
| 543 | 544 | gate := portsmocks.NewMockSessionSecurity(t) | |
| 544 | 545 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }).Maybe() | |
| @@ -555,7 +556,7 @@ func TestSwitchSecurityChangeCancels(t *testing.T) { | |||
| 555 | 556 | } | |
| 556 | 557 | ||
| 557 | 558 | func TestSwitchCmdKeyFollowsConfig(t *testing.T) { | |
| 558 | - | c, _ := switcherCore(t, 3) | |
| 559 | + | c, _, _ := switcherCore(t, 3) | |
| 559 | 560 | cfg := c.cfg | |
| 560 | 561 | cfg.Keyboard.CmdKey = "alt" | |
| 561 | 562 | if err := c.apply(cfg); err != nil { | |
| @@ -575,7 +576,7 @@ func TestSwitchCmdKeyFollowsConfig(t *testing.T) { | |||
| 575 | 576 | // --- P3.2: layout --- | |
| 576 | 577 | ||
| 577 | 578 | func TestSwitcherLayoutCards(t *testing.T) { | |
| 578 | - | c, _ := switcherCore(t, 3) | |
| 579 | + | c, _, _ := switcherCore(t, 3) | |
| 579 | 580 | c.cfg.Floating.Dim = 0.4 | |
| 580 | 581 | area := c.cur().mon.Current().overviewArea() | |
| 581 | 582 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| @@ -644,7 +645,7 @@ func focusedIDReal(c *Core) WindowID { | |||
| 644 | 645 | } | |
| 645 | 646 | ||
| 646 | 647 | func TestSwitcherLayoutHidesOtherWindowsOfColumns(t *testing.T) { | |
| 647 | - | c, _ := switcherCore(t, 3) | |
| 648 | + | c, _, _ := switcherCore(t, 3) | |
| 648 | 649 | c.cur().mon.AddWindow(4) | |
| 649 | 650 | w := c.cur().mon.Current() | |
| 650 | 651 | w.ConsumeOrExpel(-1) | |
| @@ -670,18 +671,18 @@ func TestSwitcherLayoutHidesOtherWindowsOfColumns(t *testing.T) { | |||
| 670 | 671 | } | |
| 671 | 672 | ||
| 672 | 673 | func TestSwitcherCardsKeepKeysOnRealFocus(t *testing.T) { | |
| 673 | - | c, _ := switcherCore(t, 3) | |
| 674 | + | c, _, cmds := switcherCore(t, 3) | |
| 674 | 675 | focus := switcherFocus(c) | |
| 675 | 676 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 676 | 677 | c.switcherTick() | |
| 677 | 678 | indicatorScene(t, c) | |
| 678 | - | forwarded(c) | |
| 679 | + | forwarded(cmds) | |
| 679 | 680 | if c.keyboardFocus() != focus { | |
| 680 | 681 | t.Fatalf("keyboard focus %d, want %d", c.keyboardFocus(), focus) | |
| 681 | 682 | } | |
| 682 | 683 | sendKeys(t, c, ports.KeyEvent{Keysym: "x", Keycode: 45, Mods: ports.ModSuper, Pressed: true}) | |
| 683 | 684 | // An unbound key goes to the real focus (Cmd+x has no bind here). | |
| 684 | - | fk := forwarded(c) | |
| 685 | + | fk := forwarded(cmds) | |
| 685 | 686 | if len(fk) != 1 || fk[0].ID != focus || fk[0].Key.Keysym != "x" { | |
| 686 | 687 | t.Fatalf("forwarded %+v, want x to %d", fk, focus) | |
| 687 | 688 | } | |
| @@ -701,13 +702,13 @@ func cardCenter(t *testing.T, c *Core, id WindowID) (float64, float64) { | |||
| 701 | 702 | } | |
| 702 | 703 | ||
| 703 | 704 | func TestSwitcherClickOnCardCommits(t *testing.T) { | |
| 704 | - | c, _ := switcherCore(t, 3) | |
| 705 | + | c, _, cmds := switcherCore(t, 3) | |
| 705 | 706 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 706 | 707 | c.switcherTick() | |
| 707 | 708 | indicatorScene(t, c) | |
| 708 | 709 | target := c.cur().mon.sw.order[2] | |
| 709 | 710 | c.cursorX, c.cursorY = cardCenter(t, c, target) | |
| 710 | - | forwarded(c) | |
| 711 | + | forwarded(cmds) | |
| 711 | 712 | if err := c.pointerButton(context.Background(), ports.PointerButton{Button: 0x110, Pressed: true}); err != nil { | |
| 712 | 713 | t.Fatal(err) | |
| 713 | 714 | } | |
| @@ -717,16 +718,15 @@ func TestSwitcherClickOnCardCommits(t *testing.T) { | |||
| 717 | 718 | if err := c.pointerButton(context.Background(), ports.PointerButton{Button: 0x110}); err != nil { | |
| 718 | 719 | t.Fatal(err) | |
| 719 | 720 | } | |
| 720 | - | ch := switcherCommands[c] | |
| 721 | - | for len(ch) > 0 { | |
| 722 | - | if v, ok := (<-ch).(ports.PointerButtonTo); ok { | |
| 721 | + | for len(cmds) > 0 { | |
| 722 | + | if v, ok := (<-cmds).(ports.PointerButtonTo); ok { | |
| 723 | 723 | t.Fatalf("click reached a client: %+v", v) | |
| 724 | 724 | } | |
| 725 | 725 | } | |
| 726 | 726 | } | |
| 727 | 727 | ||
| 728 | 728 | func TestSwitcherClickElsewhereCancels(t *testing.T) { | |
| 729 | - | c, _ := switcherCore(t, 3) | |
| 729 | + | c, _, _ := switcherCore(t, 3) | |
| 730 | 730 | start := switcherFocus(c) | |
| 731 | 731 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 732 | 732 | c.switcherTick() | |
| @@ -741,7 +741,7 @@ func TestSwitcherClickElsewhereCancels(t *testing.T) { | |||
| 741 | 741 | } | |
| 742 | 742 | ||
| 743 | 743 | func TestSwitcherPressBeforeCardsCancelsAndContinues(t *testing.T) { | |
| 744 | - | c, _ := switcherCore(t, 3) | |
| 744 | + | c, _, _ := switcherCore(t, 3) | |
| 745 | 745 | start := switcherFocus(c) | |
| 746 | 746 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 747 | 747 | handled, err := c.switcherClick(context.Background(), 0x110) | |
| @@ -756,7 +756,7 @@ func TestSwitcherPressBeforeCardsCancelsAndContinues(t *testing.T) { | |||
| 756 | 756 | // --- P3.4: robustness --- | |
| 757 | 757 | ||
| 758 | 758 | func TestSwitcherClosingSelectedWindowKeepsSelectionValid(t *testing.T) { | |
| 759 | - | c, _ := switcherCore(t, 3) | |
| 759 | + | c, _, _ := switcherCore(t, 3) | |
| 760 | 760 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 761 | 761 | c.switcherTick() | |
| 762 | 762 | sw := &c.cur().mon.sw | |
| @@ -777,9 +777,9 @@ func TestSwitcherClosingSelectedWindowKeepsSelectionValid(t *testing.T) { | |||
| 777 | 777 | } | |
| 778 | 778 | ||
| 779 | 779 | func TestSwitcherClosingBeforeSelectionKeepsCard(t *testing.T) { | |
| 780 | - | c, _ := switcherCore(t, 3) | |
| 780 | + | c, _, _ := switcherCore(t, 3) | |
| 781 | 781 | c.mods = ports.ModSuper | |
| 782 | - | c.runBind(context.Background(), ActionSwitchColumnPrev) | |
| 782 | + | bind(t, c, ActionSwitchColumnPrev) | |
| 783 | 783 | sw := &c.cur().mon.sw | |
| 784 | 784 | selected := sw.order[sw.at] | |
| 785 | 785 | c.cur().mon.RemoveWindow(sw.order[0]) | |
| @@ -789,7 +789,7 @@ func TestSwitcherClosingBeforeSelectionKeepsCard(t *testing.T) { | |||
| 789 | 789 | } | |
| 790 | 790 | ||
| 791 | 791 | func TestSwitcherClosingDownToOneColumnClosesIt(t *testing.T) { | |
| 792 | - | c, _ := switcherCore(t, 2) | |
| 792 | + | c, _, _ := switcherCore(t, 2) | |
| 793 | 793 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 794 | 794 | c.switcherTick() | |
| 795 | 795 | c.cur().mon.RemoveWindow(c.cur().mon.sw.order[1]) | |
| @@ -807,7 +807,7 @@ func TestSwitcherClosingDownToOneColumnClosesIt(t *testing.T) { | |||
| 807 | 807 | } | |
| 808 | 808 | ||
| 809 | 809 | func TestSwitcherOutputRemovalCancels(t *testing.T) { | |
| 810 | - | c, _ := switcherCore(t, 3) | |
| 810 | + | c, _, _ := switcherCore(t, 3) | |
| 811 | 811 | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | |
| 812 | 812 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 813 | 813 | c.switcherTick() | |
| @@ -828,7 +828,7 @@ func TestSwitcherOutputRemovalCancels(t *testing.T) { | |||
| 828 | 828 | } | |
| 829 | 829 | ||
| 830 | 830 | func TestSwitcherFocusMovingToAnotherOutputReopensThere(t *testing.T) { | |
| 831 | - | c, _ := switcherCore(t, 3) | |
| 831 | + | c, _, _ := switcherCore(t, 3) | |
| 832 | 832 | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | |
| 833 | 833 | first := c.cur() | |
| 834 | 834 | for id := WindowID(11); id <= 13; id++ { | |
| @@ -836,12 +836,12 @@ func TestSwitcherFocusMovingToAnotherOutputReopensThere(t *testing.T) { | |||
| 836 | 836 | } | |
| 837 | 837 | indicatorScene(t, c) | |
| 838 | 838 | c.mods = ports.ModSuper | |
| 839 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | |
| 839 | + | bind(t, c, ActionSwitchColumnNext) | |
| 840 | 840 | if c.switcher.sc != first { | |
| 841 | 841 | t.Fatal("switcher not on the focused screen") | |
| 842 | 842 | } | |
| 843 | 843 | c.focusScreen = c.screenIndex("B") | |
| 844 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | |
| 844 | + | bind(t, c, ActionSwitchColumnNext) | |
| 845 | 845 | if first.mon.sw.open { | |
| 846 | 846 | t.Fatal("old switcher left open") | |
| 847 | 847 | } | |
| @@ -853,7 +853,7 @@ func TestSwitcherFocusMovingToAnotherOutputReopensThere(t *testing.T) { | |||
| 853 | 853 | // With animations on, the cards fly in and the commit flies them back; the | |
| 854 | 854 | // windows end where the layout puts them, with no card left. | |
| 855 | 855 | func TestSwitcherAnimatedHoldAndCommit(t *testing.T) { | |
| 856 | - | c, ic := switcherCore(t, 3) | |
| 856 | + | c, ic, _ := switcherCore(t, 3) | |
| 857 | 857 | c.cfg.Animations.On = true | |
| 858 | 858 | target := c.cur().mon.Current().switchOrder()[1] | |
| 859 | 859 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| @@ -876,3 +876,227 @@ func TestSwitcherAnimatedHoldAndCommit(t *testing.T) { | |||
| 876 | 876 | t.Fatalf("cards %d, focus %d (want %d)", len(scenePreviews(s)), switcherFocus(c), target) | |
| 877 | 877 | } | |
| 878 | 878 | } | |
| 879 | + | ||
| 880 | + | // --- review fixes --- | |
| 881 | + | ||
| 882 | + | // twoWorkspaces is a switcher core whose second workspace holds two | |
| 883 | + | // windows (10, 11) and is not on screen. | |
| 884 | + | func twoWorkspaces(t *testing.T) (*Core, *Workspace) { | |
| 885 | + | t.Helper() | |
| 886 | + | c, _, _ := switcherCore(t, 3) | |
| 887 | + | c.cfg.Floating.Dim = 0.4 | |
| 888 | + | m := c.cur().mon | |
| 889 | + | first := m.Current() | |
| 890 | + | m.Focus(1) | |
| 891 | + | m.AddWindow(10) | |
| 892 | + | m.AddWindow(11) | |
| 893 | + | other := m.Current() | |
| 894 | + | m.show(first) | |
| 895 | + | if other == first || m.Current() != first { | |
| 896 | + | t.Fatal("workspaces not set up") | |
| 897 | + | } | |
| 898 | + | indicatorScene(t, c) | |
| 899 | + | return c, other | |
| 900 | + | } | |
| 901 | + | ||
| 902 | + | func TestSwitcherShownDropsWhenAnotherWorkspaceComesOnScreen(t *testing.T) { | |
| 903 | + | c, other := twoWorkspaces(t) | |
| 904 | + | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 905 | + | c.switcherTick() | |
| 906 | + | if s := indicatorScene(t, c); len(scenePreviews(s)) != 3 { | |
| 907 | + | t.Fatalf("cards not shown: %+v", s.Windows) | |
| 908 | + | } | |
| 909 | + | // An xdg-activation request for a window of the other workspace. | |
| 910 | + | if err := c.activate(context.Background(), 10); err != nil { | |
| 911 | + | t.Fatal(err) | |
| 912 | + | } | |
| 913 | + | m := c.cur().mon | |
| 914 | + | if m.Current() != other || m.previewing() { | |
| 915 | + | t.Fatalf("on screen %v, previewing %v", m.Current() == other, m.previewing()) | |
| 916 | + | } | |
| 917 | + | s := indicatorScene(t, c) | |
| 918 | + | if len(scenePreviews(s)) != 0 || s.Dim != 0 || s.DimBehind { | |
| 919 | + | t.Fatalf("scene still dimmed or previewing: dim %v previews %+v", s.Dim, scenePreviews(s)) | |
| 920 | + | } | |
| 921 | + | if m.sw.open || c.switcher.sc != nil || c.switcher.timerC != nil { | |
| 922 | + | t.Fatal("stale switcher left open") | |
| 923 | + | } | |
| 924 | + | var any bool | |
| 925 | + | for _, p := range c.cur().shownLayout() { | |
| 926 | + | if p.ID == 10 || p.ID == 11 { | |
| 927 | + | x, y := float64(p.Rect.X+p.Rect.W/2), float64(p.Rect.Y+p.Rect.H/2) | |
| 928 | + | if id, _, _ := c.hit(x, y); id != p.ID { | |
| 929 | + | t.Fatalf("hit %d at window %d", id, p.ID) | |
| 930 | + | } | |
| 931 | + | any = true | |
| 932 | + | } | |
| 933 | + | } | |
| 934 | + | if !any { | |
| 935 | + | t.Fatal("workspace windows not laid out") | |
| 936 | + | } | |
| 937 | + | // The next switch opens fresh on the new workspace. | |
| 938 | + | c.mods = ports.ModSuper | |
| 939 | + | bind(t, c, ActionSwitchColumnNext) | |
| 940 | + | if sw := m.sw; !sw.open || sw.ws != other || sw.at != 1 { | |
| 941 | + | t.Fatalf("open %v on other %v at %d", sw.open, sw.ws == other, sw.at) | |
| 942 | + | } | |
| 943 | + | } | |
| 944 | + | ||
| 945 | + | func TestSwitcherStepOnStaleWorkspaceOpensFresh(t *testing.T) { | |
| 946 | + | c, other := twoWorkspaces(t) | |
| 947 | + | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 948 | + | c.switcherTick() | |
| 949 | + | // Shown on screen, then the workspace changes with no publish between. | |
| 950 | + | c.cur().mon.show(other) | |
| 951 | + | if c.cur().mon.previewing() { | |
| 952 | + | t.Fatal("stale cards still count as previews") | |
| 953 | + | } | |
| 954 | + | sendKeys(t, c, tab(ports.ModSuper, true)) | |
| 955 | + | sw := c.cur().mon.sw | |
| 956 | + | if !sw.open || sw.ws != other || sw.shown || sw.at != 1 { | |
| 957 | + | t.Fatalf("open %v on other %v shown %v at %d", sw.open, sw.ws == other, sw.shown, sw.at) | |
| 958 | + | } | |
| 959 | + | // A release on a stale switcher commits nothing and closes it. | |
| 960 | + | c.cur().mon.show(c.cur().mon.Workspaces[0]) | |
| 961 | + | focus := switcherFocus(c) | |
| 962 | + | sendKeys(t, c, superKey(0, false)) | |
| 963 | + | if c.cur().mon.sw.open || c.switcher.sc != nil || switcherFocus(c) != focus { | |
| 964 | + | t.Fatal("stale switcher survived the release") | |
| 965 | + | } | |
| 966 | + | } | |
| 967 | + | ||
| 968 | + | func TestSwitchIgnoredDuringDrag(t *testing.T) { | |
| 969 | + | c, _, _ := switcherCore(t, 3) | |
| 970 | + | start := switcherFocus(c) | |
| 971 | + | d := &dragState{id: start, button: btnLeft} | |
| 972 | + | c.drag = d | |
| 973 | + | c.buttons[btnLeft] = true | |
| 974 | + | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 975 | + | if c.cur().mon.sw.open || c.switcher.sc != nil || c.switcher.timerC != nil { | |
| 976 | + | t.Fatal("switcher opened during a drag") | |
| 977 | + | } | |
| 978 | + | if c.drag != d || switcherFocus(c) != start { | |
| 979 | + | t.Fatal("drag disturbed") | |
| 980 | + | } | |
| 981 | + | sendKeys(t, c, ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true}) | |
| 982 | + | if c.drag != nil { | |
| 983 | + | t.Fatal("Escape did not cancel the drag") | |
| 984 | + | } | |
| 985 | + | } | |
| 986 | + | ||
| 987 | + | // Another bind closing the shown cards still flies them back: the snapshot | |
| 988 | + | // is taken while they are drawn, so the windows move from the card rects. | |
| 989 | + | func TestSwitchBindOverShownCardsFliesBack(t *testing.T) { | |
| 990 | + | c, ic, _ := switcherCore(t, 3) | |
| 991 | + | c.cfg.Animations.On = true | |
| 992 | + | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 993 | + | c.switcherTick() | |
| 994 | + | for range 200 { | |
| 995 | + | ic.now = ic.now.Add(16 * time.Millisecond) | |
| 996 | + | if err := c.step(context.Background(), nil); err != nil { | |
| 997 | + | t.Fatal(err) | |
| 998 | + | } | |
| 999 | + | } | |
| 1000 | + | sc := c.cur() | |
| 1001 | + | cards := map[WindowID]Rect{} | |
| 1002 | + | for _, p := range sc.shownLayout() { | |
| 1003 | + | if p.Preview > 0 { | |
| 1004 | + | cards[p.ID] = p.Rect | |
| 1005 | + | } | |
| 1006 | + | } | |
| 1007 | + | if len(cards) != 3 || len(sc.rects) != 0 { | |
| 1008 | + | t.Fatalf("%d cards, %d motions: the cards did not settle", len(cards), len(sc.rects)) | |
| 1009 | + | } | |
| 1010 | + | bind(t, c, ActionFocusColumnLeft) | |
| 1011 | + | if c.switching() || sc.mon.sw.shown { | |
| 1012 | + | t.Fatal("the bind left the switcher open") | |
| 1013 | + | } | |
| 1014 | + | settled := map[WindowID]Rect{} | |
| 1015 | + | for _, p := range sc.mon.Layout() { | |
| 1016 | + | settled[p.ID] = p.Rect | |
| 1017 | + | } | |
| 1018 | + | for id, card := range cards { | |
| 1019 | + | rm, ok := sc.rects[id] | |
| 1020 | + | if !ok || !rm.scale { | |
| 1021 | + | t.Fatalf("window %d has no fly-back motion", id) | |
| 1022 | + | } | |
| 1023 | + | if got := rm.apply(settled[id]); got != card { | |
| 1024 | + | t.Fatalf("window %d flies back from %+v, want its card %+v", id, got, card) | |
| 1025 | + | } | |
| 1026 | + | } | |
| 1027 | + | } | |
| 1028 | + | ||
| 1029 | + | func TestSwitcherClosingSelectedWindowPicksColumnsNextWindow(t *testing.T) { | |
| 1030 | + | m := NewMonitor() | |
| 1031 | + | m.SetOutput(300, 200) | |
| 1032 | + | m.SetMaxColumns(3) | |
| 1033 | + | for id := WindowID(1); id <= 4; id++ { | |
| 1034 | + | m.AddWindow(id) | |
| 1035 | + | } | |
| 1036 | + | w := m.Current() | |
| 1037 | + | w.FocusID(3) | |
| 1038 | + | w.ConsumeOrExpel(-1) // columns [1] [2 3] [4] | |
| 1039 | + | if len(w.Columns) != 3 || len(w.Columns[1].Windows) != 2 { | |
| 1040 | + | t.Fatalf("columns %+v", w.Columns) | |
| 1041 | + | } | |
| 1042 | + | w.FocusID(1) | |
| 1043 | + | openSwitcher(m, 0) | |
| 1044 | + | at := slices.IndexFunc(m.sw.order, func(id WindowID) bool { return w.columnOf(id) == 1 }) | |
| 1045 | + | m.sw.at = at | |
| 1046 | + | gone := m.sw.order[at] | |
| 1047 | + | other := w.Columns[1].Windows[0] | |
| 1048 | + | if other == gone { | |
| 1049 | + | other = w.Columns[1].Windows[1] | |
| 1050 | + | } | |
| 1051 | + | m.RemoveWindow(gone) | |
| 1052 | + | if len(m.sw.order) != 3 || m.sw.at != at || m.sw.order[at] != other { | |
| 1053 | + | t.Fatalf("order %v at %d, want card %d to stand for %d", m.sw.order, m.sw.at, at, other) | |
| 1054 | + | } | |
| 1055 | + | m.commitSwitcher() | |
| 1056 | + | if id, _ := w.Focused(); id != other { | |
| 1057 | + | t.Fatalf("focus %d, want %d", id, other) | |
| 1058 | + | } | |
| 1059 | + | } | |
| 1060 | + | ||
| 1061 | + | func TestSwitchCommitFocusesTileUnderFloat(t *testing.T) { | |
| 1062 | + | c, _, _ := switcherCore(t, 3) | |
| 1063 | + | m := c.cur().mon | |
| 1064 | + | m.AddFloating(9, 100, 80) | |
| 1065 | + | w := m.Current() | |
| 1066 | + | if !w.floatFocus { | |
| 1067 | + | t.Fatal("float not focused") | |
| 1068 | + | } | |
| 1069 | + | tile := w.Columns[w.Focus].Windows[0] | |
| 1070 | + | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 1071 | + | bind(t, c, ActionSwitchColumnPrev) // back to card 0: the column under the float | |
| 1072 | + | if m.sw.at != 0 || m.sw.order[0] != tile { | |
| 1073 | + | t.Fatalf("at %d order %v", m.sw.at, m.sw.order) | |
| 1074 | + | } | |
| 1075 | + | sendKeys(t, c, superKey(0, false)) | |
| 1076 | + | if w.floatFocus || switcherFocus(c) != tile { | |
| 1077 | + | t.Fatalf("float focused %v, focus %d, want tile %d", w.floatFocus, switcherFocus(c), tile) | |
| 1078 | + | } | |
| 1079 | + | } | |
| 1080 | + | ||
| 1081 | + | func TestSwitchCommitFollowsToItsOutput(t *testing.T) { | |
| 1082 | + | c, _, _ := switcherCore(t, 3) | |
| 1083 | + | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | |
| 1084 | + | first := c.focusScreen | |
| 1085 | + | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |
| 1086 | + | want := c.cur().mon.sw.order[1] | |
| 1087 | + | // The pointer crosses to the other output meanwhile. | |
| 1088 | + | c.focusScreen = c.screenIndex("B") | |
| 1089 | + | sendKeys(t, c, superKey(0, false)) | |
| 1090 | + | if c.focusScreen != first || switcherFocus(c) != want { | |
| 1091 | + | t.Fatalf("focus screen %d (want %d), focus %d (want %d)", c.focusScreen, first, switcherFocus(c), want) | |
| 1092 | + | } | |
| 1093 | + | } | |
| 1094 | + | ||
| 1095 | + | func TestSwitchUnmodifiedBindThroughKeyEvent(t *testing.T) { | |
| 1096 | + | c, ic, _ := switcherCore(t, 3) | |
| 1097 | + | start := switcherFocus(c) | |
| 1098 | + | sendKeys(t, c, ports.KeyEvent{Keysym: "F9", Keycode: 75, Pressed: true}) | |
| 1099 | + | if c.cur().mon.sw.open || len(ic.timers) != 0 || switcherFocus(c) == start { | |
| 1100 | + | t.Fatalf("open %v timers %v focus %d (was %d)", c.cur().mon.sw.open, ic.timers, switcherFocus(c), start) | |
| 1101 | + | } | |
| 1102 | + | } | |
| @@ -149,12 +149,10 @@ const ( | |||||
| 149 | // switcherCore is indicatorCore with the switcher binds, up to three | 149 | // switcherCore is indicatorCore with the switcher binds, up to three | ||
| 150 | // columns on screen and n one-window columns (windows 1..n, n focused), | 150 | // columns on screen and n one-window columns (windows 1..n, n focused), | ||
| 151 | // published once so the configures carry the windows' sizes. | 151 | // published once so the configures carry the windows' sizes. | ||
| 152 | - | func switcherCore(t *testing.T, n int) (*Core, *indicatorClock) { | 152 | + | func switcherCore(t *testing.T, n int) (*Core, *indicatorClock, <-chan ports.ClientCommand) { |
| 153 | t.Helper() | 153 | t.Helper() | ||
| 154 | commands := make(chan ports.ClientCommand, 64) | 154 | commands := make(chan ports.ClientCommand, 64) | ||
| 155 | c, ic := indicatorCoreWith(t, commands) | 155 | c, ic := indicatorCoreWith(t, commands) | ||
| 156 | - | switcherCommands[c] = commands | |||
| 157 | - | t.Cleanup(func() { delete(switcherCommands, c) }) | |||
| 158 | cfg := c.cfg | 156 | cfg := c.cfg | ||
| 159 | cfg.Layout.MaxColumns = 3 | 157 | cfg.Layout.MaxColumns = 3 | ||
| 160 | cfg.Binds = switcherBinds | 158 | cfg.Binds = switcherBinds | ||
| @@ -165,7 +163,7 @@ func switcherCore(t *testing.T, n int) (*Core, *indicatorClock) { | |||||
| 165 | c.cur().mon.AddWindow(id) | 163 | c.cur().mon.AddWindow(id) | ||
| 166 | } | 164 | } | ||
| 167 | indicatorScene(t, c) | 165 | indicatorScene(t, c) | ||
| 168 | - | return c, ic | 166 | + | return c, ic, commands |
| 169 | } | 167 | } | ||
| 170 | 168 | ||||
| 171 | func tab(mods ports.Mods, pressed bool) ports.KeyEvent { | 169 | func tab(mods ports.Mods, pressed bool) ports.KeyEvent { | ||
| @@ -180,6 +178,13 @@ func superKey(mods ports.Mods, pressed bool) ports.KeyEvent { | |||||
| 180 | return ports.KeyEvent{Keysym: "Super_L", Keycode: keySuper, Mods: mods, Pressed: pressed} | 178 | return ports.KeyEvent{Keysym: "Super_L", Keycode: keySuper, Mods: mods, Pressed: pressed} | ||
| 181 | } | 179 | } | ||
| 182 | 180 | ||||
| 181 | + | func bind(t *testing.T, c *Core, a Action) { | |||
| 182 | + | t.Helper() | |||
| 183 | + | if err := c.runBind(context.Background(), a); err != nil { | |||
| 184 | + | t.Fatal(err) | |||
| 185 | + | } | |||
| 186 | + | } | |||
| 187 | + | ||||
| 183 | func sendKeys(t *testing.T, c *Core, keys ...ports.KeyEvent) { | 188 | func sendKeys(t *testing.T, c *Core, keys ...ports.KeyEvent) { | ||
| 184 | t.Helper() | 189 | t.Helper() | ||
| 185 | for _, k := range keys { | 190 | for _, k := range keys { | ||
| @@ -207,13 +212,9 @@ func switcherFocus(c *Core) WindowID { | |||||
| 207 | return id | 212 | return id | ||
| 208 | } | 213 | } | ||
| 209 | 214 | ||||
| 210 | - | // switcherCommands is the receiving end of each switcherCore's commands. | |||
| 211 | - | var switcherCommands = map[*Core]chan ports.ClientCommand{} | |||
| 212 | - | ||||
| 213 | // forwarded drains the commands and returns the forwarded keys. | 215 | // forwarded drains the commands and returns the forwarded keys. | ||
| 214 | - | func forwarded(c *Core) []ports.ForwardKey { | 216 | + | func forwarded(ch <-chan ports.ClientCommand) []ports.ForwardKey { |
| 215 | var out []ports.ForwardKey | 217 | var out []ports.ForwardKey | ||
| 216 | - | ch := switcherCommands[c] | |||
| 217 | for len(ch) > 0 { | 218 | for len(ch) > 0 { | ||
| 218 | if v, ok := (<-ch).(ports.ForwardKey); ok { | 219 | if v, ok := (<-ch).(ports.ForwardKey); ok { | ||
| 219 | out = append(out, v) | 220 | out = append(out, v) | ||
| @@ -321,7 +322,7 @@ func TestSwitchCommitIgnoresGoneWindowAndOtherWorkspace(t *testing.T) { | |||||
| 321 | // --- P2.2: actions and step --- | 322 | // --- P2.2: actions and step --- | ||
| 322 | 323 | ||||
| 323 | func TestSwitchStepOpensOnPreviousColumnAndCycles(t *testing.T) { | 324 | func TestSwitchStepOpensOnPreviousColumnAndCycles(t *testing.T) { | ||
| 324 | - | c, ic := switcherCore(t, 3) | 325 | + | c, ic, _ := switcherCore(t, 3) |
| 325 | c.mods = ports.ModSuper | 326 | c.mods = ports.ModSuper | ||
| 326 | before := switcherFocus(c) | 327 | before := switcherFocus(c) | ||
| 327 | if err := c.runBind(context.Background(), ActionSwitchColumnNext); err != nil { | 328 | if err := c.runBind(context.Background(), ActionSwitchColumnNext); err != nil { | ||
| @@ -334,19 +335,19 @@ func TestSwitchStepOpensOnPreviousColumnAndCycles(t *testing.T) { | |||||
| 334 | if len(ic.timers) != 1 || ic.timers[0] != switcherDelay { | 335 | if len(ic.timers) != 1 || ic.timers[0] != switcherDelay { | ||
| 335 | t.Fatalf("timers %v, want one of %v", ic.timers, switcherDelay) | 336 | t.Fatalf("timers %v, want one of %v", ic.timers, switcherDelay) | ||
| 336 | } | 337 | } | ||
| 337 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | 338 | + | bind(t, c, ActionSwitchColumnNext) |
| 338 | if sw.at != 2 { | 339 | if sw.at != 2 { | ||
| 339 | t.Fatalf("at %d", sw.at) | 340 | t.Fatalf("at %d", sw.at) | ||
| 340 | } | 341 | } | ||
| 341 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | 342 | + | bind(t, c, ActionSwitchColumnNext) |
| 342 | if sw.at != 0 { | 343 | if sw.at != 0 { | ||
| 343 | t.Fatalf("at %d, want wrap to 0", sw.at) | 344 | t.Fatalf("at %d, want wrap to 0", sw.at) | ||
| 344 | } | 345 | } | ||
| 345 | - | c.runBind(context.Background(), ActionSwitchColumnPrev) | 346 | + | bind(t, c, ActionSwitchColumnPrev) |
| 346 | if sw.at != 2 { | 347 | if sw.at != 2 { | ||
| 347 | t.Fatalf("at %d, want wrap to 2", sw.at) | 348 | t.Fatalf("at %d, want wrap to 2", sw.at) | ||
| 348 | } | 349 | } | ||
| 349 | - | c.runBind(context.Background(), ActionSwitchColumnPrev) | 350 | + | bind(t, c, ActionSwitchColumnPrev) |
| 350 | if sw.at != 1 { | 351 | if sw.at != 1 { | ||
| 351 | t.Fatalf("at %d", sw.at) | 352 | t.Fatalf("at %d", sw.at) | ||
| 352 | } | 353 | } | ||
| @@ -356,16 +357,16 @@ func TestSwitchStepOpensOnPreviousColumnAndCycles(t *testing.T) { | |||||
| 356 | } | 357 | } | ||
| 357 | 358 | ||||
| 358 | func TestSwitchPrevOpensOnOldestColumn(t *testing.T) { | 359 | func TestSwitchPrevOpensOnOldestColumn(t *testing.T) { | ||
| 359 | - | c, _ := switcherCore(t, 3) | 360 | + | c, _, _ := switcherCore(t, 3) |
| 360 | c.mods = ports.ModSuper | 361 | c.mods = ports.ModSuper | ||
| 361 | - | c.runBind(context.Background(), ActionSwitchColumnPrev) | 362 | + | bind(t, c, ActionSwitchColumnPrev) |
| 362 | if sw := c.cur().mon.sw; !sw.open || sw.at != len(sw.order)-1 { | 363 | if sw := c.cur().mon.sw; !sw.open || sw.at != len(sw.order)-1 { | ||
| 363 | t.Fatalf("at %d of %d", sw.at, len(sw.order)) | 364 | t.Fatalf("at %d of %d", sw.at, len(sw.order)) | ||
| 364 | } | 365 | } | ||
| 365 | } | 366 | } | ||
| 366 | 367 | ||||
| 367 | func TestSwitchWithoutCmdCommitsAtOnce(t *testing.T) { | 368 | func TestSwitchWithoutCmdCommitsAtOnce(t *testing.T) { | ||
| 368 | - | c, ic := switcherCore(t, 3) | 369 | + | c, ic, _ := switcherCore(t, 3) |
| 369 | c.mods = 0 | 370 | c.mods = 0 | ||
| 370 | start := switcherFocus(c) | 371 | start := switcherFocus(c) | ||
| 371 | if err := c.runBind(context.Background(), ActionSwitchColumnNext); err != nil { | 372 | if err := c.runBind(context.Background(), ActionSwitchColumnNext); err != nil { | ||
| @@ -377,23 +378,23 @@ func TestSwitchWithoutCmdCommitsAtOnce(t *testing.T) { | |||||
| 377 | if switcherFocus(c) == start { | 378 | if switcherFocus(c) == start { | ||
| 378 | t.Fatal("focus did not move") | 379 | t.Fatal("focus did not move") | ||
| 379 | } | 380 | } | ||
| 380 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | 381 | + | bind(t, c, ActionSwitchColumnNext) |
| 381 | if switcherFocus(c) != start { | 382 | if switcherFocus(c) != start { | ||
| 382 | t.Fatalf("focus %d, want back on %d", switcherFocus(c), start) | 383 | t.Fatalf("focus %d, want back on %d", switcherFocus(c), start) | ||
| 383 | } | 384 | } | ||
| 384 | } | 385 | } | ||
| 385 | 386 | ||||
| 386 | func TestSwitchIgnoredWithOneColumnOrOverview(t *testing.T) { | 387 | func TestSwitchIgnoredWithOneColumnOrOverview(t *testing.T) { | ||
| 387 | - | c, _ := switcherCore(t, 1) | 388 | + | c, _, _ := switcherCore(t, 1) |
| 388 | c.mods = ports.ModSuper | 389 | c.mods = ports.ModSuper | ||
| 389 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | 390 | + | bind(t, c, ActionSwitchColumnNext) |
| 390 | if c.cur().mon.sw.open || c.switcher.sc != nil { | 391 | if c.cur().mon.sw.open || c.switcher.sc != nil { | ||
| 391 | t.Fatal("switcher opened with one column") | 392 | t.Fatal("switcher opened with one column") | ||
| 392 | } | 393 | } | ||
| 393 | - | c, _ = switcherCore(t, 3) | 394 | + | c, _, _ = switcherCore(t, 3) |
| 394 | c.cur().mon.ToggleOverview() | 395 | c.cur().mon.ToggleOverview() | ||
| 395 | before := switcherFocus(c) | 396 | before := switcherFocus(c) | ||
| 396 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | 397 | + | bind(t, c, ActionSwitchColumnNext) |
| 397 | if c.cur().mon.sw.open || c.switcher.sc != nil || switcherFocus(c) != before { | 398 | if c.cur().mon.sw.open || c.switcher.sc != nil || switcherFocus(c) != before { | ||
| 398 | t.Fatal("switcher acted in the overview") | 399 | t.Fatal("switcher acted in the overview") | ||
| 399 | } | 400 | } | ||
| @@ -402,7 +403,7 @@ func TestSwitchIgnoredWithOneColumnOrOverview(t *testing.T) { | |||||
| 402 | // --- P2.3: release commit and timer --- | 403 | // --- P2.3: release commit and timer --- | ||
| 403 | 404 | ||||
| 404 | func TestSwitchQuickTapSwapsWithoutCards(t *testing.T) { | 405 | func TestSwitchQuickTapSwapsWithoutCards(t *testing.T) { | ||
| 405 | - | c, _ := switcherCore(t, 3) | 406 | + | c, _, _ := switcherCore(t, 3) |
| 406 | start := switcherFocus(c) | 407 | start := switcherFocus(c) | ||
| 407 | for len(c.ch.Scenes) > 0 { | 408 | for len(c.ch.Scenes) > 0 { | ||
| 408 | <-c.ch.Scenes | 409 | <-c.ch.Scenes | ||
| @@ -435,26 +436,26 @@ func TestSwitchQuickTapSwapsWithoutCards(t *testing.T) { | |||||
| 435 | } | 436 | } | ||
| 436 | 437 | ||||
| 437 | func TestSwitchHoldCommitsOnReleaseAndForwardsIt(t *testing.T) { | 438 | func TestSwitchHoldCommitsOnReleaseAndForwardsIt(t *testing.T) { | ||
| 438 | - | c, _ := switcherCore(t, 3) | 439 | + | c, _, cmds := switcherCore(t, 3) |
| 439 | start := switcherFocus(c) | 440 | start := switcherFocus(c) | ||
| 440 | order := c.cur().mon.Current().switchOrder() | 441 | order := c.cur().mon.Current().switchOrder() | ||
| 441 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true), tab(ports.ModSuper, false)) | 442 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true), tab(ports.ModSuper, false)) | ||
| 442 | if switcherFocus(c) != start || !c.cur().mon.sw.open { | 443 | if switcherFocus(c) != start || !c.cur().mon.sw.open { | ||
| 443 | t.Fatal("focus moved or switcher closed before the release") | 444 | t.Fatal("focus moved or switcher closed before the release") | ||
| 444 | } | 445 | } | ||
| 445 | - | forwarded(c) | 446 | + | forwarded(cmds) |
| 446 | sendKeys(t, c, superKey(0, false)) | 447 | sendKeys(t, c, superKey(0, false)) | ||
| 447 | if switcherFocus(c) != order[1] || c.cur().mon.sw.open || c.switcher.timerC != nil { | 448 | if switcherFocus(c) != order[1] || c.cur().mon.sw.open || c.switcher.timerC != nil { | ||
| 448 | t.Fatalf("focus %d (want %d), open %v", switcherFocus(c), order[1], c.cur().mon.sw.open) | 449 | t.Fatalf("focus %d (want %d), open %v", switcherFocus(c), order[1], c.cur().mon.sw.open) | ||
| 449 | } | 450 | } | ||
| 450 | - | fk := forwarded(c) | 451 | + | fk := forwarded(cmds) |
| 451 | if len(fk) != 1 || fk[0].Key.Keysym != "Super_L" || fk[0].Key.Pressed || fk[0].ID != order[1] { | 452 | if len(fk) != 1 || fk[0].Key.Keysym != "Super_L" || fk[0].Key.Pressed || fk[0].ID != order[1] { | ||
| 452 | t.Fatalf("release not forwarded to the new focus: %+v", fk) | 453 | t.Fatalf("release not forwarded to the new focus: %+v", fk) | ||
| 453 | } | 454 | } | ||
| 454 | } | 455 | } | ||
| 455 | 456 | ||||
| 456 | func TestSwitcherTickShowsCards(t *testing.T) { | 457 | func TestSwitcherTickShowsCards(t *testing.T) { | ||
| 457 | - | c, _ := switcherCore(t, 3) | 458 | + | c, _, _ := switcherCore(t, 3) |
| 458 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | 459 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | ||
| 459 | if c.cur().mon.sw.shown { | 460 | if c.cur().mon.sw.shown { | ||
| 460 | t.Fatal("shown before the delay") | 461 | t.Fatal("shown before the delay") | ||
| @@ -468,9 +469,9 @@ func TestSwitcherTickShowsCards(t *testing.T) { | |||||
| 468 | } | 469 | } | ||
| 469 | 470 | ||||
| 470 | func TestSwitcherTickAfterCloseIsHarmless(t *testing.T) { | 471 | func TestSwitcherTickAfterCloseIsHarmless(t *testing.T) { | ||
| 471 | - | c, _ := switcherCore(t, 3) | 472 | + | c, _, _ := switcherCore(t, 3) |
| 472 | c.mods = ports.ModSuper | 473 | c.mods = ports.ModSuper | ||
| 473 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | 474 | + | bind(t, c, ActionSwitchColumnNext) |
| 474 | c.cancelSwitcher() | 475 | c.cancelSwitcher() | ||
| 475 | if c.switcherTick() { | 476 | if c.switcherTick() { | ||
| 476 | t.Fatal("tick on a closed switcher asks for a scene") | 477 | t.Fatal("tick on a closed switcher asks for a scene") | ||
| @@ -480,10 +481,10 @@ func TestSwitcherTickAfterCloseIsHarmless(t *testing.T) { | |||||
| 480 | // --- P3.1: keys and gestures --- | 481 | // --- P3.1: keys and gestures --- | ||
| 481 | 482 | ||||
| 482 | func TestSwitchEscapeCancelsAndIsNotForwarded(t *testing.T) { | 483 | func TestSwitchEscapeCancelsAndIsNotForwarded(t *testing.T) { | ||
| 483 | - | c, _ := switcherCore(t, 3) | 484 | + | c, _, cmds := switcherCore(t, 3) |
| 484 | start := switcherFocus(c) | 485 | start := switcherFocus(c) | ||
| 485 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true), tab(ports.ModSuper, false)) | 486 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true), tab(ports.ModSuper, false)) | ||
| 486 | - | forwarded(c) | 487 | + | forwarded(cmds) |
| 487 | esc := ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true} | 488 | esc := ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true} | ||
| 488 | sendKeys(t, c, esc) | 489 | sendKeys(t, c, esc) | ||
| 489 | esc.Pressed = false | 490 | esc.Pressed = false | ||
| @@ -491,7 +492,7 @@ func TestSwitchEscapeCancelsAndIsNotForwarded(t *testing.T) { | |||||
| 491 | if c.cur().mon.sw.open || c.switcher.sc != nil || switcherFocus(c) != start { | 492 | if c.cur().mon.sw.open || c.switcher.sc != nil || switcherFocus(c) != start { | ||
| 492 | t.Fatalf("open %v focus %d (want %d)", c.cur().mon.sw.open, switcherFocus(c), start) | 493 | t.Fatalf("open %v focus %d (want %d)", c.cur().mon.sw.open, switcherFocus(c), start) | ||
| 493 | } | 494 | } | ||
| 494 | - | for _, k := range forwarded(c) { | 495 | + | for _, k := range forwarded(cmds) { |
| 495 | if k.Key.Keysym == "Escape" { | 496 | if k.Key.Keysym == "Escape" { | ||
| 496 | t.Fatalf("Escape forwarded: %+v", k) | 497 | t.Fatalf("Escape forwarded: %+v", k) | ||
| 497 | } | 498 | } | ||
| @@ -499,7 +500,7 @@ func TestSwitchEscapeCancelsAndIsNotForwarded(t *testing.T) { | |||||
| 499 | } | 500 | } | ||
| 500 | 501 | ||||
| 501 | func TestSwitchAnotherBindCancelsThenRuns(t *testing.T) { | 502 | func TestSwitchAnotherBindCancelsThenRuns(t *testing.T) { | ||
| 502 | - | c, _ := switcherCore(t, 3) | 503 | + | c, _, _ := switcherCore(t, 3) |
| 503 | start := switcherFocus(c) | 504 | start := switcherFocus(c) | ||
| 504 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | 505 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | ||
| 505 | sendKeys(t, c, ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper, Pressed: true}) | 506 | sendKeys(t, c, ports.KeyEvent{Keysym: "f", Keycode: 33, Mods: ports.ModSuper, Pressed: true}) | ||
| @@ -515,7 +516,7 @@ func TestSwitchAnotherBindCancelsThenRuns(t *testing.T) { | |||||
| 515 | } | 516 | } | ||
| 516 | 517 | ||||
| 517 | func TestSwitchShiftTabGoesBackward(t *testing.T) { | 518 | func TestSwitchShiftTabGoesBackward(t *testing.T) { | ||
| 518 | - | c, _ := switcherCore(t, 3) | 519 | + | c, _, _ := switcherCore(t, 3) |
| 519 | sendKeys(t, c, superKey(ports.ModSuper, true), shiftTab(true)) | 520 | sendKeys(t, c, superKey(ports.ModSuper, true), shiftTab(true)) | ||
| 520 | sw := c.cur().mon.sw | 521 | sw := c.cur().mon.sw | ||
| 521 | if !sw.open || sw.at != len(sw.order)-1 { | 522 | if !sw.open || sw.at != len(sw.order)-1 { | ||
| @@ -528,7 +529,7 @@ func TestSwitchShiftTabGoesBackward(t *testing.T) { | |||||
| 528 | } | 529 | } | ||
| 529 | 530 | ||||
| 530 | func TestSwitchSwipeBeginCancels(t *testing.T) { | 531 | func TestSwitchSwipeBeginCancels(t *testing.T) { | ||
| 531 | - | c, _ := switcherCore(t, 3) | 532 | + | c, _, _ := switcherCore(t, 3) |
| 532 | start := switcherFocus(c) | 533 | start := switcherFocus(c) | ||
| 533 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | 534 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | ||
| 534 | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | 535 | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | ||
| @@ -538,7 +539,7 @@ func TestSwitchSwipeBeginCancels(t *testing.T) { | |||||
| 538 | } | 539 | } | ||
| 539 | 540 | ||||
| 540 | func TestSwitchSecurityChangeCancels(t *testing.T) { | 541 | func TestSwitchSecurityChangeCancels(t *testing.T) { | ||
| 541 | - | c, _ := switcherCore(t, 3) | 542 | + | c, _, _ := switcherCore(t, 3) |
| 542 | state := ports.SecurityState{Generation: 1} | 543 | state := ports.SecurityState{Generation: 1} | ||
| 543 | gate := portsmocks.NewMockSessionSecurity(t) | 544 | gate := portsmocks.NewMockSessionSecurity(t) | ||
| 544 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }).Maybe() | 545 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }).Maybe() | ||
| @@ -555,7 +556,7 @@ func TestSwitchSecurityChangeCancels(t *testing.T) { | |||||
| 555 | } | 556 | } | ||
| 556 | 557 | ||||
| 557 | func TestSwitchCmdKeyFollowsConfig(t *testing.T) { | 558 | func TestSwitchCmdKeyFollowsConfig(t *testing.T) { | ||
| 558 | - | c, _ := switcherCore(t, 3) | 559 | + | c, _, _ := switcherCore(t, 3) |
| 559 | cfg := c.cfg | 560 | cfg := c.cfg | ||
| 560 | cfg.Keyboard.CmdKey = "alt" | 561 | cfg.Keyboard.CmdKey = "alt" | ||
| 561 | if err := c.apply(cfg); err != nil { | 562 | if err := c.apply(cfg); err != nil { | ||
| @@ -575,7 +576,7 @@ func TestSwitchCmdKeyFollowsConfig(t *testing.T) { | |||||
| 575 | // --- P3.2: layout --- | 576 | // --- P3.2: layout --- | ||
| 576 | 577 | ||||
| 577 | func TestSwitcherLayoutCards(t *testing.T) { | 578 | func TestSwitcherLayoutCards(t *testing.T) { | ||
| 578 | - | c, _ := switcherCore(t, 3) | 579 | + | c, _, _ := switcherCore(t, 3) |
| 579 | c.cfg.Floating.Dim = 0.4 | 580 | c.cfg.Floating.Dim = 0.4 | ||
| 580 | area := c.cur().mon.Current().overviewArea() | 581 | area := c.cur().mon.Current().overviewArea() | ||
| 581 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | 582 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | ||
| @@ -644,7 +645,7 @@ func focusedIDReal(c *Core) WindowID { | |||||
| 644 | } | 645 | } | ||
| 645 | 646 | ||||
| 646 | func TestSwitcherLayoutHidesOtherWindowsOfColumns(t *testing.T) { | 647 | func TestSwitcherLayoutHidesOtherWindowsOfColumns(t *testing.T) { | ||
| 647 | - | c, _ := switcherCore(t, 3) | 648 | + | c, _, _ := switcherCore(t, 3) |
| 648 | c.cur().mon.AddWindow(4) | 649 | c.cur().mon.AddWindow(4) | ||
| 649 | w := c.cur().mon.Current() | 650 | w := c.cur().mon.Current() | ||
| 650 | w.ConsumeOrExpel(-1) | 651 | w.ConsumeOrExpel(-1) | ||
| @@ -670,18 +671,18 @@ func TestSwitcherLayoutHidesOtherWindowsOfColumns(t *testing.T) { | |||||
| 670 | } | 671 | } | ||
| 671 | 672 | ||||
| 672 | func TestSwitcherCardsKeepKeysOnRealFocus(t *testing.T) { | 673 | func TestSwitcherCardsKeepKeysOnRealFocus(t *testing.T) { | ||
| 673 | - | c, _ := switcherCore(t, 3) | 674 | + | c, _, cmds := switcherCore(t, 3) |
| 674 | focus := switcherFocus(c) | 675 | focus := switcherFocus(c) | ||
| 675 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | 676 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | ||
| 676 | c.switcherTick() | 677 | c.switcherTick() | ||
| 677 | indicatorScene(t, c) | 678 | indicatorScene(t, c) | ||
| 678 | - | forwarded(c) | 679 | + | forwarded(cmds) |
| 679 | if c.keyboardFocus() != focus { | 680 | if c.keyboardFocus() != focus { | ||
| 680 | t.Fatalf("keyboard focus %d, want %d", c.keyboardFocus(), focus) | 681 | t.Fatalf("keyboard focus %d, want %d", c.keyboardFocus(), focus) | ||
| 681 | } | 682 | } | ||
| 682 | sendKeys(t, c, ports.KeyEvent{Keysym: "x", Keycode: 45, Mods: ports.ModSuper, Pressed: true}) | 683 | sendKeys(t, c, ports.KeyEvent{Keysym: "x", Keycode: 45, Mods: ports.ModSuper, Pressed: true}) | ||
| 683 | // An unbound key goes to the real focus (Cmd+x has no bind here). | 684 | // An unbound key goes to the real focus (Cmd+x has no bind here). | ||
| 684 | - | fk := forwarded(c) | 685 | + | fk := forwarded(cmds) |
| 685 | if len(fk) != 1 || fk[0].ID != focus || fk[0].Key.Keysym != "x" { | 686 | if len(fk) != 1 || fk[0].ID != focus || fk[0].Key.Keysym != "x" { | ||
| 686 | t.Fatalf("forwarded %+v, want x to %d", fk, focus) | 687 | t.Fatalf("forwarded %+v, want x to %d", fk, focus) | ||
| 687 | } | 688 | } | ||
| @@ -701,13 +702,13 @@ func cardCenter(t *testing.T, c *Core, id WindowID) (float64, float64) { | |||||
| 701 | } | 702 | } | ||
| 702 | 703 | ||||
| 703 | func TestSwitcherClickOnCardCommits(t *testing.T) { | 704 | func TestSwitcherClickOnCardCommits(t *testing.T) { | ||
| 704 | - | c, _ := switcherCore(t, 3) | 705 | + | c, _, cmds := switcherCore(t, 3) |
| 705 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | 706 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | ||
| 706 | c.switcherTick() | 707 | c.switcherTick() | ||
| 707 | indicatorScene(t, c) | 708 | indicatorScene(t, c) | ||
| 708 | target := c.cur().mon.sw.order[2] | 709 | target := c.cur().mon.sw.order[2] | ||
| 709 | c.cursorX, c.cursorY = cardCenter(t, c, target) | 710 | c.cursorX, c.cursorY = cardCenter(t, c, target) | ||
| 710 | - | forwarded(c) | 711 | + | forwarded(cmds) |
| 711 | if err := c.pointerButton(context.Background(), ports.PointerButton{Button: 0x110, Pressed: true}); err != nil { | 712 | if err := c.pointerButton(context.Background(), ports.PointerButton{Button: 0x110, Pressed: true}); err != nil { | ||
| 712 | t.Fatal(err) | 713 | t.Fatal(err) | ||
| 713 | } | 714 | } | ||
| @@ -717,16 +718,15 @@ func TestSwitcherClickOnCardCommits(t *testing.T) { | |||||
| 717 | if err := c.pointerButton(context.Background(), ports.PointerButton{Button: 0x110}); err != nil { | 718 | if err := c.pointerButton(context.Background(), ports.PointerButton{Button: 0x110}); err != nil { | ||
| 718 | t.Fatal(err) | 719 | t.Fatal(err) | ||
| 719 | } | 720 | } | ||
| 720 | - | ch := switcherCommands[c] | 721 | + | for len(cmds) > 0 { |
| 721 | - | for len(ch) > 0 { | 722 | + | if v, ok := (<-cmds).(ports.PointerButtonTo); ok { |
| 722 | - | if v, ok := (<-ch).(ports.PointerButtonTo); ok { | |||
| 723 | t.Fatalf("click reached a client: %+v", v) | 723 | t.Fatalf("click reached a client: %+v", v) | ||
| 724 | } | 724 | } | ||
| 725 | } | 725 | } | ||
| 726 | } | 726 | } | ||
| 727 | 727 | ||||
| 728 | func TestSwitcherClickElsewhereCancels(t *testing.T) { | 728 | func TestSwitcherClickElsewhereCancels(t *testing.T) { | ||
| 729 | - | c, _ := switcherCore(t, 3) | 729 | + | c, _, _ := switcherCore(t, 3) |
| 730 | start := switcherFocus(c) | 730 | start := switcherFocus(c) | ||
| 731 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | 731 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | ||
| 732 | c.switcherTick() | 732 | c.switcherTick() | ||
| @@ -741,7 +741,7 @@ func TestSwitcherClickElsewhereCancels(t *testing.T) { | |||||
| 741 | } | 741 | } | ||
| 742 | 742 | ||||
| 743 | func TestSwitcherPressBeforeCardsCancelsAndContinues(t *testing.T) { | 743 | func TestSwitcherPressBeforeCardsCancelsAndContinues(t *testing.T) { | ||
| 744 | - | c, _ := switcherCore(t, 3) | 744 | + | c, _, _ := switcherCore(t, 3) |
| 745 | start := switcherFocus(c) | 745 | start := switcherFocus(c) | ||
| 746 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | 746 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | ||
| 747 | handled, err := c.switcherClick(context.Background(), 0x110) | 747 | handled, err := c.switcherClick(context.Background(), 0x110) | ||
| @@ -756,7 +756,7 @@ func TestSwitcherPressBeforeCardsCancelsAndContinues(t *testing.T) { | |||||
| 756 | // --- P3.4: robustness --- | 756 | // --- P3.4: robustness --- | ||
| 757 | 757 | ||||
| 758 | func TestSwitcherClosingSelectedWindowKeepsSelectionValid(t *testing.T) { | 758 | func TestSwitcherClosingSelectedWindowKeepsSelectionValid(t *testing.T) { | ||
| 759 | - | c, _ := switcherCore(t, 3) | 759 | + | c, _, _ := switcherCore(t, 3) |
| 760 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | 760 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | ||
| 761 | c.switcherTick() | 761 | c.switcherTick() | ||
| 762 | sw := &c.cur().mon.sw | 762 | sw := &c.cur().mon.sw | ||
| @@ -777,9 +777,9 @@ func TestSwitcherClosingSelectedWindowKeepsSelectionValid(t *testing.T) { | |||||
| 777 | } | 777 | } | ||
| 778 | 778 | ||||
| 779 | func TestSwitcherClosingBeforeSelectionKeepsCard(t *testing.T) { | 779 | func TestSwitcherClosingBeforeSelectionKeepsCard(t *testing.T) { | ||
| 780 | - | c, _ := switcherCore(t, 3) | 780 | + | c, _, _ := switcherCore(t, 3) |
| 781 | c.mods = ports.ModSuper | 781 | c.mods = ports.ModSuper | ||
| 782 | - | c.runBind(context.Background(), ActionSwitchColumnPrev) | 782 | + | bind(t, c, ActionSwitchColumnPrev) |
| 783 | sw := &c.cur().mon.sw | 783 | sw := &c.cur().mon.sw | ||
| 784 | selected := sw.order[sw.at] | 784 | selected := sw.order[sw.at] | ||
| 785 | c.cur().mon.RemoveWindow(sw.order[0]) | 785 | c.cur().mon.RemoveWindow(sw.order[0]) | ||
| @@ -789,7 +789,7 @@ func TestSwitcherClosingBeforeSelectionKeepsCard(t *testing.T) { | |||||
| 789 | } | 789 | } | ||
| 790 | 790 | ||||
| 791 | func TestSwitcherClosingDownToOneColumnClosesIt(t *testing.T) { | 791 | func TestSwitcherClosingDownToOneColumnClosesIt(t *testing.T) { | ||
| 792 | - | c, _ := switcherCore(t, 2) | 792 | + | c, _, _ := switcherCore(t, 2) |
| 793 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | 793 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | ||
| 794 | c.switcherTick() | 794 | c.switcherTick() | ||
| 795 | c.cur().mon.RemoveWindow(c.cur().mon.sw.order[1]) | 795 | c.cur().mon.RemoveWindow(c.cur().mon.sw.order[1]) | ||
| @@ -807,7 +807,7 @@ func TestSwitcherClosingDownToOneColumnClosesIt(t *testing.T) { | |||||
| 807 | } | 807 | } | ||
| 808 | 808 | ||||
| 809 | func TestSwitcherOutputRemovalCancels(t *testing.T) { | 809 | func TestSwitcherOutputRemovalCancels(t *testing.T) { | ||
| 810 | - | c, _ := switcherCore(t, 3) | 810 | + | c, _, _ := switcherCore(t, 3) |
| 811 | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | 811 | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | ||
| 812 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | 812 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | ||
| 813 | c.switcherTick() | 813 | c.switcherTick() | ||
| @@ -828,7 +828,7 @@ func TestSwitcherOutputRemovalCancels(t *testing.T) { | |||||
| 828 | } | 828 | } | ||
| 829 | 829 | ||||
| 830 | func TestSwitcherFocusMovingToAnotherOutputReopensThere(t *testing.T) { | 830 | func TestSwitcherFocusMovingToAnotherOutputReopensThere(t *testing.T) { | ||
| 831 | - | c, _ := switcherCore(t, 3) | 831 | + | c, _, _ := switcherCore(t, 3) |
| 832 | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | 832 | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | ||
| 833 | first := c.cur() | 833 | first := c.cur() | ||
| 834 | for id := WindowID(11); id <= 13; id++ { | 834 | for id := WindowID(11); id <= 13; id++ { | ||
| @@ -836,12 +836,12 @@ func TestSwitcherFocusMovingToAnotherOutputReopensThere(t *testing.T) { | |||||
| 836 | } | 836 | } | ||
| 837 | indicatorScene(t, c) | 837 | indicatorScene(t, c) | ||
| 838 | c.mods = ports.ModSuper | 838 | c.mods = ports.ModSuper | ||
| 839 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | 839 | + | bind(t, c, ActionSwitchColumnNext) |
| 840 | if c.switcher.sc != first { | 840 | if c.switcher.sc != first { | ||
| 841 | t.Fatal("switcher not on the focused screen") | 841 | t.Fatal("switcher not on the focused screen") | ||
| 842 | } | 842 | } | ||
| 843 | c.focusScreen = c.screenIndex("B") | 843 | c.focusScreen = c.screenIndex("B") | ||
| 844 | - | c.runBind(context.Background(), ActionSwitchColumnNext) | 844 | + | bind(t, c, ActionSwitchColumnNext) |
| 845 | if first.mon.sw.open { | 845 | if first.mon.sw.open { | ||
| 846 | t.Fatal("old switcher left open") | 846 | t.Fatal("old switcher left open") | ||
| 847 | } | 847 | } | ||
| @@ -853,7 +853,7 @@ func TestSwitcherFocusMovingToAnotherOutputReopensThere(t *testing.T) { | |||||
| 853 | // With animations on, the cards fly in and the commit flies them back; the | 853 | // With animations on, the cards fly in and the commit flies them back; the | ||
| 854 | // windows end where the layout puts them, with no card left. | 854 | // windows end where the layout puts them, with no card left. | ||
| 855 | func TestSwitcherAnimatedHoldAndCommit(t *testing.T) { | 855 | func TestSwitcherAnimatedHoldAndCommit(t *testing.T) { | ||
| 856 | - | c, ic := switcherCore(t, 3) | 856 | + | c, ic, _ := switcherCore(t, 3) |
| 857 | c.cfg.Animations.On = true | 857 | c.cfg.Animations.On = true | ||
| 858 | target := c.cur().mon.Current().switchOrder()[1] | 858 | target := c.cur().mon.Current().switchOrder()[1] | ||
| 859 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | 859 | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | ||
| @@ -876,3 +876,227 @@ func TestSwitcherAnimatedHoldAndCommit(t *testing.T) { | |||||
| 876 | t.Fatalf("cards %d, focus %d (want %d)", len(scenePreviews(s)), switcherFocus(c), target) | 876 | t.Fatalf("cards %d, focus %d (want %d)", len(scenePreviews(s)), switcherFocus(c), target) | ||
| 877 | } | 877 | } | ||
| 878 | } | 878 | } | ||
| 879 | + | ||||
| 880 | + | // --- review fixes --- | |||
| 881 | + | ||||
| 882 | + | // twoWorkspaces is a switcher core whose second workspace holds two | |||
| 883 | + | // windows (10, 11) and is not on screen. | |||
| 884 | + | func twoWorkspaces(t *testing.T) (*Core, *Workspace) { | |||
| 885 | + | t.Helper() | |||
| 886 | + | c, _, _ := switcherCore(t, 3) | |||
| 887 | + | c.cfg.Floating.Dim = 0.4 | |||
| 888 | + | m := c.cur().mon | |||
| 889 | + | first := m.Current() | |||
| 890 | + | m.Focus(1) | |||
| 891 | + | m.AddWindow(10) | |||
| 892 | + | m.AddWindow(11) | |||
| 893 | + | other := m.Current() | |||
| 894 | + | m.show(first) | |||
| 895 | + | if other == first || m.Current() != first { | |||
| 896 | + | t.Fatal("workspaces not set up") | |||
| 897 | + | } | |||
| 898 | + | indicatorScene(t, c) | |||
| 899 | + | return c, other | |||
| 900 | + | } | |||
| 901 | + | ||||
| 902 | + | func TestSwitcherShownDropsWhenAnotherWorkspaceComesOnScreen(t *testing.T) { | |||
| 903 | + | c, other := twoWorkspaces(t) | |||
| 904 | + | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |||
| 905 | + | c.switcherTick() | |||
| 906 | + | if s := indicatorScene(t, c); len(scenePreviews(s)) != 3 { | |||
| 907 | + | t.Fatalf("cards not shown: %+v", s.Windows) | |||
| 908 | + | } | |||
| 909 | + | // An xdg-activation request for a window of the other workspace. | |||
| 910 | + | if err := c.activate(context.Background(), 10); err != nil { | |||
| 911 | + | t.Fatal(err) | |||
| 912 | + | } | |||
| 913 | + | m := c.cur().mon | |||
| 914 | + | if m.Current() != other || m.previewing() { | |||
| 915 | + | t.Fatalf("on screen %v, previewing %v", m.Current() == other, m.previewing()) | |||
| 916 | + | } | |||
| 917 | + | s := indicatorScene(t, c) | |||
| 918 | + | if len(scenePreviews(s)) != 0 || s.Dim != 0 || s.DimBehind { | |||
| 919 | + | t.Fatalf("scene still dimmed or previewing: dim %v previews %+v", s.Dim, scenePreviews(s)) | |||
| 920 | + | } | |||
| 921 | + | if m.sw.open || c.switcher.sc != nil || c.switcher.timerC != nil { | |||
| 922 | + | t.Fatal("stale switcher left open") | |||
| 923 | + | } | |||
| 924 | + | var any bool | |||
| 925 | + | for _, p := range c.cur().shownLayout() { | |||
| 926 | + | if p.ID == 10 || p.ID == 11 { | |||
| 927 | + | x, y := float64(p.Rect.X+p.Rect.W/2), float64(p.Rect.Y+p.Rect.H/2) | |||
| 928 | + | if id, _, _ := c.hit(x, y); id != p.ID { | |||
| 929 | + | t.Fatalf("hit %d at window %d", id, p.ID) | |||
| 930 | + | } | |||
| 931 | + | any = true | |||
| 932 | + | } | |||
| 933 | + | } | |||
| 934 | + | if !any { | |||
| 935 | + | t.Fatal("workspace windows not laid out") | |||
| 936 | + | } | |||
| 937 | + | // The next switch opens fresh on the new workspace. | |||
| 938 | + | c.mods = ports.ModSuper | |||
| 939 | + | bind(t, c, ActionSwitchColumnNext) | |||
| 940 | + | if sw := m.sw; !sw.open || sw.ws != other || sw.at != 1 { | |||
| 941 | + | t.Fatalf("open %v on other %v at %d", sw.open, sw.ws == other, sw.at) | |||
| 942 | + | } | |||
| 943 | + | } | |||
| 944 | + | ||||
| 945 | + | func TestSwitcherStepOnStaleWorkspaceOpensFresh(t *testing.T) { | |||
| 946 | + | c, other := twoWorkspaces(t) | |||
| 947 | + | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |||
| 948 | + | c.switcherTick() | |||
| 949 | + | // Shown on screen, then the workspace changes with no publish between. | |||
| 950 | + | c.cur().mon.show(other) | |||
| 951 | + | if c.cur().mon.previewing() { | |||
| 952 | + | t.Fatal("stale cards still count as previews") | |||
| 953 | + | } | |||
| 954 | + | sendKeys(t, c, tab(ports.ModSuper, true)) | |||
| 955 | + | sw := c.cur().mon.sw | |||
| 956 | + | if !sw.open || sw.ws != other || sw.shown || sw.at != 1 { | |||
| 957 | + | t.Fatalf("open %v on other %v shown %v at %d", sw.open, sw.ws == other, sw.shown, sw.at) | |||
| 958 | + | } | |||
| 959 | + | // A release on a stale switcher commits nothing and closes it. | |||
| 960 | + | c.cur().mon.show(c.cur().mon.Workspaces[0]) | |||
| 961 | + | focus := switcherFocus(c) | |||
| 962 | + | sendKeys(t, c, superKey(0, false)) | |||
| 963 | + | if c.cur().mon.sw.open || c.switcher.sc != nil || switcherFocus(c) != focus { | |||
| 964 | + | t.Fatal("stale switcher survived the release") | |||
| 965 | + | } | |||
| 966 | + | } | |||
| 967 | + | ||||
| 968 | + | func TestSwitchIgnoredDuringDrag(t *testing.T) { | |||
| 969 | + | c, _, _ := switcherCore(t, 3) | |||
| 970 | + | start := switcherFocus(c) | |||
| 971 | + | d := &dragState{id: start, button: btnLeft} | |||
| 972 | + | c.drag = d | |||
| 973 | + | c.buttons[btnLeft] = true | |||
| 974 | + | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |||
| 975 | + | if c.cur().mon.sw.open || c.switcher.sc != nil || c.switcher.timerC != nil { | |||
| 976 | + | t.Fatal("switcher opened during a drag") | |||
| 977 | + | } | |||
| 978 | + | if c.drag != d || switcherFocus(c) != start { | |||
| 979 | + | t.Fatal("drag disturbed") | |||
| 980 | + | } | |||
| 981 | + | sendKeys(t, c, ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true}) | |||
| 982 | + | if c.drag != nil { | |||
| 983 | + | t.Fatal("Escape did not cancel the drag") | |||
| 984 | + | } | |||
| 985 | + | } | |||
| 986 | + | ||||
| 987 | + | // Another bind closing the shown cards still flies them back: the snapshot | |||
| 988 | + | // is taken while they are drawn, so the windows move from the card rects. | |||
| 989 | + | func TestSwitchBindOverShownCardsFliesBack(t *testing.T) { | |||
| 990 | + | c, ic, _ := switcherCore(t, 3) | |||
| 991 | + | c.cfg.Animations.On = true | |||
| 992 | + | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |||
| 993 | + | c.switcherTick() | |||
| 994 | + | for range 200 { | |||
| 995 | + | ic.now = ic.now.Add(16 * time.Millisecond) | |||
| 996 | + | if err := c.step(context.Background(), nil); err != nil { | |||
| 997 | + | t.Fatal(err) | |||
| 998 | + | } | |||
| 999 | + | } | |||
| 1000 | + | sc := c.cur() | |||
| 1001 | + | cards := map[WindowID]Rect{} | |||
| 1002 | + | for _, p := range sc.shownLayout() { | |||
| 1003 | + | if p.Preview > 0 { | |||
| 1004 | + | cards[p.ID] = p.Rect | |||
| 1005 | + | } | |||
| 1006 | + | } | |||
| 1007 | + | if len(cards) != 3 || len(sc.rects) != 0 { | |||
| 1008 | + | t.Fatalf("%d cards, %d motions: the cards did not settle", len(cards), len(sc.rects)) | |||
| 1009 | + | } | |||
| 1010 | + | bind(t, c, ActionFocusColumnLeft) | |||
| 1011 | + | if c.switching() || sc.mon.sw.shown { | |||
| 1012 | + | t.Fatal("the bind left the switcher open") | |||
| 1013 | + | } | |||
| 1014 | + | settled := map[WindowID]Rect{} | |||
| 1015 | + | for _, p := range sc.mon.Layout() { | |||
| 1016 | + | settled[p.ID] = p.Rect | |||
| 1017 | + | } | |||
| 1018 | + | for id, card := range cards { | |||
| 1019 | + | rm, ok := sc.rects[id] | |||
| 1020 | + | if !ok || !rm.scale { | |||
| 1021 | + | t.Fatalf("window %d has no fly-back motion", id) | |||
| 1022 | + | } | |||
| 1023 | + | if got := rm.apply(settled[id]); got != card { | |||
| 1024 | + | t.Fatalf("window %d flies back from %+v, want its card %+v", id, got, card) | |||
| 1025 | + | } | |||
| 1026 | + | } | |||
| 1027 | + | } | |||
| 1028 | + | ||||
| 1029 | + | func TestSwitcherClosingSelectedWindowPicksColumnsNextWindow(t *testing.T) { | |||
| 1030 | + | m := NewMonitor() | |||
| 1031 | + | m.SetOutput(300, 200) | |||
| 1032 | + | m.SetMaxColumns(3) | |||
| 1033 | + | for id := WindowID(1); id <= 4; id++ { | |||
| 1034 | + | m.AddWindow(id) | |||
| 1035 | + | } | |||
| 1036 | + | w := m.Current() | |||
| 1037 | + | w.FocusID(3) | |||
| 1038 | + | w.ConsumeOrExpel(-1) // columns [1] [2 3] [4] | |||
| 1039 | + | if len(w.Columns) != 3 || len(w.Columns[1].Windows) != 2 { | |||
| 1040 | + | t.Fatalf("columns %+v", w.Columns) | |||
| 1041 | + | } | |||
| 1042 | + | w.FocusID(1) | |||
| 1043 | + | openSwitcher(m, 0) | |||
| 1044 | + | at := slices.IndexFunc(m.sw.order, func(id WindowID) bool { return w.columnOf(id) == 1 }) | |||
| 1045 | + | m.sw.at = at | |||
| 1046 | + | gone := m.sw.order[at] | |||
| 1047 | + | other := w.Columns[1].Windows[0] | |||
| 1048 | + | if other == gone { | |||
| 1049 | + | other = w.Columns[1].Windows[1] | |||
| 1050 | + | } | |||
| 1051 | + | m.RemoveWindow(gone) | |||
| 1052 | + | if len(m.sw.order) != 3 || m.sw.at != at || m.sw.order[at] != other { | |||
| 1053 | + | t.Fatalf("order %v at %d, want card %d to stand for %d", m.sw.order, m.sw.at, at, other) | |||
| 1054 | + | } | |||
| 1055 | + | m.commitSwitcher() | |||
| 1056 | + | if id, _ := w.Focused(); id != other { | |||
| 1057 | + | t.Fatalf("focus %d, want %d", id, other) | |||
| 1058 | + | } | |||
| 1059 | + | } | |||
| 1060 | + | ||||
| 1061 | + | func TestSwitchCommitFocusesTileUnderFloat(t *testing.T) { | |||
| 1062 | + | c, _, _ := switcherCore(t, 3) | |||
| 1063 | + | m := c.cur().mon | |||
| 1064 | + | m.AddFloating(9, 100, 80) | |||
| 1065 | + | w := m.Current() | |||
| 1066 | + | if !w.floatFocus { | |||
| 1067 | + | t.Fatal("float not focused") | |||
| 1068 | + | } | |||
| 1069 | + | tile := w.Columns[w.Focus].Windows[0] | |||
| 1070 | + | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |||
| 1071 | + | bind(t, c, ActionSwitchColumnPrev) // back to card 0: the column under the float | |||
| 1072 | + | if m.sw.at != 0 || m.sw.order[0] != tile { | |||
| 1073 | + | t.Fatalf("at %d order %v", m.sw.at, m.sw.order) | |||
| 1074 | + | } | |||
| 1075 | + | sendKeys(t, c, superKey(0, false)) | |||
| 1076 | + | if w.floatFocus || switcherFocus(c) != tile { | |||
| 1077 | + | t.Fatalf("float focused %v, focus %d, want tile %d", w.floatFocus, switcherFocus(c), tile) | |||
| 1078 | + | } | |||
| 1079 | + | } | |||
| 1080 | + | ||||
| 1081 | + | func TestSwitchCommitFollowsToItsOutput(t *testing.T) { | |||
| 1082 | + | c, _, _ := switcherCore(t, 3) | |||
| 1083 | + | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | |||
| 1084 | + | first := c.focusScreen | |||
| 1085 | + | sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true)) | |||
| 1086 | + | want := c.cur().mon.sw.order[1] | |||
| 1087 | + | // The pointer crosses to the other output meanwhile. | |||
| 1088 | + | c.focusScreen = c.screenIndex("B") | |||
| 1089 | + | sendKeys(t, c, superKey(0, false)) | |||
| 1090 | + | if c.focusScreen != first || switcherFocus(c) != want { | |||
| 1091 | + | t.Fatalf("focus screen %d (want %d), focus %d (want %d)", c.focusScreen, first, switcherFocus(c), want) | |||
| 1092 | + | } | |||
| 1093 | + | } | |||
| 1094 | + | ||||
| 1095 | + | func TestSwitchUnmodifiedBindThroughKeyEvent(t *testing.T) { | |||
| 1096 | + | c, ic, _ := switcherCore(t, 3) | |||
| 1097 | + | start := switcherFocus(c) | |||
| 1098 | + | sendKeys(t, c, ports.KeyEvent{Keysym: "F9", Keycode: 75, Pressed: true}) | |||
| 1099 | + | if c.cur().mon.sw.open || len(ic.timers) != 0 || switcherFocus(c) == start { | |||
| 1100 | + | t.Fatalf("open %v timers %v focus %d (was %d)", c.cur().mon.sw.open, ic.timers, switcherFocus(c), start) | |||
| 1101 | + | } | |||
| 1102 | + | } | |||