""" Win32 Transparent Layered Window Overlay for Catser. Provides a frameless, topmost, 32-bit ARGB per-pixel alpha transparent window. Implements: - True alpha blending with smooth antialiased edges and drop shadows via UpdateLayeredWindow. - Pixel-perfect hit testing so transparent areas let mouse clicks pass through, while clicking the cat allows dragging or petting. - Drag-and-drop physics and mouse interaction. """ import ctypes from ctypes import wintypes import logging from typing import Optional, Tuple, Callable from PIL import Image logger = logging.getLogger("catser.overlay") # ============================================================================== # Win32 Definitions & Constants # ============================================================================== user32 = ctypes.windll.user32 gdi32 = ctypes.windll.gdi32 kernel32 = ctypes.windll.kernel32 # Set proper 64-bit argtypes and restype to prevent ctypes integer overflow user32.DefWindowProcW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM] user32.DefWindowProcW.restype = ctypes.c_longlong user32.GetDC.argtypes = [wintypes.HWND] user32.GetDC.restype = wintypes.HDC user32.ReleaseDC.argtypes = [wintypes.HWND, wintypes.HDC] user32.ReleaseDC.restype = ctypes.c_int user32.SetWindowPos.argtypes = [wintypes.HWND, ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, wintypes.UINT] user32.SetWindowPos.restype = wintypes.BOOL gdi32.CreateCompatibleDC.argtypes = [wintypes.HDC] gdi32.CreateCompatibleDC.restype = wintypes.HDC gdi32.DeleteDC.argtypes = [wintypes.HDC] gdi32.DeleteDC.restype = wintypes.BOOL gdi32.SelectObject.argtypes = [wintypes.HDC, wintypes.HGDIOBJ] gdi32.SelectObject.restype = wintypes.HGDIOBJ gdi32.DeleteObject.argtypes = [wintypes.HGDIOBJ] gdi32.DeleteObject.restype = wintypes.BOOL # Window styles WS_POPUP = 0x80000000 WS_VISIBLE = 0x10000000 # Extended window styles WS_EX_LAYERED = 0x00080000 WS_EX_TOPMOST = 0x00000008 WS_EX_TOOLWINDOW = 0x00000080 WS_EX_NOACTIVATE = 0x08000000 # UpdateLayeredWindow flags ULW_ALPHA = 0x00000002 AC_SRC_OVER = 0x00 AC_SRC_ALPHA = 0x01 # Window messages WM_DESTROY = 0x0002 WM_PAINT = 0x000F WM_LBUTTONDOWN = 0x0201 WM_LBUTTONUP = 0x0202 WM_MOUSEMOVE = 0x0200 WM_NCHITTEST = 0x0084 # Hit test return codes HTTRANSPARENT = -1 HTCLIENT = 1 # SetWindowPos flags SWP_NOSIZE = 0x0001 SWP_NOMOVE = 0x0002 SWP_NOACTIVATE = 0x0010 SWP_SHOWWINDOW = 0x0040 HWND_TOPMOST = -1 # C Structs class BLENDFUNCTION(ctypes.Structure): _fields_ = [ ("BlendOp", wintypes.BYTE), ("BlendFlags", wintypes.BYTE), ("SourceConstantAlpha", wintypes.BYTE), ("AlphaFormat", wintypes.BYTE), ] class POINT(ctypes.Structure): _fields_ = [("x", wintypes.LONG), ("y", wintypes.LONG)] class SIZE(ctypes.Structure): _fields_ = [("cx", wintypes.LONG), ("cy", wintypes.LONG)] class BITMAPINFOHEADER(ctypes.Structure): _fields_ = [ ("biSize", wintypes.DWORD), ("biWidth", wintypes.LONG), ("biHeight", wintypes.LONG), ("biPlanes", wintypes.WORD), ("biBitCount", wintypes.WORD), ("biCompression", wintypes.DWORD), ("biSizeImage", wintypes.DWORD), ("biXPelsPerMeter", wintypes.LONG), ("biYPelsPerMeter", wintypes.LONG), ("biClrUsed", wintypes.DWORD), ("biClrImportant", wintypes.DWORD), ] class BITMAPINFO(ctypes.Structure): _fields_ = [ ("bmiHeader", BITMAPINFOHEADER), ("bmiColors", wintypes.DWORD * 3), ] WNDPROC = ctypes.WINFUNCTYPE(ctypes.c_longlong, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM) class WNDCLASSEXW(ctypes.Structure): _fields_ = [ ("cbSize", wintypes.UINT), ("style", wintypes.UINT), ("lpfnWndProc", WNDPROC), ("cbClsExtra", ctypes.c_int), ("cbWndExtra", ctypes.c_int), ("hInstance", wintypes.HINSTANCE), ("hIcon", wintypes.HICON), ("hCursor", wintypes.HICON), ("hbrBackground", wintypes.HBRUSH), ("lpszMenuName", wintypes.LPCWSTR), ("lpszClassName", wintypes.LPCWSTR), ("hIconSm", wintypes.HICON), ] gdi32.CreateDIBSection.argtypes = [ wintypes.HDC, ctypes.POINTER(BITMAPINFO), wintypes.UINT, ctypes.POINTER(ctypes.c_void_p), wintypes.HANDLE, wintypes.DWORD, ] gdi32.CreateDIBSection.restype = wintypes.HBITMAP user32.UpdateLayeredWindow.argtypes = [ wintypes.HWND, wintypes.HDC, ctypes.POINTER(POINT), ctypes.POINTER(SIZE), wintypes.HDC, ctypes.POINTER(POINT), wintypes.COLORREF, ctypes.POINTER(BLENDFUNCTION), wintypes.DWORD, ] user32.UpdateLayeredWindow.restype = wintypes.BOOL class OverlayWindow: """ Manages the transparent, topmost desktop overlay window for Catser. """ CLASS_NAME = "CatserOverlayClass" def __init__( self, width: int, height: int, initial_x: int = 100, initial_y: int = 100, on_mouse_down: Optional[Callable[[int, int], None]] = None, on_mouse_move: Optional[Callable[[int, int], None]] = None, on_mouse_up: Optional[Callable[[int, int], None]] = None, ): self.width = width self.height = height self.x = initial_x self.y = initial_y self.on_mouse_down = on_mouse_down self.on_mouse_move = on_mouse_move self.on_mouse_up = on_mouse_up self.hwnd: Optional[int] = None self._wndproc_ref = None # Prevent garbage collection of callback self._current_image: Optional[Image.Image] = None self.is_dragging = False self._create_window() def _create_window(self) -> None: """Registers window class and creates the Win32 layered overlay window.""" hinstance = kernel32.GetModuleHandleW(None) self._wndproc_ref = WNDPROC(self._window_proc) wndclass = WNDCLASSEXW() wndclass.cbSize = ctypes.sizeof(WNDCLASSEXW) wndclass.style = 0 wndclass.lpfnWndProc = self._wndproc_ref wndclass.cbClsExtra = 0 wndclass.cbWndExtra = 0 wndclass.hInstance = hinstance wndclass.hIcon = 0 wndclass.hCursor = user32.LoadCursorW(0, 32512) # IDC_ARROW wndclass.hbrBackground = 0 wndclass.lpszMenuName = None wndclass.lpszClassName = self.CLASS_NAME wndclass.hIconSm = 0 user32.RegisterClassExW(ctypes.byref(wndclass)) # Extended style: layered, topmost, tool window (no taskbar icon), no-activate dw_ex_style = WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE dw_style = WS_POPUP | WS_VISIBLE self.hwnd = user32.CreateWindowExW( dw_ex_style, self.CLASS_NAME, "Catser Overlay", dw_style, self.x, self.y, self.width, self.height, 0, 0, hinstance, None, ) if not self.hwnd: err = kernel32.GetLastError() raise RuntimeError(f"Failed to create overlay window (error {err})") # Keep on top user32.SetWindowPos(self.hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW) logger.info(f"Overlay window created with HWND: {self.hwnd} at ({self.x}, {self.y})") def _window_proc(self, hwnd: int, msg: int, wparam: int, lparam: int) -> int: """Handles Windows message loop events including mouse interaction and hit-testing.""" if msg == WM_NCHITTEST: # Check pixel alpha under cursor so transparent regions let clicks pass through screen_x = lparam & 0xFFFF screen_y = (lparam >> 16) & 0xFFFF # Sign extend 16-bit to handle multi-monitor negative coordinates if screen_x >= 0x8000: screen_x -= 0x10000 if screen_y >= 0x8000: screen_y -= 0x10000 local_x = screen_x - self.x local_y = screen_y - self.y if self.is_dragging: return HTCLIENT if self._current_image and 0 <= local_x < self.width and 0 <= local_y < self.height: try: pixel = self._current_image.getpixel((local_x, local_y)) alpha = pixel[3] if len(pixel) > 3 else 255 # Clickable only if pixel is reasonably opaque if alpha > 30: return HTCLIENT except Exception: pass return HTTRANSPARENT elif msg == WM_LBUTTONDOWN: user32.SetCapture(hwnd) self.is_dragging = True pos = wintypes.POINT() user32.GetCursorPos(ctypes.byref(pos)) if self.on_mouse_down: self.on_mouse_down(pos.x, pos.y) return 0 elif msg == WM_MOUSEMOVE: if self.is_dragging: pos = wintypes.POINT() user32.GetCursorPos(ctypes.byref(pos)) if self.on_mouse_move: self.on_mouse_move(pos.x, pos.y) return 0 elif msg == WM_LBUTTONUP: if self.is_dragging: self.is_dragging = False user32.ReleaseCapture() pos = wintypes.POINT() user32.GetCursorPos(ctypes.byref(pos)) if self.on_mouse_up: self.on_mouse_up(pos.x, pos.y) return 0 elif msg == WM_DESTROY: user32.PostQuitMessage(0) return 0 return user32.DefWindowProcW(hwnd, msg, wparam, lparam) def set_position(self, x: int, y: int) -> None: """Updates the window on-screen coordinates.""" self.x = int(round(x)) self.y = int(round(y)) if self.hwnd: user32.SetWindowPos( self.hwnd, HWND_TOPMOST, self.x, self.y, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW, ) def draw_frame(self, image: Image.Image, x: Optional[int] = None, y: Optional[int] = None) -> None: """ Renders a 32-bit RGBA PIL Image to the overlay using UpdateLayeredWindow. Converts pixels to premultiplied BGRA for hardware-accelerated Windows compositing. """ if not self.hwnd: return if x is not None: self.x = int(round(x)) if y is not None: self.y = int(round(y)) # Resize image if not matching window canvas if image.size != (self.width, self.height): image = image.resize((self.width, self.height), Image.Resampling.LANCZOS) self._current_image = image # Convert PIL RGBA to premultiplied BGRA bytes # Windows UpdateLayeredWindow expects B, G, R, A with color channels premultiplied by alpha bgra_bytes = self._premultiply_bgra(image) # Create compatible Memory DC and DIB section hdc_screen = user32.GetDC(0) hdc_mem = gdi32.CreateCompatibleDC(hdc_screen) bmi = BITMAPINFO() bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER) bmi.bmiHeader.biWidth = self.width bmi.bmiHeader.biHeight = -self.height # Top-down DIB bmi.bmiHeader.biPlanes = 1 bmi.bmiHeader.biBitCount = 32 bmi.bmiHeader.biCompression = 0 # BI_RGB bits_ptr = ctypes.c_void_p() hbitmap = gdi32.CreateDIBSection( hdc_mem, ctypes.byref(bmi), 0, ctypes.byref(bits_ptr), 0, 0, ) # Copy BGRA bytes directly into the DIB memory buffer ctypes.memmove(bits_ptr, bgra_bytes, len(bgra_bytes)) old_bmp = gdi32.SelectObject(hdc_mem, hbitmap) pt_dst = POINT(self.x, self.y) size = SIZE(self.width, self.height) pt_src = POINT(0, 0) blend = BLENDFUNCTION() blend.BlendOp = AC_SRC_OVER blend.BlendFlags = 0 blend.SourceConstantAlpha = 255 blend.AlphaFormat = AC_SRC_ALPHA # Single atomic call to update layered window position and 32-bit alpha frame user32.UpdateLayeredWindow( self.hwnd, hdc_screen, ctypes.byref(pt_dst), ctypes.byref(size), hdc_mem, ctypes.byref(pt_src), 0, ctypes.byref(blend), ULW_ALPHA, ) # Cleanup GDI handles gdi32.SelectObject(hdc_mem, old_bmp) gdi32.DeleteObject(hbitmap) gdi32.DeleteDC(hdc_mem) user32.ReleaseDC(0, hdc_screen) @staticmethod def _premultiply_bgra(img: Image.Image) -> bytes: """ Converts a PIL RGBA image to premultiplied BGRA bytes for Win32 UpdateLayeredWindow (DIB_RGB_COLORS format required by GDI). Uses numpy for a fully vectorised, branch-free transformation: 1. Reorder channels R,G,B,A → B,G,R,A (GDI expects BGR order) 2. Premultiply colour channels by alpha/255 in floating-point 3. Cast back to uint8 This is ~100× faster than an equivalent pure-Python loop and is the reason the overlay no longer hangs at 60 FPS. """ import numpy as np # numpy 2.x is available in this env (checked at runtime) if img.mode != "RGBA": img = img.convert("RGBA") # Shape: (H*W, 4) — columns are R, G, B, A arr = np.frombuffer(img.tobytes(), dtype=np.uint8).reshape(-1, 4) # Reorder to BGRA: index mapping R=0,G=1,B=2,A=3 → B=2,G=1,R=0,A=3 bgra = arr[:, [2, 1, 0, 3]].copy() # Alpha channel as float32 in [0,1] for premultiplication alpha = bgra[:, 3].astype(np.float32) / 255.0 # Premultiply B, G, R in place (column indices 0,1,2) bgra[:, 0] = (bgra[:, 0] * alpha).astype(np.uint8) bgra[:, 1] = (bgra[:, 1] * alpha).astype(np.uint8) bgra[:, 2] = (bgra[:, 2] * alpha).astype(np.uint8) return bgra.tobytes() def process_messages(self) -> bool: """ Dispatches pending Windows messages. Returns False if WM_QUIT was received. """ msg = wintypes.MSG() while user32.PeekMessageW(ctypes.byref(msg), 0, 0, 0, 1): # PM_REMOVE = 1 if msg.message == 0x0012: # WM_QUIT return False user32.TranslateMessage(ctypes.byref(msg)) user32.DispatchMessageW(ctypes.byref(msg)) return True def destroy(self) -> None: """Destroys the window handle and unregisters class.""" if self.hwnd: user32.DestroyWindow(self.hwnd) self.hwnd = None