Parcourir la source

added darkwallet in bin/. See README for usage instructions

zero il y a 2 ans
Parent
commit
c78a469c83

+ 33 - 0
bin/darkwallet/Cargo.toml

@@ -0,0 +1,33 @@
+[package]
+name = "darkwallet"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[workspace]
+
+[dependencies]
+#miniquad = "0.4.0-alpha"
+miniquad = { git = "https://github.com/not-fl3/miniquad" }
+fontdue = "0.7"
+image = "0.24"
+log = "0.4.19"
+glam = "0.27.0"
+zmq = "0.10.0"
+darkfi-serial = { git = "https://codeberg.org/darkrenaissance/darkfi" }
+thiserror = "1.0.57"
+# Only for async Mutex
+smol = "1.3.0"
+atomic_float = "0.1.0"
+
+[target.'cfg(target_os = "android")'.dependencies]
+android_logger = "0.13"
+
+[target.'cfg(target_os = "linux")'.dependencies]
+simplelog = "0.12.1"
+
+[package.metadata.android.activity_attributes]
+"android:exported" = "true"
+"android:windowSoftInputMode" = "adjustResize"
+

BIN
bin/darkwallet/Inter-Regular.ttf


+ 2 - 0
bin/darkwallet/README.md

@@ -0,0 +1,2 @@
+`cargo run` then `python client.py`
+

+ 233 - 0
bin/darkwallet/client.py

@@ -0,0 +1,233 @@
+#!/usr/bin/python
+# Ping a peer
+from pydrk import Api, exc, ErrorCode, HostApi, SceneNodeType, PropertyType, vertex, face, serial
+import sys, zmq
+
+def join(parent_path, child_name):
+    if parent_path == "/":
+        return f"/{child_name}"
+    return f"{parent_path}/{child_name}"
+
+def print_tree():
+    root_id = api.lookup_node_id("/")
+    print_node_info(root_id)
+
+def print_node_info(parent_id, indent=0):
+    ws = " "*4*indent
+    for (child_name, child_id, child_type) in api.get_children(parent_id):
+        match child_type:
+            case SceneNodeType.ROOT:
+                child_type = "root"
+            case SceneNodeType.WINDOW:
+                child_type = "window"
+            case SceneNodeType.WINDOW_INPUT:
+                child_type = "window_input"
+            case SceneNodeType.KEYBOARD:
+                child_type = "keyboard"
+            case SceneNodeType.MOUSE:
+                child_type = "mouse"
+            case SceneNodeType.RENDER_LAYER:
+                child_type = "render_layer"
+            case SceneNodeType.RENDER_OBJECT:
+                child_type = "render_object"
+            case SceneNodeType.RENDER_MESH:
+                child_type = "render_mesh"
+            case SceneNodeType.RENDER_TEXT:
+                child_type = "render_text"
+            case SceneNodeType.RENDER_TEXTURE:
+                child_type = "render_texture"
+            case SceneNodeType.FONTS:
+                child_type = "fonts"
+            case SceneNodeType.FONT:
+                child_type = "font"
+            case SceneNodeType.LINE_POSITION:
+                child_type = "line_position"
+
+        desc = f"{ws}{child_name}:{child_id}/"
+        desc += " "*(50 - len(desc))
+        desc += f"[{child_type}]"
+        print(desc)
+
+        print_node_info(child_id, indent+1)
+
+    for prop_name, prop_type in api.get_properties(parent_id):
+        if prop_type == PropertyType.STR:
+            prop_val = api.get_property(parent_id, prop_name)
+            prop_val = f" = \"{prop_val}\""
+        elif prop_type != PropertyType.BUFFER:
+            prop_val = api.get_property(parent_id, prop_name)
+            prop_val = f" = {prop_val}"
+        else:
+            prop_val = ""
+
+        prop_type = PropertyType.to_str(prop_type)
+
+        print(f"{ws}{prop_name}: {prop_type}{prop_val}")
+
+    for sig in api.get_signals(parent_id):
+        print(f"{ws}~{sig}")
+        for slot_id, slot in api.get_slots(parent_id, sig):
+            print(f"{ws}- '{slot}' ({slot_id})")
+
+    for method_name in api.get_methods(parent_id):
+        args, results = api.get_method(parent_id, method_name)
+
+        args = [f"{name}: " + PropertyType.to_str(typ) for (name, typ) in args]
+        results = [f"{name}: " + PropertyType.to_str(typ) for (name, typ) in results]
+
+        method_str = f"{method_name}(" + ", ".join(args) + ") -> (" + ", ".join(results) + ")"
+        print(f"{ws}{method_str}")
+
+def make_sub_socket():
+    context = zmq.Context()
+    socket = context.socket(zmq.SUB)
+    socket.setsockopt(zmq.SUBSCRIBE, b'')
+    socket.connect("tcp://localhost:9485")
+    return socket
+
+def remove_node_recursive(node_id):
+    for (_, child_id, _) in api.get_children(node_id):
+        # Unlink the child
+        api.unlink_node(child_id, node_id)
+        # Remove the node
+        remove_node_recursive(child_id)
+    # Garbage collection
+    if not api.get_parents(node_id):
+        api.remove_node(node_id)
+
+def clear_layer(layer_path):
+    layer_id = api.lookup_node_id("/window/layer2")
+    for (_, child_id, child_type) in api.get_children(layer_id):
+        api.unlink_node(child_id, layer_id)
+        if child_type == SceneNodeType.RENDER_OBJECT:
+            remove_node_recursive(child_id)
+
+def remove_all_slots(node_path, sig):
+    node_id = api.lookup_node_id(node_path)
+    for slot_id, slot in api.get_slots(node_id, sig):
+        print(f"{node_path}:{sig}(): Unregistering slot '{slot}':{slot_id}")
+        api.unregister_slot(node_id, sig, slot_id)
+
+def register_slot(node_path, sig, tag):
+    remove_all_slots(node_path, sig)
+    node_id = api.lookup_node_id(node_path)
+    api.register_slot(node_id, sig, "", tag)
+
+def get_property(node_path, prop):
+    node_id = api.lookup_node_id(node_path)
+    return api.get_property(node_id, prop)
+
+def set_property(node_path, prop, val):
+    node_id = api.lookup_node_id(node_path)
+    match val:
+        case float():
+            api.set_property_f32(node_id, prop, val)
+        case int():
+            api.set_property_u32(node_id, prop, val)
+
+def recalc_areas():
+    w = get_property("/window", "width")
+    h = get_property("/window", "height")
+    set_property("/window/layer1", "rect_w", int(w))
+    set_property("/window/layer1", "rect_h", int(h))
+    set_property("/window/layer2", "rect_w", int(w))
+    set_property("/window/layer2", "rect_h", int(h))
+
+api = Api()
+host = HostApi(api)
+print(api.hello())
+
+clear_layer("/window/layer2")
+
+print("Generating scene...")
+subsock = make_sub_socket()
+
+register_slot("/window",                "resize",        b"resize")
+register_slot("/window/input/mouse",    "button_down",   b"click")
+register_slot("/window/input/mouse",    "wheel",         b"wheel")
+#register_slot("/window/input/mouse",    "move",          b"mouse")
+
+if True:
+    layer_id = api.lookup_node_id("/window/layer2")
+
+    obj_id = api.add_node("obj1", SceneNodeType.RENDER_OBJECT)
+    api.add_property(obj_id, "x", PropertyType.FLOAT32)
+    api.add_property(obj_id, "y", PropertyType.FLOAT32)
+    api.add_property(obj_id, "scale", PropertyType.FLOAT32)
+    api.set_property_f32(obj_id, "scale", 1.0)
+
+    mesh_id = api.add_node("mesh", SceneNodeType.RENDER_MESH)
+    api.add_property(mesh_id, "verts", PropertyType.BUFFER)
+    api.add_property(mesh_id, "faces", PropertyType.BUFFER)
+
+    texture_id = host.load_texture("da_king", "king.png")
+    api.link_node(texture_id, obj_id)
+    # To preserve the ratio, we should do this when resize happens too
+    texture_w = api.get_property(texture_id, "width")
+    texture_h = api.get_property(texture_id, "width")
+    texture_ratio = texture_w / texture_h
+    screen_w = get_property("/window", "width")
+    screen_h = get_property("/window", "height")
+    screen_ratio = screen_w / screen_h
+    aspect_ratio = texture_ratio / screen_ratio
+
+    #api.set_property_bool("/window/layer2", "is_visible", False)
+    # Lets add a poly - must be counterclockwise
+    x, y = 0.25, 0.25
+    w, h = 0.5 * aspect_ratio, 0.5
+    vert1 = vertex(x,     y,     1, 1, 1, 1, 0, 0)
+    vert2 = vertex(x + w, y,     1, 1, 1, 1, 1, 0)
+    vert3 = vertex(x,     y + h, 1, 1, 1, 1, 0, 1)
+    vert4 = vertex(x + w, y + h, 1, 1, 1, 1, 1, 1)
+    api.set_property_buffer(mesh_id, "verts", vert1 + vert2 + vert3 + vert4)
+    api.set_property_buffer(mesh_id, "faces", face(0, 2, 1) + face(1, 2, 3))
+
+    api.link_node(mesh_id, obj_id)
+    api.link_node(obj_id, layer_id)
+
+if True:
+    set_property("/window/layer1", "is_visible", False)
+
+    layer_id = api.lookup_node_id("/window/layer2")
+    obj_id = api.add_node("obj", SceneNodeType.RENDER_OBJECT)
+    api.add_property(obj_id, "x", PropertyType.FLOAT32)
+    api.add_property(obj_id, "y", PropertyType.FLOAT32)
+    api.add_property(obj_id, "scale", PropertyType.FLOAT32)
+    api.set_property_f32(obj_id, "scale", 1.0)
+
+    # Call method
+    text_id = host.create_text(
+        "/font/inter-regular", "mytxt2", "hello world", 34
+    )
+    if text_id is None:
+        sys.exit(-1)
+    print("create text node id:", text_id)
+
+    api.set_property_f32(obj_id, "x", 0.5)
+    api.set_property_f32(text_id, "r", 1.0)
+    api.set_property_f32(text_id, "g", 1.0)
+    api.set_property_f32(text_id, "b", 1.0)
+    api.set_property_f32(text_id, "a", 1.0)
+
+    api.link_node(text_id, obj_id)
+    api.link_node(obj_id, layer_id)
+
+# Screen init
+recalc_areas()
+
+print_tree()
+print()
+print("Scene created.")
+
+while True:
+    data = subsock.recv()
+    match data:
+        case b"resize":
+            recalc_areas()
+        case b"click":
+            x = get_property("/window/input/mouse", "x")
+            y = get_property("/window/input/mouse", "y")
+            print(f"mouse clicked ({x}, {y})")
+        case b"wheel":
+            print("mouse wheely")
+

BIN
bin/darkwallet/king.png


+ 4 - 0
bin/darkwallet/pydrk/__init__.py

@@ -0,0 +1,4 @@
+from .api import Api, ErrorCode, SceneNodeType, PropertyType, vertex, face
+from .host import HostApi
+from . import exc, serial
+

+ 456 - 0
bin/darkwallet/pydrk/api.py

@@ -0,0 +1,456 @@
+import zmq
+from . import serial
+from . import exc
+
+class Command:
+    HELLO = 0
+    ADD_NODE = 1
+    REMOVE_NODE = 9
+    RENAME_NODE = 23,
+    LOOKUP_NODE_ID = 12
+    ADD_PROPERTY = 11
+    LINK_NODE = 2
+    UNLINK_NODE = 8
+    GET_INFO = 19
+    GET_CHILDREN = 4
+    GET_PARENTS = 5
+    GET_PROPERTIES = 3
+    GET_PROPERTY = 6
+    SET_PROPERTY = 7
+    GET_SIGNALS = 14
+    REGISTER_SLOT = 15
+    UNREGISTER_SLOT = 16
+    LOOKUP_SLOT_ID = 17
+    GET_SLOTS = 18
+    GET_METHODS = 20
+    GET_METHOD = 21
+    CALL_METHOD = 22
+
+class SceneNodeType:
+    NULL = 0
+    ROOT = 1
+    WINDOW = 2
+    WINDOW_INPUT = 6
+    KEYBOARD = 7
+    MOUSE = 8
+    RENDER_LAYER = 3
+    RENDER_OBJECT = 4
+    RENDER_MESH = 5
+    RENDER_TEXT = 9
+    RENDER_TEXTURE = 13
+    FONTS = 10
+    FONT = 11
+    LINE_POSITION = 12
+
+class PropertyType:
+    NULL = 0
+    BUFFER = 1
+    BOOL = 2
+    UINT32 = 3
+    FLOAT32 = 4
+    STR = 5
+    SCENE_NODE_ID = 6
+
+    @staticmethod
+    def to_str(prop_type):
+        match prop_type:
+            case PropertyType.NULL:
+                return "null"
+            case PropertyType.BUFFER:
+                return "buffer"
+            case PropertyType.BOOL:
+                return "bool"
+            case PropertyType.UINT32:
+                return "uint32"
+            case PropertyType.FLOAT32:
+                return "float32"
+            case PropertyType.STR:
+                return "str"
+            case PropertyType.SCENE_NODE_ID:
+                return "scene_node_id"
+
+class ErrorCode:
+    INVALID_SCENE_PATH = 2
+    NODE_NOT_FOUND = 3
+    CHILD_NODE_NOT_FOUND = 4
+    PARENT_NODE_NOT_FOUND = 5
+    PROPERTY_ALREADY_EXISTS = 6
+    PROPERTY_NOT_FOUND = 7
+    PROPERTY_WRONG_TYPE = 8
+    SIGNAL_ALREADY_EXISTS = 9
+    SIGNAL_NOT_FOUND = 10
+    SLOT_NOT_FOUND = 11
+    METHOD_NOT_FOUND = 12
+    NODES_ARE_LINKED = 13
+    NODES_NOT_LINKED = 14
+    NODE_HAS_PARENTS = 15
+    NODE_HAS_CHILDREN = 16
+    NODE_PARENT_NAME_CONFLICT = 17
+    NODE_CHILD_NAME_CONFLICT = 18
+    NODE_SIBLING_NAME_CONFLICT = 19
+    FILE_NOT_FOUND = 20
+
+    @staticmethod
+    def to_str(errc):
+        match errc:
+            case ErrorCode.INVALID_SCENE_PATH:
+                return "invalid_scene_path"
+            case ErrorCode.NODE_NOT_FOUND:
+                return "node_not_found"
+            case ErrorCode.CHILD_NODE_NOT_FOUND:
+                return "child_node_not_found"
+            case ErrorCode.PARENT_NODE_NOT_FOUND:
+                return "parent_node_not_found"
+            case ErrorCode.PROPERTY_ALREADY_EXISTS:
+                return "property_already_exists"
+            case ErrorCode.PROPERTY_NOT_FOUND:
+                return "property_not_found"
+            case ErrorCode.PROPERTY_WRONG_TYPE:
+                return "property_wrong_type"
+            case ErrorCode.SIGNAL_ALREADY_EXISTS:
+                return "signal_already_exists"
+            case ErrorCode.SIGNAL_NOT_FOUND:
+                return "signal_not_found "
+            case ErrorCode.SLOT_NOT_FOUND:
+                return "slot_not_found "
+            case ErrorCode.METHOD_NOT_FOUND:
+                return "method_not_found "
+            case ErrorCode.NODES_ARE_LINKED:
+                return "nodes_are_linked "
+            case ErrorCode.NODES_NOT_LINKED:
+                return "nodes_not_linked "
+            case ErrorCode.NODE_HAS_PARENTS:
+                return "node_has_parents "
+            case ErrorCode.NODE_HAS_CHILDREN:
+                return "node_has_children "
+            case ErrorCode.NODE_PARENT_NAME_CONFLICT:
+                return "node_parent_name_conflict "
+            case ErrorCode.NODE_CHILD_NAME_CONFLICT:
+                return "node_child_name_conflict "
+            case ErrorCode.NODE_SIBLING_NAME_CONFLICT:
+                return "node_sibling_name_conflict "
+            case ErrorCode.FILE_NOT_FOUND:
+                return "file_not_found"
+
+def vertex(x, y, r, g, b, a, u, v):
+    buf = bytearray()
+    serial.write_f32(buf, x)
+    serial.write_f32(buf, y)
+    serial.write_f32(buf, r)
+    serial.write_f32(buf, g)
+    serial.write_f32(buf, b)
+    serial.write_f32(buf, a)
+    serial.write_f32(buf, u)
+    serial.write_f32(buf, v)
+    return buf
+
+def face(idx1, idx2, idx3):
+    buf = bytearray()
+    serial.write_u32(buf, idx1)
+    serial.write_u32(buf, idx2)
+    serial.write_u32(buf, idx3)
+    return buf
+
+class Api:
+
+    def __init__(self, addr="[::1]", port=9484):
+        context = zmq.Context()
+        self.socket = context.socket(zmq.REQ)
+        self.socket.setsockopt(zmq.IPV6, True)
+        self.socket.connect(f"tcp://{addr}:{port}")
+
+    def _make_request(self, cmd, payload):
+        req_cmd = bytearray()
+        serial.write_u8(req_cmd, cmd)
+        self.socket.send_multipart([req_cmd, payload])
+
+        errc, reply = self.socket.recv_multipart()
+        errc = int.from_bytes(errc, "little")
+        cursor = serial.Cursor(reply)
+        match errc:
+            case 2:
+                raise exc.RequestInvalidScenePath
+            case 3:
+                raise exc.RequestNodeNotFound
+            case 4:
+                raise exc.RequestChildNodeNotFound
+            case 5:
+                raise exc.RequestParentNodeNotFound
+            case 6:
+                raise exc.RequestPropertyAlreadyExists
+            case 7:
+                raise exc.RequestPropertyNotFound
+            case 8:
+                raise exc.RequestPropertyWrongType
+            case 9:
+                raise exc.RequestSignalAlreadyExists
+            case 10:
+                raise exc.RequestSignalNotFound
+            case 11:
+                raise exc.RequestSlotNotFound
+            case 12:
+                raise exc.RequestMethodNotFound
+            case 13:
+                raise exc.RequestNodesAreLinked
+            case 14:
+                raise exc.RequestNodesNotLinked
+            case 15:
+                raise exc.RequestNodeHasParents
+            case 16:
+                raise exc.RequestNodeHasChildren
+            case 17:
+                raise exc.RequestNodeParentNameConflict
+            case 18:
+                raise exc.RequestNodeChildNameConflict
+            case 19:
+                raise exc.RequestNodeSiblingNameConflict
+            case 20:
+                raise exc.RequestFileNotFound
+        return cursor
+
+    def hello(self):
+        response = self._make_request(Command.HELLO, bytearray())
+        return serial.decode_str(response)
+
+    def get_info(self, node_id):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        cur = self._make_request(Command.GET_INFO, req)
+        name = serial.decode_str(cur)
+        type = serial.read_u8(cur)
+        return (name, type)
+
+    def get_children(self, node_id):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        cur = self._make_request(Command.GET_CHILDREN, req)
+        children_len = serial.decode_varint(cur)
+        children = []
+        for _ in range(children_len):
+            child_name = serial.decode_str(cur)
+            child_id = serial.read_u32(cur)
+            child_type = serial.read_u8(cur)
+            children.append((child_name, child_id, child_type))
+        return children
+
+    def get_parents(self, node_id):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        cur = self._make_request(Command.GET_PARENTS, req)
+        parents_len = serial.decode_varint(cur)
+        parents = []
+        for _ in range(parents_len):
+            parent_name = serial.decode_str(cur)
+            parent_id = serial.read_u32(cur)
+            parent_type = serial.read_u8(cur)
+            parents.append((parent_name, parent_id, parent_type))
+        return parents
+
+    def get_properties(self, node_id):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        cur = self._make_request(Command.GET_PROPERTIES, req)
+        props_len = serial.decode_varint(cur)
+        props = []
+        for _ in range(props_len):
+            prop_name = serial.decode_str(cur)
+            prop_type = serial.read_u8(cur)
+            props.append((prop_name, prop_type))
+        return props
+
+    def get_property(self, node_id, prop_name):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, prop_name)
+        cur = self._make_request(Command.GET_PROPERTY, req)
+        prop_type = serial.read_u8(cur)
+        match prop_type:
+            case 0:
+                return None
+            case 1:
+                return []
+            case 3:
+                val = serial.read_u8(cur)
+                return bool(val)
+            case 2:
+                return serial.read_u32(cur)
+            case 4:
+                return serial.read_f32(cur)
+            case 5:
+                return serial.decode_str(cur)
+        raise Exception("unknown property type returned")
+
+    def add_node(self, node_name, node_type):
+        req = bytearray()
+        serial.encode_str(req, node_name)
+        serial.write_u8(req, int(node_type))
+        cur = self._make_request(Command.ADD_NODE, req)
+        node_id = serial.read_u32(cur)
+        return node_id
+
+    def remove_node(self, node_id):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        self._make_request(Command.REMOVE_NODE, req)
+
+    def rename_node(self, node_id, node_name):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, node_name)
+        self._make_request(Command.RENAME_NODE, req)
+
+    def lookup_node_id(self, node_path):
+        req = bytearray()
+        serial.encode_str(req, node_path)
+        try:
+            cur = self._make_request(Command.LOOKUP_NODE_ID, req)
+        except exc.RequestNodeNotFound:
+            return None
+        return serial.read_u32(cur)
+
+    def add_property(self, node_id, prop_name, prop_type):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, prop_name)
+        serial.write_u8(req, int(prop_type))
+        self._make_request(Command.ADD_PROPERTY, req)
+
+    def link_node(self, child_id, parent_id):
+        req = bytearray()
+        serial.write_u32(req, child_id)
+        serial.write_u32(req, parent_id)
+        self._make_request(Command.LINK_NODE, req)
+
+    def unlink_node(self, child_id, parent_id):
+        req = bytearray()
+        serial.write_u32(req, child_id)
+        serial.write_u32(req, parent_id)
+        self._make_request(Command.UNLINK_NODE, req)
+
+    def set_property_bool(self, node_id, prop_name, val):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, prop_name)
+        serial.write_u8(req, int(val))
+        self._make_request(Command.SET_PROPERTY, req)
+
+    def set_property_u32(self, node_id, prop_name, val):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, prop_name)
+        serial.write_u32(req, val)
+        self._make_request(Command.SET_PROPERTY, req)
+
+    def set_property_f32(self, node_id, prop_name, val):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, prop_name)
+        serial.write_f32(req, val)
+        self._make_request(Command.SET_PROPERTY, req)
+
+    def set_property_buffer(self, node_id, prop_name, buf):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, prop_name)
+        serial.encode_buf(req, buf)
+        self._make_request(Command.SET_PROPERTY, req)
+
+    def set_property_str(self, node_id, prop_name, val):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, prop_name)
+        serial.encode_str(req, val)
+        self._make_request(Command.SET_PROPERTY, req)
+
+    def get_signals(self, node_id):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        cur = self._make_request(Command.GET_SIGNALS, req)
+        sigs_len = serial.decode_varint(cur)
+        sigs = []
+        for _ in range(sigs_len):
+            sigs.append(serial.decode_str(cur))
+        return sigs
+
+    def register_slot(self, node_id, sig_name, slot_name, user_data):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, sig_name)
+        serial.encode_str(req, slot_name)
+        serial.encode_varint(req, len(user_data))
+        req += user_data
+        cur = self._make_request(Command.REGISTER_SLOT, req)
+        slot_id = serial.read_u32(cur)
+        return slot_id
+
+    def unregister_slot(self, node_id, sig_name, slot_id):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, sig_name)
+        serial.write_u32(req, slot_id)
+        self._make_request(Command.UNREGISTER_SLOT, req)
+
+    def lookup_slot_id(self, node_id, sig_name, slot_name):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, sig_name)
+        serial.encode_str(req, slot_name)
+        try:
+            cur = self._make_request(Command.LOOKUP_SLOT_ID, req)
+        except exc.RequestSlotNotFound:
+            return None
+        return serial.read_u32(cur)
+
+    def get_slots(self, node_id, sig_name):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, sig_name)
+        cur = self._make_request(Command.GET_SLOTS, req)
+        slots_len = serial.decode_varint(cur)
+        slots = []
+        for _ in range(slots_len):
+            slot_name = serial.decode_str(cur)
+            slot_id = serial.read_u32(cur)
+            slots.append((slot_id, slot_name))
+        return slots
+
+    def get_methods(self, node_id):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        cur = self._make_request(Command.GET_METHODS, req)
+        methods_len = serial.decode_varint(cur)
+        methods = []
+        for _ in range(methods_len):
+            method_name = serial.decode_str(cur)
+            methods.append(method_name)
+        return methods
+
+    def get_method(self, node_id, method_name):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, method_name)
+        cur = self._make_request(Command.GET_METHOD, req)
+        args_len = serial.decode_varint(cur)
+        args = []
+        for _ in range(args_len):
+            arg_name = serial.decode_str(cur)
+            arg_type = serial.read_u8(cur)
+            args.append((arg_name, arg_type))
+        results_len = serial.decode_varint(cur)
+        results = []
+        for _ in range(results_len):
+            result_name = serial.decode_str(cur)
+            result_type = serial.read_u8(cur)
+            results.append((result_name, result_type))
+        return (args, results)
+
+    def call_method(self, node_id, method_name, arg_data):
+        req = bytearray()
+        serial.write_u32(req, node_id)
+        serial.encode_str(req, method_name)
+        serial.encode_buf(req, arg_data)
+        cur = self._make_request(Command.CALL_METHOD, req)
+        errc = serial.read_u8(cur)
+        result = serial.decode_buf(cur)
+        return (errc, result)
+

+ 38 - 0
bin/darkwallet/pydrk/exc.py

@@ -0,0 +1,38 @@
+class RequestInvalidScenePath(Exception):
+    pass
+class RequestNodeNotFound(Exception):
+    pass
+class RequestChildNodeNotFound(Exception):
+    pass
+class RequestParentNodeNotFound(Exception):
+    pass
+class RequestPropertyAlreadyExists(Exception):
+    pass
+class RequestPropertyNotFound(Exception):
+    pass
+class RequestPropertyWrongType(Exception):
+    pass
+class RequestSignalAlreadyExists(Exception):
+    pass
+class RequestSignalNotFound(Exception):
+    pass
+class RequestSlotNotFound(Exception):
+    pass
+class RequestMethodNotFound(Exception):
+    pass
+class RequestNodesAreLinked(Exception):
+    pass
+class RequestNodesNotLinked(Exception):
+    pass
+class RequestNodeHasParents(Exception):
+    pass
+class RequestNodeHasChildren(Exception):
+    pass
+class RequestNodeParentNameConflict(Exception):
+    pass
+class RequestNodeChildNameConflict(Exception):
+    pass
+class RequestNodeSiblingNameConflict(Exception):
+    pass
+class RequestFileNotFound(Exception):
+    pass

+ 33 - 0
bin/darkwallet/pydrk/host.py

@@ -0,0 +1,33 @@
+from . import serial, ErrorCode
+
+class HostApi:
+
+    def __init__(self, api):
+        self.api = api
+
+    def create_text(self, font_path, node_name, text, font_size):
+        call_data = bytearray()
+        serial.encode_str(call_data, node_name)
+        serial.encode_str(call_data, text)
+        serial.write_f32(call_data, font_size)
+        font_id = self.api.lookup_node_id(font_path)
+        (errc, result) = self.api.call_method(font_id, "create_text", call_data)
+        if errc != 0:
+            print("create_text error:", ErrorCode.to_str(errc))
+            return None
+        text_id = int.from_bytes(result, "little")
+        return text_id
+
+    def load_texture(self, node_name, filepath):
+        call_data = bytearray()
+        serial.encode_str(call_data, node_name)
+        serial.encode_str(call_data, filepath)
+        win_id = self.api.lookup_node_id("/window")
+        (errc, result) = self.api.call_method(win_id, "load_texture", call_data)
+        if errc != 0:
+            print("load_texture error:", ErrorCode.to_str(errc))
+            return None
+        texture_id = int.from_bytes(result, "little")
+        return texture_id
+
+

+ 111 - 0
bin/darkwallet/pydrk/serial.py

@@ -0,0 +1,111 @@
+import struct
+
+def write_u8(by, v):
+    assert v < 2**256
+    by += v.to_bytes(1, 'little')
+
+def write_u16(by, v):
+    assert v < 2**(2*256)
+    by += v.to_bytes(2, 'little')
+
+def write_u32(by, v):
+    assert v < 2**(4*256)
+    by += v.to_bytes(4, 'little')
+
+def write_u64(by, v):
+    assert v < 2**(8*256)
+    by += v.to_bytes(8, 'little')
+
+def write_f32(by, v):
+    by += struct.pack("<f", v)
+
+def encode_varint(by, v):
+    if v <= 0xfc:
+        write_u8(by, v)
+        return 1
+    elif v <= 0xffff:
+        write_u8(by, 0xfd)
+        write_u16(by, v)
+        return 3
+    elif v <= 0xffffffff:
+        write_u8(by, 0xfe)
+        write_u32(by, v)
+        return 5
+    else:
+        write_u8(by, 0xff)
+        write_u64(by, v)
+        return 9
+
+def encode_str(by, s):
+    l = 0
+    l += encode_varint(by, len(s))
+    s_by = s.encode("utf-8")
+    l += len(s_by)
+    by += s_by
+    return l
+
+def encode_buf(by, buf):
+    l = 0
+    l += encode_varint(by, len(buf))
+    l += len(buf)
+    by += buf
+    return l
+
+# Cursor for bytearray type
+class Cursor:
+
+    def __init__(self, by):
+        self.by = by
+        self.i = 0
+
+    def read(self, n):
+        slice = self.by[self.i:self.i+n]
+        self.i += n
+        if self.i > len(self.by):
+            raise Exception("invalid read")
+        return slice
+
+def read_u8(cur):
+    b = cur.read(1)
+    return int.from_bytes(b, "little")
+
+def read_u16(cur):
+    b = cur.read(2)
+    return int.from_bytes(b, "little")
+
+def read_u32(cur):
+    b = cur.read(4)
+    return int.from_bytes(b, "little")
+
+def read_u64(cur):
+    b = cur.read(8)
+    return int.from_bytes(b, "little")
+
+def read_f32(cur):
+    by = cur.read(4)
+    return struct.unpack("<f", by)[0]
+
+def decode_varint(cur):
+    n = read_u8(cur)
+    match n:
+        case 0xff:
+            x = read_u64(cur)
+            assert x >= 0x100000000
+            return x
+        case 0xfe:
+            x = read_u32(cur)
+            assert x >= 0x10000
+            return x
+        case 0xfd:
+            x = read_u16(cur)
+            assert x >= 0xfd
+            return x
+    return n
+
+def decode_str(cur):
+    return decode_buf(cur).decode("utf-8")
+
+def decode_buf(cur):
+    size = decode_varint(cur)
+    return cur.read(size)
+

+ 20 - 0
bin/darkwallet/pydrk/util.py

@@ -0,0 +1,20 @@
+import asyncio
+
+async def show_exc(foo):
+    try:
+        await foo
+    except:
+        import traceback
+        traceback.print_exc()
+        loop = asyncio.get_running_loop()
+        loop.stop()
+
+def run_async_tasks(fns):
+    loop = asyncio.new_event_loop()
+    tasks = []
+    for fn in fns:
+        task = loop.create_task(show_exc(fn))
+        tasks.append(task)
+    loop.run_forever()
+
+

+ 62 - 0
bin/darkwallet/src/error.rs

@@ -0,0 +1,62 @@
+pub type Result<T> = std::result::Result<T, Error>;
+
+#[repr(u8)]
+#[derive(Debug, Copy, Clone, thiserror::Error)]
+pub enum Error {
+    #[error("Invalid scene path")]
+    InvalidScenePath = 2,
+
+    #[error("Node not found")]
+    NodeNotFound = 3,
+
+    #[error("Child node not found")]
+    ChildNodeNotFound = 4,
+
+    #[error("Parent node not found")]
+    ParentNodeNotFound = 5,
+
+    #[error("Property already exists")]
+    PropertyAlreadyExists = 6,
+
+    #[error("Property not found")]
+    PropertyNotFound = 7,
+
+    #[error("Property has wrong type")]
+    PropertyWrongType = 8,
+
+    #[error("Signal already exists")]
+    SignalAlreadyExists = 9,
+
+    #[error("Signal not found")]
+    SignalNotFound = 10,
+
+    #[error("Slot not found")]
+    SlotNotFound = 11,
+
+    #[error("Method not found")]
+    MethodNotFound = 12,
+
+    #[error("Nodes are not linked")]
+    NodesAreLinked = 13,
+
+    #[error("Nodes are not linked")]
+    NodesNotLinked = 14,
+
+    #[error("Node has parents")]
+    NodeHasParents = 15,
+
+    #[error("Node has children")]
+    NodeHasChildren = 16,
+
+    #[error("Node has a parent with this name")]
+    NodeParentNameConflict = 17,
+
+    #[error("Node has a child with this name")]
+    NodeChildNameConflict = 18,
+
+    #[error("Node has a sibling with this name")]
+    NodeSiblingNameConflict = 19,
+
+    #[error("File not found")]
+    FileNotFound = 20,
+}

+ 1136 - 0
bin/darkwallet/src/gfx.rs

@@ -0,0 +1,1136 @@
+use fontdue::{Font, FontSettings, layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle, GlyphPosition}};
+use darkfi_serial::{Decodable, Encodable, SerialEncodable};
+use std::{fmt, io::Cursor};
+use miniquad::*;
+
+use crate::{error::{Error, Result},
+scene::{
+    SceneNode,
+    PropertyType,
+    SceneGraph,
+    SceneGraphPtr,
+    SceneNodeType,
+    SceneNodeId,
+}, shader};
+
+trait MouseButtonAsU8 {
+    fn to_u8(&self) -> u8;
+}
+
+impl MouseButtonAsU8 for MouseButton {
+    fn to_u8(&self) -> u8 {
+        match self {
+            MouseButton::Left => 0,
+            MouseButton::Middle => 1,
+            MouseButton::Right => 2,
+            MouseButton::Unknown => 3,
+        }
+    }
+}
+
+#[derive(Debug, SerialEncodable)]
+#[repr(C)]
+struct Vertex {
+    pos: [f32; 2],
+    color: [f32; 4],
+    uv: [f32; 2],
+}
+
+#[derive(SerialEncodable)]
+#[repr(C)]
+struct Face {
+    idxs: [u32; 3],
+}
+
+impl fmt::Debug for Face {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{:?}", self.idxs)
+    }
+}
+
+struct Stage {
+    ctx: Box<dyn RenderingBackend>,
+    pipeline: Pipeline,
+
+    scene_graph: SceneGraphPtr,
+
+    white_texture: TextureId,
+    font: Font,
+}
+
+impl Stage {
+    pub fn new(scene_graph: SceneGraphPtr) -> Stage {
+        let mut ctx: Box<dyn RenderingBackend> = window::new_rendering_backend();
+
+        let white_texture = ctx.new_texture_from_rgba8(1, 1, &[255, 255, 255, 255]);
+
+        let mut shader_meta: ShaderMeta = shader::meta();
+        shader_meta
+            .uniforms
+            .uniforms
+            .push(UniformDesc::new("Projection", UniformType::Mat4));
+        shader_meta
+            .uniforms
+            .uniforms
+            .push(UniformDesc::new("Model", UniformType::Mat4));
+
+        let shader = ctx
+            .new_shader(
+                match ctx.info().backend {
+                    Backend::OpenGl => ShaderSource::Glsl {
+                        vertex: shader::GL_VERTEX,
+                        fragment: shader::GL_FRAGMENT,
+                    },
+                    Backend::Metal => ShaderSource::Msl {
+                        program: shader::METAL,
+                    },
+                },
+                shader_meta,
+            )
+            .unwrap();
+
+        let params = PipelineParams {
+            color_blend: Some(BlendState::new(
+                Equation::Add,
+                BlendFactor::Value(BlendValue::SourceAlpha),
+                BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
+            )),
+            ..Default::default()
+        };
+
+        let pipeline = ctx.new_pipeline(
+            &[BufferLayout::default()],
+            &[
+                VertexAttribute::new("in_pos", VertexFormat::Float2),
+                VertexAttribute::new("in_color", VertexFormat::Float4),
+                VertexAttribute::new("in_uv", VertexFormat::Float2),
+            ],
+            shader,
+            params,
+        );
+
+        let font = {
+            let mut scene_graph = scene_graph.lock().unwrap();
+
+            let font = scene_graph.add_node("font", SceneNodeType::Fonts);
+            let font_id = font.id;
+            scene_graph.link(font_id, SceneGraph::ROOT_ID).unwrap();
+
+            let inter_regular = scene_graph.add_node("inter-regular", SceneNodeType::Font);
+
+            let font = include_bytes!("../Inter-Regular.ttf") as &[u8];
+            let font = Font::from_bytes(font, FontSettings::default()).unwrap();
+            let line_metrics = font.horizontal_line_metrics(1.).unwrap();
+            inter_regular
+                .add_property("new_line_size", PropertyType::Float32)
+                .unwrap()
+                .set_f32(line_metrics.new_line_size)
+                .unwrap();
+
+            inter_regular.add_method("create_text",
+                vec![
+                    ("text", PropertyType::Str),
+                    ("font_size", PropertyType::Float32),
+                ],
+                vec![
+                    ("node_id", PropertyType::SceneNodeId)
+                ],
+            );
+
+            let inter_regular_id = inter_regular.id;
+            scene_graph.link(inter_regular_id, font_id).unwrap();
+            font
+        };
+
+        let mut stage = Stage {
+            ctx,
+            pipeline,
+            scene_graph,
+            white_texture,
+            font,
+        };
+        stage.setup_scene_graph_window();
+        stage
+    }
+
+    fn setup_scene_graph_window(&mut self) {
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+
+        let window = scene_graph.add_node("window", SceneNodeType::Window);
+        let (screen_width, screen_height) = window::screen_size();
+        window
+            .add_property("width", PropertyType::Float32)
+            .unwrap()
+            .set_f32(screen_width)
+            .unwrap();
+        window
+            .add_property("height", PropertyType::Float32)
+            .unwrap()
+            .set_f32(screen_height)
+            .unwrap();
+        window.add_signal("resize").unwrap();
+        window.add_method("load_texture",
+            vec![
+                ("node_name", PropertyType::Str),
+                ("path", PropertyType::Str),
+            ],
+            vec![
+                ("node_id", PropertyType::SceneNodeId)
+            ],
+        );
+        let window_id = window.id;
+        scene_graph.link(window_id, SceneGraph::ROOT_ID).unwrap();
+
+        let input = scene_graph.add_node("input", SceneNodeType::WindowInput);
+        let input_id = input.id;
+        scene_graph.link(input_id, window_id).unwrap();
+
+        let keyb = scene_graph.add_node("keyboard", SceneNodeType::Keyboard);
+        keyb.add_property("shift", PropertyType::Bool).unwrap();
+        keyb.add_property("ctrl", PropertyType::Bool).unwrap();
+        keyb.add_property("alt", PropertyType::Bool).unwrap();
+        keyb.add_property("logo", PropertyType::Bool).unwrap();
+        keyb.add_property("repeat", PropertyType::Bool).unwrap();
+        keyb.add_property("keycode", PropertyType::Str).unwrap();
+        keyb.add_signal("key_down").unwrap();
+        let keyb_id = keyb.id;
+        scene_graph.link(keyb_id, input_id).unwrap();
+
+        let mouse = scene_graph.add_node("mouse", SceneNodeType::Mouse);
+        mouse.add_property("button", PropertyType::Uint32).unwrap();
+        mouse.add_property("x", PropertyType::Float32).unwrap();
+        mouse.add_property("y", PropertyType::Float32).unwrap();
+        mouse.add_signal("button_up").unwrap();
+        mouse.add_signal("button_down").unwrap();
+        mouse.add_signal("wheel").unwrap();
+        mouse.add_signal("move").unwrap();
+        let mouse_id = mouse.id;
+        scene_graph.link(mouse_id, input_id).unwrap();
+
+        let layer1 = scene_graph.add_node("layer1", SceneNodeType::RenderLayer);
+        let is_visible = layer1
+            .add_property("is_visible", PropertyType::Bool)
+            .unwrap();
+        is_visible.set_bool(true).unwrap();
+        layer1
+            .add_property("rect_x", PropertyType::Uint32)
+            .unwrap()
+            .set_u32(0)
+            .unwrap();
+        layer1
+            .add_property("rect_y", PropertyType::Uint32)
+            .unwrap()
+            .set_u32(0)
+            .unwrap();
+        layer1
+            .add_property("rect_w", PropertyType::Uint32)
+            .unwrap()
+            .set_u32(0)
+            .unwrap();
+        layer1
+            .add_property("rect_h", PropertyType::Uint32)
+            .unwrap()
+            .set_u32(0)
+            .unwrap();
+        let layer1_id = layer1.id;
+        scene_graph.link(layer1_id, window_id).unwrap();
+
+        let layer2 = scene_graph.add_node("layer2", SceneNodeType::RenderLayer);
+        let is_visible = layer2
+            .add_property("is_visible", PropertyType::Bool)
+            .unwrap();
+        is_visible.set_bool(true).unwrap();
+        layer2
+            .add_property("rect_x", PropertyType::Uint32)
+            .unwrap()
+            .set_u32(0)
+            .unwrap();
+        layer2
+            .add_property("rect_y", PropertyType::Uint32)
+            .unwrap()
+            .set_u32(0)
+            .unwrap();
+        layer2
+            .add_property("rect_w", PropertyType::Uint32)
+            .unwrap()
+            .set_u32(0)
+            .unwrap();
+        layer2
+            .add_property("rect_h", PropertyType::Uint32)
+            .unwrap()
+            .set_u32(0)
+            .unwrap();
+        let layer2_id = layer2.id;
+        scene_graph.link(layer2_id, window_id).unwrap();
+
+        let funky_square = scene_graph.add_node("funky_square", SceneNodeType::RenderObject);
+        funky_square
+            .add_property("x", PropertyType::Float32)
+            .unwrap()
+            .set_f32(0.)
+            .unwrap();
+        funky_square
+            .add_property("y", PropertyType::Float32)
+            .unwrap()
+            .set_f32(0.)
+            .unwrap();
+        funky_square
+            .add_property("scale", PropertyType::Float32)
+            .unwrap()
+            .set_f32(0.)
+            .unwrap();
+        let funky_square_id = funky_square.id;
+        scene_graph.link(funky_square_id, layer1_id).unwrap();
+
+        let funky_mesh = scene_graph.add_node("mesh", SceneNodeType::RenderMesh);
+        let verts = funky_mesh
+            .add_property("verts", PropertyType::Buffer)
+            .unwrap();
+        let faces = funky_mesh
+            .add_property("faces", PropertyType::Buffer)
+            .unwrap();
+        let mut buf = vec![];
+        // top left
+        Vertex {
+            pos: [0., 0.],
+            color: [1., 0., 1., 1.],
+            uv: [0., 0.],
+        }
+        .encode(&mut buf)
+        .unwrap();
+        // top right
+        Vertex {
+            pos: [0.5, 0.],
+            color: [1., 1., 0., 1.],
+            uv: [1., 0.],
+        }
+        .encode(&mut buf)
+        .unwrap();
+        // bottom left
+        Vertex {
+            pos: [0., 0.5],
+            color: [0., 0., 0.8, 1.],
+            uv: [0., 1.],
+        }
+        .encode(&mut buf)
+        .unwrap();
+        // bottom right
+        Vertex {
+            pos: [0.5, 0.5],
+            color: [1., 1., 0., 1.],
+            uv: [1., 1.],
+        }
+        .encode(&mut buf)
+        .unwrap();
+        verts.set_buf(buf).unwrap();
+
+        let mut buf = vec![];
+        Face { idxs: [0, 2, 1] }.encode(&mut buf).unwrap();
+        Face { idxs: [1, 2, 3] }.encode(&mut buf).unwrap();
+        faces.set_buf(buf).unwrap();
+        let funky_mesh_id = funky_mesh.id;
+        scene_graph.link(funky_mesh_id, funky_square_id).unwrap();
+    }
+
+    fn draw_glyph(ctx: &mut Box<dyn RenderingBackend>, proj: &glam::Mat4, model: &glam::Mat4, font: &Font, glyph_pos: &GlyphPosition, color: [f32; 4]) {
+        let (screen_width, screen_height) = window::screen_size();
+        //let proj =
+        //    glam::Mat4::from_translation(glam::Vec3::new(-1., 1., 0.)) *
+        //    glam::Mat4::from_scale(glam::Vec3::new(2./screen_width, -2./screen_height, 1.));
+        //let model = glam::Mat4::IDENTITY;
+        let model = *model * glam::Mat4::from_scale(glam::Vec3::new(1./screen_width, 1./screen_height, 1.));
+
+        let mut uniforms_data = [0u8; 128];
+        let data: [u8; 64] = unsafe { std::mem::transmute_copy(proj) };
+        uniforms_data[0..64].copy_from_slice(&data);
+        let data: [u8; 64] = unsafe { std::mem::transmute_copy(&model) };
+        uniforms_data[64..].copy_from_slice(&data);
+        assert_eq!(128, 2 * UniformType::Mat4.size());
+
+        ctx
+            .apply_uniforms_from_bytes(uniforms_data.as_ptr(), uniforms_data.len());
+
+        let (font_metrics, text_bitmap) = font.rasterize(glyph_pos.parent, glyph_pos.key.px);
+        let text_bitmap: Vec<_> = text_bitmap
+            .iter()
+            .flat_map(|coverage| vec![255, 255, 255, *coverage])
+            .collect();
+
+        let (x, y) = (glyph_pos.x, glyph_pos.y);
+        let (w, h) = (glyph_pos.width as f32, glyph_pos.height as f32);
+        //    0             1
+        // (-1, 1) ----- (1, 1)
+        //    |          /  |
+        //    |        /    |
+        //    |      /      |
+        //    |    /        |
+        // (-1, -1) ---- (1, -1)
+        //    2             3
+        //
+        // faces: 021, 123
+        let vertices: [Vertex; 4] = 
+            [
+                // top left
+                Vertex {
+                    pos: [x, y],
+                    color,
+                    uv: [0., 0.],
+                },
+                // top right
+                Vertex {
+                    pos: [x + w, y],
+                    color,
+                    uv: [1., 0.],
+                },
+                // bottom left
+                Vertex {
+                    pos: [x, y + h],
+                    color,
+                    uv: [0., 1.],
+                },
+                // bottom right
+                Vertex {
+                    pos: [x + w, y + h],
+                    color,
+                    uv: [1., 1.],
+                },
+            ];
+
+        //debug!("screen size: {:?}", window::screen_size());
+        let vertex_buffer = ctx.new_buffer(
+            BufferType::VertexBuffer,
+            BufferUsage::Immutable,
+            BufferSource::slice(&vertices),
+        );
+
+        let indices: [u16; 6] = [0, 2, 1, 1, 2, 3];
+        let index_buffer = ctx.new_buffer(
+            BufferType::IndexBuffer,
+            BufferUsage::Immutable,
+            BufferSource::slice(&indices),
+        );
+
+        let texture = 
+            //self.king_texture;
+            ctx.new_texture_from_rgba8(
+                font_metrics.width as u16,
+                font_metrics.height as u16,
+                &text_bitmap,
+            );
+
+        let bindings = Bindings {
+            vertex_buffers: vec![vertex_buffer],
+            index_buffer: index_buffer,
+            images: vec![texture],
+        };
+
+        ctx.apply_bindings(&bindings);
+        ctx.draw(0, 6, 1);
+    }
+
+    fn create_text_node(&self, font_node_id: SceneNodeId, font_name: &str, node_name: String, text: String, font_size: f32) -> Result<SceneNodeId> {
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+        let text_node = scene_graph.add_node(node_name, SceneNodeType::RenderText);
+        text_node
+            .add_property("text", PropertyType::Str)
+            .unwrap()
+            .set_str(text.clone())
+            .unwrap();
+        text_node
+            .add_property("font_size", PropertyType::Float32)
+            .unwrap()
+            .set_f32(font_size)
+            .unwrap();
+        text_node
+            .add_property("r", PropertyType::Float32)
+            .unwrap()
+            .set_f32(0.)
+            .unwrap();
+        text_node
+            .add_property("g", PropertyType::Float32)
+            .unwrap()
+            .set_f32(0.)
+            .unwrap();
+        text_node
+            .add_property("b", PropertyType::Float32)
+            .unwrap()
+            .set_f32(0.)
+            .unwrap();
+        text_node
+            .add_property("a", PropertyType::Float32)
+            .unwrap()
+            .set_f32(0.)
+            .unwrap();
+
+        let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
+        layout.reset(&LayoutSettings {
+            ..LayoutSettings::default()
+        });
+        let font = match font_name {
+            "inter-regular" => {
+                &self.font
+            }
+            _ => panic!("unknown font name!")
+        };
+        let fonts = [font];
+        layout.append(&fonts, &TextStyle::new(&text, font_size, 0));
+
+        text_node
+            .add_property("height", PropertyType::Float32)
+            .unwrap()
+            .set_f32(layout.height())
+            .unwrap();
+
+        // Calculate the text width
+        // std::cmp::max() not impl for f32
+        let max_f32 = |x: f32, y: f32| {
+            if x > y { x } else { y }
+        };
+
+        // TODO: this calc isn't multiline, we should add width property to each line
+        let mut total_width = 0.;
+        for glyph_pos in layout.glyphs() {
+            let right = glyph_pos.x + glyph_pos.width as f32;
+            total_width = max_f32(total_width, right);
+        }
+
+        text_node
+            .add_property("width", PropertyType::Float32)
+            .unwrap()
+            .set_f32(total_width)
+            .unwrap();
+
+        let text_node_id = text_node.id;
+
+        for (idx, line) in layout.lines().unwrap().into_iter().enumerate() {
+            let line_node_name = format!("line.{}", idx);
+            let line_node = scene_graph.add_node(line_node_name, SceneNodeType::LinePosition);
+            line_node
+                .add_property("idx", PropertyType::Uint32)
+                .unwrap()
+                .set_u32(idx as u32)
+                .unwrap();
+            line_node
+                .add_property("baseline_y", PropertyType::Float32)
+                .unwrap()
+                .set_f32(line.baseline_y)
+                .unwrap();
+            line_node
+                .add_property("padding", PropertyType::Float32)
+                .unwrap()
+                .set_f32(line.padding)
+                .unwrap();
+            line_node
+                .add_property("max_ascent", PropertyType::Float32)
+                .unwrap()
+                .set_f32(line.max_ascent)
+                .unwrap();
+            line_node
+                .add_property("min_descent", PropertyType::Float32)
+                .unwrap()
+                .set_f32(line.min_descent)
+                .unwrap();
+            line_node
+                .add_property("max_line_gap", PropertyType::Float32)
+                .unwrap()
+                .set_f32(line.max_line_gap)
+                .unwrap();
+            line_node
+                .add_property("glyph_start", PropertyType::Uint32)
+                .unwrap()
+                .set_u32(line.glyph_start as u32)
+                .unwrap();
+            line_node
+                .add_property("glyph_end", PropertyType::Uint32)
+                .unwrap()
+                .set_u32(line.glyph_end as u32)
+                .unwrap();
+
+            let line_node_id = line_node.id;
+            scene_graph.link(line_node_id, text_node_id)?;
+        }
+
+        scene_graph.link(font_node_id, text_node_id)?;
+        Ok(text_node_id)
+    }
+
+    fn load_texture(&self, node_name: String, filepath: String) -> Result<SceneNodeId> {
+        let Ok(img) = image::open(filepath) else {
+            return Err(Error::FileNotFound)
+        };
+
+        let img = img.to_rgba8();
+        let width = img.width();
+        let height = img.height();
+        let bmp = img.into_raw();
+
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+        let img_node = scene_graph.add_node(node_name, SceneNodeType::RenderTexture);
+        img_node
+            .add_property("width", PropertyType::Uint32)
+            .unwrap()
+            .set_u32(width)
+            .unwrap();
+        img_node
+            .add_property("height", PropertyType::Uint32)
+            .unwrap()
+            .set_u32(height)
+            .unwrap();
+        img_node
+            .add_property("bmp", PropertyType::Buffer)
+            .unwrap()
+            .set_buf(bmp)
+            .unwrap();
+        //let king_texture = ctx.new_texture_from_rgba8(width, height, &king_bitmap);
+        Ok(img_node.id)
+    }
+}
+
+fn get_obj_props(obj: &SceneNode) -> Result<(f32, f32, f32)> {
+    let x = obj.get_property("x").ok_or(Error::PropertyNotFound)?.get_f32()?;
+    let y = obj.get_property("y").ok_or(Error::PropertyNotFound)?.get_f32()?;
+    let scale = obj.get_property("scale").ok_or(Error::PropertyNotFound)?.get_f32()?;
+    Ok((x, y, scale))
+}
+
+fn get_text_props(render_text: &SceneNode) -> Result<(String, f32, [f32; 4])> {
+    let text = render_text.get_property("text").ok_or(Error::PropertyNotFound)?.get_str()?;
+    let font_size = render_text.get_property("font_size").ok_or(Error::PropertyNotFound)?.get_f32()?;
+    let r = render_text.get_property("r").ok_or(Error::PropertyNotFound)?.get_f32()?;
+    let g = render_text.get_property("g").ok_or(Error::PropertyNotFound)?.get_f32()?;
+    let b = render_text.get_property("b").ok_or(Error::PropertyNotFound)?.get_f32()?;
+    let a = render_text.get_property("a").ok_or(Error::PropertyNotFound)?.get_f32()?;
+    let color = [r, g, b, a];
+    Ok((text, font_size, color))
+}
+
+impl EventHandler for Stage {
+    fn update(&mut self) {
+        // check /font:create_text() queue
+
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+        let font_root = 
+            scene_graph
+            .lookup_node("/font")
+            .unwrap();
+        let font_ids: Vec<_> = font_root.iter_children(&scene_graph, SceneNodeType::Font).map(|node| node.id).collect();
+
+        let mut calls = vec![];
+        for font_id in font_ids
+        {
+            let font_node = scene_graph.get_node_mut(font_id).unwrap();
+            for method in &mut font_node.methods {
+                for (arg_data, response_fn) in std::mem::take(&mut method.queue) {
+                    calls.push((font_node.id, font_node.name.clone(), method.name.clone(), arg_data, response_fn));
+                }
+            }
+        }
+        drop(scene_graph);
+
+        for (font_node_id, font_node_name, method_name, arg_data, response_fn) in calls {
+            assert_eq!(method_name, "create_text");
+
+            let mut cur = Cursor::new(&arg_data);
+            let mut reply = vec![];
+            let node_name = String::decode(&mut cur).unwrap();
+            let text = String::decode(&mut cur).unwrap();
+            let font_size = f32::decode(&mut cur).unwrap();
+            debug!(target: "win", "/font:{}({}, {})", method_name, text, font_size);
+
+            let node_id_result = self.create_text_node(font_node_id, &font_node_name, node_name, text, font_size);
+            let node_id_result = node_id_result.map(|node_id| {
+                node_id.encode(&mut reply).unwrap();
+                reply
+            });
+            response_fn(node_id_result)
+        }
+
+        // check /window:load_texture() queue
+
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+        let mut calls = vec![];
+        let window = 
+            scene_graph
+            .lookup_node_mut("/window")
+            .unwrap();
+        for method in &mut window.methods {
+            for (arg_data, response_fn) in std::mem::take(&mut method.queue) {
+                calls.push((method.name.clone(), arg_data, response_fn));
+            }
+        }
+        drop(scene_graph);
+
+        for (method_name, arg_data, response_fn) in calls {
+            assert_eq!(method_name, "load_texture");
+
+            let mut cur = Cursor::new(&arg_data);
+            let mut reply = vec![];
+            let node_name = String::decode(&mut cur).unwrap();
+            let filepath = String::decode(&mut cur).unwrap();
+            debug!(target: "win", "/window:{}({}, {})", method_name, node_name, filepath);
+
+            let node_id_result = self.load_texture(node_name, filepath);
+            let node_id_result = node_id_result.map(|node_id| {
+                node_id.encode(&mut reply).unwrap();
+                reply
+            });
+            response_fn(node_id_result)
+        }
+    }
+
+    // Only do drawing here. Apps might not call this when minimized.
+    fn draw(&mut self) {
+        let clear = PassAction::clear_color(0., 0., 0., 1.);
+        self.ctx.begin_default_pass(clear);
+        self.ctx.end_render_pass();
+
+        // This will make the top left (0, 0) and the bottom right (1, 1)
+        // Default is (-1, 1) -> (1, -1)
+        let proj =
+            glam::Mat4::from_translation(glam::Vec3::new(-1., 1., 0.)) *
+            glam::Mat4::from_scale(glam::Vec3::new(2., -2., 1.));
+
+        // Reusable text layout
+        let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
+
+        let scene_graph = self.scene_graph.lock().unwrap();
+
+        for layer in 
+            scene_graph
+            .lookup_node("/window")
+            .expect("no window attached!")
+            .iter_children(&scene_graph, SceneNodeType::RenderLayer)
+        {
+            let is_visible = layer
+                .get_property("is_visible")
+                .unwrap()
+                .get_bool()
+                .unwrap();
+            if !is_visible {
+                continue;
+            }
+
+            //self.ctx.begin_default_pass(Default::default());
+            self.ctx.begin_default_pass(PassAction::Nothing);
+            self.ctx.apply_pipeline(&self.pipeline);
+
+            let rect_x = layer
+                .get_property("rect_x")
+                .unwrap()
+                .get_u32()
+                .unwrap();
+            let rect_y = layer
+                .get_property("rect_y")
+                .unwrap()
+                .get_u32()
+                .unwrap();
+            let rect_w = layer
+                .get_property("rect_w")
+                .unwrap()
+                .get_u32()
+                .unwrap();
+            let rect_h = layer
+                .get_property("rect_h")
+                .unwrap()
+                .get_u32()
+                .unwrap();
+
+            self.ctx
+                .apply_viewport(rect_x as i32, rect_y as i32, rect_w as i32, rect_h as i32);
+            self.ctx
+                .apply_scissor_rect(rect_x as i32, rect_y as i32, rect_w as i32, rect_h as i32);
+
+            'outer: for obj in layer.iter_children(&scene_graph, SceneNodeType::RenderObject) {
+                let Ok((x, y, scale)) = get_obj_props(obj) else {
+                    error!("obj '{}':{} has a property error", obj.name, obj.id);
+                    continue
+                };
+
+                let model =
+                    glam::Mat4::from_translation(glam::Vec3::new(x, y, 0.)) *
+                    glam::Mat4::from_scale(glam::Vec3::new(scale, scale, 1.));
+
+                let texture_id = 'texture: {
+                    let Some(texture_node) = obj.iter_children(&scene_graph, SceneNodeType::RenderTexture).next() else {
+                        break 'texture self.white_texture
+                    };
+
+                    let Some(width_prop) = texture_node.get_property("width") else {
+                        error!("texture '{}':{} missing property width", texture_node.name, texture_node.id);
+                        continue 'outer
+                    };
+                    let Ok(width) = width_prop.get_u32() else {
+                        error!("texture '{}':{} width property has wrong type", texture_node.name, texture_node.id);
+                        continue 'outer
+                    };
+
+                    let Some(height_prop) = texture_node.get_property("height") else {
+                        error!("texture '{}':{} missing property height", texture_node.name, texture_node.id);
+                        continue 'outer
+                    };
+                    let Ok(height) = height_prop.get_u32() else {
+                        error!("texture '{}':{} height property has wrong type", texture_node.name, texture_node.id);
+                        continue 'outer
+                    };
+
+                    let Some(bmp_prop) = texture_node.get_property("bmp") else {
+                        error!("texture '{}':{} missing property bmp", texture_node.name, texture_node.id);
+                        continue 'outer
+                    };
+                    let Ok(bmp) = bmp_prop.get_buf() else {
+                        error!("texture '{}':{} bmp property has wrong type", texture_node.name, texture_node.id);
+                        continue 'outer
+                    };
+
+                    let texture_id = self.ctx.new_texture_from_rgba8(width as u16, height as u16, &bmp);
+                    texture_id
+                };
+
+                for mesh in obj.iter_children(&scene_graph, SceneNodeType::RenderMesh) {
+                    let Some(verts_prop) = mesh.get_property("verts") else {
+                        error!("mesh '{}':{} missing property verts", mesh.name, mesh.id);
+                        continue
+                    };
+                    let Ok(verts) = verts_prop.get_buf() else {
+                        error!("mesh '{}':{} verts property has wrong type", mesh.name, mesh.id);
+                        continue
+                    };
+
+                    let Some(faces_prop) = mesh.get_property("faces") else {
+                        error!("mesh '{}':{} missing property faces", mesh.name, mesh.id);
+                        continue
+                    };
+                    let Ok(faces) = faces_prop.get_buf() else {
+                        error!("mesh '{}':{} faces property has wrong type", mesh.name, mesh.id);
+                        continue
+                    };
+
+                    let vertex_buffer = self.ctx.new_buffer(
+                        BufferType::VertexBuffer,
+                        BufferUsage::Immutable,
+                        BufferSource::slice(&verts),
+                    );
+
+                    let bufsrc = unsafe {
+                        BufferSource::pointer(
+                            faces.as_ptr() as _,
+                            std::mem::size_of_val(&faces[..]),
+                            std::mem::size_of::<u32>(),
+                        )
+                    };
+
+                    let index_buffer = self.ctx.new_buffer(
+                        BufferType::IndexBuffer,
+                        BufferUsage::Immutable,
+                        bufsrc,
+                    );
+
+                    let bindings = Bindings {
+                        vertex_buffers: vec![vertex_buffer],
+                        index_buffer: index_buffer,
+                        images: vec![texture_id],
+                    };
+
+                    self.ctx.apply_bindings(&bindings);
+
+                    let mut uniforms_data = [0u8; 128];
+                    let data: [u8; 64] = unsafe { std::mem::transmute_copy(&proj) };
+                    uniforms_data[0..64].copy_from_slice(&data);
+                    let data: [u8; 64] = unsafe { std::mem::transmute_copy(&model) };
+                    uniforms_data[64..].copy_from_slice(&data);
+                    assert_eq!(128, 2 * UniformType::Mat4.size());
+
+                    self.ctx
+                        .apply_uniforms_from_bytes(uniforms_data.as_ptr(), uniforms_data.len());
+
+                    self.ctx.draw(0, 3 * faces.len() as i32, 1);
+                }
+
+                for render_text in obj.iter_children(&scene_graph, SceneNodeType::RenderText) {
+                    let Ok((text, font_size, color)) = get_text_props(render_text) else {
+                        error!("text '{}':{} has a property error", render_text.name, render_text.id);
+                        continue
+                    };
+
+                    let Some(font_node) = render_text.iter_children(&scene_graph, SceneNodeType::Font).next() else {
+                        error!("text '{}':{} missing a font node", render_text.name, render_text.id);
+                        continue
+                    };
+                    // No other fonts supported yet
+                    assert_eq!(font_node.name, "inter-regular");
+
+                    layout.reset(&LayoutSettings {
+                        ..LayoutSettings::default()
+                    });
+                    let fonts = [&self.font];
+                    layout.append(&fonts, &TextStyle::new(&text, font_size, 0));
+
+                    for glyph_pos in layout.glyphs() {
+                        Self::draw_glyph(&mut self.ctx, &proj, &model, &self.font, glyph_pos, color);
+                    }
+                }
+            }
+
+            self.ctx.end_render_pass();
+        }
+        self.ctx.commit_frame();
+    }
+
+    fn key_down_event(&mut self, keycode: KeyCode, modifiers: KeyMods, repeat: bool) {
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+        let win = 
+            scene_graph
+            .lookup_node_mut("/window/input/keyboard")
+            .unwrap();
+
+        win.get_property("shift")
+            .unwrap()
+            .set_bool(modifiers.shift)
+            .unwrap();
+        win.get_property("ctrl")
+            .unwrap()
+            .set_bool(modifiers.ctrl)
+            .unwrap();
+        win.get_property("alt")
+            .unwrap()
+            .set_bool(modifiers.alt)
+            .unwrap();
+        win.get_property("logo")
+            .unwrap()
+            .set_bool(modifiers.logo)
+            .unwrap();
+        win.get_property("repeat")
+            .unwrap()
+            .set_bool(repeat)
+            .unwrap();
+
+        let send_key_down = |key: &str| {
+            win.get_property("keycode").unwrap().set_str(key).unwrap();
+            win.trigger("key_down").unwrap();
+        };
+
+        match keycode {
+            KeyCode::Space => send_key_down("Space"),
+            KeyCode::Apostrophe => send_key_down("Apostrophe"),
+            KeyCode::Comma => send_key_down("Comma"),
+            KeyCode::Minus => send_key_down("Minus"),
+            KeyCode::Period => send_key_down("Period"),
+            KeyCode::Slash => send_key_down("Slash"),
+            KeyCode::Key0 => send_key_down("Key0"),
+            KeyCode::Key1 => send_key_down("Key1"),
+            KeyCode::Key2 => send_key_down("Key2"),
+            KeyCode::Key3 => send_key_down("Key3"),
+            KeyCode::Key4 => send_key_down("Key4"),
+            KeyCode::Key5 => send_key_down("Key5"),
+            KeyCode::Key6 => send_key_down("Key6"),
+            KeyCode::Key7 => send_key_down("Key7"),
+            KeyCode::Key8 => send_key_down("Key8"),
+            KeyCode::Key9 => send_key_down("Key9"),
+            KeyCode::Semicolon => send_key_down("Semicolon"),
+            KeyCode::Equal => send_key_down("Equal"),
+            KeyCode::A => send_key_down("A"),
+            KeyCode::B => send_key_down("B"),
+            KeyCode::C => send_key_down("C"),
+            KeyCode::D => send_key_down("D"),
+            KeyCode::E => send_key_down("E"),
+            KeyCode::F => send_key_down("F"),
+            KeyCode::G => send_key_down("G"),
+            KeyCode::H => send_key_down("H"),
+            KeyCode::I => send_key_down("I"),
+            KeyCode::J => send_key_down("J"),
+            KeyCode::K => send_key_down("K"),
+            KeyCode::L => send_key_down("L"),
+            KeyCode::M => send_key_down("M"),
+            KeyCode::N => send_key_down("N"),
+            KeyCode::O => send_key_down("O"),
+            KeyCode::P => send_key_down("P"),
+            KeyCode::Q => send_key_down("Q"),
+            KeyCode::R => send_key_down("R"),
+            KeyCode::S => send_key_down("S"),
+            KeyCode::T => send_key_down("T"),
+            KeyCode::U => send_key_down("U"),
+            KeyCode::V => send_key_down("V"),
+            KeyCode::W => send_key_down("W"),
+            KeyCode::X => send_key_down("X"),
+            KeyCode::Y => send_key_down("Y"),
+            KeyCode::Z => send_key_down("Z"),
+            KeyCode::LeftBracket => send_key_down("LeftBracket"),
+            KeyCode::Backslash => send_key_down("Backslash"),
+            KeyCode::RightBracket => send_key_down("RightBracket"),
+            KeyCode::GraveAccent => send_key_down("GraveAccent"),
+            KeyCode::World1 => send_key_down("World1"),
+            KeyCode::World2 => send_key_down("World2"),
+            KeyCode::Escape => send_key_down("Escape"),
+            KeyCode::Enter => send_key_down("Enter"),
+            KeyCode::Tab => send_key_down("Tab"),
+            KeyCode::Backspace => send_key_down("Backspace"),
+            KeyCode::Insert => send_key_down("Insert"),
+            KeyCode::Delete => send_key_down("Delete"),
+            KeyCode::Right => send_key_down("Right"),
+            KeyCode::Left => send_key_down("Left"),
+            KeyCode::Down => send_key_down("Down"),
+            KeyCode::Up => send_key_down("Up"),
+            KeyCode::PageUp => send_key_down("PageUp"),
+            KeyCode::PageDown => send_key_down("PageDown"),
+            KeyCode::Home => send_key_down("Home"),
+            KeyCode::End => send_key_down("End"),
+            KeyCode::CapsLock => send_key_down("CapsLock"),
+            KeyCode::ScrollLock => send_key_down("ScrollLock"),
+            KeyCode::NumLock => send_key_down("NumLock"),
+            KeyCode::PrintScreen => send_key_down("PrintScreen"),
+            KeyCode::Pause => send_key_down("Pause"),
+            KeyCode::F1 => send_key_down("F1"),
+            KeyCode::F2 => send_key_down("F2"),
+            KeyCode::F3 => send_key_down("F3"),
+            KeyCode::F4 => send_key_down("F4"),
+            KeyCode::F5 => send_key_down("F5"),
+            KeyCode::F6 => send_key_down("F6"),
+            KeyCode::F7 => send_key_down("F7"),
+            KeyCode::F8 => send_key_down("F8"),
+            KeyCode::F9 => send_key_down("F9"),
+            KeyCode::F10 => send_key_down("F10"),
+            KeyCode::F11 => send_key_down("F11"),
+            KeyCode::F12 => send_key_down("F12"),
+            KeyCode::F13 => send_key_down("F13"),
+            KeyCode::F14 => send_key_down("F14"),
+            KeyCode::F15 => send_key_down("F15"),
+            KeyCode::F16 => send_key_down("F16"),
+            KeyCode::F17 => send_key_down("F17"),
+            KeyCode::F18 => send_key_down("F18"),
+            KeyCode::F19 => send_key_down("F19"),
+            KeyCode::F20 => send_key_down("F20"),
+            KeyCode::F21 => send_key_down("F21"),
+            KeyCode::F22 => send_key_down("F22"),
+            KeyCode::F23 => send_key_down("F23"),
+            KeyCode::F24 => send_key_down("F24"),
+            KeyCode::F25 => send_key_down("F25"),
+            KeyCode::Kp0 => send_key_down("Kp0"),
+            KeyCode::Kp1 => send_key_down("Kp1"),
+            KeyCode::Kp2 => send_key_down("Kp2"),
+            KeyCode::Kp3 => send_key_down("Kp3"),
+            KeyCode::Kp4 => send_key_down("Kp4"),
+            KeyCode::Kp5 => send_key_down("Kp5"),
+            KeyCode::Kp6 => send_key_down("Kp6"),
+            KeyCode::Kp7 => send_key_down("Kp7"),
+            KeyCode::Kp8 => send_key_down("Kp8"),
+            KeyCode::Kp9 => send_key_down("Kp9"),
+            KeyCode::KpDecimal => send_key_down("KpDecimal"),
+            KeyCode::KpDivide => send_key_down("KpDivide"),
+            KeyCode::KpMultiply => send_key_down("KpMultiply"),
+            KeyCode::KpSubtract => send_key_down("KpSubtract"),
+            KeyCode::KpAdd => send_key_down("KpAdd"),
+            KeyCode::KpEnter => send_key_down("KpEnter"),
+            KeyCode::KpEqual => send_key_down("KpEqual"),
+            KeyCode::LeftShift => send_key_down("LeftShift"),
+            KeyCode::LeftControl => send_key_down("LeftControl"),
+            KeyCode::LeftAlt => send_key_down("LeftAlt"),
+            KeyCode::LeftSuper => send_key_down("LeftSuper"),
+            KeyCode::RightShift => send_key_down("RightShift"),
+            KeyCode::RightControl => send_key_down("RightControl"),
+            KeyCode::RightAlt => send_key_down("RightAlt"),
+            KeyCode::RightSuper => send_key_down("RightSuper"),
+            KeyCode::Menu => send_key_down("Menu"),
+            KeyCode::Unknown => send_key_down("Unknown"),
+        }
+    }
+    fn mouse_motion_event(&mut self, x: f32, y: f32) {
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+        let mouse = 
+            scene_graph
+            .lookup_node_mut("/window/input/mouse")
+            .unwrap();
+        mouse.get_property("x").unwrap().set_f32(x).unwrap();
+        mouse.get_property("y").unwrap().set_f32(y).unwrap();
+        mouse.trigger("move").unwrap();
+    }
+    fn mouse_wheel_event(&mut self, x: f32, y: f32) {
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+        let mouse = 
+            scene_graph
+            .lookup_node_mut("/window/input/mouse")
+            .unwrap();
+        mouse.get_property("x").unwrap().set_f32(x).unwrap();
+        mouse.get_property("y").unwrap().set_f32(y).unwrap();
+        mouse.trigger("wheel").unwrap();
+    }
+    fn mouse_button_down_event(&mut self, button: MouseButton, x: f32, y: f32) {
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+        let mouse = 
+            scene_graph
+            .lookup_node_mut("/window/input/mouse")
+            .unwrap();
+        mouse
+            .get_property("button")
+            .unwrap()
+            .set_u32(button.to_u8() as u32)
+            .unwrap();
+        mouse.get_property("x").unwrap().set_f32(x).unwrap();
+        mouse.get_property("y").unwrap().set_f32(y).unwrap();
+        mouse.trigger("button_down").unwrap();
+    }
+    fn mouse_button_up_event(&mut self, button: MouseButton, x: f32, y: f32) {
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+        let mouse = 
+            scene_graph
+            .lookup_node_mut("/window/input/mouse")
+            .unwrap();
+        mouse
+            .get_property("button")
+            .unwrap()
+            .set_u32(button.to_u8() as u32)
+            .unwrap();
+        mouse.get_property("x").unwrap().set_f32(x).unwrap();
+        mouse.get_property("y").unwrap().set_f32(y).unwrap();
+        mouse.trigger("button_up").unwrap();
+    }
+
+    fn resize_event(&mut self, width: f32, height: f32) {
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+        let win = scene_graph.lookup_node_mut("/window").unwrap();
+        win.get_property("width").unwrap().set_f32(width).unwrap();
+        win.get_property("height").unwrap().set_f32(height).unwrap();
+        win.trigger("resize").unwrap();
+    }
+}
+
+pub fn init_gui(scene_graph: SceneGraphPtr) {
+    #[cfg(target_os = "android")]
+    {
+        android_logger::init_once(
+            android_logger::Config::default()
+                .with_max_level(LevelFilter::Debug)
+                .with_tag("fagman"),
+        );
+    }
+
+    #[cfg(target_os = "linux")]
+    {
+        let term_logger = simplelog::TermLogger::new(
+            simplelog::LevelFilter::Debug,
+            simplelog::Config::default(),
+            simplelog::TerminalMode::Mixed,
+            simplelog::ColorChoice::Auto,
+        );
+        simplelog::CombinedLogger::init(vec![term_logger]).expect("logger");
+    }
+
+    let mut conf = miniquad::conf::Conf {
+        high_dpi: true,
+        window_resizable: true,
+        platform: miniquad::conf::Platform {
+            linux_backend: miniquad::conf::LinuxBackend::WaylandWithX11Fallback,
+            wayland_use_fallback_decorations: false,
+            ..Default::default()
+        },
+        ..Default::default()
+    };
+    let metal = std::env::args().nth(1).as_deref() == Some("metal");
+    conf.platform.apple_gfx_api = if metal {
+        conf::AppleGfxApi::Metal
+    } else {
+        conf::AppleGfxApi::OpenGl
+    };
+
+    miniquad::start(conf, || Box::new(Stage::new(scene_graph)));
+}
+

+ 37 - 0
bin/darkwallet/src/main.rs

@@ -0,0 +1,37 @@
+use std::{thread, sync::{Arc, Mutex}};
+
+mod error;
+
+mod gfx;
+use gfx::init_gui;
+
+mod net;
+use net::ZeroMQAdapter;
+
+mod scene;
+use scene::{
+    SceneGraph,
+    SceneGraphPtr,
+};
+
+mod shader;
+
+#[macro_use]
+extern crate log;
+#[allow(unused_imports)]
+use log::LevelFilter;
+
+fn init_zmq(scene_graph: SceneGraphPtr) {
+    // detach thread
+    let _ = thread::spawn(move || {
+        let mut zmq_rpc = ZeroMQAdapter::new(scene_graph);
+        zmq_rpc.poll();
+    });
+}
+
+fn main() {
+    let scene_graph = Arc::new(Mutex::new(SceneGraph::new()));
+    init_zmq(scene_graph.clone());
+    init_gui(scene_graph);
+}
+

+ 439 - 0
bin/darkwallet/src/net.rs

@@ -0,0 +1,439 @@
+use darkfi_serial::{deserialize, Decodable, Encodable, SerialDecodable};
+use std::{
+    io::Cursor,
+    sync::{atomic::Ordering, mpsc}, thread
+};
+
+use crate::{
+    error::{Error, Result},
+    scene::{
+        PropertyType, PropertyValue, SceneNodeId, SceneNodeType, Slot, SlotId,
+        SceneGraphPtr
+    }
+};
+
+#[derive(Debug, SerialDecodable)]
+#[repr(u8)]
+enum Command {
+    Hello = 0,
+    AddNode = 1,
+    RemoveNode = 9,
+    RenameNode = 23,
+    LookupNodeId = 12,
+    AddProperty = 11,
+    LinkNode = 2,
+    UnlinkNode = 8,
+    GetInfo = 19,
+    GetChildren = 4,
+    GetParents = 5,
+    GetProperties = 3,
+    GetProperty = 6,
+    SetProperty = 7,
+    GetSignals = 14,
+    RegisterSlot = 15,
+    UnregisterSlot = 16,
+    LookupSlotId = 17,
+    GetSlots = 18,
+    GetMethods = 20,
+    GetMethod = 21,
+    CallMethod = 22,
+}
+
+pub struct ZeroMQAdapter {
+    // req-reply commands
+    req_socket: zmq::Socket,
+    // We cannot share zmq sockets across threads, and we cannot quickly spawn
+    // pub sockets due to address reuse errors.
+    slot_sender: mpsc::SyncSender<Vec<u8>>,
+    slot_recvr: Option<mpsc::Receiver<Vec<u8>>>,
+    scene_graph: SceneGraphPtr,
+}
+
+impl ZeroMQAdapter {
+    pub fn new(scene_graph: SceneGraphPtr) -> Self {
+        let zmq_ctx = zmq::Context::new();
+        let req_socket = zmq_ctx.socket(zmq::REP).unwrap();
+        req_socket.set_ipv6(true).unwrap();
+        req_socket.bind("tcp://*:9484").unwrap();
+
+        let (slot_sender, slot_recvr) = mpsc::sync_channel(100);
+
+        Self {
+            req_socket,
+            slot_sender,
+            slot_recvr: Some(slot_recvr),
+            scene_graph
+        }
+    }
+
+    pub fn poll(&mut self) {
+        let rx = std::mem::take(&mut self.slot_recvr).unwrap();
+        let _ = thread::spawn(move || {
+            let zmq_ctx = zmq::Context::new();
+            let pub_socket = zmq_ctx.socket(zmq::PUB).unwrap();
+            pub_socket.set_ipv6(true).unwrap();
+            pub_socket.bind("tcp://*:9485").unwrap();
+
+            loop {
+                let user_data = rx.recv().unwrap();
+                pub_socket.send(&user_data, 0).unwrap();
+            }
+        });
+
+        loop {
+            // https://github.com/johnliu55tw/rust-zmq-poller/blob/master/src/main.rs
+            let mut items = [self.req_socket.as_poll_item(zmq::POLLIN)];
+            // Poll forever
+            let _rc = zmq::poll(&mut items, -1).unwrap();
+
+            // Rust borrow checker things
+            let is_item0_readable = items[0].is_readable();
+            drop(items);
+
+            if is_item0_readable {
+                let req = self.req_socket.recv_multipart(zmq::DONTWAIT).unwrap();
+
+                assert_eq!(req[0].len(), 1);
+                assert_eq!(req.len(), 2);
+                let cmd = deserialize(&req[0]).unwrap();
+                let payload = req[1].clone();
+
+                match self.process_request(cmd, payload) {
+                    Ok(reply) => {
+                        // [errc:1] [reply]
+                        self.req_socket
+                            .send_multipart(&[vec![0], reply], zmq::DONTWAIT)
+                            .unwrap();
+                    }
+                    Err(err) => {
+                        let errc = err as u8;
+                        warn!(target: "req", "errc {}: {}", errc, err);
+                        self.req_socket
+                            .send_multipart(&[vec![errc], vec![]], zmq::DONTWAIT)
+                            .unwrap();
+                    }
+                }
+            }
+        }
+    }
+
+    fn process_request(
+        &self,
+        cmd: Command,
+        payload: Vec<u8>,
+    ) -> Result<Vec<u8>> {
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+        let mut cur = Cursor::new(&payload);
+        let mut reply = vec![];
+        match cmd {
+            Command::Hello => {
+                debug!(target: "req", "hello()");
+                assert_eq!(payload.len(), 0);
+                "hello".encode(&mut reply).unwrap();
+            }
+            Command::GetInfo => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({})", cmd, node_id);
+
+                let node = scene_graph.get_node(node_id).ok_or(Error::NodeNotFound)?;
+                node.name.encode(&mut reply).unwrap();
+                node.typ.encode(&mut reply).unwrap();
+            }
+            Command::GetChildren => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({})", cmd, node_id);
+
+                let node = scene_graph.get_node(node_id).ok_or(Error::NodeNotFound)?;
+                let children: Vec<_> = node
+                    .children
+                    .iter()
+                    .map(|node_inf| (node_inf.name.clone(), node_inf.id, node_inf.typ))
+                    .collect();
+                children.encode(&mut reply).unwrap();
+            }
+            Command::GetParents => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({})", cmd, node_id);
+
+                let node = scene_graph.get_node(node_id).ok_or(Error::NodeNotFound)?;
+                let parents: Vec<_> = node
+                    .parents
+                    .iter()
+                    .map(|node_inf| (node_inf.name.clone(), node_inf.id, node_inf.typ))
+                    .collect();
+                parents.encode(&mut reply).unwrap();
+            }
+            Command::GetProperties => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({})", cmd, node_id);
+
+                let node = scene_graph.get_node(node_id).ok_or(Error::NodeNotFound)?;
+                let mut props = vec![];
+                for prop in &node.props {
+                    props.push((prop.name.clone(), prop.get_type() as u8));
+                }
+                props.encode(&mut reply).unwrap();
+            }
+            Command::GetProperty => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                let prop_name = String::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({}, {})", cmd, node_id, prop_name);
+
+                let node = scene_graph.get_node(node_id).ok_or(Error::NodeNotFound)?;
+                let prop = node
+                    .get_property(&prop_name)
+                    .ok_or(Error::PropertyNotFound)?;
+                match &prop.val {
+                    PropertyValue::Null => {
+                        0u8.encode(&mut reply).unwrap();
+                    }
+                    PropertyValue::Buffer(_) => {
+                        1u8.encode(&mut reply).unwrap();
+                    }
+                    PropertyValue::Bool(val) => {
+                        3u8.encode(&mut reply).unwrap();
+                        let val = val.load(Ordering::SeqCst);
+                        val.encode(&mut reply).unwrap();
+                    }
+                    PropertyValue::Uint32(val) => {
+                        2u8.encode(&mut reply).unwrap();
+                        let val = val.load(Ordering::SeqCst);
+                        val.encode(&mut reply).unwrap();
+                    }
+                    PropertyValue::Float32(val) => {
+                        4u8.encode(&mut reply).unwrap();
+                        let val = val.load(Ordering::SeqCst);
+                        val.encode(&mut reply).unwrap();
+                    }
+                    PropertyValue::Str(val) => {
+                        5u8.encode(&mut reply).unwrap();
+                        let val = val.lock().unwrap();
+                        val.encode(&mut reply).unwrap();
+                    }
+                    PropertyValue::SceneNodeId(val) => {
+                        6u8.encode(&mut reply).unwrap();
+                        let val = val.load(Ordering::SeqCst);
+                        val.encode(&mut reply).unwrap();
+                    }
+                };
+            }
+            Command::AddNode => {
+                let node_name = String::decode(&mut cur).unwrap();
+                let node_type = SceneNodeType::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({}, {:?})", cmd, node_name, node_type);
+
+                let node_id = scene_graph.add_node(&node_name, node_type).id;
+                node_id.encode(&mut reply).unwrap();
+            }
+            Command::RemoveNode => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({})", cmd, node_id);
+                scene_graph.remove_node(node_id)?;
+            }
+            Command::RenameNode => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                let node_name = String::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({})", cmd, node_id);
+                scene_graph.rename_node(node_id, node_name)?;
+            }
+            Command::LookupNodeId => {
+                let node_path: String = deserialize(&payload).unwrap();
+                debug!(target: "req", "{:?}({})", cmd, node_path);
+                let node_id = scene_graph
+                    .lookup_node_id(&node_path)
+                    .ok_or(Error::NodeNotFound)?;
+                node_id.encode(&mut reply).unwrap();
+            }
+            Command::AddProperty => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                let prop_name = String::decode(&mut cur).unwrap();
+                let prop_type = PropertyType::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({}, {}, {:?})", cmd, node_id, prop_name, prop_type);
+
+                let node = scene_graph
+                    .get_node_mut(node_id)
+                    .ok_or(Error::NodeNotFound)?;
+                node.add_property(prop_name, prop_type)?;
+            }
+            Command::LinkNode => {
+                let child_id = SceneNodeId::decode(&mut cur).unwrap();
+                let parent_id = SceneNodeId::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({}, {})", cmd, child_id, parent_id);
+                scene_graph.link(child_id, parent_id)?;
+            }
+            Command::UnlinkNode => {
+                let child_id = SceneNodeId::decode(&mut cur).unwrap();
+                let parent_id = SceneNodeId::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({}, {})", cmd, child_id, parent_id);
+                scene_graph.unlink(child_id, parent_id)?;
+            }
+            Command::SetProperty => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                let prop_name = String::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({}, {})", cmd, node_id, prop_name);
+
+                let node = scene_graph
+                    .get_node_mut(node_id)
+                    .ok_or(Error::NodeNotFound)?;
+                let prop = node
+                    .get_property(&prop_name)
+                    .ok_or(Error::PropertyNotFound)?;
+
+                match prop.get_type() {
+                    PropertyType::Null => {}
+                    PropertyType::Buffer => {
+                        let val = Vec::<u8>::decode(&mut cur).unwrap();
+                        prop.set_buf(val)?;
+                    }
+                    PropertyType::Bool => {
+                        let val = bool::decode(&mut cur).unwrap();
+                        prop.set_bool(val)?;
+                    }
+                    PropertyType::Uint32 => {
+                        let val = u32::decode(&mut cur).unwrap();
+                        prop.set_u32(val)?;
+                    }
+                    PropertyType::Float32 => {
+                        let val = f32::decode(&mut cur).unwrap();
+                        prop.set_f32(val)?;
+                    }
+                    PropertyType::Str => {
+                        let val = String::decode(&mut cur).unwrap();
+                        prop.set_str(val)?;
+                    }
+                    PropertyType::SceneNodeId => {
+                        let val = SceneNodeId::decode(&mut cur).unwrap();
+                        prop.set_node_id(val)?;
+                    }
+                }
+            }
+            Command::GetSignals => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({})", cmd, node_id);
+
+                let node = scene_graph
+                    .get_node_mut(node_id)
+                    .ok_or(Error::NodeNotFound)?;
+
+                let mut sigs = vec![];
+                for sig in &node.sigs {
+                    sigs.push(sig.name.clone());
+                }
+                sigs.encode(&mut reply).unwrap();
+            }
+            Command::RegisterSlot => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                let sig_name = String::decode(&mut cur).unwrap();
+                let slot_name = String::decode(&mut cur).unwrap();
+                let user_data = Vec::<u8>::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({}, {}, {}, {:?})", cmd, node_id, sig_name, slot_name, user_data);
+
+                let node = scene_graph
+                    .get_node_mut(node_id)
+                    .ok_or(Error::NodeNotFound)?;
+
+                let sender = self.slot_sender.clone();
+                let slot = Slot {
+                    name: slot_name,
+                    func: Box::new(move || {
+                        sender.send(user_data.clone()).unwrap();
+                    })
+                };
+
+                let slot_id = node.register(&sig_name, slot)?;
+                slot_id.encode(&mut reply).unwrap();
+            }
+            Command::UnregisterSlot => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                let sig_name = String::decode(&mut cur).unwrap();
+                let slot_id = SlotId::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({}, {}, {})", cmd, node_id, sig_name, slot_id);
+
+                let node = scene_graph
+                    .get_node_mut(node_id)
+                    .ok_or(Error::NodeNotFound)?;
+                node.unregister(&sig_name, slot_id)?;
+            }
+            Command::LookupSlotId => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                let sig_name = String::decode(&mut cur).unwrap();
+                let slot_name = String::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({}, {}, {})", cmd, node_id, sig_name, slot_name);
+
+                let node = scene_graph.get_node(node_id).ok_or(Error::NodeNotFound)?;
+                let signal = node.get_signal(&sig_name).ok_or(Error::SignalNotFound)?;
+                let slot_id = signal
+                    .lookup_slot_id(&slot_name)
+                    .ok_or(Error::SlotNotFound)?;
+                slot_id.encode(&mut reply).unwrap();
+            }
+            Command::GetSlots => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                let sig_name = String::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({}, {})", cmd, node_id, sig_name);
+
+                let node = scene_graph.get_node(node_id).ok_or(Error::NodeNotFound)?;
+                let signal = node.get_signal(&sig_name).ok_or(Error::SignalNotFound)?;
+
+                let mut slots = vec![];
+                for (slot_id, slot) in signal.get_slots() {
+                    slots.push((slot.name.clone(), slot_id));
+                }
+                slots.encode(&mut reply).unwrap();
+            }
+            Command::GetMethods => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({})", cmd, node_id);
+
+                let node = scene_graph.get_node(node_id).ok_or(Error::NodeNotFound)?;
+                let method_names: Vec<_> = node.methods.iter().map(|m| m.name.clone()).collect();
+
+                method_names.encode(&mut reply).unwrap();
+            }
+            Command::GetMethod => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                let method_name = String::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({}, {})", cmd, node_id, method_name);
+
+                let node = scene_graph.get_node(node_id).ok_or(Error::NodeNotFound)?;
+                let method = node.get_method(&method_name).ok_or(Error::MethodNotFound)?;
+
+                method.args.encode(&mut reply).unwrap();
+                method.result.encode(&mut reply).unwrap();
+            }
+            Command::CallMethod => {
+                let node_id = SceneNodeId::decode(&mut cur).unwrap();
+                let method_name = String::decode(&mut cur).unwrap();
+                let arg_data = Vec::<u8>::decode(&mut cur).unwrap();
+                debug!(target: "req", "{:?}({}, {}, ...)", cmd, node_id, method_name);
+
+                let node = scene_graph.get_node_mut(node_id).ok_or(Error::NodeNotFound)?;
+
+                let method_name2 = method_name.clone();
+                let (tx, rx) = mpsc::sync_channel::<Result<Vec<u8>>>(0);
+                let response_fn = Box::new(move |result| {
+                    debug!(target: "req", "processing callmethod for {}:'{}'", node_id, method_name2);
+                    tx.send(result).unwrap();
+                });
+                node.call_method(&method_name, arg_data, response_fn)?;
+                drop(scene_graph);
+
+                let result = rx.recv().unwrap();
+                debug!(target: "req", "received callmethod for {}:'{}'", node_id, method_name);
+                match result {
+                    Ok(res_data) => {
+                        0u8.encode(&mut reply).unwrap();
+                        res_data.encode(&mut reply).unwrap();
+                    }
+                    Err(err) => {
+                        let errc = err as u8;
+                        errc.encode(&mut reply).unwrap();
+                        0u8.encode(&mut reply).unwrap();
+                    }
+                }
+            }
+        }
+
+        Ok(reply)
+    }
+}

+ 683 - 0
bin/darkwallet/src/scene.rs

@@ -0,0 +1,683 @@
+use crate::error::{Error, Result};
+use atomic_float::AtomicF32;
+use darkfi_serial::{SerialEncodable, SerialDecodable};
+use std::{
+    fmt,
+    str::FromStr,
+    sync::{
+        atomic::{AtomicBool, AtomicU32, Ordering},
+        Arc, Mutex, MutexGuard,
+    },
+};
+
+pub struct ScenePath(Vec<String>);
+
+impl<S: Into<String>> From<S> for ScenePath {
+    fn from(path: S) -> Self {
+        let path: String = path.into();
+        (&path).parse().expect("invalid ScenePath &str")
+    }
+}
+
+impl fmt::Display for ScenePath {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "/")?;
+        for token in &self.0 {
+            write!(f, "{}/", token)?;
+        }
+        Ok(())
+    }
+}
+
+impl FromStr for ScenePath {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self> {
+        if s.is_empty() || s.chars().nth(0).unwrap() != '/' {
+            return Err(Error::InvalidScenePath);
+        }
+        if s == "/" {
+            return Ok(ScenePath(vec![]));
+        }
+
+        let mut tokens = s.split('/');
+        // Should start with a /
+        let initial = tokens.next().expect("should not be empty");
+        if !initial.is_empty() {
+            return Err(Error::InvalidScenePath);
+        }
+
+        let mut path = vec![];
+        for token in tokens {
+            // There should not be any double slashes //
+            if token.is_empty() {
+                return Err(Error::InvalidScenePath);
+            }
+            path.push(token.to_string());
+        }
+        Ok(ScenePath(path))
+    }
+}
+
+pub type SceneGraphPtr = Arc<Mutex<SceneGraph>>;
+
+pub struct SceneGraph {
+    // Node 0 is always the root
+    nodes: Vec<SceneNode>,
+    freed: Vec<SceneNodeId>,
+}
+
+impl SceneGraph {
+    pub const ROOT_ID: SceneNodeId = 0;
+
+    pub fn new() -> Self {
+        let root = SceneNode {
+            name: "/".to_string(),
+            id: 0,
+            typ: SceneNodeType::Root,
+            parents: vec![],
+            children: vec![],
+            props: vec![],
+            sigs: vec![],
+            methods: vec![]
+        };
+        Self {
+            nodes: vec![root],
+            freed: vec![],
+        }
+    }
+
+    pub fn add_node<S: Into<String>>(&mut self, name: S, typ: SceneNodeType) -> &mut SceneNode {
+        let node = SceneNode {
+            name: name.into(),
+            // We set this at the end
+            id: 0,
+            typ,
+            parents: vec![],
+            children: vec![],
+            props: vec![],
+            sigs: vec![],
+            methods: vec![]
+        };
+
+        let node_id = if self.freed.is_empty() {
+            let node_id = self.nodes.len() as SceneNodeId;
+            self.nodes.push(node);
+            node_id
+        } else {
+            let node_id = self.freed.pop().unwrap();
+            let _ = std::mem::replace(&mut self.nodes[node_id as usize], node);
+            node_id
+        };
+
+        self.nodes[node_id as usize].id = node_id;
+        &mut self.nodes[node_id as usize]
+    }
+
+    pub fn remove_node(&mut self, id: SceneNodeId) -> Result<()> {
+        let node = self.get_node_mut(id).ok_or(Error::NodeNotFound)?;
+        if !node.parents.is_empty() {
+            return Err(Error::NodeHasParents);
+        }
+        if !node.children.is_empty() {
+            return Err(Error::NodeHasChildren);
+        }
+        node.name.clear();
+        node.typ = SceneNodeType::Null;
+        node.props.clear();
+        self.freed.push(id);
+        Ok(())
+    }
+
+    fn root(&self) -> &SceneNode {
+        &self.nodes[0]
+    }
+    fn root_mut(&mut self) -> &mut SceneNode {
+        &mut self.nodes[0]
+    }
+
+    fn exists(&self, id: SceneNodeId) -> bool {
+        id < self.nodes.len() as SceneNodeId && !self.freed.contains(&id)
+    }
+
+    pub fn get_node(&self, id: SceneNodeId) -> Option<&SceneNode> {
+        if self.exists(id) {
+            Some(&self.nodes[id as usize])
+        } else {
+            None
+        }
+    }
+    pub fn get_node_mut(&mut self, id: SceneNodeId) -> Option<&mut SceneNode> {
+        if self.exists(id) {
+            Some(&mut self.nodes[id as usize])
+        } else {
+            None
+        }
+    }
+
+    pub fn link(&mut self, child_id: SceneNodeId, parent_id: SceneNodeId) -> Result<()> {
+        // Check both nodes are not already linked
+        let is_linked = self.is_linked(child_id, parent_id)?;
+        if is_linked {
+            return Err(Error::NodesAreLinked);
+        }
+
+        let parent = self.get_node(parent_id).unwrap();
+        let parent_inf = SceneNodeInfo {
+            name: parent.name.clone(),
+            id: parent_id,
+            typ: parent.typ,
+        };
+        let child_name = &self.get_node(child_id).unwrap().name;
+        if parent.has_child(child_name) {
+            return Err(Error::NodeChildNameConflict);
+        }
+
+        // Link parent into child
+        let child = self.get_node_mut(child_id).unwrap();
+        if child.has_parent(&parent_inf.name) {
+            return Err(Error::NodeParentNameConflict);
+        }
+        let child_inf = SceneNodeInfo {
+            name: child.name.clone(),
+            id: child_id,
+            typ: child.typ,
+        };
+        assert!(!child.has_parent_id(parent_id));
+        child.parents.push(parent_inf);
+
+        // Link child into parent
+        let parent = self.get_node_mut(parent_id).unwrap();
+        assert!(!parent.has_child(&child_inf.name));
+        parent.children.push(child_inf);
+        Ok(())
+    }
+
+    pub fn unlink(&mut self, child_id: SceneNodeId, parent_id: SceneNodeId) -> Result<()> {
+        // Check both nodes are actually linked
+        let is_linked = self.is_linked(child_id, parent_id)?;
+        if !is_linked {
+            return Err(Error::NodesNotLinked);
+        }
+
+        // Unlink parent from child
+        let child = self.get_node_mut(child_id).unwrap();
+        child.remove_parent(parent_id);
+
+        // Unlink child from parent
+        let parent = self.get_node_mut(parent_id).unwrap();
+        parent.remove_child(child_id);
+        Ok(())
+    }
+
+    pub fn is_linked(&self, child_id: SceneNodeId, parent_id: SceneNodeId) -> Result<bool> {
+        let parent = self.get_node(parent_id).ok_or(Error::ParentNodeNotFound)?;
+        let child = self.get_node(child_id).ok_or(Error::ChildNodeNotFound)?;
+        let parent_has_child = parent.has_child_id(child_id);
+        let child_has_parent = child.has_parent_id(parent_id);
+        // Internal consistency checks
+        if parent_has_child {
+            assert!(child_has_parent);
+        } else {
+            assert!(!child_has_parent);
+        }
+        Ok(parent_has_child)
+    }
+
+    pub fn lookup_node_id<P: Into<ScenePath>>(&self, path: P) -> Option<SceneNodeId> {
+        let path: ScenePath = path.into();
+        let mut current_id = Self::ROOT_ID;
+        for node_name in path.0 {
+            let parent_node = self.get_node(current_id).unwrap();
+            match parent_node.get_child(&node_name) {
+                Some(child_id) => {
+                    current_id = child_id;
+                }
+                None => return None,
+            }
+        }
+        Some(current_id)
+    }
+
+    pub fn lookup_node<P: Into<ScenePath>>(&self, path: P) -> Option<&SceneNode> {
+        let node_id = self.lookup_node_id(path)?;
+        Some(self.get_node(node_id).unwrap())
+    }
+    pub fn lookup_node_mut<P: Into<ScenePath>>(&mut self, path: P) -> Option<&mut SceneNode> {
+        let node_id = self.lookup_node_id(path)?;
+        Some(self.get_node_mut(node_id).unwrap())
+    }
+
+    pub fn rename_node<S: Into<String>>(&mut self, node_id: SceneNodeId, node_name: S) -> Result<()> {
+        let node_name = node_name.into();
+        for sibling_inf in self.node_siblings(node_id)? {
+            if sibling_inf.name == node_name {
+                return Err(Error::NodeSiblingNameConflict)
+            }
+        }
+        let node = self.get_node_mut(node_id).unwrap();
+        node.name = node_name;
+        Ok(())
+    }
+    fn node_siblings(&self, node_id: SceneNodeId) -> Result<Vec<SceneNodeInfo>> {
+        let mut siblings = vec![];
+        let node = self.get_node(node_id).ok_or(Error::NodeNotFound)?;
+        for parent_inf in &node.parents {
+            let parent = self.get_node(parent_inf.id).ok_or(Error::ParentNodeNotFound)?;
+            let mut sibling_infs = parent.children.iter().cloned().filter(|child_inf| child_inf.id != node_id).collect();
+            siblings.append(&mut sibling_infs);
+        }
+        Ok(siblings)
+    }
+}
+
+pub type SceneNodeId = u32;
+
+#[derive(Debug, Copy, Clone, PartialEq, SerialEncodable, SerialDecodable)]
+#[repr(u8)]
+pub enum SceneNodeType {
+    Null = 0,
+    Root = 1,
+    Window = 2,
+    WindowInput = 6,
+    Keyboard = 7,
+    Mouse = 8,
+    RenderLayer = 3,
+    RenderObject = 4,
+    RenderMesh = 5,
+    RenderText = 9,
+    RenderTexture = 13,
+    Fonts = 10,
+    Font = 11,
+    LinePosition = 12,
+}
+
+#[derive(Clone)]
+pub struct SceneNodeInfo {
+    pub name: String,
+    pub id: SceneNodeId,
+    pub typ: SceneNodeType,
+}
+
+pub struct SceneNode {
+    pub name: String,
+    pub id: SceneNodeId,
+    pub typ: SceneNodeType,
+    pub parents: Vec<SceneNodeInfo>,
+    pub children: Vec<SceneNodeInfo>,
+    pub props: Vec<Arc<Property>>,
+    pub sigs: Vec<Signal>,
+    pub methods: Vec<Method>
+}
+
+impl SceneNode {
+    fn has_parent_id(&self, parent_id: SceneNodeId) -> bool {
+        self.parents.iter().any(|parent| parent.id == parent_id)
+    }
+    fn has_child_id(&self, child_id: SceneNodeId) -> bool {
+        self.children.iter().any(|child| child.id == child_id)
+    }
+
+    fn has_parent(&self, parent_name: &str) -> bool {
+        self.parents.iter().any(|parent| parent.name == parent_name)
+    }
+    fn has_child(&self, child_name: &str) -> bool {
+        self.children.iter().any(|child| child.name == child_name)
+    }
+    fn get_child(&self, child_name: &str) -> Option<SceneNodeId> {
+        for child in &self.children {
+            if child.name == child_name {
+                return Some(child.id);
+            }
+        }
+        None
+    }
+
+    // Panics if parent is not linked
+    fn remove_parent(&mut self, parent_id: SceneNodeId) {
+        let parent_idx = self
+            .parents
+            .iter()
+            .position(|parent| parent.id == parent_id)
+            .unwrap();
+        self.parents.swap_remove(parent_idx);
+    }
+    // Panics if child is not linked
+    fn remove_child(&mut self, child_id: SceneNodeId) {
+        let child_idx = self
+            .children
+            .iter()
+            .position(|child| child.id == child_id)
+            .unwrap();
+        self.children.swap_remove(child_idx);
+    }
+
+    pub fn iter_children<'a>(
+        &'a self,
+        scene_graph: &'a SceneGraph,
+        typ: SceneNodeType,
+    ) -> impl Iterator<Item = &'a Self> + 'a {
+        self.children
+            .iter()
+            .filter(move |child_inf| child_inf.typ == typ)
+            .map(|child_inf| scene_graph.get_node(child_inf.id).unwrap())
+    }
+
+    pub fn add_property<S: Into<String>>(
+        &mut self,
+        name: S,
+        typ: PropertyType,
+    ) -> Result<Arc<Property>> {
+        let name = name.into();
+        if self.has_property(&name) {
+            return Err(Error::PropertyAlreadyExists);
+        }
+        let prop = Property::new(name, typ);
+        self.props.push(prop.clone());
+        Ok(prop)
+    }
+
+    fn has_property(&self, name: &str) -> bool {
+        self.props.iter().any(|prop| prop.name == name)
+    }
+
+    pub fn get_property(&self, name: &str) -> Option<Arc<Property>> {
+        self.props
+            .iter()
+            .find(|prop| prop.name == name)
+            .map(|prop| prop.clone())
+    }
+
+    pub fn add_signal<S: Into<String>>(&mut self, name: S) -> Result<()> {
+        let name = name.into();
+        if self.has_signal(&name) {
+            return Err(Error::SignalAlreadyExists);
+        }
+        self.sigs.push(Signal {
+            name: name.into(),
+            slots: vec![],
+            freed: vec![],
+        });
+        Ok(())
+    }
+
+    fn has_signal(&self, name: &str) -> bool {
+        self.sigs.iter().any(|sig| sig.name == name)
+    }
+    pub fn get_signal(&self, name: &str) -> Option<&Signal> {
+        self.sigs.iter().find(|sig| sig.name == name)
+    }
+    fn get_signal_mut(&mut self, name: &str) -> Option<&mut Signal> {
+        self.sigs.iter_mut().find(|sig| sig.name == name)
+    }
+
+    pub fn register(&mut self, sig_name: &str, slot: Slot) -> Result<SlotId> {
+        let sig = self.get_signal_mut(sig_name).ok_or(Error::SignalNotFound)?;
+        let slot_id = if sig.freed.is_empty() {
+            let slot_id = sig.slots.len() as SlotId;
+            sig.slots.push(slot);
+            slot_id
+        } else {
+            let slot_id = sig.freed.pop().unwrap();
+            let _ = std::mem::replace(&mut sig.slots[slot_id as usize], slot);
+            slot_id
+        };
+        Ok(slot_id)
+    }
+    pub fn unregister(&mut self, sig_name: &str, slot_id: SlotId) -> Result<()> {
+        let sig = self.get_signal_mut(sig_name).ok_or(Error::SignalNotFound)?;
+        if !sig.slot_exists(slot_id) {
+            return Err(Error::SlotNotFound);
+        }
+        sig.freed.push(slot_id);
+        Ok(())
+    }
+    pub fn trigger(&self, sig_name: &str) -> Result<()> {
+        let sig = self.get_signal(sig_name).ok_or(Error::SignalNotFound)?;
+        for (_, slot) in sig.get_slots() {
+            // Trigger the slot
+            slot.call();
+        }
+        Ok(())
+    }
+
+    pub fn add_method<S: Into<String>>(&mut self, name: S,
+                      args: Vec<(S, PropertyType)>,
+                      result: Vec<(S, PropertyType)>) {
+        let args = args.into_iter().map(|(s, p)| (s.into(), p)).collect();
+        let result = result.into_iter().map(|(s, p)| (s.into(), p)).collect();
+        self.methods.push(Method {
+            name: name.into(), args, result, queue: vec![]
+        })
+    }
+
+    pub fn get_method(&self, name: &str) -> Option<&Method> {
+        self.methods.iter().find(|method| method.name == name)
+    }
+    fn get_method_mut(&mut self, name: &str) -> Option<&mut Method> {
+        self.methods.iter_mut().find(|method| method.name == name)
+    }
+
+    pub fn call_method(&mut self, name: &str, arg_data: Vec<u8>, response_fn: MethodResponseFn) -> Result<()> {
+        let method = self.get_method_mut(name).ok_or(Error::MethodNotFound)?;
+        method.queue.push((arg_data, response_fn));
+        Ok(())
+    }
+}
+
+type BufferGuard<'a> = MutexGuard<'a, Vec<u8>>;
+
+#[derive(Debug, Copy, Clone, PartialEq, SerialEncodable, SerialDecodable)]
+#[repr(u8)]
+pub enum PropertyType {
+    Null = 0,
+    Buffer = 1,
+    Bool = 2,
+    Uint32 = 3,
+    Float32 = 4,
+    Str = 5,
+    SceneNodeId = 6,
+}
+
+pub enum PropertyValue {
+    Null,
+    Buffer(Mutex<Vec<u8>>),
+    Bool(AtomicBool),
+    Uint32(AtomicU32),
+    Float32(AtomicF32),
+    Str(Mutex<String>),
+    SceneNodeId(AtomicU32),
+}
+
+pub struct Property {
+    pub name: String,
+    //typ: PropertyType,
+    pub val: PropertyValue,
+}
+
+impl Property {
+    fn new(name: String, typ: PropertyType) -> Arc<Self> {
+        let val = match typ {
+            PropertyType::Null => PropertyValue::Null,
+            PropertyType::Buffer => PropertyValue::Buffer(Mutex::new(Vec::new())),
+            PropertyType::Bool => PropertyValue::Bool(AtomicBool::new(false)),
+            PropertyType::Uint32 => PropertyValue::Uint32(AtomicU32::new(0)),
+            PropertyType::Float32 => PropertyValue::Float32(AtomicF32::new(0.)),
+            PropertyType::Str => PropertyValue::Str(Mutex::new(String::new())),
+            PropertyType::SceneNodeId => PropertyValue::SceneNodeId(AtomicU32::new(0)),
+        };
+        Arc::new(Self { name, val })
+    }
+
+    pub fn get_type(&self) -> PropertyType {
+        match self.val {
+            PropertyValue::Null => PropertyType::Null,
+            PropertyValue::Buffer(_) => PropertyType::Buffer,
+            PropertyValue::Bool(_) => PropertyType::Bool,
+            PropertyValue::Uint32(_) => PropertyType::Uint32,
+            PropertyValue::Float32(_) => PropertyType::Float32,
+            PropertyValue::Str(_) => PropertyType::Str,
+            PropertyValue::SceneNodeId(_) => PropertyType::SceneNodeId,
+        }
+    }
+
+    pub fn get_buf<'a>(&'a self) -> Result<BufferGuard<'a>> {
+        match &self.val {
+            PropertyValue::Buffer(propval) => Ok(propval.lock().unwrap()),
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
+
+    pub fn get_bool(&self) -> Result<bool> {
+        match &self.val {
+            PropertyValue::Bool(propval) => Ok(propval.load(Ordering::SeqCst)),
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
+
+    pub fn get_u32(&self) -> Result<u32> {
+        match &self.val {
+            PropertyValue::Uint32(propval) => Ok(propval.load(Ordering::SeqCst)),
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
+
+    pub fn get_f32(&self) -> Result<f32> {
+        match &self.val {
+            PropertyValue::Float32(propval) => Ok(propval.load(Ordering::SeqCst)),
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
+
+    pub fn get_str(&self) -> Result<String> {
+        match &self.val {
+            PropertyValue::Str(propval) => Ok(propval.lock().unwrap().clone()),
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
+
+    pub fn get_node_id(&self) -> Result<SceneNodeId> {
+        match &self.val {
+            PropertyValue::SceneNodeId(propval) => Ok(propval.load(Ordering::SeqCst)),
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
+
+    pub fn set_buf(&self, val: Vec<u8>) -> Result<()> {
+        match &self.val {
+            PropertyValue::Buffer(propval) => {
+                let mut buf = propval.lock().unwrap();
+                let _ = std::mem::replace(&mut *buf, val);
+                Ok(())
+            }
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
+
+    pub fn set_bool(&self, val: bool) -> Result<()> {
+        match &self.val {
+            PropertyValue::Bool(propval) => {
+                propval.store(val, Ordering::SeqCst);
+                Ok(())
+            }
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
+
+    pub fn set_u32(&self, val: u32) -> Result<()> {
+        match &self.val {
+            PropertyValue::Uint32(propval) => {
+                propval.store(val, Ordering::SeqCst);
+                Ok(())
+            }
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
+
+    pub fn set_f32(&self, val: f32) -> Result<()> {
+        match &self.val {
+            PropertyValue::Float32(propval) => {
+                propval.store(val, Ordering::SeqCst);
+                Ok(())
+            }
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
+
+    pub fn set_str<S: Into<String>>(&self, val: S) -> Result<()> {
+        match &self.val {
+            PropertyValue::Str(propval) => {
+                let mut pv = propval.lock().unwrap();
+                *pv = val.into();
+                Ok(())
+            }
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
+
+    pub fn set_node_id(&self, val: SceneNodeId) -> Result<()> {
+        match &self.val {
+            PropertyValue::SceneNodeId(propval) => {
+                propval.store(val, Ordering::SeqCst);
+                Ok(())
+            }
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
+}
+
+type SlotFn = Box<dyn Fn() + Send>;
+pub type SlotId = u32;
+
+pub struct Slot {
+    pub name: String,
+    pub func: SlotFn,
+}
+
+impl Slot {
+    fn call(&self) {
+        (self.func)()
+    }
+}
+
+pub struct Signal {
+    pub name: String,
+    slots: Vec<Slot>,
+    freed: Vec<SlotId>,
+}
+
+impl Signal {
+    fn slot_exists(&self, slot_id: SlotId) -> bool {
+        if slot_id >= self.slots.len() as SlotId {
+            return false;
+        }
+        return !self.freed.contains(&slot_id);
+    }
+
+    pub fn get_slots<'a>(&'a self) -> impl Iterator<Item = (SlotId, &'a Slot)> {
+        self.slots
+            .iter()
+            .enumerate()
+            .filter(|(slot_id, _)| !self.freed.contains(&(*slot_id as SlotId)))
+            .map(|(slot_id, slot)| (slot_id as SlotId, slot))
+    }
+
+    pub fn lookup_slot_id(&self, slot_name: &str) -> Option<SlotId> {
+        for (slot_id, slot) in self.get_slots() {
+            if slot.name == slot_name {
+                return Some(slot_id);
+            }
+        }
+        None
+    }
+}
+
+type MethodResponseFn = Box<dyn Fn(Result<Vec<u8>>) + Send>;
+
+pub struct Method {
+    pub name: String,
+    pub args: Vec<(String, PropertyType)>,
+    pub result: Vec<(String, PropertyType)>,
+    pub queue: Vec<(Vec<u8>, MethodResponseFn)>,
+}
+

+ 79 - 0
bin/darkwallet/src/shader.rs

@@ -0,0 +1,79 @@
+use miniquad::*;
+
+pub const GL_VERTEX: &str = r#"#version 100
+attribute vec2 in_pos;
+attribute vec4 in_color;
+attribute vec2 in_uv;
+
+varying lowp vec4 color;
+varying lowp vec2 uv;
+
+uniform mat4 Projection;
+uniform mat4 Model;
+
+void main() {
+    gl_Position = Projection * Model * vec4(in_pos, 0, 1);
+    color = in_color;
+    uv = in_uv;
+}"#;
+
+pub const GL_FRAGMENT: &str = r#"#version 100
+varying lowp vec4 color;
+varying lowp vec2 uv;
+
+uniform sampler2D tex;
+
+void main() {
+    gl_FragColor = color * texture2D(tex, uv);
+}"#;
+
+pub const METAL: &str = r#"
+#include <metal_stdlib>
+
+using namespace metal;
+
+struct Uniforms
+{
+    float4x4 Projection;
+    float4x4 Model;
+};
+
+struct Vertex
+{
+    float2 in_pos   [[attribute(0)]];
+    float4 in_color [[attribute(1)]];
+    float2 in_uv    [[attribute(2)]];
+};
+
+struct RasterizerData
+{
+    float4 position [[position]];
+    float4 color [[user(locn0)]];
+    float2 uv [[user(locn1)]];
+};
+
+vertex RasterizerData vertexShader(Vertex v [[stage_in]])
+{
+    RasterizerData out;
+
+    out.position = uniforms.Model * uniforms.Projection * float4(v.in_pos.xy, 0.0, 1.0);
+    out.color = v.in_color;
+    out.uv = v.texcoord;
+
+    return out;
+}
+
+fragment float4 fragmentShader(RasterizerData in [[stage_in]], texture2d<float> tex [[texture(0)]], sampler texSmplr [[sampler(0)]])
+{
+    return in.color * tex.sample(texSmplr, in.uv);
+}
+
+"#;
+
+pub fn meta() -> ShaderMeta {
+    ShaderMeta {
+        images: vec!["tex".to_string()],
+        uniforms: UniformBlockLayout { uniforms: vec![] },
+    }
+}
+