Import Python userspace stuff

This commit is contained in:
Kevin Lange 2017-01-03 16:32:05 +09:00
parent 206cf07cc2
commit 3538982922
9 changed files with 1515 additions and 16 deletions

View File

@ -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}

View 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()

View 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}')

View 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

View 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

View 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
View 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
View 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
View 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}')