fix(wayland): skip resending a virtual keymap equal to the active one
- commit
- bc84cb3e4018ff7a89ae5e43b7bb31a46dd0d773
- author
- brice · · 2026-09-26 14:23 UTC
- parent
- 1e24fe6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +55 -4
M internal/adapters/wayland/server.go +3 -2
| @@ -101,6 +101,7 @@ type Server struct { | |||
| 101 | 101 | heldKeys map[uint32]bool | |
| 102 | 102 | keymapFD int | |
| 103 | 103 | keymapSize uint32 | |
| 104 | + | keymapText string // the seat keymap, to compare virtual keymaps with | |
| 104 | 105 | keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap | |
| 105 | 106 | repeatRate, repeatDelay int | |
| 106 | 107 | // eventMu protects only the notification queue, not display-owned window state. | |
| @@ -216,7 +217,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 216 | 217 | d.Close() | |
| 217 | 218 | return nil, e | |
| 218 | 219 | } | |
| 219 | - | s.keymapFD, s.keymapSize = fd, size | |
| 220 | + | s.keymapFD, s.keymapSize, s.keymapText = fd, size, opts.Keymap | |
| 220 | 221 | } | |
| 221 | 222 | s.cleanup = func() { | |
| 222 | 223 | if s.keymapFD >= 0 { | |
| @@ -702,7 +703,7 @@ func (s *Server) setKeymap(c ports.SetKeymap) { | |||
| 702 | 703 | if s.keymapFD >= 0 { | |
| 703 | 704 | unix.Close(s.keymapFD) | |
| 704 | 705 | } | |
| 705 | - | s.keymapFD, s.keymapSize = fd, size | |
| 706 | + | s.keymapFD, s.keymapSize, s.keymapText = fd, size, c.Keymap | |
| 706 | 707 | s.keymapOwner = nil | |
| 707 | 708 | focused := s.focused | |
| 708 | 709 | s.changeFocus(0) | |
| @@ -101,6 +101,7 @@ type Server struct { | |||||
| 101 | heldKeys map[uint32]bool | 101 | heldKeys map[uint32]bool | ||
| 102 | keymapFD int | 102 | keymapFD int | ||
| 103 | keymapSize uint32 | 103 | keymapSize uint32 | ||
| 104 | + | keymapText string // the seat keymap, to compare virtual keymaps with | |||
| 104 | keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap | 105 | keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap | ||
| 105 | repeatRate, repeatDelay int | 106 | repeatRate, repeatDelay int | ||
| 106 | // eventMu protects only the notification queue, not display-owned window state. | 107 | // eventMu protects only the notification queue, not display-owned window state. | ||
| @@ -216,7 +217,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 216 | d.Close() | 217 | d.Close() | ||
| 217 | return nil, e | 218 | return nil, e | ||
| 218 | } | 219 | } | ||
| 219 | - | s.keymapFD, s.keymapSize = fd, size | 220 | + | s.keymapFD, s.keymapSize, s.keymapText = fd, size, opts.Keymap |
| 220 | } | 221 | } | ||
| 221 | s.cleanup = func() { | 222 | s.cleanup = func() { | ||
| 222 | if s.keymapFD >= 0 { | 223 | if s.keymapFD >= 0 { | ||
| @@ -702,7 +703,7 @@ func (s *Server) setKeymap(c ports.SetKeymap) { | |||||
| 702 | if s.keymapFD >= 0 { | 703 | if s.keymapFD >= 0 { | ||
| 703 | unix.Close(s.keymapFD) | 704 | unix.Close(s.keymapFD) | ||
| 704 | } | 705 | } | ||
| 705 | - | s.keymapFD, s.keymapSize = fd, size | 706 | + | s.keymapFD, s.keymapSize, s.keymapText = fd, size, c.Keymap |
| 706 | s.keymapOwner = nil | 707 | s.keymapOwner = nil | ||
| 707 | focused := s.focused | 708 | focused := s.focused | ||
| 708 | s.changeFocus(0) | 709 | s.changeFocus(0) | ||
M internal/adapters/wayland/virtualkeyboard.go +20 -2
| @@ -22,6 +22,7 @@ type virtualKeyboard struct { | |||
| 22 | 22 | server *Server | |
| 23 | 23 | fd int // sealed copy of the client keymap, -1 before one | |
| 24 | 24 | size uint32 | |
| 25 | + | text string // the keymap, compared with the active one | |
| 25 | 26 | // pressed are keys held on focus; a focus change releases them by leave. | |
| 26 | 27 | pressed map[uint32]bool | |
| 27 | 28 | focus ports.WindowID | |
| @@ -65,7 +66,9 @@ func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format | |||
| 65 | 66 | unix.Close(k.fd) | |
| 66 | 67 | } | |
| 67 | 68 | k.fd, k.size = copyFD, copySize | |
| 68 | - | if s := k.server; s.keymapOwner == k { | |
| 69 | + | old := k.server.activeKeymapText() | |
| 70 | + | k.text = text | |
| 71 | + | if s := k.server; s.keymapOwner == k && text != old { | |
| 69 | 72 | // Clients must see the new layout before the next key. | |
| 70 | 73 | s.sendKeymapAll(k.fd, k.size) | |
| 71 | 74 | } | |
| @@ -164,8 +167,15 @@ func (s *Server) useKeymap(k *virtualKeyboard) { | |||
| 164 | 167 | if s.keymapOwner == k { | |
| 165 | 168 | return | |
| 166 | 169 | } | |
| 170 | + | old := s.activeKeymapText() | |
| 167 | 171 | s.keymapOwner = k | |
| 168 | - | s.sendKeymapAll(s.currentKeymap()) | |
| 172 | + | // Like wlroots and smithay, a keymap equal to the active one is not | |
| 173 | + | // sent again: tools such as dictation apps create a virtual keyboard | |
| 174 | + | // with the seat layout per use, and each resend makes every client | |
| 175 | + | // (Xwayland included) recompile its keymap. | |
| 176 | + | if s.activeKeymapText() != old { | |
| 177 | + | s.sendKeymapAll(s.currentKeymap()) | |
| 178 | + | } | |
| 169 | 179 | if k == nil { | |
| 170 | 180 | s.serial++ | |
| 171 | 181 | for _, kb := range s.clientKeyboards(s.focusClient()) { | |
| @@ -185,6 +195,14 @@ func (s *Server) sendKeymapAll(fd int, size uint32) { | |||
| 185 | 195 | } | |
| 186 | 196 | } | |
| 187 | 197 | ||
| 198 | + | // activeKeymapText is the text of the keymap keyboards carry. | |
| 199 | + | func (s *Server) activeKeymapText() string { | |
| 200 | + | if k := s.keymapOwner; k != nil { | |
| 201 | + | return k.text | |
| 202 | + | } | |
| 203 | + | return s.keymapText | |
| 204 | + | } | |
| 205 | + | ||
| 188 | 206 | // currentKeymap is the keymap a new keyboard must start with. | |
| 189 | 207 | func (s *Server) currentKeymap() (int, uint32) { | |
| 190 | 208 | if k := s.keymapOwner; k != nil { | |
| @@ -22,6 +22,7 @@ type virtualKeyboard struct { | |||||
| 22 | server *Server | 22 | server *Server | ||
| 23 | fd int // sealed copy of the client keymap, -1 before one | 23 | fd int // sealed copy of the client keymap, -1 before one | ||
| 24 | size uint32 | 24 | size uint32 | ||
| 25 | + | text string // the keymap, compared with the active one | |||
| 25 | // pressed are keys held on focus; a focus change releases them by leave. | 26 | // pressed are keys held on focus; a focus change releases them by leave. | ||
| 26 | pressed map[uint32]bool | 27 | pressed map[uint32]bool | ||
| 27 | focus ports.WindowID | 28 | focus ports.WindowID | ||
| @@ -65,7 +66,9 @@ func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format | |||||
| 65 | unix.Close(k.fd) | 66 | unix.Close(k.fd) | ||
| 66 | } | 67 | } | ||
| 67 | k.fd, k.size = copyFD, copySize | 68 | k.fd, k.size = copyFD, copySize | ||
| 68 | - | if s := k.server; s.keymapOwner == k { | 69 | + | old := k.server.activeKeymapText() |
| 70 | + | k.text = text | |||
| 71 | + | if s := k.server; s.keymapOwner == k && text != old { | |||
| 69 | // Clients must see the new layout before the next key. | 72 | // Clients must see the new layout before the next key. | ||
| 70 | s.sendKeymapAll(k.fd, k.size) | 73 | s.sendKeymapAll(k.fd, k.size) | ||
| 71 | } | 74 | } | ||
| @@ -164,8 +167,15 @@ func (s *Server) useKeymap(k *virtualKeyboard) { | |||||
| 164 | if s.keymapOwner == k { | 167 | if s.keymapOwner == k { | ||
| 165 | return | 168 | return | ||
| 166 | } | 169 | } | ||
| 170 | + | old := s.activeKeymapText() | |||
| 167 | s.keymapOwner = k | 171 | s.keymapOwner = k | ||
| 168 | - | s.sendKeymapAll(s.currentKeymap()) | 172 | + | // Like wlroots and smithay, a keymap equal to the active one is not |
| 173 | + | // sent again: tools such as dictation apps create a virtual keyboard | |||
| 174 | + | // with the seat layout per use, and each resend makes every client | |||
| 175 | + | // (Xwayland included) recompile its keymap. | |||
| 176 | + | if s.activeKeymapText() != old { | |||
| 177 | + | s.sendKeymapAll(s.currentKeymap()) | |||
| 178 | + | } | |||
| 169 | if k == nil { | 179 | if k == nil { | ||
| 170 | s.serial++ | 180 | s.serial++ | ||
| 171 | for _, kb := range s.clientKeyboards(s.focusClient()) { | 181 | for _, kb := range s.clientKeyboards(s.focusClient()) { | ||
| @@ -185,6 +195,14 @@ func (s *Server) sendKeymapAll(fd int, size uint32) { | |||||
| 185 | } | 195 | } | ||
| 186 | } | 196 | } | ||
| 187 | 197 | ||||
| 198 | + | // activeKeymapText is the text of the keymap keyboards carry. | |||
| 199 | + | func (s *Server) activeKeymapText() string { | |||
| 200 | + | if k := s.keymapOwner; k != nil { | |||
| 201 | + | return k.text | |||
| 202 | + | } | |||
| 203 | + | return s.keymapText | |||
| 204 | + | } | |||
| 205 | + | ||||
| 188 | // currentKeymap is the keymap a new keyboard must start with. | 206 | // currentKeymap is the keymap a new keyboard must start with. | ||
| 189 | func (s *Server) currentKeymap() (int, uint32) { | 207 | func (s *Server) currentKeymap() (int, uint32) { | ||
| 190 | if k := s.keymapOwner; k != nil { | 208 | if k := s.keymapOwner; k != nil { | ||
M internal/adapters/wayland/virtualkeyboard_test.go +32 -0
| @@ -180,3 +180,35 @@ func TestVirtualKeyBeforeKeymap(t *testing.T) { | |||
| 180 | 180 | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | |
| 181 | 181 | expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap)) | |
| 182 | 182 | } | |
| 183 | + | ||
| 184 | + | // A virtual keyboard with the seat keymap (dictation tools, wtype) sends | |
| 185 | + | // no keymap to clients: only its keys, then no keymap on the switch back. | |
| 186 | + | func TestVirtualKeyboardSameKeymapNotResent(t *testing.T) { | |
| 187 | + | s, events, commands, dir := keyboardServer(t) | |
| 188 | + | target, proxy := focusedTarget(t, s, events, commands, dir) | |
| 189 | + | for range 2 { // one typer per use, like a dictation app | |
| 190 | + | typer, kb := virtualTyper(t, s, dir) | |
| 191 | + | sendVirtualKeymap(t, typer, kb, keymapText(t)) | |
| 192 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | |
| 193 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0)) | |
| 194 | + | roundtrip(t, typer, target) | |
| 195 | + | _ = typer.Close() | |
| 196 | + | } | |
| 197 | + | deadline := time.Now().Add(200 * time.Millisecond) | |
| 198 | + | for time.Now().Before(deadline) { | |
| 199 | + | roundtrip(t, target) | |
| 200 | + | time.Sleep(5 * time.Millisecond) | |
| 201 | + | } | |
| 202 | + | if len(proxy.keymaps) != 0 { | |
| 203 | + | t.Fatalf("%d keymaps sent for a virtual keyboard with the seat keymap", len(proxy.keymaps)) | |
| 204 | + | } | |
| 205 | + | keys := 0 | |
| 206 | + | for _, o := range proxy.opcodes { | |
| 207 | + | if o == uint16(wayland.KeyboardEventKey) { | |
| 208 | + | keys++ | |
| 209 | + | } | |
| 210 | + | } | |
| 211 | + | if keys != 4 { | |
| 212 | + | t.Fatalf("keys %d, want 4 (events %v)", keys, proxy.opcodes) | |
| 213 | + | } | |
| 214 | + | } | |
| @@ -180,3 +180,35 @@ func TestVirtualKeyBeforeKeymap(t *testing.T) { | |||||
| 180 | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | 180 | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | ||
| 181 | expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap)) | 181 | expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap)) | ||
| 182 | } | 182 | } | ||
| 183 | + | ||||
| 184 | + | // A virtual keyboard with the seat keymap (dictation tools, wtype) sends | |||
| 185 | + | // no keymap to clients: only its keys, then no keymap on the switch back. | |||
| 186 | + | func TestVirtualKeyboardSameKeymapNotResent(t *testing.T) { | |||
| 187 | + | s, events, commands, dir := keyboardServer(t) | |||
| 188 | + | target, proxy := focusedTarget(t, s, events, commands, dir) | |||
| 189 | + | for range 2 { // one typer per use, like a dictation app | |||
| 190 | + | typer, kb := virtualTyper(t, s, dir) | |||
| 191 | + | sendVirtualKeymap(t, typer, kb, keymapText(t)) | |||
| 192 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1)) | |||
| 193 | + | requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0)) | |||
| 194 | + | roundtrip(t, typer, target) | |||
| 195 | + | _ = typer.Close() | |||
| 196 | + | } | |||
| 197 | + | deadline := time.Now().Add(200 * time.Millisecond) | |||
| 198 | + | for time.Now().Before(deadline) { | |||
| 199 | + | roundtrip(t, target) | |||
| 200 | + | time.Sleep(5 * time.Millisecond) | |||
| 201 | + | } | |||
| 202 | + | if len(proxy.keymaps) != 0 { | |||
| 203 | + | t.Fatalf("%d keymaps sent for a virtual keyboard with the seat keymap", len(proxy.keymaps)) | |||
| 204 | + | } | |||
| 205 | + | keys := 0 | |||
| 206 | + | for _, o := range proxy.opcodes { | |||
| 207 | + | if o == uint16(wayland.KeyboardEventKey) { | |||
| 208 | + | keys++ | |||
| 209 | + | } | |||
| 210 | + | } | |||
| 211 | + | if keys != 4 { | |||
| 212 | + | t.Fatalf("keys %d, want 4 (events %v)", keys, proxy.opcodes) | |||
| 213 | + | } | |||
| 214 | + | } | |||