fix(core): keep binds, moves and popup grabs off windows hidden by fullscreen
- commit
- 10fab5127ff2ec0c276e6bb9b2db6758246c158a
- author
- brice · · 2026-09-28 14:29 UTC
- committer
- brice · · 2026-09-28 14:44 UTC
- parent
- 8abeded
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +146 -5
M internal/core/action.go +3 -0
| @@ -128,6 +128,9 @@ type Effect struct { | |||
| 128 | 128 | // screen; column focus past the edge column moves to the neighbor screen; | |
| 129 | 129 | // everything else applies to the focused screen's monitor. | |
| 130 | 130 | func (c *Core) applyAction(a Action) Effect { | |
| 131 | + | // A bind acts on the window on screen: under a covering fullscreen | |
| 132 | + | // window, that is the one Focused reports. | |
| 133 | + | c.cur().mon.Current().focusCover() | |
| 131 | 134 | dir := 0 | |
| 132 | 135 | switch a { | |
| 133 | 136 | case ActionFocusMonitorLeft, ActionMoveWorkspaceLeft: | |
| @@ -128,6 +128,9 @@ type Effect struct { | |||||
| 128 | // screen; column focus past the edge column moves to the neighbor screen; | 128 | // screen; column focus past the edge column moves to the neighbor screen; | ||
| 129 | // everything else applies to the focused screen's monitor. | 129 | // everything else applies to the focused screen's monitor. | ||
| 130 | func (c *Core) applyAction(a Action) Effect { | 130 | func (c *Core) applyAction(a Action) Effect { | ||
| 131 | + | // A bind acts on the window on screen: under a covering fullscreen | |||
| 132 | + | // window, that is the one Focused reports. | |||
| 133 | + | c.cur().mon.Current().focusCover() | |||
| 131 | dir := 0 | 134 | dir := 0 | ||
| 132 | switch a { | 135 | switch a { | ||
| 133 | case ActionFocusMonitorLeft, ActionMoveWorkspaceLeft: | 136 | case ActionFocusMonitorLeft, ActionMoveWorkspaceLeft: | ||
M internal/core/floating_test.go +22 -0
| @@ -102,6 +102,28 @@ func TestExclusiveFullscreenNewWindow(t *testing.T) { | |||
| 102 | 102 | } | |
| 103 | 103 | } | |
| 104 | 104 | ||
| 105 | + | // Under a covering fullscreen float, or any fixed-overflow fullscreen, | |
| 106 | + | // focus moves inside the workspace are off and binds act on the window on | |
| 107 | + | // screen, never on a column it hides. | |
| 108 | + | func TestFullscreenPinsFocus(t *testing.T) { | |
| 109 | + | w := workspace() | |
| 110 | + | w.AddWindow(1) | |
| 111 | + | w.AddWindow(2) | |
| 112 | + | w.AddFloating(3, 20, 10) | |
| 113 | + | w.SetFullscreen(3, true) | |
| 114 | + | w.FocusColumn(-1) | |
| 115 | + | w.FocusWindow(1) | |
| 116 | + | w.focusCover() | |
| 117 | + | w.Apply(ActionMoveColumnLeft) | |
| 118 | + | w.Apply(ActionCycleColumnWidth) | |
| 119 | + | if id, _ := w.Focused(); id != 3 || w.fullscreen != 3 || w.Columns[0].Windows[0] != 1 || w.Columns[1].Width != (Width{}) { | |
| 120 | + | t.Fatalf("focused %d fullscreen %d columns %+v", id, w.fullscreen, w.Columns) | |
| 121 | + | } | |
| 122 | + | if e := w.Apply(ActionCloseWindow); e.Close != 3 { | |
| 123 | + | t.Fatalf("close %d, want the fullscreen window", e.Close) | |
| 124 | + | } | |
| 125 | + | } | |
| 126 | + | ||
| 105 | 127 | // A fullscreen window does not float until it leaves fullscreen. | |
| 106 | 128 | func TestToggleWindowFloatingIgnoresFullscreen(t *testing.T) { | |
| 107 | 129 | w := workspace() | |
| @@ -102,6 +102,28 @@ func TestExclusiveFullscreenNewWindow(t *testing.T) { | |||||
| 102 | } | 102 | } | ||
| 103 | } | 103 | } | ||
| 104 | 104 | ||||
| 105 | + | // Under a covering fullscreen float, or any fixed-overflow fullscreen, | |||
| 106 | + | // focus moves inside the workspace are off and binds act on the window on | |||
| 107 | + | // screen, never on a column it hides. | |||
| 108 | + | func TestFullscreenPinsFocus(t *testing.T) { | |||
| 109 | + | w := workspace() | |||
| 110 | + | w.AddWindow(1) | |||
| 111 | + | w.AddWindow(2) | |||
| 112 | + | w.AddFloating(3, 20, 10) | |||
| 113 | + | w.SetFullscreen(3, true) | |||
| 114 | + | w.FocusColumn(-1) | |||
| 115 | + | w.FocusWindow(1) | |||
| 116 | + | w.focusCover() | |||
| 117 | + | w.Apply(ActionMoveColumnLeft) | |||
| 118 | + | w.Apply(ActionCycleColumnWidth) | |||
| 119 | + | if id, _ := w.Focused(); id != 3 || w.fullscreen != 3 || w.Columns[0].Windows[0] != 1 || w.Columns[1].Width != (Width{}) { | |||
| 120 | + | t.Fatalf("focused %d fullscreen %d columns %+v", id, w.fullscreen, w.Columns) | |||
| 121 | + | } | |||
| 122 | + | if e := w.Apply(ActionCloseWindow); e.Close != 3 { | |||
| 123 | + | t.Fatalf("close %d, want the fullscreen window", e.Close) | |||
| 124 | + | } | |||
| 125 | + | } | |||
| 126 | + | ||||
| 105 | // A fullscreen window does not float until it leaves fullscreen. | 127 | // A fullscreen window does not float until it leaves fullscreen. | ||
| 106 | func TestToggleWindowFloatingIgnoresFullscreen(t *testing.T) { | 128 | func TestToggleWindowFloatingIgnoresFullscreen(t *testing.T) { | ||
| 107 | w := workspace() | 129 | w := workspace() | ||
M internal/core/layout.go +37 -1
| @@ -415,6 +415,9 @@ func (w *Workspace) FocusID(id WindowID) bool { | |||
| 415 | 415 | return false | |
| 416 | 416 | } | |
| 417 | 417 | func (w *Workspace) FocusColumn(dir int) { | |
| 418 | + | if w.pinned() { | |
| 419 | + | return | |
| 420 | + | } | |
| 418 | 421 | if w.floatFocus { | |
| 419 | 422 | // The first move leaves the floating window for the columns. | |
| 420 | 423 | w.floatFocus = false | |
| @@ -434,7 +437,7 @@ func (w *Workspace) FocusColumn(dir int) { | |||
| 434 | 437 | // overflow stacks columns (spiral, expanded strips), so it follows the | |
| 435 | 438 | // screen: the column on that side. | |
| 436 | 439 | func (w *Workspace) columnToward(dir int) int { | |
| 437 | - | if len(w.Columns) == 0 || (dir != -1 && dir != 1) { | |
| 440 | + | if len(w.Columns) == 0 || (dir != -1 && dir != 1) || w.pinned() { | |
| 438 | 441 | return -1 | |
| 439 | 442 | } | |
| 440 | 443 | if w.onScreenFocus() { | |
| @@ -456,6 +459,9 @@ func (w *Workspace) onScreenFocus() bool { | |||
| 456 | 459 | // Fixed overflow also stacks columns (spiral, expanded strips): past the | |
| 457 | 460 | // column edge, focus goes to the column on screen above or below. | |
| 458 | 461 | func (w *Workspace) FocusWindow(dir int) bool { | |
| 462 | + | if w.pinned() { | |
| 463 | + | return false | |
| 464 | + | } | |
| 459 | 465 | if w.floatFocus { | |
| 460 | 466 | w.floatFocus = false | |
| 461 | 467 | return true | |
| @@ -703,6 +709,36 @@ func (w *Workspace) SetMaxColumns(n int) { | |||
| 703 | 709 | w.scroll() | |
| 704 | 710 | } | |
| 705 | 711 | func (w *Workspace) gap() int { return min(w.Gaps, w.Usable.W/2, w.Usable.H/2) } | |
| 712 | + | // pinned reports whether focus moves inside the workspace are off: the | |
| 713 | + | // covering fullscreen window hides every target. Only in scroll overflow | |
| 714 | + | // does moving to a tiled neighbor scroll it off and show the target. | |
| 715 | + | // Moves to another workspace or monitor still work. | |
| 716 | + | func (w *Workspace) pinned() bool { | |
| 717 | + | c := w.cover() | |
| 718 | + | return c != 0 && (w.Overflow == OverflowFixed || w.floatIndex(c) >= 0) | |
| 719 | + | } | |
| 720 | + | ||
| 721 | + | // focusCover points the focus state at the covering fullscreen window, | |
| 722 | + | // which Focused already reports, so a user action acts on the window on | |
| 723 | + | // screen, not on one it hides. | |
| 724 | + | func (w *Workspace) focusCover() { | |
| 725 | + | c := w.cover() | |
| 726 | + | if c == 0 { | |
| 727 | + | return | |
| 728 | + | } | |
| 729 | + | if w.floatIndex(c) >= 0 { | |
| 730 | + | w.floatFocus = true | |
| 731 | + | w.FocusID(c) | |
| 732 | + | return | |
| 733 | + | } | |
| 734 | + | w.floatFocus = false | |
| 735 | + | for i, col := range w.Columns { | |
| 736 | + | if j := slices.Index(col.Windows, c); j >= 0 { | |
| 737 | + | w.Focus, w.Columns[i].Focus = i, j | |
| 738 | + | } | |
| 739 | + | } | |
| 740 | + | } | |
| 741 | + | ||
| 706 | 742 | // cover returns the fullscreen window that covers the output, or 0. The | |
| 707 | 743 | // fullscreen is exclusive: while it covers, no other window of the | |
| 708 | 744 | // workspace is drawn, so the output can scan it out. | |
| @@ -415,6 +415,9 @@ func (w *Workspace) FocusID(id WindowID) bool { | |||||
| 415 | return false | 415 | return false | ||
| 416 | } | 416 | } | ||
| 417 | func (w *Workspace) FocusColumn(dir int) { | 417 | func (w *Workspace) FocusColumn(dir int) { | ||
| 418 | + | if w.pinned() { | |||
| 419 | + | return | |||
| 420 | + | } | |||
| 418 | if w.floatFocus { | 421 | if w.floatFocus { | ||
| 419 | // The first move leaves the floating window for the columns. | 422 | // The first move leaves the floating window for the columns. | ||
| 420 | w.floatFocus = false | 423 | w.floatFocus = false | ||
| @@ -434,7 +437,7 @@ func (w *Workspace) FocusColumn(dir int) { | |||||
| 434 | // overflow stacks columns (spiral, expanded strips), so it follows the | 437 | // overflow stacks columns (spiral, expanded strips), so it follows the | ||
| 435 | // screen: the column on that side. | 438 | // screen: the column on that side. | ||
| 436 | func (w *Workspace) columnToward(dir int) int { | 439 | func (w *Workspace) columnToward(dir int) int { | ||
| 437 | - | if len(w.Columns) == 0 || (dir != -1 && dir != 1) { | 440 | + | if len(w.Columns) == 0 || (dir != -1 && dir != 1) || w.pinned() { |
| 438 | return -1 | 441 | return -1 | ||
| 439 | } | 442 | } | ||
| 440 | if w.onScreenFocus() { | 443 | if w.onScreenFocus() { | ||
| @@ -456,6 +459,9 @@ func (w *Workspace) onScreenFocus() bool { | |||||
| 456 | // Fixed overflow also stacks columns (spiral, expanded strips): past the | 459 | // Fixed overflow also stacks columns (spiral, expanded strips): past the | ||
| 457 | // column edge, focus goes to the column on screen above or below. | 460 | // column edge, focus goes to the column on screen above or below. | ||
| 458 | func (w *Workspace) FocusWindow(dir int) bool { | 461 | func (w *Workspace) FocusWindow(dir int) bool { | ||
| 462 | + | if w.pinned() { | |||
| 463 | + | return false | |||
| 464 | + | } | |||
| 459 | if w.floatFocus { | 465 | if w.floatFocus { | ||
| 460 | w.floatFocus = false | 466 | w.floatFocus = false | ||
| 461 | return true | 467 | return true | ||
| @@ -703,6 +709,36 @@ func (w *Workspace) SetMaxColumns(n int) { | |||||
| 703 | w.scroll() | 709 | w.scroll() | ||
| 704 | } | 710 | } | ||
| 705 | func (w *Workspace) gap() int { return min(w.Gaps, w.Usable.W/2, w.Usable.H/2) } | 711 | func (w *Workspace) gap() int { return min(w.Gaps, w.Usable.W/2, w.Usable.H/2) } | ||
| 712 | + | // pinned reports whether focus moves inside the workspace are off: the | |||
| 713 | + | // covering fullscreen window hides every target. Only in scroll overflow | |||
| 714 | + | // does moving to a tiled neighbor scroll it off and show the target. | |||
| 715 | + | // Moves to another workspace or monitor still work. | |||
| 716 | + | func (w *Workspace) pinned() bool { | |||
| 717 | + | c := w.cover() | |||
| 718 | + | return c != 0 && (w.Overflow == OverflowFixed || w.floatIndex(c) >= 0) | |||
| 719 | + | } | |||
| 720 | + | ||||
| 721 | + | // focusCover points the focus state at the covering fullscreen window, | |||
| 722 | + | // which Focused already reports, so a user action acts on the window on | |||
| 723 | + | // screen, not on one it hides. | |||
| 724 | + | func (w *Workspace) focusCover() { | |||
| 725 | + | c := w.cover() | |||
| 726 | + | if c == 0 { | |||
| 727 | + | return | |||
| 728 | + | } | |||
| 729 | + | if w.floatIndex(c) >= 0 { | |||
| 730 | + | w.floatFocus = true | |||
| 731 | + | w.FocusID(c) | |||
| 732 | + | return | |||
| 733 | + | } | |||
| 734 | + | w.floatFocus = false | |||
| 735 | + | for i, col := range w.Columns { | |||
| 736 | + | if j := slices.Index(col.Windows, c); j >= 0 { | |||
| 737 | + | w.Focus, w.Columns[i].Focus = i, j | |||
| 738 | + | } | |||
| 739 | + | } | |||
| 740 | + | } | |||
| 741 | + | ||||
| 706 | // cover returns the fullscreen window that covers the output, or 0. The | 742 | // cover returns the fullscreen window that covers the output, or 0. The | ||
| 707 | // fullscreen is exclusive: while it covers, no other window of the | 743 | // fullscreen is exclusive: while it covers, no other window of the | ||
| 708 | // workspace is drawn, so the output can scan it out. | 744 | // workspace is drawn, so the output can scan it out. | ||
M internal/core/monitor.go +16 -1
| @@ -503,12 +503,27 @@ func (m *Monitor) MoveToWorkspace(i int, column bool) { | |||
| 503 | 503 | if !ok || to == cur { | |
| 504 | 504 | return | |
| 505 | 505 | } | |
| 506 | + | if m.followMove && to.origin != nil { | |
| 507 | + | // The view follows the window: a fullscreen workspace would hide | |
| 508 | + | // it, so its window goes home first and the move lands there. | |
| 509 | + | origin := to.origin | |
| 510 | + | m.leaveFullscreen(to, false) | |
| 511 | + | to = origin | |
| 512 | + | if i = indexOf(m.Workspaces, to); i < 0 || to == cur { | |
| 513 | + | return | |
| 514 | + | } | |
| 515 | + | } | |
| 506 | 516 | if f := cur.floatIndex(id); f >= 0 { | |
| 507 | 517 | fl := cur.Floats[f] | |
| 508 | 518 | cur.RemoveWindow(id) | |
| 509 | 519 | to.AddFloating(id, fl.W, fl.H) | |
| 510 | 520 | to.Floats[len(to.Floats)-1] = fl | |
| 511 | - | to.FocusID(id) | |
| 521 | + | if to.origin == nil { | |
| 522 | + | // As receive: an in-place fullscreen would hide it. | |
| 523 | + | to.Activate(id) | |
| 524 | + | } else { | |
| 525 | + | to.FocusID(id) | |
| 526 | + | } | |
| 512 | 527 | } else { | |
| 513 | 528 | col := Column{Windows: []WindowID{id}} | |
| 514 | 529 | ok := true | |
| @@ -503,12 +503,27 @@ func (m *Monitor) MoveToWorkspace(i int, column bool) { | |||||
| 503 | if !ok || to == cur { | 503 | if !ok || to == cur { | ||
| 504 | return | 504 | return | ||
| 505 | } | 505 | } | ||
| 506 | + | if m.followMove && to.origin != nil { | |||
| 507 | + | // The view follows the window: a fullscreen workspace would hide | |||
| 508 | + | // it, so its window goes home first and the move lands there. | |||
| 509 | + | origin := to.origin | |||
| 510 | + | m.leaveFullscreen(to, false) | |||
| 511 | + | to = origin | |||
| 512 | + | if i = indexOf(m.Workspaces, to); i < 0 || to == cur { | |||
| 513 | + | return | |||
| 514 | + | } | |||
| 515 | + | } | |||
| 506 | if f := cur.floatIndex(id); f >= 0 { | 516 | if f := cur.floatIndex(id); f >= 0 { | ||
| 507 | fl := cur.Floats[f] | 517 | fl := cur.Floats[f] | ||
| 508 | cur.RemoveWindow(id) | 518 | cur.RemoveWindow(id) | ||
| 509 | to.AddFloating(id, fl.W, fl.H) | 519 | to.AddFloating(id, fl.W, fl.H) | ||
| 510 | to.Floats[len(to.Floats)-1] = fl | 520 | to.Floats[len(to.Floats)-1] = fl | ||
| 511 | - | to.FocusID(id) | 521 | + | if to.origin == nil { |
| 522 | + | // As receive: an in-place fullscreen would hide it. | |||
| 523 | + | to.Activate(id) | |||
| 524 | + | } else { | |||
| 525 | + | to.FocusID(id) | |||
| 526 | + | } | |||
| 512 | } else { | 527 | } else { | ||
| 513 | col := Column{Windows: []WindowID{id}} | 528 | col := Column{Windows: []WindowID{id}} | ||
| 514 | ok := true | 529 | ok := true | ||
M internal/core/monitor_test.go +32 -0
| @@ -739,3 +739,35 @@ func TestMaximizeLeavesClientFullscreen(t *testing.T) { | |||
| 739 | 739 | }) | |
| 740 | 740 | } | |
| 741 | 741 | } | |
| 742 | + | ||
| 743 | + | // A move that the view follows into a fixed-overflow fullscreen workspace | |
| 744 | + | // sends its window home first: the moved window must be seen, tiled or | |
| 745 | + | // floating. | |
| 746 | + | func TestFollowMoveIntoFullscreenLands(t *testing.T) { | |
| 747 | + | for _, floating := range []bool{false, true} { | |
| 748 | + | m := newMonitor("", "") | |
| 749 | + | m.SetOutput(100, 80) | |
| 750 | + | m.SetOverflow(OverflowFixed) | |
| 751 | + | m.SetFollowMove(true) | |
| 752 | + | m.AddWindow(1) | |
| 753 | + | m.AddWindow(2) | |
| 754 | + | m.ToggleFullscreen() // 2 on its own workspace, below 1's | |
| 755 | + | m.Focus(0) | |
| 756 | + | m.Focus(len(m.Workspaces) - 1) | |
| 757 | + | if floating { | |
| 758 | + | m.AddFloating(5, 10, 10) | |
| 759 | + | } else { | |
| 760 | + | m.AddWindow(5) | |
| 761 | + | } | |
| 762 | + | m.MoveToWorkspace(1, true) | |
| 763 | + | w := m.Current() | |
| 764 | + | if id, _ := w.Focused(); id != 5 || w.cover() != 0 { | |
| 765 | + | t.Fatalf("floating %v: focused %d cover %d, workspaces %v", floating, id, w.cover(), windows(m)) | |
| 766 | + | } | |
| 767 | + | for _, p := range m.Layout() { | |
| 768 | + | if p.ID == 5 && p.Hidden { | |
| 769 | + | t.Fatalf("floating %v: moved window hidden", floating) | |
| 770 | + | } | |
| 771 | + | } | |
| 772 | + | } | |
| 773 | + | } | |
| @@ -739,3 +739,35 @@ func TestMaximizeLeavesClientFullscreen(t *testing.T) { | |||||
| 739 | }) | 739 | }) | ||
| 740 | } | 740 | } | ||
| 741 | } | 741 | } | ||
| 742 | + | ||||
| 743 | + | // A move that the view follows into a fixed-overflow fullscreen workspace | |||
| 744 | + | // sends its window home first: the moved window must be seen, tiled or | |||
| 745 | + | // floating. | |||
| 746 | + | func TestFollowMoveIntoFullscreenLands(t *testing.T) { | |||
| 747 | + | for _, floating := range []bool{false, true} { | |||
| 748 | + | m := newMonitor("", "") | |||
| 749 | + | m.SetOutput(100, 80) | |||
| 750 | + | m.SetOverflow(OverflowFixed) | |||
| 751 | + | m.SetFollowMove(true) | |||
| 752 | + | m.AddWindow(1) | |||
| 753 | + | m.AddWindow(2) | |||
| 754 | + | m.ToggleFullscreen() // 2 on its own workspace, below 1's | |||
| 755 | + | m.Focus(0) | |||
| 756 | + | m.Focus(len(m.Workspaces) - 1) | |||
| 757 | + | if floating { | |||
| 758 | + | m.AddFloating(5, 10, 10) | |||
| 759 | + | } else { | |||
| 760 | + | m.AddWindow(5) | |||
| 761 | + | } | |||
| 762 | + | m.MoveToWorkspace(1, true) | |||
| 763 | + | w := m.Current() | |||
| 764 | + | if id, _ := w.Focused(); id != 5 || w.cover() != 0 { | |||
| 765 | + | t.Fatalf("floating %v: focused %d cover %d, workspaces %v", floating, id, w.cover(), windows(m)) | |||
| 766 | + | } | |||
| 767 | + | for _, p := range m.Layout() { | |||
| 768 | + | if p.ID == 5 && p.Hidden { | |||
| 769 | + | t.Fatalf("floating %v: moved window hidden", floating) | |||
| 770 | + | } | |||
| 771 | + | } | |||
| 772 | + | } | |||
| 773 | + | } | |||
M internal/core/popup.go +3 -1
| @@ -272,7 +272,9 @@ func (c *Core) isAncestor(a, b WindowID) bool { | |||
| 272 | 272 | // grabFocus is the topmost mapped grabbing popup, which takes the keyboard. | |
| 273 | 273 | func (c *Core) grabFocus() WindowID { | |
| 274 | 274 | for i := len(c.popupOrder) - 1; i >= 0; i-- { | |
| 275 | - | if p := c.popups[c.popupOrder[i]]; p != nil && p.grab && p.mapped { | |
| 275 | + | // A popup hidden with its parent (under a fullscreen window) has | |
| 276 | + | // no keyboard. | |
| 277 | + | if p := c.popups[c.popupOrder[i]]; p != nil && p.grab && p.mapped && c.visible(p.id) { | |
| 276 | 278 | return p.id | |
| 277 | 279 | } | |
| 278 | 280 | } | |
| @@ -272,7 +272,9 @@ func (c *Core) isAncestor(a, b WindowID) bool { | |||||
| 272 | // grabFocus is the topmost mapped grabbing popup, which takes the keyboard. | 272 | // grabFocus is the topmost mapped grabbing popup, which takes the keyboard. | ||
| 273 | func (c *Core) grabFocus() WindowID { | 273 | func (c *Core) grabFocus() WindowID { | ||
| 274 | for i := len(c.popupOrder) - 1; i >= 0; i-- { | 274 | for i := len(c.popupOrder) - 1; i >= 0; i-- { | ||
| 275 | - | if p := c.popups[c.popupOrder[i]]; p != nil && p.grab && p.mapped { | 275 | + | // A popup hidden with its parent (under a fullscreen window) has |
| 276 | + | // no keyboard. | |||
| 277 | + | if p := c.popups[c.popupOrder[i]]; p != nil && p.grab && p.mapped && c.visible(p.id) { | |||
| 276 | return p.id | 278 | return p.id | ||
| 277 | } | 279 | } | ||
| 278 | } | 280 | } | ||
M internal/core/popup_test.go +29 -0
| @@ -72,3 +72,32 @@ func TestFloatingWindows(t *testing.T) { | |||
| 72 | 72 | t.Fatalf("focus after close %d", id) | |
| 73 | 73 | } | |
| 74 | 74 | } | |
| 75 | + | ||
| 76 | + | // A grabbing popup hidden with its parent under a fullscreen window has no | |
| 77 | + | // keyboard; the fullscreen window has it. | |
| 78 | + | func TestHiddenGrabPopupLosesKeyboard(t *testing.T) { | |
| 79 | + | cfg := ports.Config{} | |
| 80 | + | cfg.Keyboard.CmdKey = "super" | |
| 81 | + | cfg.Layout.MaxColumns = 2 | |
| 82 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}) | |
| 83 | + | if err != nil { | |
| 84 | + | t.Fatal(err) | |
| 85 | + | } | |
| 86 | + | c.addScreen(ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}) | |
| 87 | + | m := c.cur().mon | |
| 88 | + | m.AddWindow(1) | |
| 89 | + | m.AddWindow(2) | |
| 90 | + | c.popups[3] = &popupState{id: 3, parent: 1, grab: true, mapped: true, rect: Rect{W: 5, H: 5}} | |
| 91 | + | c.popupOrder = append(c.popupOrder, 3) | |
| 92 | + | m.Current().FocusID(1) | |
| 93 | + | if f := c.keyboardFocus(); f != 3 { | |
| 94 | + | t.Fatalf("focus %d, want the grabbing popup", f) | |
| 95 | + | } | |
| 96 | + | m.SetFullscreen(2, true) | |
| 97 | + | // Scroll overflow: the fullscreen window covers once its column is on | |
| 98 | + | // screen. | |
| 99 | + | m.Current().FocusID(2) | |
| 100 | + | if f := c.keyboardFocus(); f != 2 { | |
| 101 | + | t.Fatalf("focus %d, want the fullscreen window", f) | |
| 102 | + | } | |
| 103 | + | } | |
| @@ -72,3 +72,32 @@ func TestFloatingWindows(t *testing.T) { | |||||
| 72 | t.Fatalf("focus after close %d", id) | 72 | t.Fatalf("focus after close %d", id) | ||
| 73 | } | 73 | } | ||
| 74 | } | 74 | } | ||
| 75 | + | ||||
| 76 | + | // A grabbing popup hidden with its parent under a fullscreen window has no | |||
| 77 | + | // keyboard; the fullscreen window has it. | |||
| 78 | + | func TestHiddenGrabPopupLosesKeyboard(t *testing.T) { | |||
| 79 | + | cfg := ports.Config{} | |||
| 80 | + | cfg.Keyboard.CmdKey = "super" | |||
| 81 | + | cfg.Layout.MaxColumns = 2 | |||
| 82 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}) | |||
| 83 | + | if err != nil { | |||
| 84 | + | t.Fatal(err) | |||
| 85 | + | } | |||
| 86 | + | c.addScreen(ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}) | |||
| 87 | + | m := c.cur().mon | |||
| 88 | + | m.AddWindow(1) | |||
| 89 | + | m.AddWindow(2) | |||
| 90 | + | c.popups[3] = &popupState{id: 3, parent: 1, grab: true, mapped: true, rect: Rect{W: 5, H: 5}} | |||
| 91 | + | c.popupOrder = append(c.popupOrder, 3) | |||
| 92 | + | m.Current().FocusID(1) | |||
| 93 | + | if f := c.keyboardFocus(); f != 3 { | |||
| 94 | + | t.Fatalf("focus %d, want the grabbing popup", f) | |||
| 95 | + | } | |||
| 96 | + | m.SetFullscreen(2, true) | |||
| 97 | + | // Scroll overflow: the fullscreen window covers once its column is on | |||
| 98 | + | // screen. | |||
| 99 | + | m.Current().FocusID(2) | |||
| 100 | + | if f := c.keyboardFocus(); f != 2 { | |||
| 101 | + | t.Fatalf("focus %d, want the fullscreen window", f) | |||
| 102 | + | } | |||
| 103 | + | } | |||
M internal/ports/messages.go +4 -2
| @@ -752,8 +752,10 @@ type Scene struct { | |||
| 752 | 752 | // the Border colors: tile lines over the tiles, under the floats; a | |
| 753 | 753 | // float's border right after the float. | |
| 754 | 754 | Separators []Separator | |
| 755 | - | // Layers are drawn in slice order: background and bottom before windows, | |
| 756 | - | // top and overlay after. A fullscreen window covers bottom and top. | |
| 755 | + | // Layers are the shown layer surfaces, drawn in slice order: background | |
| 756 | + | // and bottom before windows, top and overlay after. Core leaves out | |
| 757 | + | // those a fullscreen window hides (all but the background and a surface | |
| 758 | + | // taking the keyboard exclusively); adapters draw every layer given. | |
| 757 | 759 | // Window popups are drawn after the windows, layer popups | |
| 758 | 760 | // (SceneWindow.OverLayers) last, over every layer. | |
| 759 | 761 | Layers []SceneLayer | |
| @@ -752,8 +752,10 @@ type Scene struct { | |||||
| 752 | // the Border colors: tile lines over the tiles, under the floats; a | 752 | // the Border colors: tile lines over the tiles, under the floats; a | ||
| 753 | // float's border right after the float. | 753 | // float's border right after the float. | ||
| 754 | Separators []Separator | 754 | Separators []Separator | ||
| 755 | - | // Layers are drawn in slice order: background and bottom before windows, | 755 | + | // Layers are the shown layer surfaces, drawn in slice order: background |
| 756 | - | // top and overlay after. A fullscreen window covers bottom and top. | 756 | + | // and bottom before windows, top and overlay after. Core leaves out |
| 757 | + | // those a fullscreen window hides (all but the background and a surface | |||
| 758 | + | // taking the keyboard exclusively); adapters draw every layer given. | |||
| 757 | // Window popups are drawn after the windows, layer popups | 759 | // Window popups are drawn after the windows, layer popups | ||
| 758 | // (SceneWindow.OverLayers) last, over every layer. | 760 | // (SceneWindow.OverLayers) last, over every layer. | ||
| 759 | Layers []SceneLayer | 761 | Layers []SceneLayer | ||