From 210bf4b3ee1dcb907158bd201116112d4c3d2e06 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 23 Mar 2026 22:43:07 +0900 Subject: [PATCH] internal/ui: re-assert modifier key states using mods from key events On macOS, the text input system can intercept modifier+key combinations (e.g. Ctrl+A) and prematurely release the modifier key via flagsChanged. The mods parameter on the key event still correctly reflects which modifiers are physically held. Use it to restore modifier key states when a key is pressed. Closes #3422 Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/ui/input_glfw.go | 44 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/internal/ui/input_glfw.go b/internal/ui/input_glfw.go index b3a04938e..21b1e5468 100644 --- a/internal/ui/input_glfw.go +++ b/internal/ui/input_glfw.go @@ -46,10 +46,16 @@ func (u *UserInterface) registerInputCallbacks() error { if !ok { return } + t := u.InputTime() if action == glfw.Press { - u.inputState.setKeyPressed(uk, u.InputTime()) + u.inputState.setKeyPressed(uk, t) + // On macOS, modifier keys can appear released prematurely when the text input system + // intercepts certain key combinations (e.g. Ctrl+A). The mods parameter on the key event + // still correctly reflects which modifiers are physically held. Use it to re-assert + // modifier key states that may have been incorrectly released. + u.inputState.syncModKeysByMods(mods, t) } else { - u.inputState.setKeyReleased(uk, u.InputTime()) + u.inputState.setKeyReleased(uk, t) } }); err != nil { return err @@ -203,3 +209,37 @@ func (u *UserInterface) KeyName(key Key) string { }) return name } + +// syncModKeysByMods re-asserts modifier key states based on the mods bitmask +// from a key event. On macOS, the text input system can intercept modifier+key +// combinations (e.g. Ctrl+A) and prematurely release the modifier key via +// flagsChanged. The mods parameter on the key event still correctly reflects +// which modifiers are physically held, so we use it to restore the state. +func (i *InputState) syncModKeysByMods(mods glfw.ModifierKey, t InputTime) { + type modMapping struct { + mod glfw.ModifierKey + left Key + right Key + } + mappings := [...]modMapping{ + {glfw.ModControl, KeyControlLeft, KeyControlRight}, + {glfw.ModShift, KeyShiftLeft, KeyShiftRight}, + {glfw.ModAlt, KeyAltLeft, KeyAltRight}, + {glfw.ModSuper, KeyMetaLeft, KeyMetaRight}, + } + for _, m := range mappings { + if mods&m.mod == 0 { + continue + } + // The mod flag is set, so at least one of left/right should be pressed. + // Re-press whichever was most recently pressed. + // If neither was ever pressed, default to the left variant. + lp := i.KeyPressedTimes[m.left] + rp := i.KeyPressedTimes[m.right] + if lp >= rp { + i.setKeyPressed(m.left, t) + } else { + i.setKeyPressed(m.right, t) + } + } +}