Import Python userspace stuff
This commit is contained in:
parent
206cf07cc2
commit
3538982922
36
Makefile
36
Makefile
@ -238,6 +238,13 @@ USER_LIBFILES = $(shell find userspace -wholename '*/lib/*' -name '*.c')
|
||||
|
||||
LIBC=hdd/usr/lib/libc.so
|
||||
|
||||
# PYthon sources
|
||||
PYTHON_LIBS = $(shell find userspace -wholename '*/lib/*' -name '*.py')
|
||||
PYTHON_BINS = $(shell find userspace -wholename '*/bin/*' -name '*.py')
|
||||
|
||||
PYTHON_FILES = $(foreach file,$(PYTHON_LIBS),$(patsubst %.py,hdd/usr/python/lib/python3.6/%.py,$(notdir ${file})))
|
||||
PYTHON_FILES += $(foreach file,$(PYTHON_BINS),$(patsubst %.py,hdd/bin/%.py,$(notdir ${file})))
|
||||
|
||||
# Userspace output files (so we can define metatargets)
|
||||
NONTEST_C = $(foreach f,$(USER_CFILES),$(if $(findstring /tests/,$f),,$f))
|
||||
NONTEST_CXX = $(foreach f,$(USER_CXXFILES),$(if $(findstring /tests/,$f),,$f))
|
||||
@ -254,8 +261,6 @@ USERSPACE += $(foreach file,$(USER_CSTATICFILES),$(patsubst %.static.c,hdd/bin/%
|
||||
USERSPACE += $(foreach file,$(USER_LIBFILES),$(patsubst %.c,hdd/usr/lib/libtoaru-%.so,$(notdir ${file})))
|
||||
USERSPACE += $(LIBC) hdd/bin/init hdd/lib/ld.so
|
||||
|
||||
CORE_LIBS = $(patsubst %.c,%.o,$(wildcard userspace/lib/*.c))
|
||||
|
||||
# Init must be built static at the moment.
|
||||
hdd/bin/init: userspace/core/init.c
|
||||
@${BEG} "CC" "$< (static)"
|
||||
@ -290,18 +295,6 @@ $1: $2 $(shell util/auto-dep.py --deps $2) $(LIBC)
|
||||
endef
|
||||
$(foreach file,$(USER_CXXFILES),$(eval $(call user-cxx-rule,$(patsubst %.c++,hdd/bin/%,$(notdir ${file})),${file})))
|
||||
|
||||
hdd/usr/lib/libtoaru.a: ${CORE_LIBS}
|
||||
@${BEG} "AR" "$@"
|
||||
@${AR} rcs $@ ${CORE_LIBS}
|
||||
@mkdir -p hdd/usr/include/toaru
|
||||
@cp userspace/lib/*.h hdd/usr/include/toaru/
|
||||
@${END} "AR" "$@"
|
||||
|
||||
hdd/usr/lib/libnetwork.a: userspace/lib/network.o
|
||||
@${BEG} "AR" "$@"
|
||||
@${AR} rcs $@ ${CORE_LIBS}
|
||||
@${END} "AR" "$@"
|
||||
|
||||
hdd/usr/lib:
|
||||
@mkdir -p hdd/usr/lib
|
||||
|
||||
@ -362,6 +355,17 @@ $(eval $(call basic-so-wrapper,pixman-1,-lm))
|
||||
$(eval $(call basic-so-wrapper,cairo,-lpixman-1 -lpng15 -lfreetype))
|
||||
$(eval $(call basic-so-wrapper,freetype,-lz))
|
||||
|
||||
# Python parts of userspace
|
||||
|
||||
hdd/usr/python/lib/python3.6:
|
||||
@mkdir -p $@
|
||||
|
||||
hdd/bin/%.py: userspace/py/bin/%.py
|
||||
@cp $< $@
|
||||
|
||||
hdd/usr/python/lib/python3.6/%.py: userspace/py/lib/%.py hdd/usr/python/lib/python3.6
|
||||
@cp $< $@
|
||||
|
||||
####################
|
||||
# Hard Disk Images #
|
||||
####################
|
||||
@ -370,7 +374,7 @@ $(eval $(call basic-so-wrapper,freetype,-lz))
|
||||
GENEXT = genext2fs
|
||||
DISK_SIZE = `util/disk_size.sh`
|
||||
|
||||
toaruos-disk.img: ${USERSPACE} util/devtable
|
||||
toaruos-disk.img: ${USERSPACE} util/devtable ${PYTHON_FILES}
|
||||
@${BEG} "hdd" "Generating a Hard Disk image..."
|
||||
@-rm -f toaruos-disk.img
|
||||
@${GENEXT} -B 4096 -d hdd -D util/devtable -U -b ${DISK_SIZE} -N 4096 toaruos-disk.img ${ERRORS}
|
||||
@ -405,7 +409,7 @@ BLACKLIST += hdd/usr/share/wallpapers/southbay.png
|
||||
BLACKLIST += hdd/usr/share/wallpapers/yokohama.png
|
||||
BLACKLIST += hdd/usr/share/wallpapers/yosemite.png
|
||||
|
||||
_cdrom/ramdisk.img: ${NONTEST} hdd/usr/share/wallpapers util/devtable hdd/usr/share/terminfo/t/toaru _cdrom _cdrom/mod
|
||||
_cdrom/ramdisk.img: ${NONTEST} hdd/usr/share/wallpapers util/devtable hdd/usr/share/terminfo/t/toaru _cdrom _cdrom/mod ${PYTHON_FILES}
|
||||
@${BEG} "ext" "Generating a ramdisk image..."
|
||||
@rm -f $(filter-out ${NONTEST},${USERSPACE})
|
||||
@rm -f ${BLACKLIST}
|
||||
|
195
userspace/py/bin/multiline-text-demo.py
Executable file
195
userspace/py/bin/multiline-text-demo.py
Executable file
@ -0,0 +1,195 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
Demo of rudimentary (LTR, simple fonts only) text layout.
|
||||
"""
|
||||
import sys
|
||||
|
||||
import yutani
|
||||
import toaru_fonts
|
||||
import text_region
|
||||
|
||||
ipsum = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque orci mauris, vulputate at purus id, hendrerit volutpat est. Mauris vel nunc nulla. Aliquam justo massa, lacinia ac consequat sed, maximus mollis dolor. Nulla et risus sollicitudin, fermentum diam sed, efficitur dui. これは日本語です。 Ut eu mauris vel lorem ornare finibus vitae finibus massa. Süspendísse dàpibús ipsum eu accumsañ euismod. Proin pellentesque tellus vehicula convallis euismod. Donec aliquam pretium gravida. Quisque laoreet ut dolor non tincidunt. Mauris ultrices magna at ligula dictum accumsan. Nunc eleifend sollicitudin purus. In arcu orci, interdum sed ultricies id, tempor vel massa. Morbi bibendum nunc sed felis gravida tristique. Praesent vestibulum sem id mi pretium posuere.
|
||||
|
||||
Proin maximus bibendum porta. Vestibulum cursus et augue at fermentum. In tincidunt, risus a placerat sollicitudin, nibh nisl tincidunt quam, laoreet tincidunt massa erat id turpis. Sed nulla augue, aliquam sit amet velit id, interdum rutrum lectus. Morbi metus tellus, commodo vitae facilisis sed, porttitor sed sem. Integer dignissim vel sem vitae euismod. Nullam et nunc sit amet felis iaculis mollis. Donec ac metus ex. Sed suscipit felis arcu, et tincidunt magna fringilla eu. Sed hendrerit, odio at condimentum tempus, metus felis volutpat metus, sed gravida lorem mi sit amet turpis. Etiam porta sodales vehicula. Integer iaculis eros sed interdum convallis. Sed rhoncus orci ligula. Proin euismod lorem ut nisl vulputate, a hendrerit felis rhoncus. Ut efficitur placerat ipsum, eu consequat nisl fermentum eget. Pellentesque id volutpat arcu, ac molestie leo."""
|
||||
|
||||
rich_demo="""<b>This</b> is a demon<i>stration</i> of rich text in <mono>ToaruOS</mono>. このデモは<color 0x0000FF>日本語</color>も出来ます。 At the moment, <i>this</i> <color 0xFF0000>demo</color> only supports a few markup options in an HTML-esque syntax.\nWe <b>can <i>combine <color 0x00BB00>multiple</color> different</i> options</b>."""
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Connect to the server.
|
||||
yutani.Yutani()
|
||||
|
||||
# Initialize the decoration library.
|
||||
d = yutani.Decor()
|
||||
|
||||
# Create a new window.
|
||||
w = yutani.Window(600+d.width(),150+d.height(),title="Text Layout Demo",doublebuffer=True)
|
||||
|
||||
# We can set window shaping...
|
||||
w.update_shape(yutani.WindowShape.THRESHOLD_HALF)
|
||||
|
||||
pad = 4
|
||||
|
||||
tr = text_region.TextRegion(d.left_width() + pad,d.top_height() + pad, w.width - d.width() - pad * 2, w.height - d.height() - pad * 2)
|
||||
tr.set_line_height(20)
|
||||
bold = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF_BOLD,13)
|
||||
blue = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF,13,0xFF0000FF)
|
||||
#with open('/usr/share/licenses') as f:
|
||||
# tr.set_text(f.read())
|
||||
# for unit in tr.text_units:
|
||||
# if unit.string.startswith("http://") or unit.string.startswith("https://"):
|
||||
# unit.set_font(blue)
|
||||
# if unit.string == "Software":
|
||||
# unit.set_font(bold)
|
||||
# tr.reflow()
|
||||
#tr.set_text(ipsum)
|
||||
if len(sys.argv) > 1:
|
||||
with open(sys.argv[1]) as f:
|
||||
tr.set_richtext(f.read())
|
||||
else:
|
||||
tr.set_richtext(rich_demo)
|
||||
|
||||
# Move the window...
|
||||
w.move(100, 100)
|
||||
|
||||
def draw_decors():
|
||||
"""Render decorations for the window."""
|
||||
d.render(w)
|
||||
|
||||
def draw_window():
|
||||
"""Draw the window."""
|
||||
def rgb(r,g,b):
|
||||
return yutani.yutani_gfx_lib.rgb(r,g,b)
|
||||
w.fill(rgb(214,214,214))
|
||||
|
||||
tr.draw(w)
|
||||
|
||||
draw_decors()
|
||||
|
||||
def finish_resize(msg):
|
||||
"""Accept a resize."""
|
||||
|
||||
if msg.width < 100 or msg.height < 100:
|
||||
w.resize_offer(max(msg.width,100),max(msg.height,100))
|
||||
return
|
||||
|
||||
# Tell the server we accept.
|
||||
w.resize_accept(msg.width, msg.height)
|
||||
|
||||
# Reinitialize internal graphics context.
|
||||
w.reinit()
|
||||
|
||||
tr.resize(msg.width - d.width() - pad * 2, msg.height - d.height() - pad * 2)
|
||||
|
||||
# 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.yutani_ctx.poll()
|
||||
if msg.type == yutani.Message.MSG_SESSION_END:
|
||||
# All applications should attempt to exit on SESSION_END.
|
||||
w.close()
|
||||
break
|
||||
elif msg.type == yutani.Message.MSG_KEY_EVENT:
|
||||
if msg.event.action != 0x01:
|
||||
continue
|
||||
if msg.event.key == b'c':
|
||||
tr.set_alignment(2)
|
||||
draw_window()
|
||||
w.flip()
|
||||
elif msg.event.key == b'l':
|
||||
tr.set_alignment(0)
|
||||
draw_window()
|
||||
w.flip()
|
||||
elif msg.event.key == b'r':
|
||||
tr.set_alignment(1)
|
||||
draw_window()
|
||||
w.flip()
|
||||
elif msg.event.key == b'm':
|
||||
tr.set_valignment(2)
|
||||
draw_window()
|
||||
w.flip()
|
||||
elif msg.event.key == b'b':
|
||||
tr.set_valignment(1)
|
||||
draw_window()
|
||||
w.flip()
|
||||
elif msg.event.key == b't':
|
||||
tr.set_valignment(0)
|
||||
draw_window()
|
||||
w.flip()
|
||||
elif msg.event.keycode == 2015:
|
||||
tr.scroll = 0
|
||||
draw_window()
|
||||
w.flip()
|
||||
elif msg.event.keycode == 2016:
|
||||
tr.scroll = len(tr.lines)-tr.visible_lines()
|
||||
draw_window()
|
||||
w.flip()
|
||||
elif msg.event.key == b' ' or msg.event.keycode == 2013:
|
||||
tr.scroll += int(tr.visible_lines() / 2)
|
||||
if tr.scroll > len(tr.lines)-tr.visible_lines():
|
||||
tr.scroll = len(tr.lines)-tr.visible_lines()
|
||||
draw_window()
|
||||
w.flip()
|
||||
elif msg.event.keycode == 2014:
|
||||
tr.scroll -= int(tr.visible_lines() / 2)
|
||||
if tr.scroll < 0:
|
||||
tr.scroll = 0
|
||||
draw_window()
|
||||
w.flip()
|
||||
elif msg.event.key == b'o':
|
||||
tr.set_one_line()
|
||||
tr.set_ellipsis()
|
||||
draw_window()
|
||||
w.flip()
|
||||
elif msg.event.key == b'i':
|
||||
tr.set_one_line(False)
|
||||
tr.set_ellipsis("")
|
||||
draw_window()
|
||||
w.flip()
|
||||
elif 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 == yutani.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 == yutani.Message.MSG_RESIZE_OFFER:
|
||||
# Resize the window.
|
||||
finish_resize(msg)
|
||||
elif msg.type == yutani.Message.MSG_WINDOW_MOUSE_EVENT:
|
||||
# Handle mouse events, first by passing them
|
||||
# to the decorator library for processing.
|
||||
if d.handle_event(msg) == yutani.Decor.EVENT_CLOSE:
|
||||
# Close the window when the 'X' button is clicked.
|
||||
w.close()
|
||||
break
|
||||
else:
|
||||
e = tr.click(msg.new_x,msg.new_y)
|
||||
if e and msg.command == 0:
|
||||
new_font = toaru_fonts.Font(e.font.font_number,e.font.font_size,0xFFFF0000)
|
||||
e.set_font(new_font)
|
||||
draw_window()
|
||||
w.flip()
|
||||
|
128
userspace/py/bin/yutani-cairo-demo.py
Executable file
128
userspace/py/bin/yutani-cairo-demo.py
Executable file
@ -0,0 +1,128 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
Demo application that renders into a Yutani window with Cairo.
|
||||
"""
|
||||
import yutani
|
||||
import cairo
|
||||
import math
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Connect to the server.
|
||||
yutani.Yutani()
|
||||
|
||||
# Initialize the decoration library.
|
||||
d = yutani.Decor()
|
||||
|
||||
# Create a new window.
|
||||
w = yutani.Window(200+d.width(),200+d.height(),title="Python Cairo 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(yutani.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."""
|
||||
surface = w.get_cairo_surface()
|
||||
|
||||
WIDTH, HEIGHT = w.width, w.height
|
||||
|
||||
ctx = cairo.Context(surface)
|
||||
ctx.scale (WIDTH/1.0, HEIGHT/1.0)
|
||||
|
||||
pat = cairo.LinearGradient (0.0, 0.0, 0.0, 1.0)
|
||||
pat.add_color_stop_rgba (1, 0, 0, 0, 1)
|
||||
pat.add_color_stop_rgba (0, 1, 1, 1, 1)
|
||||
|
||||
ctx.rectangle (0,0,1,1)
|
||||
ctx.set_source (pat)
|
||||
ctx.fill ()
|
||||
|
||||
pat = cairo.RadialGradient (0.45, 0.4, 0.1,
|
||||
0.4, 0.4, 0.5)
|
||||
pat.add_color_stop_rgba (0, 1, 1, 1, 1)
|
||||
pat.add_color_stop_rgba (1, 0, 0, 0, 1)
|
||||
|
||||
ctx.set_source (pat)
|
||||
ctx.arc (0.5, 0.5, 0.3, 0, 2 * math.pi)
|
||||
ctx.fill ()
|
||||
|
||||
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.yutani_ctx.poll()
|
||||
if msg.type == yutani.Message.MSG_SESSION_END:
|
||||
# All applications should attempt to exit on SESSION_END.
|
||||
w.close()
|
||||
break
|
||||
elif msg.type == yutani.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 == yutani.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 == yutani.Message.MSG_RESIZE_OFFER:
|
||||
# Resize the window.
|
||||
finish_resize(msg)
|
||||
elif msg.type == yutani.Message.MSG_WINDOW_MOUSE_EVENT:
|
||||
# Handle mouse events, first by passing them
|
||||
# to the decorator library for processing.
|
||||
if d.handle_event(msg) == yutani.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}')
|
100
userspace/py/lib/readline.py
Normal file
100
userspace/py/lib/readline.py
Normal file
@ -0,0 +1,100 @@
|
||||
import ctypes
|
||||
|
||||
auto_history = True
|
||||
|
||||
py = ctypes.CDLL("libpython3.6m.so")
|
||||
rline_lib = ctypes.CDLL("libtoaru-rline.so")
|
||||
|
||||
readline_func = ctypes.c_void_p.in_dll(py,"PyOS_ReadlineFunctionPointer")
|
||||
t = ctypes.c_char_p.in_dll(rline_lib,"rline_for_python")
|
||||
readline_func.value = ctypes.addressof(t)
|
||||
|
||||
# Change exit string to "exit()"
|
||||
exit_string_lib = ctypes.c_char_p.in_dll(rline_lib,"rline_exit_string")
|
||||
exit_string = ctypes.c_char_p(b"")
|
||||
exit_string_lib.value = exit_string.value
|
||||
|
||||
def parse_and_bind(s):
|
||||
pass
|
||||
|
||||
def read_init_file(filename=None):
|
||||
pass
|
||||
|
||||
def get_line_buffer():
|
||||
return None
|
||||
|
||||
def insert_text(string):
|
||||
return None
|
||||
|
||||
def redisplay():
|
||||
pass
|
||||
|
||||
def read_history_file(filename):
|
||||
pass
|
||||
|
||||
def write_history_file(filename):
|
||||
pass
|
||||
|
||||
def append_history_file(nelements,filename=None):
|
||||
pass
|
||||
|
||||
def get_history_length():
|
||||
return 0
|
||||
|
||||
def set_history_length(length):
|
||||
pass
|
||||
|
||||
def clear_history():
|
||||
pass
|
||||
|
||||
def get_current_history_length():
|
||||
return ctypes.c_int.in_dll(rline_lib,"rline_history_count").value
|
||||
|
||||
def get_history_item(index):
|
||||
if index < 1 or index > get_current_history_length():
|
||||
raise ValueError("bad history index")
|
||||
index -= 1
|
||||
return cast(rline_lib.rline_history_get(index), c_char_p).value.decode('utf-8')
|
||||
|
||||
def remove_history_item(pos):
|
||||
pass
|
||||
|
||||
def replace_history_item(pos, item):
|
||||
pass
|
||||
|
||||
def add_history(line):
|
||||
rline_lib.rline_history_insert(line.encode('utf-8'))
|
||||
|
||||
def set_auto_history(enabled):
|
||||
auto_history = enabled
|
||||
|
||||
def set_startup_hook(func=None):
|
||||
pass
|
||||
|
||||
def set_pre_input_hook(func=None):
|
||||
pass
|
||||
|
||||
def set_completer(func=None):
|
||||
pass
|
||||
|
||||
def get_completer():
|
||||
return None
|
||||
|
||||
def get_completion_type():
|
||||
return None
|
||||
|
||||
def get_begidx():
|
||||
return 0
|
||||
|
||||
def get_endidx():
|
||||
return 0
|
||||
|
||||
def set_completer_delims(string):
|
||||
pass
|
||||
|
||||
def get_completer_delims():
|
||||
return ""
|
||||
|
||||
def set_completion_display_matches_hook(func=None):
|
||||
pass
|
||||
|
296
userspace/py/lib/text_region.py
Normal file
296
userspace/py/lib/text_region.py
Normal file
@ -0,0 +1,296 @@
|
||||
import unicodedata
|
||||
from html.parser import HTMLParser
|
||||
import toaru_fonts
|
||||
|
||||
class TextUnit(object):
|
||||
def __init__(self, string, unit_type, font):
|
||||
self.string = string
|
||||
self.unit_type = unit_type
|
||||
self.font = font
|
||||
self.width = font.width(self.string)
|
||||
|
||||
def set_font(self, font):
|
||||
self.font = font
|
||||
self.width = font.width(self.string)
|
||||
|
||||
def __repr__(self):
|
||||
return "(" + self.string + "," + str(self.unit_type) + "," + str(self.width) + ")"
|
||||
|
||||
|
||||
class TextRegion(object):
|
||||
|
||||
def __init__(self, x, y, width, height, font=toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, 13)):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.font = font
|
||||
self.text = ""
|
||||
self.lines = []
|
||||
self.align = 0
|
||||
self.valign = 0
|
||||
self.line_height = self.font.font_size
|
||||
self.text_units = []
|
||||
self.scroll = 0
|
||||
self.ellipsis = ""
|
||||
self.one_line = False
|
||||
|
||||
def set_alignment(self, align):
|
||||
self.align = align
|
||||
|
||||
def set_valignment(self, align):
|
||||
self.valign = align
|
||||
|
||||
def visible_lines(self):
|
||||
return int(self.height / self.line_height)
|
||||
|
||||
def reflow(self):
|
||||
self.lines = []
|
||||
|
||||
current_width = 0
|
||||
current_units = []
|
||||
leftover = None
|
||||
|
||||
i = 0
|
||||
while i < len(self.text_units):
|
||||
if leftover:
|
||||
unit = leftover
|
||||
leftover = None
|
||||
else:
|
||||
unit = self.text_units[i]
|
||||
if unit.unit_type == 3:
|
||||
self.lines.append(current_units)
|
||||
current_units = []
|
||||
current_width = 0
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if current_width + unit.width > self.width:
|
||||
if not current_units or self.one_line:
|
||||
# We need to split the current unit.
|
||||
k = len(unit.string)-1
|
||||
while k and current_width + unit.font.width(unit.string[:k] + self.ellipsis) > self.width:
|
||||
k -= 1
|
||||
ellipsis = self.ellipsis
|
||||
if not k and self.ellipsis:
|
||||
ellipsis = ""
|
||||
if not k and self.one_line:
|
||||
added_ellipsis = False
|
||||
while len(current_units) and sum([unit.width for unit in current_units]) + unit.font.width(self.ellipsis) > self.width:
|
||||
this_unit = current_units[-1]
|
||||
current_units = current_units[:-1]
|
||||
current_width = sum([unit.width for unit in current_units])
|
||||
k = len(this_unit.string)-1
|
||||
while k and current_width + unit.font.width(this_unit.string[:k] + self.ellipsis) > self.width:
|
||||
k -= 1
|
||||
if k:
|
||||
current_units.append(TextUnit(this_unit.string[:k] + self.ellipsis,this_unit.unit_type,this_unit.font))
|
||||
added_ellipsis = True
|
||||
break
|
||||
if not added_ellipsis:
|
||||
current_units.append(TextUnit(self.ellipsis,0,unit.font))
|
||||
|
||||
else:
|
||||
current_units.append(TextUnit(unit.string[:k]+ellipsis,unit.unit_type,unit.font))
|
||||
leftover = TextUnit(unit.string[k:],unit.unit_type,unit.font)
|
||||
self.lines.append(current_units)
|
||||
current_units = []
|
||||
current_width = 0
|
||||
if self.one_line:
|
||||
return
|
||||
else:
|
||||
self.lines.append(current_units)
|
||||
current_units = []
|
||||
current_width = 0
|
||||
if unit.unit_type == 1:
|
||||
i += 1
|
||||
else:
|
||||
current_units.append(unit)
|
||||
current_width += unit.width
|
||||
i += 1
|
||||
if current_units:
|
||||
self.lines.append(current_units)
|
||||
|
||||
def units_from_text(self, text, font=None):
|
||||
if not font:
|
||||
font = self.font
|
||||
|
||||
def char_width(char):
|
||||
x = unicodedata.east_asian_width(char)
|
||||
if x == 'Na': return 1
|
||||
if x == 'N': return 1
|
||||
if x == 'A': return 1
|
||||
if x == 'W': return 2
|
||||
raise ValueError("Don't know how wide "+x+" is.")
|
||||
|
||||
def classify(char):
|
||||
if char == '\n': return 3 # break on line feed
|
||||
if unicodedata.category(char) == 'Zs': return 1 # break on space
|
||||
if char_width(char) > 1: return 2 # allow break on CJK characters (TODO: only really valid for Chinese and Japanese; Korean doesn't work this way
|
||||
return 0
|
||||
|
||||
units = []
|
||||
offset = 0
|
||||
current_unit = ""
|
||||
while offset < len(text):
|
||||
c = text[offset]
|
||||
x = classify(c)
|
||||
if x == 0:
|
||||
current_unit += c
|
||||
offset += 1
|
||||
else:
|
||||
if not current_unit:
|
||||
units.append(TextUnit(c,x,font))
|
||||
offset += 1
|
||||
else:
|
||||
units.append(TextUnit(current_unit,0,font))
|
||||
current_unit = ""
|
||||
if current_unit:
|
||||
units.append(TextUnit(current_unit,0,font))
|
||||
return units
|
||||
|
||||
def set_one_line(self, one_line=True):
|
||||
self.one_line = one_line
|
||||
self.reflow()
|
||||
|
||||
def set_ellipsis(self, ellipsis="…"):
|
||||
self.ellipsis = ellipsis
|
||||
self.reflow()
|
||||
|
||||
def set_text(self, text):
|
||||
self.text = text
|
||||
self.text_units = self.units_from_text(text)
|
||||
self.reflow()
|
||||
|
||||
def set_richtext(self, text):
|
||||
f = self.font
|
||||
self.text = text
|
||||
tr = self
|
||||
|
||||
class RichTextParser(HTMLParser):
|
||||
def __init__(self):
|
||||
super(RichTextParser,self).__init__()
|
||||
self.font_stack = []
|
||||
self.tag_stack = []
|
||||
self.current_font = f
|
||||
self.units = []
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
def make_bold(n):
|
||||
if n == 0: return 1
|
||||
if n == 2: return 3
|
||||
if n == 4: return 5
|
||||
if n == 6: return 7
|
||||
return n
|
||||
def make_italic(n):
|
||||
if n == 0: return 2
|
||||
if n == 1: return 3
|
||||
if n == 4: return 6
|
||||
if n == 5: return 7
|
||||
return n
|
||||
def make_monospace(n):
|
||||
if n == 0: return 4
|
||||
if n == 1: return 5
|
||||
if n == 2: return 6
|
||||
if n == 3: return 7
|
||||
return n
|
||||
|
||||
self.tag_stack.append(tag)
|
||||
if tag == "b":
|
||||
self.font_stack.append(self.current_font)
|
||||
self.current_font = toaru_fonts.Font(make_bold(self.current_font.font_number),self.current_font.font_size,self.current_font.font_color)
|
||||
elif tag == "i":
|
||||
self.font_stack.append(self.current_font)
|
||||
self.current_font = toaru_fonts.Font(make_italic(self.current_font.font_number),self.current_font.font_size,self.current_font.font_color)
|
||||
elif tag == "color":
|
||||
self.font_stack.append(self.current_font)
|
||||
self.current_font = toaru_fonts.Font(self.current_font.font_number,self.current_font.font_size,int(attrs[0][0],16) | 0xFF000000)
|
||||
elif tag == "mono":
|
||||
self.font_stack.append(self.current_font)
|
||||
self.current_font = toaru_fonts.Font(make_monospace(self.current_font.font_number),self.current_font.font_size,self.current_font.font_color)
|
||||
else:
|
||||
pass
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if self.tag_stack[-1] != tag:
|
||||
print(f"unclosed tag {self.tag_stack[-1]} when closing tag {tag}")
|
||||
else:
|
||||
self.tag_stack.pop()
|
||||
if tag in ["b","i","color","mono"]:
|
||||
self.current_font = self.font_stack.pop()
|
||||
|
||||
def handle_data(self, data):
|
||||
units = tr.units_from_text(data, self.current_font)
|
||||
self.units.extend(units)
|
||||
|
||||
parser = RichTextParser()
|
||||
parser.feed(text)
|
||||
self.text_units = parser.units
|
||||
self.reflow()
|
||||
|
||||
def set_font(self, new_font):
|
||||
self.font = new_font
|
||||
self.line_height = self.font.font_size
|
||||
self.reflow()
|
||||
|
||||
def set_line_height(self, new_line_height):
|
||||
self.line_height = new_line_height
|
||||
self.reflow()
|
||||
|
||||
def resize(self, new_width, new_height):
|
||||
needs_reflow = self.width != new_width
|
||||
self.width = new_width
|
||||
self.height = new_height
|
||||
if needs_reflow:
|
||||
self.reflow()
|
||||
|
||||
def move(self, new_x, new_y):
|
||||
self.x = new_x
|
||||
self.y = new_y
|
||||
|
||||
def click(self, x, y):
|
||||
# Determine which line this click belongs in
|
||||
if x < self.x or x > self.x + self.width or y < self.y or y > self.y + self.height:
|
||||
return None
|
||||
top_align = 0
|
||||
if len(self.lines) < int(self.height / self.line_height):
|
||||
if self.valign == 1: # bottom
|
||||
top_align = self.height - len(self.lines) * self.line_height
|
||||
elif self.valign == 2: # middle
|
||||
top_align = int((self.height - len(self.lines) * self.line_height) / 2)
|
||||
new_y = y - top_align - self.y - 2 # fuzz factor
|
||||
line = int(new_y / self.line_height)
|
||||
if line < len(self.lines[self.scroll:]):
|
||||
left_align = 0
|
||||
xline = self.lines[self.scroll+line]
|
||||
if self.align == 1: # right align
|
||||
left_align = self.width - sum([u.width for u in xline])
|
||||
elif self.align == 2: # center
|
||||
left_align = int((self.width - sum([u.width for u in xline])) / 2)
|
||||
for unit in xline:
|
||||
if x >= self.x + left_align and x < self.x + left_align + unit.width:
|
||||
return unit
|
||||
left_align += unit.width
|
||||
else:
|
||||
return None
|
||||
|
||||
def draw(self, context):
|
||||
current_height = self.line_height
|
||||
top_align = 0
|
||||
if len(self.lines) < int(self.height / self.line_height):
|
||||
if self.valign == 1: # bottom
|
||||
top_align = self.height - len(self.lines) * self.line_height
|
||||
elif self.valign == 2: # middle
|
||||
top_align = int((self.height - len(self.lines) * self.line_height) / 2)
|
||||
for line in self.lines[self.scroll:]:
|
||||
if current_height > self.height:
|
||||
break
|
||||
left_align = 0
|
||||
if self.align == 1: # right align
|
||||
left_align = self.width - sum([u.width for u in line])
|
||||
elif self.align == 2: # center
|
||||
left_align = int((self.width - sum([u.width for u in line])) / 2)
|
||||
for unit in line:
|
||||
unit.font.write(context, self.x + left_align, self.y + current_height + top_align, unit.string)
|
||||
left_align += unit.width
|
||||
current_height += self.line_height
|
57
userspace/py/lib/toaru_fonts.py
Normal file
57
userspace/py/lib/toaru_fonts.py
Normal file
@ -0,0 +1,57 @@
|
||||
import ctypes
|
||||
|
||||
_lib = None
|
||||
|
||||
if not _lib:
|
||||
_lib = ctypes.CDLL('libtoaru-shmemfonts.so')
|
||||
_lib.init_shmemfonts()
|
||||
_lib.draw_string_width.argtypes = [ctypes.c_char_p]
|
||||
_lib.draw_string_width.restype = ctypes.c_uint32
|
||||
_lib.shmem_font_name.restype = ctypes.c_char_p
|
||||
_lib.draw_string.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_uint32, ctypes.c_char_p]
|
||||
_lib.draw_string_shadow.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_uint32, ctypes.c_char_p, ctypes.c_uint32, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_double]
|
||||
|
||||
FONT_SANS_SERIF = 0
|
||||
FONT_SANS_SERIF_BOLD = 1
|
||||
FONT_SANS_SERIF_ITALIC = 2
|
||||
FONT_SANS_SERIF_BOLD_ITALIC = 3
|
||||
FONT_MONOSPACE = 4
|
||||
FONT_MONOSPACE_BOLD = 5
|
||||
FONT_MONOSPACE_ITALIC = 6
|
||||
FONT_MONOSPACE_BOLD_ITALIC = 7
|
||||
FONT_JAPANESE = 8
|
||||
FONT_SYMBOLA = 9
|
||||
|
||||
class Font(object):
|
||||
|
||||
def __init__(self, font_number, font_size=10, font_color=0xFF000000):
|
||||
self.font_number = font_number
|
||||
self.font_size = font_size
|
||||
self.font_color = font_color
|
||||
|
||||
def _use(self):
|
||||
_lib.set_font_face(self.font_number)
|
||||
_lib.set_font_size(self.font_size)
|
||||
|
||||
def width(self, string):
|
||||
self._use()
|
||||
string = string.encode('utf-8')
|
||||
return _lib.draw_string_width(string)
|
||||
|
||||
def write(self, ctx, x, y, string, shadow=None):
|
||||
self._use()
|
||||
foreground = self.font_color
|
||||
string = string.encode('utf-8')
|
||||
if hasattr(ctx,"_gfx"):
|
||||
# Allow a yutani.Window to be passed to this instead of a real graphics context
|
||||
ctx = ctx._gfx
|
||||
if shadow:
|
||||
color, darkness, offset_x, offset_y, radius = shadow
|
||||
_lib.draw_string_shadow(ctx,x,y,foreground,string,color,darkness,offset_x,offset_y,radius)
|
||||
else:
|
||||
_lib.draw_string(ctx,x,y,foreground,string)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return _lib.shmem_font_name(self.font_number).decode('utf-8')
|
||||
|
46
userspace/py/lib/toast.py
Normal file
46
userspace/py/lib/toast.py
Normal file
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/python3
|
||||
"""ToaruOS toast library.
|
||||
|
||||
Sends messages to the notification daemon for display."""
|
||||
|
||||
import os
|
||||
from ctypes import *
|
||||
|
||||
pex_lib = CDLL("libtoaru-pex.so")
|
||||
pex_conn = None
|
||||
|
||||
class ToastMessage(Structure):
|
||||
_fields_ = [
|
||||
("ttl", c_uint),
|
||||
("strings", c_char * 0),
|
||||
]
|
||||
|
||||
def init_toast():
|
||||
"""Initialize the connection to the toast daemon. This should happen automatically."""
|
||||
global pex_conn
|
||||
server = os.environ.get("TOASTD","toastd").encode('utf-8')
|
||||
pex_conn = pex_lib.pex_connect(server)
|
||||
|
||||
def send_toast(title, message, ttl=5):
|
||||
"""Send a toast message to the daemon."""
|
||||
|
||||
# If not yet connected, connect.
|
||||
if not pex_conn:
|
||||
init_toast()
|
||||
|
||||
# Title and message need to be C strings.
|
||||
title = title.encode('utf-8')
|
||||
message = message.encode('utf-8')
|
||||
|
||||
# Build message struct.
|
||||
s = len(title) + len(message) + 2
|
||||
b = create_string_buffer(s + sizeof(ToastMessage))
|
||||
m = ToastMessage(ttl=ttl)
|
||||
memmove(addressof(b), addressof(m), sizeof(m))
|
||||
memmove(addressof(b)+sizeof(m),title,len(title)+1)
|
||||
memmove(addressof(b)+sizeof(m)+len(title)+1,message,len(message)+1)
|
||||
|
||||
# Send it off.
|
||||
pex_lib.pex_reply(pex_conn, s + sizeof(ToastMessage), b)
|
||||
|
||||
|
102
userspace/py/lib/ttk.py
Normal file
102
userspace/py/lib/ttk.py
Normal file
@ -0,0 +1,102 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import yutani
|
||||
|
||||
decorations = None
|
||||
_windows = {}
|
||||
|
||||
ttk_lib = None
|
||||
|
||||
def init_ttk():
|
||||
global ttk_lib
|
||||
from ctypes import CDLL
|
||||
ttk_lib = CDLL("libtoaru-ttk.so")
|
||||
|
||||
def rgb(r,g,b):
|
||||
return yutani.yutani_gfx_lib.rgb(r,g,b)
|
||||
|
||||
class Button(object): # TODO widget base class?
|
||||
pass
|
||||
|
||||
class Window(object): # TODO container base class?
|
||||
|
||||
def __init__(self):
|
||||
global decorations
|
||||
|
||||
if not yutani.yutani_lib:
|
||||
yutani.Yutani()
|
||||
|
||||
if not decorations:
|
||||
decorations = yutani.Decor()
|
||||
|
||||
self.decorated = True
|
||||
self._win = None
|
||||
self.title = "TTK Window"
|
||||
|
||||
def _create_window(self):
|
||||
w,h = self._calculate_bounds()
|
||||
self._win = yutani.Window(w,h, flags=0, title=self.title)
|
||||
self._win.move(100, 100)
|
||||
_windows[self._win.wid] = self
|
||||
|
||||
def _calculate_bounds(self):
|
||||
return (decorations.width() + 200, decorations.height() + 200)
|
||||
|
||||
def show(self):
|
||||
if not self._win:
|
||||
self._create_window()
|
||||
self._win.fill(rgb(214,214,214))
|
||||
if self.decorated:
|
||||
decorations.render(self._win, self.title)
|
||||
self._win.flip()
|
||||
|
||||
def close(self):
|
||||
# TODO callback
|
||||
self._win.close()
|
||||
|
||||
def main():
|
||||
"""TTK main"""
|
||||
while 1:
|
||||
# Poll for events.
|
||||
msg = yutani.yutani_ctx.poll()
|
||||
if msg.type == yutani.Message.MSG_SESSION_END:
|
||||
# All applications should attempt to exit on SESSION_END.
|
||||
for w in _windows.values():
|
||||
w.close()
|
||||
break
|
||||
elif msg.type == yutani.Message.MSG_KEY_EVENT:
|
||||
# Print key events for debugging.
|
||||
if msg.event.key == b'q':
|
||||
# Convention says to close windows when 'q' is pressed,
|
||||
# unless we're using keyboard input "normally".
|
||||
w = _windows.get(msg.wid)
|
||||
if w:
|
||||
w.close()
|
||||
break
|
||||
elif msg.type == yutani.Message.MSG_WINDOW_FOCUS_CHANGE:
|
||||
# If the focus of our window changes, redraw the borders.
|
||||
w = _windows.get(msg.wid)
|
||||
if w:
|
||||
w._win.focused = msg.focused
|
||||
w.show()
|
||||
elif msg.type == yutani.Message.MSG_RESIZE_OFFER:
|
||||
# Resize request for window.
|
||||
pass
|
||||
elif msg.type == yutani.Message.MSG_WINDOW_MOUSE_EVENT:
|
||||
w = _windows.get(msg.wid)
|
||||
if w:
|
||||
r = None
|
||||
if w.decorated:
|
||||
r = decorations.handle_event(msg)
|
||||
if r == yutani.Decor.EVENT_CLOSE:
|
||||
w.close
|
||||
break
|
||||
else:
|
||||
pass
|
||||
|
||||
init_ttk()
|
||||
|
||||
if __name__ == '__main__':
|
||||
w = Window()
|
||||
w.show()
|
||||
main()
|
571
userspace/py/lib/yutani.py
Normal file
571
userspace/py/lib/yutani.py
Normal file
@ -0,0 +1,571 @@
|
||||
#!/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_TIMER_REQUEST = 0x00000100
|
||||
MSG_TIMER_TICK = 0x00000101
|
||||
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(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 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 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, 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(yutani_ctx._ptr)
|
||||
|
||||
def unsubscribe(self):
|
||||
"""Unsubscribe from window information changes."""
|
||||
yutani_lib.yutani_unsubscribe_windows(yutani_ctx._ptr)
|
||||
|
||||
def query_windows(self):
|
||||
"""Request a window subsription list."""
|
||||
yutani_lib.yutani_query_windows(yutani_ctx._ptr)
|
||||
|
||||
def timer_request(self, precision=0, flags=0):
|
||||
"""Request timer tick messages."""
|
||||
yutani_lib.yutani_timer_request(yutani_ctx._ptr, precision, flags)
|
||||
|
||||
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(yutani_ctx._ptr, wid)
|
||||
|
||||
def session_end(self):
|
||||
"""Request the end of the user session."""
|
||||
yutani_lib.yutani_session_end(yutani_ctx._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 WindowStackOrder(object):
|
||||
"""Window stack order options."""
|
||||
ZORDER_MAX = 0xFFFF
|
||||
ZORDER_TOP = 0xFFFF
|
||||
ZORDER_BOTTOM = 0x0000
|
||||
|
||||
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 get_cairo_surface(self):
|
||||
"""Obtain a pycairo.ImageSurface representing the window backbuffer."""
|
||||
import _cairo
|
||||
buffer = self._gfx.contents.backbuffer
|
||||
width = self.width
|
||||
height = self.height
|
||||
format = _cairo.FORMAT_ARGB32
|
||||
# int cairo_format_stride_for_width(cairo_format_t format, int width)
|
||||
cfsfw = CDLL('libcairo.so').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 = CDLL('libcairo.so').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 = CDLL(_cairo.__file__).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."""
|
||||
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 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)
|
||||
|
||||
@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
|
||||
|
||||
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}')
|
Loading…
Reference in New Issue
Block a user