fix(wayland): restore the real modifier state after hint clicks
- commit
- 1c416bd20d4ba09107f03a250b5289c25c2ec335
- author
- brice · · 2026-10-19 15:32 UTC
- parent
- 61fd749
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +58 -8
M internal/adapters/wayland/keyboard_test.go +49 -6
| @@ -267,13 +267,28 @@ func TestSetModifiersOverrideAndRestore(t *testing.T) { | |||
| 267 | 267 | set := make(chan struct{}) | |
| 268 | 268 | s.display.Do(func() { s.seat.modState = ports.ModState{Locked: capsLocked}; id = s.seat.focused; close(set) }) | |
| 269 | 269 | <-set | |
| 270 | + | // A marker command applied on the display means every command sent before | |
| 271 | + | // it was applied (they apply in order); the roundtrip then delivers the | |
| 272 | + | // events they sent. | |
| 273 | + | var pending bool | |
| 270 | 274 | wait := func() { | |
| 271 | 275 | t.Helper() | |
| 272 | - | deadline := time.Now().Add(time.Second) | |
| 273 | - | for len(commands) > 0 && time.Now().Before(deadline) { | |
| 274 | - | time.Sleep(5 * time.Millisecond) | |
| 276 | + | pending = !pending | |
| 277 | + | commands <- ports.SlotsPending{Pending: pending} | |
| 278 | + | deadline := time.Now().Add(2 * time.Second) | |
| 279 | + | for { | |
| 280 | + | var now bool | |
| 281 | + | if !s.display.Do(func() { now = s.slotsPending }) { | |
| 282 | + | t.Fatal("display stopped") | |
| 283 | + | } | |
| 284 | + | if now == pending { | |
| 285 | + | break | |
| 286 | + | } | |
| 287 | + | if time.Now().After(deadline) { | |
| 288 | + | t.Fatal("commands not applied") | |
| 289 | + | } | |
| 290 | + | time.Sleep(time.Millisecond) | |
| 275 | 291 | } | |
| 276 | - | time.Sleep(20 * time.Millisecond) | |
| 277 | 292 | roundtrip(t, target) | |
| 278 | 293 | } | |
| 279 | 294 | proxy.depress, proxy.locked = nil, nil | |
| @@ -287,14 +302,42 @@ func TestSetModifiersOverrideAndRestore(t *testing.T) { | |||
| 287 | 302 | if !slices.Equal(proxy.depress, []uint32{xkbControl | xkbShift}) || !slices.Equal(proxy.locked, []uint32{capsLocked}) { | |
| 288 | 303 | t.Fatalf("override depressed %v locked %v", proxy.depress, proxy.locked) | |
| 289 | 304 | } | |
| 290 | - | commands <- ports.SetModifiers{ID: id, Restore: true} | |
| 305 | + | done := make(chan ports.ModState, 1) | |
| 306 | + | commands <- ports.SetModifiers{ID: id, Restore: true, State: ports.ModState{Locked: capsLocked}} | |
| 291 | 307 | wait() | |
| 292 | 308 | if !slices.Equal(proxy.depress, []uint32{xkbControl | xkbShift, 0}) || !slices.Equal(proxy.locked, []uint32{capsLocked, capsLocked}) { | |
| 293 | 309 | t.Fatalf("restore depressed %v locked %v", proxy.depress, proxy.locked) | |
| 294 | 310 | } | |
| 295 | - | done := make(chan ports.ModState, 1) | |
| 296 | 311 | s.display.Do(func() { done <- s.seat.modState }) | |
| 297 | 312 | if m := <-done; m != (ports.ModState{Locked: capsLocked}) { | |
| 298 | 313 | t.Fatalf("seat state changed: %+v", m) | |
| 299 | 314 | } | |
| 300 | 315 | } | |
| 316 | + | ||
| 317 | + | // Restore carries the real state core saw, which swallowed keys (a latch a | |
| 318 | + | // hint label key consumed) never gave the seat: it is sent and adopted. | |
| 319 | + | func TestSetModifiersRestoreAdoptsCoreState(t *testing.T) { | |
| 320 | + | s, events, commands, dir := keyboardServer(t) | |
| 321 | + | target, proxy := focusedTarget(t, s, events, commands, dir) | |
| 322 | + | const shiftLatched, capsLocked = 1, 2 | |
| 323 | + | var id ports.WindowID | |
| 324 | + | set := make(chan struct{}) | |
| 325 | + | s.display.Do(func() { s.seat.modState = ports.ModState{Latched: shiftLatched}; id = s.seat.focused; close(set) }) | |
| 326 | + | <-set | |
| 327 | + | proxy.depress, proxy.locked = nil, nil | |
| 328 | + | want := ports.ModState{Locked: capsLocked} // the latch was consumed | |
| 329 | + | commands <- ports.SetModifiers{ID: id, Restore: true, State: want} | |
| 330 | + | var got ports.ModState | |
| 331 | + | deadline := time.Now().Add(2 * time.Second) | |
| 332 | + | for got != want { | |
| 333 | + | if time.Now().After(deadline) { | |
| 334 | + | t.Fatalf("seat state %+v, want %+v", got, want) | |
| 335 | + | } | |
| 336 | + | s.display.Do(func() { got = s.seat.modState }) | |
| 337 | + | time.Sleep(time.Millisecond) | |
| 338 | + | } | |
| 339 | + | roundtrip(t, target) | |
| 340 | + | if !slices.Equal(proxy.locked, []uint32{capsLocked}) || !slices.Equal(proxy.depress, []uint32{0}) { | |
| 341 | + | t.Fatalf("restore depressed %v locked %v", proxy.depress, proxy.locked) | |
| 342 | + | } | |
| 343 | + | } | |
| @@ -267,13 +267,28 @@ func TestSetModifiersOverrideAndRestore(t *testing.T) { | |||||
| 267 | set := make(chan struct{}) | 267 | set := make(chan struct{}) | ||
| 268 | s.display.Do(func() { s.seat.modState = ports.ModState{Locked: capsLocked}; id = s.seat.focused; close(set) }) | 268 | s.display.Do(func() { s.seat.modState = ports.ModState{Locked: capsLocked}; id = s.seat.focused; close(set) }) | ||
| 269 | <-set | 269 | <-set | ||
| 270 | + | // A marker command applied on the display means every command sent before | |||
| 271 | + | // it was applied (they apply in order); the roundtrip then delivers the | |||
| 272 | + | // events they sent. | |||
| 273 | + | var pending bool | |||
| 270 | wait := func() { | 274 | wait := func() { | ||
| 271 | t.Helper() | 275 | t.Helper() | ||
| 272 | - | deadline := time.Now().Add(time.Second) | 276 | + | pending = !pending |
| 273 | - | for len(commands) > 0 && time.Now().Before(deadline) { | 277 | + | commands <- ports.SlotsPending{Pending: pending} |
| 274 | - | time.Sleep(5 * time.Millisecond) | 278 | + | deadline := time.Now().Add(2 * time.Second) |
| 279 | + | for { | |||
| 280 | + | var now bool | |||
| 281 | + | if !s.display.Do(func() { now = s.slotsPending }) { | |||
| 282 | + | t.Fatal("display stopped") | |||
| 283 | + | } | |||
| 284 | + | if now == pending { | |||
| 285 | + | break | |||
| 286 | + | } | |||
| 287 | + | if time.Now().After(deadline) { | |||
| 288 | + | t.Fatal("commands not applied") | |||
| 289 | + | } | |||
| 290 | + | time.Sleep(time.Millisecond) | |||
| 275 | } | 291 | } | ||
| 276 | - | time.Sleep(20 * time.Millisecond) | |||
| 277 | roundtrip(t, target) | 292 | roundtrip(t, target) | ||
| 278 | } | 293 | } | ||
| 279 | proxy.depress, proxy.locked = nil, nil | 294 | proxy.depress, proxy.locked = nil, nil | ||
| @@ -287,14 +302,42 @@ func TestSetModifiersOverrideAndRestore(t *testing.T) { | |||||
| 287 | if !slices.Equal(proxy.depress, []uint32{xkbControl | xkbShift}) || !slices.Equal(proxy.locked, []uint32{capsLocked}) { | 302 | if !slices.Equal(proxy.depress, []uint32{xkbControl | xkbShift}) || !slices.Equal(proxy.locked, []uint32{capsLocked}) { | ||
| 288 | t.Fatalf("override depressed %v locked %v", proxy.depress, proxy.locked) | 303 | t.Fatalf("override depressed %v locked %v", proxy.depress, proxy.locked) | ||
| 289 | } | 304 | } | ||
| 290 | - | commands <- ports.SetModifiers{ID: id, Restore: true} | 305 | + | done := make(chan ports.ModState, 1) |
| 306 | + | commands <- ports.SetModifiers{ID: id, Restore: true, State: ports.ModState{Locked: capsLocked}} | |||
| 291 | wait() | 307 | wait() | ||
| 292 | if !slices.Equal(proxy.depress, []uint32{xkbControl | xkbShift, 0}) || !slices.Equal(proxy.locked, []uint32{capsLocked, capsLocked}) { | 308 | if !slices.Equal(proxy.depress, []uint32{xkbControl | xkbShift, 0}) || !slices.Equal(proxy.locked, []uint32{capsLocked, capsLocked}) { | ||
| 293 | t.Fatalf("restore depressed %v locked %v", proxy.depress, proxy.locked) | 309 | t.Fatalf("restore depressed %v locked %v", proxy.depress, proxy.locked) | ||
| 294 | } | 310 | } | ||
| 295 | - | done := make(chan ports.ModState, 1) | |||
| 296 | s.display.Do(func() { done <- s.seat.modState }) | 311 | s.display.Do(func() { done <- s.seat.modState }) | ||
| 297 | if m := <-done; m != (ports.ModState{Locked: capsLocked}) { | 312 | if m := <-done; m != (ports.ModState{Locked: capsLocked}) { | ||
| 298 | t.Fatalf("seat state changed: %+v", m) | 313 | t.Fatalf("seat state changed: %+v", m) | ||
| 299 | } | 314 | } | ||
| 300 | } | 315 | } | ||
| 316 | + | ||||
| 317 | + | // Restore carries the real state core saw, which swallowed keys (a latch a | |||
| 318 | + | // hint label key consumed) never gave the seat: it is sent and adopted. | |||
| 319 | + | func TestSetModifiersRestoreAdoptsCoreState(t *testing.T) { | |||
| 320 | + | s, events, commands, dir := keyboardServer(t) | |||
| 321 | + | target, proxy := focusedTarget(t, s, events, commands, dir) | |||
| 322 | + | const shiftLatched, capsLocked = 1, 2 | |||
| 323 | + | var id ports.WindowID | |||
| 324 | + | set := make(chan struct{}) | |||
| 325 | + | s.display.Do(func() { s.seat.modState = ports.ModState{Latched: shiftLatched}; id = s.seat.focused; close(set) }) | |||
| 326 | + | <-set | |||
| 327 | + | proxy.depress, proxy.locked = nil, nil | |||
| 328 | + | want := ports.ModState{Locked: capsLocked} // the latch was consumed | |||
| 329 | + | commands <- ports.SetModifiers{ID: id, Restore: true, State: want} | |||
| 330 | + | var got ports.ModState | |||
| 331 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 332 | + | for got != want { | |||
| 333 | + | if time.Now().After(deadline) { | |||
| 334 | + | t.Fatalf("seat state %+v, want %+v", got, want) | |||
| 335 | + | } | |||
| 336 | + | s.display.Do(func() { got = s.seat.modState }) | |||
| 337 | + | time.Sleep(time.Millisecond) | |||
| 338 | + | } | |||
| 339 | + | roundtrip(t, target) | |||
| 340 | + | if !slices.Equal(proxy.locked, []uint32{capsLocked}) || !slices.Equal(proxy.depress, []uint32{0}) { | |||
| 341 | + | t.Fatalf("restore depressed %v locked %v", proxy.depress, proxy.locked) | |||
| 342 | + | } | |||
| 343 | + | } | |||
M internal/adapters/wayland/seat.go +6 -1
| @@ -249,7 +249,7 @@ func depressedMask(m ports.Mods) uint32 { | |||
| 249 | 249 | // setModifiers overrides, then restores, the modifiers the focused window's | |
| 250 | 250 | // keyboards (or the input method grab) see. The override replaces the | |
| 251 | 251 | // depressed modifiers and keeps latched, locked and group; the seat's real | |
| 252 | - | // modState is never changed by it. | |
| 252 | + | // modState is only replaced by a Restore, with the state core carries. | |
| 253 | 253 | func (s *Server) setModifiers(c ports.SetModifiers) { | |
| 254 | 254 | _, keyboards := s.focusTarget(c.ID) | |
| 255 | 255 | if c.ID != s.seat.focused || len(keyboards) == 0 { | |
| @@ -258,6 +258,11 @@ func (s *Server) setModifiers(c ports.SetModifiers) { | |||
| 258 | 258 | } | |
| 259 | 259 | s.useKeymap(nil) | |
| 260 | 260 | s.serial++ | |
| 261 | + | if c.Restore { | |
| 262 | + | // Swallowed keys never reached the seat: core's snapshot is the | |
| 263 | + | // real state. | |
| 264 | + | s.seat.modState = c.State | |
| 265 | + | } | |
| 261 | 266 | m := s.seat.modState | |
| 262 | 267 | if !c.Restore { | |
| 263 | 268 | m.Depressed = depressedMask(c.Mods) | |
| @@ -249,7 +249,7 @@ func depressedMask(m ports.Mods) uint32 { | |||||
| 249 | // setModifiers overrides, then restores, the modifiers the focused window's | 249 | // setModifiers overrides, then restores, the modifiers the focused window's | ||
| 250 | // keyboards (or the input method grab) see. The override replaces the | 250 | // keyboards (or the input method grab) see. The override replaces the | ||
| 251 | // depressed modifiers and keeps latched, locked and group; the seat's real | 251 | // depressed modifiers and keeps latched, locked and group; the seat's real | ||
| 252 | - | // modState is never changed by it. | 252 | + | // modState is only replaced by a Restore, with the state core carries. |
| 253 | func (s *Server) setModifiers(c ports.SetModifiers) { | 253 | func (s *Server) setModifiers(c ports.SetModifiers) { | ||
| 254 | _, keyboards := s.focusTarget(c.ID) | 254 | _, keyboards := s.focusTarget(c.ID) | ||
| 255 | if c.ID != s.seat.focused || len(keyboards) == 0 { | 255 | if c.ID != s.seat.focused || len(keyboards) == 0 { | ||
| @@ -258,6 +258,11 @@ func (s *Server) setModifiers(c ports.SetModifiers) { | |||||
| 258 | } | 258 | } | ||
| 259 | s.useKeymap(nil) | 259 | s.useKeymap(nil) | ||
| 260 | s.serial++ | 260 | s.serial++ | ||
| 261 | + | if c.Restore { | |||
| 262 | + | // Swallowed keys never reached the seat: core's snapshot is the | |||
| 263 | + | // real state. | |||
| 264 | + | s.seat.modState = c.State | |||
| 265 | + | } | |||
| 261 | m := s.seat.modState | 266 | m := s.seat.modState | ||
| 262 | if !c.Restore { | 267 | if !c.Restore { | ||
| 263 | m.Depressed = depressedMask(c.Mods) | 268 | m.Depressed = depressedMask(c.Mods) | ||
M internal/ports/client_commands.go +3 -1
| @@ -293,11 +293,13 @@ type SpawnRequest struct { | |||
| 293 | 293 | // SetModifiers carries core → wayland a temporary modifier override for the | |
| 294 | 294 | // focused window's keyboards: Mods are sent as depressed modifiers (mapped | |
| 295 | 295 | // to the current keymap's masks), latched/locked/group kept from the real | |
| 296 | - | // state. Mods 0 with Restore true sends the real modifier state again. | |
| 296 | + | // state. Mods 0 with Restore true sends the real modifier state again: State | |
| 297 | + | // is that state as core last saw it, which the seat adopts. | |
| 297 | 298 | type SetModifiers struct { | |
| 298 | 299 | ID WindowID | |
| 299 | 300 | Mods Mods | |
| 300 | 301 | Restore bool | |
| 302 | + | State ModState | |
| 301 | 303 | } | |
| 302 | 304 | ||
| 303 | 305 | func (SetModifiers) clientCommand() {} | |
| @@ -293,11 +293,13 @@ type SpawnRequest struct { | |||||
| 293 | // SetModifiers carries core → wayland a temporary modifier override for the | 293 | // SetModifiers carries core → wayland a temporary modifier override for the | ||
| 294 | // focused window's keyboards: Mods are sent as depressed modifiers (mapped | 294 | // focused window's keyboards: Mods are sent as depressed modifiers (mapped | ||
| 295 | // to the current keymap's masks), latched/locked/group kept from the real | 295 | // to the current keymap's masks), latched/locked/group kept from the real | ||
| 296 | - | // state. Mods 0 with Restore true sends the real modifier state again. | 296 | + | // state. Mods 0 with Restore true sends the real modifier state again: State |
| 297 | + | // is that state as core last saw it, which the seat adopts. | |||
| 297 | type SetModifiers struct { | 298 | type SetModifiers struct { | ||
| 298 | ID WindowID | 299 | ID WindowID | ||
| 299 | Mods Mods | 300 | Mods Mods | ||
| 300 | Restore bool | 301 | Restore bool | ||
| 302 | + | State ModState | |||
| 301 | } | 303 | } | ||
| 302 | 304 | ||||
| 303 | func (SetModifiers) clientCommand() {} | 305 | func (SetModifiers) clientCommand() {} | ||