Last active
December 13, 2016 05:49
-
-
Save klange/af0903325077c7ff7a0814a9dc8d26e9 to your computer and use it in GitHub Desktop.
This file contains 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/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