feat(window_manager): add multi-monitor awareness, floor calculation, and window ledge platform extraction
This commit is contained in:
@@ -60,6 +60,25 @@ class WindowInfo:
|
||||
return max(0, self.rect[3] - self.rect[1])
|
||||
|
||||
|
||||
@dataclass
|
||||
class WindowLedge:
|
||||
"""Represents a walkable top surface of a desktop window."""
|
||||
hwnd: int
|
||||
title: str
|
||||
left: float
|
||||
right: float
|
||||
top_y: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class MonitorInfo:
|
||||
"""Represents a display monitor and its working area (excluding taskbar)."""
|
||||
handle: int
|
||||
is_primary: bool
|
||||
rect: Tuple[int, int, int, int] # (left, top, right, bottom)
|
||||
work_area: Tuple[int, int, int, int] # (left, top, right, bottom) excluding taskbar
|
||||
|
||||
|
||||
class WindowManager:
|
||||
"""Win32 window query and manipulation utility."""
|
||||
|
||||
@@ -294,3 +313,130 @@ class WindowManager:
|
||||
return (0, 0, w, h)
|
||||
|
||||
return (vx, vy, vx + vw, vy + vh)
|
||||
|
||||
@classmethod
|
||||
def get_monitors(cls) -> List[MonitorInfo]:
|
||||
"""
|
||||
Enumerates all connected display monitors and returns their full
|
||||
bounding rectangles and work areas (excluding taskbars).
|
||||
"""
|
||||
class _MONITORINFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbSize", wintypes.DWORD),
|
||||
("rcMonitor", wintypes.RECT),
|
||||
("rcWork", wintypes.RECT),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
]
|
||||
|
||||
monitors: List[MonitorInfo] = []
|
||||
MONITORENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HMONITOR, wintypes.HDC, ctypes.POINTER(wintypes.RECT), wintypes.LPARAM)
|
||||
|
||||
def enum_mon_proc(h_mon, hdc, lprect, lparam):
|
||||
mi = _MONITORINFO()
|
||||
mi.cbSize = ctypes.sizeof(_MONITORINFO)
|
||||
if user32.GetMonitorInfoW(h_mon, ctypes.byref(mi)):
|
||||
m = mi.rcMonitor
|
||||
w = mi.rcWork
|
||||
is_pri = bool(mi.dwFlags & 1)
|
||||
monitors.append(MonitorInfo(
|
||||
handle=int(h_mon),
|
||||
is_primary=is_pri,
|
||||
rect=(m.left, m.top, m.right, m.bottom),
|
||||
work_area=(w.left, w.top, w.right, w.bottom),
|
||||
))
|
||||
return True
|
||||
|
||||
user32.EnumDisplayMonitors(0, None, MONITORENUMPROC(enum_mon_proc), 0)
|
||||
return monitors
|
||||
|
||||
@classmethod
|
||||
def get_floor_y_at(cls, x: float, cat_height: int) -> float:
|
||||
"""
|
||||
Returns the screen floor Y coordinate for a given horizontal X position,
|
||||
accounting for multi-monitor setups where different monitors have taskbars
|
||||
at different vertical coordinates.
|
||||
"""
|
||||
monitors = cls.get_monitors()
|
||||
for mon in monitors:
|
||||
wa = mon.work_area
|
||||
if wa[0] <= x < wa[2]:
|
||||
return float(wa[3] - cat_height)
|
||||
|
||||
# Fallback to primary work area
|
||||
wa_pri = cls.get_work_area()
|
||||
return float(wa_pri[3] - cat_height)
|
||||
|
||||
@classmethod
|
||||
def get_window_ledges(cls, min_width: int = 120) -> List[WindowLedge]:
|
||||
"""
|
||||
Queries all visible, non-minimized top-level windows to extract
|
||||
walkable horizontal platforms (window top titlebar frames).
|
||||
"""
|
||||
ledges: List[WindowLedge] = []
|
||||
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
|
||||
|
||||
def enum_proc(hwnd, lparam):
|
||||
if not user32.IsWindowVisible(hwnd) or user32.IsIconic(hwnd):
|
||||
return True
|
||||
|
||||
title = cls.get_window_title(hwnd)
|
||||
if not title:
|
||||
return True
|
||||
|
||||
# Exclude overlays and desktop shell
|
||||
t_lower = title.lower()
|
||||
if "catser overlay" in t_lower or "program manager" in t_lower:
|
||||
return True
|
||||
|
||||
rect = cls.get_window_rect(hwnd)
|
||||
if not rect:
|
||||
return True
|
||||
|
||||
left, top, right, bottom = rect
|
||||
width = right - left
|
||||
height = bottom - top
|
||||
|
||||
# Window must be large enough to serve as a platform
|
||||
if width >= min_width and height >= 60:
|
||||
ledges.append(WindowLedge(
|
||||
hwnd=hwnd,
|
||||
title=title,
|
||||
left=float(left),
|
||||
right=float(right),
|
||||
top_y=float(top),
|
||||
))
|
||||
return True
|
||||
|
||||
h_input_desk = user32.OpenInputDesktop(0, False, 0x01FF)
|
||||
if h_input_desk:
|
||||
user32.EnumDesktopWindows(h_input_desk, WNDENUMPROC(enum_proc), 0)
|
||||
user32.CloseDesktop(h_input_desk)
|
||||
else:
|
||||
user32.EnumWindows(WNDENUMPROC(enum_proc), 0)
|
||||
|
||||
# Sort highest ledge first (lowest top_y)
|
||||
ledges.sort(key=lambda l: l.top_y)
|
||||
return ledges
|
||||
|
||||
@classmethod
|
||||
def get_surface_beneath(cls, x: float, y: float, cat_width: int, cat_height: int) -> float:
|
||||
"""
|
||||
Finds the highest walking surface directly beneath the cat (either an open
|
||||
window top ledge or the monitor floor). Returns the target cat.y coordinate
|
||||
(i.e. surface_top_y - cat_height).
|
||||
"""
|
||||
cat_center_x = x + cat_width * 0.5
|
||||
feet_y = y + cat_height
|
||||
floor_y = cls.get_floor_y_at(cat_center_x, cat_height)
|
||||
|
||||
# Search for window ledges below the cat's current feet
|
||||
candidate_y = floor_y
|
||||
ledges = cls.get_window_ledges(min_width=100)
|
||||
for ledge in ledges:
|
||||
if ledge.left <= cat_center_x <= ledge.right:
|
||||
target_cat_y = ledge.top_y - cat_height
|
||||
# Is this ledge below the cat's feet (with 6px landing tolerance)?
|
||||
if ledge.top_y >= feet_y - 6.0 and target_cat_y < candidate_y:
|
||||
candidate_y = target_cat_y
|
||||
|
||||
return candidate_y
|
||||
|
||||
Reference in New Issue
Block a user