MonitorTopology.cpp
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 #include "pch.h" 6 #include "MonitorTopology.h" 7 #include "../../../common/logger/logger.h" 8 #include <algorithm> 9 #include <cmath> 10 11 void MonitorTopology::Initialize(const std::vector<MonitorInfo>& monitors) 12 { 13 Logger::info(L"======= TOPOLOGY INITIALIZATION START ======="); 14 Logger::info(L"Initializing edge-based topology for {} monitors", monitors.size()); 15 16 m_monitors = monitors; 17 m_outerEdges.clear(); 18 m_edgeMap.clear(); 19 20 if (monitors.empty()) 21 { 22 Logger::warn(L"No monitors provided to Initialize"); 23 return; 24 } 25 26 // Log monitor details 27 for (size_t i = 0; i < monitors.size(); ++i) 28 { 29 const auto& m = monitors[i]; 30 Logger::info(L"Monitor {}: hMonitor={}, rect=({},{},{},{}), primary={}", 31 i, reinterpret_cast<uintptr_t>(m.hMonitor), 32 m.rect.left, m.rect.top, m.rect.right, m.rect.bottom, 33 m.isPrimary ? L"yes" : L"no"); 34 } 35 36 BuildEdgeMap(); 37 IdentifyOuterEdges(); 38 39 Logger::info(L"Found {} outer edges", m_outerEdges.size()); 40 for (const auto& edge : m_outerEdges) 41 { 42 const wchar_t* typeStr = L"Unknown"; 43 switch (edge.type) 44 { 45 case EdgeType::Left: typeStr = L"Left"; break; 46 case EdgeType::Right: typeStr = L"Right"; break; 47 case EdgeType::Top: typeStr = L"Top"; break; 48 case EdgeType::Bottom: typeStr = L"Bottom"; break; 49 } 50 Logger::info(L"Outer edge: Monitor {} {} at position {}, range [{}, {}]", 51 edge.monitorIndex, typeStr, edge.position, edge.start, edge.end); 52 } 53 Logger::info(L"======= TOPOLOGY INITIALIZATION COMPLETE ======="); 54 } 55 56 void MonitorTopology::BuildEdgeMap() 57 { 58 // Create edges for each monitor using monitor index (not HMONITOR) 59 // This is important because HMONITOR handles can change when monitors are 60 // added/removed dynamically, but indices remain stable within a single 61 // topology configuration 62 for (size_t idx = 0; idx < m_monitors.size(); ++idx) 63 { 64 const auto& monitor = m_monitors[idx]; 65 int monitorIndex = static_cast<int>(idx); 66 67 // Left edge 68 MonitorEdge leftEdge; 69 leftEdge.monitorIndex = monitorIndex; 70 leftEdge.type = EdgeType::Left; 71 leftEdge.position = monitor.rect.left; 72 leftEdge.start = monitor.rect.top; 73 leftEdge.end = monitor.rect.bottom; 74 leftEdge.isOuter = true; // Will be updated in IdentifyOuterEdges 75 m_edgeMap[{monitorIndex, EdgeType::Left}] = leftEdge; 76 77 // Right edge 78 MonitorEdge rightEdge; 79 rightEdge.monitorIndex = monitorIndex; 80 rightEdge.type = EdgeType::Right; 81 rightEdge.position = monitor.rect.right - 1; 82 rightEdge.start = monitor.rect.top; 83 rightEdge.end = monitor.rect.bottom; 84 rightEdge.isOuter = true; 85 m_edgeMap[{monitorIndex, EdgeType::Right}] = rightEdge; 86 87 // Top edge 88 MonitorEdge topEdge; 89 topEdge.monitorIndex = monitorIndex; 90 topEdge.type = EdgeType::Top; 91 topEdge.position = monitor.rect.top; 92 topEdge.start = monitor.rect.left; 93 topEdge.end = monitor.rect.right; 94 topEdge.isOuter = true; 95 m_edgeMap[{monitorIndex, EdgeType::Top}] = topEdge; 96 97 // Bottom edge 98 MonitorEdge bottomEdge; 99 bottomEdge.monitorIndex = monitorIndex; 100 bottomEdge.type = EdgeType::Bottom; 101 bottomEdge.position = monitor.rect.bottom - 1; 102 bottomEdge.start = monitor.rect.left; 103 bottomEdge.end = monitor.rect.right; 104 bottomEdge.isOuter = true; 105 m_edgeMap[{monitorIndex, EdgeType::Bottom}] = bottomEdge; 106 } 107 } 108 109 void MonitorTopology::IdentifyOuterEdges() 110 { 111 const int tolerance = 50; 112 113 // Check each edge against all other edges to find adjacent ones 114 for (auto& [key1, edge1] : m_edgeMap) 115 { 116 for (const auto& [key2, edge2] : m_edgeMap) 117 { 118 if (edge1.monitorIndex == edge2.monitorIndex) 119 { 120 continue; // Same monitor 121 } 122 123 // Check if edges are adjacent 124 if (EdgesAreAdjacent(edge1, edge2, tolerance)) 125 { 126 edge1.isOuter = false; 127 break; // This edge has an adjacent monitor 128 } 129 } 130 131 if (edge1.isOuter) 132 { 133 m_outerEdges.push_back(edge1); 134 } 135 } 136 } 137 138 bool MonitorTopology::EdgesAreAdjacent(const MonitorEdge& edge1, const MonitorEdge& edge2, int tolerance) const 139 { 140 // Edges must be opposite types to be adjacent 141 bool oppositeTypes = false; 142 143 if ((edge1.type == EdgeType::Left && edge2.type == EdgeType::Right) || 144 (edge1.type == EdgeType::Right && edge2.type == EdgeType::Left) || 145 (edge1.type == EdgeType::Top && edge2.type == EdgeType::Bottom) || 146 (edge1.type == EdgeType::Bottom && edge2.type == EdgeType::Top)) 147 { 148 oppositeTypes = true; 149 } 150 151 if (!oppositeTypes) 152 { 153 return false; 154 } 155 156 // Check if positions are within tolerance 157 if (abs(edge1.position - edge2.position) > tolerance) 158 { 159 return false; 160 } 161 162 // Check if perpendicular ranges overlap 163 int overlapStart = max(edge1.start, edge2.start); 164 int overlapEnd = min(edge1.end, edge2.end); 165 166 return overlapEnd > overlapStart + tolerance; 167 } 168 169 bool MonitorTopology::IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType, WrapMode wrapMode) const 170 { 171 RECT monitorRect; 172 if (!GetMonitorRect(monitor, monitorRect)) 173 { 174 Logger::warn(L"IsOnOuterEdge: GetMonitorRect failed for monitor handle {}", reinterpret_cast<uintptr_t>(monitor)); 175 return false; 176 } 177 178 // Get monitor index for edge map lookup 179 int monitorIndex = GetMonitorIndex(monitor); 180 if (monitorIndex < 0) 181 { 182 Logger::warn(L"IsOnOuterEdge: Monitor index not found for handle {} at cursor ({}, {})", 183 reinterpret_cast<uintptr_t>(monitor), cursorPos.x, cursorPos.y); 184 return false; // Monitor not found in our list 185 } 186 187 // Check each edge type 188 const int edgeThreshold = 1; 189 190 // At corners, multiple edges may match - collect all candidates and try each 191 // to find one with a valid wrap destination 192 std::vector<EdgeType> candidateEdges; 193 194 // Left edge - only if mode allows horizontal wrapping 195 if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::HorizontalOnly) && 196 cursorPos.x <= monitorRect.left + edgeThreshold) 197 { 198 auto it = m_edgeMap.find({monitorIndex, EdgeType::Left}); 199 if (it != m_edgeMap.end() && it->second.isOuter) 200 { 201 candidateEdges.push_back(EdgeType::Left); 202 } 203 } 204 205 // Right edge - only if mode allows horizontal wrapping 206 if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::HorizontalOnly) && 207 cursorPos.x >= monitorRect.right - 1 - edgeThreshold) 208 { 209 auto it = m_edgeMap.find({monitorIndex, EdgeType::Right}); 210 if (it != m_edgeMap.end()) 211 { 212 if (it->second.isOuter) 213 { 214 candidateEdges.push_back(EdgeType::Right); 215 } 216 // Debug: Log why right edge isn't outer 217 else 218 { 219 Logger::trace(L"IsOnOuterEdge: Monitor {} right edge is NOT outer (inner edge)", monitorIndex); 220 } 221 } 222 } 223 224 // Top edge - only if mode allows vertical wrapping 225 if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::VerticalOnly) && 226 cursorPos.y <= monitorRect.top + edgeThreshold) 227 { 228 auto it = m_edgeMap.find({monitorIndex, EdgeType::Top}); 229 if (it != m_edgeMap.end() && it->second.isOuter) 230 { 231 candidateEdges.push_back(EdgeType::Top); 232 } 233 } 234 235 // Bottom edge - only if mode allows vertical wrapping 236 if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::VerticalOnly) && 237 cursorPos.y >= monitorRect.bottom - 1 - edgeThreshold) 238 { 239 auto it = m_edgeMap.find({monitorIndex, EdgeType::Bottom}); 240 if (it != m_edgeMap.end() && it->second.isOuter) 241 { 242 candidateEdges.push_back(EdgeType::Bottom); 243 } 244 } 245 246 if (candidateEdges.empty()) 247 { 248 return false; 249 } 250 251 // Try each candidate edge and return first with valid wrap destination 252 for (EdgeType candidate : candidateEdges) 253 { 254 MonitorEdge oppositeEdge = FindOppositeOuterEdge(candidate, 255 (candidate == EdgeType::Left || candidate == EdgeType::Right) ? cursorPos.y : cursorPos.x); 256 257 if (oppositeEdge.monitorIndex >= 0) 258 { 259 outEdgeType = candidate; 260 return true; 261 } 262 } 263 264 return false; 265 } 266 267 POINT MonitorTopology::GetWrapDestination(HMONITOR fromMonitor, const POINT& cursorPos, EdgeType edgeType) const 268 { 269 // Get monitor index for edge map lookup 270 int monitorIndex = GetMonitorIndex(fromMonitor); 271 if (monitorIndex < 0) 272 { 273 return cursorPos; // Monitor not found 274 } 275 276 auto it = m_edgeMap.find({monitorIndex, edgeType}); 277 if (it == m_edgeMap.end()) 278 { 279 return cursorPos; // Edge not found 280 } 281 282 const MonitorEdge& fromEdge = it->second; 283 284 // Calculate relative position on current edge (0.0 to 1.0) 285 double relativePos = GetRelativePosition(fromEdge, 286 (edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x); 287 288 // Find opposite outer edge 289 MonitorEdge oppositeEdge = FindOppositeOuterEdge(edgeType, 290 (edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x); 291 292 if (oppositeEdge.monitorIndex < 0) 293 { 294 // No opposite edge found, wrap within same monitor 295 RECT monitorRect; 296 if (GetMonitorRect(fromMonitor, monitorRect)) 297 { 298 POINT result = cursorPos; 299 switch (edgeType) 300 { 301 case EdgeType::Left: 302 result.x = monitorRect.right - 2; 303 break; 304 case EdgeType::Right: 305 result.x = monitorRect.left + 1; 306 break; 307 case EdgeType::Top: 308 result.y = monitorRect.bottom - 2; 309 break; 310 case EdgeType::Bottom: 311 result.y = monitorRect.top + 1; 312 break; 313 } 314 return result; 315 } 316 return cursorPos; 317 } 318 319 // Calculate target position on opposite edge 320 POINT result; 321 322 if (edgeType == EdgeType::Left || edgeType == EdgeType::Right) 323 { 324 // Horizontal edge -> vertical movement 325 result.x = oppositeEdge.position; 326 result.y = GetAbsolutePosition(oppositeEdge, relativePos); 327 } 328 else 329 { 330 // Vertical edge -> horizontal movement 331 result.y = oppositeEdge.position; 332 result.x = GetAbsolutePosition(oppositeEdge, relativePos); 333 } 334 335 return result; 336 } 337 338 MonitorEdge MonitorTopology::FindOppositeOuterEdge(EdgeType fromEdge, int relativePosition) const 339 { 340 EdgeType targetType; 341 bool findMax; // true = find max position, false = find min position 342 343 switch (fromEdge) 344 { 345 case EdgeType::Left: 346 targetType = EdgeType::Right; 347 findMax = true; 348 break; 349 case EdgeType::Right: 350 targetType = EdgeType::Left; 351 findMax = false; 352 break; 353 case EdgeType::Top: 354 targetType = EdgeType::Bottom; 355 findMax = true; 356 break; 357 case EdgeType::Bottom: 358 targetType = EdgeType::Top; 359 findMax = false; 360 break; 361 default: 362 return { .monitorIndex = -1 }; // Invalid edge type 363 } 364 365 MonitorEdge result = { .monitorIndex = -1 }; // -1 indicates not found 366 int extremePosition = findMax ? INT_MIN : INT_MAX; 367 368 for (const auto& edge : m_outerEdges) 369 { 370 if (edge.type != targetType) 371 { 372 continue; 373 } 374 375 // Check if this edge overlaps with the relative position 376 if (relativePosition >= edge.start && relativePosition <= edge.end) 377 { 378 if ((findMax && edge.position > extremePosition) || 379 (!findMax && edge.position < extremePosition)) 380 { 381 extremePosition = edge.position; 382 result = edge; 383 } 384 } 385 } 386 387 return result; 388 } 389 390 double MonitorTopology::GetRelativePosition(const MonitorEdge& edge, int coordinate) const 391 { 392 if (edge.end == edge.start) 393 { 394 return 0.5; // Avoid division by zero 395 } 396 397 int clamped = max(edge.start, min(coordinate, edge.end)); 398 // Use int64_t to avoid overflow warning C26451 399 int64_t numerator = static_cast<int64_t>(clamped) - static_cast<int64_t>(edge.start); 400 int64_t denominator = static_cast<int64_t>(edge.end) - static_cast<int64_t>(edge.start); 401 return static_cast<double>(numerator) / static_cast<double>(denominator); 402 } 403 404 int MonitorTopology::GetAbsolutePosition(const MonitorEdge& edge, double relativePosition) const 405 { 406 // Use int64_t to prevent arithmetic overflow during subtraction and multiplication 407 int64_t range = static_cast<int64_t>(edge.end) - static_cast<int64_t>(edge.start); 408 int64_t offset = static_cast<int64_t>(relativePosition * static_cast<double>(range)); 409 // Clamp result to int range before returning 410 int64_t result = static_cast<int64_t>(edge.start) + offset; 411 return static_cast<int>(result); 412 } 413 414 std::vector<MonitorTopology::GapInfo> MonitorTopology::DetectMonitorGaps() const 415 { 416 std::vector<GapInfo> gaps; 417 const int gapThreshold = 50; // Same as ADJACENCY_TOLERANCE 418 419 // Check each pair of monitors 420 for (size_t i = 0; i < m_monitors.size(); ++i) 421 { 422 for (size_t j = i + 1; j < m_monitors.size(); ++j) 423 { 424 const auto& m1 = m_monitors[i]; 425 const auto& m2 = m_monitors[j]; 426 427 // Check vertical overlap 428 int vOverlapStart = max(m1.rect.top, m2.rect.top); 429 int vOverlapEnd = min(m1.rect.bottom, m2.rect.bottom); 430 int vOverlap = vOverlapEnd - vOverlapStart; 431 432 if (vOverlap <= 0) 433 { 434 continue; // No vertical overlap, skip 435 } 436 437 // Check horizontal gap 438 int hGap = min(abs(m1.rect.right - m2.rect.left), abs(m2.rect.right - m1.rect.left)); 439 440 if (hGap > gapThreshold) 441 { 442 GapInfo gap; 443 gap.monitor1Index = static_cast<int>(i); 444 gap.monitor2Index = static_cast<int>(j); 445 gap.horizontalGap = hGap; 446 gap.verticalOverlap = vOverlap; 447 gaps.push_back(gap); 448 } 449 } 450 } 451 452 return gaps; 453 } 454 455 HMONITOR MonitorTopology::GetMonitorFromPoint(const POINT& pt) const 456 { 457 return MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST); 458 } 459 460 bool MonitorTopology::GetMonitorRect(HMONITOR monitor, RECT& rect) const 461 { 462 // First try direct HMONITOR comparison 463 for (const auto& monitorInfo : m_monitors) 464 { 465 if (monitorInfo.hMonitor == monitor) 466 { 467 rect = monitorInfo.rect; 468 return true; 469 } 470 } 471 472 // Fallback: If direct comparison fails, try matching by current monitor info 473 MONITORINFO mi{}; 474 mi.cbSize = sizeof(MONITORINFO); 475 if (GetMonitorInfo(monitor, &mi)) 476 { 477 for (const auto& monitorInfo : m_monitors) 478 { 479 if (monitorInfo.rect.left == mi.rcMonitor.left && 480 monitorInfo.rect.top == mi.rcMonitor.top && 481 monitorInfo.rect.right == mi.rcMonitor.right && 482 monitorInfo.rect.bottom == mi.rcMonitor.bottom) 483 { 484 rect = monitorInfo.rect; 485 return true; 486 } 487 } 488 } 489 490 return false; 491 } 492 493 HMONITOR MonitorTopology::GetMonitorFromRect(const RECT& rect) const 494 { 495 return MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST); 496 } 497 498 int MonitorTopology::GetMonitorIndex(HMONITOR monitor) const 499 { 500 // First try direct HMONITOR comparison (fast and accurate) 501 for (size_t i = 0; i < m_monitors.size(); ++i) 502 { 503 if (m_monitors[i].hMonitor == monitor) 504 { 505 return static_cast<int>(i); 506 } 507 } 508 509 // Fallback: If direct comparison fails (e.g., handle changed after display reconfiguration), 510 // try matching by position. Get the monitor's current rect and find matching stored rect. 511 MONITORINFO mi{}; 512 mi.cbSize = sizeof(MONITORINFO); 513 if (GetMonitorInfo(monitor, &mi)) 514 { 515 for (size_t i = 0; i < m_monitors.size(); ++i) 516 { 517 // Match by rect bounds 518 if (m_monitors[i].rect.left == mi.rcMonitor.left && 519 m_monitors[i].rect.top == mi.rcMonitor.top && 520 m_monitors[i].rect.right == mi.rcMonitor.right && 521 m_monitors[i].rect.bottom == mi.rcMonitor.bottom) 522 { 523 Logger::trace(L"GetMonitorIndex: Found monitor {} via rect fallback (handle changed from {} to {})", 524 i, reinterpret_cast<uintptr_t>(m_monitors[i].hMonitor), reinterpret_cast<uintptr_t>(monitor)); 525 return static_cast<int>(i); 526 } 527 } 528 529 // Log all stored monitors vs the requested one for debugging 530 Logger::warn(L"GetMonitorIndex: No match found. Requested monitor rect=({},{},{},{})", 531 mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom); 532 for (size_t i = 0; i < m_monitors.size(); ++i) 533 { 534 Logger::warn(L" Stored monitor {}: rect=({},{},{},{})", 535 i, m_monitors[i].rect.left, m_monitors[i].rect.top, 536 m_monitors[i].rect.right, m_monitors[i].rect.bottom); 537 } 538 } 539 else 540 { 541 Logger::warn(L"GetMonitorIndex: GetMonitorInfo failed for handle {}", reinterpret_cast<uintptr_t>(monitor)); 542 } 543 544 return -1; // Not found 545 } 546