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])
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user