detached 865e07c

fix(core): keep fullscreen windows from floating or hiding

commit
865e07c76636df5cb84c2187e69fd41452ce75af
author
brice · · 2026-09-28 13:16 UTC
committer
brice · · 2026-09-28 14:44 UTC
parent
99fdfdf
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +37 -20
M docs/config.md +2 -2
@@ -90,8 +90,8 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16
9090 | `spawn-terminal` | Run `terminal` |
9191 | `close-window` | Close the focused window |
9292 | `toggle-fullscreen` | Fullscreen the focused window |
93-| `toggle-window-floating` | Float a tiled window at 80% of usable area; toggle back to its former column, width and maximized or expanded state when possible. A native dialog becomes a new tiled column |
94-| `toggle-floating-visible` | Hide all floats on this workspace when a float has the focus, or show hidden ones again; a new float shows them too |
93+| `toggle-window-floating` | Float a tiled window at 80% of usable area; toggle back to its former column, width and maximized or expanded state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first |
94+| `toggle-floating-visible` | Hide all floats on this workspace when a float has the focus and none is fullscreen, or show hidden ones again; a new float shows them too |
9595 | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column |
9696 | `cycle-column-width` | Step through `layout.presets`. In fixed overflow, toggle the focused column to `max-columns - 1` cells in place; the other columns stack on each side in the last cell. One column is expanded at a time |
9797 | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor. In fixed overflow, the neighbor on screen |
@@ -90,8 +90,8 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16
90 | `spawn-terminal` | Run `terminal` |90 | `spawn-terminal` | Run `terminal` |
91 | `close-window` | Close the focused window |91 | `close-window` | Close the focused window |
92 | `toggle-fullscreen` | Fullscreen the focused window |92 | `toggle-fullscreen` | Fullscreen the focused window |
93-| `toggle-window-floating` | Float a tiled window at 80% of usable area; toggle back to its former column, width and maximized or expanded state when possible. A native dialog becomes a new tiled column |93+| `toggle-window-floating` | Float a tiled window at 80% of usable area; toggle back to its former column, width and maximized or expanded state when possible. A native dialog becomes a new tiled column. A fullscreen window must leave fullscreen first |
94-| `toggle-floating-visible` | Hide all floats on this workspace when a float has the focus, or show hidden ones again; a new float shows them too |94+| `toggle-floating-visible` | Hide all floats on this workspace when a float has the focus and none is fullscreen, or show hidden ones again; a new float shows them too |
95 | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column |95 | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off. On a window that made itself fullscreen (e.g. a Wine app at monitor size), it first returns the window to its column |
96 | `cycle-column-width` | Step through `layout.presets`. In fixed overflow, toggle the focused column to `max-columns - 1` cells in place; the other columns stack on each side in the last cell. One column is expanded at a time |96 | `cycle-column-width` | Step through `layout.presets`. In fixed overflow, toggle the focused column to `max-columns - 1` cells in place; the other columns stack on each side in the last cell. One column is expanded at a time |
97 | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor. In fixed overflow, the neighbor on screen |97 | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor. In fixed overflow, the neighbor on screen |
M internal/core/floating_test.go +24 -8
@@ -76,6 +76,22 @@ func TestToggleFloatingVisible(t *testing.T) {
7676 }
7777 }
7878
79+// A fullscreen window does not float until it leaves fullscreen.
80+func TestToggleWindowFloatingIgnoresFullscreen(t *testing.T) {
81+ w := workspace()
82+ w.AddWindow(1)
83+ w.SetFullscreen(1, true)
84+ w.Apply(ActionToggleWindowFloating)
85+ if len(w.Floats) != 0 || w.fullscreen != 1 {
86+ t.Fatalf("fullscreen window floated: %+v", w)
87+ }
88+ w.SetFullscreen(1, false)
89+ w.Apply(ActionToggleWindowFloating)
90+ if len(w.Floats) != 1 {
91+ t.Fatal("window did not float after leaving fullscreen")
92+ }
93+}
94+
7995 // A lone maximized column comes back maximized.
8096 func TestToggleWindowFloatingKeepsFullWidth(t *testing.T) {
8197 w := workspace()
@@ -128,8 +144,7 @@ func TestHiddenFloatFullscreenRequest(t *testing.T) {
128144 }
129145 }
130146
131-// Only a focused float hides the floats; a hidden fullscreen float no
132-// longer hides the columns.
147+// Only a focused float hides the floats, and never over a fullscreen float.
133148 func TestToggleFloatingVisibleFocusAndFullscreen(t *testing.T) {
134149 w := workspace()
135150 w.Overflow = OverflowFixed
@@ -148,17 +163,18 @@ func TestToggleFloatingVisibleFocusAndFullscreen(t *testing.T) {
148163 if id, _ := w.Focused(); id != 2 || w.floatsHidden {
149164 t.Fatalf("show from column: focused %d hidden %v", id, w.floatsHidden)
150165 }
166+ // A fullscreen float (a game in scanout) stays shown: hiding it would
167+ // drop scanout and VRR.
151168 w.FocusID(2)
152169 w.SetFullscreen(2, true)
153170 w.Apply(ActionToggleFloatingVisible)
154- for _, p := range w.Layout() {
155- if p.ID == 1 && (p.Hidden || p.Rect.W == 0) {
156- t.Fatalf("column hidden under hidden fullscreen float: %+v", p)
157- }
171+ if id, _ := w.Focused(); id != 2 || w.fullscreen != 2 || w.floatsHidden {
172+ t.Fatalf("fullscreen float hidden: focused %d fullscreen %d", id, w.fullscreen)
158173 }
174+ w.FocusID(1)
159175 w.Apply(ActionToggleFloatingVisible)
160- if id, _ := w.Focused(); id != 2 || w.fullscreen != 2 {
161- t.Fatalf("restored focus %d fullscreen %d", id, w.fullscreen)
176+ if w.fullscreen != 2 || w.floatsHidden {
177+ t.Fatalf("fullscreen float hidden from column: fullscreen %d", w.fullscreen)
162178 }
163179 }
164180
@@ -76,6 +76,22 @@ func TestToggleFloatingVisible(t *testing.T) {
76 }76 }
77 }77 }
78 78
79+// A fullscreen window does not float until it leaves fullscreen.
80+func TestToggleWindowFloatingIgnoresFullscreen(t *testing.T) {
81+ w := workspace()
82+ w.AddWindow(1)
83+ w.SetFullscreen(1, true)
84+ w.Apply(ActionToggleWindowFloating)
85+ if len(w.Floats) != 0 || w.fullscreen != 1 {
86+ t.Fatalf("fullscreen window floated: %+v", w)
87+ }
88+ w.SetFullscreen(1, false)
89+ w.Apply(ActionToggleWindowFloating)
90+ if len(w.Floats) != 1 {
91+ t.Fatal("window did not float after leaving fullscreen")
92+ }
93+}
94+
79 // A lone maximized column comes back maximized.95 // A lone maximized column comes back maximized.
80 func TestToggleWindowFloatingKeepsFullWidth(t *testing.T) {96 func TestToggleWindowFloatingKeepsFullWidth(t *testing.T) {
81 w := workspace()97 w := workspace()
@@ -128,8 +144,7 @@ func TestHiddenFloatFullscreenRequest(t *testing.T) {
128 }144 }
129 }145 }
130 146
131-// Only a focused float hides the floats; a hidden fullscreen float no147+// Only a focused float hides the floats, and never over a fullscreen float.
132-// longer hides the columns.
133 func TestToggleFloatingVisibleFocusAndFullscreen(t *testing.T) {148 func TestToggleFloatingVisibleFocusAndFullscreen(t *testing.T) {
134 w := workspace()149 w := workspace()
135 w.Overflow = OverflowFixed150 w.Overflow = OverflowFixed
@@ -148,17 +163,18 @@ func TestToggleFloatingVisibleFocusAndFullscreen(t *testing.T) {
148 if id, _ := w.Focused(); id != 2 || w.floatsHidden {163 if id, _ := w.Focused(); id != 2 || w.floatsHidden {
149 t.Fatalf("show from column: focused %d hidden %v", id, w.floatsHidden)164 t.Fatalf("show from column: focused %d hidden %v", id, w.floatsHidden)
150 }165 }
166+ // A fullscreen float (a game in scanout) stays shown: hiding it would
167+ // drop scanout and VRR.
151 w.FocusID(2)168 w.FocusID(2)
152 w.SetFullscreen(2, true)169 w.SetFullscreen(2, true)
153 w.Apply(ActionToggleFloatingVisible)170 w.Apply(ActionToggleFloatingVisible)
154- for _, p := range w.Layout() {171+ if id, _ := w.Focused(); id != 2 || w.fullscreen != 2 || w.floatsHidden {
155- if p.ID == 1 && (p.Hidden || p.Rect.W == 0) {172+ t.Fatalf("fullscreen float hidden: focused %d fullscreen %d", id, w.fullscreen)
156- t.Fatalf("column hidden under hidden fullscreen float: %+v", p)
157- }
158 }173 }
174+ w.FocusID(1)
159 w.Apply(ActionToggleFloatingVisible)175 w.Apply(ActionToggleFloatingVisible)
160- if id, _ := w.Focused(); id != 2 || w.fullscreen != 2 {176+ if w.fullscreen != 2 || w.floatsHidden {
161- t.Fatalf("restored focus %d fullscreen %d", id, w.fullscreen)177+ t.Fatalf("fullscreen float hidden from column: fullscreen %d", w.fullscreen)
162 }178 }
163 }179 }
164 180
M internal/core/layout.go +11 -10
@@ -963,7 +963,9 @@ func (w *Workspace) imposedFloat(id WindowID) bool {
963963 // column and row. Native floats instead become new columns.
964964 func (w *Workspace) ToggleWindowFloating() {
965965 id, ok := w.Focused()
966- if !ok {
966+ // A fullscreen window (a Wine game in scanout) keeps its place: it
967+ // leaves fullscreen first, then may float.
968+ if !ok || id == w.fullscreen {
967969 return
968970 }
969971 if i := w.floatIndex(id); i >= 0 {
@@ -1013,21 +1015,20 @@ func (w *Workspace) ToggleWindowFloating() {
10131015 }
10141016
10151017 // ToggleFloatingVisible hides every float without changing its placement
1016-// when a float has the focus, and shows them again from any window.
1018+// when a float has the focus, and shows them again from any window. A
1019+// fullscreen float (a game in scanout) is never hidden: hiding it drops
1020+// scanout and VRR.
10171021 func (w *Workspace) ToggleFloatingVisible() {
1018- if len(w.Floats) == 0 || !w.floatsHidden && !w.floatFocus {
1022+ if len(w.Floats) == 0 {
10191023 return
10201024 }
1021- w.floatsHidden = !w.floatsHidden
1022- if w.floatsHidden {
1023- // A hidden fullscreen float would still hide the columns.
1024- w.floatFocus = false
1025- w.hiddenFullscreen = 0
1026- if w.floatIndex(w.fullscreen) >= 0 {
1027- w.hiddenFullscreen, w.fullscreen = w.fullscreen, 0
1025+ if !w.floatsHidden {
1026+ if w.floatFocus && w.floatIndex(w.fullscreen) < 0 {
1027+ w.floatsHidden, w.floatFocus, w.hiddenFullscreen = true, false, 0
10281028 }
10291029 return
10301030 }
1031+ w.floatsHidden = false
10311032 w.floatFocus = true
10321033 if w.fullscreen == 0 && w.floatIndex(w.hiddenFullscreen) >= 0 {
10331034 w.fullscreen = w.hiddenFullscreen
@@ -963,7 +963,9 @@ func (w *Workspace) imposedFloat(id WindowID) bool {
963 // column and row. Native floats instead become new columns.963 // column and row. Native floats instead become new columns.
964 func (w *Workspace) ToggleWindowFloating() {964 func (w *Workspace) ToggleWindowFloating() {
965 id, ok := w.Focused()965 id, ok := w.Focused()
966- if !ok {966+ // A fullscreen window (a Wine game in scanout) keeps its place: it
967+ // leaves fullscreen first, then may float.
968+ if !ok || id == w.fullscreen {
967 return969 return
968 }970 }
969 if i := w.floatIndex(id); i >= 0 {971 if i := w.floatIndex(id); i >= 0 {
@@ -1013,21 +1015,20 @@ func (w *Workspace) ToggleWindowFloating() {
1013 }1015 }
1014 1016
1015 // ToggleFloatingVisible hides every float without changing its placement1017 // ToggleFloatingVisible hides every float without changing its placement
1016-// when a float has the focus, and shows them again from any window.1018+// when a float has the focus, and shows them again from any window. A
1019+// fullscreen float (a game in scanout) is never hidden: hiding it drops
1020+// scanout and VRR.
1017 func (w *Workspace) ToggleFloatingVisible() {1021 func (w *Workspace) ToggleFloatingVisible() {
1018- if len(w.Floats) == 0 || !w.floatsHidden && !w.floatFocus {1022+ if len(w.Floats) == 0 {
1019 return1023 return
1020 }1024 }
1021- w.floatsHidden = !w.floatsHidden1025+ if !w.floatsHidden {
1022- if w.floatsHidden {1026+ if w.floatFocus && w.floatIndex(w.fullscreen) < 0 {
1023- // A hidden fullscreen float would still hide the columns.1027+ w.floatsHidden, w.floatFocus, w.hiddenFullscreen = true, false, 0
1024- w.floatFocus = false
1025- w.hiddenFullscreen = 0
1026- if w.floatIndex(w.fullscreen) >= 0 {
1027- w.hiddenFullscreen, w.fullscreen = w.fullscreen, 0
1028 }1028 }
1029 return1029 return
1030 }1030 }
1031+ w.floatsHidden = false
1031 w.floatFocus = true1032 w.floatFocus = true
1032 if w.fullscreen == 0 && w.floatIndex(w.hiddenFullscreen) >= 0 {1033 if w.fullscreen == 0 && w.floatIndex(w.hiddenFullscreen) >= 0 {
1033 w.fullscreen = w.hiddenFullscreen1034 w.fullscreen = w.hiddenFullscreen

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close