diff --git a/TODO.md b/TODO.md index 45cb4b4..02cad45 100644 --- a/TODO.md +++ b/TODO.md @@ -7,6 +7,8 @@ - [ ] X11: define and implement DPI-scaling policy for mouse move/click coordinates (for example using `Xft.dpi`) so coordinates are consistent with the physical-pixel model. ## Wayland +- [x] Make `KeyEvent.modifiers` reflect effective xkb modifier state (including remaps like Caps-as-Ctrl), not only raw pressed key symbols. +- [x] Fix keyboard repeat handling in `text_input_demo`/Wayland path (robust hold/repeat behavior with sane fallback repeat settings). - [ ] Use current xkb state for key mapping so `KeyEvent` respects active layout/group, not only unmodified symbols. - [ ] Improve scroll handling by consuming `axis_source`, `axis_discrete`, and `axis_value120` events instead of relying on a fixed divisor. - [ ] Revisit scroll normalization to avoid hardcoded `kde_default_mousewheel_scroll_length = 15`. @@ -14,10 +16,14 @@ - [ ] Expose IME preedit/composition updates (composition string, cursor/candidate position) to app callbacks. ## X11 +- [x] Make `KeyEvent.modifiers` include live X11 modifier-mask state so remapped modifiers (for example Caps-as-Ctrl) are reflected correctly. - [ ] Improve wheel handling beyond fixed button 4/5/6/7 `-1/+1` deltas. - [ ] Investigate support for user scroll preferences (direction/speed) where available. - [ ] Improve XIM text-input path to handle multi-stage IME composition updates more explicitly. +## Testing +- [x] Skip `tests/t_opengl.nim` and `tests/t_opengl_es.nim` when targeting macOS in Nimble test runners. + ## Cocoa (macOS) - [x] Implement missing core window methods on Cocoa: `close`, `size=`, `pos=`, `fullscreen=`, `maximized=`, `minimized=`, `resizable=`, `minSize=`, `maxSize=`, `vsync=`, `icon=`. - [x] Fix selector typo for `otherMouseUp:` so extra mouse button release events are dispatched correctly. diff --git a/examples/text_input_demo.nim b/examples/text_input_demo.nim index e8554d5..1d5cfd7 100644 --- a/examples/text_input_demo.nim +++ b/examples/text_input_demo.nim @@ -205,6 +205,7 @@ proc pickFontPath(): string = "/usr/share/fonts/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", "/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf", + "/usr/local/share/fonts/dejavu/DejaVuSans.ttf" ] for path in candidates: diff --git a/siwin.nimble b/siwin.nimble index 4892b76..3619d8b 100644 --- a/siwin.nimble +++ b/siwin.nimble @@ -118,9 +118,18 @@ task installAndroidDeps, "install android dependencies": const testTargets = ["t_opengl_es", "t_opengl", "t_swrendering", "t_multiwindow", "t_vulkan", "t_offscreen"] +proc shouldSkipTarget(target, args: string): bool = + let targetingMacos = + (when defined(macosx): true else: false) or + args.contains("--os:macosx") + targetingMacos and target in ["t_opengl", "t_opengl_es"] + proc runTests(args: string, envPrefix = "") = withDir "tests": for target in testTargets: + if shouldSkipTarget(target, args): + echo "Skipping ", target, " on macOS" + continue exec (if envPrefix.len != 0: envPrefix & " " else: "") & "nim c " & args & " --hints:off -r " & target proc runTestsForSession(args: string) = @@ -166,10 +175,14 @@ task testMacos, "test macos": createZigccIfNeeded() let pwd = getCurrentDir() let target = "x86_64-macos-none" + let args = &"--os:macosx --cc:clang --clang.exe:{pwd}/build/zigcc --clang.linkerexe:{pwd}/build/zigcc --passc:--target={target} --passl:--target={target} --hints:off" withDir "tests": for file in testTargets: + if shouldSkipTarget(file, args): + echo "Skipping ", file, " on macOS" + continue try: - exec &"nim c --os:macosx --cc:clang --clang.exe:{pwd}/build/zigcc --clang.linkerexe:{pwd}/build/zigcc --passc:--target={target} --passl:--target={target} --hints:off -o:{file}-macos {file}" + exec &"nim c {args} -o:{file}-macos {file}" exec &"echo ./{file}-macos | darling shell" except: discard diff --git a/src/siwin/platforms/wayland/protocol_generated.nim b/src/siwin/platforms/wayland/protocol_generated.nim index 5a9edcb..c766114 100644 --- a/src/siwin/platforms/wayland/protocol_generated.nim +++ b/src/siwin/platforms/wayland/protocol_generated.nim @@ -4693,8 +4693,9 @@ proc get_keyboard*(this: Wl_seat): Wl_keyboard = ## never had the keyboard capability. The missing_capability error will ## be sent in this case. let interfaces = cast[ptr ptr WaylandInterfaces](this.proxy.raw.impl) + let version = min(9'u32, this.proxy.wl_proxy_get_version()) result = wl_proxy_marshal_flags(this.proxy.raw, 1, - addr(interfaces[].`iface Wl_keyboard`), 1, 0, + addr(interfaces[].`iface Wl_keyboard`), version, 0, nil).construct(interfaces[], Wl_keyboard, `Wl_keyboard / dispatch`, `Wl_keyboard / Callbacks`) diff --git a/src/siwin/platforms/wayland/sharedBuffer.nim b/src/siwin/platforms/wayland/sharedBuffer.nim index 89d9df8..f45e16a 100644 --- a/src/siwin/platforms/wayland/sharedBuffer.nim +++ b/src/siwin/platforms/wayland/sharedBuffer.nim @@ -1,4 +1,5 @@ -import std/[memfiles, os, times, sequtils] +import std/[memfiles, os, oserrors, times, sequtils] +import std/posix import pkg/[vmath] import ../../[siwindefs] import ./[protocol, libwayland, siwinGlobals] @@ -20,6 +21,10 @@ type proc dataAddr*(buffer: SharedBuffer): pointer = + if buffer == nil: + return nil + if buffer.file.mem == nil: + return nil cast[pointer](cast[int](buffer.file.mem) + buffer.size.x * buffer.size.y * buffer.bytesPerPixel * buffer.currentBuffer.int32) proc fileDescriptor*(buffer: SharedBuffer): FileHandle = @@ -36,6 +41,7 @@ proc locked*(buffer: SharedBuffer): bool = proc release*(buffer: SharedBuffer) {.raises: [Exception].} = for v in buffer.buffers.mitems: + v.locked = false if v.buffer.proxy.raw != nil: destroy v.buffer v.buffer.proxy.raw = nil @@ -67,29 +73,42 @@ proc swapBuffers*( ## if timeout reached, raises OsError. ## ! please make sure you attached current buffer to a surface before calling this proc + # Keep a strong ref because this proc can re-enter event dispatch. + # The window may close and set its buffer slot to nil while we're still here. + let stable = buffer + if stable == nil: return + if stable.buffers.len == 0: return + if stable.currentBuffer notin 0..stable.buffers.high: return + if stable.buffers.allIt(it.buffer.proxy.raw == nil): return + # first, lock, attach and commit current buffer - buffer.buffers[buffer.currentBuffer].locked = true + stable.buffers[stable.currentBuffer].locked = true # then, swap to not-locked buffer - for i, v in buffer.buffers: + for i, v in stable.buffers: if not v.locked: - buffer.currentBuffer = i + stable.currentBuffer = i break # if unlocked buffer was not found, wait up to timeout while trying again - if buffer.buffers[buffer.currentBuffer].locked: + if stable.buffers[stable.currentBuffer].locked: let deadline = now() + timeout block waiting_for_unlocked_buffer: while now() < deadline: - for i in 0..buffer.buffers.high: - if not buffer.buffers[i].locked: - buffer.currentBuffer = i + if stable.buffers.len == 0 or stable.buffers.allIt(it.buffer.proxy.raw == nil): + return + + for i in 0..stable.buffers.high: + if not stable.buffers[i].locked: + stable.currentBuffer = i break waiting_for_unlocked_buffer - discard wl_display_roundtrip buffer.globals.display # let libwayland process events + discard wl_display_roundtrip stable.globals.display # let libwayland process events - if buffer.buffers[buffer.currentBuffer].locked: + if stable.currentBuffer notin 0..stable.buffers.high: + return + if stable.buffers[stable.currentBuffer].locked: raise OsError.newException("timed out waiting for all buffers to be unlocked by server. (needed to commit shared buffer)") # current buffer are now unlocked and free to use. @@ -122,17 +141,69 @@ proc create*(globals: SiwinGlobalsWayland, shm: WlShm, size: IVec2, format: `WlS assert bufferCount >= 1, "at least one buffer is required" result.buffers.setLen bufferCount - let filebase = getEnv("XDG_RUNTIME_DIR") / "siwin-" - for i in 0..int.high: - if not fileExists(filebase & $i): - result.filename = filebase & $i - result.file = memfiles.open( - result.filename, mode = fmReadWrite, allowRemap = true, - newFileSize = size.x * size.y * bytesPerPixel * bufferCount.int32 + let sizeInBytes64 = int64(size.x) * int64(size.y) * int64(bytesPerPixel) * int64(bufferCount) + if sizeInBytes64 <= 0 or sizeInBytes64 > high(int32).int64: + raise ValueError.newException("invalid shared buffer size") + let sizeInBytes = sizeInBytes64.int32 + + var candidateDirs: seq[string] = @[] + let runtimeDir = getEnv("XDG_RUNTIME_DIR") + if runtimeDir.len != 0 and dirExists(runtimeDir): + candidateDirs.add runtimeDir + + let tempDir = getTempDir() + if tempDir.len != 0 and tempDir notin candidateDirs: + candidateDirs.add tempDir + + var opened = false + var openError = "" + let pid = getCurrentProcessId() + for baseDir in candidateDirs: + let filebase = baseDir / ("siwin-" & $pid & "-") + for i in 0..8192: + let filename = filebase & $i + if fileExists(filename): + continue + + let fd = posix.open( + filename.cstring, + posix.O_RDWR or posix.O_CREAT or posix.O_TRUNC or posix.O_CLOEXEC, + posix.S_IRUSR or posix.S_IWUSR ) + if fd == -1: + openError = osErrorMsg(osLastError()) + continue + if posix.ftruncate(fd, sizeInBytes) != 0: + openError = osErrorMsg(osLastError()) + discard posix.close(fd) + try: + removeFile(filename) + except OsError: + discard + continue + discard posix.close(fd) + + try: + result.file = memfiles.open(filename, mode = fmReadWrite, allowRemap = true) + result.filename = filename + opened = true + break + except CatchableError as e: + openError = e.msg + try: + if fileExists(filename): + removeFile(filename) + except OsError: + discard + if opened: break - result.pool = shm.create_pool(result.fileDescriptor, size.x * size.y * bytesPerPixel * bufferCount.int32) + if not opened: + if openError.len != 0: + raise OSError.newException("failed to create shared buffer file: " & openError) + raise OSError.newException("failed to create shared buffer file") + + result.pool = shm.create_pool(result.fileDescriptor, sizeInBytes) result.create_wl_buffers() diff --git a/src/siwin/platforms/wayland/window.nim b/src/siwin/platforms/wayland/window.nim index f345f6f..f01afc7 100644 --- a/src/siwin/platforms/wayland/window.nim +++ b/src/siwin/platforms/wayland/window.nim @@ -47,6 +47,8 @@ type kind: WindowWaylandKind ## Is this a normal window or is it a layer shell surface? lastPressedKey: Key + lastPressedRawKeycode: uint32 + lastPressedRawKeyDown: bool lastTextEntered: string lastPressedKeyTime: Time lastKeyRepeatedTime: Time @@ -184,6 +186,42 @@ proc waylandKeyToString(keycode: uint32): string = result.setLen 1 result.setLen global_xkb_state.xkb_state_key_get_utf8(keycode + 8, cast[cstring](result[0].addr), 7) +proc modifiersFromPressedKeys(keys: set[Key]): set[ModifierKey] = + if (keys * {Key.lshift, Key.rshift}).len != 0: + result.incl ModifierKey.shift + if (keys * {Key.lcontrol, Key.rcontrol}).len != 0: + result.incl ModifierKey.control + if (keys * {Key.lalt, Key.ralt, Key.level3_shift, Key.level5_shift}).len != 0: + result.incl ModifierKey.alt + if (keys * {Key.lsystem, Key.rsystem}).len != 0: + result.incl ModifierKey.system + +proc refreshKeyboardModifiers(window: WindowWayland) = + var modifiers = modifiersFromPressedKeys(window.keyboard.pressed) + if global_xkb_state != nil: + if global_xkb_state.xkb_state_mod_name_is_active("Shift".cstring, XKB_STATE_MODS_EFFECTIVE) != 0: + modifiers.incl ModifierKey.shift + if global_xkb_state.xkb_state_mod_name_is_active("Control".cstring, XKB_STATE_MODS_EFFECTIVE) != 0: + modifiers.incl ModifierKey.control + if ( + global_xkb_state.xkb_state_mod_name_is_active("Mod1".cstring, XKB_STATE_MODS_EFFECTIVE) != 0 or + global_xkb_state.xkb_state_mod_name_is_active("Alt".cstring, XKB_STATE_MODS_EFFECTIVE) != 0 + ): + modifiers.incl ModifierKey.alt + if ( + global_xkb_state.xkb_state_mod_name_is_active("Mod4".cstring, XKB_STATE_MODS_EFFECTIVE) != 0 or + global_xkb_state.xkb_state_mod_name_is_active("Super".cstring, XKB_STATE_MODS_EFFECTIVE) != 0 + ): + modifiers.incl ModifierKey.system + if global_xkb_state.xkb_state_mod_name_is_active("Lock".cstring, XKB_STATE_MODS_EFFECTIVE) != 0: + modifiers.incl ModifierKey.capsLock + if ( + global_xkb_state.xkb_state_mod_name_is_active("NumLock".cstring, XKB_STATE_MODS_EFFECTIVE) != 0 or + global_xkb_state.xkb_state_mod_name_is_active("Mod2".cstring, XKB_STATE_MODS_EFFECTIVE) != 0 + ): + modifiers.incl ModifierKey.numLock + window.keyboard.modifiers = modifiers + method swapBuffers(window: WindowWayland) {.base.} = discard @@ -325,7 +363,7 @@ method doResize(window: WindowWayland, size: IVec2) {.base.} = method doResize(window: WindowWaylandSoftwareRendering, size: IVec2) = procCall window.WindowWayland.doResize(size) - if window.buffer.locked: + if window.buffer != nil and window.buffer.locked: swap window.buffer, window.oldBuffer if window.buffer == nil: @@ -454,14 +492,27 @@ method `icon=`*(window: WindowWayland, v: PixelBuffer) = method pixelBuffer*(window: WindowWaylandSoftwareRendering): PixelBuffer = + if window.buffer == nil: + window.doResize(window.m_size) + PixelBuffer( - data: window.buffer.dataAddr, + data: (if window.buffer == nil: nil else: window.buffer.dataAddr), size: window.m_size, format: (if window.transparent: PixelBufferFormat.xrgb_32bit else: PixelBufferFormat.urgb_32bit) ) method swapBuffers(window: WindowWaylandSoftwareRendering) = + if window.m_closed: + return + + if window.buffer == nil: + if window.m_size.x <= 0 or window.m_size.y <= 0: + return + window.doResize(window.m_size) + if window.buffer == nil: + return + window.surface.attach(window.buffer.buffer, 0, 0) window.surface.damage_buffer(0, 0, window.m_size.x, window.m_size.y) commit window.surface @@ -502,7 +553,13 @@ proc releaseAllKeys(window: WindowWayland) = ## needed when window loses focus for k in window.keyboard.pressed.items.toSeq: window.keyboard.pressed.excl k - if window.opened: window.eventsHandler.onKey.pushEvent KeyEvent(window: window, key: k, pressed: false, repeated: false, generated: true) + window.refreshKeyboardModifiers() + if window.opened: window.eventsHandler.onKey.pushEvent KeyEvent( + window: window, key: k, pressed: false, repeated: false, generated: true, modifiers: window.keyboard.modifiers + ) + window.lastPressedKey = Key.unknown + window.lastPressedRawKeyDown = false + window.lastTextEntered = "" method `minimized=`*(window: WindowWayland, v: bool) = @@ -729,6 +786,8 @@ proc initSeatEvents*(globals: SiwinGlobalsWayland) = if `WlSeat / Capability`.keyboard in globals.seatCapabilities: globals.seat_keyboard = globals.seat.get_keyboard + # Default repeat values; compositor-provided repeat_info overrides these. + globals.seat_keyboard_repeatSettings = (rate: 25, delay: 600) globals.seat_keyboard.onKeymap: updateKeymap(fd, size) @@ -741,12 +800,16 @@ proc initSeatEvents*(globals: SiwinGlobalsWayland) = globals.lastSeatEventSerial = serial for key in keys.toSeq(uint32): + if global_xkb_state != nil: + discard global_xkb_state.xkb_state_update_key(key + 8, XKB_KEY_DIRECTION_DOWN) + let siwinKey = waylandKeyToKey(key) if siwinKey == Key.unknown: continue window.keyboard.pressed.incl siwinKey + window.refreshKeyboardModifiers() if window.opened: window.eventsHandler.onKey.pushEvent KeyEvent( - window: window, key: siwinKey, pressed: true, generated: true + window: window, key: siwinKey, pressed: true, generated: true, modifiers: window.keyboard.modifiers ) window.lastPressedKey = siwinKey window.lastTextEntered = "" @@ -766,11 +829,21 @@ proc initSeatEvents*(globals: SiwinGlobalsWayland) = if globals.seat_keyboard_currentWindow == nil: return let window = globals.seat_keyboard_currentWindow.WindowWayland - let pressed = state == `WlKeyboard / Key_state`.pressed + let pressed = state != `WlKeyboard / Key_state`.released if pressed: + window.lastPressedRawKeycode = key + window.lastPressedRawKeyDown = true window.lastPressedKeyTime = getTime() + window.lastTextEntered = "" + elif key == window.lastPressedRawKeycode: + window.lastPressedRawKeyDown = false globals.lastSeatEventSerial = serial + + if global_xkb_state != nil: + discard global_xkb_state.xkb_state_update_key( + key + 8, (if pressed: XKB_KEY_DIRECTION_DOWN else: XKB_KEY_DIRECTION_UP) + ) let siwinKey = waylandKeyToKey(key) @@ -780,10 +853,6 @@ proc initSeatEvents*(globals: SiwinGlobalsWayland) = else: window.keyboard.pressed.excl siwinKey - if window.opened: window.eventsHandler.onKey.pushEvent KeyEvent( - window: window, key: siwinKey, pressed: pressed - ) - if pressed: if siwinKey notin Key.lcontrol..Key.rsystem: window.lastPressedKey = siwinKey @@ -791,9 +860,15 @@ proc initSeatEvents*(globals: SiwinGlobalsWayland) = window.lastPressedKey = Key.unknown else: window.lastPressedKey = Key.unknown + + window.refreshKeyboardModifiers() + if siwinKey != Key.unknown and window.opened: + window.eventsHandler.onKey.pushEvent KeyEvent( + window: window, key: siwinKey, pressed: pressed, modifiers: window.keyboard.modifiers + ) var text = waylandKeyToString(key) - if Key.lcontrol in window.keyboard.pressed or Key.rcontrol in window.keyboard.pressed: text = "" + if ModifierKey.control in window.keyboard.modifiers: text = "" if text.len == 1 and text[0] < 32.char: text = "" if pressed and text != "": @@ -807,10 +882,13 @@ proc initSeatEvents*(globals: SiwinGlobalsWayland) = globals.seat_keyboard.onModifiers: globals.lastSeatEventSerial = serial discard global_xkb_state.xkb_state_update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group) + if globals.seat_keyboard_currentWindow != nil: + globals.seat_keyboard_currentWindow.WindowWayland.refreshKeyboardModifiers() globals.seat_keyboard.onRepeat_info: - globals.seat_keyboard_repeatSettings = (rate: rate, delay: delay) + if rate > 0 and delay >= 0: + globals.seat_keyboard_repeatSettings = (rate: rate, delay: delay) proc setIdleInhibit*(window: WindowWayland, state: bool) = @@ -1249,36 +1327,45 @@ method step*(window: WindowWayland) = if eventCount <= 2: # seems like idle event count is 2 sleep(1) - if window.globals.seat_keyboard_currentWindow == window: - # repeat keys if needed - if ( - (window.globals.seat_keyboard_repeatSettings.rate > 0 and window.globals.seat_keyboard_repeatSettings.rate < 1000) and - (window.keyboard.pressed - {Key.lcontrol, Key.lshift, Key.lalt, Key.rcontrol, Key.rshift, Key.ralt}).len != 0 - ): - let repeatStartTime = window.lastPressedKeyTime + initDuration(milliseconds = window.globals.seat_keyboard_repeatSettings.delay) - let nows = getTime() - let interval = initDuration(milliseconds = 1000 div window.globals.seat_keyboard_repeatSettings.rate) - - if repeatStartTime <= nows and window.lastKeyRepeatedTime < repeatStartTime - interval: - window.lastKeyRepeatedTime = repeatStartTime - interval - - while repeatStartTime <= nows and window.lastKeyRepeatedTime + interval <= nows: - window.lastKeyRepeatedTime += interval - - if window.lastPressedKey != Key.unknown and window.keyboard.pressed.contains(window.lastPressedKey): - window.keyboard.pressed.excl window.lastPressedKey - if window.opened: window.eventsHandler.onKey.pushEvent KeyEvent( - window: window, key: window.lastPressedKey, pressed: false, repeated: true - ) - window.keyboard.pressed.incl window.lastPressedKey - if window.opened: window.eventsHandler.onKey.pushEvent KeyEvent( - window: window, key: window.lastPressedKey, pressed: true, repeated: true - ) + # repeat keys if needed + if ( + window.globals.seat_keyboard_repeatSettings.rate > 0 and + window.lastPressedRawKeyDown + ): + let repeatStartTime = window.lastPressedKeyTime + initDuration(milliseconds = window.globals.seat_keyboard_repeatSettings.delay) + let nows = getTime() + let interval = initDuration(milliseconds = max(1'i64, (1000 div window.globals.seat_keyboard_repeatSettings.rate).int64)) + + if repeatStartTime <= nows and window.lastKeyRepeatedTime < repeatStartTime - interval: + window.lastKeyRepeatedTime = repeatStartTime - interval + + while repeatStartTime <= nows and window.lastKeyRepeatedTime + interval <= nows: + window.lastKeyRepeatedTime += interval + + let repeatedKey = waylandKeyToKey(window.lastPressedRawKeycode) + var repeatedText = waylandKeyToString(window.lastPressedRawKeycode) + if ModifierKey.control in window.keyboard.modifiers: + repeatedText = "" + if repeatedText.len == 1 and repeatedText[0] < 32.char: + repeatedText = "" + + if repeatedKey != Key.unknown and window.keyboard.pressed.contains(repeatedKey): + window.keyboard.pressed.excl repeatedKey + window.refreshKeyboardModifiers() + if window.opened: window.eventsHandler.onKey.pushEvent KeyEvent( + window: window, key: repeatedKey, pressed: false, repeated: true, modifiers: window.keyboard.modifiers + ) + window.keyboard.pressed.incl repeatedKey + window.refreshKeyboardModifiers() + if window.opened: window.eventsHandler.onKey.pushEvent KeyEvent( + window: window, key: repeatedKey, pressed: true, repeated: true, modifiers: window.keyboard.modifiers + ) - if window.lastTextEntered != "": - if window.opened: window.eventsHandler.onTextInput.pushEvent TextInputEvent( - window: window, text: window.lastTextEntered, repeated: true - ) + if repeatedText != "": + window.lastTextEntered = repeatedText + if window.opened: window.eventsHandler.onTextInput.pushEvent TextInputEvent( + window: window, text: repeatedText, repeated: true + ) let nows = getTime() if window.opened: window.eventsHandler.onTick.pushEvent TickEvent(window: window, deltaTime: nows - window.lastTickTime) diff --git a/src/siwin/platforms/x11/window.nim b/src/siwin/platforms/x11/window.nim index a7613d2..e42fcf4 100644 --- a/src/siwin/platforms/x11/window.nim +++ b/src/siwin/platforms/x11/window.nim @@ -233,6 +233,46 @@ proc xkeyToKey(sym: KeySym): Key = of Xk_9: Key.n9 else: Key.unknown +proc modifiersFromPressedKeys(keys: set[Key]): set[ModifierKey] = + if (keys * {Key.lshift, Key.rshift}).len != 0: + result.incl ModifierKey.shift + if (keys * {Key.lcontrol, Key.rcontrol}).len != 0: + result.incl ModifierKey.control + if (keys * {Key.lalt, Key.ralt, Key.level3_shift, Key.level5_shift}).len != 0: + result.incl ModifierKey.alt + if (keys * {Key.lsystem, Key.rsystem}).len != 0: + result.incl ModifierKey.system + +proc modifiersFromStateMask(stateMask: cuint): set[ModifierKey] = + if (stateMask and ShiftMask.cuint) != 0: + result.incl ModifierKey.shift + if (stateMask and ControlMask.cuint) != 0: + result.incl ModifierKey.control + if (stateMask and Mod1Mask.cuint) != 0: + result.incl ModifierKey.alt + if (stateMask and Mod4Mask.cuint) != 0: + result.incl ModifierKey.system + if (stateMask and LockMask.cuint) != 0: + result.incl ModifierKey.capsLock + if (stateMask and Mod2Mask.cuint) != 0: + result.incl ModifierKey.numLock + +proc refreshKeyboardModifiers(window: WindowX11) = + var rootWindow = window.globals.display.DefaultRootWindow + var childWindow: x.Window + var rootX, rootY: cint + var windowX, windowY: cint + var stateMask: cuint + let pointerKnown = window.globals.display.XQueryPointer( + rootWindow, rootWindow.addr, childWindow.addr, + rootX.addr, rootY.addr, windowX.addr, windowY.addr, stateMask.addr + ) != 0 + + var modifiers = modifiersFromPressedKeys(window.keyboard.pressed) + if pointerKnown: + modifiers = modifiers + modifiersFromStateMask(stateMask) + window.keyboard.modifiers = modifiers + proc newClientMessage[T](globals: SiwinGlobalsX11, window: x.Window, messageKind: Atom, data: openarray[T], serial: int = 0, sendEvent: bool = false): XEvent = result.theType = ClientMessage @@ -703,7 +743,10 @@ proc releaseAllKeys(window: WindowX11) = ## needed when window loses focus for k in window.keyboard.pressed.items: window.keyboard.pressed.excl k - window.eventsHandler.onKey.pushEvent KeyEvent(window: window, key: k, pressed: false, repeated: false, generated: true) + window.refreshKeyboardModifiers() + window.eventsHandler.onKey.pushEvent KeyEvent( + window: window, key: k, pressed: false, repeated: false, generated: true, modifiers: window.keyboard.modifiers + ) for b in window.mouse.pressed: window.mouse.pressed.excl b @@ -1004,7 +1047,10 @@ method step*(window: WindowX11) = for k in keys: # press pressed in system keys if k == Key.unknown: continue window.keyboard.pressed.incl k - window.eventsHandler.onKey.pushEvent KeyEvent(window: window, pressed: false, repeated: false) + window.refreshKeyboardModifiers() + window.eventsHandler.onKey.pushEvent KeyEvent( + window: window, key: k, pressed: false, repeated: false, modifiers: window.keyboard.modifiers + ) # todo: press pressed in system mouse buttons @@ -1217,9 +1263,12 @@ method step*(window: WindowX11) = var key = ev.xkey.extractKey if key != Key.unknown: window.keyboard.pressed.incl key - window.eventsHandler.onKey.pushEvent KeyEvent(window: window, key: key, pressed: true, repeated: repeated) + window.refreshKeyboardModifiers() + window.eventsHandler.onKey.pushEvent KeyEvent( + window: window, key: key, pressed: true, repeated: repeated, modifiers: window.keyboard.modifiers + ) - if window.eventsHandler.onTextInput != nil and window.xinContext != nil and (window.keyboard.pressed * {lcontrol, rcontrol, lalt, ralt}).len == 0: + if window.eventsHandler.onTextInput != nil and window.xinContext != nil and (window.keyboard.modifiers * {ModifierKey.control, ModifierKey.alt, ModifierKey.system}).len == 0: var status: Status var buffer: array[16, char] let length = window.xinContext.Xutf8LookupString(ev.xkey.addr, cast[cstring](buffer.addr), buffer.sizeof.cint, nil, status.addr) @@ -1241,7 +1290,10 @@ method step*(window: WindowX11) = if repeated: prevEventIsKeyUpRepeated = true window.keyboard.pressed.excl key - window.eventsHandler.onKey.pushEvent KeyEvent(window: window, key: key, pressed: false, repeated: repeated) + window.refreshKeyboardModifiers() + window.eventsHandler.onKey.pushEvent KeyEvent( + window: window, key: key, pressed: false, repeated: repeated, modifiers: window.keyboard.modifiers + ) of SelectionNotify: let clipboard =