Преглед изворни кода

wallet: add emoji picker widget (unimpl) and connect hide/show button

darkfi пре 1 година
родитељ
комит
0f1eac38ba

+ 38 - 0
bin/darkwallet/data/close.obj

@@ -0,0 +1,38 @@
+# Blender 4.3.1
+# www.blender.org
+mtllib close.mtl
+o close
+v 0.000000 0.000000 0.000000
+v 0.000000 0.000000 -0.194555
+v 0.194555 0.000000 0.000000
+v 0.550000 0.000000 -0.744555
+v 0.744555 0.000000 -0.550000
+v 0.000000 0.000000 0.000000
+v -0.194555 0.000000 0.000000
+v -0.550000 0.000000 -0.744555
+v -0.744555 0.000000 -0.550000
+v 0.000000 0.000000 0.000000
+v 0.000000 0.000000 0.194555
+v 0.550000 0.000000 0.744555
+v 0.744555 0.000000 0.550000
+v 0.000000 0.000000 0.000000
+v -0.550000 0.000000 0.744555
+v -0.744555 0.000000 0.550000
+vn -0.0000 1.0000 -0.0000
+vn -0.0000 -1.0000 -0.0000
+vt 1.000000 0.000000
+vt 1.000000 1.000000
+vt 0.000000 1.000000
+s 0
+f 1/1/1 3/2/1 2/3/1
+f 3/2/1 4/3/1 2/3/1
+f 6/1/2 7/2/2 2/3/2
+f 7/2/2 8/3/2 2/3/2
+f 10/1/2 3/2/2 11/3/2
+f 3/2/2 12/3/2 11/3/2
+f 14/1/1 7/2/1 11/3/1
+f 7/2/1 15/3/1 11/3/1
+f 3/2/1 5/2/1 4/3/1
+f 7/2/2 9/2/2 8/3/2
+f 3/2/2 13/2/2 12/3/2
+f 7/2/1 16/2/1 15/3/1

+ 19 - 0
bin/darkwallet/src/app/node.rs

@@ -47,6 +47,10 @@ pub fn create_vector_art(name: &str) -> SceneNode {
     debug!(target: "app", "create_vector_art({name})");
     let mut node = SceneNode::new(name, SceneNodeType::VectorArt);
 
+    let mut prop = Property::new("is_visible", PropertyType::Bool, PropertySubType::Null);
+    prop.set_defaults_bool(vec![true]).unwrap();
+    node.add_property(prop).unwrap();
+
     let mut prop = Property::new("rect", PropertyType::Float32, PropertySubType::Pixel);
     prop.set_array_len(4);
     prop.allow_exprs();
@@ -474,3 +478,18 @@ pub fn create_chatview(name: &str) -> SceneNode {
 
     node
 }
+
+pub fn create_emoji_picker(name: &str) -> SceneNode {
+    debug!(target: "app", "create_emoji_picker({name})");
+    let mut node = SceneNode::new(name, SceneNodeType::Image);
+
+    let mut prop = Property::new("rect", PropertyType::Float32, PropertySubType::Pixel);
+    prop.set_array_len(4);
+    prop.allow_exprs();
+    node.add_property(prop).unwrap();
+
+    let prop = Property::new("z_index", PropertyType::Uint32, PropertySubType::Null);
+    node.add_property(prop).unwrap();
+
+    node
+}

+ 89 - 5
bin/darkwallet/src/app/schema/chat.rs

@@ -16,13 +16,14 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use darkfi::system::msleep;
 use sled_overlay::sled;
 
 use crate::{
     app::{
         node::{
-            create_button, create_chatedit, create_chatview, create_editbox, create_image,
-            create_layer, create_text, create_vector_art,
+            create_button, create_chatedit, create_chatview, create_editbox, create_emoji_picker,
+            create_image, create_layer, create_text, create_vector_art,
         },
         populate_tree, App,
     },
@@ -37,8 +38,8 @@ use crate::{
     shape,
     text::TextShaperPtr,
     ui::{
-        Button, ChatEdit, ChatView, EditBox, Image, Layer, ShapeVertex, Text, VectorArt,
-        VectorShape, Window,
+        Button, ChatEdit, ChatView, EditBox, EmojiPicker, Image, Layer, ShapeVertex, Text,
+        VectorArt, VectorShape, Window,
     },
     ExecutorPtr,
 };
@@ -69,6 +70,7 @@ mod android_ui_consts {
     pub const EMOJI_SCALE: f32 = 40.;
     pub const EMOJI_NEG_Y: f32 = 85.;
     pub const EMOJIBTN_BOX: [f32; 4] = [20., 118., 80., 75.];
+    pub const EMOJI_CLOSE_SCALE: f32 = 20.;
     pub const SENDARROW_NEG_X: f32 = 50.;
     pub const SENDARROW_NEG_Y: f32 = 80.;
     pub const SENDBTN_BOX: [f32; 4] = [86., 120., 80., 70.];
@@ -128,6 +130,7 @@ mod ui_consts {
     pub const EMOJI_SCALE: f32 = 20.;
     pub const EMOJI_NEG_Y: f32 = 34.;
     pub const EMOJIBTN_BOX: [f32; 4] = [16., 50., 44., 36.];
+    pub const EMOJI_CLOSE_SCALE: f32 = 10.;
     pub const SENDARROW_NEG_X: f32 = 50.;
     pub const SENDARROW_NEG_Y: f32 = 32.;
     pub const SENDBTN_BOX: [f32; 4] = [72., 50., 45., 34.];
@@ -257,7 +260,6 @@ pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db)
     prop.set_f32(Role::App, 1, 0.).unwrap();
     prop.set_expr(Role::App, 2, expr::load_var("w")).unwrap();
     prop.set_f32(Role::App, 3, CHATEDIT_HEIGHT).unwrap();
-    node.set_property_u32(Role::App, "z_index", 2).unwrap();
     node.set_property_f32(Role::App, "baseline", CHANNEL_LABEL_BASELINE).unwrap();
     node.set_property_f32(Role::App, "font_size", FONTSIZE).unwrap();
     node.set_property_str(Role::App, "text", &("#".to_string() + channel)).unwrap();
@@ -283,6 +285,52 @@ pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db)
         .await;
     layer_node.clone().link(node);
 
+    // Create the emoji picker
+    let mut node = create_emoji_picker("emoji_picker");
+    let prop = Property::new("dynamic_h", PropertyType::Float32, PropertySubType::Pixel);
+    node.add_property(prop).unwrap();
+    let emoji_h_prop = node.get_property("dynamic_h").unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(Role::App, 0, 0.).unwrap();
+    let code = cc.compile("h - dynamic_h").unwrap();
+    prop.set_expr(Role::App, 1, code).unwrap();
+    prop.set_expr(Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_expr(Role::App, 3, expr::load_var("dynamic_h")).unwrap();
+    prop.add_depend(&emoji_h_prop, 0, "dynamic_h");
+    let emoji_h_prop = PropertyFloat32::wrap(&node, Role::App, "dynamic_h", 0).unwrap();
+    let emoji_rect_prop = prop;
+    //node.set_property_f32(Role::App, "baseline", CHANNEL_LABEL_BASELINE).unwrap();
+    //node.set_property_f32(Role::App, "font_size", FONTSIZE).unwrap();
+    node.set_property_u32(Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| {
+            EmojiPicker::new(
+                me,
+                window_scale.clone(),
+                app.render_api.clone(),
+                app.text_shaper.clone(),
+                app.ex.clone(),
+            )
+        })
+        .await;
+    layer_node.clone().link(node);
+
+    // Main content view
+    let chat_layer_node = layer_node;
+    let layer_node = create_layer("content");
+    let prop = layer_node.get_property("rect").unwrap();
+    prop.set_f32(Role::App, 0, 0.).unwrap();
+    prop.set_f32(Role::App, 1, 0.).unwrap();
+    prop.set_expr(Role::App, 2, expr::load_var("w")).unwrap();
+    let code = cc.compile("h - emoji_h").unwrap();
+    prop.set_expr(Role::App, 3, code).unwrap();
+    prop.add_depend(&emoji_rect_prop, 3, "emoji_h");
+    layer_node.set_property_bool(Role::App, "is_visible", true).unwrap();
+    layer_node.set_property_u32(Role::App, "z_index", 1).unwrap();
+    let layer_node =
+        layer_node.setup(|me| Layer::new(me, app.render_api.clone(), app.ex.clone())).await;
+    chat_layer_node.link(layer_node.clone());
+
     // ChatView
     let node = create_chatview("chatty");
     let prop = node.get_property("rect").unwrap();
@@ -468,6 +516,7 @@ pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db)
 
     // Create the emoji button
     let node = create_vector_art("emoji_btn_bg");
+    let emoji_btn_is_visible = PropertyBool::wrap(&node, Role::App, "is_visible", 0).unwrap();
     let prop = node.get_property("rect").unwrap();
     prop.set_f32(Role::App, 0, EMOJI_BTN_X).unwrap();
     let code = cc.compile("h - EMOJI_NEG_Y").unwrap();
@@ -480,6 +529,22 @@ pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db)
         node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
     layer_node.clone().link(node);
 
+    // Create the emoji button
+    let node = create_vector_art("emoji_close_btn_bg");
+    node.set_property_bool(Role::App, "is_visible", false).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    let emoji_close_is_visible = PropertyBool::wrap(&node, Role::App, "is_visible", 0).unwrap();
+    prop.set_f32(Role::App, 0, EMOJI_BTN_X).unwrap();
+    let code = cc.compile("h - EMOJI_NEG_Y").unwrap();
+    prop.set_expr(Role::App, 1, code).unwrap();
+    prop.set_f32(Role::App, 2, 500.).unwrap();
+    prop.set_f32(Role::App, 3, 500.).unwrap();
+    node.set_property_u32(Role::App, "z_index", 3).unwrap();
+    let shape = shape::create_close_icon().scaled(EMOJI_CLOSE_SCALE);
+    let node =
+        node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
+    layer_node.clone().link(node);
+
     // Text edit
     let node = create_chatedit("editz");
     node.set_property_bool(Role::App, "is_active", true).unwrap();
@@ -635,6 +700,25 @@ pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db)
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             info!(target: "app::chat", "clicked emoji");
+            if emoji_btn_is_visible.get() {
+                assert!(!emoji_close_is_visible.get());
+                assert!(emoji_h_prop.get() < 0.001);
+                emoji_btn_is_visible.set(false);
+                emoji_close_is_visible.set(true);
+                for i in 1..=20 {
+                    emoji_h_prop.set((20 * i) as f32);
+                    msleep(10).await;
+                }
+            } else {
+                assert!(emoji_close_is_visible.get());
+                assert!(emoji_h_prop.get() > 0.);
+                emoji_btn_is_visible.set(true);
+                emoji_close_is_visible.set(false);
+                for i in 1..=20 {
+                    emoji_h_prop.set((400 - 20 * i) as f32);
+                    msleep(10).await;
+                }
+            }
         }
     });
     app.tasks.lock().unwrap().push(listen_click);

+ 5 - 0
bin/darkwallet/src/prop/mod.rs

@@ -319,6 +319,11 @@ impl Property {
         }
         Ok(())
     }
+    pub fn set_defaults_bool(&mut self, defaults: Vec<bool>) -> Result<()> {
+        self.check_defaults_len(defaults.len())?;
+        self.defaults = defaults.into_iter().map(|v| PropertyValue::Bool(v)).collect();
+        Ok(())
+    }
     pub fn set_defaults_u32(&mut self, defaults: Vec<u32>) -> Result<()> {
         self.check_defaults_len(defaults.len())?;
         self.defaults = defaults.into_iter().map(|v| PropertyValue::Uint32(v)).collect();

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

@@ -476,4 +476,5 @@ pub enum Pimpl {
     ChatView(ui::ChatViewPtr),
     Image(ui::ImagePtr),
     Button(ui::ButtonPtr),
+    EmojiPicker(ui::EmojiPickerPtr),
 }

+ 27 - 0
bin/darkwallet/src/shape/close.rs

@@ -0,0 +1,27 @@
+use crate::ui::{ShapeVertex, VectorShape};
+pub fn create_close_icon() -> VectorShape {
+    VectorShape {
+        verts: vec![
+            ShapeVertex::from_xy(0.0, 0.0, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(0.0, -0.194555, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(0.194555, 0.0, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(0.55, -0.744555, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(0.744555, -0.55, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(0.0, 0.0, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(-0.194555, 0.0, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(-0.55, -0.744555, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(-0.744555, -0.55, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(0.0, 0.0, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(0.0, 0.194555, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(0.55, 0.744555, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(0.744555, 0.55, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(0.0, 0.0, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(-0.55, 0.744555, [0., 1., 1., 1.]),
+            ShapeVertex::from_xy(-0.744555, 0.55, [0., 1., 1., 1.]),
+        ],
+        indices: vec![
+            0, 2, 1, 2, 3, 1, 5, 6, 1, 6, 7, 1, 9, 2, 10, 2, 11, 10, 13, 6, 10, 6, 14, 10, 2, 4, 3,
+            6, 8, 7, 2, 12, 11, 6, 15, 14,
+        ],
+    }
+}

+ 3 - 0
bin/darkwallet/src/shape/mod.rs

@@ -21,6 +21,9 @@ use crate::ui::{ShapeVertex, VectorShape};
 mod back_arrow;
 pub use back_arrow::create_back_arrow;
 
+mod close;
+pub use close::create_close_icon;
+
 mod send_arrow;
 pub use send_arrow::create_send_arrow;
 

+ 188 - 0
bin/darkwallet/src/ui/emoji_picker.rs

@@ -0,0 +1,188 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use async_trait::async_trait;
+use image::ImageReader;
+use rand::{rngs::OsRng, Rng};
+use std::{
+    io::Cursor,
+    sync::{Arc, Mutex as SyncMutex, OnceLock, Weak},
+};
+
+use crate::{
+    gfx::{
+        GfxDrawCall, GfxDrawInstruction, GfxDrawMesh, GfxTextureId, ManagedTexturePtr, Rectangle,
+        RenderApi,
+    },
+    mesh::{MeshBuilder, MeshInfo, COLOR_WHITE},
+    prop::{PropertyFloat32, PropertyPtr, PropertyRect, PropertyStr, PropertyUint32, Role},
+    scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
+    text::{self, GlyphPositionIter, TextShaper, TextShaperPtr},
+    ExecutorPtr,
+};
+
+use super::{DrawUpdate, OnModify, UIObject};
+
+macro_rules! d {
+    ($($arg:tt)*) => {
+        debug!(target: "ui::emoji_picker", $($arg)*);
+    }
+}
+
+pub type EmojiPickerPtr = Arc<EmojiPicker>;
+
+pub struct EmojiPicker {
+    node: SceneNodeWeak,
+    render_api: RenderApi,
+    text_shaper: TextShaperPtr,
+    tasks: OnceLock<Vec<smol::Task<()>>>,
+
+    dc_key: u64,
+
+    rect: PropertyRect,
+    z_index: PropertyUint32,
+
+    window_scale: PropertyFloat32,
+    parent_rect: SyncMutex<Option<Rectangle>>,
+}
+
+impl EmojiPicker {
+    pub async fn new(
+        node: SceneNodeWeak,
+        window_scale: PropertyFloat32,
+        render_api: RenderApi,
+        text_shaper: TextShaperPtr,
+        ex: ExecutorPtr,
+    ) -> Pimpl {
+        d!("EmojiPicker::new()");
+
+        let node_ref = &node.upgrade().unwrap();
+        let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
+        let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
+
+        let node_name = node_ref.name.clone();
+        let node_id = node_ref.id;
+
+        let self_ = Arc::new(Self {
+            node,
+            render_api,
+            text_shaper,
+            tasks: OnceLock::new(),
+
+            dc_key: OsRng.gen(),
+
+            rect,
+            z_index,
+
+            window_scale,
+            parent_rect: SyncMutex::new(None),
+        });
+
+        Pimpl::EmojiPicker(self_)
+    }
+
+    async fn redraw(self: Arc<Self>) {
+        let Some(parent_rect) = self.parent_rect.lock().unwrap().clone() else { return };
+
+        let Some(draw_update) = self.get_draw_calls(parent_rect).await else {
+            error!(target: "ui::image", "Emoji picker failed to draw");
+            return;
+        };
+        self.render_api.replace_draw_calls(draw_update.draw_calls);
+        debug!(target: "ui::image", "replace draw calls done");
+    }
+
+    /*
+    fn regen_mesh(&self) -> MeshInfo {
+        let rect = self.rect.get();
+        let uv = self.uv.get();
+        let mesh_rect = Rectangle::from([0., 0., rect.w, rect.h]);
+        let mut mesh = MeshBuilder::new();
+        mesh.draw_box(&mesh_rect, COLOR_WHITE, &uv);
+        mesh.alloc(&self.render_api)
+    }
+    */
+
+    async fn get_draw_calls(&self, parent_rect: Rectangle) -> Option<DrawUpdate> {
+        if let Err(e) = self.rect.eval(&parent_rect) {
+            warn!(target: "ui::emoji_picker", "Rect eval failed: {e}");
+            return None
+        }
+        let rect = self.rect.get();
+
+        /*
+        let mesh = self.regen_mesh();
+        let texture = self.texture.lock().unwrap().clone().expect("Node missing texture_id!");
+
+        let mesh = GfxDrawMesh {
+            vertex_buffer: mesh.vertex_buffer,
+            index_buffer: mesh.index_buffer,
+            texture: Some(texture),
+            num_elements: mesh.num_elements,
+        };
+        */
+
+        Some(DrawUpdate {
+            key: self.dc_key,
+            draw_calls: vec![(
+                self.dc_key,
+                GfxDrawCall {
+                    instrs: vec![
+                        GfxDrawInstruction::Move(rect.pos()),
+                        //GfxDrawInstruction::Draw(mesh),
+                    ],
+                    dcs: vec![],
+                    z_index: self.z_index.get(),
+                },
+            )],
+        })
+    }
+}
+
+#[async_trait]
+impl UIObject for EmojiPicker {
+    fn z_index(&self) -> u32 {
+        self.z_index.get()
+    }
+
+    async fn start(self: Arc<Self>, ex: ExecutorPtr) {
+        let me = Arc::downgrade(&self);
+
+        let node_ref = &self.node.upgrade().unwrap();
+        let node_name = node_ref.name.clone();
+        let node_id = node_ref.id;
+
+        let mut on_modify = OnModify::new(ex, node_name, node_id, me.clone());
+        on_modify.when_change(self.rect.prop(), Self::redraw);
+        on_modify.when_change(self.z_index.prop(), Self::redraw);
+
+        self.tasks.set(on_modify.tasks);
+    }
+
+    async fn draw(&self, parent_rect: Rectangle) -> Option<DrawUpdate> {
+        debug!(target: "ui::image", "Image::draw()");
+        *self.parent_rect.lock().unwrap() = Some(parent_rect);
+        self.get_draw_calls(parent_rect).await
+    }
+}
+
+impl Drop for EmojiPicker {
+    fn drop(&mut self) {
+        self.render_api.replace_draw_calls(vec![(self.dc_key, Default::default())]);
+    }
+}

+ 4 - 0
bin/darkwallet/src/ui/mod.rs

@@ -38,6 +38,8 @@ mod chatedit;
 pub use chatedit::{ChatEdit, ChatEditPtr};
 mod editbox;
 pub use editbox::{EditBox, EditBoxPtr};
+mod emoji_picker;
+pub use emoji_picker::{EmojiPicker, EmojiPickerPtr};
 mod image;
 pub use image::{Image, ImagePtr};
 mod vector_art;
@@ -185,6 +187,7 @@ pub fn get_ui_object_ptr(node: &SceneNode3) -> Arc<dyn UIObject + Send> {
         Pimpl::ChatView(obj) => obj.clone(),
         Pimpl::Image(obj) => obj.clone(),
         Pimpl::Button(obj) => obj.clone(),
+        Pimpl::EmojiPicker(obj) => obj.clone(),
         _ => panic!("unhandled type for get_ui_object"),
     }
 }
@@ -198,6 +201,7 @@ pub fn get_ui_object3<'a>(node: &'a SceneNode3) -> &'a dyn UIObject {
         Pimpl::ChatView(obj) => obj.as_ref(),
         Pimpl::Image(obj) => obj.as_ref(),
         Pimpl::Button(obj) => obj.as_ref(),
+        Pimpl::EmojiPicker(obj) => obj.as_ref(),
         _ => panic!("unhandled type for get_ui_object"),
     }
 }

+ 22 - 14
bin/darkwallet/src/ui/vector_art/mod.rs

@@ -27,7 +27,7 @@ use crate::{
         GfxBufferId, GfxDrawCall, GfxDrawInstruction, GfxDrawMesh, Rectangle, RenderApi, Vertex,
     },
     mesh::Color,
-    prop::{PropertyFloat32, PropertyPtr, PropertyRect, PropertyUint32, Role},
+    prop::{PropertyBool, PropertyFloat32, PropertyPtr, PropertyRect, PropertyUint32, Role},
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
     util::enumerate,
     ExecutorPtr,
@@ -48,6 +48,7 @@ pub struct VectorArt {
     shape: VectorShape,
     dc_key: u64,
 
+    is_visible: PropertyBool,
     rect: PropertyRect,
     z_index: PropertyUint32,
 
@@ -64,6 +65,7 @@ impl VectorArt {
         debug!(target: "ui::vector_art", "VectorArt::new()");
 
         let node_ref = &node.upgrade().unwrap();
+        let is_visible = PropertyBool::wrap(node_ref, Role::Internal, "is_visible", 0).unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
 
@@ -78,6 +80,7 @@ impl VectorArt {
             shape,
             dc_key: OsRng.gen(),
 
+            is_visible,
             rect,
             z_index,
 
@@ -98,12 +101,11 @@ impl VectorArt {
         //debug!(target: "ui::vector_art", "replace draw calls done");
     }
 
-    async fn get_draw_calls(&self, parent_rect: Rectangle) -> Option<DrawUpdate> {
-        //debug!(target: "ui::vector_art", "VectorArt::draw_cached()");
-        if let Err(e) = self.rect.eval(&parent_rect) {
-            warn!(target: "ui::vector_art", "Rect eval failure: {e}");
-            return None
+    fn get_draw_instrs(&self) -> Vec<GfxDrawInstruction> {
+        if !self.is_visible.get() {
+            return vec![]
         }
+
         let rect = self.rect.get();
         let verts = self.shape.eval(rect.w, rect.h).expect("bad shape");
 
@@ -117,18 +119,23 @@ impl VectorArt {
             num_elements: self.shape.indices.len() as i32,
         };
 
+        vec![GfxDrawInstruction::Move(rect.pos()), GfxDrawInstruction::Draw(mesh)]
+    }
+
+    async fn get_draw_calls(&self, parent_rect: Rectangle) -> Option<DrawUpdate> {
+        //debug!(target: "ui::vector_art", "VectorArt::draw_cached()");
+        if let Err(e) = self.rect.eval(&parent_rect) {
+            warn!(target: "ui::vector_art", "Rect eval failure: {e}");
+            return None
+        }
+
+        let instrs = self.get_draw_instrs();
+
         Some(DrawUpdate {
             key: self.dc_key,
             draw_calls: vec![(
                 self.dc_key,
-                GfxDrawCall {
-                    instrs: vec![
-                        GfxDrawInstruction::Move(rect.pos()),
-                        GfxDrawInstruction::Draw(mesh),
-                    ],
-                    dcs: vec![],
-                    z_index: self.z_index.get(),
-                },
+                GfxDrawCall { instrs, dcs: vec![], z_index: self.z_index.get() },
             )],
         })
     }
@@ -148,6 +155,7 @@ impl UIObject for VectorArt {
         let node_id = node_ref.id;
 
         let mut on_modify = OnModify::new(ex, node_name, node_id, me.clone());
+        on_modify.when_change(self.is_visible.prop(), Self::redraw);
         on_modify.when_change(self.rect.prop(), Self::redraw);
         on_modify.when_change(self.z_index.prop(), Self::redraw);