From d26d9f745ab1cd8472d5d94bd1f14a5d2bef6c6c Mon Sep 17 00:00:00 2001 From: Mike Hall Date: Tue, 27 Jan 2026 05:27:11 +0000 Subject: [PATCH] CursorWrap improvements (#44936) ## Summary of the Pull Request - Updated engine for better multi-monitor support. - Closing the laptop lid will now update the monitor topology - New settings/dropdown to support wrapping on horizontal, vertical, or both image ## PR Checklist - [x] Closes: #44820 - [x] Closes: #44864 - [x] Closes: #44952 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments Feedback for CursorWrap shows that users want the ability to constrain wrapping for horizontal only, vertical only, or both (default behavior). This PR adds a new dropdown to CursorWrap settings to enable a user to select the appropriate wrapping model. ## Validation Steps Performed Local build and running on Surface Laptop 7 Pro - will also validate on a multi-monitor setup. --------- Co-authored-by: vanzue --- .github/actions/spell-check/expect.txt | 57 +- .../MouseUtils/CursorWrap/CursorWrap.vcxproj | 5 +- .../MouseUtils/CursorWrap/CursorWrapCore.cpp | 268 +++++ .../MouseUtils/CursorWrap/CursorWrapCore.h | 33 + .../MouseUtils/CursorWrap/MonitorTopology.cpp | 546 +++++++++ .../MouseUtils/CursorWrap/MonitorTopology.h | 106 ++ src/modules/MouseUtils/CursorWrap/dllmain.cpp | 1044 ++++------------- .../CursorWrapProperties.cs | 4 + .../Settings.UI.Library/CursorWrapSettings.cs | 11 +- .../SettingsXAML/Views/MouseUtilsPage.xaml | 7 + .../Settings.UI/Strings/en-us/Resources.resw | 12 + .../ViewModels/MouseUtilsViewModel.cs | 32 + 12 files changed, 1274 insertions(+), 851 deletions(-) create mode 100644 src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp create mode 100644 src/modules/MouseUtils/CursorWrap/CursorWrapCore.h create mode 100644 src/modules/MouseUtils/CursorWrap/MonitorTopology.cpp create mode 100644 src/modules/MouseUtils/CursorWrap/MonitorTopology.h diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index b622870905..5af3d5d3b6 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -22,7 +22,6 @@ ADate ADDSTRING ADDUNDORECORD ADifferent -adjacents ADMINS adml admx @@ -219,10 +218,11 @@ CIELCh cim CImage cla -claude CLASSDC +classguid classmethod CLASSNOTAVAILABLE +claude CLEARTYPE clickable clickonce @@ -261,7 +261,6 @@ colorhistory colorhistorylimit COLORKEY colorref -Convs comctl comdlg comexp @@ -282,6 +281,7 @@ CONTEXTHELP CONTEXTMENUHANDLER contractversion CONTROLPARENT +Convs copiedcolorrepresentation coppied copyable @@ -348,12 +348,14 @@ datareader datatracker dataversion Dayof +dbcc DBID DBLCLKS DBLEPSILON DBPROP DBPROPIDSET DBPROPSET +DBT DCBA DCOM DComposition @@ -371,8 +373,7 @@ DEFAULTICON defaultlib DEFAULTONLY DEFAULTSIZE -DEFAULTTONEAREST -Defaulttonearest +defaulttonearest DEFAULTTONULL DEFAULTTOPRIMARY DEFERERASE @@ -394,14 +395,19 @@ DESKTOPVERTRES devblogs devdocs devenv +DEVICEINTERFACE +devicetype +DEVINTERFACE devmgmt DEVMODE DEVMODEW +DEVNODES devpal +DEVTYP dfx DIALOGEX -digicert diffs +digicert DINORMAL DISABLEASACTIONKEY DISABLENOSCROLL @@ -544,7 +550,6 @@ fdx FErase fesf FFFF -FInc Figma FILEEXPLORER fileexploreraddons @@ -565,6 +570,7 @@ FILESYSPATH Filetime FILEVERSION FILTERMODE +FInc findfast findmymouse FIXEDFILEINFO @@ -666,13 +672,14 @@ HCRYPTPROV hcursor hcwhite hdc +HDEVNOTIFY hdr hdrop hdwwiz Helpline helptext -HGFE hgdiobj +HGFE hglobal hhk HHmmssfff @@ -748,9 +755,9 @@ HWNDPARENT HWNDPREV hyjiacan IAI +icf ICONERROR ICONLOCATION -icf IDCANCEL IDD idk @@ -841,8 +848,8 @@ jeli jfif jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi jjw -JOBOBJECT jobject +JOBOBJECT jpe jpnime Jsons @@ -929,9 +936,9 @@ LOWORD lparam LPBITMAPINFOHEADER LPCFHOOKPROC +lpch LPCITEMIDLIST LPCLSID -lpch lpcmi LPCMINVOKECOMMANDINFO LPCREATESTRUCT @@ -947,6 +954,7 @@ LPMONITORINFO LPOSVERSIONINFOEXW LPQUERY lprc +LPrivate LPSAFEARRAY lpstr lpsz @@ -956,7 +964,6 @@ lptpm LPTR LPTSTR lpv -LPrivate LPW lpwcx lpwndpl @@ -1000,13 +1007,13 @@ mber MBM MBR Mbuttondown +mcp MDICHILD MDL mdtext mdtxt mdwn meme -mcp memicmp MENUITEMINFO MENUITEMINFOW @@ -1042,8 +1049,8 @@ mmi mmsys mobileredirect mockapi -modelcontextprotocol MODALFRAME +modelcontextprotocol MODESPRUNED MONITORENUMPROC MONITORINFO @@ -1087,9 +1094,9 @@ MSLLHOOKSTRUCT Mso msrc msstore +mstsc msvcp MT -mstsc MTND MULTIPLEUSE multizone @@ -1099,11 +1106,11 @@ muxxc muxxh MVPs mvvm -myorg -myrepo MVVMTK MWBEx MYICON +myorg +myrepo NAMECHANGE namespaceanddescendants nao @@ -1244,10 +1251,8 @@ opencode OPENFILENAME openrdp opensource -openxmlformats -ollama -onnx openurl +openxmlformats OPTIMIZEFORINVOKE ORPHANEDDIALOGTITLE ORSCANS @@ -1464,7 +1469,6 @@ rbhid Rbuttondown rclsid RCZOOMIT -remotedesktop rdp RDW READMODE @@ -1493,6 +1497,7 @@ remappings REMAPSUCCESSFUL REMAPUNSUCCESSFUL Remotable +remotedesktop remoteip Removelnk renamable @@ -1526,8 +1531,8 @@ RIGHTSCROLLBAR riid RKey RNumber -rop rollups +rop ROUNDSMALL ROWSETEXT rpcrt @@ -1766,8 +1771,7 @@ SVGIO svgz SVSI SWFO -SWP -Swp +swp SWPNOSIZE SWPNOZORDER SWRESTORE @@ -1786,8 +1790,7 @@ SYSKEY syskeydown SYSKEYUP SYSLIB -SYSMENU -Sysmenu +sysmenu systemai SYSTEMAPPS SYSTEMMODAL @@ -1891,9 +1894,9 @@ uitests UITo ULONGLONG Ultrawide -ums UMax UMin +ums uncompilable UNCPRIORITY UNDNAME diff --git a/src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj b/src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj index 59e2095ca7..254bac4678 100644 --- a/src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj +++ b/src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj @@ -84,14 +84,17 @@ + + + - + Create diff --git a/src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp b/src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp new file mode 100644 index 0000000000..bea59e6186 --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "pch.h" +#include "CursorWrapCore.h" +#include "../../../common/logger/logger.h" +#include +#include +#include + +CursorWrapCore::CursorWrapCore() +{ +} + +#ifdef _DEBUG +std::wstring CursorWrapCore::GenerateTopologyJSON() const +{ + std::wostringstream json; + + // Get current time + auto now = std::time(nullptr); + std::tm tm{}; + localtime_s(&tm, &now); + + wchar_t computerName[MAX_COMPUTERNAME_LENGTH + 1] = {0}; + DWORD size = MAX_COMPUTERNAME_LENGTH + 1; + GetComputerNameW(computerName, &size); + + wchar_t userName[256] = {0}; + size = 256; + GetUserNameW(userName, &size); + + json << L"{\n"; + json << L" \"captured_at\": \"" << std::put_time(&tm, L"%Y-%m-%dT%H:%M:%S%z") << L"\",\n"; + json << L" \"computer_name\": \"" << computerName << L"\",\n"; + json << L" \"user_name\": \"" << userName << L"\",\n"; + json << L" \"monitor_count\": " << m_monitors.size() << L",\n"; + json << L" \"monitors\": [\n"; + + for (size_t i = 0; i < m_monitors.size(); ++i) + { + const auto& monitor = m_monitors[i]; + + // Get DPI for this monitor + UINT dpiX = 96, dpiY = 96; + POINT center = { + (monitor.rect.left + monitor.rect.right) / 2, + (monitor.rect.top + monitor.rect.bottom) / 2 + }; + HMONITOR hMon = MonitorFromPoint(center, MONITOR_DEFAULTTONEAREST); + if (hMon) + { + // Try GetDpiForMonitor (requires linking Shcore.lib) + using GetDpiForMonitorFunc = HRESULT (WINAPI *)(HMONITOR, int, UINT*, UINT*); + HMODULE shcore = LoadLibraryW(L"Shcore.dll"); + if (shcore) + { + auto getDpi = reinterpret_cast(GetProcAddress(shcore, "GetDpiForMonitor")); + if (getDpi) + { + getDpi(hMon, 0, &dpiX, &dpiY); // MDT_EFFECTIVE_DPI = 0 + } + FreeLibrary(shcore); + } + } + + int scalingPercent = static_cast((dpiX / 96.0) * 100); + + json << L" {\n"; + json << L" \"left\": " << monitor.rect.left << L",\n"; + json << L" \"top\": " << monitor.rect.top << L",\n"; + json << L" \"right\": " << monitor.rect.right << L",\n"; + json << L" \"bottom\": " << monitor.rect.bottom << L",\n"; + json << L" \"width\": " << (monitor.rect.right - monitor.rect.left) << L",\n"; + json << L" \"height\": " << (monitor.rect.bottom - monitor.rect.top) << L",\n"; + json << L" \"dpi\": " << dpiX << L",\n"; + json << L" \"scaling_percent\": " << scalingPercent << L",\n"; + json << L" \"primary\": " << (monitor.isPrimary ? L"true" : L"false") << L",\n"; + json << L" \"monitor_id\": " << monitor.monitorId << L"\n"; + json << L" }"; + if (i < m_monitors.size() - 1) + { + json << L","; + } + json << L"\n"; + } + + json << L" ]\n"; + json << L"}"; + + return json.str(); +} +#endif + +void CursorWrapCore::UpdateMonitorInfo() +{ + size_t previousMonitorCount = m_monitors.size(); + Logger::info(L"======= UPDATE MONITOR INFO START ======="); + Logger::info(L"Previous monitor count: {}", previousMonitorCount); + + m_monitors.clear(); + + EnumDisplayMonitors(nullptr, nullptr, [](HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam) -> BOOL { + auto* self = reinterpret_cast(lParam); + + MONITORINFO mi{}; + mi.cbSize = sizeof(MONITORINFO); + if (GetMonitorInfo(hMonitor, &mi)) + { + MonitorInfo info{}; + info.hMonitor = hMonitor; // Store handle for direct comparison later + info.rect = mi.rcMonitor; + info.isPrimary = (mi.dwFlags & MONITORINFOF_PRIMARY) != 0; + info.monitorId = static_cast(self->m_monitors.size()); + self->m_monitors.push_back(info); + + Logger::info(L"Enumerated monitor {}: hMonitor={}, rect=({},{},{},{}), primary={}", + info.monitorId, reinterpret_cast(hMonitor), + mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom, + info.isPrimary ? L"yes" : L"no"); + } + + return TRUE; + }, reinterpret_cast(this)); + + if (previousMonitorCount != m_monitors.size()) + { + Logger::info(L"*** MONITOR CONFIGURATION CHANGED: {} -> {} monitors ***", + previousMonitorCount, m_monitors.size()); + } + + m_topology.Initialize(m_monitors); + + // Log monitor configuration summary + Logger::info(L"Monitor configuration updated: {} monitor(s)", m_monitors.size()); + for (size_t i = 0; i < m_monitors.size(); ++i) + { + const auto& m = m_monitors[i]; + int width = m.rect.right - m.rect.left; + int height = m.rect.bottom - m.rect.top; + Logger::info(L" Monitor {}: {}x{} at ({}, {}){}", + i, width, height, m.rect.left, m.rect.top, + m.isPrimary ? L" [PRIMARY]" : L""); + } + Logger::info(L" Detected {} outer edges for cursor wrapping", m_topology.GetOuterEdges().size()); + + // Detect and log monitor gaps + auto gaps = m_topology.DetectMonitorGaps(); + if (!gaps.empty()) + { + Logger::warn(L"Monitor configuration has coordinate gaps that may prevent wrapping:"); + for (const auto& gap : gaps) + { + Logger::warn(L" Gap between Monitor {} and Monitor {}: {}px horizontal gap, {}px vertical overlap", + gap.monitor1Index, gap.monitor2Index, gap.horizontalGap, gap.verticalOverlap); + } + Logger::warn(L" If monitors appear snapped in Display Settings but show gaps here:"); + Logger::warn(L" 1. Try dragging monitors apart and snapping them back together"); + Logger::warn(L" 2. Update your GPU drivers"); + } + + Logger::info(L"======= UPDATE MONITOR INFO END ======="); +} + +POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode) +{ + // Check if wrapping should be disabled during drag + if (disableWrapDuringDrag && (GetAsyncKeyState(VK_LBUTTON) & 0x8000)) + { +#ifdef _DEBUG + OutputDebugStringW(L"[CursorWrap] [DRAG] Left mouse button down - skipping wrap\n"); +#endif + return currentPos; + } + + // Convert int wrapMode to WrapMode enum + WrapMode mode = static_cast(wrapMode); + +#ifdef _DEBUG + { + std::wostringstream oss; + oss << L"[CursorWrap] [MOVE] Cursor at (" << currentPos.x << L", " << currentPos.y << L")"; + + // Get current monitor and identify which one + HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST); + RECT monitorRect; + if (m_topology.GetMonitorRect(currentMonitor, monitorRect)) + { + // Find monitor ID + int monitorId = -1; + for (const auto& monitor : m_monitors) + { + if (monitor.rect.left == monitorRect.left && + monitor.rect.top == monitorRect.top && + monitor.rect.right == monitorRect.right && + monitor.rect.bottom == monitorRect.bottom) + { + monitorId = monitor.monitorId; + break; + } + } + oss << L" on Monitor " << monitorId << L" [" << monitorRect.left << L".." << monitorRect.right + << L", " << monitorRect.top << L".." << monitorRect.bottom << L"]"; + } + else + { + oss << L" (beyond monitor bounds)"; + } + oss << L"\n"; + OutputDebugStringW(oss.str().c_str()); + } +#endif + + // Get current monitor + HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST); + + // Check if cursor is on an outer edge (filtered by wrap mode) + EdgeType edgeType; + if (!m_topology.IsOnOuterEdge(currentMonitor, currentPos, edgeType, mode)) + { +#ifdef _DEBUG + static bool lastWasNotOuter = false; + if (!lastWasNotOuter) + { + OutputDebugStringW(L"[CursorWrap] [MOVE] Not on outer edge - no wrapping\n"); + lastWasNotOuter = true; + } +#endif + return currentPos; // Not on an outer edge + } + +#ifdef _DEBUG + { + const wchar_t* edgeStr = L"Unknown"; + switch (edgeType) + { + case EdgeType::Left: edgeStr = L"Left"; break; + case EdgeType::Right: edgeStr = L"Right"; break; + case EdgeType::Top: edgeStr = L"Top"; break; + case EdgeType::Bottom: edgeStr = L"Bottom"; break; + } + std::wostringstream oss; + oss << L"[CursorWrap] [EDGE] Detected outer " << edgeStr << L" edge at (" << currentPos.x << L", " << currentPos.y << L")\n"; + OutputDebugStringW(oss.str().c_str()); + } +#endif + + // Calculate wrap destination + POINT newPos = m_topology.GetWrapDestination(currentMonitor, currentPos, edgeType); + +#ifdef _DEBUG + if (newPos.x != currentPos.x || newPos.y != currentPos.y) + { + std::wostringstream oss; + oss << L"[CursorWrap] [WRAP] Position change: (" << currentPos.x << L", " << currentPos.y + << L") -> (" << newPos.x << L", " << newPos.y << L")\n"; + oss << L"[CursorWrap] [WRAP] Delta: (" << (newPos.x - currentPos.x) << L", " << (newPos.y - currentPos.y) << L")\n"; + OutputDebugStringW(oss.str().c_str()); + } + else + { + OutputDebugStringW(L"[CursorWrap] [WRAP] No position change (same-monitor wrap?)\n"); + } +#endif + + return newPos; +} diff --git a/src/modules/MouseUtils/CursorWrap/CursorWrapCore.h b/src/modules/MouseUtils/CursorWrap/CursorWrapCore.h new file mode 100644 index 0000000000..6c19a26e39 --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/CursorWrapCore.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once +#include +#include +#include +#include "MonitorTopology.h" + +// Core cursor wrapping engine +class CursorWrapCore +{ +public: + CursorWrapCore(); + + void UpdateMonitorInfo(); + + // Handle mouse move with wrap mode filtering + // wrapMode: 0=Both, 1=VerticalOnly, 2=HorizontalOnly + POINT HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode); + + const std::vector& GetMonitors() const { return m_monitors; } + const MonitorTopology& GetTopology() const { return m_topology; } + +private: +#ifdef _DEBUG + std::wstring GenerateTopologyJSON() const; +#endif + + std::vector m_monitors; + MonitorTopology m_topology; +}; diff --git a/src/modules/MouseUtils/CursorWrap/MonitorTopology.cpp b/src/modules/MouseUtils/CursorWrap/MonitorTopology.cpp new file mode 100644 index 0000000000..8e613996c6 --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/MonitorTopology.cpp @@ -0,0 +1,546 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "pch.h" +#include "MonitorTopology.h" +#include "../../../common/logger/logger.h" +#include +#include + +void MonitorTopology::Initialize(const std::vector& monitors) +{ + Logger::info(L"======= TOPOLOGY INITIALIZATION START ======="); + Logger::info(L"Initializing edge-based topology for {} monitors", monitors.size()); + + m_monitors = monitors; + m_outerEdges.clear(); + m_edgeMap.clear(); + + if (monitors.empty()) + { + Logger::warn(L"No monitors provided to Initialize"); + return; + } + + // Log monitor details + for (size_t i = 0; i < monitors.size(); ++i) + { + const auto& m = monitors[i]; + Logger::info(L"Monitor {}: hMonitor={}, rect=({},{},{},{}), primary={}", + i, reinterpret_cast(m.hMonitor), + m.rect.left, m.rect.top, m.rect.right, m.rect.bottom, + m.isPrimary ? L"yes" : L"no"); + } + + BuildEdgeMap(); + IdentifyOuterEdges(); + + Logger::info(L"Found {} outer edges", m_outerEdges.size()); + for (const auto& edge : m_outerEdges) + { + const wchar_t* typeStr = L"Unknown"; + switch (edge.type) + { + case EdgeType::Left: typeStr = L"Left"; break; + case EdgeType::Right: typeStr = L"Right"; break; + case EdgeType::Top: typeStr = L"Top"; break; + case EdgeType::Bottom: typeStr = L"Bottom"; break; + } + Logger::info(L"Outer edge: Monitor {} {} at position {}, range [{}, {}]", + edge.monitorIndex, typeStr, edge.position, edge.start, edge.end); + } + Logger::info(L"======= TOPOLOGY INITIALIZATION COMPLETE ======="); +} + +void MonitorTopology::BuildEdgeMap() +{ + // Create edges for each monitor using monitor index (not HMONITOR) + // This is important because HMONITOR handles can change when monitors are + // added/removed dynamically, but indices remain stable within a single + // topology configuration + for (size_t idx = 0; idx < m_monitors.size(); ++idx) + { + const auto& monitor = m_monitors[idx]; + int monitorIndex = static_cast(idx); + + // Left edge + MonitorEdge leftEdge; + leftEdge.monitorIndex = monitorIndex; + leftEdge.type = EdgeType::Left; + leftEdge.position = monitor.rect.left; + leftEdge.start = monitor.rect.top; + leftEdge.end = monitor.rect.bottom; + leftEdge.isOuter = true; // Will be updated in IdentifyOuterEdges + m_edgeMap[{monitorIndex, EdgeType::Left}] = leftEdge; + + // Right edge + MonitorEdge rightEdge; + rightEdge.monitorIndex = monitorIndex; + rightEdge.type = EdgeType::Right; + rightEdge.position = monitor.rect.right - 1; + rightEdge.start = monitor.rect.top; + rightEdge.end = monitor.rect.bottom; + rightEdge.isOuter = true; + m_edgeMap[{monitorIndex, EdgeType::Right}] = rightEdge; + + // Top edge + MonitorEdge topEdge; + topEdge.monitorIndex = monitorIndex; + topEdge.type = EdgeType::Top; + topEdge.position = monitor.rect.top; + topEdge.start = monitor.rect.left; + topEdge.end = monitor.rect.right; + topEdge.isOuter = true; + m_edgeMap[{monitorIndex, EdgeType::Top}] = topEdge; + + // Bottom edge + MonitorEdge bottomEdge; + bottomEdge.monitorIndex = monitorIndex; + bottomEdge.type = EdgeType::Bottom; + bottomEdge.position = monitor.rect.bottom - 1; + bottomEdge.start = monitor.rect.left; + bottomEdge.end = monitor.rect.right; + bottomEdge.isOuter = true; + m_edgeMap[{monitorIndex, EdgeType::Bottom}] = bottomEdge; + } +} + +void MonitorTopology::IdentifyOuterEdges() +{ + const int tolerance = 50; + + // Check each edge against all other edges to find adjacent ones + for (auto& [key1, edge1] : m_edgeMap) + { + for (const auto& [key2, edge2] : m_edgeMap) + { + if (edge1.monitorIndex == edge2.monitorIndex) + { + continue; // Same monitor + } + + // Check if edges are adjacent + if (EdgesAreAdjacent(edge1, edge2, tolerance)) + { + edge1.isOuter = false; + break; // This edge has an adjacent monitor + } + } + + if (edge1.isOuter) + { + m_outerEdges.push_back(edge1); + } + } +} + +bool MonitorTopology::EdgesAreAdjacent(const MonitorEdge& edge1, const MonitorEdge& edge2, int tolerance) const +{ + // Edges must be opposite types to be adjacent + bool oppositeTypes = false; + + if ((edge1.type == EdgeType::Left && edge2.type == EdgeType::Right) || + (edge1.type == EdgeType::Right && edge2.type == EdgeType::Left) || + (edge1.type == EdgeType::Top && edge2.type == EdgeType::Bottom) || + (edge1.type == EdgeType::Bottom && edge2.type == EdgeType::Top)) + { + oppositeTypes = true; + } + + if (!oppositeTypes) + { + return false; + } + + // Check if positions are within tolerance + if (abs(edge1.position - edge2.position) > tolerance) + { + return false; + } + + // Check if perpendicular ranges overlap + int overlapStart = max(edge1.start, edge2.start); + int overlapEnd = min(edge1.end, edge2.end); + + return overlapEnd > overlapStart + tolerance; +} + +bool MonitorTopology::IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType, WrapMode wrapMode) const +{ + RECT monitorRect; + if (!GetMonitorRect(monitor, monitorRect)) + { + Logger::warn(L"IsOnOuterEdge: GetMonitorRect failed for monitor handle {}", reinterpret_cast(monitor)); + return false; + } + + // Get monitor index for edge map lookup + int monitorIndex = GetMonitorIndex(monitor); + if (monitorIndex < 0) + { + Logger::warn(L"IsOnOuterEdge: Monitor index not found for handle {} at cursor ({}, {})", + reinterpret_cast(monitor), cursorPos.x, cursorPos.y); + return false; // Monitor not found in our list + } + + // Check each edge type + const int edgeThreshold = 1; + + // At corners, multiple edges may match - collect all candidates and try each + // to find one with a valid wrap destination + std::vector candidateEdges; + + // Left edge - only if mode allows horizontal wrapping + if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::HorizontalOnly) && + cursorPos.x <= monitorRect.left + edgeThreshold) + { + auto it = m_edgeMap.find({monitorIndex, EdgeType::Left}); + if (it != m_edgeMap.end() && it->second.isOuter) + { + candidateEdges.push_back(EdgeType::Left); + } + } + + // Right edge - only if mode allows horizontal wrapping + if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::HorizontalOnly) && + cursorPos.x >= monitorRect.right - 1 - edgeThreshold) + { + auto it = m_edgeMap.find({monitorIndex, EdgeType::Right}); + if (it != m_edgeMap.end()) + { + if (it->second.isOuter) + { + candidateEdges.push_back(EdgeType::Right); + } + // Debug: Log why right edge isn't outer + else + { + Logger::trace(L"IsOnOuterEdge: Monitor {} right edge is NOT outer (inner edge)", monitorIndex); + } + } + } + + // Top edge - only if mode allows vertical wrapping + if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::VerticalOnly) && + cursorPos.y <= monitorRect.top + edgeThreshold) + { + auto it = m_edgeMap.find({monitorIndex, EdgeType::Top}); + if (it != m_edgeMap.end() && it->second.isOuter) + { + candidateEdges.push_back(EdgeType::Top); + } + } + + // Bottom edge - only if mode allows vertical wrapping + if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::VerticalOnly) && + cursorPos.y >= monitorRect.bottom - 1 - edgeThreshold) + { + auto it = m_edgeMap.find({monitorIndex, EdgeType::Bottom}); + if (it != m_edgeMap.end() && it->second.isOuter) + { + candidateEdges.push_back(EdgeType::Bottom); + } + } + + if (candidateEdges.empty()) + { + return false; + } + + // Try each candidate edge and return first with valid wrap destination + for (EdgeType candidate : candidateEdges) + { + MonitorEdge oppositeEdge = FindOppositeOuterEdge(candidate, + (candidate == EdgeType::Left || candidate == EdgeType::Right) ? cursorPos.y : cursorPos.x); + + if (oppositeEdge.monitorIndex >= 0) + { + outEdgeType = candidate; + return true; + } + } + + return false; +} + +POINT MonitorTopology::GetWrapDestination(HMONITOR fromMonitor, const POINT& cursorPos, EdgeType edgeType) const +{ + // Get monitor index for edge map lookup + int monitorIndex = GetMonitorIndex(fromMonitor); + if (monitorIndex < 0) + { + return cursorPos; // Monitor not found + } + + auto it = m_edgeMap.find({monitorIndex, edgeType}); + if (it == m_edgeMap.end()) + { + return cursorPos; // Edge not found + } + + const MonitorEdge& fromEdge = it->second; + + // Calculate relative position on current edge (0.0 to 1.0) + double relativePos = GetRelativePosition(fromEdge, + (edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x); + + // Find opposite outer edge + MonitorEdge oppositeEdge = FindOppositeOuterEdge(edgeType, + (edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x); + + if (oppositeEdge.monitorIndex < 0) + { + // No opposite edge found, wrap within same monitor + RECT monitorRect; + if (GetMonitorRect(fromMonitor, monitorRect)) + { + POINT result = cursorPos; + switch (edgeType) + { + case EdgeType::Left: + result.x = monitorRect.right - 2; + break; + case EdgeType::Right: + result.x = monitorRect.left + 1; + break; + case EdgeType::Top: + result.y = monitorRect.bottom - 2; + break; + case EdgeType::Bottom: + result.y = monitorRect.top + 1; + break; + } + return result; + } + return cursorPos; + } + + // Calculate target position on opposite edge + POINT result; + + if (edgeType == EdgeType::Left || edgeType == EdgeType::Right) + { + // Horizontal edge -> vertical movement + result.x = oppositeEdge.position; + result.y = GetAbsolutePosition(oppositeEdge, relativePos); + } + else + { + // Vertical edge -> horizontal movement + result.y = oppositeEdge.position; + result.x = GetAbsolutePosition(oppositeEdge, relativePos); + } + + return result; +} + +MonitorEdge MonitorTopology::FindOppositeOuterEdge(EdgeType fromEdge, int relativePosition) const +{ + EdgeType targetType; + bool findMax; // true = find max position, false = find min position + + switch (fromEdge) + { + case EdgeType::Left: + targetType = EdgeType::Right; + findMax = true; + break; + case EdgeType::Right: + targetType = EdgeType::Left; + findMax = false; + break; + case EdgeType::Top: + targetType = EdgeType::Bottom; + findMax = true; + break; + case EdgeType::Bottom: + targetType = EdgeType::Top; + findMax = false; + break; + default: + return { .monitorIndex = -1 }; // Invalid edge type + } + + MonitorEdge result = { .monitorIndex = -1 }; // -1 indicates not found + int extremePosition = findMax ? INT_MIN : INT_MAX; + + for (const auto& edge : m_outerEdges) + { + if (edge.type != targetType) + { + continue; + } + + // Check if this edge overlaps with the relative position + if (relativePosition >= edge.start && relativePosition <= edge.end) + { + if ((findMax && edge.position > extremePosition) || + (!findMax && edge.position < extremePosition)) + { + extremePosition = edge.position; + result = edge; + } + } + } + + return result; +} + +double MonitorTopology::GetRelativePosition(const MonitorEdge& edge, int coordinate) const +{ + if (edge.end == edge.start) + { + return 0.5; // Avoid division by zero + } + + int clamped = max(edge.start, min(coordinate, edge.end)); + // Use int64_t to avoid overflow warning C26451 + int64_t numerator = static_cast(clamped) - static_cast(edge.start); + int64_t denominator = static_cast(edge.end) - static_cast(edge.start); + return static_cast(numerator) / static_cast(denominator); +} + +int MonitorTopology::GetAbsolutePosition(const MonitorEdge& edge, double relativePosition) const +{ + // Use int64_t to prevent arithmetic overflow during subtraction and multiplication + int64_t range = static_cast(edge.end) - static_cast(edge.start); + int64_t offset = static_cast(relativePosition * static_cast(range)); + // Clamp result to int range before returning + int64_t result = static_cast(edge.start) + offset; + return static_cast(result); +} + +std::vector MonitorTopology::DetectMonitorGaps() const +{ + std::vector gaps; + const int gapThreshold = 50; // Same as ADJACENCY_TOLERANCE + + // Check each pair of monitors + for (size_t i = 0; i < m_monitors.size(); ++i) + { + for (size_t j = i + 1; j < m_monitors.size(); ++j) + { + const auto& m1 = m_monitors[i]; + const auto& m2 = m_monitors[j]; + + // Check vertical overlap + int vOverlapStart = max(m1.rect.top, m2.rect.top); + int vOverlapEnd = min(m1.rect.bottom, m2.rect.bottom); + int vOverlap = vOverlapEnd - vOverlapStart; + + if (vOverlap <= 0) + { + continue; // No vertical overlap, skip + } + + // Check horizontal gap + int hGap = min(abs(m1.rect.right - m2.rect.left), abs(m2.rect.right - m1.rect.left)); + + if (hGap > gapThreshold) + { + GapInfo gap; + gap.monitor1Index = static_cast(i); + gap.monitor2Index = static_cast(j); + gap.horizontalGap = hGap; + gap.verticalOverlap = vOverlap; + gaps.push_back(gap); + } + } + } + + return gaps; +} + +HMONITOR MonitorTopology::GetMonitorFromPoint(const POINT& pt) const +{ + return MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST); +} + +bool MonitorTopology::GetMonitorRect(HMONITOR monitor, RECT& rect) const +{ + // First try direct HMONITOR comparison + for (const auto& monitorInfo : m_monitors) + { + if (monitorInfo.hMonitor == monitor) + { + rect = monitorInfo.rect; + return true; + } + } + + // Fallback: If direct comparison fails, try matching by current monitor info + MONITORINFO mi{}; + mi.cbSize = sizeof(MONITORINFO); + if (GetMonitorInfo(monitor, &mi)) + { + for (const auto& monitorInfo : m_monitors) + { + if (monitorInfo.rect.left == mi.rcMonitor.left && + monitorInfo.rect.top == mi.rcMonitor.top && + monitorInfo.rect.right == mi.rcMonitor.right && + monitorInfo.rect.bottom == mi.rcMonitor.bottom) + { + rect = monitorInfo.rect; + return true; + } + } + } + + return false; +} + +HMONITOR MonitorTopology::GetMonitorFromRect(const RECT& rect) const +{ + return MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST); +} + +int MonitorTopology::GetMonitorIndex(HMONITOR monitor) const +{ + // First try direct HMONITOR comparison (fast and accurate) + for (size_t i = 0; i < m_monitors.size(); ++i) + { + if (m_monitors[i].hMonitor == monitor) + { + return static_cast(i); + } + } + + // Fallback: If direct comparison fails (e.g., handle changed after display reconfiguration), + // try matching by position. Get the monitor's current rect and find matching stored rect. + MONITORINFO mi{}; + mi.cbSize = sizeof(MONITORINFO); + if (GetMonitorInfo(monitor, &mi)) + { + for (size_t i = 0; i < m_monitors.size(); ++i) + { + // Match by rect bounds + if (m_monitors[i].rect.left == mi.rcMonitor.left && + m_monitors[i].rect.top == mi.rcMonitor.top && + m_monitors[i].rect.right == mi.rcMonitor.right && + m_monitors[i].rect.bottom == mi.rcMonitor.bottom) + { + Logger::trace(L"GetMonitorIndex: Found monitor {} via rect fallback (handle changed from {} to {})", + i, reinterpret_cast(m_monitors[i].hMonitor), reinterpret_cast(monitor)); + return static_cast(i); + } + } + + // Log all stored monitors vs the requested one for debugging + Logger::warn(L"GetMonitorIndex: No match found. Requested monitor rect=({},{},{},{})", + mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom); + for (size_t i = 0; i < m_monitors.size(); ++i) + { + Logger::warn(L" Stored monitor {}: rect=({},{},{},{})", + i, m_monitors[i].rect.left, m_monitors[i].rect.top, + m_monitors[i].rect.right, m_monitors[i].rect.bottom); + } + } + else + { + Logger::warn(L"GetMonitorIndex: GetMonitorInfo failed for handle {}", reinterpret_cast(monitor)); + } + + return -1; // Not found +} + diff --git a/src/modules/MouseUtils/CursorWrap/MonitorTopology.h b/src/modules/MouseUtils/CursorWrap/MonitorTopology.h new file mode 100644 index 0000000000..0dead8e351 --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/MonitorTopology.h @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once +#include +#include +#include + +// Monitor information structure +struct MonitorInfo +{ + HMONITOR hMonitor; // Direct handle for accurate lookup after display changes + RECT rect; + bool isPrimary; + int monitorId; +}; + +// Edge type enumeration +enum class EdgeType +{ + Left = 0, + Right = 1, + Top = 2, + Bottom = 3 +}; + +// Wrap mode enumeration (matches Settings UI dropdown) +enum class WrapMode +{ + Both = 0, // Wrap in both directions + VerticalOnly = 1, // Only wrap top/bottom + HorizontalOnly = 2 // Only wrap left/right +}; + +// Represents a single edge of a monitor +struct MonitorEdge +{ + int monitorIndex; // Index into m_monitors (stable across display changes) + EdgeType type; + int start; // For vertical edges: Y start; horizontal: X start + int end; // For vertical edges: Y end; horizontal: X end + int position; // For vertical edges: X coord; horizontal: Y coord + bool isOuter; // True if no adjacent monitor touches this edge +}; + +// Monitor topology helper - manages edge-based monitor layout +struct MonitorTopology +{ + void Initialize(const std::vector& monitors); + + // Check if cursor is on an outer edge of the given monitor + // wrapMode filters which edges are considered (Both, VerticalOnly, HorizontalOnly) + bool IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType, WrapMode wrapMode) const; + + // Get the wrap destination point for a cursor on an outer edge + POINT GetWrapDestination(HMONITOR fromMonitor, const POINT& cursorPos, EdgeType edgeType) const; + + // Get monitor at point (helper) + HMONITOR GetMonitorFromPoint(const POINT& pt) const; + + // Get monitor rectangle (helper) + bool GetMonitorRect(HMONITOR monitor, RECT& rect) const; + + // Get outer edges collection (for debugging) + const std::vector& GetOuterEdges() const { return m_outerEdges; } + + // Detect gaps between monitors that should be snapped together + struct GapInfo { + int monitor1Index; + int monitor2Index; + int horizontalGap; + int verticalOverlap; + }; + std::vector DetectMonitorGaps() const; + +private: + std::vector m_monitors; + std::vector m_outerEdges; + + // Map from (monitor index, edge type) to edge info + // Using monitor index instead of HMONITOR because HMONITOR handles can change + // when monitors are added/removed dynamically + std::map, MonitorEdge> m_edgeMap; + + // Helper to resolve HMONITOR to monitor index at runtime + int GetMonitorIndex(HMONITOR monitor) const; + + // Helper to get consistent HMONITOR from RECT + HMONITOR GetMonitorFromRect(const RECT& rect) const; + + void BuildEdgeMap(); + void IdentifyOuterEdges(); + + // Check if two edges are adjacent (within tolerance) + bool EdgesAreAdjacent(const MonitorEdge& edge1, const MonitorEdge& edge2, int tolerance = 50) const; + + // Find the opposite outer edge for wrapping + MonitorEdge FindOppositeOuterEdge(EdgeType fromEdge, int relativePosition) const; + + // Calculate relative position along an edge (0.0 to 1.0) + double GetRelativePosition(const MonitorEdge& edge, int coordinate) const; + + // Convert relative position to absolute coordinate on target edge + int GetAbsolutePosition(const MonitorEdge& edge, double relativePosition) const; +}; diff --git a/src/modules/MouseUtils/CursorWrap/dllmain.cpp b/src/modules/MouseUtils/CursorWrap/dllmain.cpp index ee026b7b12..08c39bab60 100644 --- a/src/modules/MouseUtils/CursorWrap/dllmain.cpp +++ b/src/modules/MouseUtils/CursorWrap/dllmain.cpp @@ -1,3 +1,7 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + #include "pch.h" #include "../../../interface/powertoy_module_interface.h" #include "../../../common/SettingsAPI/settings_objects.h" @@ -14,8 +18,10 @@ #include #include #include +#include +#include #include "resource.h" -#include "CursorWrapTests.h" +#include "CursorWrapCore.h" // Disable C26451 arithmetic overflow warning for this file since the operations are safe in this context #pragma warning(disable: 26451) @@ -47,6 +53,7 @@ namespace const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut"; const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate"; const wchar_t JSON_KEY_DISABLE_WRAP_DURING_DRAG[] = L"disable_wrap_during_drag"; + const wchar_t JSON_KEY_WRAP_MODE[] = L"wrap_mode"; } // The PowerToy name that will be shown in the settings. @@ -54,34 +61,10 @@ const static wchar_t* MODULE_NAME = L"CursorWrap"; // Add a description that will we shown in the module settings page. const static wchar_t* MODULE_DESC = L""; -// Mouse hook data structure -struct MonitorInfo -{ - RECT rect; - bool isPrimary; - int monitorId; // Add monitor ID for easier debugging -}; - -// Add structure for logical monitor grid position -struct LogicalPosition -{ - int row; - int col; - bool isValid; -}; - -// Add monitor topology helper -struct MonitorTopology -{ - std::vector> grid; // 3x3 grid of monitors - std::map monitorToPosition; - std::map, HMONITOR> positionToMonitor; - - void Initialize(const std::vector& monitors); - LogicalPosition GetPosition(HMONITOR monitor) const; - HMONITOR GetMonitorAt(int row, int col) const; - HMONITOR FindAdjacentMonitor(HMONITOR current, int deltaRow, int deltaCol) const; -}; +// Monitor device interface GUID for RegisterDeviceNotification +// {e6f07b5f-ee97-4a90-b076-33f57bf4eaa7} +static const GUID GUID_DEVINTERFACE_MONITOR = + { 0xe6f07b5f, 0xee97, 0x4a90, { 0xb0, 0x76, 0x33, 0xf5, 0x7b, 0xf4, 0xea, 0xa7 } }; // Forward declaration class CursorWrap; @@ -97,14 +80,14 @@ private: bool m_enabled = false; bool m_autoActivate = false; bool m_disableWrapDuringDrag = true; // Default to true to prevent wrap during drag + int m_wrapMode = 0; // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly // Mouse hook HHOOK m_mouseHook = nullptr; std::atomic m_hookActive{ false }; - // Monitor information - std::vector m_monitors; - MonitorTopology m_topology; + // Core wrapping engine (edge-based polygon model) + CursorWrapCore m_core; // Hotkey Hotkey m_activationHotkey{}; @@ -115,13 +98,19 @@ private: std::thread m_eventThread; std::atomic_bool m_listening{ false }; + // Display change notification + HWND m_messageWindow = nullptr; + HDEVNOTIFY m_deviceNotify = nullptr; + static constexpr UINT_PTR TIMER_UPDATE_MONITORS = 1; + static constexpr UINT DEBOUNCE_DELAY_MS = 500; + public: // Constructor CursorWrap() { LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::cursorWrapLoggerName); init_settings(); - UpdateMonitorInfo(); + m_core.UpdateMonitorInfo(); g_cursorWrapInstance = this; // Set global instance pointer }; @@ -218,6 +207,9 @@ public: MSG msg; PeekMessage(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE); + // Create message window for display change notifications + RegisterForDisplayChanges(); + StartMouseHook(); Logger::info("CursorWrap enabled - mouse hook started"); @@ -247,6 +239,9 @@ public: } } + // Cleanup display change notifications + UnregisterDisplayChanges(); + StopMouseHook(); Logger::info("CursorWrap event listener stopped"); }); @@ -318,7 +313,17 @@ public: return false; } - private: + // Called when display configuration changes - update monitor topology + void OnDisplayChange() + { +#ifdef _DEBUG + OutputDebugStringW(L"[CursorWrap] Display configuration changed, updating monitor topology\n"); +#endif + Logger::info("Display configuration changed, updating monitor topology"); + m_core.UpdateMonitorInfo(); + } + +private: void ToggleMouseHook() { // Toggle cursor wrapping. @@ -329,10 +334,6 @@ public: else { StartMouseHook(); -#ifdef _DEBUG - // Run comprehensive tests when hook is started in debug builds - RunComprehensiveTests(); -#endif } } @@ -399,6 +400,21 @@ public: { Logger::warn("Failed to initialize CursorWrap disable wrap during drag from settings. Will use default value (true)"); } + + try + { + // Parse wrap mode + auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES); + if (propertiesObject.HasKey(JSON_KEY_WRAP_MODE)) + { + auto wrapModeObject = propertiesObject.GetNamedObject(JSON_KEY_WRAP_MODE); + m_wrapMode = static_cast(wrapModeObject.GetNamedNumber(JSON_KEY_VALUE)); + } + } + catch (...) + { + Logger::warn("Failed to initialize CursorWrap wrap mode from settings. Will use default value (0=Both)"); + } } else { @@ -416,31 +432,6 @@ public: } } - void UpdateMonitorInfo() - { - m_monitors.clear(); - - EnumDisplayMonitors(nullptr, nullptr, [](HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam) -> BOOL { - auto* self = reinterpret_cast(lParam); - - MONITORINFO mi{}; - mi.cbSize = sizeof(MONITORINFO); - if (GetMonitorInfo(hMonitor, &mi)) - { - MonitorInfo info{}; - info.rect = mi.rcMonitor; - info.isPrimary = (mi.dwFlags & MONITORINFOF_PRIMARY) != 0; - info.monitorId = static_cast(self->m_monitors.size()); - self->m_monitors.push_back(info); - } - - return TRUE; - }, reinterpret_cast(this)); - - // Initialize monitor topology - m_topology.Initialize(m_monitors); - } - void StartMouseHook() { if (m_mouseHook || m_hookActive) @@ -449,7 +440,8 @@ public: return; } - UpdateMonitorInfo(); + // Refresh monitor info before starting hook + m_core.UpdateMonitorInfo(); m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, GetModuleHandle(nullptr), 0); if (m_mouseHook) @@ -481,6 +473,167 @@ public: } } + void RegisterForDisplayChanges() + { + if (m_messageWindow) + { + return; // Already registered + } + + // Create a hidden top-level window to receive broadcast messages + // NOTE: Message-only windows (HWND_MESSAGE parent) do NOT receive + // WM_DISPLAYCHANGE, WM_SETTINGCHANGE, or WM_DEVICECHANGE broadcasts. + // We must use a real (hidden) top-level window instead. + WNDCLASSEXW wc = { sizeof(WNDCLASSEXW) }; + wc.lpfnWndProc = MessageWindowProc; + wc.hInstance = GetModuleHandle(nullptr); + wc.lpszClassName = L"CursorWrapDisplayChangeWindow"; + + RegisterClassExW(&wc); + + // Create a hidden top-level window (not message-only) + // WS_EX_TOOLWINDOW prevents taskbar button, WS_POPUP with no size makes it invisible + m_messageWindow = CreateWindowExW( + WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, + L"CursorWrapDisplayChangeWindow", + nullptr, + WS_POPUP, // Minimal window style + 0, 0, 0, 0, // Zero size = invisible + nullptr, // No parent - top-level window to receive broadcasts + nullptr, + GetModuleHandle(nullptr), + nullptr); + + if (m_messageWindow) + { +#ifdef _DEBUG + OutputDebugStringW(L"[CursorWrap] Registered for display change notifications\n"); +#endif + Logger::info("Registered for display change notifications"); + + // Register for device notifications (monitor hardware add/remove) + DEV_BROADCAST_DEVICEINTERFACE filter = {}; + filter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE); + filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; + filter.dbcc_classguid = GUID_DEVINTERFACE_MONITOR; + + m_deviceNotify = RegisterDeviceNotificationW( + m_messageWindow, + &filter, + DEVICE_NOTIFY_WINDOW_HANDLE); + + if (m_deviceNotify) + { +#ifdef _DEBUG + OutputDebugStringW(L"[CursorWrap] Registered for device notifications (monitor hardware changes)\n"); +#endif + Logger::info("Registered for device notifications (monitor hardware changes)"); + } + else + { + DWORD error = GetLastError(); +#ifdef _DEBUG + std::wostringstream oss; + oss << L"[CursorWrap] Failed to register device notifications. Error: " << error << L"\n"; + OutputDebugStringW(oss.str().c_str()); +#endif + Logger::warn("Failed to register device notifications. Error: {}", error); + } + } + else + { + DWORD error = GetLastError(); + Logger::error(L"Failed to create message window for display changes, error: {}", error); + } + } + + void UnregisterDisplayChanges() + { + if (m_deviceNotify) + { +#ifdef _DEBUG + OutputDebugStringW(L"[CursorWrap] Unregistering device notifications...\n"); +#endif + UnregisterDeviceNotification(m_deviceNotify); + m_deviceNotify = nullptr; + Logger::info("Unregistered device notifications"); + } + + if (m_messageWindow) + { + KillTimer(m_messageWindow, TIMER_UPDATE_MONITORS); + DestroyWindow(m_messageWindow); + m_messageWindow = nullptr; + UnregisterClassW(L"CursorWrapDisplayChangeWindow", GetModuleHandle(nullptr)); +#ifdef _DEBUG + OutputDebugStringW(L"[CursorWrap] Unregistered display change notifications\n"); +#endif + Logger::info("Unregistered display change notifications"); + } + } + + static LRESULT CALLBACK MessageWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) + { + if (!g_cursorWrapInstance) + { + return DefWindowProcW(hwnd, msg, wParam, lParam); + } + + switch (msg) + { + case WM_DISPLAYCHANGE: +#ifdef _DEBUG + OutputDebugStringW(L"[CursorWrap] WM_DISPLAYCHANGE received - monitor resolution/DPI changed\n"); +#endif + Logger::info("WM_DISPLAYCHANGE received - resolution/DPI changed"); + // Debounce: Wait for multiple changes to settle + KillTimer(hwnd, TIMER_UPDATE_MONITORS); + SetTimer(hwnd, TIMER_UPDATE_MONITORS, DEBOUNCE_DELAY_MS, nullptr); + break; + + case WM_SETTINGCHANGE: + if (wParam == SPI_SETWORKAREA) + { +#ifdef _DEBUG + OutputDebugStringW(L"[CursorWrap] WM_SETTINGCHANGE (SPI_SETWORKAREA) received - taskbar changed\n"); +#endif + Logger::info("WM_SETTINGCHANGE (SPI_SETWORKAREA) received"); + // Taskbar position/size changed + KillTimer(hwnd, TIMER_UPDATE_MONITORS); + SetTimer(hwnd, TIMER_UPDATE_MONITORS, DEBOUNCE_DELAY_MS, nullptr); + } + break; + + case WM_DEVICECHANGE: + // Handle monitor hardware add/remove + if (wParam == DBT_DEVNODES_CHANGED) + { +#ifdef _DEBUG + OutputDebugStringW(L"[CursorWrap] DBT_DEVNODES_CHANGED received - monitor hardware change detected\n"); +#endif + Logger::info("DBT_DEVNODES_CHANGED received - monitor hardware change detected"); + // Debounce: Wait for multiple changes to settle + KillTimer(hwnd, TIMER_UPDATE_MONITORS); + SetTimer(hwnd, TIMER_UPDATE_MONITORS, DEBOUNCE_DELAY_MS, nullptr); + return TRUE; + } + break; + + case WM_TIMER: + if (wParam == TIMER_UPDATE_MONITORS) + { +#ifdef _DEBUG + OutputDebugStringW(L"[CursorWrap] Debounce timer expired - triggering topology update\n"); +#endif + KillTimer(hwnd, TIMER_UPDATE_MONITORS); + g_cursorWrapInstance->OnDisplayChange(); + } + break; + } + + return DefWindowProcW(hwnd, msg, wParam, lParam); + } + static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode >= 0 && wParam == WM_MOUSEMOVE) @@ -490,7 +643,11 @@ public: if (g_cursorWrapInstance && g_cursorWrapInstance->m_hookActive) { - POINT newPos = g_cursorWrapInstance->HandleMouseMove(currentPos); + POINT newPos = g_cursorWrapInstance->m_core.HandleMouseMove( + currentPos, + g_cursorWrapInstance->m_disableWrapDuringDrag, + g_cursorWrapInstance->m_wrapMode); + if (newPos.x != currentPos.x || newPos.y != currentPos.y) { #ifdef _DEBUG @@ -505,765 +662,8 @@ public: return CallNextHookEx(nullptr, nCode, wParam, lParam); } - - // Helper method to check if there's a monitor adjacent in coordinate space (not grid) - bool HasAdjacentMonitorInCoordinateSpace(const RECT& currentMonitorRect, int direction) - { - // direction: 0=left, 1=right, 2=top, 3=bottom - const int tolerance = 50; // Allow small gaps - - for (const auto& monitor : m_monitors) - { - bool isAdjacent = false; - - switch (direction) - { - case 0: // Left - check if another monitor's right edge touches/overlaps our left edge - isAdjacent = (abs(monitor.rect.right - currentMonitorRect.left) <= tolerance) && - (monitor.rect.bottom > currentMonitorRect.top + tolerance) && - (monitor.rect.top < currentMonitorRect.bottom - tolerance); - break; - - case 1: // Right - check if another monitor's left edge touches/overlaps our right edge - isAdjacent = (abs(monitor.rect.left - currentMonitorRect.right) <= tolerance) && - (monitor.rect.bottom > currentMonitorRect.top + tolerance) && - (monitor.rect.top < currentMonitorRect.bottom - tolerance); - break; - - case 2: // Top - check if another monitor's bottom edge touches/overlaps our top edge - isAdjacent = (abs(monitor.rect.bottom - currentMonitorRect.top) <= tolerance) && - (monitor.rect.right > currentMonitorRect.left + tolerance) && - (monitor.rect.left < currentMonitorRect.right - tolerance); - break; - - case 3: // Bottom - check if another monitor's top edge touches/overlaps our bottom edge - isAdjacent = (abs(monitor.rect.top - currentMonitorRect.bottom) <= tolerance) && - (monitor.rect.right > currentMonitorRect.left + tolerance) && - (monitor.rect.left < currentMonitorRect.right - tolerance); - break; - } - - if (isAdjacent) - { -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: Found adjacent monitor in coordinate space (direction {})", direction); -#endif - return true; - } - } - - return false; - } - - // *** COMPLETELY REWRITTEN CURSOR WRAPPING LOGIC *** - // Implements vertical scrolling to bottom/top of vertical stack as requested - // Only wraps when there's NO adjacent monitor in the coordinate space - POINT HandleMouseMove(const POINT& currentPos) - { - POINT newPos = currentPos; - - // Check if we should skip wrapping during drag if the setting is enabled - if (m_disableWrapDuringDrag && (GetAsyncKeyState(VK_LBUTTON) & 0x8000)) - { -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: Left mouse button is down and disable_wrap_during_drag is enabled - skipping wrap"); -#endif - return currentPos; // Return unchanged position (no wrapping) - } - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: ======= HANDLE MOUSE MOVE START ======="); - Logger::info(L"CursorWrap DEBUG: Input position ({}, {})", currentPos.x, currentPos.y); -#endif - - // Find which monitor the cursor is currently on - HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST); - MONITORINFO currentMonitorInfo{}; - currentMonitorInfo.cbSize = sizeof(MONITORINFO); - GetMonitorInfo(currentMonitor, ¤tMonitorInfo); - - LogicalPosition currentLogicalPos = m_topology.GetPosition(currentMonitor); - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: Current monitor bounds: Left={}, Top={}, Right={}, Bottom={}", - currentMonitorInfo.rcMonitor.left, currentMonitorInfo.rcMonitor.top, - currentMonitorInfo.rcMonitor.right, currentMonitorInfo.rcMonitor.bottom); - Logger::info(L"CursorWrap DEBUG: Logical position: Row={}, Col={}, Valid={}", - currentLogicalPos.row, currentLogicalPos.col, currentLogicalPos.isValid); -#endif - - bool wrapped = false; - - // *** VERTICAL WRAPPING LOGIC - CONFIRMED WORKING *** - // Move to bottom of vertical stack when hitting top edge - // Only wrap if there's NO adjacent monitor in the coordinate space - if (currentPos.y <= currentMonitorInfo.rcMonitor.top) - { -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: ======= VERTICAL WRAP: TOP EDGE DETECTED ======="); -#endif - - // Check if there's an adjacent monitor above in coordinate space - if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 2)) - { -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists above (Windows will handle)"); -#endif - return currentPos; // Let Windows handle natural cursor movement - } - - // Find the bottom-most monitor in the vertical stack (same column) - HMONITOR bottomMonitor = nullptr; - - if (currentLogicalPos.isValid) { - // Search down from current position to find the bottom-most monitor in same column - for (int row = 2; row >= 0; row--) { // Start from bottom and work up - HMONITOR candidateMonitor = m_topology.GetMonitorAt(row, currentLogicalPos.col); - if (candidateMonitor) { - bottomMonitor = candidateMonitor; - break; // Found the bottom-most monitor - } - } - } - - if (bottomMonitor && bottomMonitor != currentMonitor) { - // *** MOVE TO BOTTOM OF VERTICAL STACK *** - MONITORINFO bottomInfo{}; - bottomInfo.cbSize = sizeof(MONITORINFO); - GetMonitorInfo(bottomMonitor, &bottomInfo); - - // Calculate relative X position to maintain cursor X alignment - double relativeX = static_cast(currentPos.x - currentMonitorInfo.rcMonitor.left) / - (currentMonitorInfo.rcMonitor.right - currentMonitorInfo.rcMonitor.left); - - int targetWidth = bottomInfo.rcMonitor.right - bottomInfo.rcMonitor.left; - newPos.x = bottomInfo.rcMonitor.left + static_cast(relativeX * targetWidth); - newPos.y = bottomInfo.rcMonitor.bottom - 1; // Bottom edge of bottom monitor - - // Clamp X to target monitor bounds - newPos.x = max(bottomInfo.rcMonitor.left, min(newPos.x, bottomInfo.rcMonitor.right - 1)); - wrapped = true; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: VERTICAL WRAP SUCCESS - Moved to bottom of vertical stack"); - Logger::info(L"CursorWrap DEBUG: New position: ({}, {})", newPos.x, newPos.y); -#endif - } else { - // *** NO OTHER MONITOR IN VERTICAL STACK - WRAP WITHIN CURRENT MONITOR *** - newPos.y = currentMonitorInfo.rcMonitor.bottom - 1; - wrapped = true; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: VERTICAL WRAP - No other monitor in stack, wrapping within current monitor"); -#endif - } - } - else if (currentPos.y >= currentMonitorInfo.rcMonitor.bottom - 1) - { -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: ======= VERTICAL WRAP: BOTTOM EDGE DETECTED ======="); -#endif - - // Check if there's an adjacent monitor below in coordinate space - if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 3)) - { -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists below (Windows will handle)"); -#endif - return currentPos; // Let Windows handle natural cursor movement - } - - // Find the top-most monitor in the vertical stack (same column) - HMONITOR topMonitor = nullptr; - - if (currentLogicalPos.isValid) { - // Search up from current position to find the top-most monitor in same column - for (int row = 0; row <= 2; row++) { // Start from top and work down - HMONITOR candidateMonitor = m_topology.GetMonitorAt(row, currentLogicalPos.col); - if (candidateMonitor) { - topMonitor = candidateMonitor; - break; // Found the top-most monitor - } - } - } - - if (topMonitor && topMonitor != currentMonitor) { - // *** MOVE TO TOP OF VERTICAL STACK *** - MONITORINFO topInfo{}; - topInfo.cbSize = sizeof(MONITORINFO); - GetMonitorInfo(topMonitor, &topInfo); - - // Calculate relative X position to maintain cursor X alignment - double relativeX = static_cast(currentPos.x - currentMonitorInfo.rcMonitor.left) / - (currentMonitorInfo.rcMonitor.right - currentMonitorInfo.rcMonitor.left); - - int targetWidth = topInfo.rcMonitor.right - topInfo.rcMonitor.left; - newPos.x = topInfo.rcMonitor.left + static_cast(relativeX * targetWidth); - newPos.y = topInfo.rcMonitor.top; // Top edge of top monitor - - // Clamp X to target monitor bounds - newPos.x = max(topInfo.rcMonitor.left, min(newPos.x, topInfo.rcMonitor.right - 1)); - wrapped = true; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: VERTICAL WRAP SUCCESS - Moved to top of vertical stack"); - Logger::info(L"CursorWrap DEBUG: New position: ({}, {})", newPos.x, newPos.y); -#endif - } else { - // *** NO OTHER MONITOR IN VERTICAL STACK - WRAP WITHIN CURRENT MONITOR *** - newPos.y = currentMonitorInfo.rcMonitor.top; - wrapped = true; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: VERTICAL WRAP - No other monitor in stack, wrapping within current monitor"); -#endif - } - } - - // *** FIXED HORIZONTAL WRAPPING LOGIC *** - // Move to opposite end of horizontal stack when hitting left/right edge - // Only wrap if there's NO adjacent monitor in the coordinate space (let Windows handle natural transitions) - if (!wrapped && currentPos.x <= currentMonitorInfo.rcMonitor.left) - { -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: ======= HORIZONTAL WRAP: LEFT EDGE DETECTED ======="); -#endif - - // Check if there's an adjacent monitor to the left in coordinate space - if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 0)) - { -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists to the left (Windows will handle)"); -#endif - return currentPos; // Let Windows handle natural cursor movement - } - - // Find the right-most monitor in the horizontal stack (same row) - HMONITOR rightMonitor = nullptr; - - if (currentLogicalPos.isValid) { - // Search right from current position to find the right-most monitor in same row - for (int col = 2; col >= 0; col--) { // Start from right and work left - HMONITOR candidateMonitor = m_topology.GetMonitorAt(currentLogicalPos.row, col); - if (candidateMonitor) { - rightMonitor = candidateMonitor; - break; // Found the right-most monitor - } - } - } - - if (rightMonitor && rightMonitor != currentMonitor) { - // *** MOVE TO RIGHT END OF HORIZONTAL STACK *** - MONITORINFO rightInfo{}; - rightInfo.cbSize = sizeof(MONITORINFO); - GetMonitorInfo(rightMonitor, &rightInfo); - - // Calculate relative Y position to maintain cursor Y alignment - double relativeY = static_cast(currentPos.y - currentMonitorInfo.rcMonitor.top) / - (currentMonitorInfo.rcMonitor.bottom - currentMonitorInfo.rcMonitor.top); - - int targetHeight = rightInfo.rcMonitor.bottom - rightInfo.rcMonitor.top; - newPos.y = rightInfo.rcMonitor.top + static_cast(relativeY * targetHeight); - newPos.x = rightInfo.rcMonitor.right - 1; // Right edge of right monitor - - // Clamp Y to target monitor bounds - newPos.y = max(rightInfo.rcMonitor.top, min(newPos.y, rightInfo.rcMonitor.bottom - 1)); - wrapped = true; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: HORIZONTAL WRAP SUCCESS - Moved to right end of horizontal stack"); - Logger::info(L"CursorWrap DEBUG: New position: ({}, {})", newPos.x, newPos.y); -#endif - } else { - // *** NO OTHER MONITOR IN HORIZONTAL STACK - WRAP WITHIN CURRENT MONITOR *** - newPos.x = currentMonitorInfo.rcMonitor.right - 1; - wrapped = true; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: HORIZONTAL WRAP - No other monitor in stack, wrapping within current monitor"); -#endif - } - } - else if (!wrapped && currentPos.x >= currentMonitorInfo.rcMonitor.right - 1) - { -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: ======= HORIZONTAL WRAP: RIGHT EDGE DETECTED ======="); -#endif - - // Check if there's an adjacent monitor to the right in coordinate space - if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 1)) - { -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists to the right (Windows will handle)"); -#endif - return currentPos; // Let Windows handle natural cursor movement - } - - // Find the left-most monitor in the horizontal stack (same row) - HMONITOR leftMonitor = nullptr; - - if (currentLogicalPos.isValid) { - // Search left from current position to find the left-most monitor in same row - for (int col = 0; col <= 2; col++) { // Start from left and work right - HMONITOR candidateMonitor = m_topology.GetMonitorAt(currentLogicalPos.row, col); - if (candidateMonitor) { - leftMonitor = candidateMonitor; - break; // Found the left-most monitor - } - } - } - - if (leftMonitor && leftMonitor != currentMonitor) { - // *** MOVE TO LEFT END OF HORIZONTAL STACK *** - MONITORINFO leftInfo{}; - leftInfo.cbSize = sizeof(MONITORINFO); - GetMonitorInfo(leftMonitor, &leftInfo); - - // Calculate relative Y position to maintain cursor Y alignment - double relativeY = static_cast(currentPos.y - currentMonitorInfo.rcMonitor.top) / - (currentMonitorInfo.rcMonitor.bottom - currentMonitorInfo.rcMonitor.top); - - int targetHeight = leftInfo.rcMonitor.bottom - leftInfo.rcMonitor.top; - newPos.y = leftInfo.rcMonitor.top + static_cast(relativeY * targetHeight); - newPos.x = leftInfo.rcMonitor.left; // Left edge of left monitor - - // Clamp Y to target monitor bounds - newPos.y = max(leftInfo.rcMonitor.top, min(newPos.y, leftInfo.rcMonitor.bottom - 1)); - wrapped = true; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: HORIZONTAL WRAP SUCCESS - Moved to left end of horizontal stack"); - Logger::info(L"CursorWrap DEBUG: New position: ({}, {})", newPos.x, newPos.y); -#endif - } else { - // *** NO OTHER MONITOR IN HORIZONTAL STACK - WRAP WITHIN CURRENT MONITOR *** - newPos.x = currentMonitorInfo.rcMonitor.left; - wrapped = true; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: HORIZONTAL WRAP - No other monitor in stack, wrapping within current monitor"); -#endif - } - } - -#ifdef _DEBUG - if (wrapped) - { - Logger::info(L"CursorWrap DEBUG: ======= WRAP RESULT ======="); - Logger::info(L"CursorWrap DEBUG: Original: ({}, {}) -> New: ({}, {})", - currentPos.x, currentPos.y, newPos.x, newPos.y); - } - else - { - Logger::info(L"CursorWrap DEBUG: No wrapping performed - cursor not at edge"); - } - Logger::info(L"CursorWrap DEBUG: ======= HANDLE MOUSE MOVE END ======="); -#endif - - return newPos; - } - - // Add test method for monitor topology validation - void RunMonitorTopologyTests() - { -#ifdef _DEBUG - Logger::info(L"CursorWrap: Running monitor topology tests..."); - - // Test all 9 possible monitor positions in 3x3 grid - const char* gridNames[3][3] = { - {"TL", "TC", "TR"}, // Top-Left, Top-Center, Top-Right - {"ML", "MC", "MR"}, // Middle-Left, Middle-Center, Middle-Right - {"BL", "BC", "BR"} // Bottom-Left, Bottom-Center, Bottom-Right - }; - - for (int row = 0; row < 3; row++) - { - for (int col = 0; col < 3; col++) - { - HMONITOR monitor = m_topology.GetMonitorAt(row, col); - if (monitor) - { - std::string gridName(gridNames[row][col]); - std::wstring wGridName(gridName.begin(), gridName.end()); - Logger::info(L"CursorWrap TEST: Monitor at [{}][{}] ({}) exists", - row, col, wGridName.c_str()); - - // Test adjacent monitor finding - HMONITOR up = m_topology.FindAdjacentMonitor(monitor, -1, 0); - HMONITOR down = m_topology.FindAdjacentMonitor(monitor, 1, 0); - HMONITOR left = m_topology.FindAdjacentMonitor(monitor, 0, -1); - HMONITOR right = m_topology.FindAdjacentMonitor(monitor, 0, 1); - - Logger::info(L"CursorWrap TEST: Adjacent monitors - Up: {}, Down: {}, Left: {}, Right: {}", - up ? L"YES" : L"NO", down ? L"YES" : L"NO", - left ? L"YES" : L"NO", right ? L"YES" : L"NO"); - } - } - } - - Logger::info(L"CursorWrap: Monitor topology tests completed."); -#endif - } - - // Add method to trigger test suite (can be called via hotkey in debug builds) - void RunComprehensiveTests() - { -#ifdef _DEBUG - RunMonitorTopologyTests(); - - // Test cursor wrapping scenarios - Logger::info(L"CursorWrap: Testing cursor wrapping scenarios..."); - - // Simulate cursor positions at each monitor edge and verify expected behavior - for (const auto& monitor : m_monitors) - { - HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST); - LogicalPosition pos = m_topology.GetPosition(hMonitor); - - if (pos.isValid) - { - Logger::info(L"CursorWrap TEST: Testing monitor at position [{}][{}]", pos.row, pos.col); - - // Test top edge - POINT topEdge = {(monitor.rect.left + monitor.rect.right) / 2, monitor.rect.top}; - POINT newPos = HandleMouseMove(topEdge); - Logger::info(L"CursorWrap TEST: Top edge ({}, {}) -> ({}, {})", - topEdge.x, topEdge.y, newPos.x, newPos.y); - - // Test bottom edge - POINT bottomEdge = {(monitor.rect.left + monitor.rect.right) / 2, monitor.rect.bottom - 1}; - newPos = HandleMouseMove(bottomEdge); - Logger::info(L"CursorWrap TEST: Bottom edge ({}, {}) -> ({}, {})", - bottomEdge.x, bottomEdge.y, newPos.x, newPos.y); - - // Test left edge - POINT leftEdge = {monitor.rect.left, (monitor.rect.top + monitor.rect.bottom) / 2}; - newPos = HandleMouseMove(leftEdge); - Logger::info(L"CursorWrap TEST: Left edge ({}, {}) -> ({}, {})", - leftEdge.x, leftEdge.y, newPos.x, newPos.y); - - // Test right edge - POINT rightEdge = {monitor.rect.right - 1, (monitor.rect.top + monitor.rect.bottom) / 2}; - newPos = HandleMouseMove(rightEdge); - Logger::info(L"CursorWrap TEST: Right edge ({}, {}) -> ({}, {})", - rightEdge.x, rightEdge.y, newPos.x, newPos.y); - } - } - - Logger::info(L"CursorWrap: Comprehensive tests completed."); -#endif - } }; -// Implementation of MonitorTopology methods -void MonitorTopology::Initialize(const std::vector& monitors) -{ - // Clear existing data - grid.assign(3, std::vector(3, nullptr)); - monitorToPosition.clear(); - positionToMonitor.clear(); - - if (monitors.empty()) return; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: ======= TOPOLOGY INITIALIZATION START ======="); - Logger::info(L"CursorWrap DEBUG: Initializing topology for {} monitors", monitors.size()); - for (const auto& monitor : monitors) - { - Logger::info(L"CursorWrap DEBUG: Monitor {}: bounds=({},{},{},{}), isPrimary={}", - monitor.monitorId, monitor.rect.left, monitor.rect.top, - monitor.rect.right, monitor.rect.bottom, monitor.isPrimary); - } -#endif - - // Special handling for 2 monitors - use physical position, not discovery order - if (monitors.size() == 2) - { - // Determine if arrangement is horizontal or vertical by comparing centers - POINT center0 = {(monitors[0].rect.left + monitors[0].rect.right) / 2, - (monitors[0].rect.top + monitors[0].rect.bottom) / 2}; - POINT center1 = {(monitors[1].rect.left + monitors[1].rect.right) / 2, - (monitors[1].rect.top + monitors[1].rect.bottom) / 2}; - - int xDiff = abs(center0.x - center1.x); - int yDiff = abs(center0.y - center1.y); - - bool isHorizontal = xDiff > yDiff; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: Monitor centers: M0=({}, {}), M1=({}, {})", - center0.x, center0.y, center1.x, center1.y); - Logger::info(L"CursorWrap DEBUG: Differences: X={}, Y={}, IsHorizontal={}", - xDiff, yDiff, isHorizontal); -#endif - - if (isHorizontal) - { - // Horizontal arrangement - place in middle row [1,0] and [1,2] - for (const auto& monitor : monitors) - { - HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST); - POINT center = {(monitor.rect.left + monitor.rect.right) / 2, - (monitor.rect.top + monitor.rect.bottom) / 2}; - - int row = 1; // Middle row - int col = (center.x < (center0.x + center1.x) / 2) ? 0 : 2; // Left or right based on center - - grid[row][col] = hMonitor; - monitorToPosition[hMonitor] = {row, col, true}; - positionToMonitor[{row, col}] = hMonitor; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: Monitor {} (horizontal) placed at grid[{}][{}]", - monitor.monitorId, row, col); -#endif - } - } - else - { - // *** VERTICAL ARRANGEMENT - CRITICAL LOGIC *** - // Sort monitors by Y coordinate to determine vertical order - std::vector> sortedMonitors; - for (int i = 0; i < 2; i++) { - sortedMonitors.push_back({i, monitors[i]}); - } - - // Sort by Y coordinate (top to bottom) - std::sort(sortedMonitors.begin(), sortedMonitors.end(), - [](const std::pair& a, const std::pair& b) { - int centerA = (a.second.rect.top + a.second.rect.bottom) / 2; - int centerB = (b.second.rect.top + b.second.rect.bottom) / 2; - return centerA < centerB; // Top first - }); - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: VERTICAL ARRANGEMENT DETECTED"); - Logger::info(L"CursorWrap DEBUG: Top monitor: ID={}, Y-center={}", - sortedMonitors[0].second.monitorId, - (sortedMonitors[0].second.rect.top + sortedMonitors[0].second.rect.bottom) / 2); - Logger::info(L"CursorWrap DEBUG: Bottom monitor: ID={}, Y-center={}", - sortedMonitors[1].second.monitorId, - (sortedMonitors[1].second.rect.top + sortedMonitors[1].second.rect.bottom) / 2); -#endif - - // Place monitors in grid based on sorted order - for (int i = 0; i < 2; i++) { - const auto& monitorPair = sortedMonitors[i]; - const auto& monitor = monitorPair.second; - HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST); - - int col = 1; // Middle column for vertical arrangement - int row = (i == 0) ? 0 : 2; // Top monitor at row 0, bottom at row 2 - - grid[row][col] = hMonitor; - monitorToPosition[hMonitor] = {row, col, true}; - positionToMonitor[{row, col}] = hMonitor; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: Monitor {} (vertical) placed at grid[{}][{}] - {} position", - monitor.monitorId, row, col, (i == 0) ? L"TOP" : L"BOTTOM"); -#endif - } - } - } - else - { - // For more than 2 monitors, use edge-based alignment algorithm - // This ensures monitors with aligned edges (e.g., top edges at same Y) are grouped in same row - - // Helper lambda to check if two ranges overlap or are adjacent (with tolerance) - auto rangesOverlapOrTouch = [](int start1, int end1, int start2, int end2, int tolerance = 50) -> bool { - // Check if ranges overlap or are within tolerance distance - return (start1 <= end2 + tolerance) && (start2 <= end1 + tolerance); - }; - - // Sort monitors by horizontal position (left edge) for column assignment - std::vector monitorsByX; - for (const auto& monitor : monitors) { - monitorsByX.push_back(&monitor); - } - std::sort(monitorsByX.begin(), monitorsByX.end(), [](const MonitorInfo* a, const MonitorInfo* b) { - return a->rect.left < b->rect.left; - }); - - // Sort monitors by vertical position (top edge) for row assignment - std::vector monitorsByY; - for (const auto& monitor : monitors) { - monitorsByY.push_back(&monitor); - } - std::sort(monitorsByY.begin(), monitorsByY.end(), [](const MonitorInfo* a, const MonitorInfo* b) { - return a->rect.top < b->rect.top; - }); - - // Assign rows based on vertical overlap - monitors that overlap vertically should be in same row - std::map monitorToRow; - int currentRow = 0; - - for (size_t i = 0; i < monitorsByY.size(); i++) { - const auto* monitor = monitorsByY[i]; - - // Check if this monitor overlaps vertically with any monitor already assigned to current row - bool foundOverlap = false; - for (size_t j = 0; j < i; j++) { - const auto* other = monitorsByY[j]; - if (monitorToRow[other] == currentRow) { - // Check vertical overlap - if (rangesOverlapOrTouch(monitor->rect.top, monitor->rect.bottom, - other->rect.top, other->rect.bottom)) { - monitorToRow[monitor] = currentRow; - foundOverlap = true; - break; - } - } - } - - if (!foundOverlap) { - // Start new row if no overlap found and we have room - if (currentRow < 2 && i < monitorsByY.size() - 1) { - currentRow++; - } - monitorToRow[monitor] = currentRow; - } - } - - // Assign columns based on horizontal position (left-to-right order) - // Monitors are already sorted by X coordinate (left edge) - std::map monitorToCol; - - // For horizontal arrangement, distribute monitors evenly across columns - if (monitorsByX.size() == 1) { - // Single monitor - place in middle column - monitorToCol[monitorsByX[0]] = 1; - } - else if (monitorsByX.size() == 2) { - // Two monitors - place at opposite ends for wrapping - monitorToCol[monitorsByX[0]] = 0; // Leftmost monitor - monitorToCol[monitorsByX[1]] = 2; // Rightmost monitor - } - else { - // Three or more monitors - distribute across grid - for (size_t i = 0; i < monitorsByX.size() && i < 3; i++) { - monitorToCol[monitorsByX[i]] = static_cast(i); - } - // If more than 3 monitors, place extras in rightmost column - for (size_t i = 3; i < monitorsByX.size(); i++) { - monitorToCol[monitorsByX[i]] = 2; - } - } - - // Place monitors in grid using the computed row/column assignments - for (const auto& monitor : monitors) - { - HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST); - int row = monitorToRow[&monitor]; - int col = monitorToCol[&monitor]; - - grid[row][col] = hMonitor; - monitorToPosition[hMonitor] = {row, col, true}; - positionToMonitor[{row, col}] = hMonitor; - -#ifdef _DEBUG - Logger::info(L"CursorWrap DEBUG: Monitor {} placed at grid[{}][{}] (left={}, top={}, right={}, bottom={})", - monitor.monitorId, row, col, - monitor.rect.left, monitor.rect.top, monitor.rect.right, monitor.rect.bottom); -#endif - } - } - -#ifdef _DEBUG - // *** CRITICAL: Print topology map using OutputDebugString for debug builds *** - Logger::info(L"CursorWrap DEBUG: ======= FINAL TOPOLOGY MAP ======="); - OutputDebugStringA("CursorWrap TOPOLOGY MAP:\n"); - for (int r = 0; r < 3; r++) - { - std::string rowStr = " "; - for (int c = 0; c < 3; c++) - { - if (grid[r][c]) - { - // Find monitor ID for this handle - int monitorId = -1; - for (const auto& monitor : monitors) - { - HMONITOR handle = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST); - if (handle == grid[r][c]) - { - monitorId = monitor.monitorId + 1; // Convert to 1-based for display - break; - } - } - rowStr += std::to_string(monitorId) + " "; - } - else - { - rowStr += ". "; - } - } - rowStr += "\n"; - OutputDebugStringA(rowStr.c_str()); - - // Also log to PowerToys logger - std::wstring wRowStr(rowStr.begin(), rowStr.end()); - Logger::info(wRowStr.c_str()); - } - OutputDebugStringA("======= END TOPOLOGY MAP =======\n"); - - // Additional validation logging - Logger::info(L"CursorWrap DEBUG: ======= GRID POSITION VALIDATION ======="); - for (const auto& monitor : monitors) - { - HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST); - LogicalPosition pos = GetPosition(hMonitor); - if (pos.isValid) - { - Logger::info(L"CursorWrap DEBUG: Monitor {} -> grid[{}][{}]", monitor.monitorId, pos.row, pos.col); - OutputDebugStringA(("Monitor " + std::to_string(monitor.monitorId) + " -> grid[" + std::to_string(pos.row) + "][" + std::to_string(pos.col) + "]\n").c_str()); - - // Test adjacent finding - HMONITOR up = FindAdjacentMonitor(hMonitor, -1, 0); - HMONITOR down = FindAdjacentMonitor(hMonitor, 1, 0); - HMONITOR left = FindAdjacentMonitor(hMonitor, 0, -1); - HMONITOR right = FindAdjacentMonitor(hMonitor, 0, 1); - - Logger::info(L"CursorWrap DEBUG: Monitor {} adjacents - Up: {}, Down: {}, Left: {}, Right: {}", - monitor.monitorId, up ? L"YES" : L"NO", down ? L"YES" : L"NO", - left ? L"YES" : L"NO", right ? L"YES" : L"NO"); - } - } - Logger::info(L"CursorWrap DEBUG: ======= TOPOLOGY INITIALIZATION COMPLETE ======="); -#endif -} - -LogicalPosition MonitorTopology::GetPosition(HMONITOR monitor) const -{ - auto it = monitorToPosition.find(monitor); - if (it != monitorToPosition.end()) - { - return it->second; - } - return {-1, -1, false}; -} - -HMONITOR MonitorTopology::GetMonitorAt(int row, int col) const -{ - if (row >= 0 && row < 3 && col >= 0 && col < 3) - { - return grid[row][col]; - } - return nullptr; -} - -HMONITOR MonitorTopology::FindAdjacentMonitor(HMONITOR current, int deltaRow, int deltaCol) const -{ - LogicalPosition currentPos = GetPosition(current); - if (!currentPos.isValid) return nullptr; - - int newRow = currentPos.row + deltaRow; - int newCol = currentPos.col + deltaCol; - - return GetMonitorAt(newRow, newCol); -} - extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() { return new CursorWrap(); diff --git a/src/settings-ui/Settings.UI.Library/CursorWrapProperties.cs b/src/settings-ui/Settings.UI.Library/CursorWrapProperties.cs index cf66b4ba09..bffa75a3f3 100644 --- a/src/settings-ui/Settings.UI.Library/CursorWrapProperties.cs +++ b/src/settings-ui/Settings.UI.Library/CursorWrapProperties.cs @@ -22,11 +22,15 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("disable_wrap_during_drag")] public BoolProperty DisableWrapDuringDrag { get; set; } + [JsonPropertyName("wrap_mode")] + public IntProperty WrapMode { get; set; } + public CursorWrapProperties() { ActivationShortcut = DefaultActivationShortcut; AutoActivate = new BoolProperty(false); DisableWrapDuringDrag = new BoolProperty(true); + WrapMode = new IntProperty(0); // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly } } } diff --git a/src/settings-ui/Settings.UI.Library/CursorWrapSettings.cs b/src/settings-ui/Settings.UI.Library/CursorWrapSettings.cs index 8c9059123c..0ee6c4a523 100644 --- a/src/settings-ui/Settings.UI.Library/CursorWrapSettings.cs +++ b/src/settings-ui/Settings.UI.Library/CursorWrapSettings.cs @@ -47,7 +47,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library // This can be utilized in the future if the settings.json file is to be modified/deleted. public bool UpgradeSettingsConfiguration() { - return false; + bool settingsUpgraded = false; + + // Add WrapMode property if it doesn't exist (for users upgrading from older versions) + if (Properties.WrapMode == null) + { + Properties.WrapMode = new IntProperty(0); // Default to Both + settingsUpgraded = true; + } + + return settingsUpgraded; } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml index 567c4246eb..39c3800f93 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml @@ -47,6 +47,13 @@ + + + + + + + diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index de1bf99919..72eac551ff 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -2728,6 +2728,18 @@ From there, simply click on one of the supported files in the File Explorer and Automatically activate on utility startup + + Wrap mode + + + Vertical only + + + Horizontal only + + + Vertical and horizontal + Mouse Pointer Crosshairs Mouse as in the hardware peripheral. diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs index 0c3eb06649..11045e0108 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs @@ -113,6 +113,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels // Null-safe access in case property wasn't upgraded yet - default to TRUE _cursorWrapDisableWrapDuringDrag = CursorWrapSettingsConfig.Properties.DisableWrapDuringDrag?.Value ?? true; + // Null-safe access in case property wasn't upgraded yet - default to 0 (Both) + _cursorWrapWrapMode = CursorWrapSettingsConfig.Properties.WrapMode?.Value ?? 0; + int isEnabled = 0; Utilities.NativeMethods.SystemParametersInfo(Utilities.NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0); @@ -1083,6 +1086,34 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public int CursorWrapWrapMode + { + get + { + return _cursorWrapWrapMode; + } + + set + { + if (value != _cursorWrapWrapMode) + { + _cursorWrapWrapMode = value; + + // Ensure the property exists before setting value + if (CursorWrapSettingsConfig.Properties.WrapMode == null) + { + CursorWrapSettingsConfig.Properties.WrapMode = new IntProperty(value); + } + else + { + CursorWrapSettingsConfig.Properties.WrapMode.Value = value; + } + + NotifyCursorWrapPropertyChanged(); + } + } + } + public void NotifyCursorWrapPropertyChanged([CallerMemberName] string propertyName = null) { OnPropertyChanged(propertyName); @@ -1154,5 +1185,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private bool _isCursorWrapEnabled; private bool _cursorWrapAutoActivate; private bool _cursorWrapDisableWrapDuringDrag; // Will be initialized in constructor from settings + private int _cursorWrapWrapMode; // 0=Both, 1=VerticalOnly, 2=HorizontalOnly } }