#!/usr/bin/python3 """ Bindings for the Yutani graphics libraries, including the core Yutani protocol, general graphics routines, and the system decoration library. """ from ctypes import * import importlib yutani_lib = None yutani_gfx_lib = None yutani_ctx = None yutani_menu_lib = None yutani_windows = {} _cairo_lib = None _cairo_module = None _cairo_module_lib = None _libc = CDLL('libc.so') def usleep(microseconds): _libc.usleep(microseconds) class Message(object): """A generic event message from the Yutani server.""" class _yutani_msg_t(Structure): _fields_ = [ ('magic', c_uint32), ('type', c_uint32), ('size', c_uint32), ('data', c_char*0), ] MSG_HELLO = 0x00000001 MSG_WINDOW_NEW = 0x00000002 MSG_FLIP = 0x00000003 MSG_KEY_EVENT = 0x00000004 MSG_MOUSE_EVENT = 0x00000005 MSG_WINDOW_MOVE = 0x00000006 MSG_WINDOW_CLOSE = 0x00000007 MSG_WINDOW_SHOW = 0x00000008 MSG_WINDOW_HIDE = 0x00000009 MSG_WINDOW_STACK = 0x0000000A MSG_WINDOW_FOCUS_CHANGE = 0x0000000B MSG_WINDOW_MOUSE_EVENT = 0x0000000C MSG_FLIP_REGION = 0x0000000D MSG_WINDOW_NEW_FLAGS = 0x0000000E MSG_RESIZE_REQUEST = 0x00000010 MSG_RESIZE_OFFER = 0x00000011 MSG_RESIZE_ACCEPT = 0x00000012 MSG_RESIZE_BUFID = 0x00000013 MSG_RESIZE_DONE = 0x00000014 MSG_WINDOW_ADVERTISE = 0x00000020 MSG_SUBSCRIBE = 0x00000021 MSG_UNSUBSCRIBE = 0x00000022 MSG_NOTIFY = 0x00000023 MSG_QUERY_WINDOWS = 0x00000024 MSG_WINDOW_FOCUS = 0x00000025 MSG_WINDOW_DRAG_START = 0x00000026 MSG_WINDOW_WARP_MOUSE = 0x00000027 MSG_WINDOW_SHOW_MOUSE = 0x00000028 MSG_WINDOW_RESIZE_START = 0x00000029 MSG_SESSION_END = 0x00000030 MSG_KEY_BIND = 0x00000040 MSG_WINDOW_UPDATE_SHAPE = 0x00000050 MSG_GOODBYE = 0x000000F0 MSG_TIMER_REQUEST = 0x00000100 # XXX deprecated MSG_TIMER_TICK = 0x00000101 # xxx deprecated MSG_WELCOME = 0x00010001 MSG_WINDOW_INIT = 0x00010002 def __init__(self, msg): self._ptr = msg def free(self): if self._ptr: _libc.free(self._ptr) self._ptr = None @property def type(self): return self._ptr.contents.type _message_types = {} class MessageBuilder(type): def __new__(cls, name, bases, dct): global _message_types new_cls = super(MessageBuilder, cls).__new__(cls, name, bases, dct) if 'type_val' in dct: _message_types[dct['type_val']] = new_cls return new_cls class MessageEx(Message, metaclass=MessageBuilder): """An event message with extra data available.""" type_val = None data_struct = None def __init__(self, msg): Message.__init__(self, msg) self._data_ptr = cast(byref(self._ptr.contents,Message._yutani_msg_t.data.offset), POINTER(self.data_struct)) def __getattr__(self, name): if name in dir(self._data_ptr.contents): return getattr(self._data_ptr.contents, name) raise AttributeError(name) class MessageWelcome(MessageEx): """Message sent by the server on display size changes.""" type_val = Message.MSG_WELCOME class data_struct(Structure): _fields_ = [ ('display_width', c_uint32), ('display_height', c_uint32), ] class Keycode(object): """Keycodes.""" NONE = 0 BACKSPACE = 8 CTRL_A = 1 CTRL_B = 2 CTRL_C = 3 CTRL_D = 4 CTRL_E = 5 CTRL_F = 6 CTRL_G = 7 CTRL_H = 8 CTRL_I = 9 CTRL_J = 10 CTRL_K = 11 CTRL_L = 12 CTRL_M = 13 CTRL_N = 14 CTRL_O = 15 CTRL_P = 16 CTRL_Q = 17 CTRL_R = 18 CTRL_S = 19 CTRL_T = 20 CTRL_U = 21 CTRL_V = 22 CTRL_W = 23 CTRL_X = 24 CTRL_Y = 25 CTRL_Z = 26 ESCAPE = 27 NORMAL_MAX = 256 ARROW_UP = 257 ARROW_DOWN = 258 ARROW_RIGHT = 259 ARROW_LEFT = 260 BAD_STATE = -1 CTRL_ARROW_UP = 261 CTRL_ARROW_DOWN = 262 CTRL_ARROW_RIGHT = 263 CTRL_ARROW_LEFT = 264 SHIFT_ARROW_UP = 265 SHIFT_ARROW_DOWN = 266 SHIFT_ARROW_RIGHT = 267 SHIFT_ARROW_LEFT = 268 LEFT_CTRL = 1001 LEFT_SHIFT = 1002 LEFT_ALT = 1003 LEFT_SUPER = 1004 RIGHT_CTRL = 1011 RIGHT_SHIFT = 1012 RIGHT_ALT = 1013 RIGHT_SUPER = 1014 F1 = 2001 F2 = 2002 F3 = 2003 F4 = 2004 F5 = 2005 F6 = 2006 F7 = 2007 F8 = 2008 F9 = 2009 F10 = 2010 F11 = 2011 F12 = 2012 PAGE_DOWN = 2013 PAGE_UP = 2014 HOME = 2015 END = 2016 DEL = 2017 INSERT = 2018 class Modifier(object): """Modifier key flags.""" MOD_LEFT_CTRL = 0x01 MOD_LEFT_SHIFT = 0x02 MOD_LEFT_ALT = 0x04 MOD_LEFT_SUPER = 0x08 MOD_RIGHT_CTRL = 0x10 MOD_RIGHT_SHIFT = 0x20 MOD_RIGHT_ALT = 0x40 MOD_RIGHT_SUPER = 0x80 class KeyAction(object): """Keyboard action (up or down)""" ACTION_DOWN = 1 ACTION_UP = 2 class MessageKeyEvent(MessageEx): """Message containing key event information.""" type_val = Message.MSG_KEY_EVENT class data_struct(Structure): class key_event_t(Structure): _fields_ = [ ('keycode', c_uint), ('modifiers', c_uint), ('action', c_ubyte), ('key', c_char), ] class key_event_state_t(Structure): _fields = [ ("kbd_state", c_int), ("kbd_s_state", c_int), ("k_ctrl", c_int), ("k_shift", c_int), ("k_alt", c_int), ("k_super", c_int), ("kl_ctrl", c_int), ("kl_shift", c_int), ("kl_alt", c_int), ("kl_super", c_int), ("kr_ctrl", c_int), ("kr_shift", c_int), ("kr_alt", c_int), ("kr_super", c_int), ("kbd_esc_buf", c_int), ] _fields_ = [ ('wid', c_uint32), ('event', key_event_t), ('state', key_event_state_t), ] class MessageWindowMouseEvent(MessageEx): """Message containing window-relative mouse event information.""" type_val = Message.MSG_WINDOW_MOUSE_EVENT class data_struct(Structure): _fields_ = [ ('wid', c_uint32), ('new_x', c_int32), ('new_y', c_int32), ('old_x', c_int32), ('old_y', c_int32), ('buttons', c_ubyte), ('command', c_ubyte), ] class MessageWindowFocusChange(MessageEx): """Message indicating the focus state of a window has changed.""" type_val = Message.MSG_WINDOW_FOCUS_CHANGE class data_struct(Structure): _fields_ = [ ('wid', c_uint32), ('focused', c_int), ] class MessageWindowResize(MessageEx): """Message indicating the server wishes to resize this window.""" type_val = Message.MSG_RESIZE_OFFER class data_struct(Structure): _fields_ = [ ('wid', c_uint32), ('width', c_uint32), ('height', c_uint32), ('bufid', c_uint32), ] class MessageWindowAdvertisement(MessageEx): """Message containing information about a foreign window.""" type_val = Message.MSG_WINDOW_ADVERTISE class data_struct(Structure): _fields_ = [ ('wid', c_uint32), ('flags', c_uint32), ('offsets', c_uint16 * 5), ('size', c_uint32), ('strings', c_byte * 0), ] @property def name(self): return string_at(addressof(self.strings) + self.offsets[0]).decode('utf-8') @property def icon(self): return string_at(addressof(self.strings) + self.offsets[1]).decode('utf-8') class MessageWindowMove(MessageEx): """Message received when a window has moved containing its new coordinates.""" type_val = Message.MSG_WINDOW_MOVE class data_struct(Structure): _fields_ = [ ('wid', c_uint32), ('x', c_int32), ('y', c_int32), ] class Yutani(object): """Base Yutani communication class. Must be initialized to start a connection.""" class _yutani_t(Structure): _fields_ = [ ("sock", c_void_p), # File pointer ("display_width", c_size_t), ("display_height", c_size_t), ("windows", c_void_p), # hashmap ("queued", c_void_p), # list ("server_ident", c_char_p), ] def __init__(self): global yutani_lib global yutani_ctx global yutani_gfx_lib global yutani_menu_lib if not yutani_lib: yutani_lib = CDLL("libtoaru_yutani.so") if not yutani_gfx_lib: yutani_gfx_lib = CDLL("libtoaru_graphics.so") if not yutani_menu_lib: yutani_menu_lib = CDLL("libtoaru_menu.so") self._ptr = cast(yutani_lib.yutani_init(), POINTER(self._yutani_t)) if not self._ptr: raise ConnectionRefusedError("Could not connect to compositor.") yutani_ctx = self self._fileno = _libc.fileno(self._ptr.contents.sock) def process_menus(self, event): return yutani_menu_lib.menu_process_event(self._ptr, event._ptr) def poll(self, sync=True): """Poll for an event message.""" if sync: result = yutani_lib.yutani_poll(self._ptr) else: result = yutani_lib.yutani_poll_async(self._ptr) if not result: return None msg_ptr = cast(result, POINTER(Message._yutani_msg_t)) msg_class = _message_types.get(msg_ptr.contents.type, Message) return msg_class(msg_ptr) def wait_for(self, message): """Wait for a particular kind of message to be delivered.""" result = yutani_lib.yutani_wait_for(self._ptr, message) msg_ptr = cast(result, POINTER(Message._yutani_msg_t)) msg_class = _message_types.get(msg_ptr.contents.type, Message) return msg_class(msg_ptr) def subscribe(self): """Subscribe to window information changes.""" yutani_lib.yutani_subscribe_windows(self._ptr) def unsubscribe(self): """Unsubscribe from window information changes.""" yutani_lib.yutani_unsubscribe_windows(self._ptr) def query_windows(self): """Request a window subsription list.""" yutani_lib.yutani_query_windows(self._ptr) def focus_window(self, wid): """Request that the server change the focused window to the window with the specified wid.""" yutani_lib.yutani_focus_window(self._ptr, wid) def session_end(self): """Request the end of the user session.""" yutani_lib.yutani_session_end(self._ptr) def key_bind(self, keycode, modifiers, flags): """Set global key binding.""" yutani_lib.yutani_key_bind(self._ptr, keycode, modifiers, flags) def fileno(self): """Act file-like and return our file descriptor number.""" return self._fileno def query(self): """Asynchronously check if data is available.""" return yutani_lib.yutani_query(self._ptr) class KeybindFlag(object): """Flags for global key bindings.""" BIND_PASSTHROUGH = 0 # The key bind should be received by other clients. BIND_STEAL = 1 # The key bind should stop after being processed here. class WindowShape(object): """Window shaping modes for Window.update_shape.""" # These are actually values representing the minimum required # alpha value for a pixel to be registered as part of the given window. # 256 is more than the maximum alpha value, so all clicks will pass through. # 0 will catch every pixel, even if it is entirely transparent. THRESHOLD_NONE = 0 THRESHOLD_CLEAR = 1 THRESHOLD_HALF = 127 THRESHOLD_ANY = 255 THRESHOLD_PASSTHROUGH = 256 class WindowStackOrder(object): """Window stack order options.""" ZORDER_MAX = 0xFFFF ZORDER_TOP = 0xFFFF ZORDER_BOTTOM = 0x0000 class WindowFlag(object): """Flags for window creation.""" FLAG_NO_STEAL_FOCUS = (1 << 0) # Don't steal focus on window creation. FLAG_DISALLOW_DRAG = (1 << 1) # Don't allow this window to be dragged. FLAG_DISALLOW_RESIZE = (1 << 2) # Don't allow this window to be resized. FLAG_ALT_ANIMATION = (1 << 3) # Use the alternate animation when mapping and unmapping. class MouseButton(object): """Mouse button flags.""" BUTTON_LEFT = 0x01 BUTTON_RIGHT = 0x02 BUTTON_MIDDLE = 0x04 SCROLL_UP = 0x10 SCROLL_DOWN = 0x20 class MouseEvent(object): """Mouse event types.""" CLICK = 0 DRAG = 1 RAISE = 2 DOWN = 3 MOVE = 4 LEAVE = 5 ENTER = 6 class CursorType(object): """Cursor types for show_mouse.""" RESET = -1 HIDE = 0 NORMAL = 1 DRAG = 2 RESIZE_VERTICAL = 3 RESIZE_HORIZONTAL = 4 RESIZE_UP_DOWN = 5 RESIZE_DOWN_UP = 6 class Sprite(object): class _sprite_t(Structure): _fields_ = [ ('width', c_uint16), ('height', c_uint16), ('bitmap', POINTER(c_char)), ('masks', POINTER(c_char)), ('blank', c_uint32), ('alpha', c_uint8), ] @property def width(self): return self._ptr.contents.width @property def height(self): return self._ptr.contents.height @property def buffer(self): return self._ptr.contents.bitmap def get_cairo_surface(self): class Derp(object): pass self._gfx = Derp() self._gfx.contents = Derp() self._gfx.contents.backbuffer = self._ptr.contents.bitmap self._cairo_surface = Window.get_cairo_surface(self) return self._cairo_surface def from_file(path): global yutani_gfx_lib if not yutani_gfx_lib: yutani_gfx_lib = CDLL("libtoaru_graphics.so") s = Sprite() s._contents = create_string_buffer(sizeof(Sprite._sprite_t)) s._ptr = cast(addressof(s._contents), POINTER(Sprite._sprite_t)) yutani_gfx_lib.load_sprite.argtypes = [c_void_p, c_char_p] yutani_gfx_lib.load_sprite(s._ptr, path.encode('utf-8')) return s def create(width, height): s = Sprite() s._ptr = yutani_gfx_lib.create_sprite(width, height) return s def free(self): yutani_gfx_lib.sprite_free(self._ptr) class GraphicsBuffer(object): """Generic buffer for rendering.""" def __init__(self, width, height): self.width = width self.height = height self._sprite = yutani_gfx_lib.create_sprite(width,height,2) self._gfx = cast(yutani_gfx_lib.init_graphics_sprite(self._sprite),POINTER(Window._gfx_context_t)) def get_cairo_surface(self): return Window.get_cairo_surface(self) def get_value(self,x,y): return cast(self._gfx.contents.backbuffer,POINTER(c_uint32))[self.width * y + x] def destroy(self): yutani_gfx_lib.sprite_free(self._sprite) _libc.free(self._gfx) class Window(object): """Yutani Window object.""" class _yutani_window_t(Structure): _fields_ = [ ("wid", c_uint), ("width", c_uint32), ("height", c_uint32), ("buffer", POINTER(c_uint8)), ("bufid", c_uint32), ("focused", c_uint8), ("oldbufid", c_uint32), ("userdata", c_void_p), ("x", c_int32), ("y", c_int32), ("decorator_flags", c_uint32), ("ctx", c_void_p), ] class _gfx_context_t(Structure): _fields_ = [ ('width', c_uint16), ('height', c_uint16), ('depth', c_uint16), ('size', c_uint32), ('buffer', POINTER(c_char)), ('backbuffer', POINTER(c_char)), ] def __init__(self, width, height, flags=0, title=None, icon=None, doublebuffer=False): if not yutani_ctx: raise ValueError("Not connected.") self._ptr = cast(yutani_lib.yutani_window_create_flags(yutani_ctx._ptr, width, height, flags), POINTER(self._yutani_window_t)) yutani_windows[self.wid] = self self.doublebuffer = doublebuffer if doublebuffer: self._gfx = cast(yutani_lib.init_graphics_yutani_double_buffer(self._ptr), POINTER(self._gfx_context_t)) else: self._gfx = cast(yutani_lib.init_graphics_yutani(self._ptr), POINTER(self._gfx_context_t)) if title: self.set_title(title, icon) self.closed = False def get_cairo_surface(self): """Obtain a pycairo.ImageSurface representing the window backbuffer.""" global _cairo_lib global _cairo_module global _cairo_module_lib if not _cairo_lib: _cairo_lib = CDLL('libcairo.so') _cairo_module = importlib.import_module('_cairo') _cairo_module_lib = CDLL(_cairo_module.__file__) buffer = self._gfx.contents.backbuffer width = self.width height = self.height format = _cairo_module.FORMAT_ARGB32 # int cairo_format_stride_for_width(cairo_format_t format, int width) cfsfw = _cairo_lib.cairo_format_stride_for_width cfsfw.argtypes = [c_int, c_int] cfsfw.restype = c_int # stride = cairo_format_stride_for_width(format, width) stride = cfsfw(format, width) # cairo_surface_t * cairo_image_surface_create_for_data(unsigned char * data, cairo_format_t format, int ...) ciscfd = _cairo_lib.cairo_image_surface_create_for_data ciscfd.argtypes = [POINTER(c_char), c_int, c_int, c_int, c_int] ciscfd.restype = c_void_p # surface = cairo_image_surface_create_for_data(buffer,format,width,height,stride) surface = ciscfd(buffer,format,width,height,stride) # PyObject * PycairoSurface_FromSurface(cairo_surface_t * surface, PyObject * base) pcsfs = _cairo_module_lib.PycairoSurface_FromSurface pcsfs.argtypes = [c_void_p, c_int] pcsfs.restype = py_object # return PycairoSurface_FromSurface(surface, NULL) return pcsfs(surface, 0) def set_title(self, title, icon=None): """Advertise this window with the given title and optional icon string.""" self.title = title self.icon = icon title_string = title.encode('utf-8') if title else None icon_string = icon.encode('utf-8') if icon else None if not icon: yutani_lib.yutani_window_advertise(yutani_ctx._ptr, self._ptr, title_string) else: yutani_lib.yutani_window_advertise_icon(yutani_ctx._ptr, self._ptr, title_string, icon_string) def buffer(self): """Obtain a reference to the graphics backbuffer representing this window's canvas.""" return cast(self._gfx.contents.backbuffer, POINTER(c_uint32)) def flip(self, region=None): """Flip the window buffer when double buffered and inform the server of updates.""" if self.doublebuffer: yutani_gfx_lib.flip(self._gfx) yutani_lib.yutani_flip(yutani_ctx._ptr, self._ptr) def close(self): """Close the window.""" if self.wid in yutani_windows: del yutani_windows[self.wid] self.closed = True yutani_lib.yutani_close(yutani_ctx._ptr, self._ptr) yutani_lib.release_graphics_yutani(self._gfx) def move(self, x, y): """Move the window to the requested location.""" yutani_lib.yutani_window_move(yutani_ctx._ptr, self._ptr, x, y) def resize_accept(self, w, h): """Inform the server that we have accepted the offered resize.""" yutani_lib.yutani_window_resize_accept(yutani_ctx._ptr, self._ptr, w, h) def resize_done(self): """Inform the server that we are done resizing and the new window may be displayed.""" yutani_lib.yutani_window_resize_done(yutani_ctx._ptr, self._ptr) def resize_offer(self, width, height): """Offer alternative dimensions in response to a server offer.""" yutani_lib.yutani_window_resize_offer(yutani_ctx._ptr, self._ptr, width, height) def resize(self, width, height): """Request that a window be resized to the given dimensions.""" yutani_lib.yutani_window_resize(yutani_ctx._ptr, self._ptr, width, height) def reinit(self): """Reinitialize the internal graphics context for the window. Should be done after a resize_accept.""" yutani_lib.reinit_graphics_yutani(self._gfx, self._ptr) def fill(self, color): """Fill the entire window with a given color.""" yutani_gfx_lib.draw_fill(self._gfx, color) def update_shape(self, mode): """Set the mouse passthrough / window shaping mode. Does not affect appearance of window.""" yutani_lib.yutani_window_update_shape(yutani_ctx._ptr, self._ptr, mode) def set_stack(self, stack): """Set the stack layer for the window.""" yutani_lib.yutani_set_stack(yutani_ctx._ptr, self._ptr, stack) def drag_start(self): """Begin window drag.""" yutani_lib.yutani_window_drag_start(yutani_ctx._ptr, self._ptr) @property def x(self): return self._ptr.contents.x @property def y(self): return self._ptr.contents.y @property def width(self): return self._ptr.contents.width @property def height(self): return self._ptr.contents.height # TODO: setters for width/height call resize? @property def wid(self): """The identifier of the window.""" return self._ptr.contents.wid @property def focused(self): """Whether the window is current focused.""" return self._ptr.contents.focused @focused.setter def focused(self, value): self._ptr.contents.focused = value class Decor(object): """Class for rendering decorations with the system decorator library.""" EVENT_OTHER = 1 EVENT_CLOSE = 2 EVENT_RESIZE = 3 EVENT_MAXIMIZE = 4 EVENT_RIGHT = 5 class decor_bound(Structure): _fields_ = [ ("top_height", c_int), ("bottom_height", c_int), ("left_width", c_int), ("right_width", c_int), ("width", c_int), ("height", c_int), ] def __init__(self): self.lib = CDLL("libtoaru_decorations.so") self.lib.init_decorations() self._bounds = self.decor_bound() self._decor_get_bounds = cast(c_void_p.in_dll(self.lib, "decor_get_bounds").value, CFUNCTYPE(c_int, c_void_p, c_void_p)) def _get_bounds(self, window): self._decor_get_bounds(window._ptr if window else None, byref(self._bounds)) def bounds(self,window=None): self._get_bounds(window) return self._bounds def width(self,window=None): """The complete width of the left and right window borders.""" self._get_bounds(window) return int(self._bounds.width) def height(self,window=None): """The complete height of the top and bottom window borders.""" self._get_bounds(window) return int(self._bounds.height) def top_height(self,window=None): """The height of the top edge of the decorations.""" self._get_bounds(window) return int(self._bounds.top_height) def bottom_height(self,window=None): """The height of the bottom edge of the decorations.""" self._get_bounds(window) return int(self._bounds.bottom_height) def left_width(self,window=None): """The width of the left edge of the decorations.""" self._get_bounds(window) return int(self._bounds.left_width) def right_width(self,window=None): """The width of the right edge of the decorations.""" self._get_bounds(window) return int(self._bounds.right_width) def render(self, window, title=None): """Render decorations on this window. If a title is not provided, it will be retreived from the window object.""" if not title: title = window.title title_string = title.encode('utf-8') if title else None self.lib.render_decorations(window._ptr, window._gfx, title_string) def handle_event(self, msg): """Let the decorator library handle an event. Usually passed mouse events.""" return self.lib.decor_handle_event(yutani_ctx._ptr, msg._ptr) def show_menu(self, window, event): self.lib.decor_show_default_menu(window._ptr, window.x + event.new_x, window.y + event.new_y) # Demo follows. if __name__ == '__main__': # Connect to the server. Yutani() # Initialize the decoration library. d = Decor() # Create a new window. w = Window(300+d.width(),300+d.height(),title="Python Demo") # Since this is Python, we can attach data to our window, such # as its internal width (excluding the decorations). w.int_width = 300 w.int_height = 300 # We can set window shaping... #w.update_shape(WindowShape.THRESHOLD_HALF) # Move the window... w.move(100, 100) def draw_decors(): """Render decorations for the window.""" d.render(w) def draw_window(): """Draw the window.""" w.fill(0xFFFF00FF) draw_decors() def finish_resize(msg): """Accept a resize.""" # Tell the server we accept. w.resize_accept(msg.width, msg.height) # Reinitialize internal graphics context. w.reinit() # Calculate new internal dimensions. w.int_width = msg.width - d.width() w.int_height = msg.height - d.height() # Redraw the window buffer. draw_window() # Inform the server we are done. w.resize_done() # And flip. w.flip() # Do an initial draw. draw_window() # Don't forget to flip. Our single-buffered window only needs # the Yutani flip call, but the API will perform both if needed. w.flip() while 1: # Poll for events. msg = yutani_ctx.poll() if msg.type == Message.MSG_SESSION_END: # All applications should attempt to exit on SESSION_END. w.close() break elif msg.type == Message.MSG_KEY_EVENT: # Print key events for debugging. print(f'W({msg.wid}) key {msg.event.key} {msg.event.action}') if msg.event.key == b'q': # Convention says to close windows when 'q' is pressed, # unless we're using keyboard input "normally". w.close() break elif msg.type == Message.MSG_WINDOW_FOCUS_CHANGE: # If the focus of our window changes, redraw the borders. if msg.wid == w.wid: # This attribute is stored in the underlying struct # and used by the decoration library to pick which # version of the decorations to draw for the window. w.focused = msg.focused draw_decors() w.flip() elif msg.type == Message.MSG_RESIZE_OFFER: # Resize the window. finish_resize(msg) elif msg.type == Message.MSG_WINDOW_MOUSE_EVENT: # Handle mouse events, first by passing them # to the decorator library for processing. if d.handle_event(msg) == Decor.EVENT_CLOSE: # Close the window when the 'X' button is clicked. w.close() break else: # For events that didn't get handled by the decorations, # print a debug message with details. print(f'W({msg.wid}) mouse {msg.new_x},{msg.new_y}')