mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
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 <img width="1103" height="643" alt="image" src="https://github.com/user-attachments/assets/ff4f0835-a8ca-4603-9441-123b71747d5c" /> <!-- Please review the items on the PR checklist before submitting--> ## 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 <vanzue@outlook.com>
This commit is contained in:
@@ -84,14 +84,17 @@
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="CursorWrapCore.h" />
|
||||
<ClInclude Include="CursorWrapTests.h" />
|
||||
<ClInclude Include="MonitorTopology.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="CursorWrapCore.cpp" />
|
||||
<ClCompile Include="dllmain.cpp" />
|
||||
|
||||
<ClCompile Include="MonitorTopology.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
|
||||
268
src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp
Normal file
268
src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp
Normal file
@@ -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 <sstream>
|
||||
#include <iomanip>
|
||||
#include <ctime>
|
||||
|
||||
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<GetDpiForMonitorFunc>(GetProcAddress(shcore, "GetDpiForMonitor"));
|
||||
if (getDpi)
|
||||
{
|
||||
getDpi(hMon, 0, &dpiX, &dpiY); // MDT_EFFECTIVE_DPI = 0
|
||||
}
|
||||
FreeLibrary(shcore);
|
||||
}
|
||||
}
|
||||
|
||||
int scalingPercent = static_cast<int>((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<CursorWrapCore*>(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<int>(self->m_monitors.size());
|
||||
self->m_monitors.push_back(info);
|
||||
|
||||
Logger::info(L"Enumerated monitor {}: hMonitor={}, rect=({},{},{},{}), primary={}",
|
||||
info.monitorId, reinterpret_cast<uintptr_t>(hMonitor),
|
||||
mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom,
|
||||
info.isPrimary ? L"yes" : L"no");
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}, reinterpret_cast<LPARAM>(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>(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;
|
||||
}
|
||||
33
src/modules/MouseUtils/CursorWrap/CursorWrapCore.h
Normal file
33
src/modules/MouseUtils/CursorWrap/CursorWrapCore.h
Normal file
@@ -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 <windows.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#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<MonitorInfo>& GetMonitors() const { return m_monitors; }
|
||||
const MonitorTopology& GetTopology() const { return m_topology; }
|
||||
|
||||
private:
|
||||
#ifdef _DEBUG
|
||||
std::wstring GenerateTopologyJSON() const;
|
||||
#endif
|
||||
|
||||
std::vector<MonitorInfo> m_monitors;
|
||||
MonitorTopology m_topology;
|
||||
};
|
||||
546
src/modules/MouseUtils/CursorWrap/MonitorTopology.cpp
Normal file
546
src/modules/MouseUtils/CursorWrap/MonitorTopology.cpp
Normal file
@@ -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 <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
void MonitorTopology::Initialize(const std::vector<MonitorInfo>& 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<uintptr_t>(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<int>(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<uintptr_t>(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<uintptr_t>(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<EdgeType> 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<int64_t>(clamped) - static_cast<int64_t>(edge.start);
|
||||
int64_t denominator = static_cast<int64_t>(edge.end) - static_cast<int64_t>(edge.start);
|
||||
return static_cast<double>(numerator) / static_cast<double>(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<int64_t>(edge.end) - static_cast<int64_t>(edge.start);
|
||||
int64_t offset = static_cast<int64_t>(relativePosition * static_cast<double>(range));
|
||||
// Clamp result to int range before returning
|
||||
int64_t result = static_cast<int64_t>(edge.start) + offset;
|
||||
return static_cast<int>(result);
|
||||
}
|
||||
|
||||
std::vector<MonitorTopology::GapInfo> MonitorTopology::DetectMonitorGaps() const
|
||||
{
|
||||
std::vector<GapInfo> 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<int>(i);
|
||||
gap.monitor2Index = static_cast<int>(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<int>(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<uintptr_t>(m_monitors[i].hMonitor), reinterpret_cast<uintptr_t>(monitor));
|
||||
return static_cast<int>(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<uintptr_t>(monitor));
|
||||
}
|
||||
|
||||
return -1; // Not found
|
||||
}
|
||||
|
||||
106
src/modules/MouseUtils/CursorWrap/MonitorTopology.h
Normal file
106
src/modules/MouseUtils/CursorWrap/MonitorTopology.h
Normal file
@@ -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 <windows.h>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
// 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<MonitorInfo>& 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<MonitorEdge>& 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<GapInfo> DetectMonitorGaps() const;
|
||||
|
||||
private:
|
||||
std::vector<MonitorInfo> m_monitors;
|
||||
std::vector<MonitorEdge> 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<std::pair<int, EdgeType>, 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;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user