test(core): cover auto-fullscreen focus rules
- commit
- 2ca07b7799ef7ad9e857a1c42c53c8c89dc7c9d4
- author
- brice · · 2026-10-23 00:55 UTC
- parent
- bc208ba
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +54 -5
M docs/config.md +1 -1
| @@ -260,7 +260,7 @@ bind.XF86MonBrightnessDown = spawn brightnessctl set 5%- | |||
| 260 | 260 | | `scale-up` / `scale-down` | Zoom the display; the scale is saved to the config | | |
| 261 | 261 | | `quit` | Exit NeferWL | | |
| 262 | 262 | ||
| 263 | - | A parentless window whose first committed size is exactly the logical size of the output it is on, without asking for fullscreen (a game in borderless-windowed mode), is put fullscreen as if by `toggle-fullscreen`, without being resized, when it is the focused window of the shown workspace. Later resizes do not. | |
| 263 | + | Note on `toggle-fullscreen`: a parentless window whose first committed size is exactly the logical size of the output it is on, without asking for fullscreen (a game in borderless-windowed mode), is put fullscreen as if by `toggle-fullscreen`, without being resized, when it is the focused window of the shown workspace. Later resizes do not. | |
| 264 | 264 | ||
| 265 | 265 | ### Default binds | |
| 266 | 266 | ||
| @@ -260,7 +260,7 @@ bind.XF86MonBrightnessDown = spawn brightnessctl set 5%- | |||||
| 260 | | `scale-up` / `scale-down` | Zoom the display; the scale is saved to the config | | 260 | | `scale-up` / `scale-down` | Zoom the display; the scale is saved to the config | | ||
| 261 | | `quit` | Exit NeferWL | | 261 | | `quit` | Exit NeferWL | | ||
| 262 | 262 | ||||
| 263 | - | A parentless window whose first committed size is exactly the logical size of the output it is on, without asking for fullscreen (a game in borderless-windowed mode), is put fullscreen as if by `toggle-fullscreen`, without being resized, when it is the focused window of the shown workspace. Later resizes do not. | 263 | + | Note on `toggle-fullscreen`: a parentless window whose first committed size is exactly the logical size of the output it is on, without asking for fullscreen (a game in borderless-windowed mode), is put fullscreen as if by `toggle-fullscreen`, without being resized, when it is the focused window of the shown workspace. Later resizes do not. |
| 264 | 264 | ||||
| 265 | ### Default binds | 265 | ### Default binds | ||
| 266 | 266 | ||||
M internal/adapters/wayland/xdg.go +3 -3
| @@ -418,10 +418,10 @@ func (w *window) afterCommit() { | |||
| 418 | 418 | return | |
| 419 | 419 | } | |
| 420 | 420 | if !w.floating { | |
| 421 | - | // Core is authoritative: this prefilter matches any output, and | |
| 422 | - | // core compares with the output the window is on. | |
| 423 | 421 | // Core reads a tile's first size once, to spot a window that maps | |
| 424 | - | // at its output's size without asking for fullscreen. | |
| 422 | + | // at its output's size without asking for fullscreen. This | |
| 423 | + | // prefilter accepts any output size; core compares with the output | |
| 424 | + | // the window is on. | |
| 425 | 425 | if fw, fh := w.size(); !w.sized && fw > 0 && fh > 0 { | |
| 426 | 426 | w.sized = true | |
| 427 | 427 | if !w.xdg.server.outputSized(fw, fh) { | |
| @@ -418,10 +418,10 @@ func (w *window) afterCommit() { | |||||
| 418 | return | 418 | return | ||
| 419 | } | 419 | } | ||
| 420 | if !w.floating { | 420 | if !w.floating { | ||
| 421 | - | // Core is authoritative: this prefilter matches any output, and | |||
| 422 | - | // core compares with the output the window is on. | |||
| 423 | // Core reads a tile's first size once, to spot a window that maps | 421 | // Core reads a tile's first size once, to spot a window that maps | ||
| 424 | - | // at its output's size without asking for fullscreen. | 422 | + | // at its output's size without asking for fullscreen. This |
| 423 | + | // prefilter accepts any output size; core compares with the output | |||
| 424 | + | // the window is on. | |||
| 425 | if fw, fh := w.size(); !w.sized && fw > 0 && fh > 0 { | 425 | if fw, fh := w.size(); !w.sized && fw > 0 && fh > 0 { | ||
| 426 | w.sized = true | 426 | w.sized = true | ||
| 427 | if !w.xdg.server.outputSized(fw, fh) { | 427 | if !w.xdg.server.outputSized(fw, fh) { | ||
M internal/core/legacy_fullscreen_internal_test.go +50 -1
| @@ -120,7 +120,7 @@ func TestLegacyFullscreenRespectsFocus(t *testing.T) { | |||
| 120 | 120 | ws := sc.mon.Current() | |
| 121 | 121 | sc.mon.Focus(1) | |
| 122 | 122 | if sc.mon.Current() == ws { | |
| 123 | - | t.Skip("no second workspace") | |
| 123 | + | t.Fatal("setup: no second workspace") | |
| 124 | 124 | } | |
| 125 | 125 | o := sc.rect() | |
| 126 | 126 | c.resizeFloating(ports.WindowResized{ID: 1, Width: o.W, Height: o.H}) | |
| @@ -167,3 +167,52 @@ func TestLegacyFullscreenRemapAndOutputs(t *testing.T) { | |||
| 167 | 167 | } | |
| 168 | 168 | }) | |
| 169 | 169 | } | |
| 170 | + | ||
| 171 | + | // Only the focused window of the shown workspace is a candidate. | |
| 172 | + | func TestLegacyFullscreenNeedsFocusAtFirstSize(t *testing.T) { | |
| 173 | + | t.Run("focus moved before the commit", func(t *testing.T) { | |
| 174 | + | c, _, _ := fadeCore(t) | |
| 175 | + | sc := c.cur() | |
| 176 | + | c.mapWindow(ports.WindowMapped{ID: 1}) | |
| 177 | + | c.mapWindow(ports.WindowMapped{ID: 2}) | |
| 178 | + | w := sc.mon.Current() | |
| 179 | + | w.FocusID(1) | |
| 180 | + | o := sc.rect() | |
| 181 | + | c.resizeFloating(ports.WindowResized{ID: 2, Width: o.W, Height: o.H}) | |
| 182 | + | if w.fullscreen != 0 { | |
| 183 | + | t.Fatalf("unfocused window went fullscreen: %d", w.fullscreen) | |
| 184 | + | } | |
| 185 | + | if id, _ := w.Focused(); id != 1 { | |
| 186 | + | t.Fatalf("focus moved to %d", id) | |
| 187 | + | } | |
| 188 | + | }) | |
| 189 | + | t.Run("quiet column", func(t *testing.T) { | |
| 190 | + | c, _, _ := fadeCore(t) | |
| 191 | + | sc := c.cur() | |
| 192 | + | c.mapWindow(ports.WindowMapped{ID: 1}) | |
| 193 | + | c.windows.mapped(ports.WindowMapped{ID: 2}, c.now()) | |
| 194 | + | w := sc.mon.Current() | |
| 195 | + | w.addColumnQuiet(Column{Windows: []WindowID{2}}) | |
| 196 | + | o := sc.rect() | |
| 197 | + | c.resizeFloating(ports.WindowResized{ID: 2, Width: o.W, Height: o.H}) | |
| 198 | + | if w.fullscreen != 0 { | |
| 199 | + | t.Fatalf("quiet window went fullscreen: %d", w.fullscreen) | |
| 200 | + | } | |
| 201 | + | if id, _ := w.Focused(); id != 1 { | |
| 202 | + | t.Fatalf("focus moved to %d", id) | |
| 203 | + | } | |
| 204 | + | }) | |
| 205 | + | t.Run("quiet float", func(t *testing.T) { | |
| 206 | + | c, _, _ := fadeCore(t) | |
| 207 | + | sc := c.cur() | |
| 208 | + | c.mapWindow(ports.WindowMapped{ID: 1}) | |
| 209 | + | c.windows.mapped(ports.WindowMapped{ID: 2, Floating: true}, c.now()) | |
| 210 | + | w := sc.mon.Current() | |
| 211 | + | w.addFloat(Float{ID: 2}, true) | |
| 212 | + | o := sc.rect() | |
| 213 | + | c.resizeFloating(ports.WindowResized{ID: 2, Width: o.W, Height: o.H}) | |
| 214 | + | if w.fullscreen != 0 { | |
| 215 | + | t.Fatalf("quiet float went fullscreen: %d", w.fullscreen) | |
| 216 | + | } | |
| 217 | + | }) | |
| 218 | + | } | |
| @@ -120,7 +120,7 @@ func TestLegacyFullscreenRespectsFocus(t *testing.T) { | |||||
| 120 | ws := sc.mon.Current() | 120 | ws := sc.mon.Current() | ||
| 121 | sc.mon.Focus(1) | 121 | sc.mon.Focus(1) | ||
| 122 | if sc.mon.Current() == ws { | 122 | if sc.mon.Current() == ws { | ||
| 123 | - | t.Skip("no second workspace") | 123 | + | t.Fatal("setup: no second workspace") |
| 124 | } | 124 | } | ||
| 125 | o := sc.rect() | 125 | o := sc.rect() | ||
| 126 | c.resizeFloating(ports.WindowResized{ID: 1, Width: o.W, Height: o.H}) | 126 | c.resizeFloating(ports.WindowResized{ID: 1, Width: o.W, Height: o.H}) | ||
| @@ -167,3 +167,52 @@ func TestLegacyFullscreenRemapAndOutputs(t *testing.T) { | |||||
| 167 | } | 167 | } | ||
| 168 | }) | 168 | }) | ||
| 169 | } | 169 | } | ||
| 170 | + | ||||
| 171 | + | // Only the focused window of the shown workspace is a candidate. | |||
| 172 | + | func TestLegacyFullscreenNeedsFocusAtFirstSize(t *testing.T) { | |||
| 173 | + | t.Run("focus moved before the commit", func(t *testing.T) { | |||
| 174 | + | c, _, _ := fadeCore(t) | |||
| 175 | + | sc := c.cur() | |||
| 176 | + | c.mapWindow(ports.WindowMapped{ID: 1}) | |||
| 177 | + | c.mapWindow(ports.WindowMapped{ID: 2}) | |||
| 178 | + | w := sc.mon.Current() | |||
| 179 | + | w.FocusID(1) | |||
| 180 | + | o := sc.rect() | |||
| 181 | + | c.resizeFloating(ports.WindowResized{ID: 2, Width: o.W, Height: o.H}) | |||
| 182 | + | if w.fullscreen != 0 { | |||
| 183 | + | t.Fatalf("unfocused window went fullscreen: %d", w.fullscreen) | |||
| 184 | + | } | |||
| 185 | + | if id, _ := w.Focused(); id != 1 { | |||
| 186 | + | t.Fatalf("focus moved to %d", id) | |||
| 187 | + | } | |||
| 188 | + | }) | |||
| 189 | + | t.Run("quiet column", func(t *testing.T) { | |||
| 190 | + | c, _, _ := fadeCore(t) | |||
| 191 | + | sc := c.cur() | |||
| 192 | + | c.mapWindow(ports.WindowMapped{ID: 1}) | |||
| 193 | + | c.windows.mapped(ports.WindowMapped{ID: 2}, c.now()) | |||
| 194 | + | w := sc.mon.Current() | |||
| 195 | + | w.addColumnQuiet(Column{Windows: []WindowID{2}}) | |||
| 196 | + | o := sc.rect() | |||
| 197 | + | c.resizeFloating(ports.WindowResized{ID: 2, Width: o.W, Height: o.H}) | |||
| 198 | + | if w.fullscreen != 0 { | |||
| 199 | + | t.Fatalf("quiet window went fullscreen: %d", w.fullscreen) | |||
| 200 | + | } | |||
| 201 | + | if id, _ := w.Focused(); id != 1 { | |||
| 202 | + | t.Fatalf("focus moved to %d", id) | |||
| 203 | + | } | |||
| 204 | + | }) | |||
| 205 | + | t.Run("quiet float", func(t *testing.T) { | |||
| 206 | + | c, _, _ := fadeCore(t) | |||
| 207 | + | sc := c.cur() | |||
| 208 | + | c.mapWindow(ports.WindowMapped{ID: 1}) | |||
| 209 | + | c.windows.mapped(ports.WindowMapped{ID: 2, Floating: true}, c.now()) | |||
| 210 | + | w := sc.mon.Current() | |||
| 211 | + | w.addFloat(Float{ID: 2}, true) | |||
| 212 | + | o := sc.rect() | |||
| 213 | + | c.resizeFloating(ports.WindowResized{ID: 2, Width: o.W, Height: o.H}) | |||
| 214 | + | if w.fullscreen != 0 { | |||
| 215 | + | t.Fatalf("quiet float went fullscreen: %d", w.fullscreen) | |||
| 216 | + | } | |||
| 217 | + | }) | |||
| 218 | + | } | |||