/ src / modules / MouseUtils / CursorWrap / MonitorTopology.cpp
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