Просмотр исходного кода

wallet: impl properties with PY_EXPR subtype which are dynamically evaluated every frame.

rsx 2 лет назад
Родитель
Сommit
199485d071

+ 4 - 0
bin/darkwallet/Cargo.toml

@@ -21,6 +21,10 @@ thiserror = "1.0.57"
 smol = "1.3.0"
 atomic_float = "0.1.0"
 
+[dependencies.pyo3]
+version = "0.21.2"
+features = ["auto-initialize"]
+
 [target.'cfg(target_os = "android")'.dependencies]
 android_logger = "0.13"
 

+ 135 - 36
bin/darkwallet/gui/__init__.py

@@ -270,42 +270,141 @@ def resize_rounded_box():
     #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))
 
+def draw():
+    win_id = api.lookup_node_id("/window")
+
+    # Add foo layer
+
+    layer_id = api.add_node("foo", SceneNodeType.RENDER_LAYER)
+
+    prop = Property(
+        "is_visible", PropertyType.BOOL, PropertySubType.NULL,
+        None,
+        "is_visible", "Visibility of the layer",
+        False, 1, None, None, []
+    )
+    api.add_property(layer_id, prop)
+    api.set_property_bool(layer_id, "is_visible", 0, True)
+
+    #prop = Property(
+    #    "rect", PropertyType.UINT32, PropertySubType.PIXEL,
+    #    None,
+    #    "layer_rect", "(x, y, w, h) viewport rectangle for current layer",
+    #    False, 4, None, None, []
+    #)
+    prop = Property(
+        "rect", PropertyType.STR, PropertySubType.PY_EXPR,
+        ["0", "0", "0", "0"],
+        "mesh_rect", "The position and size within the layer",
+        False, 4, None, None, []
+    )
+    api.add_property(layer_id, prop)
+    ## x
+    #api.set_property_u32(layer_id, "rect", 0, 0)
+    ## y
+    #api.set_property_u32(layer_id, "rect", 1, 0)
+    ## w
+    #api.set_property_u32(layer_id, "rect", 2, int(3838/2))
+    ## h
+    #api.set_property_u32(layer_id, "rect", 3, int(2158/2))
+    # x
+    api.set_property_str(layer_id, "rect", 0, "0")
+    # y
+    api.set_property_str(layer_id, "rect", 1, "0")
+    # w
+    api.set_property_str(layer_id, "rect", 2, "sw/2")
+    # h
+    api.set_property_str(layer_id, "rect", 3, "sh")
+
+    api.link_node(layer_id, win_id)
+
+    # Add a mesh to our layer
+
+    mesh_id = api.add_node("meshie", SceneNodeType.RENDER_MESH)
+
+    prop = Property(
+        "data", PropertyType.BUFFER, PropertySubType.NULL,
+        None,
+        "mesh_data", "The face and vertex data for the mesh",
+        False, 2, None, None, []
+    )
+    api.add_property(mesh_id, prop)
+
+    #x, y = 0.1, 0.1
+    #w, h = 0.1, 0.1
+    x, y, w, h = 0, 0, 1, 1
+    #x, y, w, h = -1, 1, 2, -2
+    vert1 = vertex(x,     y,     1, 0, 0, 1, 0, 0)
+    vert2 = vertex(x + w, y,     0, 1, 0, 1, 1, 0)
+    vert3 = vertex(x,     y + h, 0, 0, 1, 1, 0, 1)
+    vert4 = vertex(x + w, y + h, 1, 1, 1, 1, 1, 1)
+
+    verts = vert1 + vert2 + vert3 + vert4
+    faces = face(0, 2, 1) + face(1, 2, 3)
+
+    api.set_property_buf(mesh_id, "data", 0, verts)
+    api.set_property_buf(mesh_id, "data", 1, faces)
+
+    prop = Property(
+        "rect", PropertyType.STR, PropertySubType.PY_EXPR,
+        ["0", "0", "0", "0"],
+        "mesh_rect", "The position and size within the layer",
+        False, 4, None, None, []
+    )
+    api.add_property(mesh_id, prop)
+    # x
+    api.set_property_str(mesh_id, "rect", 0, "10")
+    # y
+    api.set_property_str(mesh_id, "rect", 1, "10")
+    # w
+    api.set_property_str(mesh_id, "rect", 2, "lw - 10")
+    # h
+    api.set_property_str(mesh_id, "rect", 3, "lh - 10")
+
+    api.link_node(mesh_id, layer_id)
+
 def main():
-    if True:
-        node_id = api.add_node("foo", SceneNodeType.WINDOW)
-        prop = Property(
-            "myprop", PropertyType.FLOAT32, PropertySubType.NULL,
-            None,
-            "myprop", "",
-            False, 2, None, None, []
-        )
-        api.add_property(1, prop)
-        api.link_node(node_id, 0)
-        api.set_property_f32(1, "myprop", 0, 4.0)
-        api.set_property_f32(1, "myprop", 1, 110.0)
-    print("val =", api.get_property_value(1, "myprop"))
-    for prop in api.get_properties(1):
-        print("Property:")
-        print(f"  name = {prop.name}")
-        print(f"  type = {prop.type}")
-        print(f"  subtype = {prop.subtype}")
-        print(f"  defaults = {prop.defaults}")
-        print(f"  ui_name = {prop.ui_name}")
-        print(f"  desc = {prop.desc}")
-        print(f"  is_null_allowed = {prop.is_null_allowed}")
-        print(f"  array_len = {prop.array_len}")
-        print(f"  min_val = {prop.min_val}")
-        print(f"  max_val = {prop.max_val}")
-        print(f"  enum_items = {prop.enum_items}")
-        print()
+    draw()
+
+    # DEBUG
     print_tree()
-    #garbage_collect()
-
-    #app = App()
-    #draw_box()
-    #draw_rounded_box()
-    #draw_cursor()
-    #reposition_cursor()
-    ##print_tree()
-    #app.run()
+
+#def main():
+#    if True:
+#        node_id = api.add_node("foo", SceneNodeType.WINDOW)
+#        prop = Property(
+#            "myprop", PropertyType.FLOAT32, PropertySubType.NULL,
+#            None,
+#            "myprop", "",
+#            False, 2, None, None, []
+#        )
+#        api.add_property(1, prop)
+#        api.link_node(node_id, 0)
+#        api.set_property_f32(1, "myprop", 0, 4.0)
+#        api.set_property_f32(1, "myprop", 1, 110.0)
+#    print("val =", api.get_property_value(1, "myprop"))
+#    for prop in api.get_properties(1):
+#        print("Property:")
+#        print(f"  name = {prop.name}")
+#        print(f"  type = {prop.type}")
+#        print(f"  subtype = {prop.subtype}")
+#        print(f"  defaults = {prop.defaults}")
+#        print(f"  ui_name = {prop.ui_name}")
+#        print(f"  desc = {prop.desc}")
+#        print(f"  is_null_allowed = {prop.is_null_allowed}")
+#        print(f"  array_len = {prop.array_len}")
+#        print(f"  min_val = {prop.min_val}")
+#        print(f"  max_val = {prop.max_val}")
+#        print(f"  enum_items = {prop.enum_items}")
+#        print()
+#    print_tree()
+#    #garbage_collect()
+#
+#    #app = App()
+#    #draw_box()
+#    #draw_rounded_box()
+#    #draw_cursor()
+#    #reposition_cursor()
+#    ##print_tree()
+#    #app.run()
 

+ 13 - 13
bin/darkwallet/gui/print_tree.py

@@ -49,19 +49,19 @@ def print_node_info(parent_id, indent):
 
         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 = ""
+    for prop in api.get_properties(parent_id):
+        if prop.type == PropertyType.STR:
+            prop_val = api.get_property_value(parent_id, prop.name)
+            prop_val = f" = \"{prop_val}\""
+        elif prop.type != PropertyType.BUFFER:
+            prop_val = api.get_property_value(parent_id, prop.name)
+            prop_val = f" = {prop_val}"
+        else:
+            prop_val = ""
 
-    #    prop_type = PropertyType.to_str(prop_type)
+        prop_type = PropertyType.to_str(prop.type)
 
-    #    print(f"{ws}{prop_name}: {prop_type}{prop_val}")
+        print(f"{ws}{prop.name}: {prop_type}{prop_val}")
 
     for sig in api.get_signals(parent_id):
         print(f"{ws}~{sig}")
@@ -71,8 +71,8 @@ def print_node_info(parent_id, indent):
     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]
+        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}")

+ 92 - 76
bin/darkwallet/pydrk/api.py

@@ -93,6 +93,7 @@ class PropertySubType:
     COLOR = 1
     PIXEL = 2
     RESOURCE_ID = 3
+    PY_EXPR = 4
 
     @staticmethod
     def to_str(prop_type):
@@ -105,6 +106,8 @@ class PropertySubType:
                 return "pixel"
             case PropertySubType.RESOURCE_ID:
                 return "resource_id"
+            case PropertySubType.PY_EXPR:
+                return "py_expr"
 
 class PropertyStatus:
     OK = 0
@@ -119,25 +122,28 @@ class ErrorCode:
     PROPERTY_ALREADY_EXISTS = 5
     PROPERTY_NOT_FOUND = 6
     PROPERTY_WRONG_TYPE = 7
-    PROPERTY_WRONG_LEN = 8
-    PROPERTY_WRONG_INDEX = 9
-    PROPERTY_OUT_OF_RANGE = 10
-    PROPERTY_NULL_NOT_ALLOWED = 11
-    PROPERTY_IS_BOUNDED = 12
-    PROPERTY_WRONG_ENUM_ITEM = 13
-    SIGNAL_ALREADY_EXISTS = 14
-    SIGNAL_NOT_FOUND = 15
-    SLOT_NOT_FOUND = 16
-    METHOD_ALREADY_EXISTS = 17
-    METHOD_NOT_FOUND = 18
-    NODES_ARE_LINKED = 19
-    NODES_NOT_LINKED = 20
-    NODE_HAS_PARENTS = 21
-    NODE_HAS_CHILDREN = 22
-    NODE_PARENT_NAME_CONFLICT = 23
-    NODE_CHILD_NAME_CONFLICT = 24
-    NODE_SIBLING_NAME_CONFLICT = 25
-    FILE_NOT_FOUND = 26
+    PROPERTY_WRONG_SUB_TYPE = 8
+    PROPERTY_WRONG_LEN = 9
+    PROPERTY_WRONG_INDEX = 10
+    PROPERTY_OUT_OF_RANGE = 11
+    PROPERTY_NULL_NOT_ALLOWED = 12
+    PROPERTY_IS_BOUNDED = 13
+    PROPERTY_WRONG_ENUM_ITEM = 14
+    SIGNAL_ALREADY_EXISTS = 15
+    SIGNAL_NOT_FOUND = 16
+    SLOT_NOT_FOUND = 17
+    METHOD_ALREADY_EXISTS = 18
+    METHOD_NOT_FOUND = 19
+    NODES_ARE_LINKED = 20
+    NODES_NOT_LINKED = 21
+    NODE_HAS_PARENTS = 22
+    NODE_HAS_CHILDREN = 23
+    NODE_PARENT_NAME_CONFLICT = 24
+    NODE_CHILD_NAME_CONFLICT = 25
+    NODE_SIBLING_NAME_CONFLICT = 26
+    FILE_NOT_FOUND = 27
+    RESOURCE_NOT_FOUND = 28
+    PY_EVAL_ERR = 29
 
     @staticmethod
     def to_str(errc):
@@ -194,6 +200,10 @@ class ErrorCode:
                 return "node_sibling_name_conflict"
             case ErrorCode.FILE_NOT_FOUND:
                 return "file_not_found"
+            case ErrorCode.RESOURCE_NOT_FOUND:
+                return "resource_not_found"
+            case ErrorCode.PY_EVAL_ERR:
+                return "py_eval_err"
 
 def vertex(x, y, r, g, b, a, u, v):
     buf = bytearray()
@@ -246,43 +256,49 @@ class Api:
             case 7:
                 raise exc.PropertyWrongType
             case 8:
-                raise exc.PropertyWrongLen
+                raise exc.PropertyWrongSubType
             case 9:
-                raise exc.PropertyWrongIndex
+                raise exc.PropertyWrongLen
             case 10:
-                raise exc.PropertyOutOfRange
+                raise exc.PropertyWrongIndex
             case 11:
-                raise exc.PropertyNullNotAllowed
+                raise exc.PropertyOutOfRange
             case 12:
-                raise exc.PropertyIsBounded
+                raise exc.PropertyNullNotAllowed
             case 13:
-                raise exc.PropertyWrongEnumItem
+                raise exc.PropertyIsBounded
             case 14:
-                raise exc.SignalAlreadyExists
+                raise exc.PropertyWrongEnumItem
             case 15:
-                raise exc.SignalNotFound
+                raise exc.SignalAlreadyExists
             case 16:
-                raise exc.SlotNotFound
+                raise exc.SignalNotFound
             case 17:
-                raise exc.MethodAlreadyExists
+                raise exc.SlotNotFound
             case 18:
-                raise exc.MethodNotFound
+                raise exc.MethodAlreadyExists
             case 19:
-                raise exc.NodesAreLinked
+                raise exc.MethodNotFound
             case 20:
-                raise exc.NodesNotLinked
+                raise exc.NodesAreLinked
             case 21:
-                raise exc.NodeHasParents
+                raise exc.NodesNotLinked
             case 22:
-                raise exc.NodeHasChildren
+                raise exc.NodeHasParents
             case 23:
-                raise exc.NodeParentNameConflict
+                raise exc.NodeHasChildren
             case 24:
-                raise exc.NodeChildNameConflict
+                raise exc.NodeParentNameConflict
             case 25:
-                raise exc.NodeSiblingNameConflict
+                raise exc.NodeChildNameConflict
             case 26:
+                raise exc.NodeSiblingNameConflict
+            case 27:
                 raise exc.FileNotFound
+            case 28:
+                raise exc.ResourceNotFound
+            case 29:
+                raise exc.PyEvalErr
         return cursor
 
     def hello(self):
@@ -446,10 +462,9 @@ class Api:
         serial.write_u8(req, int(prop.type))
         serial.write_u8(req, int(prop.subtype))
         serial.write_u32(req, int(prop.array_len))
-        if prop.defaults is None:
-            serial.write_u8(req, 0)
-        else:
-            serial.write_u8(req, 1)
+
+        def write_defaults(by):
+            assert prop.defaults is not None
             defaults_len = len(prop.defaults)
             serial.encode_varint(req, defaults_len)
             for default in prop.defaults:
@@ -458,15 +473,19 @@ class Api:
                         serial.write_u32(req, default)
                     case PropertyType.FLOAT32:
                         serial.write_f32(req, default)
+                    case PropertyType.STR:
+                        serial.encode_str(req, default)
                     case _:
                         raise exc.PropertyWrongType
+
+        serial.encode_opt(req, prop.defaults, write_defaults)
+
         serial.encode_str(req, prop.ui_name)
         serial.encode_str(req, prop.desc)
         serial.write_u8(req, int(prop.is_null_allowed))
-        if prop.min_val is None:
-            serial.write_u8(req, 0)
-        else:
-            serial.write_u8(req, 1)
+
+        def write_mxx(v, by):
+            assert v is not None
             match prop.type:
                 case PropertyType.UINT32:
                     serial.write_u32(req, prop.min_val)
@@ -474,17 +493,13 @@ class Api:
                     serial.write_f32(req, prop.min_val)
                 case _:
                     raise exc.PropertyWrongType
-        if prop.max_val is None:
-            serial.write_u8(req, 0)
-        else:
-            serial.write_u8(req, 1)
-            match prop.type:
-                case PropertyType.UINT32:
-                    serial.write_u32(req, prop.max_val)
-                case PropertyType.FLOAT32:
-                    serial.write_f32(req, prop.max_val)
-                case _:
-                    raise exc.PropertyWrongType
+
+        write_min = lambda by: write_mxx(prop.min_val, by)
+        write_max = lambda by: write_mxx(prop.max_val, by)
+
+        serial.encode_opt(req, prop.min_val, write_min)
+        serial.encode_opt(req, prop.max_val, write_max)
+
         serial.encode_varint(req, len(prop.enum_items))
         for enum_item in prop.enum_items:
             if prop.type != PropertyType.ENUM:
@@ -504,10 +519,11 @@ class Api:
         serial.write_u32(req, parent_id)
         self._make_request(Command.UNLINK_NODE, req)
 
-    def set_property_bool(self, node_id, prop_name, val):
+    def set_property_bool(self, node_id, prop_name, i, val):
         req = bytearray()
         serial.write_u32(req, node_id)
         serial.encode_str(req, prop_name)
+        serial.write_u32(req, i)
         serial.write_u8(req, int(val))
         self._make_request(Command.SET_PROPERTY_VALUE, req)
 
@@ -527,7 +543,7 @@ class Api:
         serial.write_f32(req, val)
         self._make_request(Command.SET_PROPERTY_VALUE, req)
 
-    def set_property_buffer(self, node_id, prop_name, i, buf):
+    def set_property_buf(self, node_id, prop_name, i, buf):
         req = bytearray()
         serial.write_u32(req, node_id)
         serial.encode_str(req, prop_name)
@@ -587,23 +603,25 @@ class Api:
         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):
+
+        def read_slot(cur):
             slot_name = serial.decode_str(cur)
             slot_id = serial.read_u32(cur)
-            slots.append((slot_id, slot_name))
+            return (slot_name, slot_id)
+
+        slots = serial.decode_arr(cur, read_slot)
         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):
+
+        def read_method(cur):
             method_name = serial.decode_str(cur)
-            methods.append(method_name)
+            return method_name
+
+        methods = serial.decode_arr(cur, read_method)
         return methods
 
     def get_method(self, node_id, method_name):
@@ -611,18 +629,16 @@ class Api:
         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):
+
+        def read_arg(cur):
             arg_name = serial.decode_str(cur)
+            arg_desc = 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 (arg_name, arg_desc, arg_type)
+
+        args = serial.decode_arr(cur, read_arg)
+        results = serial.decode_arr(cur, read_arg)
+
         return (args, results)
 
     def call_method(self, node_id, method_name, arg_data):

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

@@ -12,6 +12,8 @@ class PropertyNotFound(Exception):
     pass
 class PropertyWrongType(Exception):
     pass
+class PropertyWrongSubType(Exception):
+    pass
 class PropertyWrongLen(Exception):
     pass
 class PropertyWrongIndex(Exception):
@@ -50,3 +52,5 @@ class NodeSiblingNameConflict(Exception):
     pass
 class FileNotFound(Exception):
     pass
+class PyEvalErr(Exception):
+    pass

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

@@ -51,6 +51,14 @@ def encode_buf(by, buf):
     by += buf
     return l
 
+def encode_opt(by, val, write_fn):
+    l = 0
+    if val is None:
+        write_u8(by, 0)
+    else:
+        write_u8(by, 1)
+        write_fn(by)
+
 # Cursor for bytearray type
 class Cursor:
 

+ 26 - 20
bin/darkwallet/src/error.rs

@@ -24,63 +24,69 @@ pub enum Error {
     #[error("Property has wrong type")]
     PropertyWrongType = 7,
 
+    #[error("Property has wrong subtype")]
+    PropertyWrongSubType = 8,
+
     #[error("Property value has the wrong length")]
-    PropertyWrongLen = 8,
+    PropertyWrongLen = 9,
 
     #[error("Property index is wrong")]
-    PropertyWrongIndex = 9,
+    PropertyWrongIndex = 10,
 
     #[error("Property out of range")]
-    PropertyOutOfRange = 10,
+    PropertyOutOfRange = 11,
 
     #[error("Property null not allowed")]
-    PropertyNullNotAllowed = 11,
+    PropertyNullNotAllowed = 12,
 
     #[error("Property array is bounded length")]
-    PropertyIsBounded = 12,
+    PropertyIsBounded = 13,
 
     #[error("Property enum item is invalid")]
-    PropertyWrongEnumItem = 13,
+    PropertyWrongEnumItem = 14,
 
     #[error("Signal already exists")]
-    SignalAlreadyExists = 14,
+    SignalAlreadyExists = 15,
 
     #[error("Signal not found")]
-    SignalNotFound = 15,
+    SignalNotFound = 16,
 
     #[error("Slot not found")]
-    SlotNotFound = 16,
+    SlotNotFound = 17,
 
     #[error("Signal already exists")]
-    MethodAlreadyExists = 17,
+    MethodAlreadyExists = 18,
 
     #[error("Method not found")]
-    MethodNotFound = 18,
+    MethodNotFound = 19,
 
     #[error("Nodes are not linked")]
-    NodesAreLinked = 19,
+    NodesAreLinked = 20,
 
     #[error("Nodes are not linked")]
-    NodesNotLinked = 20,
+    NodesNotLinked = 21,
 
     #[error("Node has parents")]
-    NodeHasParents = 21,
+    NodeHasParents = 22,
 
     #[error("Node has children")]
-    NodeHasChildren = 22,
+    NodeHasChildren = 23,
 
     #[error("Node has a parent with this name")]
-    NodeParentNameConflict = 23,
+    NodeParentNameConflict = 24,
 
     #[error("Node has a child with this name")]
-    NodeChildNameConflict = 24,
+    NodeChildNameConflict = 25,
 
     #[error("Node has a sibling with this name")]
-    NodeSiblingNameConflict = 25,
+    NodeSiblingNameConflict = 26,
 
     #[error("File not found")]
-    FileNotFound = 26,
+    FileNotFound = 27,
 
     #[error("Resource is not found")]
-    ResourceNotFound = 27,
+    ResourceNotFound = 28,
+
+    #[error("Python expr eval error")]
+    PyEvalErr = 29,
 }

+ 304 - 3
bin/darkwallet/src/gfx.rs

@@ -5,11 +5,13 @@ use fontdue::{
 };
 use miniquad::*;
 use std::{
+    array::IntoIter,
     fmt,
     io::Cursor,
-    sync::mpsc,
+    sync::{mpsc, MutexGuard, Arc},
     time::{Duration, Instant},
 };
+use pyo3::{prelude::*, types::{PyDict, IntoPyDict}, PyClass, py_run};
 
 use crate::{
     error::{Error, Result},
@@ -60,6 +62,26 @@ struct Mesh {
     pub index_buffer: BufferId,
 }
 
+#[derive(Debug)]
+struct Rectangle<T> {
+    x: T,
+    y: T,
+    w: T,
+    h: T,
+}
+
+impl<T> Rectangle<T> {
+    fn from_array(arr: [T; 4]) -> Self {
+        let mut iter = IntoIter::new(arr);
+        Self {
+            x: iter.next().unwrap(),
+            y: iter.next().unwrap(),
+            w: iter.next().unwrap(),
+            h: iter.next().unwrap(),
+        }
+    }
+}
+
 type ResourceId = u32;
 
 struct ResourceManager<T> {
@@ -602,6 +624,264 @@ impl Stage {
     }
 }
 
+struct RenderContext<'a> {
+    scene_graph: MutexGuard<'a, SceneGraph>,
+    ctx: &'a mut Box<dyn RenderingBackend>,
+    pipeline: &'a Pipeline,
+    proj: glam::Mat4,
+    textures: &'a ResourceManager<TextureId>,
+}
+
+impl<'a> RenderContext<'a> {
+    fn render_window(&mut self) {
+        for layer in self.scene_graph
+            .lookup_node("/window")
+            .expect("no window attached!")
+            .get_children(&[SceneNodeType::RenderLayer])
+        {
+            if let Err(err) = self.render_layer(layer.id) {
+                error!("error rendering layer '{}': {}", layer.name, err)
+            }
+        }
+    }
+
+    fn get_rect(layer: &SceneNode) -> Result<Rectangle<i32>> {
+        let prop = layer.get_property("rect").ok_or(Error::PropertyNotFound)?;
+        if prop.array_len != 4 {
+            return Err(Error::PropertyWrongLen)
+        }
+        match prop.typ {
+            PropertyType::Uint32 => {
+                if prop.subtype != PropertySubType::Null {
+                    return Err(Error::PropertyWrongSubType)
+                }
+                Ok(Rectangle {
+                    x: prop.get_u32(0)? as i32,
+                    y: prop.get_u32(1)? as i32,
+                    w: prop.get_u32(2)? as i32,
+                    h: prop.get_u32(3)? as i32,
+                })
+            }
+            PropertyType::Str => {
+                if prop.subtype != PropertySubType::PyExpr {
+                    return Err(Error::PropertyWrongSubType)
+                }
+
+                let (screen_width, screen_height) = window::screen_size();
+
+                let locals = Python::with_gil(|py| {
+                    let locals = PyDict::new_bound(py);
+                    locals.set_item("sw", screen_width).expect("set item sw");
+                    locals.set_item("sh", screen_height).expect("set item sh");
+                    locals.unbind()
+                });
+
+                let mut rect = [0; 4];
+                for i in 0..4 {
+                    let code = prop.get_str(i)?;
+                    match eval_py_str(&code, locals.clone()) {
+                        Ok(v) => rect[i] = v as i32,
+                        Err(err) => {
+                            error!("layer '{}': python running `{}` encountered err: {}", layer.name, code, err);
+                            return Err(Error::PyEvalErr)
+                        }
+                    }
+                }
+
+                Ok(Rectangle::from_array(rect))
+            }
+            _ => {
+                Err(Error::PropertyWrongType)
+            }
+        }
+    }
+
+    fn render_layer(&mut self, layer_id: SceneNodeId,
+                    // parent rect
+                    ) -> Result<()> {
+        let layer = self.scene_graph.get_node(layer_id).unwrap();
+
+        if !layer.get_property_bool("is_visible")? {
+            return Ok(())
+        }
+
+        self.ctx.begin_default_pass(PassAction::Nothing);
+        self.ctx.apply_pipeline(&self.pipeline);
+
+        let (_, screen_height) = window::screen_size();
+
+        let mut rect = Self::get_rect(&layer)?;
+        rect.y = screen_height as i32 - (rect.y + rect.h);
+
+        self.ctx.apply_viewport(rect.x, rect.y, rect.w, rect.h);
+        self.ctx.apply_scissor_rect(rect.x, rect.y, rect.w, rect.h);
+
+        // get the rectangle
+        // make sure it's inside the parent's rect
+        for child in layer.get_children(&[SceneNodeType::RenderMesh]) {
+            // x, y, w, h as pixels
+
+            // note that (x, y) is offset by layer rect so it is the pos within layer
+            // layer coords are (0, 0) -> (1, 1)
+
+            // optionally evaluated using python
+
+            // mesh data is (0, 0) to (1, 1)
+            // so scale by (w, h)
+
+            match child.typ {
+                SceneNodeType::RenderMesh => {
+                    if let Err(err) = self.render_mesh(child.id, &rect) {
+                        error!("error rendering mesh '{}': {}", child.name, err);
+                    }
+                },
+                _ => panic!("render_layer(): unknown type")
+            }
+        }
+
+        self.ctx.end_render_pass();
+
+        Ok(())
+    }
+
+    fn get_dim(mesh: &SceneNode, layer_rect: &Rectangle<i32>) -> Result<Rectangle<f32>> {
+        let prop = mesh.get_property("rect").ok_or(Error::PropertyNotFound)?;
+        if prop.array_len != 4 {
+            return Err(Error::PropertyWrongLen)
+        }
+        match prop.typ {
+            PropertyType::Float32 => {
+                if prop.subtype != PropertySubType::Null {
+                    return Err(Error::PropertyWrongSubType)
+                }
+                Ok(Rectangle {
+                    x: prop.get_f32(0)?,
+                    y: prop.get_f32(1)?,
+                    w: prop.get_f32(2)?,
+                    h: prop.get_f32(3)?,
+                })
+            }
+            PropertyType::Str => {
+                if prop.subtype != PropertySubType::PyExpr {
+                    return Err(Error::PropertyWrongSubType)
+                }
+
+                let locals = Python::with_gil(|py| {
+                    let locals = PyDict::new_bound(py);
+                    locals.set_item("lw", layer_rect.w as f32).expect("set item lw");
+                    locals.set_item("lh", layer_rect.h as f32).expect("set item lh");
+                    locals.unbind()
+                });
+
+                let mut rect = [0.; 4];
+                for i in 0..4 {
+                    let code = prop.get_str(i)?;
+                    match eval_py_str(&code, locals.clone()) {
+                        Ok(v) => rect[i] = v,
+                        Err(err) => {
+                            error!("mesh '{}': python running `{}` encountered err: {}", mesh.name, code, err);
+                            return Err(Error::PyEvalErr)
+                        }
+                    }
+                }
+
+                Ok(Rectangle::from_array(rect))
+            }
+            _ => {
+                Err(Error::PropertyWrongType)
+            }
+        }
+    }
+
+    fn render_mesh(&mut self, mesh_id: SceneNodeId, layer_rect: &Rectangle<i32>) -> Result<()> {
+        let mesh = self.scene_graph.get_node(mesh_id).unwrap();
+
+        let data = mesh.get_property("data").ok_or(Error::PropertyNotFound)?;
+        let verts = data.get_buf(0)?;
+        let faces = data.get_buf(1)?;
+
+        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,
+        );
+
+        // temp
+        let texture = self.textures.get(Stage::WHITE_TEXTURE_ID).unwrap();
+
+        let bindings = Bindings {
+            vertex_buffers: vec![vertex_buffer],
+            index_buffer,
+            images: vec![*texture],
+        };
+
+        self.ctx.apply_bindings(&bindings);
+
+        let rect = Self::get_dim(mesh, layer_rect)?;
+        //debug!("mesh rect: {:?}", rect);
+
+        let layer_w = layer_rect.w as f32;
+        let layer_h = layer_rect.h as f32;
+        let off_x = rect.x / layer_w;
+        let off_y = rect.y / layer_h;
+        let scale_x = rect.w / layer_w;
+        let scale_y = rect.h / layer_h;
+        //let model = glam::Mat4::IDENTITY;
+        let model = glam::Mat4::from_translation(glam::Vec3::new(off_x, off_y, 0.)) *
+            glam::Mat4::from_scale(glam::Vec3::new(scale_x, scale_y, 1.));
+
+        let mut uniforms_data = [0u8; 128];
+        let data: [u8; 64] = unsafe { std::mem::transmute_copy(&self.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);
+
+        self.ctx.delete_buffer(index_buffer);
+        self.ctx.delete_buffer(vertex_buffer);
+
+        Ok(())
+    }
+}
+
+fn eval_py_str<'py>(code: &str, locals: Py<PyDict>) -> PyResult<f32> {
+    Python::with_gil(|py| {
+        let null = ();
+        // https://stackoverflow.com/questions/35804961/python-eval-is-it-still-dangerous-if-i-disable-builtins-and-attribute-access
+        // See safe_eval() by tardyp and astrun
+        // We don't care about resource usage, just accessing system resources.
+        // Can also use restrictedpython lib to eval the code.
+        // Also PyPy sandboxing
+        // and starlark / starlark-rust
+        py_run!(py, null, r#"
+__builtins__.__dict__['__import__'] = None
+__builtins__.__dict__['open'] = None
+        "#);
+
+        let locals = locals.bind(py);
+        let result: f32 = py.eval_bound(code, None, Some(locals))?.extract()?;
+        Ok(result)
+    })
+}
+
 /*
 fn get_obj_props(obj: &SceneNode) -> Result<(f32, f32, f32, f32, bool)> {
     let x = obj.get_property_f32("x")?;
@@ -655,13 +935,29 @@ impl EventHandler for Stage {
         // 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. / screen_width, -2. / screen_height, 1.));
+            glam::Mat4::from_scale(glam::Vec3::new(2., -2., 1.));
+        //let proj = glam::Mat4::IDENTITY;
 
         // Reusable text layout
         //let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
 
         let scene_graph = self.scene_graph.lock().unwrap();
+        let window_id = scene_graph.lookup_node_id("/window").expect("no window attached");
+
+        // We need this because scene_graph must remain locked for the duration of the rendering
+        let mut render_context = RenderContext {
+            scene_graph,
+            ctx: &mut self.ctx,
+            pipeline: &self.pipeline,
+            proj,
+            textures: &self.textures,
+        };
+
+        render_context.render_window();
 
+        drop(render_context);
+
+        /*
         for layer in scene_graph
             .lookup_node("/window")
             .expect("no window attached!")
@@ -823,6 +1119,7 @@ impl EventHandler for Stage {
             self.ctx.end_render_pass();
             */
         }
+        */
         self.ctx.commit_frame();
     }
 
@@ -1001,11 +1298,15 @@ impl EventHandler for Stage {
     }
 
     fn resize_event(&mut self, width: f32, height: f32) {
-        let mut scene_graph = self.scene_graph.lock().unwrap();
         let mut data = vec![];
         width.encode(&mut data).unwrap();
         height.encode(&mut data).unwrap();
+
+        let mut scene_graph = self.scene_graph.lock().unwrap();
         let win = scene_graph.lookup_node_mut("/window").unwrap();
+        let prop = win.get_property("screen_size").unwrap();
+        prop.set_f32(0, width).unwrap();
+        prop.set_f32(1, height).unwrap();
         win.trigger("resize", data).unwrap();
     }
 }

+ 32 - 4
bin/darkwallet/src/main.rs

@@ -35,8 +35,36 @@ fn init_zmq(scene_graph: SceneGraphPtr) {
 
 fn main() {
     let scene_graph = Arc::new(Mutex::new(SceneGraph::new()));
-    //init_zmq(scene_graph.clone());
-    //init_gui(scene_graph);
-    let mut zmq_rpc = ZeroMQAdapter::new(scene_graph);
-    zmq_rpc.poll();
+    init_zmq(scene_graph.clone());
+    init_gui(scene_graph);
 }
+
+/*
+use pyo3::{prelude::*, types::{PyDict, IntoPyDict}, py_run};
+
+fn main() -> PyResult<()> {
+    Python::with_gil(|py| {
+        let null = ();
+        // https://stackoverflow.com/questions/35804961/python-eval-is-it-still-dangerous-if-i-disable-builtins-and-attribute-access
+        // See safe_eval() by tardyp and astrun
+        // We don't care about resource usage, just accessing system resources.
+        // Can also use restrictedpython lib to eval the code.
+        // Also PyPy sandboxing
+        // and starlark / starlark-rust
+        py_run!(py, null, r#"
+__builtins__.__dict__['__import__'] = None
+__builtins__.__dict__['open'] = None
+        "#);
+
+        let locals = PyDict::new_bound(py);
+        locals.set_item("lw", 110)?;
+        locals.set_item("lh", 4)?;
+
+        let code = "min(1 + lw/3, 4*10)";
+        let user: f32 = py.eval_bound(code, None, Some(&locals))?.extract()?;
+
+        println!("{}", user);
+        Ok(())
+    })
+}
+*/

+ 17 - 1
bin/darkwallet/src/net.rs

@@ -262,6 +262,13 @@ impl ZeroMQAdapter {
                             }
                             prop.set_defaults_f32(prop_defaults)?;
                         }
+                        PropertyType::Str => {
+                            let mut prop_defaults = vec![];
+                            for _ in 0..prop_defaults_len.0 {
+                                prop_defaults.push(String::decode(&mut cur).unwrap());
+                            }
+                            prop.set_defaults_str(prop_defaults)?;
+                        }
                         _ => return Err(Error::PropertyWrongType),
                     }
                 }
@@ -307,7 +314,16 @@ impl ZeroMQAdapter {
                         prop.min_val = min;
                         prop.max_val = max;
                     }
-                    _ => return Err(Error::PropertyWrongType),
+                    _ => {
+                        let min_is_some = bool::decode(&mut cur).unwrap();
+                        if min_is_some {
+                            return Err(Error::PropertyWrongType)
+                        }
+                        let max_is_some = bool::decode(&mut cur).unwrap();
+                        if max_is_some {
+                            return Err(Error::PropertyWrongType)
+                        }
+                    },
                 }
 
                 let prop_enum_items = Vec::<String>::decode(&mut cur).unwrap();

+ 6 - 2
bin/darkwallet/src/prop.rs

@@ -13,8 +13,6 @@ use std::{
 
 use crate::scene::SceneNodeId;
 
-type BufferGuard<'a> = MutexGuard<'a, Vec<u8>>;
-
 type Buffer = Arc<Vec<u8>>;
 
 #[derive(Debug, Copy, Clone, PartialEq, SerialEncodable, SerialDecodable)]
@@ -53,6 +51,7 @@ pub enum PropertySubType {
     // Size of something in pixels
     Pixel = 2,
     ResourceId = 3,
+    PyExpr = 4,
 }
 
 #[derive(Debug, Clone)]
@@ -250,6 +249,11 @@ impl Property {
         self.defaults = defaults.into_iter().map(|v| PropertyValue::Float32(v)).collect();
         Ok(())
     }
+    pub fn set_defaults_str(&mut self, defaults: Vec<String>) -> Result<()> {
+        self.check_defaults_len(defaults.len())?;
+        self.defaults = defaults.into_iter().map(|v| PropertyValue::Str(v)).collect();
+        Ok(())
+    }
 
     /// This will clear all values, resetting them to the default
     pub fn clear_values(&self) {

+ 7 - 7
bin/darkwallet/src/scene.rs

@@ -390,15 +390,15 @@ impl SceneNode {
         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 {
+    pub fn get_children(
+        &self,
+        allowed_types: &[SceneNodeType],
+    ) -> Vec<SceneNodeInfo> {
         self.children
             .iter()
-            .filter(move |child_inf| child_inf.typ == typ)
-            .map(|child_inf| scene_graph.get_node(child_inf.id).unwrap())
+            .cloned()
+            .filter(move |child_inf| allowed_types.contains(&child_inf.typ))
+            .collect()
     }
 
     pub fn add_property(&mut self, prop: Property) -> Result<()> {