refactor(core): limit focus flash to windows and columns
Focus the nearest visible window at the entry edge when directional focus crosses screens. Preserve destination focus without flashing when no window is visible.
- commit
- 15f833928c647ed5edccf9f3fc72f769a799c350
- author
- brice · · 2026-10-23 05:59 UTC
- committer
- brice · · 2026-10-23 05:59 UTC
- parent
- 3a820d6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +219 -59
M docs/config.md +9 -5
| @@ -54,7 +54,7 @@ See [examples/config](../examples/config) for every key with its default. | |||
| 54 | 54 | | `animations.speed` | `normal` | `slow`, `normal`, `fast`, or a factor from `0.1` to `10` | | |
| 55 | 55 | | `focus.follow-move` | `off` | Follow a column moved to another workspace | | |
| 56 | 56 | | `focus.position-hint` | `on` | Show workspace/band dots near the output bottom for 600ms after a position change, then fade for 150ms (no fade with animations off); hidden in overview, fullscreen and session lock; never captured | | |
| 57 | - | | `focus.flash` | `on` | Briefly draw an inner border, inside the window border, on the window, column or output an action changed | | |
| 57 | + | | `focus.flash` | `on` | Briefly draw an inner border, inside the window border, on the window or column an action changed | | |
| 58 | 58 | | `focus.flash-width` | `2` | Flash border width in logical pixels, `1` to `8` | | |
| 59 | 59 | | `focus.flash-opacity` | `0.6` | Starting opacity of the flash, `0.1` to `1` | | |
| 60 | 60 | | `focus.flash-duration` | `200ms` | Fade duration, `50ms` to `1s`; not scaled by `animations.speed` | | |
| @@ -97,10 +97,14 @@ even when empty. Cascade workspaces use top-aligned band columns; scroll/fixed | |||
| 97 | 97 | workspaces use one vertical workspace column. Large lists show at most 16 entries | |
| 98 | 98 | with the active entry kept visible; band rows are capped at 16. | |
| 99 | 99 | ||
| 100 | - | The focus flash marks the window, column or output an action changes with a brief | |
| 101 | - | inner border. When a flash-triggering action crosses screens, it marks the whole | |
| 102 | - | destination output, even if that output has no windows. Same-screen workspace | |
| 103 | - | switches and mouse focus do not trigger a flash. | |
| 100 | + | The focus flash marks the window or column an action changes with a brief | |
| 101 | + | inner border. When a flash-triggering action crosses screens, it marks the focused window on the | |
| 102 | + | destination output; an empty destination does not flash. Directional monitor focus | |
| 103 | + | and focus past a screen edge select the visible window nearest the entry edge | |
| 104 | + | (rightmost when moving left, bottommost when moving up, and conversely). Hidden | |
| 105 | + | and off-screen windows are ignored. If none are visible, the destination keeps | |
| 106 | + | its focus and does not flash. Same-screen workspace switches and mouse focus do | |
| 107 | + | not trigger a flash. | |
| 104 | 108 | It runs when `animations = on`. | |
| 105 | 109 | ||
| 106 | 110 | ## Automatic terminal | |
| @@ -54,7 +54,7 @@ See [examples/config](../examples/config) for every key with its default. | |||||
| 54 | | `animations.speed` | `normal` | `slow`, `normal`, `fast`, or a factor from `0.1` to `10` | | 54 | | `animations.speed` | `normal` | `slow`, `normal`, `fast`, or a factor from `0.1` to `10` | | ||
| 55 | | `focus.follow-move` | `off` | Follow a column moved to another workspace | | 55 | | `focus.follow-move` | `off` | Follow a column moved to another workspace | | ||
| 56 | | `focus.position-hint` | `on` | Show workspace/band dots near the output bottom for 600ms after a position change, then fade for 150ms (no fade with animations off); hidden in overview, fullscreen and session lock; never captured | | 56 | | `focus.position-hint` | `on` | Show workspace/band dots near the output bottom for 600ms after a position change, then fade for 150ms (no fade with animations off); hidden in overview, fullscreen and session lock; never captured | | ||
| 57 | - | | `focus.flash` | `on` | Briefly draw an inner border, inside the window border, on the window, column or output an action changed | | 57 | + | | `focus.flash` | `on` | Briefly draw an inner border, inside the window border, on the window or column an action changed | |
| 58 | | `focus.flash-width` | `2` | Flash border width in logical pixels, `1` to `8` | | 58 | | `focus.flash-width` | `2` | Flash border width in logical pixels, `1` to `8` | | ||
| 59 | | `focus.flash-opacity` | `0.6` | Starting opacity of the flash, `0.1` to `1` | | 59 | | `focus.flash-opacity` | `0.6` | Starting opacity of the flash, `0.1` to `1` | | ||
| 60 | | `focus.flash-duration` | `200ms` | Fade duration, `50ms` to `1s`; not scaled by `animations.speed` | | 60 | | `focus.flash-duration` | `200ms` | Fade duration, `50ms` to `1s`; not scaled by `animations.speed` | | ||
| @@ -97,10 +97,14 @@ even when empty. Cascade workspaces use top-aligned band columns; scroll/fixed | |||||
| 97 | workspaces use one vertical workspace column. Large lists show at most 16 entries | 97 | workspaces use one vertical workspace column. Large lists show at most 16 entries | ||
| 98 | with the active entry kept visible; band rows are capped at 16. | 98 | with the active entry kept visible; band rows are capped at 16. | ||
| 99 | 99 | ||||
| 100 | - | The focus flash marks the window, column or output an action changes with a brief | 100 | + | The focus flash marks the window or column an action changes with a brief |
| 101 | - | inner border. When a flash-triggering action crosses screens, it marks the whole | 101 | + | inner border. When a flash-triggering action crosses screens, it marks the focused window on the |
| 102 | - | destination output, even if that output has no windows. Same-screen workspace | 102 | + | destination output; an empty destination does not flash. Directional monitor focus |
| 103 | - | switches and mouse focus do not trigger a flash. | 103 | + | and focus past a screen edge select the visible window nearest the entry edge |
| 104 | + | (rightmost when moving left, bottommost when moving up, and conversely). Hidden | |||
| 105 | + | and off-screen windows are ignored. If none are visible, the destination keeps | |||
| 106 | + | its focus and does not flash. Same-screen workspace switches and mouse focus do | |||
| 107 | + | not trigger a flash. | |||
| 104 | It runs when `animations = on`. | 108 | It runs when `animations = on`. | ||
| 105 | 109 | ||||
| 106 | ## Automatic terminal | 110 | ## Automatic terminal | ||
M internal/core/action.go +43 -3
| @@ -216,7 +216,7 @@ type Effect struct { | |||
| 216 | 216 | // workspace, then a neighbor screen, then a trailing empty workspace. Vertical | |
| 217 | 217 | // edges do this only with a vertical workspace axis; cascade stays within bands. | |
| 218 | 218 | // Other actions apply to the focused screen's monitor unless explicitly moving | |
| 219 | - | // between screens. Screen changes request a flash on the destination output. | |
| 219 | + | // between screens. Screen changes flash the focused window on the destination. | |
| 220 | 220 | func (c *Core) applyAction(a Action) Effect { | |
| 221 | 221 | if !c.flashOn() { | |
| 222 | 222 | return c.dispatchAction(a) | |
| @@ -252,9 +252,16 @@ func (c *Core) applyAction(a Action) Effect { | |||
| 252 | 252 | } | |
| 253 | 253 | return | |
| 254 | 254 | } | |
| 255 | - | // Crossing outputs marks the destination itself, even if it is empty. | |
| 255 | + | // Crossing outputs marks the destination’s focused window, if any. | |
| 256 | 256 | if sc != beforeScreen { | |
| 257 | - | c.requestFlash(flashMonitor, 0) | |
| 257 | + | if id != 0 { | |
| 258 | + | for _, p := range sc.mon.Layout() { | |
| 259 | + | if p.ID == id && sc.mon.viewport().shows(p) { | |
| 260 | + | c.requestFlash(flashWindow, id) | |
| 261 | + | break | |
| 262 | + | } | |
| 263 | + | } | |
| 264 | + | } | |
| 258 | 265 | return | |
| 259 | 266 | } | |
| 260 | 267 | level := flashWindow | |
| @@ -312,6 +319,7 @@ func (c *Core) dispatchAction(a Action) Effect { | |||
| 312 | 319 | case ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionFocusMonitorUp, ActionFocusMonitorDown: | |
| 313 | 320 | if i := c.neighbor(monitorDirection(a)); i >= 0 { | |
| 314 | 321 | c.focusScreen = i | |
| 322 | + | c.cur().mon.focusEntry(monitorDirection(a)) | |
| 315 | 323 | } | |
| 316 | 324 | return Effect{} | |
| 317 | 325 | case ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionMoveWorkspaceToMonitorUp, ActionMoveWorkspaceToMonitorDown: | |
| @@ -461,6 +469,7 @@ func (c *Core) edgeFocus(axis layoutAxis, dir int, d direction) bool { | |||
| 461 | 469 | } | |
| 462 | 470 | if i := c.neighbor(d); i >= 0 { | |
| 463 | 471 | c.focusScreen = i | |
| 472 | + | c.cur().mon.focusEntry(d) | |
| 464 | 473 | return true | |
| 465 | 474 | } | |
| 466 | 475 | return m.stepWorkspace(axis, dir) | |
| @@ -661,3 +670,34 @@ func moveFloat(cur, to *Workspace, id WindowID) { | |||
| 661 | 670 | to.Activate(id) | |
| 662 | 671 | to.SetFullscreen(id, true) | |
| 663 | 672 | } | |
| 673 | + | ||
| 674 | + | // focusEntry selects the visible window nearest the edge entered from d. | |
| 675 | + | // Layout order breaks ties; an empty viewport preserves the existing focus. | |
| 676 | + | func (m *Monitor) focusEntry(d direction) { | |
| 677 | + | view := m.viewport() | |
| 678 | + | var id WindowID | |
| 679 | + | best := 0 | |
| 680 | + | for _, p := range m.Layout() { | |
| 681 | + | if !view.shows(p) { | |
| 682 | + | continue | |
| 683 | + | } | |
| 684 | + | var distance int | |
| 685 | + | area := view.area(p) | |
| 686 | + | switch d { | |
| 687 | + | case dirLeft: | |
| 688 | + | distance = max(0, area.X+area.W-(p.Rect.X+p.Rect.W)) | |
| 689 | + | case dirRight: | |
| 690 | + | distance = max(0, p.Rect.X-area.X) | |
| 691 | + | case dirUp: | |
| 692 | + | distance = max(0, area.Y+area.H-(p.Rect.Y+p.Rect.H)) | |
| 693 | + | case dirDown: | |
| 694 | + | distance = max(0, p.Rect.Y-area.Y) | |
| 695 | + | } | |
| 696 | + | if id == 0 || distance < best { | |
| 697 | + | id, best = p.ID, distance | |
| 698 | + | } | |
| 699 | + | } | |
| 700 | + | if id != 0 { | |
| 701 | + | m.Current().FocusID(id) | |
| 702 | + | } | |
| 703 | + | } | |
| @@ -216,7 +216,7 @@ type Effect struct { | |||||
| 216 | // workspace, then a neighbor screen, then a trailing empty workspace. Vertical | 216 | // workspace, then a neighbor screen, then a trailing empty workspace. Vertical | ||
| 217 | // edges do this only with a vertical workspace axis; cascade stays within bands. | 217 | // edges do this only with a vertical workspace axis; cascade stays within bands. | ||
| 218 | // Other actions apply to the focused screen's monitor unless explicitly moving | 218 | // Other actions apply to the focused screen's monitor unless explicitly moving | ||
| 219 | - | // between screens. Screen changes request a flash on the destination output. | 219 | + | // between screens. Screen changes flash the focused window on the destination. |
| 220 | func (c *Core) applyAction(a Action) Effect { | 220 | func (c *Core) applyAction(a Action) Effect { | ||
| 221 | if !c.flashOn() { | 221 | if !c.flashOn() { | ||
| 222 | return c.dispatchAction(a) | 222 | return c.dispatchAction(a) | ||
| @@ -252,9 +252,16 @@ func (c *Core) applyAction(a Action) Effect { | |||||
| 252 | } | 252 | } | ||
| 253 | return | 253 | return | ||
| 254 | } | 254 | } | ||
| 255 | - | // Crossing outputs marks the destination itself, even if it is empty. | 255 | + | // Crossing outputs marks the destination’s focused window, if any. |
| 256 | if sc != beforeScreen { | 256 | if sc != beforeScreen { | ||
| 257 | - | c.requestFlash(flashMonitor, 0) | 257 | + | if id != 0 { |
| 258 | + | for _, p := range sc.mon.Layout() { | |||
| 259 | + | if p.ID == id && sc.mon.viewport().shows(p) { | |||
| 260 | + | c.requestFlash(flashWindow, id) | |||
| 261 | + | break | |||
| 262 | + | } | |||
| 263 | + | } | |||
| 264 | + | } | |||
| 258 | return | 265 | return | ||
| 259 | } | 266 | } | ||
| 260 | level := flashWindow | 267 | level := flashWindow | ||
| @@ -312,6 +319,7 @@ func (c *Core) dispatchAction(a Action) Effect { | |||||
| 312 | case ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionFocusMonitorUp, ActionFocusMonitorDown: | 319 | case ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionFocusMonitorUp, ActionFocusMonitorDown: | ||
| 313 | if i := c.neighbor(monitorDirection(a)); i >= 0 { | 320 | if i := c.neighbor(monitorDirection(a)); i >= 0 { | ||
| 314 | c.focusScreen = i | 321 | c.focusScreen = i | ||
| 322 | + | c.cur().mon.focusEntry(monitorDirection(a)) | |||
| 315 | } | 323 | } | ||
| 316 | return Effect{} | 324 | return Effect{} | ||
| 317 | case ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionMoveWorkspaceToMonitorUp, ActionMoveWorkspaceToMonitorDown: | 325 | case ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionMoveWorkspaceToMonitorUp, ActionMoveWorkspaceToMonitorDown: | ||
| @@ -461,6 +469,7 @@ func (c *Core) edgeFocus(axis layoutAxis, dir int, d direction) bool { | |||||
| 461 | } | 469 | } | ||
| 462 | if i := c.neighbor(d); i >= 0 { | 470 | if i := c.neighbor(d); i >= 0 { | ||
| 463 | c.focusScreen = i | 471 | c.focusScreen = i | ||
| 472 | + | c.cur().mon.focusEntry(d) | |||
| 464 | return true | 473 | return true | ||
| 465 | } | 474 | } | ||
| 466 | return m.stepWorkspace(axis, dir) | 475 | return m.stepWorkspace(axis, dir) | ||
| @@ -661,3 +670,34 @@ func moveFloat(cur, to *Workspace, id WindowID) { | |||||
| 661 | to.Activate(id) | 670 | to.Activate(id) | ||
| 662 | to.SetFullscreen(id, true) | 671 | to.SetFullscreen(id, true) | ||
| 663 | } | 672 | } | ||
| 673 | + | ||||
| 674 | + | // focusEntry selects the visible window nearest the edge entered from d. | |||
| 675 | + | // Layout order breaks ties; an empty viewport preserves the existing focus. | |||
| 676 | + | func (m *Monitor) focusEntry(d direction) { | |||
| 677 | + | view := m.viewport() | |||
| 678 | + | var id WindowID | |||
| 679 | + | best := 0 | |||
| 680 | + | for _, p := range m.Layout() { | |||
| 681 | + | if !view.shows(p) { | |||
| 682 | + | continue | |||
| 683 | + | } | |||
| 684 | + | var distance int | |||
| 685 | + | area := view.area(p) | |||
| 686 | + | switch d { | |||
| 687 | + | case dirLeft: | |||
| 688 | + | distance = max(0, area.X+area.W-(p.Rect.X+p.Rect.W)) | |||
| 689 | + | case dirRight: | |||
| 690 | + | distance = max(0, p.Rect.X-area.X) | |||
| 691 | + | case dirUp: | |||
| 692 | + | distance = max(0, area.Y+area.H-(p.Rect.Y+p.Rect.H)) | |||
| 693 | + | case dirDown: | |||
| 694 | + | distance = max(0, p.Rect.Y-area.Y) | |||
| 695 | + | } | |||
| 696 | + | if id == 0 || distance < best { | |||
| 697 | + | id, best = p.ID, distance | |||
| 698 | + | } | |||
| 699 | + | } | |||
| 700 | + | if id != 0 { | |||
| 701 | + | m.Current().FocusID(id) | |||
| 702 | + | } | |||
| 703 | + | } | |||
M internal/core/flash.go +5 -11
| @@ -12,7 +12,6 @@ type flashLevel uint8 | |||
| 12 | 12 | const ( | |
| 13 | 13 | flashWindow flashLevel = iota | |
| 14 | 14 | flashColumn | |
| 15 | - | flashMonitor | |
| 16 | 15 | flashSettle = 150 * time.Millisecond | |
| 17 | 16 | flashRecheck = 50 * time.Millisecond | |
| 18 | 17 | flashCooldown = time.Second | |
| @@ -72,13 +71,12 @@ func (c *Core) flashTick() bool { | |||
| 72 | 71 | return false | |
| 73 | 72 | } | |
| 74 | 73 | now := c.now() | |
| 75 | - | if target.level != flashMonitor { | |
| 76 | - | owner, _ := c.screenOf(target.window) | |
| 77 | - | record := c.windows.lookup(target.window) | |
| 78 | - | if owner != sc || record.client.ID == 0 { | |
| 79 | - | return false | |
| 80 | - | } | |
| 74 | + | owner, _ := c.screenOf(target.window) | |
| 75 | + | record := c.windows.lookup(target.window) | |
| 76 | + | if owner != sc || record.client.ID == 0 { | |
| 77 | + | return false | |
| 81 | 78 | } | |
| 79 | + | ||
| 82 | 80 | wait := flashSettle - now.Sub(f.since) | |
| 83 | 81 | if wait > 0 || !sc.settled() { | |
| 84 | 82 | if wait <= 0 { | |
| @@ -143,10 +141,6 @@ func (c *Core) flashRect(target flashTarget, sc *screen) (Rect, int, bool) { | |||
| 143 | 141 | if sc.mon.ov.open { | |
| 144 | 142 | return Rect{}, 0, false | |
| 145 | 143 | } | |
| 146 | - | if target.level == flashMonitor { | |
| 147 | - | o := sc.mon.Output() | |
| 148 | - | return Rect{W: o.W, H: o.H}, 0, true | |
| 149 | - | } | |
| 150 | 144 | owner, _ := c.screenOf(target.window) | |
| 151 | 145 | if owner != sc || c.windows.lookup(target.window).client.ID == 0 { | |
| 152 | 146 | return Rect{}, 0, false | |
| @@ -12,7 +12,6 @@ type flashLevel uint8 | |||||
| 12 | const ( | 12 | const ( | ||
| 13 | flashWindow flashLevel = iota | 13 | flashWindow flashLevel = iota | ||
| 14 | flashColumn | 14 | flashColumn | ||
| 15 | - | flashMonitor | |||
| 16 | flashSettle = 150 * time.Millisecond | 15 | flashSettle = 150 * time.Millisecond | ||
| 17 | flashRecheck = 50 * time.Millisecond | 16 | flashRecheck = 50 * time.Millisecond | ||
| 18 | flashCooldown = time.Second | 17 | flashCooldown = time.Second | ||
| @@ -72,13 +71,12 @@ func (c *Core) flashTick() bool { | |||||
| 72 | return false | 71 | return false | ||
| 73 | } | 72 | } | ||
| 74 | now := c.now() | 73 | now := c.now() | ||
| 75 | - | if target.level != flashMonitor { | 74 | + | owner, _ := c.screenOf(target.window) |
| 76 | - | owner, _ := c.screenOf(target.window) | 75 | + | record := c.windows.lookup(target.window) |
| 77 | - | record := c.windows.lookup(target.window) | 76 | + | if owner != sc || record.client.ID == 0 { |
| 78 | - | if owner != sc || record.client.ID == 0 { | 77 | + | return false |
| 79 | - | return false | |||
| 80 | - | } | |||
| 81 | } | 78 | } | ||
| 79 | + | ||||
| 82 | wait := flashSettle - now.Sub(f.since) | 80 | wait := flashSettle - now.Sub(f.since) | ||
| 83 | if wait > 0 || !sc.settled() { | 81 | if wait > 0 || !sc.settled() { | ||
| 84 | if wait <= 0 { | 82 | if wait <= 0 { | ||
| @@ -143,10 +141,6 @@ func (c *Core) flashRect(target flashTarget, sc *screen) (Rect, int, bool) { | |||||
| 143 | if sc.mon.ov.open { | 141 | if sc.mon.ov.open { | ||
| 144 | return Rect{}, 0, false | 142 | return Rect{}, 0, false | ||
| 145 | } | 143 | } | ||
| 146 | - | if target.level == flashMonitor { | |||
| 147 | - | o := sc.mon.Output() | |||
| 148 | - | return Rect{W: o.W, H: o.H}, 0, true | |||
| 149 | - | } | |||
| 150 | owner, _ := c.screenOf(target.window) | 144 | owner, _ := c.screenOf(target.window) | ||
| 151 | if owner != sc || c.windows.lookup(target.window).client.ID == 0 { | 145 | if owner != sc || c.windows.lookup(target.window).client.ID == 0 { | ||
| 152 | return Rect{}, 0, false | 146 | return Rect{}, 0, false | ||
M internal/core/flash_test.go +48 -35
| @@ -98,7 +98,7 @@ func TestFlashDebounceCooldownAndReplacement(t *testing.T) { | |||
| 98 | 98 | } | |
| 99 | 99 | ||
| 100 | 100 | func TestFlashSettlesAndResolvesFinalRect(t *testing.T) { | |
| 101 | - | for _, level := range []flashLevel{flashWindow, flashColumn, flashMonitor} { | |
| 101 | + | for _, level := range []flashLevel{flashWindow, flashColumn} { | |
| 102 | 102 | t.Run(string(rune('0'+level)), func(t *testing.T) { | |
| 103 | 103 | c, clock := flashCore(t) | |
| 104 | 104 | w := c.cur().mon.Current() | |
| @@ -115,13 +115,12 @@ func TestFlashSettlesAndResolvesFinalRect(t *testing.T) { | |||
| 115 | 115 | t.Fatal("did not start after settling") | |
| 116 | 116 | } | |
| 117 | 117 | want := Rect{W: 300, H: 200} | |
| 118 | - | if level != flashMonitor { | |
| 119 | - | for _, p := range c.cur().mon.Layout() { | |
| 120 | - | if p.ID == 3 { | |
| 121 | - | want = p.Rect | |
| 122 | - | } | |
| 118 | + | for _, p := range c.cur().mon.Layout() { | |
| 119 | + | if p.ID == 3 { | |
| 120 | + | want = p.Rect | |
| 123 | 121 | } | |
| 124 | 122 | } | |
| 123 | + | ||
| 125 | 124 | if c.flash.rect != want { | |
| 126 | 125 | t.Fatalf("rect %+v want %+v", c.flash.rect, want) | |
| 127 | 126 | } | |
| @@ -232,7 +231,7 @@ func TestFlashExcludedActions(t *testing.T) { | |||
| 232 | 231 | } | |
| 233 | 232 | } | |
| 234 | 233 | ||
| 235 | - | func TestFlashMonitorTriggers(t *testing.T) { | |
| 234 | + | func TestFlashCrossOutputActionTriggers(t *testing.T) { | |
| 236 | 235 | for _, action := range []Action{ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionFocusMonitorUp, ActionFocusMonitorDown, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionMoveWorkspaceToMonitorUp, ActionMoveWorkspaceToMonitorDown} { | |
| 237 | 236 | t.Run(string(action), func(t *testing.T) { | |
| 238 | 237 | c, clock := flashCore(t) | |
| @@ -249,12 +248,23 @@ func TestFlashMonitorTriggers(t *testing.T) { | |||
| 249 | 248 | b.x, b.y = 0, 200 | |
| 250 | 249 | } | |
| 251 | 250 | c.applyAction(action) | |
| 252 | - | if c.flash.pending == nil || c.flash.pending.level != flashMonitor || c.flash.pending.output != "B" { | |
| 251 | + | id, _ := b.mon.Focused() | |
| 252 | + | if id == 0 { | |
| 253 | + | if c.flash.pending != nil { | |
| 254 | + | t.Fatalf("empty destination request %+v", c.flash.pending) | |
| 255 | + | } | |
| 256 | + | return | |
| 257 | + | } | |
| 258 | + | if c.flash.pending == nil || *c.flash.pending != (flashTarget{level: flashWindow, window: id, output: "B"}) { | |
| 253 | 259 | t.Fatalf("destination request %+v", c.flash.pending) | |
| 254 | 260 | } | |
| 255 | 261 | clock.now = clock.now.Add(flashSettle) | |
| 256 | - | if !c.flashTick() || c.flash.rect != (Rect{W: 400, H: 300}) { | |
| 257 | - | t.Fatalf("monitor rect %+v", c.flash.rect) | |
| 262 | + | if !c.flashTick() { | |
| 263 | + | t.Fatal("destination window flash did not start") | |
| 264 | + | } | |
| 265 | + | want, _, ok := c.flashRect(*c.flash.running, b) | |
| 266 | + | if !ok || c.flash.rect != want { | |
| 267 | + | t.Fatalf("window rect %+v want %+v", c.flash.rect, want) | |
| 258 | 268 | } | |
| 259 | 269 | }) | |
| 260 | 270 | } | |
| @@ -395,7 +405,7 @@ func TestFlashNoImplicitFocusTriggers(t *testing.T) { | |||
| 395 | 405 | } | |
| 396 | 406 | ||
| 397 | 407 | func TestFlashCancelsStaleRunningTargets(t *testing.T) { | |
| 398 | - | for _, level := range []flashLevel{flashWindow, flashColumn, flashMonitor} { | |
| 408 | + | for _, level := range []flashLevel{flashWindow, flashColumn} { | |
| 399 | 409 | for _, change := range []string{"overview", "output gone", "fullscreen", "unmapped", "hidden", "other output", "workspace changed"} { | |
| 400 | 410 | t.Run(string(rune('0'+level))+"/"+change, func(t *testing.T) { | |
| 401 | 411 | c, clock := flashCore(t) | |
| @@ -428,13 +438,8 @@ func TestFlashCancelsStaleRunningTargets(t *testing.T) { | |||
| 428 | 438 | } | |
| 429 | 439 | // Cancellation must not wait for the target output's next flip. | |
| 430 | 440 | scenes := stepScenes(t, c, b) | |
| 431 | - | shouldCancel := level != flashMonitor || change == "overview" || change == "output gone" | |
| 432 | - | if shouldCancel { | |
| 433 | - | if scenes["A"].Flash != nil || c.flash.running != nil || c.animating() { | |
| 434 | - | t.Fatalf("stale flash retained: %+v, animating=%v", scenes["A"].Flash, c.animating()) | |
| 435 | - | } | |
| 436 | - | } else if scenes["A"].Flash == nil || !c.animating() { | |
| 437 | - | t.Fatal("monitor flash cancelled for a window change") | |
| 441 | + | if scenes["A"].Flash != nil || c.flash.running != nil || c.animating() { | |
| 442 | + | t.Fatalf("stale flash retained: %+v, animating=%v", scenes["A"].Flash, c.animating()) | |
| 438 | 443 | } | |
| 439 | 444 | }) | |
| 440 | 445 | } | |
| @@ -458,7 +463,7 @@ func TestFlashCooldownRemembersEveryTarget(t *testing.T) { | |||
| 458 | 463 | } | |
| 459 | 464 | // Inserting after all previous starts expire prunes old target keys. | |
| 460 | 465 | clock.now = clock.now.Add(flashCooldown) | |
| 461 | - | startFlash(t, c, clock, flashMonitor, 0) | |
| 466 | + | startFlash(t, c, clock, flashWindow, 3) | |
| 462 | 467 | if len(c.flash.lastStarts) != 1 { | |
| 463 | 468 | t.Fatalf("expired cooldown entries retained: %v", c.flash.lastStarts) | |
| 464 | 469 | } | |
| @@ -596,7 +601,7 @@ func TestFlashUnchangedSampleIsReused(t *testing.T) { | |||
| 596 | 601 | } | |
| 597 | 602 | } | |
| 598 | 603 | ||
| 599 | - | func TestFlashCrossScreenActionsMarkDestinationOutput(t *testing.T) { | |
| 604 | + | func TestFlashCrossScreenActionsMarkDestinationWindow(t *testing.T) { | |
| 600 | 605 | for _, tc := range []struct { | |
| 601 | 606 | name string | |
| 602 | 607 | action Action | |
| @@ -621,7 +626,18 @@ func TestFlashCrossScreenActionsMarkDestinationOutput(t *testing.T) { | |||
| 621 | 626 | if c.cur() != b { | |
| 622 | 627 | t.Fatal("action did not cross screens") | |
| 623 | 628 | } | |
| 624 | - | if c.flash.pending == nil || *c.flash.pending != (flashTarget{level: flashMonitor, output: "B"}) { | |
| 629 | + | id, _ := b.mon.Focused() | |
| 630 | + | if id == 0 { | |
| 631 | + | if c.flash.pending != nil { | |
| 632 | + | t.Fatalf("empty destination request %+v", c.flash.pending) | |
| 633 | + | } | |
| 634 | + | clock.now = clock.now.Add(flashSettle) | |
| 635 | + | if c.flashTick() || stepScenes(t, c, b)["B"].Flash != nil { | |
| 636 | + | t.Fatal("empty destination flashed") | |
| 637 | + | } | |
| 638 | + | return | |
| 639 | + | } | |
| 640 | + | if c.flash.pending == nil || *c.flash.pending != (flashTarget{level: flashWindow, window: id, output: "B"}) { | |
| 625 | 641 | t.Fatalf("destination request %+v", c.flash.pending) | |
| 626 | 642 | } | |
| 627 | 643 | clock.now = clock.now.Add(flashSettle) | |
| @@ -633,7 +649,8 @@ func TestFlashCrossScreenActionsMarkDestinationOutput(t *testing.T) { | |||
| 633 | 649 | t.Fatal("source output flashed") | |
| 634 | 650 | } | |
| 635 | 651 | f := scenes["B"].Flash | |
| 636 | - | if f == nil || f.Rect != (Rect{W: 400, H: 300}) || !scenes["B"].Animating { | |
| 652 | + | want, _, ok := c.flashRect(*c.flash.running, b) | |
| 653 | + | if f == nil || !ok || f.Rect != want || !scenes["B"].Animating { | |
| 637 | 654 | t.Fatalf("destination scene flash %+v", f) | |
| 638 | 655 | } | |
| 639 | 656 | if !tc.occupied && tc.action != ActionMoveColumnRight && len(scenes["B"].Windows) != 0 { | |
| @@ -669,7 +686,7 @@ func TestFlashSameScreenColumnFocusKeepsColumnLevel(t *testing.T) { | |||
| 669 | 686 | } | |
| 670 | 687 | ||
| 671 | 688 | func TestFlashCancelsOnGeometryChange(t *testing.T) { | |
| 672 | - | for _, level := range []flashLevel{flashWindow, flashColumn, flashMonitor} { | |
| 689 | + | for _, level := range []flashLevel{flashWindow, flashColumn} { | |
| 673 | 690 | for _, change := range []string{"resize", "new window"} { | |
| 674 | 691 | t.Run(string(rune('0'+level))+"/"+change, func(t *testing.T) { | |
| 675 | 692 | c, clock := flashCore(t) | |
| @@ -678,9 +695,7 @@ func TestFlashCancelsOnGeometryChange(t *testing.T) { | |||
| 678 | 695 | if before.Flash == nil { | |
| 679 | 696 | t.Fatal("setup: no flash") | |
| 680 | 697 | } | |
| 681 | - | if level == flashMonitor { | |
| 682 | - | c.cur().mon.SetOutput(400, 300) | |
| 683 | - | } else if change == "resize" { | |
| 698 | + | if change == "resize" { | |
| 684 | 699 | c.cur().mon.Current().Apply(ActionCycleColumnWidth) | |
| 685 | 700 | } else { | |
| 686 | 701 | c.cur().mon.AddWindow(4) | |
| @@ -699,7 +714,7 @@ func TestFlashCancelsOnGeometryChange(t *testing.T) { | |||
| 699 | 714 | } | |
| 700 | 715 | ||
| 701 | 716 | func TestFlashGeometryRevalidationAllocations(t *testing.T) { | |
| 702 | - | for _, level := range []flashLevel{flashWindow, flashColumn, flashMonitor} { | |
| 717 | + | for _, level := range []flashLevel{flashWindow, flashColumn} { | |
| 703 | 718 | t.Run(string(rune('0'+level)), func(t *testing.T) { | |
| 704 | 719 | c, clock := flashCore(t) | |
| 705 | 720 | startFlash(t, c, clock, level, 3) | |
| @@ -727,7 +742,6 @@ func TestFlashFollowsInnerBorderAndRadius(t *testing.T) { | |||
| 727 | 742 | {"column with gaps", 12, false, flashColumn, 11}, | |
| 728 | 743 | {"tile without gaps", 0, false, flashWindow, 0}, | |
| 729 | 744 | {"float", 0, true, flashWindow, 11}, | |
| 730 | - | {"monitor", 12, false, flashMonitor, 0}, | |
| 731 | 745 | } { | |
| 732 | 746 | t.Run(test.name, func(t *testing.T) { | |
| 733 | 747 | c, clock := flashCore(t) | |
| @@ -745,16 +759,15 @@ func TestFlashFollowsInnerBorderAndRadius(t *testing.T) { | |||
| 745 | 759 | } | |
| 746 | 760 | startFlash(t, c, clock, test.level, id) | |
| 747 | 761 | want := Rect{W: 300, H: 200} | |
| 748 | - | if test.level != flashMonitor { | |
| 749 | - | for _, p := range c.cur().mon.Layout() { | |
| 750 | - | if p.ID == id { | |
| 751 | - | want = p.Rect.Inset(p.Inset, 3) | |
| 752 | - | if test.level == flashColumn { | |
| 753 | - | want = c.flash.rect | |
| 754 | - | } | |
| 762 | + | for _, p := range c.cur().mon.Layout() { | |
| 763 | + | if p.ID == id { | |
| 764 | + | want = p.Rect.Inset(p.Inset, 3) | |
| 765 | + | if test.level == flashColumn { | |
| 766 | + | want = c.flash.rect | |
| 755 | 767 | } | |
| 756 | 768 | } | |
| 757 | 769 | } | |
| 770 | + | ||
| 758 | 771 | if c.flash.rect != want || c.flash.radius != test.radius { | |
| 759 | 772 | t.Fatalf("rect %+v radius %d, want %+v radius %d", c.flash.rect, c.flash.radius, want, test.radius) | |
| 760 | 773 | } | |
| @@ -98,7 +98,7 @@ func TestFlashDebounceCooldownAndReplacement(t *testing.T) { | |||||
| 98 | } | 98 | } | ||
| 99 | 99 | ||||
| 100 | func TestFlashSettlesAndResolvesFinalRect(t *testing.T) { | 100 | func TestFlashSettlesAndResolvesFinalRect(t *testing.T) { | ||
| 101 | - | for _, level := range []flashLevel{flashWindow, flashColumn, flashMonitor} { | 101 | + | for _, level := range []flashLevel{flashWindow, flashColumn} { |
| 102 | t.Run(string(rune('0'+level)), func(t *testing.T) { | 102 | t.Run(string(rune('0'+level)), func(t *testing.T) { | ||
| 103 | c, clock := flashCore(t) | 103 | c, clock := flashCore(t) | ||
| 104 | w := c.cur().mon.Current() | 104 | w := c.cur().mon.Current() | ||
| @@ -115,13 +115,12 @@ func TestFlashSettlesAndResolvesFinalRect(t *testing.T) { | |||||
| 115 | t.Fatal("did not start after settling") | 115 | t.Fatal("did not start after settling") | ||
| 116 | } | 116 | } | ||
| 117 | want := Rect{W: 300, H: 200} | 117 | want := Rect{W: 300, H: 200} | ||
| 118 | - | if level != flashMonitor { | 118 | + | for _, p := range c.cur().mon.Layout() { |
| 119 | - | for _, p := range c.cur().mon.Layout() { | 119 | + | if p.ID == 3 { |
| 120 | - | if p.ID == 3 { | 120 | + | want = p.Rect |
| 121 | - | want = p.Rect | |||
| 122 | - | } | |||
| 123 | } | 121 | } | ||
| 124 | } | 122 | } | ||
| 123 | + | ||||
| 125 | if c.flash.rect != want { | 124 | if c.flash.rect != want { | ||
| 126 | t.Fatalf("rect %+v want %+v", c.flash.rect, want) | 125 | t.Fatalf("rect %+v want %+v", c.flash.rect, want) | ||
| 127 | } | 126 | } | ||
| @@ -232,7 +231,7 @@ func TestFlashExcludedActions(t *testing.T) { | |||||
| 232 | } | 231 | } | ||
| 233 | } | 232 | } | ||
| 234 | 233 | ||||
| 235 | - | func TestFlashMonitorTriggers(t *testing.T) { | 234 | + | func TestFlashCrossOutputActionTriggers(t *testing.T) { |
| 236 | for _, action := range []Action{ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionFocusMonitorUp, ActionFocusMonitorDown, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionMoveWorkspaceToMonitorUp, ActionMoveWorkspaceToMonitorDown} { | 235 | for _, action := range []Action{ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionFocusMonitorUp, ActionFocusMonitorDown, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionMoveWorkspaceToMonitorUp, ActionMoveWorkspaceToMonitorDown} { | ||
| 237 | t.Run(string(action), func(t *testing.T) { | 236 | t.Run(string(action), func(t *testing.T) { | ||
| 238 | c, clock := flashCore(t) | 237 | c, clock := flashCore(t) | ||
| @@ -249,12 +248,23 @@ func TestFlashMonitorTriggers(t *testing.T) { | |||||
| 249 | b.x, b.y = 0, 200 | 248 | b.x, b.y = 0, 200 | ||
| 250 | } | 249 | } | ||
| 251 | c.applyAction(action) | 250 | c.applyAction(action) | ||
| 252 | - | if c.flash.pending == nil || c.flash.pending.level != flashMonitor || c.flash.pending.output != "B" { | 251 | + | id, _ := b.mon.Focused() |
| 252 | + | if id == 0 { | |||
| 253 | + | if c.flash.pending != nil { | |||
| 254 | + | t.Fatalf("empty destination request %+v", c.flash.pending) | |||
| 255 | + | } | |||
| 256 | + | return | |||
| 257 | + | } | |||
| 258 | + | if c.flash.pending == nil || *c.flash.pending != (flashTarget{level: flashWindow, window: id, output: "B"}) { | |||
| 253 | t.Fatalf("destination request %+v", c.flash.pending) | 259 | t.Fatalf("destination request %+v", c.flash.pending) | ||
| 254 | } | 260 | } | ||
| 255 | clock.now = clock.now.Add(flashSettle) | 261 | clock.now = clock.now.Add(flashSettle) | ||
| 256 | - | if !c.flashTick() || c.flash.rect != (Rect{W: 400, H: 300}) { | 262 | + | if !c.flashTick() { |
| 257 | - | t.Fatalf("monitor rect %+v", c.flash.rect) | 263 | + | t.Fatal("destination window flash did not start") |
| 264 | + | } | |||
| 265 | + | want, _, ok := c.flashRect(*c.flash.running, b) | |||
| 266 | + | if !ok || c.flash.rect != want { | |||
| 267 | + | t.Fatalf("window rect %+v want %+v", c.flash.rect, want) | |||
| 258 | } | 268 | } | ||
| 259 | }) | 269 | }) | ||
| 260 | } | 270 | } | ||
| @@ -395,7 +405,7 @@ func TestFlashNoImplicitFocusTriggers(t *testing.T) { | |||||
| 395 | } | 405 | } | ||
| 396 | 406 | ||||
| 397 | func TestFlashCancelsStaleRunningTargets(t *testing.T) { | 407 | func TestFlashCancelsStaleRunningTargets(t *testing.T) { | ||
| 398 | - | for _, level := range []flashLevel{flashWindow, flashColumn, flashMonitor} { | 408 | + | for _, level := range []flashLevel{flashWindow, flashColumn} { |
| 399 | for _, change := range []string{"overview", "output gone", "fullscreen", "unmapped", "hidden", "other output", "workspace changed"} { | 409 | for _, change := range []string{"overview", "output gone", "fullscreen", "unmapped", "hidden", "other output", "workspace changed"} { | ||
| 400 | t.Run(string(rune('0'+level))+"/"+change, func(t *testing.T) { | 410 | t.Run(string(rune('0'+level))+"/"+change, func(t *testing.T) { | ||
| 401 | c, clock := flashCore(t) | 411 | c, clock := flashCore(t) | ||
| @@ -428,13 +438,8 @@ func TestFlashCancelsStaleRunningTargets(t *testing.T) { | |||||
| 428 | } | 438 | } | ||
| 429 | // Cancellation must not wait for the target output's next flip. | 439 | // Cancellation must not wait for the target output's next flip. | ||
| 430 | scenes := stepScenes(t, c, b) | 440 | scenes := stepScenes(t, c, b) | ||
| 431 | - | shouldCancel := level != flashMonitor || change == "overview" || change == "output gone" | 441 | + | if scenes["A"].Flash != nil || c.flash.running != nil || c.animating() { |
| 432 | - | if shouldCancel { | 442 | + | t.Fatalf("stale flash retained: %+v, animating=%v", scenes["A"].Flash, c.animating()) |
| 433 | - | if scenes["A"].Flash != nil || c.flash.running != nil || c.animating() { | |||
| 434 | - | t.Fatalf("stale flash retained: %+v, animating=%v", scenes["A"].Flash, c.animating()) | |||
| 435 | - | } | |||
| 436 | - | } else if scenes["A"].Flash == nil || !c.animating() { | |||
| 437 | - | t.Fatal("monitor flash cancelled for a window change") | |||
| 438 | } | 443 | } | ||
| 439 | }) | 444 | }) | ||
| 440 | } | 445 | } | ||
| @@ -458,7 +463,7 @@ func TestFlashCooldownRemembersEveryTarget(t *testing.T) { | |||||
| 458 | } | 463 | } | ||
| 459 | // Inserting after all previous starts expire prunes old target keys. | 464 | // Inserting after all previous starts expire prunes old target keys. | ||
| 460 | clock.now = clock.now.Add(flashCooldown) | 465 | clock.now = clock.now.Add(flashCooldown) | ||
| 461 | - | startFlash(t, c, clock, flashMonitor, 0) | 466 | + | startFlash(t, c, clock, flashWindow, 3) |
| 462 | if len(c.flash.lastStarts) != 1 { | 467 | if len(c.flash.lastStarts) != 1 { | ||
| 463 | t.Fatalf("expired cooldown entries retained: %v", c.flash.lastStarts) | 468 | t.Fatalf("expired cooldown entries retained: %v", c.flash.lastStarts) | ||
| 464 | } | 469 | } | ||
| @@ -596,7 +601,7 @@ func TestFlashUnchangedSampleIsReused(t *testing.T) { | |||||
| 596 | } | 601 | } | ||
| 597 | } | 602 | } | ||
| 598 | 603 | ||||
| 599 | - | func TestFlashCrossScreenActionsMarkDestinationOutput(t *testing.T) { | 604 | + | func TestFlashCrossScreenActionsMarkDestinationWindow(t *testing.T) { |
| 600 | for _, tc := range []struct { | 605 | for _, tc := range []struct { | ||
| 601 | name string | 606 | name string | ||
| 602 | action Action | 607 | action Action | ||
| @@ -621,7 +626,18 @@ func TestFlashCrossScreenActionsMarkDestinationOutput(t *testing.T) { | |||||
| 621 | if c.cur() != b { | 626 | if c.cur() != b { | ||
| 622 | t.Fatal("action did not cross screens") | 627 | t.Fatal("action did not cross screens") | ||
| 623 | } | 628 | } | ||
| 624 | - | if c.flash.pending == nil || *c.flash.pending != (flashTarget{level: flashMonitor, output: "B"}) { | 629 | + | id, _ := b.mon.Focused() |
| 630 | + | if id == 0 { | |||
| 631 | + | if c.flash.pending != nil { | |||
| 632 | + | t.Fatalf("empty destination request %+v", c.flash.pending) | |||
| 633 | + | } | |||
| 634 | + | clock.now = clock.now.Add(flashSettle) | |||
| 635 | + | if c.flashTick() || stepScenes(t, c, b)["B"].Flash != nil { | |||
| 636 | + | t.Fatal("empty destination flashed") | |||
| 637 | + | } | |||
| 638 | + | return | |||
| 639 | + | } | |||
| 640 | + | if c.flash.pending == nil || *c.flash.pending != (flashTarget{level: flashWindow, window: id, output: "B"}) { | |||
| 625 | t.Fatalf("destination request %+v", c.flash.pending) | 641 | t.Fatalf("destination request %+v", c.flash.pending) | ||
| 626 | } | 642 | } | ||
| 627 | clock.now = clock.now.Add(flashSettle) | 643 | clock.now = clock.now.Add(flashSettle) | ||
| @@ -633,7 +649,8 @@ func TestFlashCrossScreenActionsMarkDestinationOutput(t *testing.T) { | |||||
| 633 | t.Fatal("source output flashed") | 649 | t.Fatal("source output flashed") | ||
| 634 | } | 650 | } | ||
| 635 | f := scenes["B"].Flash | 651 | f := scenes["B"].Flash | ||
| 636 | - | if f == nil || f.Rect != (Rect{W: 400, H: 300}) || !scenes["B"].Animating { | 652 | + | want, _, ok := c.flashRect(*c.flash.running, b) |
| 653 | + | if f == nil || !ok || f.Rect != want || !scenes["B"].Animating { | |||
| 637 | t.Fatalf("destination scene flash %+v", f) | 654 | t.Fatalf("destination scene flash %+v", f) | ||
| 638 | } | 655 | } | ||
| 639 | if !tc.occupied && tc.action != ActionMoveColumnRight && len(scenes["B"].Windows) != 0 { | 656 | if !tc.occupied && tc.action != ActionMoveColumnRight && len(scenes["B"].Windows) != 0 { | ||
| @@ -669,7 +686,7 @@ func TestFlashSameScreenColumnFocusKeepsColumnLevel(t *testing.T) { | |||||
| 669 | } | 686 | } | ||
| 670 | 687 | ||||
| 671 | func TestFlashCancelsOnGeometryChange(t *testing.T) { | 688 | func TestFlashCancelsOnGeometryChange(t *testing.T) { | ||
| 672 | - | for _, level := range []flashLevel{flashWindow, flashColumn, flashMonitor} { | 689 | + | for _, level := range []flashLevel{flashWindow, flashColumn} { |
| 673 | for _, change := range []string{"resize", "new window"} { | 690 | for _, change := range []string{"resize", "new window"} { | ||
| 674 | t.Run(string(rune('0'+level))+"/"+change, func(t *testing.T) { | 691 | t.Run(string(rune('0'+level))+"/"+change, func(t *testing.T) { | ||
| 675 | c, clock := flashCore(t) | 692 | c, clock := flashCore(t) | ||
| @@ -678,9 +695,7 @@ func TestFlashCancelsOnGeometryChange(t *testing.T) { | |||||
| 678 | if before.Flash == nil { | 695 | if before.Flash == nil { | ||
| 679 | t.Fatal("setup: no flash") | 696 | t.Fatal("setup: no flash") | ||
| 680 | } | 697 | } | ||
| 681 | - | if level == flashMonitor { | 698 | + | if change == "resize" { |
| 682 | - | c.cur().mon.SetOutput(400, 300) | |||
| 683 | - | } else if change == "resize" { | |||
| 684 | c.cur().mon.Current().Apply(ActionCycleColumnWidth) | 699 | c.cur().mon.Current().Apply(ActionCycleColumnWidth) | ||
| 685 | } else { | 700 | } else { | ||
| 686 | c.cur().mon.AddWindow(4) | 701 | c.cur().mon.AddWindow(4) | ||
| @@ -699,7 +714,7 @@ func TestFlashCancelsOnGeometryChange(t *testing.T) { | |||||
| 699 | } | 714 | } | ||
| 700 | 715 | ||||
| 701 | func TestFlashGeometryRevalidationAllocations(t *testing.T) { | 716 | func TestFlashGeometryRevalidationAllocations(t *testing.T) { | ||
| 702 | - | for _, level := range []flashLevel{flashWindow, flashColumn, flashMonitor} { | 717 | + | for _, level := range []flashLevel{flashWindow, flashColumn} { |
| 703 | t.Run(string(rune('0'+level)), func(t *testing.T) { | 718 | t.Run(string(rune('0'+level)), func(t *testing.T) { | ||
| 704 | c, clock := flashCore(t) | 719 | c, clock := flashCore(t) | ||
| 705 | startFlash(t, c, clock, level, 3) | 720 | startFlash(t, c, clock, level, 3) | ||
| @@ -727,7 +742,6 @@ func TestFlashFollowsInnerBorderAndRadius(t *testing.T) { | |||||
| 727 | {"column with gaps", 12, false, flashColumn, 11}, | 742 | {"column with gaps", 12, false, flashColumn, 11}, | ||
| 728 | {"tile without gaps", 0, false, flashWindow, 0}, | 743 | {"tile without gaps", 0, false, flashWindow, 0}, | ||
| 729 | {"float", 0, true, flashWindow, 11}, | 744 | {"float", 0, true, flashWindow, 11}, | ||
| 730 | - | {"monitor", 12, false, flashMonitor, 0}, | |||
| 731 | } { | 745 | } { | ||
| 732 | t.Run(test.name, func(t *testing.T) { | 746 | t.Run(test.name, func(t *testing.T) { | ||
| 733 | c, clock := flashCore(t) | 747 | c, clock := flashCore(t) | ||
| @@ -745,16 +759,15 @@ func TestFlashFollowsInnerBorderAndRadius(t *testing.T) { | |||||
| 745 | } | 759 | } | ||
| 746 | startFlash(t, c, clock, test.level, id) | 760 | startFlash(t, c, clock, test.level, id) | ||
| 747 | want := Rect{W: 300, H: 200} | 761 | want := Rect{W: 300, H: 200} | ||
| 748 | - | if test.level != flashMonitor { | 762 | + | for _, p := range c.cur().mon.Layout() { |
| 749 | - | for _, p := range c.cur().mon.Layout() { | 763 | + | if p.ID == id { |
| 750 | - | if p.ID == id { | 764 | + | want = p.Rect.Inset(p.Inset, 3) |
| 751 | - | want = p.Rect.Inset(p.Inset, 3) | 765 | + | if test.level == flashColumn { |
| 752 | - | if test.level == flashColumn { | 766 | + | want = c.flash.rect |
| 753 | - | want = c.flash.rect | |||
| 754 | - | } | |||
| 755 | } | 767 | } | ||
| 756 | } | 768 | } | ||
| 757 | } | 769 | } | ||
| 770 | + | ||||
| 758 | if c.flash.rect != want || c.flash.radius != test.radius { | 771 | if c.flash.rect != want || c.flash.radius != test.radius { | ||
| 759 | t.Fatalf("rect %+v radius %d, want %+v radius %d", c.flash.rect, c.flash.radius, want, test.radius) | 772 | t.Fatalf("rect %+v radius %d, want %+v radius %d", c.flash.rect, c.flash.radius, want, test.radius) | ||
| 760 | } | 773 | } | ||
A internal/core/screen_entry_test.go +105 -0
| @@ -0,0 +1,105 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestCrossScreenFocusEntry(t *testing.T) { | |
| 10 | + | for _, tc := range []struct { | |
| 11 | + | action Action | |
| 12 | + | d direction | |
| 13 | + | x, y int | |
| 14 | + | want WindowID | |
| 15 | + | }{ | |
| 16 | + | {ActionFocusMonitorLeft, dirLeft, -400, 0, 5}, | |
| 17 | + | {ActionFocusMonitorRight, dirRight, 300, 0, 4}, | |
| 18 | + | {ActionFocusMonitorUp, dirUp, 0, -300, 7}, | |
| 19 | + | {ActionFocusMonitorDown, dirDown, 0, 200, 6}, | |
| 20 | + | } { | |
| 21 | + | for _, edge := range []bool{false, true} { | |
| 22 | + | t.Run(string(tc.action)+"/edge="+map[bool]string{false: "false", true: "true"}[edge], func(t *testing.T) { | |
| 23 | + | c, clock := flashCore(t) | |
| 24 | + | c.addScreen(ports.OutputInfo{Name: "B", Width: 400, Height: 300}) | |
| 25 | + | b := c.screens[1] | |
| 26 | + | b.x, b.y = tc.x, tc.y | |
| 27 | + | w := b.mon.Current() | |
| 28 | + | for i, pos := range [][2]float64{{.1, .5}, {.9, .5}, {.5, .1}, {.5, .9}} { | |
| 29 | + | id := WindowID(i + 4) | |
| 30 | + | c.windows.mapped(ports.WindowMapped{ID: id}, clock.now) | |
| 31 | + | w.addFloat(Float{ID: id, W: 40, H: 40, free: true, cx: pos[0], cy: pos[1]}, true) | |
| 32 | + | } | |
| 33 | + | w.FocusID(4) | |
| 34 | + | if edge { | |
| 35 | + | if !c.edgeFocus(horizontalAxis, 1, tc.d) { | |
| 36 | + | t.Fatal("did not cross") | |
| 37 | + | } | |
| 38 | + | } else { | |
| 39 | + | c.applyAction(tc.action) | |
| 40 | + | } | |
| 41 | + | id, _ := b.mon.Focused() | |
| 42 | + | if c.cur() != b || id != tc.want { | |
| 43 | + | t.Fatalf("screen %s focus %d want %d", c.cur().name(), id, tc.want) | |
| 44 | + | } | |
| 45 | + | if !edge && (c.flash.pending == nil || *c.flash.pending != (flashTarget{level: flashWindow, window: tc.want, output: "B"})) { | |
| 46 | + | t.Fatalf("flash %+v", c.flash.pending) | |
| 47 | + | } | |
| 48 | + | }) | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | func TestCrossScreenFocusEntryIgnoresOffscreenColumns(t *testing.T) { | |
| 54 | + | c, clock := flashCore(t) | |
| 55 | + | c.addScreen(ports.OutputInfo{Name: "B", Width: 400, Height: 300}) | |
| 56 | + | b := c.screens[1] | |
| 57 | + | w := b.mon.Current() | |
| 58 | + | for id := WindowID(4); id <= 10; id++ { | |
| 59 | + | c.windows.mapped(ports.WindowMapped{ID: id}, clock.now) | |
| 60 | + | b.mon.AddWindow(id) | |
| 61 | + | } | |
| 62 | + | // Leave focus at the right end, so the earlier columns are off-screen. | |
| 63 | + | w.view.stop() | |
| 64 | + | view := b.mon.viewport() | |
| 65 | + | var want WindowID | |
| 66 | + | best := 0 | |
| 67 | + | offscreen := false | |
| 68 | + | for _, p := range b.mon.Layout() { | |
| 69 | + | if !view.shows(p) { | |
| 70 | + | offscreen = true | |
| 71 | + | continue | |
| 72 | + | } | |
| 73 | + | if want == 0 || p.Rect.X < best { | |
| 74 | + | want, best = p.ID, p.Rect.X | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | if !offscreen || want == 0 { | |
| 78 | + | t.Fatal("setup: need both visible and off-screen columns") | |
| 79 | + | } | |
| 80 | + | c.applyAction(ActionFocusMonitorRight) | |
| 81 | + | id, _ := b.mon.Focused() | |
| 82 | + | if id != want { | |
| 83 | + | t.Fatalf("focus %d want visible leftmost %d", id, want) | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func TestCrossScreenNoVisibleWindowPreservesFocus(t *testing.T) { | |
| 88 | + | c, clock := flashCore(t) | |
| 89 | + | c.addScreen(ports.OutputInfo{Name: "B", Width: 400, Height: 300}) | |
| 90 | + | b := c.screens[1] | |
| 91 | + | w := b.mon.Current() | |
| 92 | + | c.windows.mapped(ports.WindowMapped{ID: 4}, clock.now) | |
| 93 | + | b.mon.AddWindow(4) | |
| 94 | + | w.view.off = 2000 | |
| 95 | + | for _, p := range b.mon.Layout() { | |
| 96 | + | if b.mon.viewport().shows(p) { | |
| 97 | + | t.Fatal("setup: visible window") | |
| 98 | + | } | |
| 99 | + | } | |
| 100 | + | c.applyAction(ActionFocusMonitorRight) | |
| 101 | + | id, _ := b.mon.Focused() | |
| 102 | + | if c.cur() != b || id != 4 || c.flash.pending != nil { | |
| 103 | + | t.Fatalf("focus %d flash %+v", id, c.flash.pending) | |
| 104 | + | } | |
| 105 | + | } | |
| @@ -0,0 +1,105 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestCrossScreenFocusEntry(t *testing.T) { | |||
| 10 | + | for _, tc := range []struct { | |||
| 11 | + | action Action | |||
| 12 | + | d direction | |||
| 13 | + | x, y int | |||
| 14 | + | want WindowID | |||
| 15 | + | }{ | |||
| 16 | + | {ActionFocusMonitorLeft, dirLeft, -400, 0, 5}, | |||
| 17 | + | {ActionFocusMonitorRight, dirRight, 300, 0, 4}, | |||
| 18 | + | {ActionFocusMonitorUp, dirUp, 0, -300, 7}, | |||
| 19 | + | {ActionFocusMonitorDown, dirDown, 0, 200, 6}, | |||
| 20 | + | } { | |||
| 21 | + | for _, edge := range []bool{false, true} { | |||
| 22 | + | t.Run(string(tc.action)+"/edge="+map[bool]string{false: "false", true: "true"}[edge], func(t *testing.T) { | |||
| 23 | + | c, clock := flashCore(t) | |||
| 24 | + | c.addScreen(ports.OutputInfo{Name: "B", Width: 400, Height: 300}) | |||
| 25 | + | b := c.screens[1] | |||
| 26 | + | b.x, b.y = tc.x, tc.y | |||
| 27 | + | w := b.mon.Current() | |||
| 28 | + | for i, pos := range [][2]float64{{.1, .5}, {.9, .5}, {.5, .1}, {.5, .9}} { | |||
| 29 | + | id := WindowID(i + 4) | |||
| 30 | + | c.windows.mapped(ports.WindowMapped{ID: id}, clock.now) | |||
| 31 | + | w.addFloat(Float{ID: id, W: 40, H: 40, free: true, cx: pos[0], cy: pos[1]}, true) | |||
| 32 | + | } | |||
| 33 | + | w.FocusID(4) | |||
| 34 | + | if edge { | |||
| 35 | + | if !c.edgeFocus(horizontalAxis, 1, tc.d) { | |||
| 36 | + | t.Fatal("did not cross") | |||
| 37 | + | } | |||
| 38 | + | } else { | |||
| 39 | + | c.applyAction(tc.action) | |||
| 40 | + | } | |||
| 41 | + | id, _ := b.mon.Focused() | |||
| 42 | + | if c.cur() != b || id != tc.want { | |||
| 43 | + | t.Fatalf("screen %s focus %d want %d", c.cur().name(), id, tc.want) | |||
| 44 | + | } | |||
| 45 | + | if !edge && (c.flash.pending == nil || *c.flash.pending != (flashTarget{level: flashWindow, window: tc.want, output: "B"})) { | |||
| 46 | + | t.Fatalf("flash %+v", c.flash.pending) | |||
| 47 | + | } | |||
| 48 | + | }) | |||
| 49 | + | } | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | func TestCrossScreenFocusEntryIgnoresOffscreenColumns(t *testing.T) { | |||
| 54 | + | c, clock := flashCore(t) | |||
| 55 | + | c.addScreen(ports.OutputInfo{Name: "B", Width: 400, Height: 300}) | |||
| 56 | + | b := c.screens[1] | |||
| 57 | + | w := b.mon.Current() | |||
| 58 | + | for id := WindowID(4); id <= 10; id++ { | |||
| 59 | + | c.windows.mapped(ports.WindowMapped{ID: id}, clock.now) | |||
| 60 | + | b.mon.AddWindow(id) | |||
| 61 | + | } | |||
| 62 | + | // Leave focus at the right end, so the earlier columns are off-screen. | |||
| 63 | + | w.view.stop() | |||
| 64 | + | view := b.mon.viewport() | |||
| 65 | + | var want WindowID | |||
| 66 | + | best := 0 | |||
| 67 | + | offscreen := false | |||
| 68 | + | for _, p := range b.mon.Layout() { | |||
| 69 | + | if !view.shows(p) { | |||
| 70 | + | offscreen = true | |||
| 71 | + | continue | |||
| 72 | + | } | |||
| 73 | + | if want == 0 || p.Rect.X < best { | |||
| 74 | + | want, best = p.ID, p.Rect.X | |||
| 75 | + | } | |||
| 76 | + | } | |||
| 77 | + | if !offscreen || want == 0 { | |||
| 78 | + | t.Fatal("setup: need both visible and off-screen columns") | |||
| 79 | + | } | |||
| 80 | + | c.applyAction(ActionFocusMonitorRight) | |||
| 81 | + | id, _ := b.mon.Focused() | |||
| 82 | + | if id != want { | |||
| 83 | + | t.Fatalf("focus %d want visible leftmost %d", id, want) | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | func TestCrossScreenNoVisibleWindowPreservesFocus(t *testing.T) { | |||
| 88 | + | c, clock := flashCore(t) | |||
| 89 | + | c.addScreen(ports.OutputInfo{Name: "B", Width: 400, Height: 300}) | |||
| 90 | + | b := c.screens[1] | |||
| 91 | + | w := b.mon.Current() | |||
| 92 | + | c.windows.mapped(ports.WindowMapped{ID: 4}, clock.now) | |||
| 93 | + | b.mon.AddWindow(4) | |||
| 94 | + | w.view.off = 2000 | |||
| 95 | + | for _, p := range b.mon.Layout() { | |||
| 96 | + | if b.mon.viewport().shows(p) { | |||
| 97 | + | t.Fatal("setup: visible window") | |||
| 98 | + | } | |||
| 99 | + | } | |||
| 100 | + | c.applyAction(ActionFocusMonitorRight) | |||
| 101 | + | id, _ := b.mon.Focused() | |||
| 102 | + | if c.cur() != b || id != 4 || c.flash.pending != nil { | |||
| 103 | + | t.Fatalf("focus %d flash %+v", id, c.flash.pending) | |||
| 104 | + | } | |||
| 105 | + | } | |||
M internal/core/screens_test.go +4 -0
| @@ -946,6 +946,8 @@ func TestExpelCrossesToFullNeighbor(t *testing.T) { | |||
| 946 | 946 | r.mapWindow(t, 4) | |
| 947 | 947 | r.mapWindow(t, 5) | |
| 948 | 948 | r.key(t, "Left", ports.ModAlt|ports.ModCtrl) | |
| 949 | + | // Entry now selects the top window of the rightmost column; select 3. | |
| 950 | + | r.key(t, "Down", ports.ModAlt) | |
| 949 | 951 | // Expel right at the edge: 3 stacks under 4, the first column of DP-2. | |
| 950 | 952 | set := r.key(t, "bracketright", ports.ModAlt) | |
| 951 | 953 | if got, _ := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{1, 2}) { | |
| @@ -983,6 +985,8 @@ func TestExpelOntoFixedFullscreenLeavesIt(t *testing.T) { | |||
| 983 | 985 | r.mapWindow(t, 5) | |
| 984 | 986 | r.key(t, "f", ports.ModAlt|ports.ModShift) | |
| 985 | 987 | r.key(t, "Left", ports.ModAlt|ports.ModCtrl) | |
| 988 | + | // Entry now selects the top window of the rightmost column; select 3. | |
| 989 | + | r.key(t, "Down", ports.ModAlt) | |
| 986 | 990 | // 3 joins as a tile: fullscreen ends so it is seen, focused. | |
| 987 | 991 | set := r.key(t, "bracketright", ports.ModAlt) | |
| 988 | 992 | got, focused := windowsOf(set, "DP-2") | |
| @@ -946,6 +946,8 @@ func TestExpelCrossesToFullNeighbor(t *testing.T) { | |||||
| 946 | r.mapWindow(t, 4) | 946 | r.mapWindow(t, 4) | ||
| 947 | r.mapWindow(t, 5) | 947 | r.mapWindow(t, 5) | ||
| 948 | r.key(t, "Left", ports.ModAlt|ports.ModCtrl) | 948 | r.key(t, "Left", ports.ModAlt|ports.ModCtrl) | ||
| 949 | + | // Entry now selects the top window of the rightmost column; select 3. | |||
| 950 | + | r.key(t, "Down", ports.ModAlt) | |||
| 949 | // Expel right at the edge: 3 stacks under 4, the first column of DP-2. | 951 | // Expel right at the edge: 3 stacks under 4, the first column of DP-2. | ||
| 950 | set := r.key(t, "bracketright", ports.ModAlt) | 952 | set := r.key(t, "bracketright", ports.ModAlt) | ||
| 951 | if got, _ := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{1, 2}) { | 953 | if got, _ := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{1, 2}) { | ||
| @@ -983,6 +985,8 @@ func TestExpelOntoFixedFullscreenLeavesIt(t *testing.T) { | |||||
| 983 | r.mapWindow(t, 5) | 985 | r.mapWindow(t, 5) | ||
| 984 | r.key(t, "f", ports.ModAlt|ports.ModShift) | 986 | r.key(t, "f", ports.ModAlt|ports.ModShift) | ||
| 985 | r.key(t, "Left", ports.ModAlt|ports.ModCtrl) | 987 | r.key(t, "Left", ports.ModAlt|ports.ModCtrl) | ||
| 988 | + | // Entry now selects the top window of the rightmost column; select 3. | |||
| 989 | + | r.key(t, "Down", ports.ModAlt) | |||
| 986 | // 3 joins as a tile: fullscreen ends so it is seen, focused. | 990 | // 3 joins as a tile: fullscreen ends so it is seen, focused. | ||
| 987 | set := r.key(t, "bracketright", ports.ModAlt) | 991 | set := r.key(t, "bracketright", ports.ModAlt) | ||
| 988 | got, focused := windowsOf(set, "DP-2") | 992 | got, focused := windowsOf(set, "DP-2") | ||
M internal/core/workspace_axis_test.go +2 -2
| @@ -325,7 +325,7 @@ func TestCascadeVerticalEdgeDoesNotCrossScreen(t *testing.T) { | |||
| 325 | 325 | } | |
| 326 | 326 | } | |
| 327 | 327 | ||
| 328 | - | func TestVerticalEdgeFocusFlashesDestination(t *testing.T) { | |
| 328 | + | func TestVerticalEdgeFocusEmptyDestinationDoesNotFlash(t *testing.T) { | |
| 329 | 329 | for _, tc := range []struct { | |
| 330 | 330 | a Action | |
| 331 | 331 | y int | |
| @@ -336,7 +336,7 @@ func TestVerticalEdgeFocusFlashesDestination(t *testing.T) { | |||
| 336 | 336 | c.addScreen(ports.OutputInfo{Name: "B", Width: 400, Height: 300}) | |
| 337 | 337 | c.screens[1].x, c.screens[1].y = 0, tc.y | |
| 338 | 338 | c.applyAction(tc.a) | |
| 339 | - | if c.focusScreen != 1 || c.flash.pending == nil || *c.flash.pending != (flashTarget{level: flashMonitor, output: "B"}) { | |
| 339 | + | if c.focusScreen != 1 || c.flash.pending != nil { | |
| 340 | 340 | t.Fatalf("screen %d, flash %+v", c.focusScreen, c.flash.pending) | |
| 341 | 341 | } | |
| 342 | 342 | }) | |
| @@ -325,7 +325,7 @@ func TestCascadeVerticalEdgeDoesNotCrossScreen(t *testing.T) { | |||||
| 325 | } | 325 | } | ||
| 326 | } | 326 | } | ||
| 327 | 327 | ||||
| 328 | - | func TestVerticalEdgeFocusFlashesDestination(t *testing.T) { | 328 | + | func TestVerticalEdgeFocusEmptyDestinationDoesNotFlash(t *testing.T) { |
| 329 | for _, tc := range []struct { | 329 | for _, tc := range []struct { | ||
| 330 | a Action | 330 | a Action | ||
| 331 | y int | 331 | y int | ||
| @@ -336,7 +336,7 @@ func TestVerticalEdgeFocusFlashesDestination(t *testing.T) { | |||||
| 336 | c.addScreen(ports.OutputInfo{Name: "B", Width: 400, Height: 300}) | 336 | c.addScreen(ports.OutputInfo{Name: "B", Width: 400, Height: 300}) | ||
| 337 | c.screens[1].x, c.screens[1].y = 0, tc.y | 337 | c.screens[1].x, c.screens[1].y = 0, tc.y | ||
| 338 | c.applyAction(tc.a) | 338 | c.applyAction(tc.a) | ||
| 339 | - | if c.focusScreen != 1 || c.flash.pending == nil || *c.flash.pending != (flashTarget{level: flashMonitor, output: "B"}) { | 339 | + | if c.focusScreen != 1 || c.flash.pending != nil { |
| 340 | t.Fatalf("screen %d, flash %+v", c.focusScreen, c.flash.pending) | 340 | t.Fatalf("screen %d, flash %+v", c.focusScreen, c.flash.pending) | ||
| 341 | } | 341 | } | ||
| 342 | }) | 342 | }) | ||
M internal/ports/config.go +1 -1
| @@ -154,7 +154,7 @@ type Config struct { | |||
| 154 | 154 | // moves to it. | |
| 155 | 155 | FollowMove bool | |
| 156 | 156 | // Flash briefly draws an inner border on the entity an action changed | |
| 157 | - | // (window, column, output). FlashWidth is in logical pixels (1 to 8), | |
| 157 | + | // (window or column). FlashWidth is in logical pixels (1 to 8), | |
| 158 | 158 | // FlashOpacity the starting opacity (0.1 to 1), FlashDuration the fade | |
| 159 | 159 | // (50ms to 1s, not scaled by Animations.Slowdown), FlashColor #rrggbb or | |
| 160 | 160 | // empty for Border.Active. | |
| @@ -154,7 +154,7 @@ type Config struct { | |||||
| 154 | // moves to it. | 154 | // moves to it. | ||
| 155 | FollowMove bool | 155 | FollowMove bool | ||
| 156 | // Flash briefly draws an inner border on the entity an action changed | 156 | // Flash briefly draws an inner border on the entity an action changed | ||
| 157 | - | // (window, column, output). FlashWidth is in logical pixels (1 to 8), | 157 | + | // (window or column). FlashWidth is in logical pixels (1 to 8), |
| 158 | // FlashOpacity the starting opacity (0.1 to 1), FlashDuration the fade | 158 | // FlashOpacity the starting opacity (0.1 to 1), FlashDuration the fade | ||
| 159 | // (50ms to 1s, not scaled by Animations.Slowdown), FlashColor #rrggbb or | 159 | // (50ms to 1s, not scaled by Animations.Slowdown), FlashColor #rrggbb or | ||
| 160 | // empty for Border.Active. | 160 | // empty for Border.Active. | ||
M internal/ports/scene.go +2 -2
| @@ -85,7 +85,7 @@ type Scene struct { | |||
| 85 | 85 | // then only those of Wayland clients. | |
| 86 | 86 | HintLabels []HintLabel | |
| 87 | 87 | HintStyle HintStyle | |
| 88 | - | // Flash is the focus flash drawn over everything of this output; nil | |
| 88 | + | // Flash is the focus flash highlighting a window or column on this output; nil | |
| 89 | 89 | // draws nothing. | |
| 90 | 90 | Flash *FocusFlash | |
| 91 | 91 | // PositionHint is drawn on the physical output only, never in captures. | |
| @@ -209,7 +209,7 @@ type SceneLayer struct { | |||
| 209 | 209 | } | |
| 210 | 210 | ||
| 211 | 211 | // FocusFlash is a brief inner border marking the entity an action just | |
| 212 | - | // changed: a window, a column or a whole output. Rect is in logical | |
| 212 | + | // changed: a window or a column. Rect is in logical | |
| 213 | 213 | // pixels, Width the border width inside it, Color #rrggbb and Alpha its | |
| 214 | 214 | // opacity at this frame (0 to 1). | |
| 215 | 215 | type FocusFlash struct { | |
| @@ -85,7 +85,7 @@ type Scene struct { | |||||
| 85 | // then only those of Wayland clients. | 85 | // then only those of Wayland clients. | ||
| 86 | HintLabels []HintLabel | 86 | HintLabels []HintLabel | ||
| 87 | HintStyle HintStyle | 87 | HintStyle HintStyle | ||
| 88 | - | // Flash is the focus flash drawn over everything of this output; nil | 88 | + | // Flash is the focus flash highlighting a window or column on this output; nil |
| 89 | // draws nothing. | 89 | // draws nothing. | ||
| 90 | Flash *FocusFlash | 90 | Flash *FocusFlash | ||
| 91 | // PositionHint is drawn on the physical output only, never in captures. | 91 | // PositionHint is drawn on the physical output only, never in captures. | ||
| @@ -209,7 +209,7 @@ type SceneLayer struct { | |||||
| 209 | } | 209 | } | ||
| 210 | 210 | ||||
| 211 | // FocusFlash is a brief inner border marking the entity an action just | 211 | // FocusFlash is a brief inner border marking the entity an action just | ||
| 212 | - | // changed: a window, a column or a whole output. Rect is in logical | 212 | + | // changed: a window or a column. Rect is in logical |
| 213 | // pixels, Width the border width inside it, Color #rrggbb and Alpha its | 213 | // pixels, Width the border width inside it, Color #rrggbb and Alpha its | ||
| 214 | // opacity at this frame (0 to 1). | 214 | // opacity at this frame (0 to 1). | ||
| 215 | type FocusFlash struct { | 215 | type FocusFlash struct { | ||