feat(window_manager): add multi-monitor awareness, floor calculation, and window ledge platform extraction

This commit is contained in:
2026-09-08 23:41:25 +02:00
parent 71fe013c80
commit 5d152db057
+146
View File
@@ -60,6 +60,25 @@ class WindowInfo:
return max(0, self.rect[3] - self.rect[1]) 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: class WindowManager:
"""Win32 window query and manipulation utility.""" """Win32 window query and manipulation utility."""
@@ -294,3 +313,130 @@ class WindowManager:
return (0, 0, w, h) return (0, 0, w, h)
return (vx, vy, vx + vw, vy + vh) 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