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

app: make shape data a native property value, make shape a vector_art property and allow passing it from netdbg

darkfi 3 недель назад
Родитель
Сommit
a5b8f34f6e

+ 25 - 0
bin/app/pydrk/api.py

@@ -86,6 +86,7 @@ class PropertyType:
     ENUM = 5
     SCENE_NODE_ID = 7
     SEXPR = 8
+    VECTOR_SHAPE = 9
 
     @staticmethod
     def to_str(prop_type):
@@ -106,6 +107,8 @@ class PropertyType:
                 return "scene_node_id"
             case PropertyType.SEXPR:
                 return "sexpr"
+            case PropertyType.VECTOR_SHAPE:
+                return "vector_shape"
 
 class PropertySubType:
     NULL = 0
@@ -528,6 +531,9 @@ class Api:
                 return serial.decode_str(cur)
             case PropertyType.SCENE_NODE_ID:
                 return serial.read_u32(cur)
+            case PropertyType.VECTOR_SHAPE:
+                # Shapes carry no payload on the get path
+                return "<...>"
             case _:
                 raise Exception("unknown property type returned")
 
@@ -715,6 +721,25 @@ class Api:
         serial.encode_str(req, expr_str)
         self._make_request(Command.SET_PROPERTY_VALUE, req)
 
+    def set_property_shape(self, node_path, prop_name, i, verts, indices):
+        # verts: list of (x_expr, y_expr, [r, g, b, a]) tuples, where the
+        # coordinate exprs use the same source language as set_property_expr
+        req = bytearray()
+        serial.encode_str(req, node_path)
+        serial.encode_str(req, prop_name)
+        serial.write_u32(req, i)
+        serial.write_u8(req, PropertyType.VECTOR_SHAPE)
+        serial.encode_varint(req, len(verts))
+        for (x_expr, y_expr, color) in verts:
+            serial.encode_str(req, x_expr)
+            serial.encode_str(req, y_expr)
+            for c in color:
+                serial.write_f32(req, c)
+        serial.encode_varint(req, len(indices))
+        for index in indices:
+            serial.write_u16(req, index)
+        self._make_request(Command.SET_PROPERTY_VALUE, req)
+
     def get_signals(self, node_path):
         req = bytearray()
         serial.encode_str(req, node_path)

+ 3 - 0
bin/app/src/app/node.rs

@@ -93,6 +93,9 @@ pub fn create_vector_art(name: &str) -> SceneNode {
     prop.set_defaults_bool(vec![true]).unwrap();
     node.add_property(prop).unwrap();
 
+    let prop = Property::new("shape", PropertyType::VectorShape, PropertySubType::Null);
+    node.add_property(prop).unwrap();
+
     let mut prop = Property::new("rect", PropertyType::Float32, PropertySubType::Pixel);
     prop.set_array_len(4);
     prop.allow_exprs();

+ 31 - 14
bin/app/src/app/schema/chat.rs

@@ -290,7 +290,8 @@ pub async fn make(
         expr::load_var("h"),
         [[0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.8]],
     );
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
 
     // Create the toolbar bg
@@ -348,7 +349,9 @@ pub async fn make(
         0.2,
     );
 
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+
+    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
 
     // Create the send button
@@ -361,8 +364,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
     let back_btn_bg_node =
-        node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+        node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(back_btn_bg_node.clone());
 
     // Create the back button
@@ -528,7 +532,8 @@ pub async fn make(
     //    expr::load_var("h"),
     //    [0.41, 0.6, 0.65, 1.],
     //);
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
 
     // Main content view
@@ -755,7 +760,8 @@ pub async fn make(
     let mut shape =
         shape::create_down_bgtab(rgba!(0x003232ff), rgba!(0x294f60ff), 0.1).scaled(DOWNARROW_SCALE);
     shape.join(shape::create_down_arrow(rgba!(0x00f0ffff), 1.));
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     down_layer.link(node);
 
     // Create the p2p toggle button
@@ -819,7 +825,8 @@ pub async fn make(
         1.,
         COLOR_RED,
     ));
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     select_layer.link(node);
 
     let node = create_vector_art("copy_bg");
@@ -831,7 +838,8 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, CHATEDIT_HEIGHT).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
     let shape = shape::create_copy(COLOR_WHITE).scaled(COPY_BTN_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     select_layer.link(node);
 
     // unselect_btn sits over the back button and calls the chatview's unselect.
@@ -965,7 +973,8 @@ pub async fn make(
     //    expr::load_var("h"),
     //    [0.41, 0.6, 0.65, 1.],
     //);
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
 
     // Create the send button
@@ -979,7 +988,8 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 5).unwrap();
     let shape = shape::create_send_arrow().scaled(EMOJI_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
 
     // Create the emoji button
@@ -997,7 +1007,8 @@ pub async fn make(
         ColorScheme::PaperLight => [0., 0., 0., 1.],
     };
     let shape = shape::create_emoji_selector(color).scaled(EMOJI_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
 
     // Create the emoji button
@@ -1012,7 +1023,8 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 5).unwrap();
     let shape = shape::create_close_icon().scaled(EMOJI_CLOSE_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
 
     // Text edit
@@ -1186,8 +1198,9 @@ pub async fn make(
             [0., 0., 0., 0.],
         ]
     );
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
     let node =
-        node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+        node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
     */
 
@@ -1581,7 +1594,9 @@ pub async fn make(
         [0.29, 0.51, 0.45, 1.],
     );
 
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+
+    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     nick_row_node.link(node);
 
     // Create some text
@@ -1674,7 +1689,9 @@ pub async fn make(
         [0.29, 0.51, 0.45, 1.],
     );
 
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+
+    let node = node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     me_row_node.link(node);
 
     // Create some text

+ 57 - 57
bin/app/src/app/schema/menu/channel.rs

@@ -246,9 +246,9 @@ pub async fn make(
         0.2,
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content.link(node);
 
     // Create back arrow
@@ -261,9 +261,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content.link(node);
 
     // Create back button
@@ -382,9 +382,9 @@ pub async fn make(
     shape.verts.append(&mut verts);
     shape.indices.append(&mut indices);
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("active_tab_overlay");
@@ -421,9 +421,9 @@ pub async fn make(
         COLOR_CYAN,
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("active_tab_bg");
@@ -443,9 +443,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("inactive_tab_overlay");
@@ -489,9 +489,9 @@ pub async fn make(
         COLOR_INACTIVE,
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("input_area_bg");
@@ -513,9 +513,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("fullscreen_label_bg");
@@ -580,9 +580,9 @@ pub async fn make(
         COLOR_CYAN,
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_button("channels_tab_btn");
@@ -635,9 +635,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, CHANNELS_ICON_SCALE).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_channels_icon(COLOR_MINT).scaled(CHANNELS_ICON_SCALE);
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_text("channels_tab_text");
@@ -730,9 +730,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, CONTACTS_ICON_SCALE).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_contacts_icon(COLOR_INACTIVE).scaled(CONTACTS_ICON_SCALE);
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_text("contacts_tab_text");
@@ -895,9 +895,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("search_outline");
@@ -919,9 +919,9 @@ pub async fn make(
         [0.3, 0.3, 0.3, 1.],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_singleline_edit("nick_edit");
@@ -1045,9 +1045,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("nick_outline");
@@ -1067,9 +1067,9 @@ pub async fn make(
         CONTENT_OUTLINE_SIZE,
         [0.3, 0.3, 0.3, 1.],
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_singleline_edit("secret_edit");
@@ -1195,9 +1195,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("secret_outline");
@@ -1219,9 +1219,9 @@ pub async fn make(
         CONTENT_OUTLINE_SIZE,
         [0.3, 0.3, 0.3, 1.],
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("receive_copy_btn_bg");
@@ -1240,9 +1240,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 8).unwrap();
 
     let shape = shape::create_copy(COLOR_CYAN).scaled(COPY_SCALE);
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     // paste clipboard into the KEY field
@@ -1318,9 +1318,9 @@ pub async fn make(
         COLOR_CYAN,
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     editlayer_node.link(node);
 
     let mut node = create_button("addchannel_btn");
@@ -1406,9 +1406,9 @@ pub async fn make(
         CONTENT_OUTLINE_SIZE,
         COLOR_CYAN,
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     editlayer_node.link(node);
 
     let node = create_button("gensecret_btn");

+ 54 - 54
bin/app/src/app/schema/menu/contact.rs

@@ -246,9 +246,9 @@ pub async fn make(
         0.2,
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content.link(node);
 
     // Create back arrow
@@ -261,9 +261,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content.link(node);
 
     // Create back button
@@ -382,9 +382,9 @@ pub async fn make(
     shape.verts.append(&mut verts);
     shape.indices.append(&mut indices);
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("active_tab_overlay");
@@ -421,9 +421,9 @@ pub async fn make(
         COLOR_CYAN,
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("active_tab_bg");
@@ -443,9 +443,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("inactive_tab_overlay");
@@ -489,9 +489,9 @@ pub async fn make(
         COLOR_INACTIVE,
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("input_area_bg");
@@ -513,9 +513,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("fullscreen_label_bg");
@@ -581,9 +581,9 @@ pub async fn make(
         COLOR_CYAN,
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_button("contacts_tab_btn");
@@ -634,9 +634,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, CONTACTS_ICON_SCALE).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_contacts_icon(COLOR_MINT).scaled(CONTACTS_ICON_SCALE);
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_text("contacts_tab_text");
@@ -727,9 +727,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, CHANNELS_ICON_SCALE).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_channels_icon(COLOR_INACTIVE).scaled(CHANNELS_ICON_SCALE);
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_text("channels_tab_text");
@@ -896,9 +896,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("search_outline");
@@ -920,9 +920,9 @@ pub async fn make(
         [0.3, 0.3, 0.3, 1.],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_singleline_edit("nick_edit");
@@ -1047,9 +1047,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("nick_outline");
@@ -1070,9 +1070,9 @@ pub async fn make(
         [0.3, 0.3, 0.3, 1.],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_singleline_edit("secret_edit");
@@ -1198,9 +1198,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("secret_outline");
@@ -1222,9 +1222,9 @@ pub async fn make(
         CONTENT_OUTLINE_SIZE,
         [0.3, 0.3, 0.3, 1.],
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     let node = create_vector_art("receive_copy_btn_bg");
@@ -1243,9 +1243,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 8).unwrap();
 
     let shape = shape::create_copy(COLOR_CYAN).scaled(COPY_SCALE);
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
 
     // Clickable overlay: paste clipboard contents into the KEY field.
@@ -1340,9 +1340,9 @@ pub async fn make(
         COLOR_CYAN,
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     editlayer_node.link(node);
 
     let node = create_button("addcontact_btn");

+ 3 - 3
bin/app/src/app/schema/menu/edit_buttons.rs

@@ -154,9 +154,9 @@ pub async fn create_edit_buttons(
         COLOR_CYAN,
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     editlayer_node.link(node);
 
     // Create the cancel button

+ 12 - 12
bin/app/src/app/schema/menu/mod.rs

@@ -258,9 +258,9 @@ pub async fn make(
         0.2,
     );
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     // Create some text
@@ -334,9 +334,9 @@ pub async fn make(
     node.set_property_f32(atom, Role::App, "scale", VERBLOCK_SCALE).unwrap();
     let shape = shape::create_version_block([1., 0., 0.25, 1.]);
 
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     // Write / Menu button
@@ -380,9 +380,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, MENU_ICON_SCALE).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     let shape = shape::create_menu_icon(COLOR_CYAN).scaled(MENU_ICON_SCALE);
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     mainlayer_node.link(node);
 
     // Create cancel/done edit buttons
@@ -557,9 +557,9 @@ pub async fn setup_wallet_button(app: &App, chat_layer: SceneNodePtr, i18n_fish:
     shape.join(shape::create_blockchain_netlogo2(COLOR_CYAN));
     shape.join(shape::create_blockchain_netlogo3(COLOR_CYAN));
     shape.join(shape::create_blockchain_netlogo4(COLOR_CYAN));
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     menu_layer.link(node);
 
     // Wallet button (above write_btn, square: w = h = MENU_BTN_W_R)

+ 22 - 20
bin/app/src/app/schema/mod.rs

@@ -442,8 +442,9 @@ pub async fn make(
             expr::load_var("h"),
             [c, c, c, 0.3],
         );
+        node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
         let node = node
-            .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+            .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
             .await;
         window.link(node);
     }
@@ -506,9 +507,9 @@ pub async fn make(
     let mut shape = shape::create_netlogo1([1., 0., 0.25, 1.]);
     shape.join(shape::create_netlogo2([0.27, 0.4, 0.4, 1.]));
     shape.join(shape::create_netlogo3([0.27, 0.4, 0.4, 1.]));
-    let net0_node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let net0_node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     netlayer_node.link(net0_node);
 
     let node = create_vector_art("net1");
@@ -523,9 +524,9 @@ pub async fn make(
     let mut shape = shape::create_netlogo1([0.49, 0.57, 1., 1.]);
     shape.join(shape::create_netlogo2([0.49, 0.57, 1., 1.]));
     shape.join(shape::create_netlogo3([0.27, 0.4, 0.4, 1.]));
-    let net1_node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let net1_node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     netlayer_node.link(net1_node);
 
     let node = create_vector_art("net2");
@@ -540,9 +541,9 @@ pub async fn make(
     let mut shape = shape::create_netlogo1([0., 0.94, 1., 1.]);
     shape.join(shape::create_netlogo2([0., 0.94, 1., 1.]));
     shape.join(shape::create_netlogo3([0., 0.94, 1., 1.]));
-    let net2_node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let net2_node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     netlayer_node.link(net2_node);
 
     let node = create_vector_art("net3");
@@ -557,9 +558,9 @@ pub async fn make(
     let mut shape = shape::create_netlogo1([0., 0.94, 1., 1.]);
     shape.join(shape::create_netlogo2([0., 0.94, 1., 1.]));
     shape.join(shape::create_netlogo3([0., 0.94, 1., 1.]));
-    let net3_node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let net3_node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     netlayer_node.link(net3_node);
 
     // netstat-klik icon (visual feedback when reconnect button is clicked)
@@ -581,9 +582,9 @@ pub async fn make(
         expr::const_f32(NETSTATUS_ICON_SIZE),
         klik_color,
     );
-    let netstat_klik_node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let netstat_klik_node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     netlayer_node.link(netstat_klik_node.clone());
 
     // Reconnect Button (overlaid on netstatus icons)
@@ -745,9 +746,9 @@ pub async fn make(
         NETSTAT_OVERLAY_TOGGLE_OUTLINE_W,
         [0.08, 0.68, 0.72, 1.],
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     overlay_node.link(node);
 
     let node = create_text("p2p_label");
@@ -908,8 +909,9 @@ pub async fn make(
             NETSTAT_OVERLAY_TOGGLE_OUTLINE_W,
             [0.08, 0.68, 0.72, 1.],
         );
+        node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
         let node = node
-            .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+            .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
             .await;
         overlay_node.link(node.clone());
         transport_sel_nodes.push(node);

+ 34 - 15
bin/app/src/app/schema/settings.rs

@@ -200,8 +200,10 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         sep_color,
     );
 
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     // Create the back button
@@ -214,8 +216,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     let node = create_button("back_btn");
@@ -310,8 +313,10 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         [0.41, 0.6, 0.65, 1.],
     );
 
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     // Search Bar Input
@@ -359,8 +364,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
     let shape = shape::create_logo([1., 1., 1., 1.]).scaled(500.);
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     // Search placeholder
@@ -527,8 +533,10 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         //[0., 0.11, 0.11, 0.4],
     );
 
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     let node = create_vector_art("search_bg2");
@@ -569,8 +577,10 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         [0.15, 0.2, 0.19, 1.],
     );
 
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     // Create a BTreeMap to store settings
@@ -681,8 +691,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
             [0.15, 0.2, 0.19, 1.],
         );
 
+        node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
         let node = node
-            .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+            .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
             .await;
         setting_layer_node.link(node);
 
@@ -748,8 +759,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
                 [0.15, 0.2, 0.19, 1.],
             );
 
+            node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
             let node = node
-                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+                .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
                 .await;
             setting_layer_node.link(node);
 
@@ -814,8 +826,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
                 [0.15, 0.2, 0.19, 1.],
             );
 
+            node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
             let node = node
-                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+                .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
                 .await;
             setting_layer_node.link(node);
         } else {
@@ -874,8 +887,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
                 [0.15, 0.2, 0.19, 1.],
             );
 
+            node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
             let node = node
-                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+                .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
                 .await;
             setting_layer_node.link(node);
         }
@@ -1026,8 +1040,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
                 .unwrap();
 
                 let shape = shape::create_circle([0.9, 0.4, 0.4, 0.7]).scaled(5.);
+                node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
                 let node = node
-                    .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+                    .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
                     .await;
                 setting_layer_node.link(node);
 
@@ -1055,8 +1070,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
                 .unwrap();
 
                 let shape = shape::create_circle([0., 0.94, 1., 1.]).scaled(5.);
+                node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
                 let node = node
-                    .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+                    .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
                     .await;
                 setting_layer_node.link(node);
 
@@ -1374,8 +1390,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
             node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
 
             let shape = shape::create_switch([0., 0.94, 1., 1.]).scaled(10.);
+            node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
             let node = node
-                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+                .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
                 .await;
             setting_layer_node.link(node);
         } else {
@@ -1390,8 +1407,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
             node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
 
             let shape = shape::create_confirm([0., 0.94, 1., 1.]).scaled(10.);
+            node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
             let node = node
-                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+                .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
                 .await;
             setting_layer_node.link(node.clone());
 
@@ -1443,8 +1461,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
 
         let shape = shape::create_reset([0., 0.94, 1., 1.]).scaled(15.);
+        node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
         let node = node
-            .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+            .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
             .await;
         setting_layer_node.link(node);
 

+ 9 - 6
bin/app/src/app/schema/test.rs

@@ -128,9 +128,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         // Color
         [0., 1., 0., 1.],
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     /*
@@ -162,8 +162,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     };
     let indices = vec![0, 2, 1, 1, 2, 3];
     let shape = VectorShape { verts, indices };
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     // Create the button
@@ -210,8 +211,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     };
     let indices = vec![0, 2, 1, 1, 2, 3];
     let shape = VectorShape { verts, indices };
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     // Debugging tool
@@ -240,8 +242,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         expr::load_var("h"),
         [0., 1., 0., 1.],
     );
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     // Create KING GNU!

+ 3 - 3
bin/app/src/app/schema/test_edit.rs

@@ -80,9 +80,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         expr::load_var("h"),
         [0., 0., 0., 1.],
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 
     // Text edit

+ 3 - 3
bin/app/src/app/schema/test_scroll_layer.rs

@@ -73,8 +73,8 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         1.,
         COLOR_PURPLE,
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
 }

+ 15 - 15
bin/app/src/app/schema/wallet/main.rs

@@ -108,9 +108,9 @@ pub async fn make(
             .scaled(BACKARROW_SCALE)
             .offset(Point::new(BACKARROW_X, BACKARROW_Y)),
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     wallet_layer.link(node.clone());
     let back_bg_is_visible = PropertyBool::wrap(&node, Role::App, "is_visible", 0).unwrap();
 
@@ -307,9 +307,9 @@ pub async fn make(
         1.,
         COLOR_TEAL,
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     main_layer.link(node);
 
     // Receive button click handler
@@ -425,9 +425,9 @@ pub async fn make(
         1.,
         COLOR_TEAL,
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     main_layer.link(node);
 
     // Send button click handler
@@ -590,9 +590,9 @@ async fn create_chat_btn(
         1.,
         [0.2, 0.2745, 0.2784, 1.],
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     parent.link(node);
 
     let node = create_vector_art("chat_btn_shape");
@@ -608,9 +608,9 @@ async fn create_chat_btn(
     let mut shape = shape::create_netlogo1([0., 0.94, 1., 1.]);
     shape.join(shape::create_netlogo2([0., 0.94, 1., 1.]));
     shape.join(shape::create_netlogo3([0., 0.94, 1., 1.]));
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     parent.link(node);
 
     let node = create_button("chat_btn");

+ 12 - 12
bin/app/src/app/schema/wallet/netstatus.rs

@@ -105,9 +105,9 @@ pub async fn make(
     shape.join(shape::create_blockchain_netlogo2([0.27, 0.4, 0.4, 1.]));
     shape.join(shape::create_blockchain_netlogo3([0.27, 0.4, 0.4, 1.]));
     shape.join(shape::create_blockchain_netlogo4([0.27, 0.4, 0.4, 1.]));
-    let net0_node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let net0_node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     netlayer_node.link(net0_node);
 
     let node = create_vector_art("net1");
@@ -124,9 +124,9 @@ pub async fn make(
     shape.join(shape::create_blockchain_netlogo2([0.73, 0.62, 1., 1.]));
     shape.join(shape::create_blockchain_netlogo3([0.27, 0.4, 0.4, 1.]));
     shape.join(shape::create_blockchain_netlogo4([0.27, 0.4, 0.4, 1.]));
-    let net1_node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let net1_node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     netlayer_node.link(net1_node);
 
     let node = create_vector_art("net2");
@@ -143,9 +143,9 @@ pub async fn make(
     shape.join(shape::create_blockchain_netlogo2([0.49, 0.57, 1., 1.]));
     shape.join(shape::create_blockchain_netlogo3([0.49, 0.57, 1., 1.]));
     shape.join(shape::create_blockchain_netlogo4([0.27, 0.4, 0.4, 1.]));
-    let net2_node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let net2_node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     netlayer_node.link(net2_node);
 
     let node = create_vector_art("net3");
@@ -162,9 +162,9 @@ pub async fn make(
     shape.join(shape::create_blockchain_netlogo2([0., 0.94, 1., 1.]));
     shape.join(shape::create_blockchain_netlogo3([0., 0.94, 1., 1.]));
     shape.join(shape::create_blockchain_netlogo4([0., 0.94, 1., 1.]));
-    let net3_node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let net3_node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     netlayer_node.link(net3_node);
 
     netlayer_node

+ 3 - 3
bin/app/src/app/schema/wallet/receive.rs

@@ -131,9 +131,9 @@ pub async fn make(
     prop.add_depend(&addr_h_prop, 0, "addr_height");
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_copy(COLOR_CYAN).scaled(COPY_SCALE);
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     receive_layer.link(node);
 
     let node = create_button("receive_copy_btn");

+ 6 - 6
bin/app/src/app/schema/wallet/send_step2.rs

@@ -217,9 +217,9 @@ pub async fn make(
         1.,
         [0.2, 0.2745, 0.2784, 1.],
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     send_step2_layer.link(node);
 
     // Recipient input
@@ -317,9 +317,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_copy(COLOR_CYAN).scaled(PASTE_SCALE);
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     send_step2_layer.link(node);
 
     let node = create_button("send_paste_btn");

+ 20 - 17
bin/app/src/app/schema/wallet/util.rs

@@ -259,9 +259,9 @@ pub async fn create_bg_mesh(
         expr::load_var("h"),
         [[0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.8]],
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer.link(node);
 }
 
@@ -313,9 +313,9 @@ pub async fn create_header_bg(
         8,
         0.2,
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer.link(node);
 }
 
@@ -345,8 +345,9 @@ pub async fn create_separator_expr(
         expr::const_f32(1.),
         [0.2, 0.2745, 0.2784, 1.],
     );
+    sep_node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
     let sep_node = sep_node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
         .await;
     layer.link(sep_node.clone());
     sep_node
@@ -377,8 +378,8 @@ pub async fn create_separator(
         expr::const_f32(1.),
         [0.2, 0.2745, 0.2784, 1.],
     );
-    let sep_node =
-        sep_node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
+    sep_node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let sep_node = sep_node.setup(|me| VectorArt::new(me, renderer.clone(), redraw.clone())).await;
     layer.link(sep_node.clone());
 
     *y += 1.;
@@ -417,9 +418,9 @@ pub async fn create_bottom_button(
         1.,
         COLOR_TEAL,
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer.link(node.clone());
 
     // Button label text (if provided)
@@ -508,8 +509,9 @@ pub async fn create_bottom_button_with_states(
         1.,
         COLOR_TEAL,
     );
+    bg_valid.set_property_shape(atom, Role::App, "shape", shape).unwrap();
     let bg_valid = bg_valid
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
         .await;
     layer.link(bg_valid.clone());
 
@@ -533,8 +535,9 @@ pub async fn create_bottom_button_with_states(
         1.,
         [0.5, 0.5, 0.5, 1.],
     );
+    bg_invalid.set_property_shape(atom, Role::App, "shape", shape).unwrap();
     let bg_invalid = bg_invalid
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
         .await;
     layer.link(bg_invalid.clone());
 
@@ -658,9 +661,9 @@ pub async fn create_tooltip(
         1.,
         text_color,
     );
-    let node = node
-        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     tooltip_layer.link(node);
 
     // Create text

+ 44 - 4
bin/app/src/net.rs

@@ -27,7 +27,7 @@ use crate::{
     gfx::gfxtag,
     prop::{PropertyType, Role},
     scene::{SceneNodeId, SceneNodePtr, ScenePath, Slot},
-    ui::RedrawTrigger,
+    ui::{RedrawTrigger, ShapeVertex, VectorShape},
     ExecutorPtr,
 };
 
@@ -238,13 +238,19 @@ impl ZeroMQAdapter {
                         decompile(&expr).encode(&mut reply).unwrap();
                     } else if val.is_unset() {
                         1u8.encode(&mut reply).unwrap();
-                        let default = &prop.defaults[i];
-                        default.encode(&mut reply).unwrap();
+                        // Shapes are not serialized on the get path;
+                        // the python client shows a "<...>" placeholder.
+                        if prop.typ != PropertyType::VectorShape {
+                            let default = &prop.defaults[i];
+                            default.encode(&mut reply).unwrap();
+                        }
                     } else if val.is_null() {
                         2u8.encode(&mut reply).unwrap();
                     } else {
                         0u8.encode(&mut reply).unwrap();
-                        val.encode(&mut reply).unwrap();
+                        if prop.typ != PropertyType::VectorShape {
+                            val.encode(&mut reply).unwrap();
+                        }
                     }
                 }
             }
@@ -297,6 +303,40 @@ impl ZeroMQAdapter {
                         let code = Compiler::new().compile(&expr_str)?;
                         prop.set_expr(atom, Role::User, prop_i, code)?;
                     }
+                    PropertyType::VectorShape => {
+                        // Vertices carry coordinate exprs as source strings,
+                        // compiled with the same const-free compiler. The
+                        // payload is: vert count varint; per vert: x expr
+                        // string, y expr string, 4x f32 color; index count
+                        // varint; u16 indices.
+                        let cc = Compiler::new();
+                        let vert_count = VarInt::decode(&mut cur)?.0 as usize;
+                        let mut verts = vec![];
+                        for _ in 0..vert_count {
+                            let x_src = String::decode(&mut cur)?;
+                            let y_src = String::decode(&mut cur)?;
+                            let color = [
+                                f32::decode(&mut cur)?,
+                                f32::decode(&mut cur)?,
+                                f32::decode(&mut cur)?,
+                                f32::decode(&mut cur)?,
+                            ];
+                            let x = cc.compile(&x_src)?;
+                            let y = cc.compile(&y_src)?;
+                            verts.push(ShapeVertex::new(x, y, color));
+                        }
+                        let index_count = VarInt::decode(&mut cur)?.0 as usize;
+                        let mut indices = vec![];
+                        for _ in 0..index_count {
+                            let index = u16::decode(&mut cur)?;
+                            if index as usize >= verts.len() {
+                                return Err(Error::PropertyWrongIndex)
+                            }
+                            indices.push(index);
+                        }
+                        let shape = VectorShape { verts, indices };
+                        prop.set_shape(atom, Role::User, prop_i, shape)?;
+                    }
                 }
             }
             Command::AddNode => {

+ 54 - 2
bin/app/src/prop/mod.rs

@@ -27,6 +27,7 @@ use crate::{
     expr::SExprCode,
     pubsub::{Publisher, PublisherPtr, Subscription},
     scene::{SceneNodeId, SceneNodeWeak},
+    ui::VectorShape,
 };
 
 mod guard;
@@ -34,7 +35,7 @@ pub use guard::{BatchGuardPtr, PropertyAtomicGuard};
 mod wrap;
 pub use wrap::{
     PropertyBool, PropertyColor, PropertyDimension, PropertyEnum, PropertyFloat32, PropertyRect,
-    PropertyStr, PropertyUint32,
+    PropertyShape, PropertyStr, PropertyUint32,
 };
 
 #[derive(Debug, Copy, Clone, PartialEq, SerialEncodable, SerialDecodable)]
@@ -48,6 +49,7 @@ pub enum PropertyType {
     Enum = 5,
     SceneNodeId = 7,
     SExpr = 8,
+    VectorShape = 9,
 }
 
 impl PropertyType {
@@ -61,6 +63,7 @@ impl PropertyType {
             Self::Enum => PropertyValue::Enum(String::new()),
             Self::SceneNodeId => PropertyValue::SceneNodeId(0),
             Self::SExpr => PropertyValue::SExpr(Arc::new(vec![])),
+            Self::VectorShape => PropertyValue::VectorShape(Arc::new(VectorShape::new())),
         }
     }
 }
@@ -96,6 +99,7 @@ pub enum PropertyValue {
     Enum(String),
     SceneNodeId(SceneNodeId),
     SExpr(Arc<SExprCode>),
+    VectorShape(Arc<VectorShape>),
 }
 
 impl PropertyValue {
@@ -110,6 +114,7 @@ impl PropertyValue {
             Self::Enum(_) => PropertyType::Enum,
             Self::SceneNodeId(_) => PropertyType::SceneNodeId,
             Self::SExpr(_) => PropertyType::SExpr,
+            Self::VectorShape(_) => PropertyType::VectorShape,
         }
     }
 
@@ -176,6 +181,13 @@ impl PropertyValue {
             _ => Err(Error::PropertyWrongType),
         }
     }
+
+    pub fn as_shape(&self) -> Result<Arc<VectorShape>> {
+        match self {
+            Self::VectorShape(v) => Ok(v.clone()),
+            _ => Err(Error::PropertyWrongType),
+        }
+    }
 }
 
 impl Encodable for PropertyValue {
@@ -192,6 +204,7 @@ impl Encodable for PropertyValue {
             Self::Enum(v) => v.encode(s),
             Self::SceneNodeId(v) => v.encode(s),
             Self::SExpr(v) => v.encode(s),
+            Self::VectorShape(v) => v.encode(s),
         }
     }
 }
@@ -547,6 +560,18 @@ impl Property {
         Ok(())
     }
 
+    pub fn set_shape(
+        self: &Arc<Self>,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        i: usize,
+        val: VectorShape,
+    ) -> Result<()> {
+        self.set_raw_value(i, PropertyValue::VectorShape(Arc::new(val)))?;
+        atom.add(self.clone(), role, ModifyAction::Set(i));
+        Ok(())
+    }
+
     fn set_value_vec<T, F>(
         self: &Arc<Self>,
         atom: &mut PropertyAtomicGuard,
@@ -1113,6 +1138,10 @@ impl Property {
         self.get_raw_value(i)?.as_sexpr()
     }
 
+    pub fn get_shape(&self, i: usize) -> Result<Arc<VectorShape>> {
+        self.get_value(i)?.as_shape()
+    }
+
     pub fn get_cached(&self, i: usize) -> Result<PropertyValue> {
         let cache = &self.cache.lock().unwrap();
         if self.is_bounded() {
@@ -1235,7 +1264,30 @@ impl std::fmt::Debug for Property {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::expr::Op;
+    use crate::{expr::Op, ui::VectorShape as Shape};
+
+    #[test]
+    fn test_shape() {
+        let mut shape = Shape::new();
+        shape.add_filled_box(
+            vec![Op::ConstFloat32(0.)],
+            vec![Op::ConstFloat32(0.)],
+            vec![Op::ConstFloat32(10.)],
+            vec![Op::ConstFloat32(10.)],
+            [0., 0., 0., 1.],
+        );
+
+        let prop =
+            Arc::new(Property::new("shape", PropertyType::VectorShape, PropertySubType::Null));
+        let atom = &mut PropertyAtomicGuard::none();
+        // Default is an empty shape
+        assert_eq!(prop.get_shape(0).unwrap().verts.len(), 0);
+        prop.set_shape(atom, Role::App, 0, shape).unwrap();
+        assert_eq!(prop.get_shape(0).unwrap().verts.len(), 4);
+        assert_eq!(prop.get_shape(0).unwrap().indices.len(), 6);
+        // Wrong index
+        assert!(prop.set_shape(atom, Role::App, 4, Shape::new()).is_err());
+    }
 
     #[test]
     fn test_getset() {

+ 34 - 0
bin/app/src/prop/wrap.rs

@@ -21,10 +21,44 @@ use crate::{
     expr::{SExprMachine, SExprVal},
     gfx::{Dimension, Rectangle},
     scene::SceneNode as SceneNode3,
+    ui::VectorShape,
 };
+use std::sync::Arc;
 
 use super::{PropertyAtomicGuard, PropertyPtr, PropertyType, Role};
 
+#[derive(Clone)]
+pub struct PropertyShape {
+    prop: PropertyPtr,
+    role: Role,
+    idx: usize,
+}
+
+impl PropertyShape {
+    pub fn wrap(node: &SceneNode3, role: Role, prop_name: &str, idx: usize) -> Result<Self> {
+        let prop = node.get_property(prop_name).ok_or(Error::PropertyNotFound)?;
+
+        // Test if it works
+        let _ = prop.get_shape(idx)?;
+
+        Ok(Self { prop, role, idx })
+    }
+
+    pub fn get(&self) -> Arc<VectorShape> {
+        self.prop.get_shape(self.idx).unwrap()
+    }
+
+    #[allow(dead_code)]
+    pub fn set(&self, atom: &mut PropertyAtomicGuard, val: VectorShape) {
+        self.prop().set_shape(atom, self.role, self.idx, val).unwrap()
+    }
+
+    #[inline]
+    pub fn prop(&self) -> PropertyPtr {
+        self.prop.clone()
+    }
+}
+
 #[derive(Clone)]
 pub struct PropertyBool {
     prop: PropertyPtr,

+ 10 - 0
bin/app/src/scene.rs

@@ -36,6 +36,7 @@ use crate::{
     pubsub::{Publisher, PublisherPtr, Subscription},
     setting::SettingPtr,
     ui,
+    ui::VectorShape,
 };
 
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "scene", $($arg)*); } }
@@ -330,6 +331,15 @@ impl SceneNode {
     ) -> Result<()> {
         self.get_property(name).ok_or(Error::PropertyNotFound)?.set_node_id(atom, role, 0, val)
     }
+    pub fn set_property_shape(
+        &self,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        name: &str,
+        val: VectorShape,
+    ) -> Result<()> {
+        self.get_property(name).ok_or(Error::PropertyNotFound)?.set_shape(atom, role, 0, val)
+    }
 
     pub fn set_property_f32_vec(
         &self,

+ 6 - 2
bin/app/src/setting.rs

@@ -187,7 +187,9 @@ impl Setting {
             PropertyType::Str => decode_ty!(String, Str),
             PropertyType::Enum => decode_ty!(String, Enum),
             PropertyType::SceneNodeId => decode_ty!(u32, SceneNodeId),
-            PropertyType::Null | PropertyType::SExpr => return Err(Error::PropertyWrongType),
+            PropertyType::Null | PropertyType::SExpr | PropertyType::VectorShape => {
+                return Err(Error::PropertyWrongType)
+            }
         };
 
         Ok(val)
@@ -208,7 +210,9 @@ impl Setting {
             PropertyValue::Enum(v) => prop.set_enum(atom, Role::User, i, v),
             PropertyValue::SceneNodeId(v) => prop.set_node_id(atom, Role::User, i, v),
             PropertyValue::Null => prop.set_null(atom, Role::User, i),
-            PropertyValue::Unset | PropertyValue::SExpr(_) => Err(Error::PropertyWrongType),
+            PropertyValue::Unset | PropertyValue::SExpr(_) | PropertyValue::VectorShape(_) => {
+                Err(Error::PropertyWrongType)
+            }
         }
     }
 }

+ 21 - 14
bin/app/src/ui/vector_art/mod.rs

@@ -27,7 +27,8 @@ use crate::{
         gfxtag, DrawCall, DrawInstruction, DrawMesh, EpochCache, Rectangle, RenderApi, Renderer,
     },
     prop::{
-        PropertyAtomicGuard, PropertyBool, PropertyFloat32, PropertyRect, PropertyUint32, Role,
+        PropertyAtomicGuard, PropertyBool, PropertyFloat32, PropertyRect, PropertyShape,
+        PropertyUint32, Role,
     },
     scene::{Pimpl, SceneNodeWeak},
     ExecutorPtr,
@@ -36,7 +37,6 @@ use crate::{
 use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
 
 pub mod shape;
-use shape::VectorShape;
 
 pub type VectorArtPtr = Arc<VectorArt>;
 
@@ -46,7 +46,7 @@ pub struct VectorArt {
     redraw: RedrawTrigger,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
 
-    shape: VectorShape,
+    shape: PropertyShape,
     dc_key: u64,
 
     is_visible: PropertyBool,
@@ -57,20 +57,16 @@ pub struct VectorArt {
 
     /// Cached draw instructions. Empty means the output is stale and must
     /// be recomputed by the draw pass. Entries from a dead UI epoch are
-    /// evicted automatically. Shape is static, so only visibility, rect,
-    /// scale and z_index changes invalidate it.
+    /// evicted automatically. Visibility, rect, scale, z_index and shape
+    /// changes invalidate it.
     draw_cache: EpochCache<Vec<DrawInstruction>>,
 }
 
 impl VectorArt {
-    pub async fn new(
-        node: SceneNodeWeak,
-        shape: VectorShape,
-        renderer: Renderer,
-        redraw: RedrawTrigger,
-    ) -> Pimpl {
+    pub async fn new(node: SceneNodeWeak, renderer: Renderer, redraw: RedrawTrigger) -> Pimpl {
         let node_ref = &node.upgrade().unwrap();
         let is_visible = PropertyBool::wrap(node_ref, Role::Internal, "is_visible", 0).unwrap();
+        let shape = PropertyShape::wrap(node_ref, Role::Internal, "shape", 0).unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let scale = PropertyFloat32::wrap(node_ref, Role::Internal, "scale", 0).unwrap();
         let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
@@ -107,9 +103,16 @@ impl VectorArt {
 
         let rect = self.rect.get();
         let scale = self.scale.get();
-        let mut verts = self.shape.eval(rect.w, rect.h).expect("bad shape");
-        let indices = self.shape.indices.clone();
-        let num_elements = self.shape.indices.len() as i32;
+        let shape = self.shape.get();
+        let mut verts = match shape.eval(rect.w, rect.h) {
+            Ok(verts) => verts,
+            Err(e) => {
+                warn!(target: "ui::vector_art", "Shape eval failure: {e}");
+                return vec![]
+            }
+        };
+        let indices = shape.indices.clone();
+        let num_elements = shape.indices.len() as i32;
 
         // Apply scaling
         for v in &mut verts {
@@ -176,6 +179,10 @@ impl UIObject for VectorArt {
             self_.draw_cache.clear();
             self_.redraw.trigger();
         });
+        on_modify.when_change_external(self.shape.prop(), |self_, _| async move {
+            self_.draw_cache.clear();
+            self_.redraw.trigger();
+        });
         on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
             self_.draw_cache.clear();
             self_.redraw.trigger();

+ 56 - 2
bin/app/src/ui/vector_art/shape.rs

@@ -22,8 +22,9 @@ use crate::{
     gfx::{Point, Vertex},
     mesh::Color,
 };
+use darkfi_serial::{Encodable, VarInt};
 
-#[derive(Debug)]
+#[derive(Debug, PartialEq)]
 pub struct ShapeVertex {
     x: SExprCode,
     y: SExprCode,
@@ -73,7 +74,7 @@ fn sexpr_mul(mut x: SExprCode, op: Op) -> Option<Op> {
     Some(Op::Mul((Box::new(eqn), Box::new(op))))
 }
 
-#[derive(Debug)]
+#[derive(Debug, PartialEq)]
 pub struct VectorShape {
     pub verts: Vec<ShapeVertex>,
     pub indices: Vec<u16>,
@@ -350,3 +351,56 @@ impl VectorShape {
         }
     }
 }
+
+impl Encodable for ShapeVertex {
+    fn encode<S: std::io::Write>(&self, s: &mut S) -> std::result::Result<usize, std::io::Error> {
+        let mut len = 0;
+        len += self.x.encode(s)?;
+        len += self.y.encode(s)?;
+        for c in self.color {
+            len += c.encode(s)?;
+        }
+        Ok(len)
+    }
+}
+
+impl Encodable for VectorShape {
+    fn encode<S: std::io::Write>(&self, s: &mut S) -> std::result::Result<usize, std::io::Error> {
+        let mut len = 0;
+        len += VarInt(self.verts.len() as u64).encode(s)?;
+        for vert in &self.verts {
+            len += vert.encode(s)?;
+        }
+        len += VarInt(self.indices.len() as u64).encode(s)?;
+        for index in &self.indices {
+            len += index.encode(s)?;
+        }
+        Ok(len)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use darkfi_serial::serialize;
+
+    #[test]
+    fn encode_shape() {
+        let mut shape = VectorShape::new();
+        shape.add_filled_box(
+            vec![Op::ConstFloat32(0.)],
+            vec![Op::ConstFloat32(0.)],
+            vec![Op::ConstFloat32(10.)],
+            vec![Op::ConstFloat32(10.)],
+            [0., 0., 0., 1.],
+        );
+
+        let data = serialize(&shape);
+        // vert count, then first vert: x code len 1, ConstFloat32 op tag 7
+        assert_eq!(data[0], 4);
+        assert_eq!(data[1], 1);
+        assert_eq!(data[2], 7);
+        // 4 verts x (1+1+4) x 2 coords + 16 color bytes, then 6 u16 indices
+        assert_eq!(data.len(), 1 + 4 * (6 + 6 + 16) + 1 + 6 * 2);
+    }
+}