Last active
December 28, 2021 23:52
-
-
Save s-zeid/52767b52d36cfa055d8681987ef2ea9b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# vim: set fdm=marker: | |
import atexit | |
import os | |
import sys | |
from typing import * | |
class TransparentWindow: #{{{1 | |
def __init__(self, | |
x: int, y: int, w: int, h: int, | |
*, | |
warn_if_unable: bool = True, | |
wm_class: str = None, | |
): | |
raise NotImplementedError | |
def _tick(self): | |
raise NotImplementedError | |
@property # is_transparent #{{{2 | |
def is_transparent(self): | |
return self._is_transparent | |
def loop(self, inner=None): #{{{2 | |
self._running = True | |
while self._running: | |
if not self.tick(inner=inner): | |
break | |
def stop(self): #{{{2 | |
self._running = False | |
def tick(self, inner=None) -> bool: #{{{2 | |
if inner: | |
inner() | |
return self._tick() | |
def __del__(self): #{{{2 | |
pass | |
@classmethod # _x_match_visual_info_reimpl #{{{2 | |
def _x_match_visual_info_reimpl(self, | |
_display: "Xlib.display.Display", # type: ignore | |
screen: "Xlib.display.Display.screen()", # type: ignore | |
visual_depth: int, | |
visual_class: int, | |
): | |
"""A reimplementation of `XMatchVisualInfo()`.""" | |
visuals = None | |
for i in screen.allowed_depths: | |
if i["depth"] == visual_depth: | |
visuals = i["visuals"] | |
break | |
if not visuals: | |
raise RuntimeError(f"no depth {visual_depth} in screen.allowed_depths") | |
visual = None | |
for i in visuals: | |
if i["visual_class"] == visual_class: | |
visual = i | |
break | |
if not visual: | |
raise RuntimeError(f"no visual with class {visual_class} in depth {visual_depth}") | |
visual["_visual_depth"] = visual_depth | |
return visual | |
class TransparentSDLWindow(TransparentWindow): #{{{1 | |
def __init__(self, #{{{2 | |
x: int, y: int, w: int, h: int, | |
*, | |
warn_if_unable: bool = True, | |
flags: int = 0, | |
wm_class: str = None, | |
): | |
import sdl2 # type: ignore # pip3: PySDL2; apt: python3-sdl2; apk: sdl2, then use pip3 | |
try: | |
import Xlib.X # type: ignore # pip3: python-xlib; apt: python3-xlib; apk: py3-xlib | |
import Xlib.Xatom # type: ignore | |
import Xlib.display # type: ignore | |
except ImportError: | |
Xlib = None # type: ignore | |
if os.environ.get("XAUTHORITY") and os.environ.get("DISPLAY"): | |
for line in [ | |
"warning: cannot make X window transparent: python-xlib is not installed", | |
"hint: run `pip3 install python-xlib`, `apt install python3-xlib`,", | |
" or `apk add py3-xlib`", | |
]: | |
print(line, file=sys.stderr) | |
self._is_transparent = False | |
supported_syswm_types = [] | |
# make window transparent on supported windowing systems | |
if Xlib and self._have_x(): | |
supported_syswm_types += [sdl2.SDL_SYSWM_X11] | |
self.x_display = Xlib.display.Display() | |
self.x_screen = self.x_display.screen() | |
try: | |
visual = self._x_match_visual_info_reimpl( | |
self.x_display, self.x_screen, | |
32, Xlib.X.TrueColor, | |
) | |
self._is_transparent = True | |
except RuntimeError as exc: | |
if warn_if_unable: | |
print(f"warning: could not make window transparent: {exc}", file=sys.stderr) | |
visual = None | |
if visual: | |
os.environ["SDL_VIDEO_X11_VISUALID"] = str(visual["visual_id"]) | |
if wm_class and "SDL_VIDEO_X11_WMCLASS" not in os.environ: | |
os.environ["SDL_VIDEO_X11_WMCLASS"] = wm_class | |
# XXX: do not call self._get_default_syswm_type() before this line; | |
# SDL_* environment variables set above must be present before SDL_Init() is called | |
syswm_type = self._get_default_syswm_type() | |
syswm_types = { | |
getattr(sdl2, attr): attr.replace("SDL_SYSWM_", "") | |
for attr in sdl2.__dict__.keys() | |
if attr.startswith("SDL_SYSWM_") and attr != "SDL_SYSWM_TYPE" | |
} | |
syswm_name = syswm_types.get(syswm_type, str(syswm_type)) | |
if warn_if_unable and syswm_type not in supported_syswm_types: | |
print( | |
"warning: could not make window transparent: unsupported windowing system:", | |
syswm_name, | |
file=sys.stderr, | |
) | |
sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO) | |
atexit.register(sdl2.SDL_Quit) | |
self._tick_event = sdl2.SDL_Event() | |
forced_flags = sdl2.SDL_WINDOW_HIDDEN | |
if syswm_type == sdl2.SDL_SYSWM_X11: | |
sdl2.SDL_SetHint(sdl2.SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, b"0") | |
self.sdl_window = sdl2.SDL_CreateWindow(b"", x, y, w, h, flags | forced_flags) | |
self.sdl_window_id = sdl2.SDL_GetWindowID(self.sdl_window) | |
self.sdl_wm_info = sdl2.SDL_SysWMinfo() | |
sdl2.SDL_VERSION(self.sdl_wm_info.version) | |
sdl2.SDL_GetWindowWMInfo(self.sdl_window, self.sdl_wm_info) | |
if hasattr(self, "x_display") and self.sdl_wm_info.subsystem == sdl2.SDL_SYSWM_X11: | |
window_id = self.sdl_wm_info.info.x11.window | |
self.x_window = self.x_display.create_resource_object("window", window_id) | |
self._sdl2 = sdl2 | |
def _tick(self): #{{{2 | |
import sdl2 # type: ignore | |
if sdl2.SDL_PollEvent(self._tick_event) != 0: | |
if self._tick_event.type == sdl2.SDL_WINDOWEVENT: | |
if self._tick_event.window.event == sdl2.SDL_WINDOWEVENT_CLOSE: | |
if self._tick_event.window.windowID == self.sdl_window_id: | |
return False | |
if self._tick_event.type == sdl2.SDL_QUIT: | |
return False | |
return True | |
def __del__(self): #{{{2 | |
self._sdl2.SDL_DestroyWindow(self.sdl_window) | |
super().__del__() | |
@classmethod # _get_default_syswm_type #{{{2 | |
def _get_default_syswm_type(cls) -> int: | |
import sdl2 # type: ignore | |
sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO) | |
test_window = sdl2.SDL_CreateWindow( | |
b"TransparentSDLWindow test window", | |
0, 0, | |
1, 1, | |
sdl2.SDL_WINDOW_HIDDEN, | |
) | |
wm_info = sdl2.SDL_SysWMinfo() | |
sdl2.SDL_VERSION(wm_info.version) | |
sdl2.SDL_GetWindowWMInfo(test_window, wm_info) | |
sdl2.SDL_DestroyWindow(test_window) | |
return wm_info.subsystem | |
@classmethod # _have_x #{{{2 | |
def _have_x(cls) -> bool: | |
try: | |
import Xlib.display # type: ignore | |
import Xlib.error # type: ignore | |
try: | |
test_display = Xlib.display.Display() | |
return True | |
except (Xlib.error.XError, Xlib.error.DisplayError): | |
pass | |
except ImportError: | |
pass | |
return False | |
class TransparentXWindow(TransparentWindow): #{{{1 | |
def __init__(self, #{{{2 | |
x: int, y: int, w: int, h: int, | |
display=None, | |
*, | |
warn_if_unable: bool = True, | |
wm_class: str = None, | |
wm_protocols: list = None, | |
**attributes, | |
): | |
import Xlib.X # type: ignore # pip3: python-xlib; apt: python3-xlib; apk: py3-xlib | |
import Xlib.Xatom # type: ignore | |
import Xlib.display # type: ignore | |
self.x_display = display or Xlib.display.Display() | |
self.x_screen = self.x_display.screen() | |
self.WM_PROTOCOLS = self.x_display.intern_atom("WM_PROTOCOLS") | |
self.WM_DELETE_WINDOW = self.x_display.intern_atom("WM_DELETE_WINDOW") | |
try: | |
visual = self._x_match_visual_info_reimpl( | |
self.x_display, self.x_screen, | |
32, Xlib.X.TrueColor, | |
) | |
self._is_transparent = True | |
except RuntimeError as exc: | |
if warn_if_unable: | |
print(f"warning: could not make window transparent: {exc}", file=sys.stderr) | |
visual = None | |
self._is_transparent = False | |
if visual: | |
depth = visual["_visual_depth"] | |
self._visual_id = visual_id = visual["visual_id"] | |
colormap = self.x_screen.root.create_colormap(visual_id, Xlib.X.AllocNone) | |
else: | |
depth = self.x_screen.root_depth | |
self._visual_id = visual_id = Xlib.X.CopyFromParent | |
colormap = Xlib.X.CopyFromParent | |
attributes["border_pixel"] = attributes.get("border_pixel", 0) | |
attributes["background_pixel"] = attributes.get("background_pixel", 0) | |
self.x_window = self.x_screen.root.create_window( | |
x, y, w, h, | |
0, # border_width | |
depth, | |
Xlib.X.InputOutput, # window_class | |
visual_id, | |
colormap=colormap, | |
**attributes, | |
) | |
self.x_window.set_wm_protocols([self.WM_DELETE_WINDOW] + (wm_protocols or [])) | |
if wm_class is not None: | |
wm_class = wm_class.strip("\0") | |
if "\0" in wm_class: | |
wm_class_instance, wm_class = wm_class.split("\0", 1) | |
else: | |
wm_class_instance = wm_class | |
self.x_window.set_wm_class(wm_class_instance, wm_class) | |
_NET_WM_PID = self.x_display.intern_atom("_NET_WM_PID") | |
self.x_window.change_property(_NET_WM_PID, Xlib.Xatom.CARDINAL, 32, [os.getpid()]) | |
def _tick(self) -> bool: #{{{2 | |
import Xlib.X # type: ignore | |
pending_events = self.x_display.pending_events() | |
while pending_events > 0: | |
event = self.x_display.next_event() | |
if event.type == Xlib.X.DestroyNotify: | |
return False | |
elif event.type == Xlib.X.ClientMessage: | |
if event.client_type == self.WM_PROTOCOLS: | |
format, data = event.data | |
if format == 32 and data[0] == self.WM_DELETE_WINDOW: | |
return False | |
pending_events -= 1 | |
return True | |
if __name__ == "__main__": #{{{1 | |
t: TransparentWindow | |
kind = sys.argv[1] if len(sys.argv) > 1 else "sdl-renderer" | |
kinds = { | |
"sdl-renderer": "SDL (with renderer)", | |
"sdl-surface": "SDL (with surface)", | |
"x": "X", | |
} | |
kind_name = kinds.get(kind, kind) | |
print(f"making a transparent {kind_name} window", file=sys.stderr) | |
if False: | |
pass | |
elif kind in ("sdl-renderer", "sdl-surface"): #{{{2 | |
import sdl2 | |
wm_name = "Transparent SDL Window" | |
wm_class = "transparentsdlwindow" | |
t = TransparentSDLWindow( | |
x=sdl2.SDL_WINDOWPOS_CENTERED, y=sdl2.SDL_WINDOWPOS_CENTERED, | |
w=400, h=200, | |
wm_class=wm_class, | |
) | |
sdl2.SDL_SetWindowTitle(t.sdl_window, wm_name.encode("utf-8")) | |
sdl2.SDL_ShowWindow(t.sdl_window) | |
rect = sdl2.SDL_Rect(x=0, y=0, w=200, h=200) | |
if kind == "sdl-renderer": | |
renderer = sdl2.SDL_CreateRenderer(t.sdl_window, -1, 0) | |
sdl2.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 127) | |
sdl2.SDL_RenderFillRect(renderer, rect) | |
sdl2.SDL_RenderPresent(renderer) | |
if kind == "sdl-surface": | |
surface = sdl2.SDL_GetWindowSurface(t.sdl_window) | |
surface_format = surface.contents.format | |
sdl2.SDL_FillRect(surface, rect, sdl2.SDL_MapRGBA(surface_format, 0, 0, 0, 127)) | |
sdl2.SDL_UpdateWindowSurface(t.sdl_window) | |
print( | |
"window's pixel format:", | |
sdl2.SDL_GetPixelFormatName(sdl2.SDL_GetWindowPixelFormat(t.sdl_window)) | |
.decode("utf-8"), | |
file=sys.stderr, | |
) | |
t.loop(inner=None) | |
if kind == "sdl-renderer": | |
sdl2.SDL_DestroyRenderer(renderer) | |
if kind == "sdl-surface": | |
sdl2.SDL_FreeSurface(surface) | |
elif kind == "x": #{{{2 | |
import tkinter | |
import tkinter.ttk | |
wm_name = "Transparent X Window" | |
wm_class = "transparentxwindow" | |
t = TransparentXWindow( | |
x=0, y=0, | |
w=400, h=200, | |
wm_class=wm_class, | |
) | |
t.x_window.set_wm_name(wm_name) | |
t.x_window.map() | |
root = tkinter.Tk(use=str(t.x_window.id)) | |
frame = tkinter.ttk.Frame(root, padding=100) | |
frame.grid() | |
# XXX: non-empty text leads to a BadValue X error: | |
# | |
# X Error of failed request: BadValue (integer parameter out of range for operation) | |
# Major opcode of failed request: 91 (X_QueryColors) | |
# Value in failed request: 0xff000000 | |
# Serial number of failed request: [...] | |
# Current serial number in output stream: [...] | |
tkinter.ttk.Label(frame, text="").grid(column=0, row=0) | |
def tk_tick(): | |
root.update_idletasks() | |
root.update() | |
t.loop(inner=tk_tick) | |
else: #{{{2 | |
print("invalid kind argument:", kind, file=sys.stderr) | |
print("valid values:", ", ".join(kinds.keys()), file=sys.stderr) | |
sys.exit(2) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment