/ src / modules / MouseUtils / FindMyMouse / FindMyMouse.cpp
FindMyMouse.cpp
   1  // FindMyMouse.cpp : Based on Raymond Chen's SuperSonar.cpp
   2  //
   3  #include "pch.h"
   4  #include "FindMyMouse.h"
   5  #include "WinHookEventIDs.h"
   6  #include "trace.h"
   7  #include "common/utils/game_mode.h"
   8  #include "common/utils/process_path.h"
   9  #include "common/utils/excluded_apps.h"
  10  #include "common/utils/MsWindowsSettings.h"
  11  #include <winrt/Windows.Graphics.h>
  12  
  13  #include <winrt/Microsoft.UI.Composition.Interop.h>
  14  #include <winrt/Microsoft.UI.Dispatching.h>
  15  #include <winrt/Microsoft.UI.Xaml.h>
  16  #include <winrt/Microsoft.UI.Xaml.Controls.h>
  17  #include <winrt/Microsoft.UI.Xaml.Media.h>
  18  #include <winrt/Microsoft.UI.Xaml.Hosting.h>
  19  #include <winrt/Microsoft.UI.Interop.h>
  20  #include <winrt/Microsoft.UI.Content.h>
  21  
  22  #include <vector>
  23  
  24  namespace winrt
  25  {
  26      using namespace winrt::Windows::System;
  27  }
  28  
  29  namespace muxc = winrt::Microsoft::UI::Composition;
  30  namespace muxx = winrt::Microsoft::UI::Xaml;
  31  namespace muxxc = winrt::Microsoft::UI::Xaml::Controls;
  32  namespace muxxh = winrt::Microsoft::UI::Xaml::Hosting;
  33  
  34  #pragma region Super_Sonar_Base_Code
  35  
  36  template<typename D>
  37  struct SuperSonar
  38  {
  39      bool Initialize(HINSTANCE hinst);
  40      void Terminate();
  41  
  42  protected:
  43      // You are expected to override these, as appropriate.
  44  
  45      DWORD GetExtendedStyle()
  46      {
  47          return 0;
  48      }
  49  
  50      LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept
  51      {
  52          return BaseWndProc(message, wParam, lParam);
  53      }
  54  
  55      void BeforeMoveSonar() {}
  56      void AfterMoveSonar() {}
  57      void SetSonarVisibility(bool visible) = delete;
  58      void UpdateMouseSnooping();
  59      bool IsForegroundAppExcluded();
  60  
  61  protected:
  62      // Base class members you can access.
  63      D* Shim() { return static_cast<D*>(this); }
  64      LRESULT BaseWndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept;
  65  
  66      HWND m_hwnd{};
  67      POINT m_sonarPos = ptNowhere;
  68  
  69      // Only consider double left control click if at least 100ms passed between the clicks, to avoid keyboards that might be sending rapid clicks.
  70      // At actual check, time a fifth of the current double click setting might be used instead to take into account users who might have low values.
  71      static const int MIN_DOUBLE_CLICK_TIME = 100;
  72  
  73      bool m_destroyed = false;
  74      FindMyMouseActivationMethod m_activationMethod = FIND_MY_MOUSE_DEFAULT_ACTIVATION_METHOD;
  75      bool m_includeWinKey = FIND_MY_MOUSE_DEFAULT_INCLUDE_WIN_KEY;
  76      bool m_doNotActivateOnGameMode = FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE;
  77      int m_sonarRadius = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS;
  78      int m_sonarZoomFactor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM;
  79      DWORD m_fadeDuration = FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS;
  80      std::vector<std::wstring> m_excludedApps;
  81      int m_shakeMinimumDistance = FIND_MY_MOUSE_DEFAULT_SHAKE_MINIMUM_DISTANCE;
  82      winrt::Microsoft::UI::Dispatching::DispatcherQueueController m_dispatcherQueueController{ nullptr };
  83  
  84      // Don't consider movements started past these milliseconds to detect shaking.
  85      int m_shakeIntervalMs = FIND_MY_MOUSE_DEFAULT_SHAKE_INTERVAL_MS;
  86      // By which factor must travelled distance be than the diagonal of the rectangle containing the movements. (value in percent)
  87      int m_shakeFactor = FIND_MY_MOUSE_DEFAULT_SHAKE_FACTOR;
  88  
  89  private:
  90      // Save the mouse movement that occurred in any direction.
  91      struct PointerRecentMovement
  92      {
  93          POINT diff;
  94          ULONGLONG tick;
  95      };
  96      std::vector<PointerRecentMovement> m_movementHistory;
  97      // Raw Input may give relative or absolute values. Need to take each case into account.
  98      bool m_seenAnAbsoluteMousePosition = false;
  99      POINT m_lastAbsolutePosition = { 0, 0 };
 100  
 101      static inline byte GetSign(LONG const& num)
 102      {
 103          if (num > 0)
 104              return 1;
 105          if (num < 0)
 106              return -1;
 107          return 0;
 108      }
 109  
 110      static bool IsEqual(POINT const& p1, POINT const& p2)
 111      {
 112          return p1.x == p2.x && p1.y == p2.y;
 113      }
 114  
 115      static constexpr POINT ptNowhere = { LONG_MIN, LONG_MIN };
 116      static constexpr DWORD TIMER_ID_TRACK = 100;
 117      static constexpr DWORD IdlePeriod = 1000;
 118  
 119      // Activate sonar: Hit LeftControl twice.
 120      enum class SonarState
 121      {
 122          Idle,
 123          ControlDown1,
 124          ControlUp1,
 125          ControlDown2,
 126          ControlUp2,
 127      };
 128  
 129      HWND m_hwndOwner{};
 130      SonarState m_sonarState = SonarState::Idle;
 131      POINT m_lastKeyPos{};
 132      ULONGLONG m_lastKeyTime{};
 133  
 134      static constexpr DWORD NoSonar = 0;
 135      static constexpr DWORD SonarWaitingForMouseMove = 1;
 136      ULONGLONG m_sonarStart = NoSonar;
 137      bool m_isSnoopingMouse = false;
 138  
 139  private:
 140      static constexpr auto className = L"FindMyMouse";
 141  
 142      static constexpr auto windowTitle = L"PowerToys Find My Mouse";
 143  
 144      static LRESULT CALLBACK s_WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
 145  
 146      BOOL OnSonarCreate();
 147      void OnSonarDestroy();
 148      void OnSonarInput(WPARAM flags, HRAWINPUT hInput);
 149      void OnSonarKeyboardInput(RAWINPUT const& input);
 150      void OnSonarMouseInput(RAWINPUT const& input);
 151      void OnMouseTimer();
 152  
 153      void DetectShake();
 154      bool KeyboardInputCanActivate();
 155  
 156      void StartSonar(FindMyMouseActivationMethod activationMethod);
 157      void StopSonar();
 158  };
 159  
 160  template<typename D>
 161  bool SuperSonar<D>::Initialize(HINSTANCE hinst)
 162  {
 163      SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
 164  
 165      WNDCLASS wc{};
 166      if (!GetClassInfoW(hinst, className, &wc))
 167      {
 168          wc.lpfnWndProc = s_WndProc;
 169          wc.hInstance = hinst;
 170          wc.hIcon = LoadIcon(hinst, IDI_APPLICATION);
 171          wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
 172          wc.hbrBackground = static_cast<HBRUSH>(GetStockObject(NULL_BRUSH));
 173          wc.lpszClassName = className;
 174  
 175          if (!RegisterClassW(&wc))
 176          {
 177              Logger::error("RegisterClassW failed. GetLastError={}", GetLastError());
 178              return false;
 179          }
 180      }
 181      // else: class already registered
 182  
 183      m_hwndOwner = CreateWindow(L"static", nullptr, WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, hinst, nullptr);
 184      if (!m_hwndOwner)
 185      {
 186          Logger::error("Failed to create owner window. GetLastError={}", GetLastError());
 187          return false;
 188      }
 189  
 190      DWORD exStyle = WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW | Shim()->GetExtendedStyle();
 191      HWND created = CreateWindowExW(exStyle, className, windowTitle, WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hinst, this);
 192      if (!created)
 193      {
 194          Logger::error("CreateWindowExW failed. GetLastError={}", GetLastError());
 195          return false;
 196      }
 197  
 198      return true;
 199  }
 200  
 201  template<typename D>
 202  void SuperSonar<D>::Terminate()
 203  {
 204      auto dispatcherQueue = m_dispatcherQueueController.DispatcherQueue();
 205      bool enqueueSucceeded = dispatcherQueue.TryEnqueue([=]() {
 206          m_destroyed = true;
 207          DestroyWindow(m_hwndOwner);
 208      });
 209      if (!enqueueSucceeded)
 210      {
 211          Logger::error("Couldn't enqueue message to destroy the sonar Window.");
 212      }
 213  }
 214  
 215  template<typename D>
 216  LRESULT SuperSonar<D>::s_WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
 217  {
 218      SuperSonar* self;
 219      if (message == WM_NCCREATE)
 220      {
 221          auto info = reinterpret_cast<LPCREATESTRUCT>(lParam);
 222          SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(info->lpCreateParams));
 223          self = static_cast<SuperSonar*>(info->lpCreateParams);
 224          self->m_hwnd = hwnd;
 225      }
 226      else
 227      {
 228          self = reinterpret_cast<SuperSonar*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
 229      }
 230      if (self)
 231      {
 232          return self->Shim()->WndProc(message, wParam, lParam);
 233      }
 234      else
 235      {
 236          return DefWindowProc(hwnd, message, wParam, lParam);
 237      }
 238  }
 239  
 240  template<typename D>
 241  LRESULT SuperSonar<D>::BaseWndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept
 242  {
 243      switch (message)
 244      {
 245      case WM_CREATE:
 246          if (!OnSonarCreate())
 247              return -1;
 248          UpdateMouseSnooping();
 249          return 0;
 250  
 251      case WM_DESTROY:
 252          OnSonarDestroy();
 253          break;
 254  
 255      case WM_INPUT:
 256          OnSonarInput(wParam, reinterpret_cast<HRAWINPUT>(lParam));
 257          break;
 258  
 259      case WM_TIMER:
 260          switch (wParam)
 261          {
 262          case TIMER_ID_TRACK:
 263              OnMouseTimer();
 264              break;
 265          }
 266          break;
 267  
 268      case WM_NCHITTEST:
 269          return HTTRANSPARENT;
 270      }
 271  
 272      if (message == WM_PRIV_SHORTCUT)
 273      {
 274          if (m_sonarStart == NoSonar)
 275          {
 276              StartSonar(FindMyMouseActivationMethod::Shortcut);
 277          }
 278          else
 279          {
 280              StopSonar();
 281          }
 282      }
 283  
 284      return DefWindowProc(m_hwnd, message, wParam, lParam);
 285  }
 286  
 287  template<typename D>
 288  BOOL SuperSonar<D>::OnSonarCreate()
 289  {
 290      RAWINPUTDEVICE keyboard{};
 291      keyboard.usUsagePage = HID_USAGE_PAGE_GENERIC;
 292      keyboard.usUsage = HID_USAGE_GENERIC_KEYBOARD;
 293      keyboard.dwFlags = RIDEV_INPUTSINK;
 294      keyboard.hwndTarget = m_hwnd;
 295      return RegisterRawInputDevices(&keyboard, 1, sizeof(keyboard));
 296  }
 297  
 298  template<typename D>
 299  void SuperSonar<D>::OnSonarDestroy()
 300  {
 301      PostQuitMessage(0);
 302  }
 303  
 304  template<typename D>
 305  void SuperSonar<D>::OnSonarInput(WPARAM flags, HRAWINPUT hInput)
 306  {
 307      RAWINPUT input;
 308      UINT size = sizeof(input);
 309      auto result = GetRawInputData(hInput, RID_INPUT, &input, &size, sizeof(RAWINPUTHEADER));
 310      if (result < sizeof(RAWINPUTHEADER))
 311      {
 312          return;
 313      }
 314  
 315      switch (input.header.dwType)
 316      {
 317      case RIM_TYPEKEYBOARD:
 318          OnSonarKeyboardInput(input);
 319          break;
 320      case RIM_TYPEMOUSE:
 321          OnSonarMouseInput(input);
 322          break;
 323      }
 324  }
 325  
 326  template<typename D>
 327  void SuperSonar<D>::OnSonarKeyboardInput(RAWINPUT const& input)
 328  {
 329      // Don't stop the sonar when the shortcut is released
 330      if (m_activationMethod == FindMyMouseActivationMethod::Shortcut && (input.data.keyboard.Flags & RI_KEY_BREAK) != 0)
 331      {
 332          return;
 333      }
 334  
 335      if ((m_activationMethod != FindMyMouseActivationMethod::DoubleRightControlKey && m_activationMethod != FindMyMouseActivationMethod::DoubleLeftControlKey) || input.data.keyboard.VKey != VK_CONTROL)
 336      {
 337          StopSonar();
 338          return;
 339      }
 340  
 341      bool pressed = (input.data.keyboard.Flags & RI_KEY_BREAK) == 0;
 342  
 343      bool leftCtrlPressed = (input.data.keyboard.Flags & RI_KEY_E0) == 0;
 344      bool rightCtrlPressed = (input.data.keyboard.Flags & RI_KEY_E0) != 0;
 345  
 346      if ((m_activationMethod == FindMyMouseActivationMethod::DoubleRightControlKey && !rightCtrlPressed) || (m_activationMethod == FindMyMouseActivationMethod::DoubleLeftControlKey && !leftCtrlPressed))
 347      {
 348          StopSonar();
 349          return;
 350      }
 351  
 352      switch (m_sonarState)
 353      {
 354      case SonarState::Idle:
 355          if (pressed)
 356          {
 357              m_sonarState = SonarState::ControlDown1;
 358              m_lastKeyTime = GetTickCount64();
 359              m_lastKeyPos = {};
 360              GetCursorPos(&m_lastKeyPos);
 361              UpdateMouseSnooping();
 362          }
 363          break;
 364  
 365      case SonarState::ControlDown1:
 366          if (!pressed)
 367          {
 368              m_sonarState = SonarState::ControlUp1;
 369          }
 370          break;
 371  
 372      case SonarState::ControlUp1:
 373          if (pressed && KeyboardInputCanActivate())
 374          {
 375              auto now = GetTickCount64();
 376              auto doubleClickInterval = now - m_lastKeyTime;
 377              POINT ptCursor{};
 378              auto doubleClickTimeSetting = GetDoubleClickTime();
 379              if (GetCursorPos(&ptCursor) &&
 380                  doubleClickInterval >= min(MIN_DOUBLE_CLICK_TIME, doubleClickTimeSetting / 5) &&
 381                  doubleClickInterval <= doubleClickTimeSetting &&
 382                  IsEqual(m_lastKeyPos, ptCursor))
 383              {
 384                  m_sonarState = SonarState::ControlDown2;
 385                  StartSonar(m_activationMethod);
 386              }
 387              else
 388              {
 389                  m_sonarState = SonarState::ControlDown1;
 390                  m_lastKeyTime = GetTickCount64();
 391                  m_lastKeyPos = {};
 392                  GetCursorPos(&m_lastKeyPos);
 393                  UpdateMouseSnooping();
 394              }
 395              m_lastKeyTime = now;
 396              m_lastKeyPos = ptCursor;
 397          }
 398          break;
 399      case SonarState::ControlUp2:
 400          // Also deactivate sonar with left control.
 401          if (pressed)
 402          {
 403              StopSonar();
 404          }
 405          break;
 406      case SonarState::ControlDown2:
 407          if (!pressed)
 408          {
 409              m_sonarState = SonarState::ControlUp2;
 410          }
 411          break;
 412      }
 413  }
 414  
 415  // Shaking detection algorithm is: Has distance travelled been much greater than the diagonal of the rectangle containing the movement?
 416  template<typename D>
 417  void SuperSonar<D>::DetectShake()
 418  {
 419      ULONGLONG shakeStartTick = GetTickCount64() - m_shakeIntervalMs;
 420  
 421      // Prune the story of movements for those movements that started too long ago.
 422      std::erase_if(m_movementHistory, [shakeStartTick](const PointerRecentMovement& movement) { return movement.tick < shakeStartTick; });
 423  
 424      double distanceTravelled = 0;
 425      LONGLONG currentX = 0, minX = 0, maxX = 0;
 426      LONGLONG currentY = 0, minY = 0, maxY = 0;
 427  
 428      for (const PointerRecentMovement& movement : m_movementHistory)
 429      {
 430          currentX += movement.diff.x;
 431          currentY += movement.diff.y;
 432          distanceTravelled += sqrt(static_cast<double>(movement.diff.x) * movement.diff.x + static_cast<double>(movement.diff.y) * movement.diff.y); // Pythagorean theorem
 433          minX = min(currentX, minX);
 434          maxX = max(currentX, maxX);
 435          minY = min(currentY, minY);
 436          maxY = max(currentY, maxY);
 437      }
 438  
 439      if (distanceTravelled < m_shakeMinimumDistance)
 440      {
 441          return;
 442      }
 443  
 444      // Size of the rectangle that the pointer moved in.
 445      double rectangleWidth = static_cast<double>(maxX) - minX;
 446      double rectangleHeight = static_cast<double>(maxY) - minY;
 447  
 448      double diagonal = sqrt(rectangleWidth * rectangleWidth + rectangleHeight * rectangleHeight);
 449      if (diagonal > 0 && distanceTravelled / diagonal > (m_shakeFactor / 100.f))
 450      {
 451          m_movementHistory.clear();
 452          StartSonar(m_activationMethod);
 453      }
 454  }
 455  
 456  template<typename D>
 457  bool SuperSonar<D>::KeyboardInputCanActivate()
 458  {
 459      return !m_includeWinKey || (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000);
 460  }
 461  
 462  template<typename D>
 463  void SuperSonar<D>::OnSonarMouseInput(RAWINPUT const& input)
 464  {
 465      if (m_activationMethod == FindMyMouseActivationMethod::ShakeMouse)
 466      {
 467          LONG relativeX = 0;
 468          LONG relativeY = 0;
 469          if ((input.data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE) == MOUSE_MOVE_ABSOLUTE && (input.data.mouse.lLastX != 0 || input.data.mouse.lLastY != 0))
 470          {
 471              // Getting absolute mouse coordinates. Likely inside a VM / RDP session.
 472              if (m_seenAnAbsoluteMousePosition)
 473              {
 474                  relativeX = input.data.mouse.lLastX - m_lastAbsolutePosition.x;
 475                  relativeY = input.data.mouse.lLastY - m_lastAbsolutePosition.y;
 476                  m_lastAbsolutePosition.x = input.data.mouse.lLastX;
 477                  m_lastAbsolutePosition.y = input.data.mouse.lLastY;
 478              }
 479              m_seenAnAbsoluteMousePosition = true;
 480          }
 481          else
 482          {
 483              relativeX = input.data.mouse.lLastX;
 484              relativeY = input.data.mouse.lLastY;
 485          }
 486          if (m_movementHistory.size() > 0)
 487          {
 488              PointerRecentMovement& lastMovement = m_movementHistory.back();
 489              // If the pointer is still moving in the same direction, just add to that movement instead of adding a new movement.
 490              // This helps in keeping the list of movements smaller even in cases where a high number of messages is sent.
 491              if (GetSign(lastMovement.diff.x) == GetSign(relativeX) && GetSign(lastMovement.diff.y) == GetSign(relativeY))
 492              {
 493                  lastMovement.diff.x += relativeX;
 494                  lastMovement.diff.y += relativeY;
 495              }
 496              else
 497              {
 498                  m_movementHistory.push_back({ .diff = { .x = relativeX, .y = relativeY }, .tick = GetTickCount64() });
 499                  // Mouse movement changed directions. Take the opportunity do detect shake.
 500                  DetectShake();
 501              }
 502          }
 503          else
 504          {
 505              m_movementHistory.push_back({ .diff = { .x = relativeX, .y = relativeY }, .tick = GetTickCount64() });
 506          }
 507      }
 508  
 509      if (input.data.mouse.usButtonFlags)
 510      {
 511          StopSonar();
 512      }
 513      else if (m_sonarStart != NoSonar)
 514      {
 515          OnMouseTimer();
 516      }
 517  }
 518  
 519  template<typename D>
 520  void SuperSonar<D>::StartSonar(FindMyMouseActivationMethod activationMethod)
 521  {
 522      // Don't activate if game mode is on.
 523      if (m_doNotActivateOnGameMode && detect_game_mode())
 524      {
 525          return;
 526      }
 527  
 528      if (IsForegroundAppExcluded())
 529      {
 530          return;
 531      }
 532  
 533      Trace::MousePointerFocused(static_cast<int>(activationMethod));
 534      // Cover the entire virtual screen.
 535      // HACK: Draw with 1 pixel off. Otherwise, Windows glitches the task bar transparency when a transparent window fill the whole screen.
 536      SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN) + 1, GetSystemMetrics(SM_YVIRTUALSCREEN) + 1, GetSystemMetrics(SM_CXVIRTUALSCREEN) - 2, GetSystemMetrics(SM_CYVIRTUALSCREEN) - 2, 0);
 537      m_sonarPos = ptNowhere;
 538      OnMouseTimer();
 539      UpdateMouseSnooping();
 540      Shim()->SetSonarVisibility(true);
 541  }
 542  
 543  template<typename D>
 544  void SuperSonar<D>::StopSonar()
 545  {
 546      if (m_sonarStart != NoSonar)
 547      {
 548          m_sonarStart = NoSonar;
 549          Shim()->SetSonarVisibility(false);
 550          KillTimer(m_hwnd, TIMER_ID_TRACK);
 551      }
 552      m_sonarState = SonarState::Idle;
 553      UpdateMouseSnooping();
 554  }
 555  
 556  template<typename D>
 557  void SuperSonar<D>::OnMouseTimer()
 558  {
 559      auto now = GetTickCount64();
 560  
 561      // If mouse has moved, then reset the sonar timer.
 562      POINT ptCursor{};
 563      if (!GetCursorPos(&ptCursor))
 564      {
 565          // We are no longer the active desktop - done.
 566          StopSonar();
 567          return;
 568      }
 569      ScreenToClient(m_hwnd, &ptCursor);
 570  
 571      if (IsEqual(m_sonarPos, ptCursor))
 572      {
 573          // Mouse is stationary.
 574          if (m_sonarStart != SonarWaitingForMouseMove && now - m_sonarStart >= IdlePeriod)
 575          {
 576              StopSonar();
 577              return;
 578          }
 579      }
 580      else
 581      {
 582          // Mouse has moved.
 583          if (IsEqual(m_sonarPos, ptNowhere))
 584          {
 585              // Initial call, mark sonar as active but waiting for first mouse-move.
 586              now = SonarWaitingForMouseMove;
 587          }
 588          SetTimer(m_hwnd, TIMER_ID_TRACK, IdlePeriod, nullptr);
 589          Shim()->BeforeMoveSonar();
 590          m_sonarPos = ptCursor;
 591          m_sonarStart = now;
 592          Shim()->AfterMoveSonar();
 593      }
 594  }
 595  
 596  template<typename D>
 597  void SuperSonar<D>::UpdateMouseSnooping()
 598  {
 599      bool wantSnoopingMouse = m_sonarStart != NoSonar || m_sonarState != SonarState::Idle || m_activationMethod == FindMyMouseActivationMethod::ShakeMouse;
 600      if (m_isSnoopingMouse != wantSnoopingMouse)
 601      {
 602          m_isSnoopingMouse = wantSnoopingMouse;
 603          RAWINPUTDEVICE mouse{};
 604          mouse.usUsagePage = HID_USAGE_PAGE_GENERIC;
 605          mouse.usUsage = HID_USAGE_GENERIC_MOUSE;
 606          if (wantSnoopingMouse)
 607          {
 608              mouse.dwFlags = RIDEV_INPUTSINK;
 609              mouse.hwndTarget = m_hwnd;
 610          }
 611          else
 612          {
 613              mouse.dwFlags = RIDEV_REMOVE;
 614              mouse.hwndTarget = nullptr;
 615          }
 616          RegisterRawInputDevices(&mouse, 1, sizeof(mouse));
 617      }
 618  }
 619  
 620  template<typename D>
 621  bool SuperSonar<D>::IsForegroundAppExcluded()
 622  {
 623      if (m_excludedApps.size() < 1)
 624      {
 625          return false;
 626      }
 627      if (HWND foregroundApp{ GetForegroundWindow() })
 628      {
 629          auto processPath = get_process_path(foregroundApp);
 630          CharUpperBuffW(processPath.data(), static_cast<DWORD>(processPath.length()));
 631  
 632          return check_excluded_app(foregroundApp, processPath, m_excludedApps);
 633      }
 634      else
 635      {
 636          return false;
 637      }
 638  }
 639  
 640  struct CompositionSpotlight : SuperSonar<CompositionSpotlight>
 641  {
 642      static constexpr UINT WM_OPACITY_ANIMATION_COMPLETED = WM_APP;
 643      float m_sonarRadiusFloat = static_cast<float>(m_sonarRadius);
 644  
 645      DWORD GetExtendedStyle()
 646      {
 647          // Remove WS_EX_NOREDIRECTIONBITMAP for Composition/XAML to allow DWM redirection.
 648          return 0;
 649      }
 650  
 651      void AfterMoveSonar()
 652      {
 653          const float scale = static_cast<float>(m_surface.XamlRoot().RasterizationScale());
 654          // Move gradient center
 655          if (m_spotlightMaskGradient)
 656          {
 657              m_spotlightMaskGradient.EllipseCenter({ static_cast<float>(m_sonarPos.x) / scale,
 658                                                      static_cast<float>(m_sonarPos.y) / scale });
 659          }
 660          // Move spotlight visual (color fill) below masked backdrop
 661          if (m_spotlight)
 662          {
 663              m_spotlight.Offset({ static_cast<float>(m_sonarPos.x) / scale,
 664                                   static_cast<float>(m_sonarPos.y) / scale,
 665                                   0.0f });
 666          }
 667      }
 668  
 669      LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept
 670      {
 671          switch (message)
 672          {
 673          case WM_CREATE:
 674              if (!OnCompositionCreate())
 675                  return -1;
 676              return BaseWndProc(message, wParam, lParam);
 677  
 678          case WM_OPACITY_ANIMATION_COMPLETED:
 679              OnOpacityAnimationCompleted();
 680              break;
 681          case WM_SIZE:
 682              UpdateIslandSize();
 683              break;
 684          }
 685          return BaseWndProc(message, wParam, lParam);
 686      }
 687  
 688      void SetSonarVisibility(bool visible)
 689      {
 690          m_batch = m_compositor.GetCommitBatch(muxc::CompositionBatchTypes::Animation);
 691          BOOL isEnabledAnimations = GetAnimationsEnabled();
 692          m_animation.Duration(std::chrono::milliseconds{ isEnabledAnimations ? m_fadeDuration : 1 });
 693          m_batch.Completed([hwnd = m_hwnd](auto&&, auto&&) {
 694              PostMessage(hwnd, WM_OPACITY_ANIMATION_COMPLETED, 0, 0);
 695          });
 696          m_root.Opacity(visible ? 1.0f : 0.0f);
 697          if (visible)
 698          {
 699              ShowWindow(m_hwnd, SW_SHOWNOACTIVATE);
 700          }
 701      }
 702  
 703      HWND GetHwnd() noexcept
 704      {
 705          return m_hwnd;
 706      }
 707  
 708  private:
 709      bool OnCompositionCreate()
 710      try
 711      {
 712      // Creating composition resources
 713          // Ensure a DispatcherQueue bound to this thread (required by WinAppSDK composition/XAML)
 714          if (!m_dispatcherQueueController)
 715          {
 716              // Ensure COM is initialized
 717              try
 718              {
 719                  winrt::init_apartment(winrt::apartment_type::single_threaded);
 720                  // COM STA initialized
 721              }
 722              catch (const winrt::hresult_error& e)
 723              {
 724                  Logger::error("Failed to initialize COM apartment: {}", winrt::to_string(e.message()));
 725                  return false;
 726              }
 727  
 728              try
 729              {
 730                  m_dispatcherQueueController =
 731                      winrt::Microsoft::UI::Dispatching::DispatcherQueueController::CreateOnCurrentThread();
 732                  // DispatcherQueueController created
 733              }
 734              catch (const winrt::hresult_error& e)
 735              {
 736                  Logger::error("Failed to create DispatcherQueueController: {}", winrt::to_string(e.message()));
 737                  return false;
 738              }
 739          }
 740  
 741          // 1) Create a XAML island and attach it to this HWND
 742          try
 743          {
 744              m_island = winrt::Microsoft::UI::Xaml::Hosting::DesktopWindowXamlSource{};
 745              auto windowId = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd);
 746              m_island.Initialize(windowId);
 747              // Xaml source initialized
 748          }
 749          catch (const winrt::hresult_error& e)
 750          {
 751              Logger::error("Failed to create XAML island: {}", winrt::to_string(e.message()));
 752              return false;
 753          }
 754  
 755          UpdateIslandSize();
 756          // Island size set
 757  
 758          // 2) Create a XAML container to host the Composition child visual
 759          m_surface = winrt::Microsoft::UI::Xaml::Controls::Grid{};
 760  
 761          // A transparent background keeps hit-testing consistent vs. null brush
 762          m_surface.Background(winrt::Microsoft::UI::Xaml::Media::SolidColorBrush{
 763              winrt::Microsoft::UI::Colors::Transparent() });
 764          m_surface.HorizontalAlignment(muxx::HorizontalAlignment::Stretch);
 765          m_surface.VerticalAlignment(muxx::VerticalAlignment::Stretch);
 766  
 767          m_island.Content(m_surface);
 768  
 769          // 3) Get the compositor from the XAML visual tree (pure MUXC path)
 770          try
 771          {
 772              auto elementVisual =
 773                  winrt::Microsoft::UI::Xaml::Hosting::ElementCompositionPreview::GetElementVisual(m_surface);
 774              m_compositor = elementVisual.Compositor();
 775              // Compositor acquired
 776          }
 777          catch (const winrt::hresult_error& e)
 778          {
 779              Logger::error("Failed to get compositor: {}", winrt::to_string(e.message()));
 780              return false;
 781          }
 782  
 783          // 4) Build the composition tree
 784          //
 785          // [root] ContainerVisual (fills host)
 786          //  \ LayerVisual
 787          //     \ [backdrop dim * radial gradient mask (hole)]
 788          m_root = m_compositor.CreateContainerVisual();
 789          m_root.RelativeSizeAdjustment({ 1.0f, 1.0f });
 790          m_root.Opacity(0.0f);
 791  
 792          // Insert our root as a hand-in Visual under the XAML element
 793          winrt::Microsoft::UI::Xaml::Hosting::ElementCompositionPreview::SetElementChildVisual(m_surface, m_root);
 794  
 795          auto layer = m_compositor.CreateLayerVisual();
 796          layer.RelativeSizeAdjustment({ 1.0f, 1.0f });
 797          m_root.Children().InsertAtTop(layer);
 798  
 799          const float scale = static_cast<float>(m_surface.XamlRoot().RasterizationScale());
 800          const float rDip = m_sonarRadiusFloat / scale;
 801          const float zoom = static_cast<float>(m_sonarZoomFactor);
 802  
 803          // Spotlight shape (below backdrop, visible through hole)
 804          m_circleGeometry = m_compositor.CreateEllipseGeometry();
 805          m_circleShape = m_compositor.CreateSpriteShape(m_circleGeometry);
 806          m_circleShape.FillBrush(m_compositor.CreateColorBrush(m_spotlightColor));
 807          m_circleShape.Offset({ rDip * zoom, rDip * zoom });
 808          m_spotlight = m_compositor.CreateShapeVisual();
 809          m_spotlight.Size({ rDip * 2 * zoom, rDip * 2 * zoom });
 810          m_spotlight.AnchorPoint({ 0.5f, 0.5f });
 811          m_spotlight.Shapes().Append(m_circleShape);
 812          layer.Children().InsertAtTop(m_spotlight);
 813  
 814          // Dim color (source)
 815          m_dimColorBrush = m_compositor.CreateColorBrush(m_backgroundColor);
 816          // Radial gradient mask (center transparent, outer opaque)
 817          // Fixed feather width: 1px for radius < 300, 2px for radius >= 300
 818          const float featherPixels = (m_sonarRadius >= 300) ? 2.0f : 1.0f;
 819          const float featherOffset = 1.0f - featherPixels / (rDip * zoom);
 820          m_spotlightMaskGradient = m_compositor.CreateRadialGradientBrush();
 821          m_spotlightMaskGradient.MappingMode(muxc::CompositionMappingMode::Absolute);
 822          m_maskStopCenter = m_compositor.CreateColorGradientStop();
 823          m_maskStopCenter.Offset(0.0f);
 824          m_maskStopCenter.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
 825          m_maskStopInner = m_compositor.CreateColorGradientStop();
 826          m_maskStopInner.Offset(featherOffset);
 827          m_maskStopInner.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
 828          m_maskStopOuter = m_compositor.CreateColorGradientStop();
 829          m_maskStopOuter.Offset(1.0f);
 830          m_maskStopOuter.Color(winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255));
 831          m_spotlightMaskGradient.ColorStops().Append(m_maskStopCenter);
 832          m_spotlightMaskGradient.ColorStops().Append(m_maskStopInner);
 833          m_spotlightMaskGradient.ColorStops().Append(m_maskStopOuter);
 834          m_spotlightMaskGradient.EllipseCenter({ rDip * zoom, rDip * zoom });
 835          m_spotlightMaskGradient.EllipseRadius({ rDip * zoom, rDip * zoom });
 836  
 837          m_maskBrush = m_compositor.CreateMaskBrush();
 838          m_maskBrush.Source(m_dimColorBrush);
 839          m_maskBrush.Mask(m_spotlightMaskGradient);
 840  
 841          m_backdrop = m_compositor.CreateSpriteVisual();
 842          m_backdrop.RelativeSizeAdjustment({ 1.0f, 1.0f });
 843          m_backdrop.Brush(m_maskBrush);
 844          layer.Children().InsertAtTop(m_backdrop);
 845  
 846          // 5) Implicit opacity animation on the root
 847          m_animation = m_compositor.CreateScalarKeyFrameAnimation();
 848          m_animation.Target(L"Opacity");
 849          m_animation.InsertExpressionKeyFrame(1.0f, L"this.FinalValue");
 850          m_animation.Duration(std::chrono::milliseconds{ m_fadeDuration });
 851          auto collection = m_compositor.CreateImplicitAnimationCollection();
 852          collection.Insert(L"Opacity", m_animation);
 853          m_root.ImplicitAnimations(collection);
 854  
 855          // 6) Spotlight radius shrinks as opacity increases (expression animation)
 856          SetupRadiusAnimations(rDip * zoom, rDip, featherPixels);
 857  
 858          // Composition created successfully
 859          return true;
 860      }
 861      catch (const winrt::hresult_error& e)
 862      {
 863          Logger::error("Failed to create FindMyMouse visual: {}", winrt::to_string(e.message()));
 864          return false;
 865      }
 866  
 867      void OnOpacityAnimationCompleted()
 868      {
 869          if (m_root.Opacity() < 0.01f)
 870          {
 871              ShowWindow(m_hwnd, SW_HIDE);
 872          }
 873      }
 874  
 875      // Helper to setup radius and feather expression animations
 876      void SetupRadiusAnimations(float startRadiusDip, float endRadiusDip, float featherPixels)
 877      {
 878          // Radius expression: shrinks from startRadiusDip to endRadiusDip as opacity goes 0->1
 879          auto radiusExpression = m_compositor.CreateExpressionAnimation();
 880          radiusExpression.SetReferenceParameter(L"Root", m_root);
 881          wchar_t expressionText[256];
 882          winrt::check_hresult(StringCchPrintfW(
 883              expressionText, ARRAYSIZE(expressionText),
 884              L"Lerp(Vector2(%.1f, %.1f), Vector2(%.1f, %.1f), Root.Opacity)",
 885              startRadiusDip, startRadiusDip, endRadiusDip, endRadiusDip));
 886          radiusExpression.Expression(expressionText);
 887          m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", radiusExpression);
 888  
 889          // Feather expression: maintains fixed pixel width as radius changes
 890          auto featherExpression = m_compositor.CreateExpressionAnimation();
 891          featherExpression.SetReferenceParameter(L"Root", m_root);
 892          wchar_t featherExpressionText[256];
 893          winrt::check_hresult(StringCchPrintfW(
 894              featherExpressionText, ARRAYSIZE(featherExpressionText),
 895              L"1.0f - %.1ff / Lerp(%.1ff, %.1ff, Root.Opacity)",
 896              featherPixels, startRadiusDip, endRadiusDip));
 897          featherExpression.Expression(featherExpressionText);
 898          m_maskStopInner.StartAnimation(L"Offset", featherExpression);
 899  
 900          // Circle geometry radius for visual consistency
 901          if (m_circleGeometry)
 902          {
 903              auto radiusExpression2 = m_compositor.CreateExpressionAnimation();
 904              radiusExpression2.SetReferenceParameter(L"Root", m_root);
 905              radiusExpression2.Expression(expressionText);
 906              m_circleGeometry.StartAnimation(L"Radius", radiusExpression2);
 907          }
 908      }
 909  
 910      void UpdateIslandSize()
 911      {
 912          if (!m_island)
 913              return;
 914  
 915          RECT rc{};
 916          if (!GetClientRect(m_hwnd, &rc))
 917              return;
 918  
 919          const int width = rc.right - rc.left;
 920          const int height = rc.bottom - rc.top;
 921  
 922          auto bridge = m_island.SiteBridge();
 923          bridge.MoveAndResize(winrt::Windows::Graphics::RectInt32{ 0, 0, width, height });
 924      }
 925  
 926  public:
 927      void ApplySettings(const FindMyMouseSettings& settings, bool applyToRuntimeObjects)
 928      {
 929          if (!applyToRuntimeObjects)
 930          {
 931              m_sonarRadius = settings.spotlightRadius;
 932              m_sonarRadiusFloat = static_cast<float>(m_sonarRadius);
 933              m_backgroundColor = settings.backgroundColor;
 934              m_spotlightColor = settings.spotlightColor;
 935              m_activationMethod = settings.activationMethod;
 936              m_includeWinKey = settings.includeWinKey;
 937              m_doNotActivateOnGameMode = settings.doNotActivateOnGameMode;
 938              m_fadeDuration = settings.animationDurationMs > 0 ? settings.animationDurationMs : 1;
 939              m_sonarZoomFactor = settings.spotlightInitialZoom;
 940              m_excludedApps = settings.excludedApps;
 941              m_shakeMinimumDistance = settings.shakeMinimumDistance;
 942              m_shakeIntervalMs = settings.shakeIntervalMs;
 943              m_shakeFactor = settings.shakeFactor;
 944          }
 945          else
 946          {
 947              if (m_dispatcherQueueController == nullptr)
 948              {
 949                  Logger::warn("Tried accessing the dispatch queue controller before it was initialized.");
 950                  return;
 951              }
 952              auto dispatcherQueue = m_dispatcherQueueController.DispatcherQueue();
 953              FindMyMouseSettings localSettings = settings;
 954              bool enqueueSucceeded = dispatcherQueue.TryEnqueue([=]() {
 955                  if (!m_destroyed)
 956                  {
 957                      m_sonarRadius = localSettings.spotlightRadius;
 958                      m_sonarRadiusFloat = static_cast<float>(m_sonarRadius);
 959                      m_backgroundColor = localSettings.backgroundColor;
 960                      m_spotlightColor = localSettings.spotlightColor;
 961                      m_activationMethod = localSettings.activationMethod;
 962                      m_includeWinKey = localSettings.includeWinKey;
 963                      m_doNotActivateOnGameMode = localSettings.doNotActivateOnGameMode;
 964                      m_fadeDuration = localSettings.animationDurationMs > 0 ? localSettings.animationDurationMs : 1;
 965                      m_sonarZoomFactor = localSettings.spotlightInitialZoom;
 966                      m_excludedApps = localSettings.excludedApps;
 967                      m_shakeMinimumDistance = localSettings.shakeMinimumDistance;
 968                      m_shakeIntervalMs = localSettings.shakeIntervalMs;
 969                      m_shakeFactor = localSettings.shakeFactor;
 970                      UpdateMouseSnooping(); // For the shake mouse activation method
 971  
 972                      // Apply new settings to runtime composition objects.
 973                      if (m_dimColorBrush)
 974                      {
 975                          m_dimColorBrush.Color(m_backgroundColor);
 976                      }
 977                      if (m_circleShape)
 978                      {
 979                          if (auto brush = m_circleShape.FillBrush().try_as<muxc::CompositionColorBrush>())
 980                          {
 981                              brush.Color(m_spotlightColor);
 982                          }
 983                      }
 984                      const float scale = static_cast<float>(m_surface.XamlRoot().RasterizationScale());
 985                      const float rDip = m_sonarRadiusFloat / scale;
 986                      const float zoom = static_cast<float>(m_sonarZoomFactor);
 987                      const float featherPixels = (m_sonarRadius >= 300) ? 2.0f : 1.0f;
 988                      const float startRadiusDip = rDip * zoom;
 989                      m_spotlightMaskGradient.StopAnimation(L"EllipseRadius");
 990                      m_maskStopInner.StopAnimation(L"Offset");
 991                      if (m_circleGeometry)
 992                      {
 993                          m_circleGeometry.StopAnimation(L"Radius");
 994                      }
 995                      m_spotlightMaskGradient.EllipseCenter({ startRadiusDip, startRadiusDip });
 996                      if (m_spotlight)
 997                      {
 998                          m_spotlight.Size({ rDip * 2 * zoom, rDip * 2 * zoom });
 999                          m_circleShape.Offset({ startRadiusDip, startRadiusDip });
1000                      }
1001                      SetupRadiusAnimations(startRadiusDip, rDip, featherPixels);
1002                  }
1003              });
1004              if (!enqueueSucceeded)
1005              {
1006                  Logger::error("Couldn't enqueue message to update the sonar settings.");
1007              }
1008          }
1009      }
1010  
1011  private:
1012      muxc::Compositor m_compositor{ nullptr };
1013      muxxh::DesktopWindowXamlSource m_island{ nullptr };
1014      muxxc::Grid m_surface{ nullptr };
1015  
1016      muxc::ContainerVisual m_root{ nullptr };
1017      muxc::CompositionCommitBatch m_batch{ nullptr };
1018      muxc::SpriteVisual m_backdrop{ nullptr };
1019      // Spotlight shape visuals
1020      muxc::CompositionEllipseGeometry m_circleGeometry{ nullptr };
1021      muxc::ShapeVisual m_spotlight{ nullptr };
1022      muxc::CompositionSpriteShape m_circleShape{ nullptr };
1023      // Radial gradient mask components
1024      muxc::CompositionMaskBrush m_maskBrush{ nullptr };
1025      muxc::CompositionColorBrush m_dimColorBrush{ nullptr };
1026      muxc::CompositionRadialGradientBrush m_spotlightMaskGradient{ nullptr };
1027      muxc::CompositionColorGradientStop m_maskStopCenter{ nullptr };
1028      muxc::CompositionColorGradientStop m_maskStopInner{ nullptr };
1029      muxc::CompositionColorGradientStop m_maskStopOuter{ nullptr };
1030      winrt::Windows::UI::Color m_backgroundColor = FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR;
1031      winrt::Windows::UI::Color m_spotlightColor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR;
1032      muxc::ScalarKeyFrameAnimation m_animation{ nullptr };
1033  };
1034  
1035  #pragma endregion Super_Sonar_Base_Code
1036  
1037  #pragma region Super_Sonar_API
1038  
1039  CompositionSpotlight* m_sonar = nullptr;
1040  void FindMyMouseApplySettings(const FindMyMouseSettings& settings)
1041  {
1042      if (m_sonar != nullptr)
1043      {
1044          m_sonar->ApplySettings(settings, true);
1045      }
1046  }
1047  
1048  void FindMyMouseDisable()
1049  {
1050      if (m_sonar != nullptr)
1051      {
1052          m_sonar->Terminate();
1053      }
1054  }
1055  
1056  bool FindMyMouseIsEnabled()
1057  {
1058      return (m_sonar != nullptr);
1059  }
1060  
1061  // Based on SuperSonar's original wWinMain.
1062  int FindMyMouseMain(HINSTANCE hinst, const FindMyMouseSettings& settings)
1063  {
1064      if (m_sonar != nullptr)
1065      {
1066          Logger::error("A sonar instance was still working when trying to start a new one.");
1067          return 0;
1068      }
1069  
1070      CompositionSpotlight sonar;
1071      sonar.ApplySettings(settings, false);
1072      if (!sonar.Initialize(hinst))
1073      {
1074          Logger::error("Couldn't initialize a sonar instance.");
1075          return 0;
1076      }
1077      m_sonar = &sonar;
1078  
1079      InitializeWinhookEventIds();
1080  
1081      MSG msg;
1082  
1083      // Main message loop:
1084      while (GetMessage(&msg, nullptr, 0, 0))
1085      {
1086          TranslateMessage(&msg);
1087          DispatchMessage(&msg);
1088      }
1089  
1090      m_sonar = nullptr;
1091  
1092      return (int)msg.wParam;
1093  }
1094  
1095  HWND GetSonarHwnd() noexcept
1096  {
1097      if (m_sonar != nullptr)
1098      {
1099          return m_sonar->GetHwnd();
1100      }
1101  
1102      return nullptr;
1103  }
1104  
1105  #pragma endregion Super_Sonar_API