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