Skip to content

Instantly share code, notes, and snippets.

@klange
Last active December 13, 2016 05:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save klange/af0903325077c7ff7a0814a9dc8d26e9 to your computer and use it in GitHub Desktop.
Save klange/af0903325077c7ff7a0814a9dc8d26e9 to your computer and use it in GitHub Desktop.
#!/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 *
yutani_lib = None
yutani_gfx_lib = None
yutani_ctx = None
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_WELCOME = 0x00010001
MSG_WINDOW_INIT = 0x00010002
def __init__(self, msg):
self._ptr = msg
@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()
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 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 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
yutani_lib = CDLL("libtoaru-yutani.so")
yutani_gfx_lib = CDLL("libtoaru-graphics.so")
self._ptr = cast(yutani_lib.yutani_init(), POINTER(self._yutani_t))
yutani_ctx = self
def poll(self):
"""Poll synchronously for an event message."""
msg_ptr = cast(yutani_lib.yutani_poll(self._ptr), POINTER(Message._yutani_msg_t))
msg_class = _message_types.get(msg_ptr.contents.type, Message)
return msg_class(msg_ptr)
class WindowShape(object):
"""Window shaping modes for Window.update_shape."""
THRESHOLD_NONE = 0
THRESHOLD_CLEAR = 1
THRESHOLD_HALF = 127
THRESHOLD_ANY = 255
THRESHOLD_PASSTHROUGH = 256
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),
]
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))
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)
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."""
yutani_lib.yutani_close(yutani_ctx._ptr, self._ptr)
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 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)
@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
def __init__(self):
self.lib = CDLL("libtoaru-decorations.so")
self.lib.init_decorations()
def width(self):
"""The complete width of the left and right window borders."""
return int(self.lib.decor_width())
def height(self):
"""The complete height of the top and bottom window borders."""
return int(self.lib.decor_height())
def top_height(self):
"""The height of the top edge of the decorations."""
return c_uint32.in_dll(self.lib, "decor_top_height").value
def bottom_height(self):
"""The height of the bottom edge of the decorations."""
return c_uint32.in_dll(self.lib, "decor_bottom_height").value
def left_width(self):
"""The width of the left edge of the decorations."""
return c_uint32.in_dll(self.lib, "decor_left_width").value
def right_width(self):
"""The width of the right edge of the decorations."""
return c_uint32.in_dll(self.lib, "decor_right_width").value
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)
# Demo follows.
if __name__ == '__main__':
# Connect to the server.
Yutani()
# Initialize the decoration library.
d = Decor()
# Create a new window.
w = Window(200+d.width(),200+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 = 200
w.int_height = 200
# 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}')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment