From a7d2e4592f94dd2fb0e05e14c9afadecca56bcd7 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sat, 12 Sep 2026 11:19:07 +1000 Subject: [PATCH] [client] windows: add native display and input --- client/CMakeLists.txt | 10 + client/displayservers/Windows/CMakeLists.txt | 35 + client/displayservers/Windows/input_event.c | 399 ++++ client/displayservers/Windows/input_event.h | 91 + client/displayservers/Windows/platform.h | 33 + client/displayservers/Windows/window_size.c | 50 + client/displayservers/Windows/window_size.h | 41 + client/displayservers/Windows/windows.c | 1847 ++++++++++++++++++ client/include/app.h | 1 + client/include/egl_dynprocs.h | 20 +- client/src/app.c | 5 + client/src/core.c | 6 + client/src/core.h | 1 + client/src/egl_dynprocs.c | 40 +- client/tests/CMakeLists.txt | 23 + client/tests/mouse_test.c | 27 + client/tests/windows_input_test.c | 575 ++++++ 17 files changed, 3180 insertions(+), 24 deletions(-) create mode 100644 client/displayservers/Windows/CMakeLists.txt create mode 100644 client/displayservers/Windows/input_event.c create mode 100644 client/displayservers/Windows/input_event.h create mode 100644 client/displayservers/Windows/platform.h create mode 100644 client/displayservers/Windows/window_size.c create mode 100644 client/displayservers/Windows/window_size.h create mode 100644 client/displayservers/Windows/windows.c create mode 100644 client/tests/windows_input_test.c diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 65150e71..e8932579 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -313,6 +313,16 @@ endif() add_subdirectory("${PROJECT_TOP}/repos/gui" "${CMAKE_BINARY_DIR}/gui" EXCLUDE_FROM_ALL) +get_target_property(LG_GUI_INTERFACE_COMPILE_DEFINITIONS + lg_gui INTERFACE_COMPILE_DEFINITIONS) +if(LG_GUI_INTERFACE_COMPILE_DEFINITIONS) + list(FILTER LG_GUI_INTERFACE_COMPILE_DEFINITIONS EXCLUDE + REGEX "^IMGUI_IMPL_API=") + set_property(TARGET lg_gui PROPERTY INTERFACE_COMPILE_DEFINITIONS + ${LG_GUI_INTERFACE_COMPILE_DEFINITIONS}) +endif() +unset(LG_GUI_INTERFACE_COMPILE_DEFINITIONS) + target_sources(lg_gui PRIVATE "${PROJECT_TOP}/repos/gui/cimgui/imgui/imgui_demo.cpp" "${PROJECT_TOP}/repos/gui/cimplot/implot/implot_demo.cpp" diff --git a/client/displayservers/Windows/CMakeLists.txt b/client/displayservers/Windows/CMakeLists.txt new file mode 100644 index 00000000..108ddb0c --- /dev/null +++ b/client/displayservers/Windows/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.10) +project(displayserver_Windows LANGUAGES C) + +add_library(displayserver_Windows STATIC + input_event.c + window_size.c + windows.c +) + +target_compile_definitions(displayserver_Windows PRIVATE + WINVER=0x0A00 + _WIN32_WINNT=0x0A00 + WIN32_LEAN_AND_MEAN + NOMINMAX + UNICODE + _UNICODE +) + +target_include_directories(displayserver_Windows + PRIVATE + . + ../../src +) + +target_link_libraries(displayserver_Windows + lg_common + user32 + gdi32 +) + +if(ENABLE_EGL) + target_link_libraries(displayserver_Windows + lg_egl_provider + ) +endif() diff --git a/client/displayservers/Windows/input_event.c b/client/displayservers/Windows/input_event.c new file mode 100644 index 00000000..be7f4a19 --- /dev/null +++ b/client/displayservers/Windows/input_event.c @@ -0,0 +1,399 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "input_event.h" + +#include "../../src/kb.h" + +#include + +enum ModifierKey +{ + MODIFIER_KEY_LEFT = 1U << 0, + MODIFIER_KEY_RIGHT = 1U << 1, +}; + +_Static_assert(sizeof(double) == sizeof(uint64_t), "Unexpected double size"); + +static bool validCoordinate(double value) +{ + uint64_t bits; + memcpy(&bits, &value, sizeof(bits)); + return (bits & UINT64_C(0x7FF0000000000000)) != + UINT64_C(0x7FF0000000000000); +} + +static bool finishPointerLeave(WindowsInput * input) +{ + if (!input->entered || !input->leavePending || input->buttons || + input->pointerGrabbed) + return false; + + input->entered = false; + input->leavePending = false; + input->sink->enter(input->opaque, false); + return true; +} + +static void updateKeyState(WindowsInput * input, int key, bool pressed) +{ + uint8_t * modifier = NULL; + uint8_t mask = 0; + bool * lock = NULL; + bool * lockDown = NULL; + + switch (key) + { + case LG_KEY_LEFTCTRL: + modifier = &input->ctrlKeys; + mask = MODIFIER_KEY_LEFT; + break; + + case LG_KEY_RIGHTCTRL: + modifier = &input->ctrlKeys; + mask = MODIFIER_KEY_RIGHT; + break; + + case LG_KEY_LEFTSHIFT: + modifier = &input->shiftKeys; + mask = MODIFIER_KEY_LEFT; + break; + + case LG_KEY_RIGHTSHIFT: + modifier = &input->shiftKeys; + mask = MODIFIER_KEY_RIGHT; + break; + + case LG_KEY_LEFTALT: + modifier = &input->altKeys; + mask = MODIFIER_KEY_LEFT; + break; + + case LG_KEY_RIGHTALT: + modifier = &input->altKeys; + mask = MODIFIER_KEY_RIGHT; + break; + + case LG_KEY_LEFTMETA: + modifier = &input->superKeys; + mask = MODIFIER_KEY_LEFT; + break; + + case LG_KEY_RIGHTMETA: + modifier = &input->superKeys; + mask = MODIFIER_KEY_RIGHT; + break; + + case LG_KEY_NUMLOCK: + lock = &input->numLock; + lockDown = &input->numLockPressed; + break; + + case LG_KEY_CAPSLOCK: + lock = &input->capsLock; + lockDown = &input->capsLockPressed; + break; + + case LG_KEY_SCROLLLOCK: + lock = &input->scrollLock; + lockDown = &input->scrollLockPressed; + break; + } + + if (modifier) + { + if (pressed) + *modifier |= mask; + else + *modifier &= ~mask; + } + + if (lock) + { + if (pressed && !*lockDown) + *lock = !*lock; + *lockDown = pressed; + } +} + +static bool modifierKey(int key) +{ + return key == LG_KEY_LEFTCTRL || key == LG_KEY_RIGHTCTRL || + key == LG_KEY_LEFTSHIFT || key == LG_KEY_RIGHTSHIFT || + key == LG_KEY_LEFTALT || key == LG_KEY_RIGHTALT || + key == LG_KEY_LEFTMETA || key == LG_KEY_RIGHTMETA; +} + +void windowsInputInit(WindowsInput * input, const LG_DSInputSink * sink, + void * opaque) +{ + memset(input, 0, sizeof(*input)); + input->sink = sink; + input->opaque = opaque; +} + +void windowsInputSetPointerGrabbed(WindowsInput * input, bool grabbed) +{ + if (input->pointerGrabbed == grabbed) + return; + + input->pointerGrabbed = grabbed; + if (!grabbed) + finishPointerLeave(input); +} + +bool windowsInputIsPointerGrabbed(const WindowsInput * input) +{ + return input->pointerGrabbed; +} + +void windowsInputSetKeyboardGrabbed(WindowsInput * input, bool grabbed) +{ + input->keyboardGrabbed = grabbed; +} + +bool windowsInputIsKeyboardGrabbed(const WindowsInput * input) +{ + return input->keyboardGrabbed; +} + +bool windowsInputFocus(WindowsInput * input, bool focused) +{ + if (input->focused == focused) + return false; + + input->focused = focused; + if (!focused) + { + input->ctrlKeys = 0; + input->shiftKeys = 0; + input->altKeys = 0; + input->superKeys = 0; + input->numLockPressed = false; + input->capsLockPressed = false; + input->scrollLockPressed = false; + input->printScreenKey = LG_KEY_RESERVED; + } + + input->sink->focus(input->opaque, focused); + return true; +} + +bool windowsInputPointerEnter(WindowsInput * input, double x, double y) +{ + if (!validCoordinate(x) || !validCoordinate(y)) + return false; + + input->leavePending = false; + if (input->entered) + return false; + + input->entered = true; + input->sink->enter(input->opaque, true); + input->sink->position(input->opaque, x, y); + return true; +} + +bool windowsInputPointerLeave(WindowsInput * input) +{ + if (!input->entered) + return false; + + input->leavePending = true; + return finishPointerLeave(input); +} + +void windowsInputPointerMotion(WindowsInput * input, double x, double y) +{ + if (!input->entered || input->leavePending || input->pointerGrabbed || + !validCoordinate(x) || !validCoordinate(y)) + return; + + input->sink->position(input->opaque, x, y); +} + +void windowsInputRelativeMotion(WindowsInput * input, double x, double y) +{ + if (!input->entered || !input->pointerGrabbed || + !validCoordinate(x) || !validCoordinate(y)) + return; + + input->sink->relative(input->opaque, x, y, x, y); +} + +bool windowsInputPointerButton(WindowsInput * input, unsigned int button, + bool pressed) +{ + if ((!input->entered && !input->pointerGrabbed) || + button == 0 || button >= 32) + return false; + + const uint32_t mask = UINT32_C(1) << button; + if (((input->buttons & mask) != 0) == pressed) + return false; + + if (pressed) + input->buttons |= mask; + else + input->buttons &= ~mask; + + input->sink->button(input->opaque, button, pressed); + if (!pressed) + finishPointerLeave(input); + return true; +} + +void windowsInputReleaseButtons(WindowsInput * input) +{ + const uint32_t buttons = input->buttons; + input->buttons = 0; + + for (unsigned int button = 1; button < 32; ++button) + if (buttons & (UINT32_C(1) << button)) + input->sink->button(input->opaque, button, false); + + finishPointerLeave(input); +} + +void windowsInputPointerWheel(WindowsInput * input, double motion) +{ + if ((!input->entered && !input->pointerGrabbed) || + !validCoordinate(motion) || motion == 0.0) + return; + + input->wheel += motion; + while (input->wheel >= 1.0) + { + input->sink->button(input->opaque, 4, true); + input->sink->button(input->opaque, 4, false); + input->wheel -= 1.0; + } + + while (input->wheel <= -1.0) + { + input->sink->button(input->opaque, 5, true); + input->sink->button(input->opaque, 5, false); + input->wheel += 1.0; + } + + input->sink->wheel(input->opaque, -motion); +} + +int windowsInputMapScanCode(uint8_t scanCode, bool extended) +{ + const uint32_t ps2 = scanCode | + (extended ? UINT32_C(0xE000) : UINT32_C(0)); + if (!ps2) + return LG_KEY_RESERVED; + + for (int key = 1; key < LG_KEY_MAX; ++key) + if (lg_keycode_to_ps2[key] == ps2) + return key; + + return LG_KEY_RESERVED; +} + +int windowsInputMapPrintScreen(bool altDown) +{ + return altDown ? LG_KEY_SYSRQ : LG_KEY_PRINT; +} + +int windowsInputMapPrintScreenEvent(WindowsInput * input, + bool altDown, bool pressed) +{ + if (pressed) + { + if (input->printScreenKey == LG_KEY_RESERVED) + input->printScreenKey = windowsInputMapPrintScreen(altDown); + + return input->printScreenKey; + } + + const int key = input->printScreenKey == LG_KEY_RESERVED ? + windowsInputMapPrintScreen(altDown) : input->printScreenKey; + input->printScreenKey = LG_KEY_RESERVED; + return key; +} + +bool windowsInputKeyCode(WindowsInput * input, int key, bool pressed, + bool grabbedEvent) +{ + if (!input->focused || grabbedEvent != input->keyboardGrabbed || + key <= LG_KEY_RESERVED || key >= LG_KEY_MAX) + return false; + + updateKeyState(input, key, pressed); + input->sink->key(input->opaque, key, pressed); + return true; +} + +bool windowsInputKey(WindowsInput * input, uint8_t scanCode, bool extended, + bool pressed, bool grabbedEvent) +{ + return windowsInputKeyCode(input, + windowsInputMapScanCode(scanCode, extended), pressed, grabbedEvent); +} + +size_t windowsInputSyncHeldKeys(WindowsInput * input, + const int * keys, size_t count, bool grabbedEvent) +{ + if (!keys) + return 0; + + size_t synced = 0; + for (unsigned int pass = 0; pass < 2; ++pass) + for (size_t i = 0; i < count; ++i) + { + const bool modifier = modifierKey(keys[i]); + if (modifier != (pass == 0)) + continue; + + if (windowsInputKeyCode(input, keys[i], true, grabbedEvent)) + ++synced; + } + + return synced; +} + +void windowsInputText(WindowsInput * input, const char * text) +{ + if (input->focused && !input->keyboardGrabbed && text && *text) + input->sink->text(input->opaque, text); +} + +void windowsInputState(WindowsInput * input, + bool ctrl, bool shift, bool alt, bool super, + bool numLock, bool capsLock, bool scrollLock) +{ + input->numLock = numLock; + input->capsLock = capsLock; + input->scrollLock = scrollLock; + input->sink->modifiers(input->opaque, ctrl, shift, alt, super); + input->sink->leds(input->opaque, numLock, capsLock, scrollLock); +} + +void windowsInputTrackedState(WindowsInput * input) +{ + input->sink->modifiers(input->opaque, + input->ctrlKeys != 0, input->shiftKeys != 0, + input->altKeys != 0, input->superKeys != 0); + input->sink->leds(input->opaque, + input->numLock, input->capsLock, input->scrollLock); +} diff --git a/client/displayservers/Windows/input_event.h b/client/displayservers/Windows/input_event.h new file mode 100644 index 00000000..978d7ef6 --- /dev/null +++ b/client/displayservers/Windows/input_event.h @@ -0,0 +1,91 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_LG_WINDOWS_INPUT_EVENT_ +#define _H_LG_WINDOWS_INPUT_EVENT_ + +#include +#include +#include + +#include "input/keycodes.h" + +#include "../input.h" + +typedef struct WindowsInput +{ + const LG_DSInputSink * sink; + void * opaque; + uint32_t buttons; + double wheel; + uint8_t ctrlKeys; + uint8_t shiftKeys; + uint8_t altKeys; + uint8_t superKeys; + int printScreenKey; + bool entered; + bool leavePending; + bool focused; + bool pointerGrabbed; + bool keyboardGrabbed; + bool numLock; + bool capsLock; + bool scrollLock; + bool numLockPressed; + bool capsLockPressed; + bool scrollLockPressed; +} +WindowsInput; + +void windowsInputInit(WindowsInput * input, const LG_DSInputSink * sink, + void * opaque); + +void windowsInputSetPointerGrabbed(WindowsInput * input, bool grabbed); +bool windowsInputIsPointerGrabbed(const WindowsInput * input); +void windowsInputSetKeyboardGrabbed(WindowsInput * input, bool grabbed); +bool windowsInputIsKeyboardGrabbed(const WindowsInput * input); + +bool windowsInputFocus(WindowsInput * input, bool focused); +bool windowsInputPointerEnter(WindowsInput * input, double x, double y); +bool windowsInputPointerLeave(WindowsInput * input); +void windowsInputPointerMotion(WindowsInput * input, double x, double y); +void windowsInputRelativeMotion(WindowsInput * input, double x, double y); +bool windowsInputPointerButton(WindowsInput * input, unsigned int button, + bool pressed); +void windowsInputReleaseButtons(WindowsInput * input); +void windowsInputPointerWheel(WindowsInput * input, double motion); + +int windowsInputMapScanCode(uint8_t scanCode, bool extended); +int windowsInputMapPrintScreen(bool altDown); +int windowsInputMapPrintScreenEvent(WindowsInput * input, + bool altDown, bool pressed); +bool windowsInputKeyCode(WindowsInput * input, int key, bool pressed, + bool grabbedEvent); +bool windowsInputKey(WindowsInput * input, uint8_t scanCode, bool extended, + bool pressed, bool grabbedEvent); +size_t windowsInputSyncHeldKeys(WindowsInput * input, + const int * keys, size_t count, bool grabbedEvent); +void windowsInputText(WindowsInput * input, const char * text); +void windowsInputState(WindowsInput * input, + bool ctrl, bool shift, bool alt, bool super, + bool numLock, bool capsLock, bool scrollLock); +void windowsInputTrackedState(WindowsInput * input); + +#endif diff --git a/client/displayservers/Windows/platform.h b/client/displayservers/Windows/platform.h new file mode 100644 index 00000000..3f7adbcd --- /dev/null +++ b/client/displayservers/Windows/platform.h @@ -0,0 +1,33 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_LG_WINDOWS_PLATFORM_ +#define _H_LG_WINDOWS_PLATFORM_ + +#include +#include + +typedef bool (*WindowsMessageHandler)(void * opaque, HWND window, + UINT message, WPARAM wParam, LPARAM lParam, LRESULT * result); + +HWND windowsGetWindow(void); +void windowsSetMessageHandler(WindowsMessageHandler handler, void * opaque); + +#endif diff --git a/client/displayservers/Windows/window_size.c b/client/displayservers/Windows/window_size.c new file mode 100644 index 00000000..22d29ca2 --- /dev/null +++ b/client/displayservers/Windows/window_size.c @@ -0,0 +1,50 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "window_size.h" + +void windowsWindowSizeInit(WindowsWindowSize * state, + int width, int height) +{ + state->width = width; + state->height = height; + state->pending = false; +} + +bool windowsWindowSizeRequest(WindowsWindowSize * state, + int width, int height, bool defer) +{ + state->width = width; + state->height = height; + state->pending = defer; + return !defer; +} + +bool windowsWindowSizeTakePending(WindowsWindowSize * state, + bool defer, int * width, int * height) +{ + if (!state->pending || defer) + return false; + + *width = state->width; + *height = state->height; + state->pending = false; + return true; +} diff --git a/client/displayservers/Windows/window_size.h b/client/displayservers/Windows/window_size.h new file mode 100644 index 00000000..bce07f1a --- /dev/null +++ b/client/displayservers/Windows/window_size.h @@ -0,0 +1,41 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_LG_WINDOWS_WINDOW_SIZE_ +#define _H_LG_WINDOWS_WINDOW_SIZE_ + +#include + +typedef struct WindowsWindowSize +{ + int width; + int height; + bool pending; +} +WindowsWindowSize; + +void windowsWindowSizeInit(WindowsWindowSize * state, + int width, int height); +bool windowsWindowSizeRequest(WindowsWindowSize * state, + int width, int height, bool defer); +bool windowsWindowSizeTakePending(WindowsWindowSize * state, + bool defer, int * width, int * height); + +#endif diff --git a/client/displayservers/Windows/windows.c b/client/displayservers/Windows/windows.c new file mode 100644 index 00000000..14e5a7ae --- /dev/null +++ b/client/displayservers/Windows/windows.c @@ -0,0 +1,1847 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include +#include + +#undef SW_MAX + +#include "interface/displayserver.h" + +#include "input_event.h" +#include "platform.h" +#include "window_size.h" + +#include "app.h" +#include "common/debug.h" +#include "common/event.h" +#include "common/thread.h" +#include "common/time.h" +#include "common/win32.h" +#include "common/windebug.h" +#include "egl_dynprocs.h" +#include "eglutil.h" +#include "input/keycodes.h" +#include "kb.h" + +#undef SW_MAX + +#ifdef ENABLE_EGL +#include +#include +#endif + +#include +#include +#include +#include +#include + +#define LG_WINDOW_CLASS L"LookingGlassClientWindow" + +enum WindowsMessage +{ + WM_LG_SHUTDOWN = WM_APP + 1, + WM_LG_SET_POINTER, + WM_LG_GRAB_POINTER, + WM_LG_GRAB_KEYBOARD, + WM_LG_WARP_POINTER, + WM_LG_GUEST_POINTER, + WM_LG_SET_SIZE, + WM_LG_SET_FULLSCREEN, + WM_LG_MINIMIZE, + WM_LG_ACTIVATE, + WM_LG_INHIBIT_IDLE, + WM_LG_SET_HANDLER, +}; + +struct WindowsPointMessage +{ + int x; + int y; + bool exiting; +}; + +struct WindowsGuestPointerMessage +{ + double localX; + double localY; +}; + +struct WindowsHandlerMessage +{ + WindowsMessageHandler handler; + void * opaque; +}; + +struct WindowsDSState +{ + HINSTANCE instance; + HWND window; + DWORD threadID; + HHOOK keyboardHook; + HCURSOR cursors[LG_POINTER_COUNT]; + + LGThread * eventThread; + LGEvent * initEvent; + + WindowsInput input; + + WindowsMessageHandler messageHandler; + void * messageOpaque; + + WindowsWindowSize windowSize; + + wchar_t * title; + int x; + int y; + bool center; + bool resizable; + bool borderless; + bool maximize; + bool initialFullscreen; + bool initOK; + bool dispatchReady; + bool idleInhibited; + + DWORD windowedStyle; + DWORD windowedExStyle; + WINDOWPLACEMENT windowedPlacement; + + UINT dpi; + double scale; + int rawAbsoluteX; + int rawAbsoluteY; + bool rawAbsoluteValid; + bool mouseLeaveTracked; + wchar_t highSurrogate; + + _Atomic(int) pointer; + _Atomic(bool) pointerGrabbed; + _Atomic(bool) pointerCaptured; + _Atomic(bool) keyboardGrabbed; + _Atomic(bool) fullscreen; + _Atomic(bool) stopping; +}; + +static struct WindowsDSState windows; + +static LRESULT CALLBACK windowProc(HWND window, UINT message, + WPARAM wParam, LPARAM lParam); +static LRESULT CALLBACK keyboardHookProc(int code, WPARAM wParam, + LPARAM lParam); +static void armPointerLeave(void); +static void requestWindowSize(int width, int height); +static void applyPendingWindowSize(void); + +static UINT queryWindowDPI(HWND window) +{ + typedef UINT (WINAPI * GetDpiForWindowFn)(HWND window); + + const HMODULE user32 = GetModuleHandleW(L"user32.dll"); + const FARPROC procedure = user32 ? + GetProcAddress(user32, "GetDpiForWindow") : NULL; + GetDpiForWindowFn getDpiForWindow = NULL; + memcpy(&getDpiForWindow, &procedure, sizeof(getDpiForWindow)); + if (getDpiForWindow) + { + const UINT dpi = getDpiForWindow(window); + if (dpi) + return dpi; + } + + HDC dc = GetDC(window); + if (!dc) + return 96; + + const int dpi = GetDeviceCaps(dc, LOGPIXELSX); + ReleaseDC(window, dc); + return dpi > 0 ? (UINT)dpi : 96; +} + +static double logicalScale(void) +{ + return windows.scale > 0.0 ? windows.scale : 1.0; +} + +static int toLogical(int value) +{ + return (int)(value / logicalScale() + 0.5); +} + +static int toPhysical(int value) +{ + return (int)(value * logicalScale() + 0.5); +} + +static void updateWindowGeometry(void) +{ + RECT client; + if (!GetClientRect(windows.window, &client)) + return; + + POINT origin = { 0, 0 }; + ClientToScreen(windows.window, &origin); + + windows.dpi = queryWindowDPI(windows.window); + windows.scale = (double)windows.dpi / 96.0; + + const int width = client.right - client.left; + const int height = client.bottom - client.top; + app_updateWindowPos(toLogical(origin.x), toLogical(origin.y)); + app_handleResizeEvent(toLogical(width), toLogical(height), windows.scale, + (struct Border) { 0, 0, 0, 0 }); + app_invalidateWindow(true); +} + +static bool cursorInClient(POINT point) +{ + RECT client; + if (!GetClientRect(windows.window, &client)) + return false; + + return point.x >= client.left && point.x < client.right && + point.y >= client.top && point.y < client.bottom; +} + +static void updatePointerClip(void) +{ + if (!windowsInputIsPointerGrabbed(&windows.input) || + GetForegroundWindow() != windows.window) + { + ClipCursor(NULL); + return; + } + + RECT clip; + if (!GetClientRect(windows.window, &clip)) + return; + + MapWindowPoints(windows.window, NULL, (POINT *)&clip, 2); + ClipCursor(&clip); +} + +static void emitInputState(void) +{ + const bool ctrl = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; + const bool shift = (GetAsyncKeyState(VK_SHIFT ) & 0x8000) != 0; + const bool alt = (GetAsyncKeyState(VK_MENU ) & 0x8000) != 0; + const bool super = (GetAsyncKeyState(VK_LWIN ) & 0x8000) != 0 || + (GetAsyncKeyState(VK_RWIN) & 0x8000) != 0; + const bool num = (GetKeyState(VK_NUMLOCK) & 1) != 0; + const bool caps = (GetKeyState(VK_CAPITAL) & 1) != 0; + const bool scroll = (GetKeyState(VK_SCROLL ) & 1) != 0; + + windowsInputState(&windows.input, ctrl, shift, alt, super, + num, caps, scroll); +} + +static int keyCode(WPARAM virtualKey, uint8_t scanCode, bool extended, + bool altDown, bool pressed) +{ + if (virtualKey == VK_PAUSE) + return LG_KEY_PAUSE; + if (virtualKey == VK_SNAPSHOT) + return windowsInputMapPrintScreenEvent( + &windows.input, altDown, pressed); + + return windowsInputMapScanCode(scanCode, extended); +} + +static bool dispatchKey(WPARAM virtualKey, uint8_t scanCode, + bool extended, bool altDown, bool pressed, bool grabbedEvent) +{ + const int key = keyCode( + virtualKey, scanCode, extended, altDown, pressed); + const bool handled = windowsInputKeyCode(&windows.input, key, + pressed, grabbedEvent); + if (handled) + { + if (grabbedEvent) + windowsInputTrackedState(&windows.input); + else + emitInputState(); + } + return handled; +} + +static bool virtualKeyHeld(UINT virtualKey) +{ + return (GetAsyncKeyState((int)virtualKey) & 0x8000) != 0; +} + +static bool mouseVirtualKey(UINT virtualKey) +{ + return virtualKey == VK_LBUTTON || virtualKey == VK_RBUTTON || + virtualKey == VK_MBUTTON || virtualKey == VK_XBUTTON1 || + virtualKey == VK_XBUTTON2; +} + +static bool heldKeyAlreadyAdded(const int * keys, size_t count, int key) +{ + for (size_t i = 0; i < count; ++i) + if (keys[i] == key) + return true; + + return false; +} + +static size_t queryHeldKeys(int * keys, size_t capacity) +{ + const bool altDown = virtualKeyHeld(VK_MENU); + size_t count = 0; + + for (UINT virtualKey = 1; virtualKey < 0xFF && count < capacity; + ++virtualKey) + { + if (!virtualKeyHeld(virtualKey) || + mouseVirtualKey(virtualKey) || + virtualKey == VK_SHIFT || + virtualKey == VK_CONTROL || + virtualKey == VK_MENU) + continue; + + const UINT scanCodeEx = MapVirtualKeyW( + virtualKey, MAPVK_VK_TO_VSC_EX); + const bool extended = (scanCodeEx & 0xFF00U) == 0xE000U; + const int key = keyCode(virtualKey, + (uint8_t)(scanCodeEx & 0xFFU), extended, altDown, true); + if (key <= LG_KEY_RESERVED || key >= LG_KEY_MAX || + heldKeyAlreadyAdded(keys, count, key)) + continue; + + keys[count++] = key; + } + + return count; +} + +static void syncHeldKeys(void) +{ + int keys[256]; + const size_t count = queryHeldKeys(keys, sizeof(keys) / sizeof(*keys)); + windowsInputSyncHeldKeys(&windows.input, keys, count, + windowsInputIsKeyboardGrabbed(&windows.input)); +} + +static void emitCodepoint(uint32_t codepoint) +{ + char text[5] = { 0 }; + if (codepoint <= 0x7F) + text[0] = (char)codepoint; + else if (codepoint <= 0x7FF) + { + text[0] = (char)(0xC0 | codepoint >> 6); + text[1] = (char)(0x80 | (codepoint & 0x3F)); + } + else if (codepoint <= 0xFFFF) + { + text[0] = (char)(0xE0 | codepoint >> 12); + text[1] = (char)(0x80 | ((codepoint >> 6) & 0x3F)); + text[2] = (char)(0x80 | (codepoint & 0x3F)); + } + else if (codepoint <= 0x10FFFF) + { + text[0] = (char)(0xF0 | codepoint >> 18); + text[1] = (char)(0x80 | ((codepoint >> 12) & 0x3F)); + text[2] = (char)(0x80 | ((codepoint >> 6) & 0x3F)); + text[3] = (char)(0x80 | (codepoint & 0x3F)); + } + + if (text[0]) + windowsInputText(&windows.input, text); +} + +static void handleUTF16(wchar_t value) +{ + if (value >= 0xD800 && value <= 0xDBFF) + { + windows.highSurrogate = value; + return; + } + + uint32_t codepoint = value; + if (value >= 0xDC00 && value <= 0xDFFF && + windows.highSurrogate >= 0xD800 && + windows.highSurrogate <= 0xDBFF) + { + codepoint = UINT32_C(0x10000) + + ((uint32_t)(windows.highSurrogate - 0xD800) << 10) + + (uint32_t)(value - 0xDC00); + } + + windows.highSurrogate = 0; + if (codepoint < 0xD800 || codepoint > 0xDFFF) + emitCodepoint(codepoint); +} + +static unsigned int mouseButton(UINT message, WPARAM wParam) +{ + switch (message) + { + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + return 1; + + case WM_MBUTTONDOWN: + case WM_MBUTTONUP: + return 2; + + case WM_RBUTTONDOWN: + case WM_RBUTTONUP: + return 3; + + case WM_XBUTTONDOWN: + case WM_XBUTTONUP: + return GET_XBUTTON_WPARAM(wParam) == XBUTTON1 ? 6 : 7; + } + + return 0; +} + +static bool mouseButtonPressed(UINT message) +{ + return message == WM_LBUTTONDOWN || message == WM_MBUTTONDOWN || + message == WM_RBUTTONDOWN || message == WM_XBUTTONDOWN; +} + +static void checkPointerLeave(void) +{ + POINT point; + if (!GetCursorPos(&point)) + return; + ScreenToClient(windows.window, &point); + if (cursorInClient(point)) + { + armPointerLeave(); + windowsInputPointerEnter(&windows.input, + point.x / logicalScale(), point.y / logicalScale()); + } + else + windowsInputPointerLeave(&windows.input); +} + +static void armPointerLeave(void) +{ + if (windows.mouseLeaveTracked) + return; + + TRACKMOUSEEVENT track = + { + .cbSize = sizeof(track), + .dwFlags = TME_LEAVE, + .hwndTrack = windows.window, + .dwHoverTime = HOVER_DEFAULT, + }; + windows.mouseLeaveTracked = TrackMouseEvent(&track) != FALSE; +} + +static void handleMouseButton(UINT message, WPARAM wParam) +{ + const unsigned int button = mouseButton(message, wParam); + const bool pressed = mouseButtonPressed(message); + if (!windows.input.entered) + { + POINT point; + if (GetCursorPos(&point)) + { + ScreenToClient(windows.window, &point); + if (cursorInClient(point)) + { + armPointerLeave(); + windowsInputPointerEnter(&windows.input, + point.x / logicalScale(), point.y / logicalScale()); + } + } + } + + if (!windowsInputPointerButton(&windows.input, button, pressed)) + return; + + if (pressed) + SetCapture(windows.window); + else if (!windows.input.buttons && + !windowsInputIsPointerGrabbed(&windows.input)) + { + ReleaseCapture(); + checkPointerLeave(); + } +} + +static void handleMouseMove(LPARAM lParam) +{ + const int x = GET_X_LPARAM(lParam); + const int y = GET_Y_LPARAM(lParam); + const POINT point = { .x = x, .y = y }; + if (!cursorInClient(point)) + { + windowsInputPointerLeave(&windows.input); + return; + } + + armPointerLeave(); + if (!windowsInputPointerEnter(&windows.input, + x / logicalScale(), y / logicalScale())) + windowsInputPointerMotion(&windows.input, + x / logicalScale(), y / logicalScale()); +} + +static void handleRawMouse(const RAWMOUSE * mouse) +{ + double x; + double y; + if (mouse->usFlags & MOUSE_MOVE_ABSOLUTE) + { + const bool virtualDesktop = + (mouse->usFlags & MOUSE_VIRTUAL_DESKTOP) != 0; + const int originX = virtualDesktop ? + GetSystemMetrics(SM_XVIRTUALSCREEN) : 0; + const int originY = virtualDesktop ? + GetSystemMetrics(SM_YVIRTUALSCREEN) : 0; + const int width = GetSystemMetrics( + virtualDesktop ? SM_CXVIRTUALSCREEN : SM_CXSCREEN); + const int height = GetSystemMetrics( + virtualDesktop ? SM_CYVIRTUALSCREEN : SM_CYSCREEN); + const int currentX = originX + + (int)((uint64_t)(uint32_t)mouse->lLastX * (uint32_t)width / 65535U); + const int currentY = originY + + (int)((uint64_t)(uint32_t)mouse->lLastY * (uint32_t)height / 65535U); + + if (!windows.rawAbsoluteValid) + { + windows.rawAbsoluteX = currentX; + windows.rawAbsoluteY = currentY; + windows.rawAbsoluteValid = true; + return; + } + + x = currentX - windows.rawAbsoluteX; + y = currentY - windows.rawAbsoluteY; + windows.rawAbsoluteX = currentX; + windows.rawAbsoluteY = currentY; + } + else + { + windows.rawAbsoluteValid = false; + + x = mouse->lLastX; + y = mouse->lLastY; + } + + if (x != 0.0 || y != 0.0) + windowsInputRelativeMotion(&windows.input, x, y); +} + +static void handleRawInput(HRAWINPUT handle) +{ + if (!windows.input.focused || + !windowsInputIsPointerGrabbed(&windows.input) || + GetForegroundWindow() != windows.window) + { + windows.rawAbsoluteValid = false; + return; + } + + RAWINPUT input; + UINT size = sizeof(input); + if (GetRawInputData(handle, RID_INPUT, &input, &size, + sizeof(RAWINPUTHEADER)) != size) + return; + + if (input.header.dwType == RIM_TYPEMOUSE) + handleRawMouse(&input.data.mouse); +} + +static bool installKeyboardHook(void) +{ + if (windows.keyboardHook) + return true; + + windows.keyboardHook = SetWindowsHookExW( + WH_KEYBOARD_LL, keyboardHookProc, windows.instance, 0); + if (!windows.keyboardHook) + { + DEBUG_WINERROR("SetWindowsHookExW failed", GetLastError()); + return false; + } + + return true; +} + +static void removeKeyboardHook(void) +{ + if (!windows.keyboardHook) + return; + + UnhookWindowsHookEx(windows.keyboardHook); + windows.keyboardHook = NULL; +} + +static void setKeyboardGrab(bool grab) +{ + if (grab == windowsInputIsKeyboardGrabbed(&windows.input)) + return; + + if (grab && !installKeyboardHook()) + return; + + if (!grab) + removeKeyboardHook(); + + windowsInputSetKeyboardGrabbed(&windows.input, grab); + atomic_store_explicit(&windows.keyboardGrabbed, grab, + memory_order_release); +} + +static void setPointerGrab(bool grab, bool captured) +{ + const bool old = windowsInputIsPointerGrabbed(&windows.input); + if (grab) + { + windowsInputSetPointerGrabbed(&windows.input, true); + windows.rawAbsoluteValid = false; + SetCapture(windows.window); + updatePointerClip(); + } + else + { + ClipCursor(NULL); + windowsInputSetPointerGrabbed(&windows.input, false); + if (!windows.input.buttons) + ReleaseCapture(); + } + + atomic_store_explicit(&windows.pointerGrabbed, grab, + memory_order_release); + atomic_store_explicit(&windows.pointerCaptured, grab && captured, + memory_order_release); + if (old != grab) + app_handleGrabEvent(grab); + if (!grab) + checkPointerLeave(); +} + +static void releasePointerState(bool releaseCapture) +{ + windowsInputReleaseButtons(&windows.input); + const bool captured = atomic_load_explicit( + &windows.pointerCaptured, memory_order_acquire); + if (windowsInputIsPointerGrabbed(&windows.input)) + { + ClipCursor(NULL); + windowsInputSetPointerGrabbed(&windows.input, false); + atomic_store_explicit(&windows.pointerGrabbed, false, + memory_order_release); + atomic_store_explicit(&windows.pointerCaptured, false, + memory_order_release); + if (releaseCapture) + ReleaseCapture(); + + if (captured) + app_handleCaptureLost(); + else + app_handleGrabEvent(false); + } + else if (releaseCapture) + ReleaseCapture(); + + checkPointerLeave(); +} + +static void setFullscreen(bool fullscreen) +{ + const bool old = atomic_load_explicit( + &windows.fullscreen, memory_order_acquire); + if (old == fullscreen) + return; + + if (fullscreen) + { + atomic_store_explicit(&windows.fullscreen, true, + memory_order_release); + windows.windowedStyle = (DWORD)GetWindowLongPtrW( + windows.window, GWL_STYLE); + windows.windowedExStyle = (DWORD)GetWindowLongPtrW( + windows.window, GWL_EXSTYLE); + windows.windowedPlacement = (WINDOWPLACEMENT) + { + .length = sizeof(windows.windowedPlacement), + }; + GetWindowPlacement(windows.window, &windows.windowedPlacement); + + const HMONITOR monitor = MonitorFromWindow( + windows.window, MONITOR_DEFAULTTONEAREST); + MONITORINFO info = { .cbSize = sizeof(info) }; + GetMonitorInfoW(monitor, &info); + + SetWindowLongPtrW(windows.window, GWL_STYLE, + WS_POPUP | WS_VISIBLE); + SetWindowLongPtrW(windows.window, GWL_EXSTYLE, + windows.windowedExStyle & ~(DWORD)WS_EX_WINDOWEDGE); + SetWindowPos(windows.window, HWND_TOP, + info.rcMonitor.left, info.rcMonitor.top, + info.rcMonitor.right - info.rcMonitor.left, + info.rcMonitor.bottom - info.rcMonitor.top, + SWP_FRAMECHANGED | SWP_NOOWNERZORDER); + } + else + { + SetWindowLongPtrW(windows.window, GWL_STYLE, + windows.windowedStyle); + SetWindowLongPtrW(windows.window, GWL_EXSTYLE, + windows.windowedExStyle); + SetWindowPlacement(windows.window, &windows.windowedPlacement); + SetWindowPos(windows.window, NULL, 0, 0, 0, 0, + SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | + SWP_NOOWNERZORDER | SWP_NOZORDER); + atomic_store_explicit(&windows.fullscreen, false, + memory_order_release); + applyPendingWindowSize(); + } +} + +static bool deferWindowResize(void) +{ + return atomic_load_explicit( + &windows.fullscreen, memory_order_acquire) || + IsZoomed(windows.window); +} + +static void applyWindowSize(int width, int height) +{ + RECT rect = + { + .left = 0, + .top = 0, + .right = toPhysical(width), + .bottom = toPhysical(height), + }; + const DWORD style = (DWORD)GetWindowLongPtrW( + windows.window, GWL_STYLE); + const DWORD exStyle = (DWORD)GetWindowLongPtrW( + windows.window, GWL_EXSTYLE); + AdjustWindowRectExForDpi(&rect, style, FALSE, exStyle, windows.dpi); + SetWindowPos(windows.window, NULL, 0, 0, + rect.right - rect.left, rect.bottom - rect.top, + SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOZORDER); +} + +static void requestWindowSize(int width, int height) +{ + if (windowsWindowSizeRequest( + &windows.windowSize, width, height, deferWindowResize())) + applyWindowSize(width, height); +} + +static void applyPendingWindowSize(void) +{ + int width; + int height; + if (windowsWindowSizeTakePending(&windows.windowSize, + deferWindowResize(), &width, &height)) + applyWindowSize(width, height); +} + +static void centerWindow(void) +{ + const HMONITOR monitor = MonitorFromWindow( + windows.window, MONITOR_DEFAULTTONEAREST); + MONITORINFO info = { .cbSize = sizeof(info) }; + RECT window; + if (!GetMonitorInfoW(monitor, &info) || + !GetWindowRect(windows.window, &window)) + return; + + const int width = window.right - window.left; + const int height = window.bottom - window.top; + const int x = info.rcWork.left + + (info.rcWork.right - info.rcWork.left - width) / 2; + const int y = info.rcWork.top + + (info.rcWork.bottom - info.rcWork.top - height) / 2; + SetWindowPos(windows.window, NULL, x, y, 0, 0, + SWP_NOSIZE | SWP_NOOWNERZORDER | SWP_NOZORDER); +} + +static void setPointerPosition(const struct WindowsPointMessage * point) +{ + POINT position = + { + .x = toPhysical(point->x), + .y = toPhysical(point->y), + }; + ClientToScreen(windows.window, &position); + SetCursorPos(position.x, position.y); +} + +static void setGuestPointerPosition( + const struct WindowsGuestPointerMessage * point) +{ + if (atomic_load_explicit( + &windows.pointerCaptured, memory_order_acquire) || + !windows.input.entered) + return; + + struct WindowsPointMessage position = + { + .x = (int)(point->localX + 0.5), + .y = (int)(point->localY + 0.5), + }; + setPointerPosition(&position); +} + +static void setCursorShape(LG_DSPointer pointer) +{ + const int index = (int)pointer; + if (index < LG_POINTER_NONE || index >= LG_POINTER_COUNT) + pointer = LG_POINTER_ARROW; + + atomic_store_explicit(&windows.pointer, pointer, memory_order_release); + POINT point; + if (windowsInputIsPointerGrabbed(&windows.input) || + (GetCursorPos(&point) && + ScreenToClient(windows.window, &point) && cursorInClient(point))) + SetCursor(windows.cursors[pointer]); +} + +static LRESULT handleCustomMessage(UINT message, WPARAM wParam, + LPARAM lParam) +{ + switch (message) + { + case WM_LG_SHUTDOWN: + setKeyboardGrab(false); + setPointerGrab(false, false); + if (windows.idleInhibited) + SetThreadExecutionState(ES_CONTINUOUS); + DestroyWindow(windows.window); + return 0; + + case WM_LG_SET_POINTER: + setCursorShape((LG_DSPointer)wParam); + return 0; + + case WM_LG_GRAB_POINTER: + setPointerGrab(wParam != 0, lParam != 0); + return 0; + + case WM_LG_GRAB_KEYBOARD: + setKeyboardGrab(wParam != 0); + return 0; + + case WM_LG_WARP_POINTER: + setPointerPosition((const struct WindowsPointMessage *)lParam); + return 0; + + case WM_LG_GUEST_POINTER: + setGuestPointerPosition( + (const struct WindowsGuestPointerMessage *)lParam); + return 0; + + case WM_LG_SET_SIZE: + requestWindowSize((int)wParam, (int)lParam); + return 0; + + case WM_LG_SET_FULLSCREEN: + setFullscreen(wParam != 0); + return 0; + + case WM_LG_MINIMIZE: + ShowWindow(windows.window, SW_MINIMIZE); + return 0; + + case WM_LG_ACTIVATE: + { + FLASHWINFO flash = + { + .cbSize = sizeof(flash), + .hwnd = windows.window, + .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG, + .uCount = 3, + }; + SetForegroundWindow(windows.window); + FlashWindowEx(&flash); + return 0; + } + + case WM_LG_INHIBIT_IDLE: + windows.idleInhibited = wParam != 0; + SetThreadExecutionState(ES_CONTINUOUS | + (windows.idleInhibited ? ES_DISPLAY_REQUIRED : 0)); + return 0; + + case WM_LG_SET_HANDLER: + { + const struct WindowsHandlerMessage * handler = + (const struct WindowsHandlerMessage *)lParam; + windows.messageHandler = handler->handler; + windows.messageOpaque = handler->opaque; + return 0; + } + } + + return 0; +} + +static LRESULT CALLBACK windowProc(HWND window, UINT message, + WPARAM wParam, LPARAM lParam) +{ + if (message >= WM_LG_SHUTDOWN && message <= WM_LG_SET_HANDLER) + return handleCustomMessage(message, wParam, lParam); + + if (windows.dispatchReady && windows.messageHandler) + { + LRESULT result = 0; + if (windows.messageHandler(windows.messageOpaque, window, + message, wParam, lParam, &result)) + return result; + } + + switch (message) + { + case WM_CLOSE: + app_handleCloseEvent(); + return 0; + + case WM_DESTROY: + PostQuitMessage(0); + return 0; + + case WM_ERASEBKGND: + return 1; + + case WM_NCHITTEST: + if (windows.borderless && windows.resizable && + !atomic_load_explicit(&windows.fullscreen, memory_order_acquire)) + { + RECT bounds; + if (!GetWindowRect(window, &bounds)) + break; + + const int x = GET_X_LPARAM(lParam); + const int y = GET_Y_LPARAM(lParam); + const int margin = toPhysical(8); + const bool left = x < bounds.left + margin; + const bool right = x >= bounds.right - margin; + const bool top = y < bounds.top + margin; + const bool bottom = y >= bounds.bottom - margin; + + if (top && left) + return HTTOPLEFT; + if (top && right) + return HTTOPRIGHT; + if (bottom && left) + return HTBOTTOMLEFT; + if (bottom && right) + return HTBOTTOMRIGHT; + if (left) + return HTLEFT; + if (right) + return HTRIGHT; + if (top) + return HTTOP; + if (bottom) + return HTBOTTOM; + } + break; + + case WM_PAINT: + { + PAINTSTRUCT paint; + BeginPaint(window, &paint); + EndPaint(window, &paint); + app_invalidateWindow(true); + return 0; + } + + case WM_MOVE: + if (windows.dispatchReady && !IsIconic(window)) + { + updateWindowGeometry(); + updatePointerClip(); + } + return 0; + + case WM_SIZE: + if (windows.dispatchReady && wParam != SIZE_MINIMIZED) + { + updateWindowGeometry(); + updatePointerClip(); + if (wParam == SIZE_RESTORED) + applyPendingWindowSize(); + } + return 0; + + case WM_DPICHANGED: + if (windows.dispatchReady) + { + const RECT * suggested = (const RECT *)lParam; + SetWindowPos(window, NULL, + suggested->left, suggested->top, + suggested->right - suggested->left, + suggested->bottom - suggested->top, + SWP_NOOWNERZORDER | SWP_NOZORDER); + updateWindowGeometry(); + } + return 0; + + case WM_SETFOCUS: + if (windows.dispatchReady) + { + const bool changed = windowsInputFocus(&windows.input, true); + if (changed) + syncHeldKeys(); + emitInputState(); + updatePointerClip(); + } + return 0; + + case WM_KILLFOCUS: + if (windows.dispatchReady) + { + releasePointerState(true); + setKeyboardGrab(false); + windowsInputFocus(&windows.input, false); + windows.rawAbsoluteValid = false; + } + return 0; + + case WM_MOUSEMOVE: + if (windows.dispatchReady) + handleMouseMove(lParam); + return 0; + + case WM_MOUSELEAVE: + if (windows.dispatchReady) + { + windows.mouseLeaveTracked = false; + windowsInputPointerLeave(&windows.input); + } + return 0; + + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_MBUTTONDOWN: + case WM_MBUTTONUP: + case WM_RBUTTONDOWN: + case WM_RBUTTONUP: + case WM_XBUTTONDOWN: + case WM_XBUTTONUP: + if (windows.dispatchReady) + handleMouseButton(message, wParam); + return message == WM_XBUTTONDOWN || message == WM_XBUTTONUP ? + TRUE : 0; + + case WM_MOUSEWHEEL: + if (windows.dispatchReady) + windowsInputPointerWheel(&windows.input, + (double)GET_WHEEL_DELTA_WPARAM(wParam) / WHEEL_DELTA); + return 0; + + case WM_INPUT: + if (windows.dispatchReady) + handleRawInput((HRAWINPUT)lParam); + return DefWindowProcW(window, message, wParam, lParam); + + case WM_CAPTURECHANGED: + if (windows.dispatchReady && (HWND)lParam != windows.window) + releasePointerState(false); + return 0; + + case WM_CANCELMODE: + if (windows.dispatchReady) + releasePointerState(true); + return 0; + + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + case WM_KEYUP: + case WM_SYSKEYUP: + if (windows.dispatchReady) + { + const bool pressed = message == WM_KEYDOWN || + message == WM_SYSKEYDOWN; + dispatchKey(wParam, (uint8_t)((lParam >> 16) & 0xFF), + (lParam & (UINT32_C(1) << 24)) != 0, + (lParam & (UINT32_C(1) << 29)) != 0, pressed, false); + } + return 0; + + case WM_CHAR: + case WM_SYSCHAR: + if (windows.dispatchReady) + handleUTF16((wchar_t)wParam); + return 0; + + case WM_UNICHAR: + if (wParam == UNICODE_NOCHAR) + return TRUE; + if (windows.dispatchReady) + emitCodepoint((uint32_t)wParam); + return 0; + + case WM_SETCURSOR: + if (LOWORD(lParam) == HTCLIENT) + { + const int pointer = atomic_load_explicit( + &windows.pointer, memory_order_acquire); + SetCursor(windows.cursors[pointer]); + return TRUE; + } + break; + } + + return DefWindowProcW(window, message, wParam, lParam); +} + +static LRESULT CALLBACK keyboardHookProc(int code, WPARAM wParam, + LPARAM lParam) +{ + if (code == HC_ACTION && + windowsInputIsKeyboardGrabbed(&windows.input) && + windows.input.focused && + GetForegroundWindow() == windows.window) + { + const KBDLLHOOKSTRUCT * event = (const KBDLLHOOKSTRUCT *)lParam; + const bool pressed = wParam == WM_KEYDOWN || + wParam == WM_SYSKEYDOWN; + const bool released = wParam == WM_KEYUP || + wParam == WM_SYSKEYUP; + if (pressed || released) + { + dispatchKey(event->vkCode, (uint8_t)event->scanCode, + (event->flags & LLKHF_EXTENDED) != 0, + (event->flags & LLKHF_ALTDOWN) != 0, pressed, true); + return 1; + } + } + + return CallNextHookEx(windows.keyboardHook, code, wParam, lParam); +} + +static HCURSOR createSquareCursor(bool large) +{ + const int width = GetSystemMetrics(SM_CXCURSOR); + const int height = GetSystemMetrics(SM_CYCURSOR); + if (width <= 0 || height <= 0) + return NULL; + + const size_t stride = ((size_t)width + 15) / 16 * 2; + if ((size_t)height > SIZE_MAX / stride) + return NULL; + + const size_t size = stride * (size_t)height; + uint8_t * andMask = malloc(size); + uint8_t * xorMask = calloc(size, 1); + if (!andMask || !xorMask) + { + free(andMask); + free(xorMask); + return NULL; + } + + memset(andMask, 0xFF, size); + int dotSize = large ? 9 : 5; + if (dotSize > width) + dotSize = width; + if (dotSize > height) + dotSize = height; + + const int left = (width - dotSize) / 2; + const int top = (height - dotSize) / 2; + for (int y = top; y < top + dotSize; ++y) + for (int x = left; x < left + dotSize; ++x) + andMask[(size_t)y * stride + (size_t)x / 8] &= + (uint8_t)~(UINT8_C(0x80) >> (x & 7)); + + HCURSOR cursor = CreateCursor(windows.instance, + width / 2, height / 2, width, height, andMask, xorMask); + free(andMask); + free(xorMask); + return cursor; +} + +static bool loadCursors(bool largeCursorDot) +{ + windows.cursors[LG_POINTER_NONE ] = NULL; + windows.cursors[LG_POINTER_SQUARE ] = createSquareCursor(largeCursorDot); + windows.cursors[LG_POINTER_ARROW ] = LoadCursorW(NULL, IDC_ARROW); + windows.cursors[LG_POINTER_INPUT ] = LoadCursorW(NULL, IDC_IBEAM); + windows.cursors[LG_POINTER_MOVE ] = LoadCursorW(NULL, IDC_SIZEALL); + windows.cursors[LG_POINTER_RESIZE_NS ] = LoadCursorW(NULL, IDC_SIZENS); + windows.cursors[LG_POINTER_RESIZE_EW ] = LoadCursorW(NULL, IDC_SIZEWE); + windows.cursors[LG_POINTER_RESIZE_NESW] = LoadCursorW(NULL, IDC_SIZENESW); + windows.cursors[LG_POINTER_RESIZE_NWSE] = LoadCursorW(NULL, IDC_SIZENWSE); + windows.cursors[LG_POINTER_HAND ] = LoadCursorW(NULL, IDC_HAND); + windows.cursors[LG_POINTER_NOT_ALLOWED] = LoadCursorW(NULL, IDC_NO); + if (!windows.cursors[LG_POINTER_SQUARE]) + { + DEBUG_WINERROR("CreateCursor failed", GetLastError()); + return false; + } + + return true; +} + +static void freeCursors(void) +{ + if (!windows.cursors[LG_POINTER_SQUARE]) + return; + + DestroyCursor(windows.cursors[LG_POINTER_SQUARE]); + windows.cursors[LG_POINTER_SQUARE] = NULL; +} + +static DWORD initialStyle(void) +{ + if (windows.borderless) + return WS_POPUP; + + DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | + WS_MINIMIZEBOX; + if (windows.resizable) + style |= WS_THICKFRAME | WS_MAXIMIZEBOX; + return style; +} + +static int eventThread(void * opaque) +{ + (void)opaque; + windows.threadID = GetCurrentThreadId(); + + HICON icon = LoadIconW(windows.instance, IDI_APPLICATION); + if (!icon) + icon = LoadIconW(NULL, IDI_APPLICATION); + + WNDCLASSEXW windowClass = + { + .cbSize = sizeof(windowClass), + .style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC, + .lpfnWndProc = windowProc, + .hInstance = windows.instance, + .hIcon = icon, + .hCursor = windows.cursors[LG_POINTER_ARROW], + .hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH), + .lpszClassName = LG_WINDOW_CLASS, + .hIconSm = icon, + }; + if (!RegisterClassExW(&windowClass)) + { + DEBUG_WINERROR("RegisterClassExW failed", GetLastError()); + lgSignalEvent(windows.initEvent); + return 0; + } + + windows.dpi = 96; + windows.scale = 1.0; + RECT rect = + { + .left = 0, + .top = 0, + .right = windows.windowSize.width, + .bottom = windows.windowSize.height, + }; + const DWORD style = initialStyle(); + const DWORD exStyle = windows.borderless ? 0 : WS_EX_APPWINDOW; + AdjustWindowRectEx(&rect, style, FALSE, exStyle); + + int x = windows.x; + int y = windows.y; + if (windows.center) + { + const HMONITOR monitor = MonitorFromPoint( + (POINT) { windows.x, windows.y }, MONITOR_DEFAULTTONEAREST); + MONITORINFO info = { .cbSize = sizeof(info) }; + GetMonitorInfoW(monitor, &info); + x = info.rcWork.left + + (info.rcWork.right - info.rcWork.left - (rect.right - rect.left)) / 2; + y = info.rcWork.top + + (info.rcWork.bottom - info.rcWork.top - (rect.bottom - rect.top)) / 2; + } + + windows.window = CreateWindowExW(exStyle, LG_WINDOW_CLASS, windows.title, + style, x, y, rect.right - rect.left, rect.bottom - rect.top, + NULL, NULL, windows.instance, NULL); + if (!windows.window) + { + DEBUG_WINERROR("CreateWindowExW failed", GetLastError()); + UnregisterClassW(LG_WINDOW_CLASS, windows.instance); + lgSignalEvent(windows.initEvent); + return 0; + } + + windows.dpi = queryWindowDPI(windows.window); + windows.scale = (double)windows.dpi / 96.0; + requestWindowSize(windows.windowSize.width, windows.windowSize.height); + if (windows.center) + centerWindow(); + + RAWINPUTDEVICE mouse = + { + .usUsagePage = 0x01, + .usUsage = 0x02, + .dwFlags = 0, + .hwndTarget = windows.window, + }; + if (!RegisterRawInputDevices(&mouse, 1, sizeof(mouse))) + { + DEBUG_WINERROR("RegisterRawInputDevices failed", GetLastError()); + DestroyWindow(windows.window); + windows.window = NULL; + UnregisterClassW(LG_WINDOW_CLASS, windows.instance); + lgSignalEvent(windows.initEvent); + return 0; + } + + windows.dispatchReady = true; + windows.initOK = true; + updateWindowGeometry(); + ShowWindow(windows.window, windows.maximize ? SW_MAXIMIZE : SW_SHOW); + UpdateWindow(windows.window); + if (windows.initialFullscreen) + setFullscreen(true); + lgSignalEvent(windows.initEvent); + + MSG message; + while (GetMessageW(&message, NULL, 0, 0) > 0) + { + TranslateMessage(&message); + DispatchMessageW(&message); + } + + windows.dispatchReady = false; + windows.window = NULL; + removeKeyboardHook(); + ClipCursor(NULL); + UnregisterClassW(LG_WINDOW_CLASS, windows.instance); + return 0; +} + +static void inputPosition(void * opaque, double x, double y) +{ + (void)opaque; + app_updateCursorPos(x, y); +} + +static void inputRelative(void * opaque, double x, double y, + double rawX, double rawY) +{ + (void)opaque; + app_handleMouseRelative(x, y, rawX, rawY); +} + +static void inputButton(void * opaque, unsigned int button, bool pressed) +{ + (void)opaque; + if (pressed) + app_handleButtonPress(button); + else + app_handleButtonRelease(button); +} + +static void inputWheel(void * opaque, double motion) +{ + (void)opaque; + app_handleWheelMotion(motion); +} + +static void inputKey(void * opaque, int scancode, bool pressed) +{ + (void)opaque; + if (pressed) + app_handleKeyPress(scancode); + else + app_handleKeyRelease(scancode); +} + +static void inputText(void * opaque, const char * text) +{ + (void)opaque; + app_handleKeyboardTyped(text); +} + +static void inputModifiers(void * opaque, bool ctrl, bool shift, + bool alt, bool super) +{ + (void)opaque; + app_handleKeyboardModifiers(ctrl, shift, alt, super); +} + +static void inputLEDs(void * opaque, bool numLock, bool capsLock, + bool scrollLock) +{ + (void)opaque; + app_handleKeyboardLEDs(numLock, capsLock, scrollLock); +} + +static void inputEnter(void * opaque, bool entered) +{ + (void)opaque; + app_handleEnterEvent(entered); +} + +static void inputFocus(void * opaque, bool focused) +{ + (void)opaque; + app_handleFocusEvent(focused); +} + +static const LG_DSInputSink inputSink = +{ + .position = inputPosition, + .relative = inputRelative, + .button = inputButton, + .wheel = inputWheel, + .key = inputKey, + .text = inputText, + .modifiers = inputModifiers, + .leds = inputLEDs, + .enter = inputEnter, + .focus = inputFocus, +}; + +static void windowsSetup(void) +{ +} + +static bool windowsProbe(void) +{ + return true; +} + +static bool windowsEarlyInit(void) +{ + typedef BOOL (WINAPI * SetProcessDpiAwarenessContextFn)(HANDLE value); + + const HMODULE user32 = + GetModuleHandleW(L"user32.dll"); + const FARPROC procedure = user32 ? GetProcAddress( + user32, "SetProcessDpiAwarenessContext") : NULL; + SetProcessDpiAwarenessContextFn setDPIAwareness = NULL; + memcpy(&setDPIAwareness, &procedure, sizeof(setDPIAwareness)); + if (setDPIAwareness) + setDPIAwareness((HANDLE)(intptr_t)-4); + return true; +} + +static bool windowsInit(const LG_DSInitParams params) +{ + memset(&windows, 0, sizeof(windows)); + windowsInputInit(&windows.input, &inputSink, NULL); + + windows.instance = GetModuleHandleW(NULL); + windows.title = lgUtf8ToWide(params.title); + windows.x = params.x; + windows.y = params.y; + windows.center = params.center; + windows.resizable = params.resizable; + windows.borderless = params.borderless; + windows.maximize = params.maximize; + windows.initialFullscreen = params.fullscreen; + windowsWindowSizeInit(&windows.windowSize, params.w, params.h); + + windows.windowedPlacement.length = sizeof(windows.windowedPlacement); + + atomic_init(&windows.pointer, LG_POINTER_SQUARE); + atomic_init(&windows.pointerGrabbed, false); + atomic_init(&windows.pointerCaptured, false); + atomic_init(&windows.keyboardGrabbed, false); + atomic_init(&windows.fullscreen, false); + atomic_init(&windows.stopping, false); + + if (!windows.title) + { + DEBUG_ERROR("Failed to convert the window title to UTF-16"); + return false; + } + + if (!loadCursors(params.largeCursorDot)) + { + free(windows.title); + windows.title = NULL; + return false; + } + + windows.initEvent = lgCreateEvent(true, 0); + if (!windows.initEvent) + { + DEBUG_ERROR("Failed to create the Win32 initialization event"); + freeCursors(); + free(windows.title); + windows.title = NULL; + return false; + } + + if (!lgCreateThread("Win32EventThread", eventThread, NULL, + &windows.eventThread)) + { + DEBUG_ERROR("Failed to create the Win32 event thread"); + lgFreeEvent(windows.initEvent); + freeCursors(); + free(windows.title); + windows.initEvent = NULL; + windows.title = NULL; + return false; + } + + lgWaitEvent(windows.initEvent, TIMEOUT_INFINITE); + if (!windows.initOK) + { + lgJoinThread(windows.eventThread, NULL); + windows.eventThread = NULL; + lgFreeEvent(windows.initEvent); + freeCursors(); + free(windows.title); + windows.initEvent = NULL; + windows.title = NULL; + return false; + } + + return true; +} + +static void windowsStartup(void) +{ +} + +static void windowsShutdown(void) +{ + if (!windows.eventThread || atomic_exchange_explicit( + &windows.stopping, true, memory_order_acq_rel)) + return; + + if (windows.window) + SendMessageW(windows.window, WM_LG_SHUTDOWN, 0, 0); + else if (windows.threadID) + PostThreadMessageW(windows.threadID, WM_QUIT, 0, 0); + + lgJoinThread(windows.eventThread, NULL); + windows.eventThread = NULL; +} + +static void windowsFree(void) +{ + windowsShutdown(); + if (windows.initEvent) + { + lgFreeEvent(windows.initEvent); + windows.initEvent = NULL; + } + free(windows.title); + windows.title = NULL; + freeCursors(); +} + +static bool windowsGetProp(LG_DSProperty prop, void * ret) +{ + switch (prop) + { + case LG_DS_WARP_SUPPORT: + *(enum LG_DSWarpSupport *)ret = LG_DS_WARP_SCREEN; + return true; + + case LG_DS_NATIVE_HDR: + *(bool *)ret = false; + return true; + + case LG_DS_MAX_MULTISAMPLE: + case LG_DS_HDR_WHITE_LEVELS: + return false; + } + + return false; +} + +#ifdef ENABLE_EGL +static EGLDisplay windowsGetEGLDisplay(void) +{ + const EGLAttrib coreAttributes[] = + { + EGL_PLATFORM_ANGLE_TYPE_ANGLE, + EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, + EGL_NONE, + }; + const EGLint extAttributes [] = + { + EGL_PLATFORM_ANGLE_TYPE_ANGLE, + EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, + EGL_NONE, + }; + + if (g_egl_dynProcs.eglGetPlatformDisplay) + return g_egl_dynProcs.eglGetPlatformDisplay( + EGL_PLATFORM_ANGLE_ANGLE, EGL_DEFAULT_DISPLAY, coreAttributes); + + if (g_egl_dynProcs.eglGetPlatformDisplayEXT) + return g_egl_dynProcs.eglGetPlatformDisplayEXT( + EGL_PLATFORM_ANGLE_ANGLE, EGL_DEFAULT_DISPLAY, extAttributes); + + return eglGetDisplay(EGL_DEFAULT_DISPLAY); +} + +static EGLNativeWindowType windowsGetEGLNativeWindow(void) +{ + return (EGLNativeWindowType)windows.window; +} + +static bool windowsEGLSwapBuffers(EGLDisplay display, EGLSurface surface, + const struct Rect * damage, int count, uint64_t frameToken, + uint64_t * swapTime, bool * presentTracked) +{ + static struct SwapWithDamageData swap = { 0 }; + if (!swap.init) + swapWithDamageInit(&swap, display); + + (void)frameToken; + *presentTracked = false; + const uint64_t start = nanotime(); + const bool result = swapWithDamage( + &swap, display, surface, damage, count); + *swapTime = nanotime() - start; + return result; +} +#endif + +static bool windowsGetFramePeriod(uint64_t * period) +{ + const HMONITOR monitor = MonitorFromWindow( + windows.window, MONITOR_DEFAULTTONEAREST); + MONITORINFOEXW info = { .cbSize = sizeof(info) }; + if (!GetMonitorInfoW(monitor, (MONITORINFO *)&info)) + return false; + + DEVMODEW mode = { .dmSize = sizeof(mode) }; + if (!EnumDisplaySettingsW(info.szDevice, ENUM_CURRENT_SETTINGS, &mode) || + mode.dmDisplayFrequency <= 1) + return false; + + *period = UINT64_C(1000000000) / mode.dmDisplayFrequency; + return true; +} + +static void windowsGuestPointerUpdated(double x, double y, + double localX, double localY) +{ + (void)x; + (void)y; + const struct WindowsGuestPointerMessage point = + { + .localX = localX, + .localY = localY, + }; + SendMessageW(windows.window, WM_LG_GUEST_POINTER, 0, (LPARAM)&point); +} + +static void windowsSetPointer(LG_DSPointer pointer) +{ + SendMessageW(windows.window, WM_LG_SET_POINTER, pointer, 0); +} + +static void windowsGrabKeyboard(void) +{ + SendMessageW(windows.window, WM_LG_GRAB_KEYBOARD, true, 0); +} + +static void windowsUngrabKeyboard(void) +{ + SendMessageW(windows.window, WM_LG_GRAB_KEYBOARD, false, 0); +} + +static void windowsGrabPointer(void) +{ + SendMessageW(windows.window, WM_LG_GRAB_POINTER, true, false); +} + +static void windowsUngrabPointer(void) +{ + SendMessageW(windows.window, WM_LG_GRAB_POINTER, false, false); +} + +static bool windowsIsPointerGrabbed(void) +{ + return atomic_load_explicit( + &windows.pointerGrabbed, memory_order_acquire); +} + +static void windowsCapturePointer(void) +{ + SendMessageW(windows.window, WM_LG_GRAB_POINTER, true, true); +} + +static void windowsUncapturePointer(void) +{ + SendMessageW(windows.window, WM_LG_GRAB_POINTER, false, false); +} + +static bool windowsIsPointerCaptured(void) +{ + return atomic_load_explicit( + &windows.pointerCaptured, memory_order_acquire); +} + +static bool windowsGetKeyLabel(int scancode, char * label, size_t size) +{ + if (scancode < 0 || scancode >= LG_KEY_MAX || !label || + size < 2 || size > INT_MAX) + return false; + + const uint32_t ps2 = lg_keycode_to_ps2[scancode]; + if (!ps2) + return false; + + LONG keyNameParam = (LONG)((ps2 & 0xFF) << 16); + if ((ps2 & 0xFF00) == 0xE000) + keyNameParam |= 1L << 24; + + wchar_t wide[64]; + const int wideLength = GetKeyNameTextW( + keyNameParam, wide, (int)(sizeof(wide) / sizeof(*wide))); + if (wideLength <= 0) + return false; + + const int length = WideCharToMultiByte(CP_UTF8, 0, + wide, wideLength, label, (int)size - 1, NULL, NULL); + if (length <= 0) + return false; + + label[length] = '\0'; + return true; +} + +static void windowsWarpPointer(int x, int y, bool exiting) +{ + const struct WindowsPointMessage point = + { + .x = x, + .y = y, + .exiting = exiting, + }; + SendMessageW(windows.window, WM_LG_WARP_POINTER, 0, (LPARAM)&point); +} + +static void windowsRealignPointer(void) +{ + app_handleMouseRelative(0.0, 0.0, 0.0, 0.0); +} + +static bool windowsIsValidPointerPos(int x, int y) +{ + const POINT point = + { + .x = toPhysical(x), + .y = toPhysical(y), + }; + return MonitorFromPoint(point, MONITOR_DEFAULTTONULL) != NULL; +} + +static void windowsInhibitIdle(void) +{ + SendMessageW(windows.window, WM_LG_INHIBIT_IDLE, true, 0); +} + +static void windowsUninhibitIdle(void) +{ + SendMessageW(windows.window, WM_LG_INHIBIT_IDLE, false, 0); +} + +static void windowsRequestActivation(void) +{ + PostMessageW(windows.window, WM_LG_ACTIVATE, 0, 0); +} + +static void windowsWait(unsigned int time) +{ + Sleep(time); +} + +static void windowsSetWindowSize(int width, int height) +{ + SendMessageW(windows.window, WM_LG_SET_SIZE, width, height); +} + +static bool windowsGetFullscreen(void) +{ + return atomic_load_explicit(&windows.fullscreen, memory_order_acquire); +} + +static void windowsSetFullscreen(bool fullscreen) +{ + SendMessageW(windows.window, WM_LG_SET_FULLSCREEN, fullscreen, 0); +} + +static void windowsMinimize(void) +{ + PostMessageW(windows.window, WM_LG_MINIMIZE, 0, 0); +} + +HWND windowsGetWindow(void) +{ + return windows.window; +} + +void windowsSetMessageHandler(WindowsMessageHandler handler, void * opaque) +{ + const struct WindowsHandlerMessage message = + { + .handler = handler, + .opaque = opaque, + }; + SendMessageW(windows.window, WM_LG_SET_HANDLER, 0, (LPARAM)&message); +} + +struct LG_DisplayServerOps LGDS_Windows = +{ + .name = "Windows", + .setup = windowsSetup, + .probe = windowsProbe, + .earlyInit = windowsEarlyInit, + .init = windowsInit, + .startup = windowsStartup, + .shutdown = windowsShutdown, + .free = windowsFree, + .getProp = windowsGetProp, +#ifdef ENABLE_EGL + .getEGLDisplay = windowsGetEGLDisplay, + .getEGLNativeWindow = windowsGetEGLNativeWindow, + .eglSwapBuffers = windowsEGLSwapBuffers, +#endif + .getFramePeriod = windowsGetFramePeriod, + .guestPointerUpdated = windowsGuestPointerUpdated, + .setPointer = windowsSetPointer, + .grabKeyboard = windowsGrabKeyboard, + .ungrabKeyboard = windowsUngrabKeyboard, + .grabPointer = windowsGrabPointer, + .ungrabPointer = windowsUngrabPointer, + .isPointerGrabbed = windowsIsPointerGrabbed, + .capturePointer = windowsCapturePointer, + .uncapturePointer = windowsUncapturePointer, + .isPointerCaptured = windowsIsPointerCaptured, + .getKeyLabel = windowsGetKeyLabel, + .warpPointer = windowsWarpPointer, + .realignPointer = windowsRealignPointer, + .isValidPointerPos = windowsIsValidPointerPos, + .inhibitIdle = windowsInhibitIdle, + .uninhibitIdle = windowsUninhibitIdle, + .requestActivation = windowsRequestActivation, + .wait = windowsWait, + .setWindowSize = windowsSetWindowSize, + .getFullscreen = windowsGetFullscreen, + .setFullscreen = windowsSetFullscreen, + .minimize = windowsMinimize, +}; diff --git a/client/include/app.h b/client/include/app.h index cbb07f26..aadd7dcf 100644 --- a/client/include/app.h +++ b/client/include/app.h @@ -93,6 +93,7 @@ void app_handleKeyboardModifiers(bool ctrl, bool shift, bool alt, bool super); void app_handleKeyboardLEDs(bool numLock, bool capsLock, bool scrollLock); void app_handleEnterEvent(bool entered); void app_handleGrabEvent(bool active); +void app_handleCaptureLost(void); void app_handleFocusEvent(bool focused); void app_handleCloseEvent(void); void app_handleRenderEvent(const uint64_t timeUs); diff --git a/client/include/egl_dynprocs.h b/client/include/egl_dynprocs.h index 2e4b07d4..3c3d0141 100644 --- a/client/include/egl_dynprocs.h +++ b/client/include/egl_dynprocs.h @@ -30,16 +30,16 @@ struct EGLDynProcs { - PFNEGLGETPLATFORMDISPLAYPROC eglGetPlatformDisplay; - PFNEGLGETPLATFORMDISPLAYPROC eglGetPlatformDisplayEXT; - PFNEGLSWAPBUFFERSWITHDAMAGEKHRPROC eglSwapBuffersWithDamageKHR; - PFNEGLSWAPBUFFERSWITHDAMAGEEXTPROC eglSwapBuffersWithDamageEXT; - PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES; - PFNGLDEBUGMESSAGECALLBACKKHRPROC glDebugMessageCallback; - PFNGLDEBUGMESSAGECALLBACKKHRPROC glDebugMessageCallbackKHR; - PFNGLBUFFERSTORAGEEXTPROC glBufferStorageEXT; - PFNEGLCREATEIMAGEPROC eglCreateImage; - PFNEGLDESTROYIMAGEPROC eglDestroyImage; + PFNEGLGETPLATFORMDISPLAYPROC eglGetPlatformDisplay; + PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT; + PFNEGLSWAPBUFFERSWITHDAMAGEKHRPROC eglSwapBuffersWithDamageKHR; + PFNEGLSWAPBUFFERSWITHDAMAGEEXTPROC eglSwapBuffersWithDamageEXT; + PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES; + PFNGLDEBUGMESSAGECALLBACKKHRPROC glDebugMessageCallback; + PFNGLDEBUGMESSAGECALLBACKKHRPROC glDebugMessageCallbackKHR; + PFNGLBUFFERSTORAGEEXTPROC glBufferStorageEXT; + PFNEGLCREATEIMAGEPROC eglCreateImage; + PFNEGLDESTROYIMAGEPROC eglDestroyImage; }; extern struct EGLDynProcs g_egl_dynProcs; diff --git a/client/src/app.c b/client/src/app.c index ae697298..975ea281 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -689,6 +689,11 @@ void app_handleGrabEvent(bool active) core_handleGrabEvent(active); } +void app_handleCaptureLost(void) +{ + core_handleCaptureLost(); +} + void app_updateWindowPos(int x, int y) { g_state.windowPos.x = x; diff --git a/client/src/core.c b/client/src/core.c index 896d3a2e..0c039df0 100644 --- a/client/src/core.c +++ b/client/src/core.c @@ -306,6 +306,12 @@ void core_handleGrabEvent(bool active) core_handleMouseAbsolute(); } +void core_handleCaptureLost(void) +{ + if (g_cursor.grab) + core_setGrabQuiet(false); +} + void core_setGrab(bool enable) { core_setGrabQuiet(enable); diff --git a/client/src/core.h b/client/src/core.h index 1a299189..23167a41 100644 --- a/client/src/core.h +++ b/client/src/core.h @@ -29,6 +29,7 @@ void core_updateKeyboardGrab(void); void core_invalidatePointer(bool detectInView); void core_setCursorInView(bool enable); void core_handleGrabEvent(bool active); +void core_handleCaptureLost(void); void core_setGrab(bool enable); void core_setGrabQuiet(bool enable); bool core_warpPointer(int x, int y, bool exiting); diff --git a/client/src/egl_dynprocs.c b/client/src/egl_dynprocs.c index e18985a3..a79979c0 100644 --- a/client/src/egl_dynprocs.c +++ b/client/src/egl_dynprocs.c @@ -22,37 +22,49 @@ #include "egl_dynprocs.h" -struct EGLDynProcs g_egl_dynProcs = {0}; +struct EGLDynProcs g_egl_dynProcs = { 0 }; void egl_dynProcsInit(void) { - g_egl_dynProcs.eglGetPlatformDisplay = (PFNEGLGETPLATFORMDISPLAYPROC) + g_egl_dynProcs.eglGetPlatformDisplay = + (PFNEGLGETPLATFORMDISPLAYPROC) eglGetProcAddress("eglGetPlatformDisplay"); - g_egl_dynProcs.eglGetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYPROC) + g_egl_dynProcs.eglGetPlatformDisplayEXT = + (PFNEGLGETPLATFORMDISPLAYEXTPROC) eglGetProcAddress("eglGetPlatformDisplayEXT"); - g_egl_dynProcs.glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC) + g_egl_dynProcs.glEGLImageTargetTexture2DOES = + (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC) eglGetProcAddress("glEGLImageTargetTexture2DOES"); - g_egl_dynProcs.eglSwapBuffersWithDamageKHR = (PFNEGLSWAPBUFFERSWITHDAMAGEKHRPROC) + g_egl_dynProcs.eglSwapBuffersWithDamageKHR = + (PFNEGLSWAPBUFFERSWITHDAMAGEKHRPROC) eglGetProcAddress("eglSwapBuffersWithDamageKHR"); - g_egl_dynProcs.eglSwapBuffersWithDamageEXT = (PFNEGLSWAPBUFFERSWITHDAMAGEEXTPROC) + g_egl_dynProcs.eglSwapBuffersWithDamageEXT = + (PFNEGLSWAPBUFFERSWITHDAMAGEEXTPROC) eglGetProcAddress("eglSwapBuffersWithDamageEXT"); - g_egl_dynProcs.glDebugMessageCallback = (PFNGLDEBUGMESSAGECALLBACKKHRPROC) + g_egl_dynProcs.glDebugMessageCallback = + (PFNGLDEBUGMESSAGECALLBACKKHRPROC) eglGetProcAddress("glDebugMessageCallback"); - g_egl_dynProcs.glDebugMessageCallbackKHR = (PFNGLDEBUGMESSAGECALLBACKKHRPROC) + g_egl_dynProcs.glDebugMessageCallbackKHR = + (PFNGLDEBUGMESSAGECALLBACKKHRPROC) eglGetProcAddress("glDebugMessageCallbackKHR"); - g_egl_dynProcs.glBufferStorageEXT = (PFNGLBUFFERSTORAGEEXTPROC) + g_egl_dynProcs.glBufferStorageEXT = + (PFNGLBUFFERSTORAGEEXTPROC) eglGetProcAddress("glBufferStorageEXT"); - g_egl_dynProcs.eglCreateImage = (PFNEGLCREATEIMAGEPROC) + g_egl_dynProcs.eglCreateImage = + (PFNEGLCREATEIMAGEPROC) eglGetProcAddress("eglCreateImage"); - g_egl_dynProcs.eglDestroyImage = (PFNEGLDESTROYIMAGEPROC) + g_egl_dynProcs.eglDestroyImage = + (PFNEGLDESTROYIMAGEPROC) eglGetProcAddress("eglDestroyImage"); if (!g_egl_dynProcs.eglCreateImage) - g_egl_dynProcs.eglCreateImage = (PFNEGLCREATEIMAGEPROC) + g_egl_dynProcs.eglCreateImage = + (PFNEGLCREATEIMAGEPROC) eglGetProcAddress("eglCreateImageKHR"); if (!g_egl_dynProcs.eglDestroyImage) - g_egl_dynProcs.eglDestroyImage = (PFNEGLDESTROYIMAGEPROC) + g_egl_dynProcs.eglDestroyImage = + (PFNEGLDESTROYIMAGEPROC) eglGetProcAddress("eglDestroyImageKHR"); -}; +} #endif diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 18be548c..c7a451ec 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -429,6 +429,28 @@ foreach(name IN ITEMS pointer-routing keyboard-routing held-keys) ) endforeach() +add_executable(windows-input-tests + windows_input_test.c + ../displayservers/Windows/input_event.c + ../displayservers/Windows/window_size.c + ../src/kb.c +) +target_compile_definitions(windows-input-tests PRIVATE + CIMGUI_DEFINE_ENUMS_AND_STRUCTS=1 +) +target_include_directories(windows-input-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../displayservers" + "${CMAKE_CURRENT_SOURCE_DIR}/../src" + "${PROJECT_TOP}/repos/gui/cimgui" + "${PROJECT_TOP}/repos/gui/cimgui/imgui" +) +add_test(NAME windows-input-tests + COMMAND windows-input-tests +) +set_tests_properties(windows-input-tests PROPERTIES + TIMEOUT 10 +) + add_executable(mouse-tests mouse_test.c ../src/core.c @@ -459,6 +481,7 @@ set(MOUSE_CASES capture-pending capture-revoked capture-release + capture-lost capture-align x11-capture-exit capture-fallback diff --git a/client/tests/mouse_test.c b/client/tests/mouse_test.c index 8754fe6e..8ae5b225 100644 --- a/client/tests/mouse_test.c +++ b/client/tests/mouse_test.c @@ -783,6 +783,32 @@ static void testCapRelease(void) CHECK(count(EV_MOTION) == 1); } +static void testCapLost(void) +{ + reset(); + setLocal(50, 50); + g_params.captureInputOnly = true; + g_params.grabKeyboard = true; + core_setGrabQuiet(true); + + m.lock.on = true; + CHECK(g_cursor.grab); + CHECK(m.lock.req); + CHECK(m.keyboardGrabbed); + + m.count = 0; + core_handleCaptureLost(); + CHECK(!g_cursor.grab); + CHECK(!m.lock.req); + CHECK(!m.keyboardGrabbed); + CHECK(count(EV_UNCAPTURE) == 1); + CHECK(count(EV_KEY_UNGRAB) == 1); + + core_handleCaptureLost(); + CHECK(count(EV_UNCAPTURE) == 1); + CHECK(count(EV_KEY_UNGRAB) == 1); +} + static void testCapAlign(void) { reset(); @@ -1098,6 +1124,7 @@ static const struct Test tests[] = { { "capture-pending" , testCapWait }, { "capture-revoked" , testCapRevoke }, { "capture-release" , testCapRelease }, + { "capture-lost" , testCapLost }, { "capture-align" , testCapAlign }, { "x11-capture-exit", testX11CapExit }, { "capture-fallback", testCapFallback }, diff --git a/client/tests/windows_input_test.c b/client/tests/windows_input_test.c new file mode 100644 index 00000000..27de7ffb --- /dev/null +++ b/client/tests/windows_input_test.c @@ -0,0 +1,575 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "Windows/input_event.h" +#include "Windows/window_size.h" +#include "input/keycodes.h" +#include "test.h" + +#include +#include + +enum TraceType +{ + TRACE_POSITION, + TRACE_RELATIVE, + TRACE_BUTTON, + TRACE_WHEEL, + TRACE_KEY, + TRACE_TEXT, + TRACE_MODIFIERS, + TRACE_LEDS, + TRACE_ENTER, + TRACE_FOCUS, +}; + +struct Trace +{ + enum TraceType type; + int value; + double x; + double y; + bool state; + char text[16]; +}; + +#define TRACE_CAPACITY 256 + +struct Fixture +{ + WindowsInput input; + struct Trace trace[TRACE_CAPACITY]; + size_t count; +}; + +static struct Trace * push(struct Fixture * fixture, enum TraceType type) +{ + CHECK(fixture->count < TRACE_CAPACITY); + struct Trace * trace = &fixture->trace[fixture->count++]; + memset(trace, 0, sizeof(*trace)); + trace->type = type; + return trace; +} + +static void position(void * opaque, double x, double y) +{ + struct Trace * trace = push(opaque, TRACE_POSITION); + trace->x = x; + trace->y = y; +} + +static void relative(void * opaque, double x, double y, + double rawX, double rawY) +{ + struct Trace * trace = push(opaque, TRACE_RELATIVE); + trace->x = x; + trace->y = y; + CHECK(x == rawX); + CHECK(y == rawY); +} + +static void button(void * opaque, unsigned int button, bool pressed) +{ + struct Trace * trace = push(opaque, TRACE_BUTTON); + trace->value = button; + trace->state = pressed; +} + +static void wheel(void * opaque, double motion) +{ + struct Trace * trace = push(opaque, TRACE_WHEEL); + trace->x = motion; +} + +static void key(void * opaque, int scancode, bool pressed) +{ + struct Trace * trace = push(opaque, TRACE_KEY); + trace->value = scancode; + trace->state = pressed; +} + +static void textInput(void * opaque, const char * text) +{ + struct Trace * trace = push(opaque, TRACE_TEXT); + snprintf(trace->text, sizeof(trace->text), "%s", text); +} + +static void modifiers(void * opaque, bool ctrl, bool shift, + bool alt, bool super) +{ + struct Trace * trace = push(opaque, TRACE_MODIFIERS); + trace->value = ctrl | shift << 1 | alt << 2 | super << 3; +} + +static void leds(void * opaque, bool numLock, bool capsLock, + bool scrollLock) +{ + struct Trace * trace = push(opaque, TRACE_LEDS); + trace->value = numLock | capsLock << 1 | scrollLock << 2; +} + +static void enter(void * opaque, bool entered) +{ + struct Trace * trace = push(opaque, TRACE_ENTER); + trace->state = entered; +} + +static void focus(void * opaque, bool focused) +{ + struct Trace * trace = push(opaque, TRACE_FOCUS); + trace->state = focused; +} + +static const LG_DSInputSink sink = +{ + .position = position, + .relative = relative, + .button = button, + .wheel = wheel, + .key = key, + .text = textInput, + .modifiers = modifiers, + .leds = leds, + .enter = enter, + .focus = focus, +}; + +static void init(struct Fixture * fixture) +{ + memset(fixture, 0, sizeof(*fixture)); + windowsInputInit(&fixture->input, &sink, fixture); +} + +static double doubleFromBits(uint64_t bits) +{ + double value; + _Static_assert(sizeof(value) == sizeof(bits), "Unexpected double size"); + memcpy(&value, &bits, sizeof(value)); + return value; +} + +static void testScanCodes(void) +{ + CHECK(windowsInputMapScanCode(0x1E, false) == LG_KEY_A); + CHECK(windowsInputMapScanCode(0x1D, false) == LG_KEY_LEFTCTRL); + CHECK(windowsInputMapScanCode(0x1D, true ) == LG_KEY_RIGHTCTRL); + CHECK(windowsInputMapScanCode(0x5B, true ) == LG_KEY_LEFTMETA); + CHECK(windowsInputMapScanCode(0x53, true ) == LG_KEY_DELETE); + CHECK(windowsInputMapScanCode(0x00, false) == LG_KEY_RESERVED); + CHECK(windowsInputMapScanCode(0xFF, false) == LG_KEY_RESERVED); + CHECK(windowsInputMapPrintScreen(false) == LG_KEY_PRINT); + CHECK(windowsInputMapPrintScreen(true ) == LG_KEY_SYSRQ); +} + +static void testKeyboardRouting(void) +{ + struct Fixture fixture; + init(&fixture); + + windowsInputText(&fixture.input, "ignored"); + CHECK(!windowsInputKey(&fixture.input, 0x1E, false, true, false)); + CHECK(windowsInputFocus(&fixture.input, true)); + CHECK(!windowsInputFocus(&fixture.input, true)); + CHECK(windowsInputKey(&fixture.input, 0x1E, false, true, false)); + windowsInputText(&fixture.input, "a"); + windowsInputSetKeyboardGrabbed(&fixture.input, true); + CHECK(!windowsInputKey(&fixture.input, 0x1E, false, false, false)); + CHECK(windowsInputKey(&fixture.input, 0x1E, false, false, true)); + windowsInputText(&fixture.input, "ignored"); + windowsInputState(&fixture.input, true, false, true, false, + true, false, true); + + CHECK(fixture.count == 6); + CHECK(fixture.trace[0].type == TRACE_FOCUS); + CHECK(fixture.trace[0].state == true); + CHECK(fixture.trace[1].type == TRACE_KEY); + CHECK(fixture.trace[1].value == LG_KEY_A); + CHECK(fixture.trace[1].state == true); + CHECK(fixture.trace[2].type == TRACE_TEXT); + CHECK(strcmp(fixture.trace[2].text, "a") == 0); + CHECK(fixture.trace[3].type == TRACE_KEY); + CHECK(fixture.trace[3].value == LG_KEY_A); + CHECK(fixture.trace[3].state == false); + CHECK(fixture.trace[4].type == TRACE_MODIFIERS); + CHECK(fixture.trace[4].value == 5); + CHECK(fixture.trace[5].type == TRACE_LEDS); + CHECK(fixture.trace[5].value == 5); +} + +static void testPrintScreenLatch(void) +{ + struct Fixture fixture; + init(&fixture); + + CHECK(windowsInputMapPrintScreenEvent( + &fixture.input, true, true) == LG_KEY_SYSRQ); + CHECK(windowsInputMapPrintScreenEvent( + &fixture.input, false, true) == LG_KEY_SYSRQ); + CHECK(windowsInputMapPrintScreenEvent( + &fixture.input, false, false) == LG_KEY_SYSRQ); + CHECK(windowsInputMapPrintScreenEvent( + &fixture.input, false, true) == LG_KEY_PRINT); + CHECK(windowsInputMapPrintScreenEvent( + &fixture.input, true, false) == LG_KEY_PRINT); + + CHECK(windowsInputFocus(&fixture.input, true)); + CHECK(windowsInputMapPrintScreenEvent( + &fixture.input, true, true) == LG_KEY_SYSRQ); + CHECK(windowsInputFocus(&fixture.input, false)); + CHECK(windowsInputMapPrintScreenEvent( + &fixture.input, false, false) == LG_KEY_PRINT); +} + +static void testHeldKeys(void) +{ + struct Fixture fixture; + init(&fixture); + + const int keys[] = + { + LG_KEY_A, + LG_KEY_RESERVED, + LG_KEY_LEFTSHIFT, + LG_KEY_MAX, + }; + + CHECK(windowsInputSyncHeldKeys(&fixture.input, + keys, sizeof(keys) / sizeof(*keys), false) == 0); + CHECK(windowsInputFocus(&fixture.input, true)); + CHECK(windowsInputSyncHeldKeys(&fixture.input, + keys, sizeof(keys) / sizeof(*keys), false) == 2); + windowsInputTrackedState(&fixture.input); + + windowsInputSetKeyboardGrabbed(&fixture.input, true); + CHECK(windowsInputSyncHeldKeys(&fixture.input, keys, 1, false) == 0); + CHECK(windowsInputSyncHeldKeys(&fixture.input, keys, 1, true ) == 1); + CHECK(windowsInputSyncHeldKeys(&fixture.input, NULL, 1, true) == 0); + + CHECK(fixture.count == 6); + CHECK(fixture.trace[0].type == TRACE_FOCUS); + CHECK(fixture.trace[1].type == TRACE_KEY); + CHECK(fixture.trace[1].value == LG_KEY_LEFTSHIFT); + CHECK(fixture.trace[2].type == TRACE_KEY); + CHECK(fixture.trace[2].value == LG_KEY_A); + CHECK(fixture.trace[3].type == TRACE_MODIFIERS); + CHECK(fixture.trace[3].value == 2); + CHECK(fixture.trace[4].type == TRACE_LEDS); + CHECK(fixture.trace[5].type == TRACE_KEY); + CHECK(fixture.trace[5].value == LG_KEY_A); +} + +static void testHeldKeyModifierOrder(void) +{ + struct Fixture fixture; + init(&fixture); + + const int keys[] = + { + LG_KEY_SYSRQ, + LG_KEY_LEFTALT, + }; + + CHECK(windowsInputFocus(&fixture.input, true)); + CHECK(windowsInputSyncHeldKeys(&fixture.input, + keys, sizeof(keys) / sizeof(*keys), false) == 2); + CHECK(fixture.count == 3); + CHECK(fixture.trace[1].type == TRACE_KEY); + CHECK(fixture.trace[1].value == LG_KEY_LEFTALT); + CHECK(fixture.trace[2].type == TRACE_KEY); + CHECK(fixture.trace[2].value == LG_KEY_SYSRQ); +} + +static void testKeyboardStateTransitions(void) +{ + struct Fixture fixture; + init(&fixture); + + CHECK(windowsInputFocus(&fixture.input, true)); + windowsInputSetKeyboardGrabbed(&fixture.input, true); + windowsInputState(&fixture.input, false, false, false, false, + true, false, false); + + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_LEFTCTRL, true, true)); + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_RIGHTCTRL, true, true)); + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_LEFTCTRL, false, true)); + windowsInputTrackedState(&fixture.input); + CHECK(fixture.trace[6].type == TRACE_MODIFIERS); + CHECK(fixture.trace[6].value == 1); + + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_RIGHTCTRL, false, true)); + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_CAPSLOCK, true, true)); + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_CAPSLOCK, true, true)); + windowsInputTrackedState(&fixture.input); + CHECK(fixture.trace[12].type == TRACE_LEDS); + CHECK(fixture.trace[12].value == 3); + + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_CAPSLOCK, false, true)); + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_CAPSLOCK, true, true)); + windowsInputTrackedState(&fixture.input); + CHECK(fixture.trace[16].type == TRACE_LEDS); + CHECK(fixture.trace[16].value == 1); + + CHECK(windowsInputFocus(&fixture.input, false)); + CHECK(windowsInputFocus(&fixture.input, true)); + windowsInputTrackedState(&fixture.input); + CHECK(fixture.trace[19].type == TRACE_MODIFIERS); + CHECK(fixture.trace[19].value == 0); + + CHECK(fixture.input.numLock); + CHECK(!fixture.input.capsLock); + CHECK(!fixture.input.scrollLock); + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_NUMLOCK, true, true)); + CHECK(!fixture.input.numLock); + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_NUMLOCK, true, true)); + CHECK(!fixture.input.numLock); + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_NUMLOCK, false, true)); + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_NUMLOCK, true, true)); + CHECK(fixture.input.numLock); + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_SCROLLLOCK, true, true)); + CHECK(fixture.input.scrollLock); + + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_LEFTSHIFT, true, true)); + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_RIGHTALT, true, true)); + CHECK(windowsInputKeyCode( + &fixture.input, LG_KEY_LEFTMETA, true, true)); + windowsInputTrackedState(&fixture.input); + CHECK(fixture.count == 31); + CHECK(fixture.trace[29].type == TRACE_MODIFIERS); + CHECK(fixture.trace[29].value == 14); + CHECK(fixture.trace[30].type == TRACE_LEDS); + CHECK(fixture.trace[30].value == 5); +} + +static void testPointerRouting(void) +{ + struct Fixture fixture; + init(&fixture); + + CHECK(windowsInputPointerEnter(&fixture.input, 10.0, 20.0)); + windowsInputPointerMotion(&fixture.input, 12.0, 24.0); + CHECK(windowsInputPointerButton(&fixture.input, 1, true)); + CHECK(windowsInputPointerButton(&fixture.input, 1, false)); + windowsInputPointerWheel(&fixture.input, 0.5); + windowsInputPointerWheel(&fixture.input, 0.5); + windowsInputSetPointerGrabbed(&fixture.input, true); + windowsInputPointerMotion(&fixture.input, 14.0, 28.0); + windowsInputRelativeMotion(&fixture.input, 2.0, -3.0); + CHECK(!windowsInputPointerLeave(&fixture.input)); + windowsInputSetPointerGrabbed(&fixture.input, false); + CHECK(!fixture.input.entered); + + CHECK(fixture.count == 11); + CHECK(fixture.trace[0].type == TRACE_ENTER); + CHECK(fixture.trace[1].type == TRACE_POSITION); + CHECK(fixture.trace[2].type == TRACE_POSITION); + CHECK(fixture.trace[3].type == TRACE_BUTTON); + CHECK(fixture.trace[3].state == true); + CHECK(fixture.trace[4].type == TRACE_BUTTON); + CHECK(fixture.trace[4].state == false); + CHECK(fixture.trace[5].type == TRACE_WHEEL); + CHECK(fixture.trace[5].x == -0.5); + CHECK(fixture.trace[6].type == TRACE_BUTTON); + CHECK(fixture.trace[6].value == 4); + CHECK(fixture.trace[6].state == true); + CHECK(fixture.trace[7].type == TRACE_BUTTON); + CHECK(fixture.trace[7].value == 4); + CHECK(fixture.trace[7].state == false); + CHECK(fixture.trace[8].type == TRACE_WHEEL); + CHECK(fixture.trace[8].x == -0.5); + CHECK(fixture.trace[9].type == TRACE_RELATIVE); + CHECK(fixture.trace[10].type == TRACE_ENTER); + CHECK(fixture.trace[10].state == false); +} + +static void testPointerLeaveAndCaptureLoss(void) +{ + struct Fixture fixture; + init(&fixture); + + CHECK(windowsInputPointerEnter(&fixture.input, 1.0, 2.0)); + CHECK(windowsInputPointerButton(&fixture.input, 1, true)); + CHECK(windowsInputPointerButton(&fixture.input, 3, true)); + CHECK(!windowsInputPointerLeave(&fixture.input)); + CHECK(fixture.input.leavePending); + windowsInputReleaseButtons(&fixture.input); + CHECK(fixture.input.buttons == 0); + CHECK(!fixture.input.entered); + CHECK(!fixture.input.leavePending); + + CHECK(fixture.count == 7); + CHECK(fixture.trace[4].type == TRACE_BUTTON); + CHECK(fixture.trace[4].value == 1); + CHECK(fixture.trace[4].state == false); + CHECK(fixture.trace[5].type == TRACE_BUTTON); + CHECK(fixture.trace[5].value == 3); + CHECK(fixture.trace[5].state == false); + CHECK(fixture.trace[6].type == TRACE_ENTER); + CHECK(fixture.trace[6].state == false); + + CHECK(windowsInputPointerEnter(&fixture.input, 3.0, 4.0)); + windowsInputSetPointerGrabbed(&fixture.input, true); + CHECK(!windowsInputPointerLeave(&fixture.input)); + CHECK(fixture.input.leavePending); + windowsInputSetPointerGrabbed(&fixture.input, false); + CHECK(!fixture.input.entered); + + CHECK(windowsInputPointerEnter(&fixture.input, 5.0, 6.0)); + CHECK(windowsInputPointerButton(&fixture.input, 1, true)); + CHECK(!windowsInputPointerLeave(&fixture.input)); + CHECK(!windowsInputPointerEnter(&fixture.input, 7.0, 8.0)); + CHECK(!fixture.input.leavePending); + CHECK(windowsInputPointerButton(&fixture.input, 1, false)); + CHECK(fixture.input.entered); +} + +static size_t countTrace(const struct Fixture * fixture, + enum TraceType type, int value, bool state) +{ + size_t count = 0; + for (size_t i = 0; i < fixture->count; ++i) + { + const struct Trace * trace = &fixture->trace[i]; + if (trace->type == type && trace->value == value && + trace->state == state) + ++count; + } + + return count; +} + +static void testPointerWheel(void) +{ + struct Fixture fixture; + init(&fixture); + + CHECK(windowsInputPointerEnter(&fixture.input, 0.0, 0.0)); + windowsInputPointerWheel(&fixture.input, 2.25); + windowsInputPointerWheel(&fixture.input, 0.75); + windowsInputPointerWheel(&fixture.input, -2.5); + windowsInputPointerWheel(&fixture.input, -0.5); + + CHECK(fixture.input.wheel == 0.0); + CHECK(countTrace(&fixture, TRACE_BUTTON, 4, true ) == 3); + CHECK(countTrace(&fixture, TRACE_BUTTON, 4, false) == 3); + CHECK(countTrace(&fixture, TRACE_BUTTON, 5, true ) == 3); + CHECK(countTrace(&fixture, TRACE_BUTTON, 5, false) == 3); + CHECK(countTrace(&fixture, TRACE_WHEEL, 0, false) == 4); + + const size_t count = fixture.count; + const double nan = doubleFromBits(UINT64_C(0x7FF8000000000000)); + const double infinity = doubleFromBits(UINT64_C(0x7FF0000000000000)); + windowsInputPointerWheel(&fixture.input, 0.0); + windowsInputPointerWheel(&fixture.input, nan); + windowsInputPointerWheel(&fixture.input, infinity); + CHECK(fixture.count == count); +} + +static void testInvalidPointerInput(void) +{ + struct Fixture fixture; + init(&fixture); + + const double nan = doubleFromBits(UINT64_C(0x7FF8000000000000)); + const double infinity = doubleFromBits(UINT64_C(0x7FF0000000000000)); + CHECK(!windowsInputPointerEnter(&fixture.input, nan, 0.0)); + CHECK(!windowsInputPointerEnter(&fixture.input, 0.0, infinity)); + CHECK(!windowsInputPointerButton(&fixture.input, 0, true)); + CHECK(!windowsInputPointerButton(&fixture.input, 32, true)); + CHECK(windowsInputPointerEnter(&fixture.input, 1.0, 1.0)); + CHECK(windowsInputPointerButton(&fixture.input, 1, true)); + CHECK(!windowsInputPointerButton(&fixture.input, 1, true)); + CHECK(windowsInputPointerButton(&fixture.input, 1, false)); + CHECK(!windowsInputPointerButton(&fixture.input, 1, false)); + + const size_t count = fixture.count; + windowsInputPointerMotion(&fixture.input, nan, 1.0); + windowsInputPointerMotion(&fixture.input, 1.0, infinity); + windowsInputRelativeMotion(&fixture.input, nan, 1.0); + CHECK(fixture.count == count); + + windowsInputText(&fixture.input, NULL); + windowsInputText(&fixture.input, ""); + CHECK(fixture.count == count); +} + +static void testWindowSizePolicy(void) +{ + WindowsWindowSize state; + windowsWindowSizeInit(&state, 800, 600); + CHECK(state.width == 800); + CHECK(state.height == 600); + CHECK(!state.pending); + + CHECK(!windowsWindowSizeRequest(&state, 1024, 768, true)); + CHECK(state.width == 1024); + CHECK(state.height == 768); + CHECK(state.pending); + + int width = -1; + int height = -1; + CHECK(!windowsWindowSizeTakePending( + &state, true, &width, &height)); + CHECK(width == -1); + CHECK(height == -1); + CHECK(state.pending); + + CHECK(!windowsWindowSizeRequest(&state, 1280, 720, true)); + CHECK(windowsWindowSizeTakePending( + &state, false, &width, &height)); + CHECK(width == 1280); + CHECK(height == 720); + CHECK(!state.pending); + + CHECK(windowsWindowSizeRequest(&state, 640, 480, false)); + CHECK(!state.pending); + CHECK(!windowsWindowSizeTakePending( + &state, false, &width, &height)); +} + +int main(void) +{ + testScanCodes(); + testKeyboardRouting(); + testPrintScreenLatch(); + testHeldKeys(); + testHeldKeyModifierOrder(); + testKeyboardStateTransitions(); + testPointerRouting(); + testPointerLeaveAndCaptureLoss(); + testPointerWheel(); + testInvalidPointerInput(); + testWindowSizePolicy(); + return 0; +}