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