yutani: replace old _yutani module

This commit is contained in:
K. Lange 2023-02-22 10:44:37 +09:00
parent 14994141c2
commit 275e97739e
4 changed files with 370 additions and 1491 deletions

View File

@ -59,6 +59,7 @@ LIBS_Y=$(foreach lib,$(LIBS),.make/$(lib).lmak)
KRK_MODS = $(patsubst kuroko/src/modules/module_%.c,$(BASE)/lib/kuroko/%.so,$(wildcard kuroko/src/modules/module_*.c)) KRK_MODS = $(patsubst kuroko/src/modules/module_%.c,$(BASE)/lib/kuroko/%.so,$(wildcard kuroko/src/modules/module_*.c))
KRK_MODS += $(patsubst kuroko/modules/%,$(BASE)/lib/kuroko/%,$(wildcard kuroko/modules/*.krk kuroko/modules/*/*/.krk kuroko/modules/*/*/*.krk)) KRK_MODS += $(patsubst kuroko/modules/%,$(BASE)/lib/kuroko/%,$(wildcard kuroko/modules/*.krk kuroko/modules/*/*/.krk kuroko/modules/*/*/*.krk))
KRK_MODS += $(patsubst lib/kuroko/%,$(BASE)/lib/kuroko/%,$(wildcard lib/kuroko/*.krk))
KRK_MODS_X = $(patsubst lib/kuroko/%.c,$(BASE)/lib/kuroko/%.so,$(wildcard lib/kuroko/*.c)) KRK_MODS_X = $(patsubst lib/kuroko/%.c,$(BASE)/lib/kuroko/%.so,$(wildcard lib/kuroko/*.c))
KRK_MODS_Y = $(patsubst lib/kuroko/%.c,.make/%.kmak,$(wildcard lib/kuroko/*.c)) KRK_MODS_Y = $(patsubst lib/kuroko/%.c,.make/%.kmak,$(wildcard lib/kuroko/*.c))
@ -93,6 +94,10 @@ $(BASE)/lib/kuroko/%.krk: kuroko/modules/%.krk | dirs
mkdir -p $(dir $@) mkdir -p $(dir $@)
cp $< $@ cp $< $@
$(BASE)/lib/kuroko/%.krk: lib/kuroko/%.krk | dirs
mkdir -p $(dir $@)
cp $< $@
$(BASE)/lib/libkuroko.so: $(KRK_SRC) | $(LC) $(BASE)/lib/libkuroko.so: $(KRK_SRC) | $(LC)
$(CC) -O2 -shared -fPIC -Ikuroko/src -o $@ $(filter-out kuroko/src/kuroko.c,$(KRK_SRC)) $(CC) -O2 -shared -fPIC -Ikuroko/src -o $@ $(filter-out kuroko/src/kuroko.c,$(KRK_SRC))

View File

@ -1,80 +1,175 @@
#!/bin/kuroko #!/bin/kuroko
from _yutani import (color, Yutani, Window, Decorator, Message, from _yutani2 import (YutaniCtx, Font, rgb, rgb, MenuBar, decor_get_bounds, decor_render,
MenuBar, MenuList, MenuEntry, MenuEntrySeparator, MenuList, MenuEntry, MenuEntrySeparator, Message, decor_handle_event, decor_show_default_menu,
Font MenuEntryCustom)
)
let running = True from yutani_mainloop import Window, yctx as y, AsyncMainloop, Task, sleep
let y = Yutani() import math
let w = Window(640,480,title="Test Window",doublebuffer=True) import random
let dejavu = Font("sans-serif",13)
w.move(200,200) let mainloop = AsyncMainloop()
let mb = MenuBar((("File",'file'),("Help",'help'))) def close_enough(me):
let _menu_File = MenuList() return me.command == 2 and math.sqrt((me.new_x - me.old_x) ** 2 + (me.new_y - me.old_y) **2) < 10
_menu_File.insert(MenuEntry("Test", lambda menu: print("hello, world"))) class Button:
_menu_File.insert(MenuEntrySeparator()) ''' Basic button widget based on draw_button '''
_menu_File.insert(MenuEntry("Quit", lambda menu: (running = False)))
mb.insert('file', _menu_File) def __init__(self, window, x, y, width, height, title, callback):
let _menu_Help = MenuList() self.ctx = window
let _menu_Help_help = MenuEntry("Help",lambda menu: print("oh no!")) self.x = x
_menu_Help.insert(_menu_Help_help) self.y = y
mb.insert('help', _menu_Help) self.width = width
self.height = height
self.title = title
self.state = 0
self.callback = asyncCallback
def drawWindow(): def __contains__(self, coord):
w.fill(color(255,255,255)) if not isinstance(cord, tuple):
Decorator.render(w) return False
let bounds = Decorator.get_bounds(w)
mb.place(bounds['left_width'],bounds['top_height'],w.width-bounds['width'],w)
mb.render(w)
dejavu.draw_string(w,"Hello, world.",20,120)
w.flip()
drawWindow() def focus_enter(self):
self.state = 1
mb.callback = lambda x: drawWindow()
print(globals())
def handleMouseEvent(msg):
let decResponse = Decorator.handle_event(msg)
if decResponse == Decorator.DECOR_CLOSE:
w.close()
return True return True
else if decResponse == Decorator.DECOR_RIGHT:
Decorator.show_default_menu(w,w.x+msg.new_x,w.y+msg.new_y)
mb.mouse_event(w,msg)
def finishResize(msg): def focus_leave(self):
w.resize_accept(msg.width, msg.height) self.state = 0
w.reinit() return True
drawWindow()
w.resize_done()
w.flip()
while running: def mouse_down(self, msg):
let msg = y.poll() self.state = 2
if y.menu_process_event(msg): return True
drawWindow()
if msg.type == Message.MSG_SESSION_END: def mouse_up(self, msg):
w.close() # TODO check if old_x, old_y is in button
break self.callback(self)
else if msg.type == Message.MSG_KEY_EVENT: self.state = 0
return True
def draw(self):
draw_button(self.ctx, self.x, self.y, self.width, self.height, self.title, self.state)
class MyMenuWidget(MenuEntryCustom):
def __init__(self):
super().__init__()
self.height = 30
self.rwidth = 148
self.dejavu = Font("sans-serif", 13)
self.x = 0
self.y = 0
def render(self, ctx, offset):
self.offset = offset
if self.hilight:
ctx.rect(1,offset,self.width-2,self.height,rgb(100,int(255 * self.x / self.width),int(255 * (self.y-offset) / self.height)),radius=4)
self.dejavu.draw_string(ctx, f"{self.x=},{self.y=}", 2, offset + 13)
else:
self.dejavu.draw_string(ctx, f"{offset=}", 2, offset + 13)
def activate(self, flags):
print("Activated by keyboard!")
def click_within(self, evt):
if evt.command == 0: return True
if evt.command == 2:
if \
evt.new_x >= 0 and evt.new_x < self.width and \
evt.new_y >= self.offset and evt.new_y < self.offset + self.height and \
evt.old_x >= 0 and evt.old_x < self.width and \
evt.old_y >= self.offset and evt.old_y < self.offset + self.height:
return True
else:
print(f'{evt.old_x=} {evt.old_y=} {evt.new_x=} {evt.old_y=} {self.offset=} {self.width=} {self.height=}')
async def async_thingy(self):
print("Schedule async callback")
await sleep(2)
print("Finish async callback")
def mouse_event(self, evt):
self.x = evt.new_x
self.y = evt.new_y
if self.click_within(evt):
Task(self.async_thingy())
print("Clicked!")
return True
class MyWindow(Window):
def __init__(self):
super().__init__(640, 480, title="Hello", doublebuffer=True)
self.bgc = rgb(255,255,255)
self.dejavu = Font("sans-serif", 13)
self.mb = MenuBar((("File",'file'),("Help",'help')))
let _menu_File = MenuList()
_menu_File.insert(MenuEntry("Test", lambda menu: print("hello, world")))
_menu_File.insert(MenuEntrySeparator())
_menu_File.insert(MenuEntry("Quit", lambda menu: self.close()))
self.mb.insert('file', _menu_File)
let _menu_Help = MenuList()
let _menu_Help_help = MenuEntry("Help",lambda menu: print("oh no!"))
_menu_Help.insert(_menu_Help_help)
_menu_Help.insert(MyMenuWidget())
_menu_Help.insert(MyMenuWidget())
self.mb.insert('help', _menu_Help)
self.mb.callback = lambda x: self.draw()
def draw(self):
self.fill(self.bgc)
decor_render(self)
let bounds = decor_get_bounds(self)
self.mb.place(bounds['left_width'],bounds['top_height'],self.width-bounds['width'],self)
self.mb.render(self)
self.dejavu.draw_string(self,"Hello, world.",20,120)
self.dejavu.draw_string(self,f"{self.x}, {self.y}",20,140)
self.flip()
def mouse_event(self, msg):
let decResponse = decor_handle_event(msg)
if decResponse == 2:
print("Close me?")
self.close()
return True
else if decResponse == 5:
decor_show_default_menu(self, self.x + msg.new_x, self.y + msg.new_y)
self.mb.mouse_event(self, msg)
def interp(a,b,p):
return a * (1.0 - p) + b * p
def interp_color(a,b,p):
return rgb(*(int(self.interp(l,r,p)) for l,r in zip(a,b)))
async def animate(self):
let mySentinel = object()
self.animator = mySentinel
let start = (255 * random.random(),255 * random.random(),255 * random.random())
let end = (255,255,255)
for i in range(31):
await sleep(0.033)
if self.animator is not mySentinel:
break
self.bgc = self.interp_color(start,end,i/30)
self.draw()
def keyboard_event(self, msg):
print(msg.keycode) print(msg.keycode)
else if msg.type == Message.MSG_WINDOW_FOCUS_CHANGE: if msg.keycode == 102 and msg.action == 1:
if msg.wid == w.wid: Task(self.animate())
print("focus changed")
w.set_focused(msg.focused)
drawWindow()
else if msg.type == Message.MSG_RESIZE_OFFER:
finishResize(msg)
else if msg.type == Message.MSG_WINDOW_MOUSE_EVENT:
if msg.wid == w.wid:
if handleMouseEvent(msg): break
else if msg.type == Message.MSG_WINDOW_CLOSE:
w.close()
break
def close(self):
super().close()
mainloop.exit()
def window_moved(self, msg):
self.draw()
def menu_close(self):
self.draw()
let w = MyWindow()
w.move(200,200)
w.draw()
mainloop.menu_closed_callback = w.menu_close
mainloop.run()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,203 @@
'''
Asynchronous event loop for Yutani applications.
'''
import time
import _yutani2
let _windows = {}
let yctx = _yutani2.YutaniCtx()
class Window(_yutani2.Window):
'''
Base class for eventloop-managed windows, providing stub implementations
of the core callback functions.
'''
def __init__(self, *args, **kwargs):
super().__init__(*args,**kwargs)
_windows[self.wid] = self
def close(self):
if self.wid in _windows:
del _windows[self.wid]
super().close()
def draw(self):
pass
def keyboard_event(self, msg):
pass
def focus_changed(self, msg):
self.focused = msg.focused
self.draw()
def finish_resize(self, msg):
self.resize_accept(msg.width, msg.height)
self.reinit()
self.draw()
self.resize_done()
self.flip()
def window_moved(self, msg):
pass
def mouse_event(self, msg):
pass
let current_loop
class Future():
def __init__(self):
self.result = None
self.loop = current_loop
self.done = False
self.callbacks = []
def add_callback(self, func):
self.callbacks.append(func)
def schedule_callbacks(self):
let mycallbacks = self.callbacks[:]
self.callbacks = []
for callback in mycallbacks:
self.loop.call_soon(callback, self)
def set_result(self, result):
self.result = result
self.done = True
self.schedule_callbacks()
def __await__(self):
if not self.done:
yield self
return self.result
class Task():
def __init__(self, coro):
self.coro = coro
current_loop.call_soon(self.step)
def step(self):
let result = self.coro.send(None)
if isinstance(result,Future):
result.add_callback(self.wakeup)
else if result == self.coro:
return # This task is done
else if result is None:
current_loop.call_soon(self.step)
else:
print("Don't know what to do with",result)
def wakeup(self, future):
self.step()
async def sleep(delay, result=None):
let future = Future()
current_loop.call_later(delay, future.set_result, result)
return await future
class Timer():
def __init__(self, time, func, args):
self.time = time
self.func = func
self.args = args
def __lt__(self, other):
return self.time < other.time
def __gt__(self, other):
return self.time > other.time
def __le__(self, other):
return self.time <= other.time
def __ge__(self, other):
return self.time >= other.time
def __eq__(self, other):
return self.time == other.time
class AsyncMainloop():
def __init__(self):
self.should_exit = 0
self.status_code = 0
self.ready = []
self.schedule = []
self.fileno = yctx.fileno()
self.menu_closed_callback = None
def exit(self, arg=0):
self.status_code = arg
self.should_exit = 1
def call_soon(self, func, *args):
self.ready.append((func,args))
def call_later(self, delay, func, *args):
self.call_at(time.time() + delay, func, *args)
def call_at(self, time, func, *args):
self.schedule.append(Timer(time,func,args))
self.schedule.sort()
def maybe_coro(self, result):
if isinstance(result, generator):
Task(result)
def handle_message(self):
let msg = yctx.poll()
if yctx.menu_process_event(msg):
if self.menu_closed_callback:
self.maybe_coro(self.menu_closed_callback())
if msg.msg_type == _yutani2.Message.MSG_SESSION_END:
return False
else if msg.msg_type == _yutani2.Message.MSG_KEY_EVENT:
if msg.wid in _windows:
self.maybe_coro(_windows[msg.wid].keyboard_event(msg))
else if msg.msg_type == _yutani2.Message.MSG_WINDOW_FOCUS_CHANGE:
if msg.wid in _windows:
self.maybe_coro(_windows[msg.wid].focus_changed(msg))
else if msg.msg_type == _yutani2.Message.MSG_RESIZE_OFFER:
if msg.wid in _windows:
self.maybe_coro(_windows[msg.wid].finish_resize(msg))
else if msg.msg_type == _yutani2.Message.MSG_WINDOW_MOVE:
if msg.wid in _windows:
self.maybe_coro(_windows[msg.wid].window_moved(msg))
else if msg.msg_type == _yutani2.Message.MSG_WINDOW_MOUSE_EVENT:
if msg.wid in _windows:
self.maybe_coro(_windows[msg.wid].mouse_event(msg))
else if msg.msg_type == _yutani2.Message.MSG_WINDOW_CLOSE:
if msg.wid in _windows:
self.maybe_coro(_windows[msg.wid].close())
return True
def run_once(self):
# Determine if anything in the schedule list can be run
let timeout = -1
let now = time.time()
if self.ready:
timeout = 0
else if self.schedule:
timeout = max(0,self.schedule[0].time - now)
# Poll
let res = _yutani2.fswait((self.fileno,), int(timeout * 1000))
# TODO probably hooks if these have callbacks
if res[0]:
self.handle_message()
# Schedule future stuff
while self.schedule and self.schedule[0].time <= now:
self.ready.append((self.schedule[0].func,self.schedule[0].args))
self.schedule.pop(0)
let count = len(self.ready)
for i in range(count):
let func, args = self.ready.pop(0)
func(*args)
def run(self):
current_loop = self
while not self.should_exit:
self.run_once()