/ src / modules / MouseUtils / CursorWrap / dllmain.cpp
dllmain.cpp
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  #include "pch.h"
  6  #include "../../../interface/powertoy_module_interface.h"
  7  #include "../../../common/SettingsAPI/settings_objects.h"
  8  #include "trace.h"
  9  #include "../../../common/utils/process_path.h"
 10  #include "../../../common/utils/resources.h"
 11  #include "../../../common/logger/logger.h"
 12  #include "../../../common/utils/logger_helper.h"
 13  #include "../../../common/interop/shared_constants.h"
 14  #include <atomic>
 15  #include <thread>
 16  #include <vector>
 17  #include <map>
 18  #include <string>
 19  #include <algorithm>
 20  #include <windows.h>
 21  #include <dbt.h>
 22  #include <sstream>
 23  #include "resource.h"
 24  #include "CursorWrapCore.h"
 25  
 26  // Disable C26451 arithmetic overflow warning for this file since the operations are safe in this context
 27  #pragma warning(disable: 26451)
 28  
 29  extern "C" IMAGE_DOS_HEADER __ImageBase;
 30  
 31  BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
 32  {
 33      switch (ul_reason_for_call)
 34      {
 35      case DLL_PROCESS_ATTACH:
 36          Trace::RegisterProvider();
 37          break;
 38      case DLL_THREAD_ATTACH:
 39      case DLL_THREAD_DETACH:
 40          break;
 41      case DLL_PROCESS_DETACH:
 42          Trace::UnregisterProvider();
 43          break;
 44      }
 45      return TRUE;
 46  }
 47  
 48  // Non-Localizable strings
 49  namespace
 50  {
 51      const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
 52      const wchar_t JSON_KEY_VALUE[] = L"value";
 53      const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
 54      const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
 55      const wchar_t JSON_KEY_DISABLE_WRAP_DURING_DRAG[] = L"disable_wrap_during_drag";
 56      const wchar_t JSON_KEY_WRAP_MODE[] = L"wrap_mode";
 57      const wchar_t JSON_KEY_DISABLE_ON_SINGLE_MONITOR[] = L"disable_cursor_wrap_on_single_monitor";
 58  }
 59  
 60  // The PowerToy name that will be shown in the settings.
 61  const static wchar_t* MODULE_NAME = L"CursorWrap";
 62  // Add a description that will we shown in the module settings page.
 63  const static wchar_t* MODULE_DESC = L"<no description>";
 64  
 65  // Monitor device interface GUID for RegisterDeviceNotification
 66  // {e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}
 67  static const GUID GUID_DEVINTERFACE_MONITOR =
 68      { 0xe6f07b5f, 0xee97, 0x4a90, { 0xb0, 0x76, 0x33, 0xf5, 0x7b, 0xf4, 0xea, 0xa7 } };
 69  
 70  // Forward declaration
 71  class CursorWrap;
 72  
 73  // Global instance pointer for the mouse hook
 74  static CursorWrap* g_cursorWrapInstance = nullptr;
 75  
 76  // Implement the PowerToy Module Interface and all the required methods.
 77  class CursorWrap : public PowertoyModuleIface
 78  {
 79  private:
 80      // The PowerToy state.
 81      bool m_enabled = false;
 82      bool m_autoActivate = false;
 83      bool m_disableWrapDuringDrag = true; // Default to true to prevent wrap during drag
 84      bool m_disableOnSingleMonitor = false; // Default to false
 85      int m_wrapMode = 0; // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly
 86      
 87      // Mouse hook
 88      HHOOK m_mouseHook = nullptr;
 89      std::atomic<bool> m_hookActive{ false };
 90      
 91      // Core wrapping engine (edge-based polygon model)
 92      CursorWrapCore m_core;
 93      
 94      // Hotkey
 95      Hotkey m_activationHotkey{};
 96  
 97      // Event-driven trigger support (for CmdPal/automation)
 98      HANDLE m_triggerEventHandle = nullptr;
 99      HANDLE m_terminateEventHandle = nullptr;
100      std::thread m_eventThread;
101      std::atomic_bool m_listening{ false };
102  
103      // Display change notification
104      HWND m_messageWindow = nullptr;
105      HDEVNOTIFY m_deviceNotify = nullptr;
106      static constexpr UINT_PTR TIMER_UPDATE_MONITORS = 1;
107      static constexpr UINT DEBOUNCE_DELAY_MS = 500;
108  
109  public:
110      // Constructor
111      CursorWrap()
112      {
113          LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::cursorWrapLoggerName);
114          init_settings();
115          m_core.UpdateMonitorInfo();
116          g_cursorWrapInstance = this; // Set global instance pointer
117      };
118  
119      // Destroy the powertoy and free memory
120      virtual void destroy() override
121      {
122          // Ensure hooks/threads/handles are torn down before deletion
123          disable();
124          g_cursorWrapInstance = nullptr; // Clear global instance pointer
125          delete this;
126      }
127  
128      // Return the localized display name of the powertoy
129      virtual const wchar_t* get_name() override
130      {
131          return MODULE_NAME;
132      }
133  
134      // Return the non localized key of the powertoy, this will be cached by the runner
135      virtual const wchar_t* get_key() override
136      {
137          return MODULE_NAME;
138      }
139  
140      // Return the configured status for the gpo policy for the module
141      virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
142      {
143          return powertoys_gpo::getConfiguredCursorWrapEnabledValue();
144      }
145  
146      // Return JSON with the configuration options.
147      virtual bool get_config(wchar_t* buffer, int* buffer_size) override
148      {
149          HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
150  
151          PowerToysSettings::Settings settings(hinstance, get_name());
152  
153          settings.set_description(IDS_CURSORWRAP_NAME);
154          settings.set_icon_key(L"pt-cursor-wrap");
155          
156          // Create HotkeyObject from the Hotkey struct for the settings
157          auto hotkey_object = PowerToysSettings::HotkeyObject::from_settings(
158              m_activationHotkey.win,
159              m_activationHotkey.ctrl,
160              m_activationHotkey.alt,
161              m_activationHotkey.shift,
162              m_activationHotkey.key);
163  
164          settings.add_hotkey(JSON_KEY_ACTIVATION_SHORTCUT, IDS_CURSORWRAP_NAME, hotkey_object);
165          settings.add_bool_toggle(JSON_KEY_AUTO_ACTIVATE, IDS_CURSORWRAP_NAME, m_autoActivate);
166          settings.add_bool_toggle(JSON_KEY_DISABLE_WRAP_DURING_DRAG, IDS_CURSORWRAP_NAME, m_disableWrapDuringDrag);
167  
168          return settings.serialize_to_buffer(buffer, buffer_size);
169      }
170  
171      // Signal from the Settings editor to call a custom action.
172      // This can be used to spawn more complex editors.
173      virtual void call_custom_action(const wchar_t* /*action*/) override {}
174  
175      // Called by the runner to pass the updated settings values as a serialized JSON.
176      virtual void set_config(const wchar_t* config) override
177      {
178          try
179          {
180              // Parse the input JSON string.
181              PowerToysSettings::PowerToyValues values =
182                  PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
183  
184              parse_settings(values);
185          }
186          catch (std::exception&)
187          {
188              Logger::error("Invalid json when trying to parse CursorWrap settings json.");
189          }
190      }
191  
192      // Enable the powertoy
193      virtual void enable()
194      {
195          m_enabled = true;
196          Trace::EnableCursorWrap(true);
197  
198          // Start listening for external trigger event so we can invoke the same logic as the activation hotkey.
199          m_triggerEventHandle = CreateEventW(nullptr, false, false, CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT);
200          m_terminateEventHandle = CreateEventW(nullptr, false, false, nullptr);
201          if (m_triggerEventHandle)
202          {
203              ResetEvent(m_triggerEventHandle);
204          }
205          if (m_triggerEventHandle && m_terminateEventHandle)
206          {
207              m_listening = true;
208              m_eventThread = std::thread([this]() {
209                  HANDLE handles[2] = { m_triggerEventHandle, m_terminateEventHandle };
210  
211                  // WH_MOUSE_LL callbacks are delivered to the thread that installed the hook.
212                  // Ensure this thread has a message queue and pumps messages while the hook is active.
213                  MSG msg;
214                  PeekMessage(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
215  
216                  // Create message window for display change notifications
217                  RegisterForDisplayChanges();
218  
219                  // Only start the mouse hook automatically if auto-activate is enabled
220                  if (m_autoActivate)
221                  {
222                      StartMouseHook();
223                      Logger::info("CursorWrap enabled - mouse hook started (auto-activate on)");
224                  }
225                  else
226                  {
227                      Logger::info("CursorWrap enabled - waiting for activation hotkey (auto-activate off)");
228                  }
229  
230                  while (m_listening)
231                  {
232                      auto res = MsgWaitForMultipleObjects(2, handles, false, INFINITE, QS_ALLINPUT);
233                      if (!m_listening)
234                      {
235                          break;
236                      }
237  
238                      if (res == WAIT_OBJECT_0)
239                      {
240                          ToggleMouseHook();
241                      }
242                      else if (res == WAIT_OBJECT_0 + 1)
243                      {
244                          break;
245                      }
246                      else
247                      {
248                          while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
249                          {
250                              TranslateMessage(&msg);
251                              DispatchMessage(&msg);
252                          }
253                      }
254                  }
255  
256                  // Cleanup display change notifications
257                  UnregisterDisplayChanges();
258  
259                  StopMouseHook();
260                  Logger::info("CursorWrap event listener stopped");
261              });
262          }
263      }
264  
265      // Disable the powertoy
266      virtual void disable()
267      {
268          m_enabled = false;
269          Trace::EnableCursorWrap(false);
270  
271          m_listening = false;
272          if (m_terminateEventHandle)
273          {
274              SetEvent(m_terminateEventHandle);
275          }
276          if (m_eventThread.joinable())
277          {
278              m_eventThread.join();
279          }
280          if (m_triggerEventHandle)
281          {
282              CloseHandle(m_triggerEventHandle);
283              m_triggerEventHandle = nullptr;
284          }
285          if (m_terminateEventHandle)
286          {
287              CloseHandle(m_terminateEventHandle);
288              m_terminateEventHandle = nullptr;
289          }
290      }
291  
292      // Returns if the powertoys is enabled
293      virtual bool is_enabled() override
294      {
295          return m_enabled;
296      }
297  
298      // Returns whether the PowerToys should be enabled by default
299      virtual bool is_enabled_by_default() const override
300      {
301          return false;
302      }
303  
304      // Legacy hotkey support
305      virtual size_t get_hotkeys(Hotkey* buffer, size_t buffer_size) override
306      {
307          if (buffer && buffer_size >= 1)
308          {
309              buffer[0] = m_activationHotkey;
310          }
311          return 1;
312      }
313  
314      virtual bool on_hotkey(size_t hotkeyId) override
315      {
316          if (!m_enabled || hotkeyId != 0)
317          {
318              return false;
319          }
320  
321          // Toggle on the thread that owns the WH_MOUSE_LL hook (the event listener thread).
322          if (m_triggerEventHandle)
323          {
324              return SetEvent(m_triggerEventHandle);
325          }
326  
327          return false;
328      }
329  
330      // Called when display configuration changes - update monitor topology
331      void OnDisplayChange()
332      {
333  #ifdef _DEBUG
334          OutputDebugStringW(L"[CursorWrap] Display configuration changed, updating monitor topology\n");
335  #endif
336          Logger::info("Display configuration changed, updating monitor topology");
337          m_core.UpdateMonitorInfo();
338      }
339  
340  private:
341      void ToggleMouseHook()
342      {
343          // Toggle cursor wrapping.
344          if (m_hookActive)
345          {
346              StopMouseHook();
347          }
348          else
349          {
350              StartMouseHook();
351          }
352      }
353  
354      // Load the settings file.
355      void init_settings()
356      {
357          try
358          {
359              // Load and parse the settings file for this PowerToy.
360              PowerToysSettings::PowerToyValues settings =
361                  PowerToysSettings::PowerToyValues::load_from_settings_file(CursorWrap::get_key());
362              parse_settings(settings);
363          }
364          catch (std::exception&)
365          {
366              Logger::error("Invalid json when trying to load the CursorWrap settings json from file.");
367          }
368      }
369  
370      void parse_settings(PowerToysSettings::PowerToyValues& settings)
371      {
372          auto settingsObject = settings.get_raw_json();
373          if (settingsObject.GetView().Size())
374          {
375              try
376              {
377                  // Parse activation HotKey
378                  auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
379                  auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
380  
381                  m_activationHotkey.win = hotkey.win_pressed();
382                  m_activationHotkey.ctrl = hotkey.ctrl_pressed();
383                  m_activationHotkey.shift = hotkey.shift_pressed();
384                  m_activationHotkey.alt = hotkey.alt_pressed();
385                  m_activationHotkey.key = static_cast<unsigned char>(hotkey.get_code());
386              }
387              catch (...)
388              {
389                  Logger::warn("Failed to initialize CursorWrap activation shortcut");
390              }
391              
392              try
393              {
394                  // Parse auto activate
395                  auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_AUTO_ACTIVATE);
396                  m_autoActivate = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
397              }
398              catch (...)
399              {
400                  Logger::warn("Failed to initialize CursorWrap auto activate from settings. Will use default value");
401              }
402              
403              try
404              {
405                  // Parse disable wrap during drag
406                  auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
407                  if (propertiesObject.HasKey(JSON_KEY_DISABLE_WRAP_DURING_DRAG))
408                  {
409                      auto disableDragObject = propertiesObject.GetNamedObject(JSON_KEY_DISABLE_WRAP_DURING_DRAG);
410                      m_disableWrapDuringDrag = disableDragObject.GetNamedBoolean(JSON_KEY_VALUE);
411                  }
412              }
413              catch (...)
414              {
415                  Logger::warn("Failed to initialize CursorWrap disable wrap during drag from settings. Will use default value (true)");
416              }
417              
418              try
419              {
420                  // Parse wrap mode
421                  auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
422                  if (propertiesObject.HasKey(JSON_KEY_WRAP_MODE))
423                  {
424                      auto wrapModeObject = propertiesObject.GetNamedObject(JSON_KEY_WRAP_MODE);
425                      m_wrapMode = static_cast<int>(wrapModeObject.GetNamedNumber(JSON_KEY_VALUE));
426                  }
427              }
428              catch (...)
429              {
430                  Logger::warn("Failed to initialize CursorWrap wrap mode from settings. Will use default value (0=Both)");
431              }
432              
433              try
434              {
435                  // Parse disable on single monitor
436                  auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
437                  if (propertiesObject.HasKey(JSON_KEY_DISABLE_ON_SINGLE_MONITOR))
438                  {
439                      auto disableOnSingleMonitorObject = propertiesObject.GetNamedObject(JSON_KEY_DISABLE_ON_SINGLE_MONITOR);
440                      m_disableOnSingleMonitor = disableOnSingleMonitorObject.GetNamedBoolean(JSON_KEY_VALUE);
441                  }
442              }
443              catch (...)
444              {
445                  Logger::warn("Failed to initialize CursorWrap disable on single monitor from settings. Will use default value (false)");
446              }
447          }
448          else
449          {
450              Logger::info("CursorWrap settings are empty");
451          }
452          
453          // Set default hotkey if not configured
454          if (m_activationHotkey.key == 0)
455          {
456              m_activationHotkey.win = true;
457              m_activationHotkey.alt = true;
458              m_activationHotkey.ctrl = false;
459              m_activationHotkey.shift = false;
460              m_activationHotkey.key = 'U'; // Win+Alt+U
461          }
462      }
463  
464      void StartMouseHook()
465      {
466          if (m_mouseHook || m_hookActive)
467          {
468              Logger::info("CursorWrap mouse hook already active");
469              return;
470          }
471  
472          // Refresh monitor info before starting hook
473          m_core.UpdateMonitorInfo();
474          
475          m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, GetModuleHandle(nullptr), 0);
476          if (m_mouseHook)
477          {
478              m_hookActive = true;
479              Logger::info("CursorWrap mouse hook started successfully");
480  #ifdef _DEBUG
481              Logger::info(L"CursorWrap DEBUG: Hook installed");
482  #endif
483          }
484          else
485          {
486              DWORD error = GetLastError();
487              Logger::error(L"Failed to install CursorWrap mouse hook, error: {}", error);
488          }
489      }
490  
491      void StopMouseHook()
492      {
493          if (m_mouseHook)
494          {
495              UnhookWindowsHookEx(m_mouseHook);
496              m_mouseHook = nullptr;
497              m_hookActive = false;
498              Logger::info("CursorWrap mouse hook stopped");
499  #ifdef _DEBUG
500              Logger::info("CursorWrap DEBUG: Mouse hook stopped");
501  #endif
502          }
503      }
504  
505      void RegisterForDisplayChanges()
506      {
507          if (m_messageWindow)
508          {
509              return; // Already registered
510          }
511  
512          // Create a hidden top-level window to receive broadcast messages
513          // NOTE: Message-only windows (HWND_MESSAGE parent) do NOT receive
514          // WM_DISPLAYCHANGE, WM_SETTINGCHANGE, or WM_DEVICECHANGE broadcasts.
515          // We must use a real (hidden) top-level window instead.
516          WNDCLASSEXW wc = { sizeof(WNDCLASSEXW) };
517          wc.lpfnWndProc = MessageWindowProc;
518          wc.hInstance = GetModuleHandle(nullptr);
519          wc.lpszClassName = L"CursorWrapDisplayChangeWindow";
520  
521          RegisterClassExW(&wc);
522  
523          // Create a hidden top-level window (not message-only)
524          // WS_EX_TOOLWINDOW prevents taskbar button, WS_POPUP with no size makes it invisible
525          m_messageWindow = CreateWindowExW(
526              WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
527              L"CursorWrapDisplayChangeWindow",
528              nullptr,
529              WS_POPUP,  // Minimal window style
530              0, 0, 0, 0,  // Zero size = invisible
531              nullptr,  // No parent - top-level window to receive broadcasts
532              nullptr,
533              GetModuleHandle(nullptr),
534              nullptr);
535  
536          if (m_messageWindow)
537          {
538  #ifdef _DEBUG
539              OutputDebugStringW(L"[CursorWrap] Registered for display change notifications\n");
540  #endif
541              Logger::info("Registered for display change notifications");
542  
543              // Register for device notifications (monitor hardware add/remove)
544              DEV_BROADCAST_DEVICEINTERFACE filter = {};
545              filter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
546              filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
547              filter.dbcc_classguid = GUID_DEVINTERFACE_MONITOR;
548  
549              m_deviceNotify = RegisterDeviceNotificationW(
550                  m_messageWindow,
551                  &filter,
552                  DEVICE_NOTIFY_WINDOW_HANDLE);
553  
554              if (m_deviceNotify)
555              {
556  #ifdef _DEBUG
557                  OutputDebugStringW(L"[CursorWrap] Registered for device notifications (monitor hardware changes)\n");
558  #endif
559                  Logger::info("Registered for device notifications (monitor hardware changes)");
560              }
561              else
562              {
563                  DWORD error = GetLastError();
564  #ifdef _DEBUG
565                  std::wostringstream oss;
566                  oss << L"[CursorWrap] Failed to register device notifications. Error: " << error << L"\n";
567                  OutputDebugStringW(oss.str().c_str());
568  #endif
569                  Logger::warn("Failed to register device notifications. Error: {}", error);
570              }
571          }
572          else
573          {
574              DWORD error = GetLastError();
575              Logger::error(L"Failed to create message window for display changes, error: {}", error);
576          }
577      }
578  
579      void UnregisterDisplayChanges()
580      {
581          if (m_deviceNotify)
582          {
583  #ifdef _DEBUG
584              OutputDebugStringW(L"[CursorWrap] Unregistering device notifications...\n");
585  #endif
586              UnregisterDeviceNotification(m_deviceNotify);
587              m_deviceNotify = nullptr;
588              Logger::info("Unregistered device notifications");
589          }
590  
591          if (m_messageWindow)
592          {
593              KillTimer(m_messageWindow, TIMER_UPDATE_MONITORS);
594              DestroyWindow(m_messageWindow);
595              m_messageWindow = nullptr;
596              UnregisterClassW(L"CursorWrapDisplayChangeWindow", GetModuleHandle(nullptr));
597  #ifdef _DEBUG
598              OutputDebugStringW(L"[CursorWrap] Unregistered display change notifications\n");
599  #endif
600              Logger::info("Unregistered display change notifications");
601          }
602      }
603  
604      static LRESULT CALLBACK MessageWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
605      {
606          if (!g_cursorWrapInstance)
607          {
608              return DefWindowProcW(hwnd, msg, wParam, lParam);
609          }
610  
611          switch (msg)
612          {
613          case WM_DISPLAYCHANGE:
614  #ifdef _DEBUG
615              OutputDebugStringW(L"[CursorWrap] WM_DISPLAYCHANGE received - monitor resolution/DPI changed\n");
616  #endif
617              Logger::info("WM_DISPLAYCHANGE received - resolution/DPI changed");
618              // Debounce: Wait for multiple changes to settle
619              KillTimer(hwnd, TIMER_UPDATE_MONITORS);
620              SetTimer(hwnd, TIMER_UPDATE_MONITORS, DEBOUNCE_DELAY_MS, nullptr);
621              break;
622  
623          case WM_SETTINGCHANGE:
624              if (wParam == SPI_SETWORKAREA)
625              {
626  #ifdef _DEBUG
627                  OutputDebugStringW(L"[CursorWrap] WM_SETTINGCHANGE (SPI_SETWORKAREA) received - taskbar changed\n");
628  #endif
629                  Logger::info("WM_SETTINGCHANGE (SPI_SETWORKAREA) received");
630                  // Taskbar position/size changed
631                  KillTimer(hwnd, TIMER_UPDATE_MONITORS);
632                  SetTimer(hwnd, TIMER_UPDATE_MONITORS, DEBOUNCE_DELAY_MS, nullptr);
633              }
634              break;
635  
636          case WM_DEVICECHANGE:
637              // Handle monitor hardware add/remove
638              if (wParam == DBT_DEVNODES_CHANGED)
639              {
640  #ifdef _DEBUG
641                  OutputDebugStringW(L"[CursorWrap] DBT_DEVNODES_CHANGED received - monitor hardware change detected\n");
642  #endif
643                  Logger::info("DBT_DEVNODES_CHANGED received - monitor hardware change detected");
644                  // Debounce: Wait for multiple changes to settle
645                  KillTimer(hwnd, TIMER_UPDATE_MONITORS);
646                  SetTimer(hwnd, TIMER_UPDATE_MONITORS, DEBOUNCE_DELAY_MS, nullptr);
647                  return TRUE;
648              }
649              break;
650  
651          case WM_TIMER:
652              if (wParam == TIMER_UPDATE_MONITORS)
653              {
654  #ifdef _DEBUG
655                  OutputDebugStringW(L"[CursorWrap] Debounce timer expired - triggering topology update\n");
656  #endif
657                  KillTimer(hwnd, TIMER_UPDATE_MONITORS);
658                  g_cursorWrapInstance->OnDisplayChange();
659              }
660              break;
661          }
662  
663          return DefWindowProcW(hwnd, msg, wParam, lParam);
664      }
665  
666      static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam)
667      {
668          if (nCode >= 0 && wParam == WM_MOUSEMOVE)
669          {
670              auto* pMouseStruct = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
671              POINT currentPos = { pMouseStruct->pt.x, pMouseStruct->pt.y };
672              
673              if (g_cursorWrapInstance && g_cursorWrapInstance->m_hookActive)
674              {
675                  POINT newPos = g_cursorWrapInstance->m_core.HandleMouseMove(
676                      currentPos,
677                      g_cursorWrapInstance->m_disableWrapDuringDrag,
678                      g_cursorWrapInstance->m_wrapMode,
679                      g_cursorWrapInstance->m_disableOnSingleMonitor);
680                      
681                  if (newPos.x != currentPos.x || newPos.y != currentPos.y)
682                  {
683  #ifdef _DEBUG
684                      Logger::info(L"CursorWrap DEBUG: Wrapping cursor from ({}, {}) to ({}, {})", 
685                                  currentPos.x, currentPos.y, newPos.x, newPos.y);
686  #endif
687                      SetCursorPos(newPos.x, newPos.y);
688                      return 1; // Suppress the original message
689                  }
690              }
691          }
692          
693          return CallNextHookEx(nullptr, nCode, wParam, lParam);
694      }
695  };
696  
697  extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
698  {
699      return new CursorWrap();
700  }