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

app: migrate all remaining widgets to the new draw paradigm. remove RendererSync and batch IDs from gfx.

darkfi 5 дней назад
Родитель
Сommit
12691bdca1
41 измененных файлов с 1644 добавлено и 1212 удалено
  1. 31 21
      bin/app/src/app/schema/chat.rs
  2. 100 26
      bin/app/src/app/schema/menu/channel.rs
  3. 102 27
      bin/app/src/app/schema/menu/contact.rs
  4. 25 5
      bin/app/src/app/schema/menu/edit_buttons.rs
  5. 43 9
      bin/app/src/app/schema/menu/mod.rs
  6. 27 10
      bin/app/src/app/schema/mod.rs
  7. 19 19
      bin/app/src/app/schema/settings.rs
  8. 16 8
      bin/app/src/app/schema/test.rs
  9. 4 1
      bin/app/src/app/schema/test_edit.rs
  10. 3 1
      bin/app/src/app/schema/test_scroll_layer.rs
  11. 66 13
      bin/app/src/app/schema/wallet/main.rs
  12. 12 4
      bin/app/src/app/schema/wallet/netstatus.rs
  13. 19 5
      bin/app/src/app/schema/wallet/receive.rs
  14. 8 4
      bin/app/src/app/schema/wallet/send_step1.rs
  15. 41 8
      bin/app/src/app/schema/wallet/send_step2.rs
  16. 71 9
      bin/app/src/app/schema/wallet/send_step3.rs
  17. 87 11
      bin/app/src/app/schema/wallet/send_step4.rs
  18. 45 6
      bin/app/src/app/schema/wallet/tx_status.rs
  19. 90 18
      bin/app/src/app/schema/wallet/util.rs
  20. 25 1
      bin/app/src/gfx/anim.rs
  21. 41 102
      bin/app/src/gfx/api.rs
  22. 1 1
      bin/app/src/gfx/linalg.rs
  23. 32 124
      bin/app/src/gfx/mod.rs
  24. 9 25
      bin/app/src/gfx/prune.rs
  25. 1 1
      bin/app/src/prop/mod.rs
  26. 13 28
      bin/app/src/ui/button.rs
  27. 44 103
      bin/app/src/ui/chatview/mod.rs
  28. 4 2
      bin/app/src/ui/chatview/page.rs
  29. 7 9
      bin/app/src/ui/edit/action.rs
  30. 153 217
      bin/app/src/ui/edit/mod.rs
  31. 64 53
      bin/app/src/ui/emoji_picker/mod.rs
  32. 48 45
      bin/app/src/ui/image.rs
  33. 4 10
      bin/app/src/ui/layer.rs
  34. 130 102
      bin/app/src/ui/menu/mod.rs
  35. 2 8
      bin/app/src/ui/mod.rs
  36. 4 10
      bin/app/src/ui/scroll_layer.rs
  37. 60 35
      bin/app/src/ui/text.rs
  38. 66 38
      bin/app/src/ui/tokentable/mod.rs
  39. 54 31
      bin/app/src/ui/vector_art/mod.rs
  40. 68 51
      bin/app/src/ui/vid/mod.rs
  41. 5 11
      bin/app/src/ui/win/mod.rs

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

@@ -242,7 +242,7 @@ pub async fn make(
         expr::load_var("h"),
         expr::load_var("h"),
         [[0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.8]],
         [[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())).await;
+    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create the toolbar bg
     // Create the toolbar bg
@@ -293,7 +293,7 @@ pub async fn make(
         0.2,
         0.2,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone())).await;
+    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create the send button
     // Create the send button
@@ -306,7 +306,8 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
 
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
-    let back_btn_bg_node = node.setup(|me| VectorArt::new(me, shape, renderer.clone())).await;
+    let back_btn_bg_node =
+        node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     layer_node.link(back_btn_bg_node.clone());
     layer_node.link(back_btn_bg_node.clone());
 
 
     // Create the back button
     // Create the back button
@@ -347,7 +348,7 @@ pub async fn make(
     });
     });
     layer_node.push_task(listen_click);
     layer_node.push_task(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, renderer.clone())).await;
+    let node = node.setup(|me| Button::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create shortcut to go back as well
     // Create shortcut to go back as well
@@ -399,7 +400,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
 
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(me, window_scale.clone(), renderer.clone(), i18n_fish.clone(), redraw.clone())
+        })
         .await;
         .await;
     layer_node.link(node);
     layer_node.link(node);
 
 
@@ -420,7 +423,8 @@ pub async fn make(
     //node.set_property_f32(atom, Role::App, "font_size", FONTSIZE).unwrap();
     //node.set_property_f32(atom, Role::App, "font_size", FONTSIZE).unwrap();
     node.set_property_f32(atom, Role::App, "emoji_size", EMOJI_PICKER_ICON_SIZE).unwrap();
     node.set_property_f32(atom, Role::App, "emoji_size", EMOJI_PICKER_ICON_SIZE).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let node = node.setup(|me| EmojiPicker::new(me, renderer.clone(), emoji_meshes)).await;
+    let node =
+        node.setup(|me| EmojiPicker::new(me, renderer.clone(), emoji_meshes, redraw.clone())).await;
     let emoji_picker_node = node.clone();
     let emoji_picker_node = node.clone();
     layer_node.link(node);
     layer_node.link(node);
 
 
@@ -460,7 +464,7 @@ pub async fn make(
     //    expr::load_var("h"),
     //    expr::load_var("h"),
     //    [0.41, 0.6, 0.65, 1.],
     //    [0.41, 0.6, 0.65, 1.],
     //);
     //);
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone())).await;
+    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Main content view
     // Main content view
@@ -599,6 +603,7 @@ pub async fn make(
                 chat_tree,
                 chat_tree,
                 window_scale.clone(),
                 window_scale.clone(),
                 renderer.clone(),
                 renderer.clone(),
+                redraw.clone(),
                 sg_root.clone(),
                 sg_root.clone(),
                 ex.clone(),
                 ex.clone(),
             )
             )
@@ -670,7 +675,7 @@ pub async fn make(
         bg_color,
         bg_color,
     );
     );
     */
     */
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone())).await;
+    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     select_layer.link(node);
     select_layer.link(node);
 
 
     // unselect_btn sits over the back button and calls the chatview's unselect.
     // unselect_btn sits over the back button and calls the chatview's unselect.
@@ -692,7 +697,7 @@ pub async fn make(
         });
         });
         select_layer.push_task(listen_click);
         select_layer.push_task(listen_click);
     }
     }
-    let node = node.setup(|me| Button::new(me, renderer.clone())).await;
+    let node = node.setup(|me| Button::new(me, renderer.clone(), redraw.clone())).await;
     select_layer.link(node);
     select_layer.link(node);
 
 
     // copy_btn sits over the reconnect button and calls the chatview's
     // copy_btn sits over the reconnect button and calls the chatview's
@@ -716,7 +721,7 @@ pub async fn make(
         });
         });
         select_layer.push_task(listen_click);
         select_layer.push_task(listen_click);
     }
     }
-    let node = node.setup(|me| Button::new(me, renderer.clone())).await;
+    let node = node.setup(|me| Button::new(me, renderer.clone(), redraw.clone())).await;
     select_layer.link(node);
     select_layer.link(node);
 
 
     // Show/hide the overlay from the chatview's select_changed signal.
     // Show/hide the overlay from the chatview's select_changed signal.
@@ -802,7 +807,7 @@ pub async fn make(
     //    expr::load_var("h"),
     //    expr::load_var("h"),
     //    [0.41, 0.6, 0.65, 1.],
     //    [0.41, 0.6, 0.65, 1.],
     //);
     //);
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone())).await;
+    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create the send button
     // Create the send button
@@ -816,7 +821,7 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 5).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 5).unwrap();
     let shape = shape::create_send_arrow().scaled(EMOJI_SCALE);
     let shape = shape::create_send_arrow().scaled(EMOJI_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone())).await;
+    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create the emoji button
     // Create the emoji button
@@ -834,7 +839,7 @@ pub async fn make(
         ColorScheme::PaperLight => [0., 0., 0., 1.],
         ColorScheme::PaperLight => [0., 0., 0., 1.],
     };
     };
     let shape = shape::create_emoji_selector(color).scaled(EMOJI_SCALE);
     let shape = shape::create_emoji_selector(color).scaled(EMOJI_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone())).await;
+    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create the emoji button
     // Create the emoji button
@@ -849,7 +854,7 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 5).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 5).unwrap();
     let shape = shape::create_close_icon().scaled(EMOJI_CLOSE_SCALE);
     let shape = shape::create_close_icon().scaled(EMOJI_CLOSE_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone())).await;
+    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Text edit
     // Text edit
@@ -971,6 +976,7 @@ pub async fn make(
                 me,
                 me,
                 window_scale.clone(),
                 window_scale.clone(),
                 renderer.clone(),
                 renderer.clone(),
+                redraw.clone(),
                 BaseEditType::MultiLine,
                 BaseEditType::MultiLine,
                 ex.clone(),
                 ex.clone(),
             )
             )
@@ -1023,7 +1029,7 @@ pub async fn make(
         ]
         ]
     );
     );
     let node =
     let node =
-        node.setup(|me| VectorArt::new(me, shape, renderer.clone())).await;
+        node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
     */
     */
 
 
@@ -1112,7 +1118,7 @@ pub async fn make(
     });
     });
     layer_node.push_task(listen_click);
     layer_node.push_task(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, renderer.clone())).await;
+    let node = node.setup(|me| Button::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create shortcut to send as well
     // Create shortcut to send as well
@@ -1216,7 +1222,7 @@ pub async fn make(
     });
     });
     layer_node.push_task(listen_click);
     layer_node.push_task(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, renderer.clone())).await;
+    let node = node.setup(|me| Button::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Commands help hint
     // Commands help hint
@@ -1264,7 +1270,7 @@ pub async fn make(
     });
     });
     layer_node.push_task(listen_click);
     layer_node.push_task(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, renderer.clone())).await;
+    let node = node.setup(|me| Button::new(me, renderer.clone(), redraw.clone())).await;
     cmd_layer_node.link(node);
     cmd_layer_node.link(node);
 
 
     // Create the actionbar bg
     // Create the actionbar bg
@@ -1307,7 +1313,7 @@ pub async fn make(
         [0.29, 0.51, 0.45, 1.],
         [0.29, 0.51, 0.45, 1.],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone())).await;
+    let node = node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     cmd_layer_node.link(node);
     cmd_layer_node.link(node);
 
 
     // Create some text
     // Create some text
@@ -1329,7 +1335,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
 
 
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(me, window_scale.clone(), renderer.clone(), i18n_fish.clone(), redraw.clone())
+        })
         .await;
         .await;
     cmd_layer_node.link(node);
     cmd_layer_node.link(node);
 
 
@@ -1352,7 +1360,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
 
 
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(me, window_scale.clone(), renderer.clone(), i18n_fish.clone(), redraw.clone())
+        })
         .await;
         .await;
     cmd_layer_node.link(node);
     cmd_layer_node.link(node);
 
 

+ 100 - 26
bin/app/src/app/schema/menu/channel.rs

@@ -227,7 +227,9 @@ pub async fn make(
         0.2,
         0.2,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(node);
     content.link(node);
 
 
     // Create back arrow
     // Create back arrow
@@ -240,7 +242,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
 
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(node);
     content.link(node);
 
 
     // Create back button
     // Create back button
@@ -284,7 +288,8 @@ pub async fn make(
     });
     });
     content.push_task(listen_click);
     content.push_task(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content.link(node);
     content.link(node);
 
 
     // Create shortcut to go back as well
     // Create shortcut to go back as well
@@ -339,7 +344,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
     let mut shape = VectorShape::new();
     let mut shape = VectorShape::new();
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(node);
     content.link(node);
 
 
     // Channels label bg
     // Channels label bg
@@ -371,7 +378,9 @@ pub async fn make(
     shape.verts.append(&mut verts);
     shape.verts.append(&mut verts);
     shape.indices.append(&mut indices);
     shape.indices.append(&mut indices);
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("active_tab_overlay");
     let node = create_vector_art("active_tab_overlay");
@@ -408,7 +417,9 @@ pub async fn make(
         COLOR_CYAN,
         COLOR_CYAN,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("active_tab_bg");
     let node = create_vector_art("active_tab_bg");
@@ -428,7 +439,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
         [0., 0., 0., 0.5],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("inactive_tab_overlay");
     let node = create_vector_art("inactive_tab_overlay");
@@ -472,7 +485,9 @@ pub async fn make(
         COLOR_INACTIVE,
         COLOR_INACTIVE,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("input_area_bg");
     let node = create_vector_art("input_area_bg");
@@ -494,7 +509,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
         [0., 0., 0., 0.5],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("fullscreen_label_bg");
     let node = create_vector_art("fullscreen_label_bg");
@@ -559,7 +576,9 @@ pub async fn make(
         COLOR_CYAN,
         COLOR_CYAN,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_button("channels_tab_btn");
     let node = create_button("channels_tab_btn");
@@ -598,7 +617,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_text("channels_tab_text");
     let node = create_text("channels_tab_text");
@@ -631,7 +651,15 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, COLOR_MINT[3]).unwrap();
     prop.set_f32(atom, Role::App, 3, COLOR_MINT[3]).unwrap();
 
 
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     content_area.link(node);
     content_area.link(node);
 
 
@@ -670,7 +698,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_text("contacts_tab_text");
     let node = create_text("contacts_tab_text");
@@ -698,7 +727,15 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 2, COLOR_INACTIVE[2]).unwrap();
     prop.set_f32(atom, Role::App, 2, COLOR_INACTIVE[2]).unwrap();
     prop.set_f32(atom, Role::App, 3, COLOR_INACTIVE[3]).unwrap();
     prop.set_f32(atom, Role::App, 3, COLOR_INACTIVE[3]).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     content_area.link(node);
     content_area.link(node);
 
 
@@ -797,6 +834,7 @@ pub async fn make(
                 me,
                 me,
                 window_scale.clone(),
                 window_scale.clone(),
                 app.renderer.clone(),
                 app.renderer.clone(),
+                app.redraw_trigger.clone(),
                 BaseEditType::SingleLine,
                 BaseEditType::SingleLine,
                 app.ex.clone(),
                 app.ex.clone(),
             )
             )
@@ -824,7 +862,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
         [0., 0., 0., 0.5],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("search_outline");
     let node = create_vector_art("search_outline");
@@ -846,7 +886,9 @@ pub async fn make(
         [0.3, 0.3, 0.3, 1.],
         [0.3, 0.3, 0.3, 1.],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_singleline_edit("nick_edit");
     let node = create_singleline_edit("nick_edit");
@@ -943,6 +985,7 @@ pub async fn make(
                 me,
                 me,
                 window_scale.clone(),
                 window_scale.clone(),
                 app.renderer.clone(),
                 app.renderer.clone(),
+                app.redraw_trigger.clone(),
                 BaseEditType::SingleLine,
                 BaseEditType::SingleLine,
                 app.ex.clone(),
                 app.ex.clone(),
             )
             )
@@ -969,7 +1012,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
         [0., 0., 0., 0.5],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("nick_outline");
     let node = create_vector_art("nick_outline");
@@ -989,7 +1034,9 @@ pub async fn make(
         CONTENT_OUTLINE_SIZE,
         CONTENT_OUTLINE_SIZE,
         [0.3, 0.3, 0.3, 1.],
         [0.3, 0.3, 0.3, 1.],
     );
     );
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_singleline_edit("secret_edit");
     let node = create_singleline_edit("secret_edit");
@@ -1087,6 +1134,7 @@ pub async fn make(
                 me,
                 me,
                 window_scale.clone(),
                 window_scale.clone(),
                 app.renderer.clone(),
                 app.renderer.clone(),
+                app.redraw_trigger.clone(),
                 BaseEditType::SingleLine,
                 BaseEditType::SingleLine,
                 app.ex.clone(),
                 app.ex.clone(),
             )
             )
@@ -1114,7 +1162,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
         [0., 0., 0., 0.5],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("secret_outline");
     let node = create_vector_art("secret_outline");
@@ -1136,7 +1186,9 @@ pub async fn make(
         CONTENT_OUTLINE_SIZE,
         CONTENT_OUTLINE_SIZE,
         [0.3, 0.3, 0.3, 1.],
         [0.3, 0.3, 0.3, 1.],
     );
     );
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("receive_copy_btn_bg");
     let node = create_vector_art("receive_copy_btn_bg");
@@ -1155,7 +1207,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 8).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 8).unwrap();
 
 
     let shape = shape::create_copy(COLOR_CYAN).scaled(COPY_SCALE);
     let shape = shape::create_copy(COLOR_CYAN).scaled(COPY_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     // paste clipboard into the KEY field
     // paste clipboard into the KEY field
@@ -1193,7 +1247,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_layer("addchannel_btn_layer");
     let node = create_layer("addchannel_btn_layer");
@@ -1230,7 +1285,9 @@ pub async fn make(
         COLOR_CYAN,
         COLOR_CYAN,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     editlayer_node.link(node);
     editlayer_node.link(node);
 
 
     let mut node = create_button("addchannel_btn");
     let mut node = create_button("addchannel_btn");
@@ -1245,7 +1302,8 @@ pub async fn make(
     let (slot, addchannel_recvr) = Slot::new("add_channel_clicked_handler");
     let (slot, addchannel_recvr) = Slot::new("add_channel_clicked_handler");
     node.register("click", slot).unwrap();
     node.register("click", slot).unwrap();
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     editlayer_node.link(node.clone());
     editlayer_node.link(node.clone());
 
 
     let addchannel_btn = node;
     let addchannel_btn = node;
@@ -1270,7 +1328,15 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
     prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
 
 
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     editlayer_node.link(node);
     editlayer_node.link(node);
 
 
@@ -1330,7 +1396,15 @@ pub async fn make(
     }
     }
 
 
     let menu_node = node
     let menu_node = node
-        .setup(|me| Menu::new(me, window_scale.clone(), app.renderer.clone(), app.ex.clone()))
+        .setup(|me| {
+            Menu::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                app.redraw_trigger.clone(),
+                app.ex.clone(),
+            )
+        })
         .await;
         .await;
     content_area.link(menu_node.clone());
     content_area.link(menu_node.clone());
 
 

+ 102 - 27
bin/app/src/app/schema/menu/contact.rs

@@ -228,7 +228,9 @@ pub async fn make(
         0.2,
         0.2,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(node);
     content.link(node);
 
 
     // Create back arrow
     // Create back arrow
@@ -241,7 +243,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
 
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(node);
     content.link(node);
 
 
     // Create back button
     // Create back button
@@ -285,7 +289,8 @@ pub async fn make(
     });
     });
     content.push_task(listen_click);
     content.push_task(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content.link(node);
     content.link(node);
 
 
     // Create shortcut to go back as well
     // Create shortcut to go back as well
@@ -340,7 +345,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
     let mut shape = VectorShape::new();
     let mut shape = VectorShape::new();
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(node);
     content.link(node);
 
 
     // Contacts label bg
     // Contacts label bg
@@ -372,7 +379,9 @@ pub async fn make(
     shape.verts.append(&mut verts);
     shape.verts.append(&mut verts);
     shape.indices.append(&mut indices);
     shape.indices.append(&mut indices);
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("active_tab_overlay");
     let node = create_vector_art("active_tab_overlay");
@@ -409,7 +418,9 @@ pub async fn make(
         COLOR_CYAN,
         COLOR_CYAN,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("active_tab_bg");
     let node = create_vector_art("active_tab_bg");
@@ -429,7 +440,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
         [0., 0., 0., 0.5],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("inactive_tab_overlay");
     let node = create_vector_art("inactive_tab_overlay");
@@ -473,7 +486,9 @@ pub async fn make(
         COLOR_INACTIVE,
         COLOR_INACTIVE,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("input_area_bg");
     let node = create_vector_art("input_area_bg");
@@ -495,7 +510,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
         [0., 0., 0., 0.5],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("fullscreen_label_bg");
     let node = create_vector_art("fullscreen_label_bg");
@@ -561,7 +578,9 @@ pub async fn make(
         COLOR_CYAN,
         COLOR_CYAN,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_button("contacts_tab_btn");
     let node = create_button("contacts_tab_btn");
@@ -599,7 +618,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_text("contacts_tab_text");
     let node = create_text("contacts_tab_text");
@@ -628,7 +648,15 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, COLOR_MINT[3]).unwrap();
     prop.set_f32(atom, Role::App, 3, COLOR_MINT[3]).unwrap();
 
 
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     content_area.link(node);
     content_area.link(node);
 
 
@@ -668,7 +696,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_text("channels_tab_text");
     let node = create_text("channels_tab_text");
@@ -700,7 +729,15 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 2, COLOR_INACTIVE[2]).unwrap();
     prop.set_f32(atom, Role::App, 2, COLOR_INACTIVE[2]).unwrap();
     prop.set_f32(atom, Role::App, 3, COLOR_INACTIVE[3]).unwrap();
     prop.set_f32(atom, Role::App, 3, COLOR_INACTIVE[3]).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     content_area.link(node);
     content_area.link(node);
 
 
@@ -799,6 +836,7 @@ pub async fn make(
                 me,
                 me,
                 window_scale.clone(),
                 window_scale.clone(),
                 app.renderer.clone(),
                 app.renderer.clone(),
+                app.redraw_trigger.clone(),
                 BaseEditType::SingleLine,
                 BaseEditType::SingleLine,
                 app.ex.clone(),
                 app.ex.clone(),
             )
             )
@@ -826,7 +864,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
         [0., 0., 0., 0.5],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("search_outline");
     let node = create_vector_art("search_outline");
@@ -848,7 +888,9 @@ pub async fn make(
         [0.3, 0.3, 0.3, 1.],
         [0.3, 0.3, 0.3, 1.],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_singleline_edit("nick_edit");
     let node = create_singleline_edit("nick_edit");
@@ -946,6 +988,7 @@ pub async fn make(
                 me,
                 me,
                 window_scale.clone(),
                 window_scale.clone(),
                 app.renderer.clone(),
                 app.renderer.clone(),
+                app.redraw_trigger.clone(),
                 BaseEditType::SingleLine,
                 BaseEditType::SingleLine,
                 app.ex.clone(),
                 app.ex.clone(),
             )
             )
@@ -972,7 +1015,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
         [0., 0., 0., 0.5],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("nick_outline");
     let node = create_vector_art("nick_outline");
@@ -993,7 +1038,9 @@ pub async fn make(
         [0.3, 0.3, 0.3, 1.],
         [0.3, 0.3, 0.3, 1.],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_singleline_edit("secret_edit");
     let node = create_singleline_edit("secret_edit");
@@ -1091,6 +1138,7 @@ pub async fn make(
                 me,
                 me,
                 window_scale.clone(),
                 window_scale.clone(),
                 app.renderer.clone(),
                 app.renderer.clone(),
+                app.redraw_trigger.clone(),
                 BaseEditType::SingleLine,
                 BaseEditType::SingleLine,
                 app.ex.clone(),
                 app.ex.clone(),
             )
             )
@@ -1118,7 +1166,9 @@ pub async fn make(
         [0., 0., 0., 0.5],
         [0., 0., 0., 0.5],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("secret_outline");
     let node = create_vector_art("secret_outline");
@@ -1140,7 +1190,9 @@ pub async fn make(
         CONTENT_OUTLINE_SIZE,
         CONTENT_OUTLINE_SIZE,
         [0.3, 0.3, 0.3, 1.],
         [0.3, 0.3, 0.3, 1.],
     );
     );
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_vector_art("receive_copy_btn_bg");
     let node = create_vector_art("receive_copy_btn_bg");
@@ -1159,7 +1211,9 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 8).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 8).unwrap();
 
 
     let shape = shape::create_copy(COLOR_CYAN).scaled(COPY_SCALE);
     let shape = shape::create_copy(COLOR_CYAN).scaled(COPY_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content_area.link(node);
     content_area.link(node);
 
 
     // Clickable overlay: paste clipboard contents into the KEY field.
     // Clickable overlay: paste clipboard contents into the KEY field.
@@ -1197,7 +1251,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_button("add_contact_btn");
     let node = create_button("add_contact_btn");
@@ -1215,7 +1270,8 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 2, 150.).unwrap();
     prop.set_f32(atom, Role::App, 2, 150.).unwrap();
     prop.set_f32(atom, Role::App, 3, 50.).unwrap();
     prop.set_f32(atom, Role::App, 3, 50.).unwrap();
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(node);
     content_area.link(node);
 
 
     let node = create_layer("addcontact_btn_layer");
     let node = create_layer("addcontact_btn_layer");
@@ -1252,7 +1308,9 @@ pub async fn make(
         COLOR_CYAN,
         COLOR_CYAN,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     editlayer_node.link(node);
     editlayer_node.link(node);
 
 
     let node = create_button("addcontact_btn");
     let node = create_button("addcontact_btn");
@@ -1267,7 +1325,8 @@ pub async fn make(
     let (slot, addcontact_recvr) = Slot::new("add_contact_clicked_handler");
     let (slot, addcontact_recvr) = Slot::new("add_contact_clicked_handler");
     node.register("click", slot).unwrap();
     node.register("click", slot).unwrap();
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     editlayer_node.link(node);
     editlayer_node.link(node);
 
 
     let node = create_text("add_contact");
     let node = create_text("add_contact");
@@ -1290,7 +1349,15 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
     prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
 
 
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     editlayer_node.link(node);
     editlayer_node.link(node);
 
 
@@ -1350,7 +1417,15 @@ pub async fn make(
     }
     }
 
 
     let menu_node = node
     let menu_node = node
-        .setup(|me| Menu::new(me, window_scale.clone(), app.renderer.clone(), app.ex.clone()))
+        .setup(|me| {
+            Menu::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                app.redraw_trigger.clone(),
+                app.ex.clone(),
+            )
+        })
         .await;
         .await;
     content_area.link(menu_node.clone());
     content_area.link(menu_node.clone());
 
 

+ 25 - 5
bin/app/src/app/schema/menu/edit_buttons.rs

@@ -154,7 +154,9 @@ pub async fn create_edit_buttons(
         COLOR_CYAN,
         COLOR_CYAN,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     editlayer_node.link(node);
     editlayer_node.link(node);
 
 
     // Create the cancel button
     // Create the cancel button
@@ -166,7 +168,8 @@ pub async fn create_edit_buttons(
     prop.set_f32(atom, Role::App, 2, MENU_BTN_W_L).unwrap();
     prop.set_f32(atom, Role::App, 2, MENU_BTN_W_L).unwrap();
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
 
 
-    let cancel_btn = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let cancel_btn =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     editlayer_node.link(cancel_btn.clone());
     editlayer_node.link(cancel_btn.clone());
 
 
     // Text for cancel button
     // Text for cancel button
@@ -190,7 +193,15 @@ pub async fn create_edit_buttons(
     prop.set_f32(atom, Role::App, 3, COLOR_RED[3]).unwrap();
     prop.set_f32(atom, Role::App, 3, COLOR_RED[3]).unwrap();
 
 
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     editlayer_node.link(node);
     editlayer_node.link(node);
 
 
@@ -204,7 +215,8 @@ pub async fn create_edit_buttons(
     prop.set_f32(atom, Role::App, 2, MENU_BTN_W_R).unwrap();
     prop.set_f32(atom, Role::App, 2, MENU_BTN_W_R).unwrap();
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
 
 
-    let done_btn = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let done_btn =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     editlayer_node.link(done_btn.clone());
     editlayer_node.link(done_btn.clone());
 
 
     // Text for done button
     // Text for done button
@@ -229,7 +241,15 @@ pub async fn create_edit_buttons(
     prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
     prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
 
 
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     editlayer_node.link(node);
     editlayer_node.link(node);
 
 

+ 43 - 9
bin/app/src/app/schema/menu/mod.rs

@@ -250,7 +250,9 @@ pub async fn make(
         [0.15, 0.2, 0.19, 1.],
         [0.15, 0.2, 0.19, 1.],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create some text
     // Create some text
@@ -281,7 +283,15 @@ pub async fn make(
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
 
 
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     layer_node.link(node);
     layer_node.link(node);
 
 
@@ -314,7 +324,9 @@ pub async fn make(
     node.set_property_f32(atom, Role::App, "scale", VERBLOCK_SCALE).unwrap();
     node.set_property_f32(atom, Role::App, "scale", VERBLOCK_SCALE).unwrap();
     let shape = shape::create_version_block([1., 0., 0.25, 1.]);
     let shape = shape::create_version_block([1., 0., 0.25, 1.]);
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     mainlayer_node.link(node);
     mainlayer_node.link(node);
 
 
     // Write / Menu button
     // Write / Menu button
@@ -343,7 +355,9 @@ pub async fn make(
         COLOR_CYAN,
         COLOR_CYAN,
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     mainlayer_node.link(node);
     mainlayer_node.link(node);
 
 
     let node = create_button("write_btn");
     let node = create_button("write_btn");
@@ -373,7 +387,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     mainlayer_node.link(node);
     mainlayer_node.link(node);
 
 
     let node = create_text("write_text");
     let node = create_text("write_text");
@@ -397,7 +412,15 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
     prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
 
 
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     mainlayer_node.link(node);
     mainlayer_node.link(node);
 
 
@@ -472,7 +495,15 @@ pub async fn make(
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
     let menu_node = node
     let menu_node = node
-        .setup(|me| Menu::new(me, window_scale.clone(), app.renderer.clone(), app.ex.clone()))
+        .setup(|me| {
+            Menu::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                app.redraw_trigger.clone(),
+                app.ex.clone(),
+            )
+        })
         .await;
         .await;
     layer_node.link(menu_node.clone());
     layer_node.link(menu_node.clone());
 
 
@@ -564,7 +595,9 @@ pub async fn setup_wallet_button(app: &App, menu_layer: SceneNodePtr, i18n_fish:
         [0., 0.94, 1., 1.],
         [0., 0.94, 1., 1.],
     );
     );
 
 
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     mainlayer_node.link(node);
     mainlayer_node.link(node);
 
 
     let node = create_button("wallet_btn");
     let node = create_button("wallet_btn");
@@ -599,7 +632,8 @@ pub async fn setup_wallet_button(app: &App, menu_layer: SceneNodePtr, i18n_fish:
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
     let renderer = app.renderer.clone();
     let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
 
 
-    let node = node.setup(|me| Button::new(me, renderer)).await;
+    let node = node.setup(|me| Button::new(me, renderer, redraw)).await;
     mainlayer_node.link(node);
     mainlayer_node.link(node);
 }
 }

+ 27 - 10
bin/app/src/app/schema/mod.rs

@@ -374,7 +374,11 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
         node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
         node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
         //let node = node.setup(|me| Image::new(me, app.renderer.clone())).await;
         //let node = node.setup(|me| Image::new(me, app.renderer.clone())).await;
         //layer_node.link(node);
         //layer_node.link(node);
-        let node = node.setup(|me| Video::new(me, app.renderer.clone(), app.ex.clone())).await;
+        let node = node
+            .setup(|me| {
+                Video::new(me, app.renderer.clone(), app.redraw_trigger.clone(), app.ex.clone())
+            })
+            .await;
         content.link(node);
         content.link(node);
     } else if COLOR_SCHEME == ColorScheme::PaperLight {
     } else if COLOR_SCHEME == ColorScheme::PaperLight {
         let node = create_vector_art("bg");
         let node = create_vector_art("bg");
@@ -395,7 +399,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
             expr::load_var("h"),
             expr::load_var("h"),
             [c, c, c, 0.3],
             [c, c, c, 0.3],
         );
         );
-        let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+        let node = node
+            .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+            .await;
         window.link(node);
         window.link(node);
     }
     }
 
 
@@ -427,7 +433,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     let mut shape = shape::create_netlogo1([1., 0., 0.25, 1.]);
     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_netlogo2([0.27, 0.4, 0.4, 1.]));
     shape.join(shape::create_netlogo3([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())).await;
+    let net0_node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     netlayer_node.link(net0_node);
     netlayer_node.link(net0_node);
 
 
     let node = create_vector_art("net1");
     let node = create_vector_art("net1");
@@ -442,7 +450,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     let mut shape = shape::create_netlogo1([0.49, 0.57, 1., 1.]);
     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_netlogo2([0.49, 0.57, 1., 1.]));
     shape.join(shape::create_netlogo3([0.27, 0.4, 0.4, 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())).await;
+    let net1_node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     netlayer_node.link(net1_node);
     netlayer_node.link(net1_node);
 
 
     let node = create_vector_art("net2");
     let node = create_vector_art("net2");
@@ -457,7 +467,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     let mut shape = shape::create_netlogo1([0., 0.94, 1., 1.]);
     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_netlogo2([0., 0.94, 1., 1.]));
     shape.join(shape::create_netlogo3([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())).await;
+    let net2_node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     netlayer_node.link(net2_node);
     netlayer_node.link(net2_node);
 
 
     let node = create_vector_art("net3");
     let node = create_vector_art("net3");
@@ -472,7 +484,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     let mut shape = shape::create_netlogo1([0., 0.94, 1., 1.]);
     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_netlogo2([0., 0.94, 1., 1.]));
     shape.join(shape::create_netlogo3([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())).await;
+    let net3_node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     netlayer_node.link(net3_node);
     netlayer_node.link(net3_node);
 
 
     // netstat-klik icon (visual feedback when reconnect button is clicked)
     // netstat-klik icon (visual feedback when reconnect button is clicked)
@@ -494,7 +508,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
         expr::const_f32(NETSTATUS_ICON_SIZE),
         expr::const_f32(NETSTATUS_ICON_SIZE),
         klik_color,
         klik_color,
     );
     );
-    let netstat_klik_node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let netstat_klik_node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     netlayer_node.link(netstat_klik_node.clone());
     netlayer_node.link(netstat_klik_node.clone());
 
 
     // Reconnect Button (overlaid on netstatus icons)
     // Reconnect Button (overlaid on netstatus icons)
@@ -552,7 +568,8 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     });
     });
     app.tasks.lock().unwrap().push(reconnect_task);
     app.tasks.lock().unwrap().push(reconnect_task);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     netlayer_node.link(node);
     netlayer_node.link(node);
 
 
     // Navbar Settings Button
     // Navbar Settings Button
@@ -583,7 +600,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
     let shape = shape::create_settings([0., 0.94, 1., 1.]).scaled(20.);
     let shape = shape::create_settings([0., 0.94, 1., 1.]).scaled(20.);
     let node =
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
     settingslayer_node.link(node);
     settingslayer_node.link(node);
 
 
     // Button
     // Button
@@ -642,7 +659,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node = node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     settingslayer_node.link(node);
     settingslayer_node.link(node);
     */
     */
 
 

+ 19 - 19
bin/app/src/app/schema/settings.rs

@@ -201,7 +201,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     );
     );
 
 
     let node =
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone())).await;
+        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create the back button
     // Create the back button
@@ -215,7 +215,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
 
 
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
     let node =
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone())).await;
+        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     let node = create_button("back_btn");
     let node = create_button("back_btn");
@@ -259,7 +259,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node = node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node.clone());
     layer_node.link(node.clone());
 
 
     // Label: "SETTINGS" title
     // Label: "SETTINGS" title
@@ -311,7 +311,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     );
     );
 
 
     let node =
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone())).await;
+        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Search Bar Input
     // Search Bar Input
@@ -360,7 +360,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
 
 
     let shape = shape::create_logo([1., 1., 1., 1.]).scaled(500.);
     let shape = shape::create_logo([1., 1., 1., 1.]).scaled(500.);
     let node =
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone())).await;
+        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Search placeholder
     // Search placeholder
@@ -528,7 +528,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     );
     );
 
 
     let node =
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone())).await;
+        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     let node = create_vector_art("search_bg2");
     let node = create_vector_art("search_bg2");
@@ -570,7 +570,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     );
     );
 
 
     let node =
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone())).await;
+        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create a BTreeMap to store settings
     // Create a BTreeMap to store settings
@@ -682,7 +682,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         );
         );
 
 
         let node = node
         let node = node
-            .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone()))
+            .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
             .await;
             .await;
         setting_layer_node.link(node);
         setting_layer_node.link(node);
 
 
@@ -749,7 +749,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
             );
             );
 
 
             let node = node
             let node = node
-                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone()))
+                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
                 .await;
                 .await;
             setting_layer_node.link(node);
             setting_layer_node.link(node);
 
 
@@ -815,7 +815,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
             );
             );
 
 
             let node = node
             let node = node
-                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone()))
+                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
                 .await;
                 .await;
             setting_layer_node.link(node);
             setting_layer_node.link(node);
         } else {
         } else {
@@ -875,7 +875,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
             );
             );
 
 
             let node = node
             let node = node
-                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone()))
+                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
                 .await;
                 .await;
             setting_layer_node.link(node);
             setting_layer_node.link(node);
         }
         }
@@ -1027,7 +1027,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
 
 
                 let shape = shape::create_circle([0.9, 0.4, 0.4, 0.7]).scaled(5.);
                 let shape = shape::create_circle([0.9, 0.4, 0.4, 0.7]).scaled(5.);
                 let node = node
                 let node = node
-                    .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone()))
+                    .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
                     .await;
                     .await;
                 setting_layer_node.link(node);
                 setting_layer_node.link(node);
 
 
@@ -1056,7 +1056,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
 
 
                 let shape = shape::create_circle([0., 0.94, 1., 1.]).scaled(5.);
                 let shape = shape::create_circle([0., 0.94, 1., 1.]).scaled(5.);
                 let node = node
                 let node = node
-                    .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone()))
+                    .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
                     .await;
                     .await;
                 setting_layer_node.link(node);
                 setting_layer_node.link(node);
 
 
@@ -1358,7 +1358,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
                 });
                 });
                 app.tasks.lock().unwrap().push(listen_click);
                 app.tasks.lock().unwrap().push(listen_click);
 
 
-                let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+                let node = node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
                 setting_layer_node.link(node.clone());
                 setting_layer_node.link(node.clone());
             }
             }
         }
         }
@@ -1375,7 +1375,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
 
 
             let shape = shape::create_switch([0., 0.94, 1., 1.]).scaled(10.);
             let shape = shape::create_switch([0., 0.94, 1., 1.]).scaled(10.);
             let node = node
             let node = node
-                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone()))
+                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
                 .await;
                 .await;
             setting_layer_node.link(node);
             setting_layer_node.link(node);
         } else {
         } else {
@@ -1391,7 +1391,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
 
 
             let shape = shape::create_confirm([0., 0.94, 1., 1.]).scaled(10.);
             let shape = shape::create_confirm([0., 0.94, 1., 1.]).scaled(10.);
             let node = node
             let node = node
-                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone()))
+                .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
                 .await;
                 .await;
             setting_layer_node.link(node.clone());
             setting_layer_node.link(node.clone());
 
 
@@ -1404,7 +1404,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
             prop.set_f32(atom, Role::App, 3, SETTING_LABEL_LINESPACE).unwrap();
             prop.set_f32(atom, Role::App, 3, SETTING_LABEL_LINESPACE).unwrap();
             node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
             node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
 
-            let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+            let node = node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
             setting_layer_node.link(node.clone());
             setting_layer_node.link(node.clone());
 
 
             // Handle confirm button click
             // Handle confirm button click
@@ -1444,7 +1444,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
 
 
         let shape = shape::create_reset([0., 0.94, 1., 1.]).scaled(15.);
         let shape = shape::create_reset([0., 0.94, 1., 1.]).scaled(15.);
         let node = node
         let node = node
-            .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone()))
+            .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
             .await;
             .await;
         setting_layer_node.link(node);
         setting_layer_node.link(node);
 
 
@@ -1457,7 +1457,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         prop.set_f32(atom, Role::App, 3, SETTING_LABEL_LINESPACE).unwrap();
         prop.set_f32(atom, Role::App, 3, SETTING_LABEL_LINESPACE).unwrap();
         node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
         node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
 
-        let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+        let node = node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
         setting_layer_node.link(node.clone());
         setting_layer_node.link(node.clone());
 
 
         // Handle reset button click
         // Handle reset button click

+ 16 - 8
bin/app/src/app/schema/test.rs

@@ -129,7 +129,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         // Color
         // Color
         [0., 1., 0., 1.],
         [0., 1., 0., 1.],
     );
     );
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     /*
     /*
@@ -162,7 +164,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     let indices = vec![0, 2, 1, 1, 2, 3];
     let indices = vec![0, 2, 1, 1, 2, 3];
     let shape = VectorShape { verts, indices };
     let shape = VectorShape { verts, indices };
     let node =
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone())).await;
+        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create the button
     // Create the button
@@ -179,7 +181,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     //let slot_click = Slot { name: "button_clicked".to_string(), notify: sender };
     //let slot_click = Slot { name: "button_clicked".to_string(), notify: sender };
     //node.register("click", slot_click).unwrap();
     //node.register("click", slot_click).unwrap();
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node = node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create another mesh
     // Create another mesh
@@ -210,7 +212,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     let indices = vec![0, 2, 1, 1, 2, 3];
     let indices = vec![0, 2, 1, 1, 2, 3];
     let shape = VectorShape { verts, indices };
     let shape = VectorShape { verts, indices };
     let node =
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone())).await;
+        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Debugging tool
     // Debugging tool
@@ -240,7 +242,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         [0., 1., 0., 1.],
         [0., 1., 0., 1.],
     );
     );
     let node =
     let node =
-        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.ex.clone())).await;
+        node.setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     // Create KING GNU!
     // Create KING GNU!
@@ -252,7 +254,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     prop.set_f32(atom, Role::App, 3, 60.).unwrap();
     prop.set_f32(atom, Role::App, 3, 60.).unwrap();
     node.set_property_str(atom, Role::App, "path", KING_PATH).unwrap();
     node.set_property_str(atom, Role::App, "path", KING_PATH).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
-    let node = node.setup(|me| Image::new(me, app.renderer.clone(), app.ex.clone())).await;
+    let node = node.setup(|me| Image::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer_node.link(node);
     layer_node.link(node);
     */
     */
 
 
@@ -265,7 +267,11 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     prop.set_f32(atom, Role::App, 3, 600.).unwrap();
     prop.set_f32(atom, Role::App, 3, 600.).unwrap();
     node.set_property_str(atom, Role::App, "path", VID_PATH).unwrap();
     node.set_property_str(atom, Role::App, "path", VID_PATH).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
-    let node = node.setup(|me| Video::new(me, app.renderer.clone(), app.ex.clone())).await;
+    let node = node
+        .setup(|me| {
+            Video::new(me, app.renderer.clone(), app.redraw_trigger.clone(), app.ex.clone())
+        })
+        .await;
     layer_node.link(node);
     layer_node.link(node);
 
 
     /*
     /*
@@ -296,7 +302,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     node.set_property_bool(atom, Role::App, "debug", true).unwrap();
     node.set_property_bool(atom, Role::App, "debug", true).unwrap();
 
 
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone(), app.redraw_trigger.clone()))
         .await;
         .await;
     layer_node.link(node);
     layer_node.link(node);
 
 
@@ -379,6 +385,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
                 chat_tree,
                 chat_tree,
                 window_scale.clone(),
                 window_scale.clone(),
                 app.renderer.clone(),
                 app.renderer.clone(),
+                app.redraw_trigger.clone(),
                 app.sg_root.clone(),
                 app.sg_root.clone(),
                 app.ex.clone(),
                 app.ex.clone(),
             )
             )
@@ -467,6 +474,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
                 me,
                 me,
                 window_scale.clone(),
                 window_scale.clone(),
                 app.renderer.clone(),
                 app.renderer.clone(),
+                app.redraw_trigger.clone(),
                 BaseEditType::SingleLine,
                 BaseEditType::SingleLine,
                 app.ex.clone(),
                 app.ex.clone(),
                 //BaseEditType::MultiLine,
                 //BaseEditType::MultiLine,

+ 4 - 1
bin/app/src/app/schema/test_edit.rs

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

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

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

+ 66 - 13
bin/app/src/app/schema/wallet/main.rs

@@ -101,13 +101,29 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     main_layer.link(node);
     main_layer.link(node);
 
 
     y += TITLE_PADDING * 2. + TITLE_FONTSIZE + 1.;
     y += TITLE_PADDING * 2. + TITLE_FONTSIZE + 1.;
 
 
-    create_separator(&app.renderer, atom, &main_layer, "wallet_balance_separator", &mut y).await;
+    create_separator(
+        &app.renderer,
+        &app.redraw_trigger,
+        atom,
+        &main_layer,
+        "wallet_balance_separator",
+        &mut y,
+    )
+    .await;
 
 
     // Receive button bg
     // Receive button bg
     let node = create_vector_art("receive_btn_bg");
     let node = create_vector_art("receive_btn_bg");
@@ -127,7 +143,9 @@ pub async fn make(
         1.,
         1.,
         COLOR_TEAL,
         COLOR_TEAL,
     );
     );
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     main_layer.link(node);
     main_layer.link(node);
 
 
     // Receive button click handler
     // Receive button click handler
@@ -182,7 +200,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     main_layer.link(node);
     main_layer.link(node);
 
 
     // Receive label
     // Receive label
@@ -211,7 +230,15 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     main_layer.link(node);
     main_layer.link(node);
 
 
@@ -234,7 +261,9 @@ pub async fn make(
         1.,
         1.,
         COLOR_TEAL,
         COLOR_TEAL,
     );
     );
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     main_layer.link(node);
     main_layer.link(node);
 
 
     // Send button click handler
     // Send button click handler
@@ -264,7 +293,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     main_layer.link(node);
     main_layer.link(node);
 
 
     // Send label
     // Send label
@@ -294,13 +324,29 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     main_layer.link(node);
     main_layer.link(node);
 
 
     y += PADDING_X * 2. + BUTTON_HEIGHT + 1.;
     y += PADDING_X * 2. + BUTTON_HEIGHT + 1.;
 
 
-    create_separator(&app.renderer, atom, &main_layer, "wallet_buttons_separator", &mut y).await;
+    create_separator(
+        &app.renderer,
+        &app.redraw_trigger,
+        atom,
+        &main_layer,
+        "wallet_buttons_separator",
+        &mut y,
+    )
+    .await;
 
 
     create_title(app, atom, &main_layer, &window_scale, i18n_fish, "TOKENS", &mut y).await;
     create_title(app, atom, &main_layer, &window_scale, i18n_fish, "TOKENS", &mut y).await;
 
 
@@ -340,7 +386,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 2, 0.2784).unwrap();
     prop.set_f32(atom, Role::App, 2, 0.2784).unwrap();
     prop.set_f32(atom, Role::App, 3, 1.).unwrap();
     prop.set_f32(atom, Role::App, 3, 1.).unwrap();
 
 
-    let tokens_table = tokens_table.setup(|me| TokenTable::new(me, app.renderer.clone())).await;
+    let tokens_table = tokens_table
+        .setup(|me| TokenTable::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     main_layer.link(tokens_table.clone());
     main_layer.link(tokens_table.clone());
 
 
     main_layer
     main_layer
@@ -378,7 +426,9 @@ async fn create_chat_btn(
         1.,
         1.,
         [0.2, 0.2745, 0.2784, 1.],
         [0.2, 0.2745, 0.2784, 1.],
     );
     );
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     parent.link(node);
     parent.link(node);
 
 
     let node = create_vector_art("chat_btn_shape");
     let node = create_vector_art("chat_btn_shape");
@@ -394,7 +444,9 @@ async fn create_chat_btn(
     let mut shape = shape::create_netlogo1([0., 0.94, 1., 1.]);
     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_netlogo2([0., 0.94, 1., 1.]));
     shape.join(shape::create_netlogo3([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())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     parent.link(node);
     parent.link(node);
 
 
     let node = create_button("chat_btn");
     let node = create_button("chat_btn");
@@ -435,6 +487,7 @@ async fn create_chat_btn(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     parent.link(node);
     parent.link(node);
 }
 }

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

@@ -70,7 +70,9 @@ pub async fn make(
     shape.join(shape::create_blockchain_netlogo2([0.27, 0.4, 0.4, 1.]));
     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_netlogo3([0.27, 0.4, 0.4, 1.]));
     shape.join(shape::create_blockchain_netlogo4([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())).await;
+    let net0_node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     netlayer_node.link(net0_node);
     netlayer_node.link(net0_node);
 
 
     let node = create_vector_art("net1");
     let node = create_vector_art("net1");
@@ -86,7 +88,9 @@ pub async fn make(
     shape.join(shape::create_blockchain_netlogo2([0.73, 0.62, 1., 1.]));
     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_netlogo3([0.27, 0.4, 0.4, 1.]));
     shape.join(shape::create_blockchain_netlogo4([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())).await;
+    let net1_node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     netlayer_node.link(net1_node);
     netlayer_node.link(net1_node);
 
 
     let node = create_vector_art("net2");
     let node = create_vector_art("net2");
@@ -102,7 +106,9 @@ pub async fn make(
     shape.join(shape::create_blockchain_netlogo2([0.49, 0.57, 1., 1.]));
     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_netlogo3([0.49, 0.57, 1., 1.]));
     shape.join(shape::create_blockchain_netlogo4([0.27, 0.4, 0.4, 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())).await;
+    let net2_node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     netlayer_node.link(net2_node);
     netlayer_node.link(net2_node);
 
 
     let node = create_vector_art("net3");
     let node = create_vector_art("net3");
@@ -118,7 +124,9 @@ pub async fn make(
     shape.join(shape::create_blockchain_netlogo2([0., 0.94, 1., 1.]));
     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_netlogo3([0., 0.94, 1., 1.]));
     shape.join(shape::create_blockchain_netlogo4([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())).await;
+    let net3_node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     netlayer_node.link(net3_node);
     netlayer_node.link(net3_node);
 
 
     netlayer_node
     netlayer_node

+ 19 - 5
bin/app/src/app/schema/wallet/receive.rs

@@ -74,7 +74,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     receive_layer.link(node);
     receive_layer.link(node);
 
 
     let mut y = 0.;
     let mut y = 0.;
@@ -102,7 +104,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     receive_layer.link(node);
     receive_layer.link(node);
 
 
     y += HEADER_HEIGHT;
     y += HEADER_HEIGHT;
@@ -148,7 +151,15 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     receive_layer.link(node);
     receive_layer.link(node);
 
 
@@ -164,7 +175,9 @@ pub async fn make(
     prop.add_depend(&addr_h_prop, 0, "addr_height");
     prop.add_depend(&addr_h_prop, 0, "addr_height");
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_copy(COLOR_CYAN).scaled(COPY_SCALE);
     let shape = shape::create_copy(COLOR_CYAN).scaled(COPY_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     receive_layer.link(node);
     receive_layer.link(node);
 
 
     let node = create_button("receive_copy_btn");
     let node = create_button("receive_copy_btn");
@@ -199,7 +212,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     receive_layer.link(node);
     receive_layer.link(node);
 
 
     // Create tooltip
     // Create tooltip

+ 8 - 4
bin/app/src/app/schema/wallet/send_step1.rs

@@ -90,7 +90,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     send_step1_layer.link(node);
     send_step1_layer.link(node);
 
 
     let mut y = 0.;
     let mut y = 0.;
@@ -118,7 +120,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     send_step1_layer.link(node);
     send_step1_layer.link(node);
 
 
     y += HEADER_HEIGHT;
     y += HEADER_HEIGHT;
@@ -168,8 +171,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 2, 0.2784).unwrap();
     prop.set_f32(atom, Role::App, 2, 0.2784).unwrap();
     prop.set_f32(atom, Role::App, 3, 1.).unwrap();
     prop.set_f32(atom, Role::App, 3, 1.).unwrap();
 
 
-    let send_tokens_table =
-        send_tokens_table.setup(|me| TokenTable::new(me, app.renderer.clone())).await;
+    let send_tokens_table = send_tokens_table
+        .setup(|me| TokenTable::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     send_step1_layer.link(send_tokens_table.clone());
     send_step1_layer.link(send_tokens_table.clone());
 
 
     let (slot, recvr) = Slot::new("token_row_clicked");
     let (slot, recvr) = Slot::new("token_row_clicked");

+ 41 - 8
bin/app/src/app/schema/wallet/send_step2.rs

@@ -119,7 +119,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     send_step2_layer.link(node);
     send_step2_layer.link(node);
 
 
     let mut y = 0.;
     let mut y = 0.;
@@ -146,7 +148,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     send_step2_layer.link(node);
     send_step2_layer.link(node);
 
 
     y += HEADER_HEIGHT;
     y += HEADER_HEIGHT;
@@ -176,13 +179,29 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step2_layer.link(node);
     send_step2_layer.link(node);
 
 
     y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
     y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
 
 
-    create_separator(&app.renderer, atom, &send_step2_layer, "send_token_separator", &mut y).await;
+    create_separator(
+        &app.renderer,
+        &app.redraw_trigger,
+        atom,
+        &send_step2_layer,
+        "send_token_separator",
+        &mut y,
+    )
+    .await;
 
 
     // Recipient label
     // Recipient label
     let node = create_text("send_recipient_label");
     let node = create_text("send_recipient_label");
@@ -207,7 +226,15 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step2_layer.link(node);
     send_step2_layer.link(node);
 
 
@@ -231,7 +258,9 @@ pub async fn make(
         1.,
         1.,
         [0.2, 0.2745, 0.2784, 1.],
         [0.2, 0.2745, 0.2784, 1.],
     );
     );
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     send_step2_layer.link(node);
     send_step2_layer.link(node);
 
 
     // Recipient input
     // Recipient input
@@ -311,6 +340,7 @@ pub async fn make(
                 me,
                 me,
                 window_scale.clone(),
                 window_scale.clone(),
                 app.renderer.clone(),
                 app.renderer.clone(),
+                app.redraw_trigger.clone(),
                 BaseEditType::SingleLine,
                 BaseEditType::SingleLine,
                 app.ex.clone(),
                 app.ex.clone(),
             )
             )
@@ -328,7 +358,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_copy(COLOR_CYAN).scaled(PASTE_SCALE);
     let shape = shape::create_copy(COLOR_CYAN).scaled(PASTE_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     send_step2_layer.link(node);
     send_step2_layer.link(node);
 
 
     let node = create_button("send_paste_btn");
     let node = create_button("send_paste_btn");
@@ -359,7 +391,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     send_step2_layer.link(node);
     send_step2_layer.link(node);
 
 
     // Add recipient button
     // Add recipient button

+ 71 - 9
bin/app/src/app/schema/wallet/send_step3.rs

@@ -94,7 +94,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     send_step3_layer.link(node);
     send_step3_layer.link(node);
 
 
     let mut y = 0.;
     let mut y = 0.;
@@ -128,7 +130,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     send_step3_layer.link(node);
     send_step3_layer.link(node);
 
 
     y += HEADER_HEIGHT;
     y += HEADER_HEIGHT;
@@ -161,7 +164,15 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step3_layer.link(node);
     send_step3_layer.link(node);
 
 
@@ -190,13 +201,29 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let selected_token_text3 = node
     let selected_token_text3 = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step3_layer.link(selected_token_text3.clone());
     send_step3_layer.link(selected_token_text3.clone());
 
 
     y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
     y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
 
 
-    create_separator(&app.renderer, atom, &send_step3_layer, "send_token_separator3", &mut y).await;
+    create_separator(
+        &app.renderer,
+        &app.redraw_trigger,
+        atom,
+        &send_step3_layer,
+        "send_token_separator3",
+        &mut y,
+    )
+    .await;
 
 
     // Recipient display
     // Recipient display
     let node = create_text("send_recipient_label3");
     let node = create_text("send_recipient_label3");
@@ -221,7 +248,15 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step3_layer.link(node);
     send_step3_layer.link(node);
 
 
@@ -253,7 +288,15 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step3_layer.link(node);
     send_step3_layer.link(node);
 
 
@@ -262,6 +305,7 @@ pub async fn make(
     let y2 = format!("{y} + (PADDING_Y * 2. + addr_height) + 1");
     let y2 = format!("{y} + (PADDING_Y * 2. + addr_height) + 1");
     let node = create_separator(
     let node = create_separator(
         &app.renderer,
         &app.renderer,
+        &app.redraw_trigger,
         atom,
         atom,
         &send_step3_layer,
         &send_step3_layer,
         "send_amount_label_separator",
         "send_amount_label_separator",
@@ -298,7 +342,15 @@ pub async fn make(
     }
     }
     available_balance_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     available_balance_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let available_balance_node = available_balance_node
     let available_balance_node = available_balance_node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step3_layer.link(available_balance_node.clone());
     send_step3_layer.link(available_balance_node.clone());
 
 
@@ -351,7 +403,15 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 1.).unwrap();
     prop.set_f32(atom, Role::App, 3, 1.).unwrap();
     error_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     error_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let error_node = error_node
     let error_node = error_node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step3_layer.link(error_node.clone());
     send_step3_layer.link(error_node.clone());
 
 
@@ -421,6 +481,7 @@ pub async fn make(
                 me,
                 me,
                 window_scale.clone(),
                 window_scale.clone(),
                 app.renderer.clone(),
                 app.renderer.clone(),
+                app.redraw_trigger.clone(),
                 BaseEditType::SingleLine,
                 BaseEditType::SingleLine,
                 app.ex.clone(),
                 app.ex.clone(),
             )
             )
@@ -493,6 +554,7 @@ pub async fn make(
                 me,
                 me,
                 window_scale.clone(),
                 window_scale.clone(),
                 app.renderer.clone(),
                 app.renderer.clone(),
+                app.redraw_trigger.clone(),
                 BaseEditType::SingleLine,
                 BaseEditType::SingleLine,
                 app.ex.clone(),
                 app.ex.clone(),
             )
             )

+ 87 - 11
bin/app/src/app/schema/wallet/send_step4.rs

@@ -89,7 +89,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     prop.set_f32(atom, Role::App, 3, 500.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
     let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     send_step4_layer.link(node);
     send_step4_layer.link(node);
 
 
     let mut y = 0.;
     let mut y = 0.;
@@ -116,7 +118,8 @@ pub async fn make(
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     send_step4_layer.link(node);
     send_step4_layer.link(node);
 
 
     y += HEADER_HEIGHT;
     y += HEADER_HEIGHT;
@@ -146,7 +149,15 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step4_layer.link(node);
     send_step4_layer.link(node);
 
 
@@ -172,13 +183,29 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step4_layer.link(node);
     send_step4_layer.link(node);
 
 
     y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
     y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
 
 
-    create_separator(&app.renderer, atom, &send_step4_layer, "send_token_separator4", &mut y).await;
+    create_separator(
+        &app.renderer,
+        &app.redraw_trigger,
+        atom,
+        &send_step4_layer,
+        "send_token_separator4",
+        &mut y,
+    )
+    .await;
 
 
     // Recipient label
     // Recipient label
     let node = create_text("send_recipient_label4");
     let node = create_text("send_recipient_label4");
@@ -203,7 +230,15 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step4_layer.link(node);
     send_step4_layer.link(node);
 
 
@@ -235,7 +270,15 @@ pub async fn make(
     }
     }
     recipient_addr_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     recipient_addr_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let recipient_addr_node = recipient_addr_node
     let recipient_addr_node = recipient_addr_node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step4_layer.link(recipient_addr_node.clone());
     send_step4_layer.link(recipient_addr_node.clone());
 
 
@@ -244,6 +287,7 @@ pub async fn make(
     let y2 = format!("{y} + (PADDING_Y * 2. + addr_height) + 1");
     let y2 = format!("{y} + (PADDING_Y * 2. + addr_height) + 1");
     let node = create_separator(
     let node = create_separator(
         &app.renderer,
         &app.renderer,
+        &app.redraw_trigger,
         atom,
         atom,
         &send_step4_layer,
         &send_step4_layer,
         "send_amount_label_separator4",
         "send_amount_label_separator4",
@@ -288,7 +332,15 @@ pub async fn make(
     amount_text_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     amount_text_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     amount_text_node.set_property_str(atom, Role::App, "text", "0").unwrap();
     amount_text_node.set_property_str(atom, Role::App, "text", "0").unwrap();
     let amount_text_node = amount_text_node
     let amount_text_node = amount_text_node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     amount_wrapper.link(amount_text_node.clone());
     amount_wrapper.link(amount_text_node.clone());
 
 
@@ -316,7 +368,15 @@ pub async fn make(
     }
     }
     token_symbol_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     token_symbol_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let token_symbol_node = token_symbol_node
     let token_symbol_node = token_symbol_node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     amount_wrapper.link(token_symbol_node.clone());
     amount_wrapper.link(token_symbol_node.clone());
 
 
@@ -338,7 +398,15 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 0.).unwrap();
     prop.set_f32(atom, Role::App, 3, 0.).unwrap();
     tx_fee_label_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     tx_fee_label_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let tx_fee_label_node = tx_fee_label_node
     let tx_fee_label_node = tx_fee_label_node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step4_layer.link(tx_fee_label_node.clone());
     send_step4_layer.link(tx_fee_label_node.clone());
 
 
@@ -362,7 +430,15 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 2, 0.).unwrap();
     prop.set_f32(atom, Role::App, 2, 0.).unwrap();
     prop.set_f32(atom, Role::App, 3, 0.).unwrap();
     prop.set_f32(atom, Role::App, 3, 0.).unwrap();
     let tx_fee_value_node = tx_fee_value_node
     let tx_fee_value_node = tx_fee_value_node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     send_step4_layer.link(tx_fee_value_node.clone());
     send_step4_layer.link(tx_fee_value_node.clone());
 
 

+ 45 - 6
bin/app/src/app/schema/wallet/tx_status.rs

@@ -140,13 +140,29 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     tx_status_layer.link(node);
     tx_status_layer.link(node);
 
 
     y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
     y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
 
 
-    create_separator(&app.renderer, atom, &tx_status_layer, "tx_status_separator", &mut y).await;
+    create_separator(
+        &app.renderer,
+        &app.redraw_trigger,
+        atom,
+        &tx_status_layer,
+        "tx_status_separator",
+        &mut y,
+    )
+    .await;
 
 
     // Transaction info text: "Sending {amount} {token_symbol} to {recipient_address}"
     // Transaction info text: "Sending {amount} {token_symbol} to {recipient_address}"
     let node = create_text("tx_info");
     let node = create_text("tx_info");
@@ -174,12 +190,27 @@ pub async fn make(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     tx_status_layer.link(node);
     tx_status_layer.link(node);
 
 
-    let sep =
-        create_separator(&app.renderer, atom, &tx_status_layer, "tx_info_separator", &mut 0.).await;
+    let sep = create_separator(
+        &app.renderer,
+        &app.redraw_trigger,
+        atom,
+        &tx_status_layer,
+        "tx_info_separator",
+        &mut 0.,
+    )
+    .await;
     let prop = sep.get_property("rect").unwrap();
     let prop = sep.get_property("rect").unwrap();
     let code = cc.compile(format!("{y} + PADDING_Y * 2 + info_height + 1")).unwrap();
     let code = cc.compile(format!("{y} + PADDING_Y * 2 + info_height + 1")).unwrap();
     prop.set_expr(atom, Role::App, 1, code).unwrap();
     prop.set_expr(atom, Role::App, 1, code).unwrap();
@@ -213,7 +244,15 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 0.45).unwrap();
     prop.set_f32(atom, Role::App, 3, 0.45).unwrap();
     hint_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     hint_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let hint_node = hint_node
     let hint_node = hint_node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     tx_status_layer.link(hint_node.clone());
     tx_status_layer.link(hint_node.clone());
 
 

+ 90 - 18
bin/app/src/app/schema/wallet/util.rs

@@ -35,7 +35,7 @@ use crate::{
     prop::{PropertyAtomicGuard, PropertyFloat32, Role},
     prop::{PropertyAtomicGuard, PropertyFloat32, Role},
     scene::{Pimpl, SceneNodePtr},
     scene::{Pimpl, SceneNodePtr},
     text,
     text,
-    ui::{Button, Layer, Text, VectorArt, VectorShape},
+    ui::{Button, Layer, RedrawTrigger, Text, VectorArt, VectorShape},
     util::i18n::I18nBabelFish,
     util::i18n::I18nBabelFish,
 };
 };
 
 
@@ -150,13 +150,29 @@ pub async fn create_title(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     layer.link(node.clone());
     layer.link(node.clone());
 
 
     *y += TITLE_PADDING * 2. + TITLE_FONTSIZE + 1.;
     *y += TITLE_PADDING * 2. + TITLE_FONTSIZE + 1.;
 
 
-    create_separator(&app.renderer, atom, layer, &format!("{}_separator", name), y).await;
+    create_separator(
+        &app.renderer,
+        &app.redraw_trigger,
+        atom,
+        layer,
+        &format!("{}_separator", name),
+        y,
+    )
+    .await;
     node
     node
 }
 }
 
 
@@ -194,13 +210,29 @@ pub async fn create_subtitle(
     }
     }
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     layer.link(node.clone());
     layer.link(node.clone());
 
 
     *y += PADDING_Y * 2. + TITLE_FONTSIZE + 1.;
     *y += PADDING_Y * 2. + TITLE_FONTSIZE + 1.;
 
 
-    create_separator(&app.renderer, atom, layer, &format!("{}_separator", text), y).await;
+    create_separator(
+        &app.renderer,
+        &app.redraw_trigger,
+        atom,
+        layer,
+        &format!("{}_separator", text),
+        y,
+    )
+    .await;
 
 
     node
     node
 }
 }
@@ -227,7 +259,9 @@ pub async fn create_bg_mesh(
         expr::load_var("h"),
         expr::load_var("h"),
         [[0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.8]],
         [[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())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     layer.link(node);
     layer.link(node);
 }
 }
 
 
@@ -266,7 +300,9 @@ pub async fn create_header_bg(
         expr::const_f32(HEADER_HEIGHT),
         expr::const_f32(HEADER_HEIGHT),
         sep_color,
         sep_color,
     );
     );
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     layer.link(node);
     layer.link(node);
 }
 }
 
 
@@ -296,7 +332,9 @@ pub async fn create_separator_expr(
         expr::const_f32(1.),
         expr::const_f32(1.),
         [0.2, 0.2745, 0.2784, 1.],
         [0.2, 0.2745, 0.2784, 1.],
     );
     );
-    let sep_node = sep_node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let sep_node = sep_node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     layer.link(sep_node.clone());
     layer.link(sep_node.clone());
     sep_node
     sep_node
 }
 }
@@ -305,6 +343,7 @@ pub async fn create_separator_expr(
 /// Returns the separator node after setup, linked to the layer
 /// Returns the separator node after setup, linked to the layer
 pub async fn create_separator(
 pub async fn create_separator(
     renderer: &Renderer,
     renderer: &Renderer,
+    redraw: &RedrawTrigger,
     atom: &mut PropertyAtomicGuard,
     atom: &mut PropertyAtomicGuard,
     layer: &SceneNodePtr,
     layer: &SceneNodePtr,
     name: &str,
     name: &str,
@@ -325,7 +364,8 @@ pub async fn create_separator(
         expr::const_f32(1.),
         expr::const_f32(1.),
         [0.2, 0.2745, 0.2784, 1.],
         [0.2, 0.2745, 0.2784, 1.],
     );
     );
-    let sep_node = sep_node.setup(|me| VectorArt::new(me, shape, renderer.clone())).await;
+    let sep_node =
+        sep_node.setup(|me| VectorArt::new(me, shape, renderer.clone(), redraw.clone())).await;
     layer.link(sep_node.clone());
     layer.link(sep_node.clone());
 
 
     *y += 1.;
     *y += 1.;
@@ -364,7 +404,9 @@ pub async fn create_bottom_button(
         1.,
         1.,
         COLOR_TEAL,
         COLOR_TEAL,
     );
     );
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     layer.link(node.clone());
     layer.link(node.clone());
 
 
     // Button label text (if provided)
     // Button label text (if provided)
@@ -391,7 +433,13 @@ pub async fn create_bottom_button(
         label_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
         label_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
         let label_node = label_node
         let label_node = label_node
             .setup(|me| {
             .setup(|me| {
-                Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone())
+                Text::new(
+                    me,
+                    window_scale.clone(),
+                    app.renderer.clone(),
+                    i18n_fish.clone(),
+                    app.redraw_trigger.clone(),
+                )
             })
             })
             .await;
             .await;
         layer.link(label_node);
         layer.link(label_node);
@@ -408,7 +456,8 @@ pub async fn create_bottom_button(
     prop.set_expr(atom, Role::App, 2, code).unwrap();
     prop.set_expr(atom, Role::App, 2, code).unwrap();
     prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
     prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
 
 
-    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let node =
+        node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer.link(node.clone());
     layer.link(node.clone());
     node
     node
 }
 }
@@ -446,7 +495,9 @@ pub async fn create_bottom_button_with_states(
         1.,
         1.,
         COLOR_TEAL,
         COLOR_TEAL,
     );
     );
-    let bg_valid = bg_valid.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let bg_valid = bg_valid
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     layer.link(bg_valid.clone());
     layer.link(bg_valid.clone());
 
 
     // Button bg (grey outline - invalid state)
     // Button bg (grey outline - invalid state)
@@ -469,7 +520,9 @@ pub async fn create_bottom_button_with_states(
         1.,
         1.,
         [0.5, 0.5, 0.5, 1.],
         [0.5, 0.5, 0.5, 1.],
     );
     );
-    let bg_invalid = bg_invalid.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let bg_invalid = bg_invalid
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     layer.link(bg_invalid.clone());
     layer.link(bg_invalid.clone());
 
 
     // Button label text
     // Button label text
@@ -501,7 +554,15 @@ pub async fn create_bottom_button_with_states(
     }
     }
     label_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     label_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     let label_node = label_node
     let label_node = label_node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     layer.link(label_node.clone());
     layer.link(label_node.clone());
 
 
@@ -516,7 +577,8 @@ pub async fn create_bottom_button_with_states(
     prop.set_expr(atom, Role::App, 2, code).unwrap();
     prop.set_expr(atom, Role::App, 2, code).unwrap();
     prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
     prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
 
 
-    let btn = btn.setup(|me| Button::new(me, app.renderer.clone())).await;
+    let btn =
+        btn.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     layer.link(btn.clone());
     layer.link(btn.clone());
 
 
     (btn, bg_valid, bg_invalid, label_node)
     (btn, bg_valid, bg_invalid, label_node)
@@ -583,7 +645,9 @@ pub async fn create_tooltip(
         1.,
         1.,
         text_color,
         text_color,
     );
     );
-    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    let node = node
+        .setup(|me| VectorArt::new(me, shape, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     tooltip_layer.link(node);
     tooltip_layer.link(node);
 
 
     // Create text
     // Create text
@@ -603,7 +667,15 @@ pub async fn create_tooltip(
     prop.set_f32(atom, Role::App, 3, text_color[3]).unwrap();
     prop.set_f32(atom, Role::App, 3, text_color[3]).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     let node = node
     let node = node
-        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+                app.redraw_trigger.clone(),
+            )
+        })
         .await;
         .await;
     tooltip_layer.link(node);
     tooltip_layer.link(node);
 
 

+ 25 - 1
bin/app/src/gfx/anim.rs

@@ -40,6 +40,11 @@ pub(super) struct GfxSeqAnim {
     /// Timer between frames
     /// Timer between frames
     timer: std::time::Instant,
     timer: std::time::Instant,
     current_idx: usize,
     current_idx: usize,
+    /// While `Some` and unexpired, the anim holds `current_idx` instead of
+    /// advancing. Used for e.g. pausing cursor blink while typing: the app
+    /// holds the visible frame for an idle duration and the anim resumes
+    /// ticking on its own afterwards, with no app-side timed commits.
+    paused_until: Option<std::time::Instant>,
     pub(super) is_visible: bool,
     pub(super) is_visible: bool,
 }
 }
 
 
@@ -51,6 +56,7 @@ impl GfxSeqAnim {
             frames,
             frames,
             timer: std::time::Instant::now(),
             timer: std::time::Instant::now(),
             current_idx: 0,
             current_idx: 0,
+            paused_until: None,
             is_visible: false,
             is_visible: false,
         }
         }
     }
     }
@@ -64,7 +70,7 @@ impl GfxSeqAnim {
     ) {
     ) {
         assert!(frame_idx < self.frames.len());
         assert!(frame_idx < self.frames.len());
         let duration = std::time::Duration::from_millis(frame.duration as u64);
         let duration = std::time::Duration::from_millis(frame.duration as u64);
-        let dc = frame.dc.compile(textures, buffers, 0);
+        let dc = frame.dc.compile(textures, buffers);
         self.frames[frame_idx] = Some(GfxFrame { duration, dc });
         self.frames[frame_idx] = Some(GfxFrame { duration, dc });
         //t!("got frame {frame_idx}");
         //t!("got frame {frame_idx}");
     }
     }
@@ -86,7 +92,25 @@ impl GfxSeqAnim {
         Some(curr_frame.dc)
         Some(curr_frame.dc)
     }
     }
 
 
+    /// Hold `frame_idx` for `duration_ms`, then resume ticking from there.
+    /// The timer restarts when the pause expires, so the held frame gets a
+    /// full frame duration before advancing.
+    pub fn hold(&mut self, frame_idx: usize, duration_ms: u64) {
+        assert!(frame_idx < self.frames.len());
+        self.current_idx = frame_idx;
+        self.timer = std::time::Instant::now();
+        let until = std::time::Instant::now() + std::time::Duration::from_millis(duration_ms);
+        self.paused_until = Some(until);
+    }
+
     fn increment(&mut self) {
     fn increment(&mut self) {
+        if let Some(until) = self.paused_until {
+            if std::time::Instant::now() < until {
+                return
+            }
+            self.paused_until = None;
+        }
+
         // One shot anims dont loop
         // One shot anims dont loop
         if self.oneshot && self.current_idx + 1 == self.frames.len() {
         if self.oneshot && self.current_idx + 1 == self.frames.len() {
             return
             return

+ 41 - 102
bin/app/src/gfx/api.rs

@@ -17,7 +17,7 @@
  */
  */
 
 
 use std::{
 use std::{
-    cell::{Cell, UnsafeCell},
+    cell::Cell,
     sync::{
     sync::{
         atomic::{AtomicU32, Ordering},
         atomic::{AtomicU32, Ordering},
         Arc,
         Arc,
@@ -25,13 +25,10 @@ use std::{
 };
 };
 
 
 use super::{
 use super::{
-    anim::Frame as AnimFrame, AnimId, BufferId, DebugTag, DrawCall, Stage, TextureFormat,
-    TextureId, Vertex,
-};
-use crate::{
-    prop::{BatchGuardId, PropertyAtomicGuard},
-    util::unixtime,
+    anim::Frame as AnimFrame, AnimId, BufferId, DebugTag, DrawCall, TextureFormat, TextureId,
+    Vertex,
 };
 };
+use crate::prop::PropertyAtomicGuard;
 
 
 pub type EpochIndex = u32;
 pub type EpochIndex = u32;
 type DcId = u64;
 type DcId = u64;
@@ -98,6 +95,13 @@ impl ManagedSeqAnim {
         assert!(frame_idx < self.frames_len);
         assert!(frame_idx < self.frames_len);
         self.renderer.update_unmanaged_anim(self.id, frame_idx, frame, self.epoch, self.tag);
         self.renderer.update_unmanaged_anim(self.id, frame_idx, frame, self.epoch, self.tag);
     }
     }
+
+    /// Hold `frame_idx` for `duration_ms`, then resume ticking. Re-issuing
+    /// re-arms the hold. No timed commit happens app-side; the anim's own
+    /// frame ticks implement the resume.
+    pub fn pause(&self, frame_idx: usize, duration_ms: u64) {
+        self.renderer.pause_unmanaged_anim(self.id, frame_idx, duration_ms, self.epoch, self.tag);
+    }
 }
 }
 
 
 impl Drop for ManagedSeqAnim {
 impl Drop for ManagedSeqAnim {
@@ -112,8 +116,8 @@ impl std::fmt::Debug for ManagedSeqAnim {
     }
     }
 }
 }
 
 
-/// This trait allows `Renderer` and `RendererSync` to be used interchangeably helping with
-/// code reuse.
+/// The async renderer API: allocates GPU resources and modifies the render
+/// tree by sending methods to the gfx Stage thread.
 pub trait RenderApi {
 pub trait RenderApi {
     /// Allocate a texture on the gfx card
     /// Allocate a texture on the gfx card
     fn new_texture(
     fn new_texture(
@@ -132,7 +136,7 @@ pub trait RenderApi {
     fn new_index_buffer(&self, indices: Vec<u16>, tag: DebugTag) -> ManagedBufferPtr;
     fn new_index_buffer(&self, indices: Vec<u16>, tag: DebugTag) -> ManagedBufferPtr;
 
 
     /// Modify render tree.
     /// Modify render tree.
-    fn replace_draw_calls(&self, batch_id: Option<BatchGuardId>, dcs: Vec<(DcId, DrawCall)>);
+    fn replace_draw_calls(&self, dcs: Vec<(DcId, DrawCall)>);
 }
 }
 
 
 #[derive(Clone)]
 #[derive(Clone)]
@@ -250,31 +254,33 @@ impl Renderer {
         self.send_with_epoch(method, epoch);
         self.send_with_epoch(method, epoch);
     }
     }
 
 
-    fn delete_unmanaged_anim(&self, anim: AnimId, epoch: EpochIndex, tag: DebugTag) {
-        let method = GraphicsMethod::DeleteSeqAnim((anim, tag));
+    pub fn pause_unmanaged_anim(
+        &self,
+        anim: AnimId,
+        frame_idx: usize,
+        duration_ms: u64,
+        epoch: EpochIndex,
+        tag: DebugTag,
+    ) {
+        let method = GraphicsMethod::PauseSeqAnim { id: anim, frame_idx, duration_ms, tag };
         self.send_with_epoch(method, epoch);
         self.send_with_epoch(method, epoch);
-    }
 
 
-    fn start_batch(&self, batch_id: BatchGuardId, tag: DebugTag) {
-        let method = GraphicsMethod::StartBatch { batch_id, tag };
-        self.send(method);
-    }
-    fn end_batch(&self, batch_id: BatchGuardId) {
-        let timest = unixtime();
-        let method = GraphicsMethod::EndBatch { batch_id, timest };
-        self.send(method);
-
-        // Force an update
+        // Force an update so the pause takes effect promptly on Android
         #[cfg(target_os = "android")]
         #[cfg(target_os = "android")]
         miniquad::window::schedule_update();
         miniquad::window::schedule_update();
     }
     }
 
 
-    pub fn make_guard(&self, debug_str: Option<&'static str>) -> PropertyAtomicGuard {
-        let r = self.clone();
-        let start_batch = Box::new(move |bid| r.start_batch(bid, debug_str));
-        let r = self.clone();
-        let end_batch = Box::new(move |bid| r.end_batch(bid));
-        PropertyAtomicGuard::new(start_batch, end_batch)
+    fn delete_unmanaged_anim(&self, anim: AnimId, epoch: EpochIndex, tag: DebugTag) {
+        let method = GraphicsMethod::DeleteSeqAnim((anim, tag));
+        self.send_with_epoch(method, epoch);
+    }
+
+    /// Property transactions only: notifications are deferred until the
+    /// guard drops. Since the draw-pass migration there is no gfx-side
+    /// batching anymore — draw commits are single immediate messages —
+    /// so the guard no longer opens or closes renderer batches.
+    pub fn make_guard(&self, _debug_str: Option<&'static str>) -> PropertyAtomicGuard {
+        PropertyAtomicGuard::none()
     }
     }
 }
 }
 
 
@@ -300,8 +306,8 @@ impl RenderApi for Renderer {
         Arc::new(ManagedBuffer { id, epoch, renderer: self.clone(), tag, buftype: 1 })
         Arc::new(ManagedBuffer { id, epoch, renderer: self.clone(), tag, buftype: 1 })
     }
     }
 
 
-    fn replace_draw_calls(&self, batch_id: Option<BatchGuardId>, dcs: Vec<(DcId, DrawCall)>) {
-        let method = GraphicsMethod::ReplaceGfxDrawCalls { batch_id, dcs };
+    fn replace_draw_calls(&self, dcs: Vec<(DcId, DrawCall)>) {
+        let method = GraphicsMethod::ReplaceGfxDrawCalls { dcs };
         self.send(method);
         self.send(method);
 
 
         // I'm not sure whether we need this. Anyway its not fully reliable either since
         // I'm not sure whether we need this. Anyway its not fully reliable either since
@@ -321,10 +327,9 @@ pub enum GraphicsMethod {
     DeleteBuffer((BufferId, DebugTag, u8)),
     DeleteBuffer((BufferId, DebugTag, u8)),
     NewSeqAnim { id: AnimId, frames_len: usize, oneshot: bool, tag: DebugTag },
     NewSeqAnim { id: AnimId, frames_len: usize, oneshot: bool, tag: DebugTag },
     UpdateSeqAnim { id: AnimId, frame_idx: usize, frame: AnimFrame, tag: DebugTag },
     UpdateSeqAnim { id: AnimId, frame_idx: usize, frame: AnimFrame, tag: DebugTag },
+    PauseSeqAnim { id: AnimId, frame_idx: usize, duration_ms: u64, tag: DebugTag },
     DeleteSeqAnim((AnimId, DebugTag)),
     DeleteSeqAnim((AnimId, DebugTag)),
-    ReplaceGfxDrawCalls { batch_id: Option<BatchGuardId>, dcs: Vec<(DcId, DrawCall)> },
-    StartBatch { batch_id: BatchGuardId, tag: DebugTag },
-    EndBatch { batch_id: BatchGuardId, timest: u64 },
+    ReplaceGfxDrawCalls { dcs: Vec<(DcId, DrawCall)> },
     Noop,
     Noop,
 }
 }
 
 
@@ -338,13 +343,9 @@ impl std::fmt::Debug for GraphicsMethod {
             Self::DeleteBuffer(_) => write!(f, "DeleteBuffer"),
             Self::DeleteBuffer(_) => write!(f, "DeleteBuffer"),
             Self::NewSeqAnim { .. } => write!(f, "NewSeqAnim"),
             Self::NewSeqAnim { .. } => write!(f, "NewSeqAnim"),
             Self::UpdateSeqAnim { .. } => write!(f, "UpdateSeqAnim"),
             Self::UpdateSeqAnim { .. } => write!(f, "UpdateSeqAnim"),
+            Self::PauseSeqAnim { .. } => write!(f, "PauseSeqAnim"),
             Self::DeleteSeqAnim(_) => write!(f, "DeleteSeqAnim"),
             Self::DeleteSeqAnim(_) => write!(f, "DeleteSeqAnim"),
-            Self::ReplaceGfxDrawCalls { batch_id, dcs: _ } => match batch_id {
-                Some(bid) => write!(f, "ReplaceGfxDrawCalls({bid})"),
-                None => write!(f, "ReplaceGfxDrawCalls(immediate)"),
-            },
-            Self::StartBatch { batch_id, tag } => write!(f, "StartBatch({batch_id}, {tag:?})"),
-            Self::EndBatch { batch_id, timest } => write!(f, "EndBatch({batch_id}, {timest})"),
+            Self::ReplaceGfxDrawCalls { dcs: _ } => write!(f, "ReplaceGfxDrawCalls"),
             Self::Noop => write!(f, "Noop"),
             Self::Noop => write!(f, "Noop"),
         }
         }
     }
     }
@@ -355,65 +356,3 @@ impl Default for GraphicsMethod {
         GraphicsMethod::Noop
         GraphicsMethod::Noop
     }
     }
 }
 }
-
-pub struct RendererSync<'a> {
-    stage: UnsafeCell<&'a mut Stage>,
-}
-
-impl<'a> RendererSync<'a> {
-    pub(super) fn new(stage: &'a mut Stage) -> Self {
-        Self { stage: UnsafeCell::new(stage) }
-    }
-
-    // SAFETY: The '&a mut Stage' reference stored in UnsafeCell is guaranteed to be unique
-    // for the lifetime 'a of this RendererSync. No other references to this Stage can exist
-    // while RendererSync is alive, due to Rust's borrow checker rules on mutable references.
-    // Therefore, it's safe to return &mut Stage through &self.
-    fn stage(&self) -> &mut Stage {
-        unsafe { &mut *self.stage.get() }
-    }
-}
-
-impl RenderApi for RendererSync<'_> {
-    fn new_texture(
-        &self,
-        width: u16,
-        height: u16,
-        data: Vec<u8>,
-        fmt: TextureFormat,
-        tag: DebugTag,
-    ) -> ManagedTexturePtr {
-        let stage = self.stage();
-        let renderer = stage.renderer.clone();
-        let id = NEXT_TEXTURE_ID.fetch_add(1, Ordering::Relaxed);
-        stage.method_new_texture(width, height, &data, fmt, id);
-        Arc::new(ManagedTexture { id, epoch: 0, renderer, tag })
-    }
-
-    fn new_vertex_buffer(&self, verts: Vec<Vertex>, tag: DebugTag) -> ManagedBufferPtr {
-        let stage = self.stage();
-        let renderer = stage.renderer.clone();
-        let id = NEXT_BUFFER_ID.fetch_add(1, Ordering::Relaxed);
-        stage.method_new_vertex_buffer(&verts, id);
-        Arc::new(ManagedBuffer { id, epoch: 0, renderer, tag, buftype: 0 })
-    }
-
-    fn new_index_buffer(&self, indices: Vec<u16>, tag: DebugTag) -> ManagedBufferPtr {
-        let stage = self.stage();
-        let renderer = stage.renderer.clone();
-        let id = NEXT_BUFFER_ID.fetch_add(1, Ordering::Relaxed);
-        stage.method_new_index_buffer(&indices, id);
-        Arc::new(ManagedBuffer { id, epoch: 0, renderer, tag, buftype: 1 })
-    }
-
-    /// Draw calls (no batching). The batch ID is ignored here.
-    fn replace_draw_calls(&self, _: Option<BatchGuardId>, dcs: Vec<(DcId, DrawCall)>) {
-        let stage = self.stage();
-        let timest = unixtime();
-        stage.apply_draw_calls(timest, dcs);
-
-        // Force an update
-        #[cfg(target_os = "android")]
-        miniquad::window::schedule_update();
-    }
-}

+ 1 - 1
bin/app/src/gfx/linalg.rs

@@ -165,7 +165,7 @@ impl Div<f32> for Point {
     }
     }
 }
 }
 
 
-#[derive(Clone, Copy, SerialEncodable, SerialDecodable)]
+#[derive(Clone, Copy, PartialEq, SerialEncodable, SerialDecodable)]
 pub struct Rectangle {
 pub struct Rectangle {
     pub x: f32,
     pub x: f32,
     pub y: f32,
     pub y: f32,

+ 32 - 124
bin/app/src/gfx/mod.rs

@@ -28,7 +28,7 @@ use miniquad::{
     TextureWrap, TouchPhase, UniformType,
     TextureWrap, TouchPhase, UniformType,
 };
 };
 use std::{
 use std::{
-    collections::{HashMap, HashSet},
+    collections::HashMap,
     fs::File,
     fs::File,
     io::Write,
     io::Write,
     path::PathBuf,
     path::PathBuf,
@@ -43,7 +43,7 @@ use anim::{Frame as AnimFrame, GfxSeqAnim};
 mod api;
 mod api;
 pub use api::{
 pub use api::{
     EpochIndex, GraphicsMethod, ManagedBuffer, ManagedBufferPtr, ManagedSeqAnim, ManagedSeqAnimPtr,
     EpochIndex, GraphicsMethod, ManagedBuffer, ManagedBufferPtr, ManagedSeqAnim, ManagedSeqAnimPtr,
-    ManagedTexture, ManagedTexturePtr, RenderApi, Renderer, RendererSync,
+    ManagedTexture, ManagedTexturePtr, RenderApi, Renderer,
 };
 };
 mod ev;
 mod ev;
 pub use ev::{
 pub use ev::{
@@ -60,9 +60,8 @@ pub use linalg::{Dimension, Point, Rectangle, Segment, Vector};
 mod shader;
 mod shader;
 
 
 use crate::{
 use crate::{
-    prop::{BatchGuardId, PropertyAtomicGuard},
+    prop::PropertyAtomicGuard,
     scene::{Pimpl, SceneNodePtr},
     scene::{Pimpl, SceneNodePtr},
-    util::unixtime,
     GOD,
     GOD,
 };
 };
 
 
@@ -286,7 +285,6 @@ impl DrawCall {
         self,
         self,
         textures: &HashMap<TextureId, miniquad::TextureId>,
         textures: &HashMap<TextureId, miniquad::TextureId>,
         buffers: &HashMap<BufferId, miniquad::BufferId>,
         buffers: &HashMap<BufferId, miniquad::BufferId>,
-        timest: Timestamp,
     ) -> GfxDrawCall {
     ) -> GfxDrawCall {
         GfxDrawCall {
         GfxDrawCall {
             instrs: self
             instrs: self
@@ -296,7 +294,6 @@ impl DrawCall {
                 .collect(),
                 .collect(),
             dcs: self.dcs,
             dcs: self.dcs,
             z_index: self.z_index,
             z_index: self.z_index,
-            timest,
         }
         }
     }
     }
 }
 }
@@ -329,7 +326,6 @@ struct GfxDrawCall {
     instrs: Vec<GfxDrawInstruction>,
     instrs: Vec<GfxDrawInstruction>,
     dcs: Vec<DcId>,
     dcs: Vec<DcId>,
     z_index: u32,
     z_index: u32,
-    timest: Timestamp,
 }
 }
 
 
 struct OverlayDefer {
 struct OverlayDefer {
@@ -588,7 +584,6 @@ impl<'a> RenderContext<'a> {
     }
     }
 }
 }
 
 
-type Timestamp = u64;
 type DcId = u64;
 type DcId = u64;
 
 
 struct Stage {
 struct Stage {
@@ -598,10 +593,6 @@ struct Stage {
     loaded_pipelines: [Pipeline; 2],
     loaded_pipelines: [Pipeline; 2],
     white_texture: miniquad::TextureId,
     white_texture: miniquad::TextureId,
     draw_calls: HashMap<DcId, GfxDrawCall>,
     draw_calls: HashMap<DcId, GfxDrawCall>,
-    pending_batches: HashMap<BatchGuardId, Vec<GraphicsMethod>>,
-    /// When dropping batches, we add to this set so that we keep track
-    /// of the internal state's correctness.
-    dropped_batches: Box<HashSet<BatchGuardId>>,
 
 
     textures: Box<HashMap<TextureId, miniquad::TextureId>>,
     textures: Box<HashMap<TextureId, miniquad::TextureId>>,
     buffers: Box<HashMap<BufferId, miniquad::BufferId>>,
     buffers: Box<HashMap<BufferId, miniquad::BufferId>>,
@@ -651,10 +642,8 @@ impl Stage {
             white_texture,
             white_texture,
             draw_calls: HashMap::from([(
             draw_calls: HashMap::from([(
                 0,
                 0,
-                GfxDrawCall { instrs: vec![], dcs: vec![], z_index: 0, timest: 0 },
+                GfxDrawCall { instrs: vec![], dcs: vec![], z_index: 0 },
             )]),
             )]),
-            pending_batches: HashMap::new(),
-            dropped_batches: Box::new(HashSet::new()),
 
 
             textures: Box::new(HashMap::new()),
             textures: Box::new(HashMap::new()),
             buffers: Box::new(HashMap::new()),
             buffers: Box::new(HashMap::new()),
@@ -673,7 +662,6 @@ impl Stage {
         self_.pruner.textures = &*self_.textures as *const _;
         self_.pruner.textures = &*self_.textures as *const _;
         self_.pruner.buffers = &*self_.buffers as *const _;
         self_.pruner.buffers = &*self_.buffers as *const _;
         self_.pruner.anims = &*self_.anims as *const _;
         self_.pruner.anims = &*self_.anims as *const _;
-        self_.pruner.dropped_batches = &mut *self_.dropped_batches as *mut _;
         self_
         self_
     }
     }
 
 
@@ -697,60 +685,13 @@ impl Stage {
             GraphicsMethod::UpdateSeqAnim { id, frame_idx, frame, tag: _ } => {
             GraphicsMethod::UpdateSeqAnim { id, frame_idx, frame, tag: _ } => {
                 self.method_update_anim(*id, *frame_idx, frame.clone())
                 self.method_update_anim(*id, *frame_idx, frame.clone())
             }
             }
-            GraphicsMethod::DeleteSeqAnim((ganim_id, _)) => self.method_delete_anim(*ganim_id),
-            GraphicsMethod::ReplaceGfxDrawCalls { batch_id, ref mut dcs } => {
-                match batch_id {
-                    Some(bid) => {
-                        //let debug_strs: Vec<_> = dcs.iter().map(|(_, dc)| dc.debug_str).collect();
-                        //t!("Commit dc to {bid}: {debug_strs:?}");
-                        if self.dropped_batches.contains(&bid) {
-                            t!("Discarding ReplaceGfxDrawCalls from dropped {bid}");
-                            return
-                        }
-                        let Some(batch) = self.pending_batches.get_mut(&bid) else {
-                            panic!("unknown batch {bid}")
-                        };
-                        let method = std::mem::take(&mut method);
-                        batch.push(method);
-                    }
-                    None => {
-                        // Process immediately without batching
-                        let timest = unixtime();
-                        let dcs = std::mem::take(dcs);
-                        self.method_replace_draw_calls(timest, dcs);
-                    }
-                }
+            GraphicsMethod::PauseSeqAnim { id, frame_idx, duration_ms, tag: _ } => {
+                self.method_pause_anim(*id, *frame_idx, *duration_ms)
             }
             }
-            GraphicsMethod::StartBatch { batch_id, tag } => {
-                if DEBUG_GFXAPI {
-                    t!("Start batch {batch_id}: {tag:?}");
-                }
-                if !self.pending_batches.insert(*batch_id, vec![]).is_none() {
-                    panic!("batch {batch_id} already open!")
-                }
-            }
-            GraphicsMethod::EndBatch { batch_id, timest } => {
-                if self.dropped_batches.remove(batch_id) {
-                    if DEBUG_GFXAPI {
-                        t!("End batch {batch_id} was dropped");
-                    }
-                    return
-                }
-                if DEBUG_GFXAPI {
-                    t!("End batch {batch_id}");
-                }
-                let Some(batch) = self.pending_batches.remove(batch_id) else {
-                    panic!("unknown batch {batch_id}")
-                };
-                for mut method in batch {
-                    match &mut method {
-                        GraphicsMethod::ReplaceGfxDrawCalls { batch_id: _, dcs } => {
-                            let dcs = std::mem::take(dcs);
-                            self.method_replace_draw_calls(*timest, dcs)
-                        }
-                        _ => panic!("unexpected method in batch!"),
-                    }
-                }
+            GraphicsMethod::DeleteSeqAnim((ganim_id, _)) => self.method_delete_anim(*ganim_id),
+            GraphicsMethod::ReplaceGfxDrawCalls { ref mut dcs } => {
+                let dcs = std::mem::take(dcs);
+                self.method_replace_draw_calls(dcs);
             }
             }
             GraphicsMethod::Noop => panic!("noop"),
             GraphicsMethod::Noop => panic!("noop"),
         }
         }
@@ -876,30 +817,27 @@ impl Stage {
             d!("Invoked method: delete_anim({} => {:?})", gfx_anim_id, anim);
             d!("Invoked method: delete_anim({} => {:?})", gfx_anim_id, anim);
         }
         }
     }
     }
-    fn method_replace_draw_calls(&mut self, batch_timest: Timestamp, dcs: Vec<(DcId, DrawCall)>) {
+    pub(self) fn method_pause_anim(
+        &mut self,
+        gfx_anim_id: AnimId,
+        frame_idx: usize,
+        duration_ms: u64,
+    ) {
+        let Some(anim) = self.anims.get_mut(&gfx_anim_id) else {
+            panic!("couldn't find anim {gfx_anim_id}");
+        };
+        if DEBUG_GFXAPI {
+            d!("Invoked method: pause_anim({gfx_anim_id}[{frame_idx}] for {duration_ms}ms)");
+        }
+        anim.hold(frame_idx, duration_ms);
+    }
+    fn method_replace_draw_calls(&mut self, dcs: Vec<(DcId, DrawCall)>) {
         if DEBUG_GFXAPI {
         if DEBUG_GFXAPI {
             d!("Invoked method: replace_draw_calls({:?})", dcs);
             d!("Invoked method: replace_draw_calls({:?})", dcs);
         }
         }
 
 
-        // Phase 1: Check for conflicts with newer batches
-        // If any draw call in this batch belongs to an older batch than the existing one,
-        // reject the entire batch to maintain atomicity
-        for (key, _) in &dcs {
-            if let Some(old_val) = self.draw_calls.get(key) {
-                if old_val.timest > batch_timest {
-                    // Entire batch is stale, reject all
-                    t!("Rejected stale batch {batch_timest}: conflict with newer batch {} on {key}", old_val.timest);
-                    return;
-                }
-            }
-        }
-
-        // Phase 2: Apply entire batch atomically
-        self.apply_draw_calls(batch_timest, dcs)
-    }
-    pub(self) fn apply_draw_calls(&mut self, batch_timest: Timestamp, dcs: Vec<(DcId, DrawCall)>) {
         for (key, val) in dcs {
         for (key, val) in dcs {
-            let val = val.compile(&self.textures, &self.buffers, batch_timest);
+            let val = val.compile(&self.textures, &self.buffers);
 
 
             // Insert/replace draw call
             // Insert/replace draw call
             self.draw_calls.insert(key, val);
             self.draw_calls.insert(key, val);
@@ -936,10 +874,9 @@ impl Stage {
     #[instrument(skip_all, target = "gfx::pruner")]
     #[instrument(skip_all, target = "gfx::pruner")]
     fn prime_screen(&mut self) {
     fn prime_screen(&mut self) {
         let methods = self.pruner.recv_all();
         let methods = self.pruner.recv_all();
-        assert!(self.pending_batches.is_empty());
         // Process all cached methods by the pruner from while the screen was off.
         // Process all cached methods by the pruner from while the screen was off.
         for method in methods {
         for method in methods {
-            // We discard batches here but process_method uses them so implement this
+            // We discard draw calls here but process_method uses them so implement this
             // workaround.
             // workaround.
             match method {
             match method {
                 GraphicsMethod::NewTexture(_) |
                 GraphicsMethod::NewTexture(_) |
@@ -949,11 +886,10 @@ impl Stage {
                 GraphicsMethod::DeleteBuffer(_) |
                 GraphicsMethod::DeleteBuffer(_) |
                 GraphicsMethod::NewSeqAnim { .. } |
                 GraphicsMethod::NewSeqAnim { .. } |
                 GraphicsMethod::UpdateSeqAnim { .. } |
                 GraphicsMethod::UpdateSeqAnim { .. } |
+                GraphicsMethod::PauseSeqAnim { .. } |
                 GraphicsMethod::DeleteSeqAnim(_) => self.process_method(method),
                 GraphicsMethod::DeleteSeqAnim(_) => self.process_method(method),
 
 
-                GraphicsMethod::ReplaceGfxDrawCalls { .. } |
-                GraphicsMethod::StartBatch { .. } |
-                GraphicsMethod::EndBatch { .. } => {
+                GraphicsMethod::ReplaceGfxDrawCalls { .. } => {
                     panic!("unsupported pruned methods should be dropped!")
                     panic!("unsupported pruned methods should be dropped!")
                 }
                 }
 
 
@@ -965,22 +901,6 @@ impl Stage {
         let (width, height) = miniquad::window::screen_size();
         let (width, height) = miniquad::window::screen_size();
         self.event_pub.notify_resize(Dimension::from([width, height]));
         self.event_pub.notify_resize(Dimension::from([width, height]));
     }
     }
-
-    fn close_pending_batches(&mut self) {
-        // Immediately apply any pending batches when the screen is switched off
-        let batch_ids: Vec<_> = self.pending_batches.keys().cloned().collect();
-        if !batch_ids.is_empty() {
-            t!("Force closing pending batches: {batch_ids:?}");
-        }
-
-        for batch_id in batch_ids {
-            self.process_method(GraphicsMethod::EndBatch { batch_id, timest: unixtime() });
-            if !self.dropped_batches.insert(batch_id) {
-                panic!("dropped batch {batch_id} already exits!");
-            }
-        }
-        assert!(self.pending_batches.is_empty());
-    }
 }
 }
 
 
 #[derive(Copy, Clone, Debug, PartialEq)]
 #[derive(Copy, Clone, Debug, PartialEq)]
@@ -1032,14 +952,10 @@ impl EventHandler for Stage {
 
 
         match self.screen_state {
         match self.screen_state {
             ScreenState::SwitchOff => {
             ScreenState::SwitchOff => {
-                self.close_pending_batches();
-
                 // Screen is off so collect all methods into the pruner
                 // Screen is off so collect all methods into the pruner
                 self.pruner.drain(&self.method_recv);
                 self.pruner.drain(&self.method_recv);
             }
             }
             ScreenState::Off => {
             ScreenState::Off => {
-                assert!(self.pending_batches.is_empty());
-
                 // Screen is off so collect all methods into the pruner
                 // Screen is off so collect all methods into the pruner
                 self.pruner.drain(&self.method_recv);
                 self.pruner.drain(&self.method_recv);
             }
             }
@@ -1159,17 +1075,11 @@ impl EventHandler for Stage {
             self.window_node = god.app.sg_root.lookup_node("/window");
             self.window_node = god.app.sg_root.lookup_node("/window");
         }
         }
 
 
-        // Clone window_node to avoid borrow conflict with RenderApiSync
-        let window_node = self.window_node.clone();
-
-        // Create RenderApiSync for direct graphics operations
-        let renderer_sync = RendererSync::new(self);
-
-        // Direct call to Window's handle_touch_event_sync
-        if let Some(window_node) = &window_node {
+        // Direct call to Window's handle_touch_sync
+        if let Some(window_node) = &self.window_node {
             match window_node.pimpl() {
             match window_node.pimpl() {
                 Pimpl::Window(win) => {
                 Pimpl::Window(win) => {
-                    if win.handle_touch_sync(&renderer_sync, phase, id, pos) {
+                    if win.handle_touch_sync(phase, id, pos) {
                         return
                         return
                     }
                     }
                 }
                 }
@@ -1177,8 +1087,6 @@ impl EventHandler for Stage {
             }
             }
         }
         }
 
 
-        drop(renderer_sync);
-
         self.event_pub.notify_touch(phase, id, pos);
         self.event_pub.notify_touch(phase, id, pos);
     }
     }
 
 

+ 9 - 25
bin/app/src/gfx/prune.rs

@@ -16,12 +16,11 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
-use std::collections::{HashMap, HashSet};
+use std::collections::HashMap;
 
 
 use super::{
 use super::{
     anim::GfxSeqAnim, AnimId, BufferId, EpochIndex, GraphicsMethod, TextureId, DEBUG_GFXAPI,
     anim::GfxSeqAnim, AnimId, BufferId, EpochIndex, GraphicsMethod, TextureId, DEBUG_GFXAPI,
 };
 };
-use crate::prop::BatchGuardId;
 
 
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "gfx::prune", $($arg)*) }; }
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "gfx::prune", $($arg)*) }; }
 
 
@@ -45,14 +44,13 @@ pub struct PruneMethodHeap {
     /// Existing anim updates
     /// Existing anim updates
     anim_updates: HashMap<AnimId, HashMap<usize, GraphicsMethod>>,
     anim_updates: HashMap<AnimId, HashMap<usize, GraphicsMethod>>,
     /// Existing anim deletes
     /// Existing anim deletes
-    anim_deletes: HashSet<AnimId>,
+    anim_deletes: HashMap<AnimId, ()>,
 
 
     epoch: EpochIndex,
     epoch: EpochIndex,
 
 
     pub textures: *const HashMap<TextureId, miniquad::TextureId>,
     pub textures: *const HashMap<TextureId, miniquad::TextureId>,
     pub buffers: *const HashMap<BufferId, miniquad::BufferId>,
     pub buffers: *const HashMap<BufferId, miniquad::BufferId>,
     pub anims: *const HashMap<AnimId, GfxSeqAnim>,
     pub anims: *const HashMap<AnimId, GfxSeqAnim>,
-    pub dropped_batches: *mut HashSet<BatchGuardId>,
 }
 }
 
 
 impl PruneMethodHeap {
 impl PruneMethodHeap {
@@ -63,12 +61,11 @@ impl PruneMethodHeap {
             del: vec![],
             del: vec![],
             new_anim: HashMap::new(),
             new_anim: HashMap::new(),
             anim_updates: HashMap::new(),
             anim_updates: HashMap::new(),
-            anim_deletes: HashSet::new(),
+            anim_deletes: HashMap::new(),
             epoch,
             epoch,
             textures: std::ptr::null(),
             textures: std::ptr::null(),
             buffers: std::ptr::null(),
             buffers: std::ptr::null(),
             anims: std::ptr::null(),
             anims: std::ptr::null(),
-            dropped_batches: std::ptr::null_mut(),
         }
         }
     }
     }
 
 
@@ -160,26 +157,17 @@ impl PruneMethodHeap {
             GraphicsMethod::DeleteSeqAnim((id, _)) => {
             GraphicsMethod::DeleteSeqAnim((id, _)) => {
                 if self.new_anim.remove(id).is_some() {
                 if self.new_anim.remove(id).is_some() {
                 } else if self.anims().contains_key(id) {
                 } else if self.anims().contains_key(id) {
-                    self.anim_deletes.insert(*id);
+                    self.anim_deletes.insert(*id, ());
                     self.anim_updates.remove(id);
                     self.anim_updates.remove(id);
                 } else {
                 } else {
                     panic!("DeleteSeqAnim for unknown anim {id}");
                     panic!("DeleteSeqAnim for unknown anim {id}");
                 }
                 }
             }
             }
             GraphicsMethod::ReplaceGfxDrawCalls { .. } => {}
             GraphicsMethod::ReplaceGfxDrawCalls { .. } => {}
-            // Discard batches since we will apply everything all at once anyway
-            // once the screen is switched on.
-            GraphicsMethod::StartBatch { batch_id, tag } => {
-                t!("Pruner drop start batch {batch_id} debug={tag:?}");
-                if !self.dropped_batches().insert(*batch_id) {
-                    panic!("dropped batch {batch_id} already exits!");
-                }
-            }
-            GraphicsMethod::EndBatch { batch_id, timest: _ } => {
-                t!("Pruner drop end batch {batch_id}");
-                // Should have already been dropped previously
-                assert!(self.dropped_batches().contains(batch_id));
-            }
+            // Pauses are transient holds with an absolute expiry; one that
+            // arrives while the screen is off has (almost certainly) already
+            // served its purpose by the time the screen is on again.
+            GraphicsMethod::PauseSeqAnim { .. } => {}
             GraphicsMethod::Noop => panic!("noop"),
             GraphicsMethod::Noop => panic!("noop"),
         }
         }
     }
     }
@@ -196,10 +184,6 @@ impl PruneMethodHeap {
         assert!(!self.anims.is_null());
         assert!(!self.anims.is_null());
         unsafe { &*self.anims }
         unsafe { &*self.anims }
     }
     }
-    fn dropped_batches(&mut self) -> &mut HashSet<BatchGuardId> {
-        assert!(!self.dropped_batches.is_null());
-        unsafe { &mut *self.dropped_batches }
-    }
 
 
     /// Collect everything now the screen is on
     /// Collect everything now the screen is on
     pub fn recv_all(&mut self) -> Vec<GraphicsMethod> {
     pub fn recv_all(&mut self) -> Vec<GraphicsMethod> {
@@ -259,7 +243,7 @@ impl PruneMethodHeap {
             meth.push(update);
             meth.push(update);
         }
         }
 
 
-        for id in std::mem::take(&mut self.anim_deletes) {
+        for (id, _) in std::mem::take(&mut self.anim_deletes) {
             meth.push(GraphicsMethod::DeleteSeqAnim((id, None)));
             meth.push(GraphicsMethod::DeleteSeqAnim((id, None)));
         }
         }
     }
     }

+ 1 - 1
bin/app/src/prop/mod.rs

@@ -30,7 +30,7 @@ use crate::{
 };
 };
 
 
 mod guard;
 mod guard;
-pub use guard::{BatchGuardId, BatchGuardPtr, PropertyAtomicGuard};
+pub use guard::{BatchGuardPtr, PropertyAtomicGuard};
 mod wrap;
 mod wrap;
 pub use wrap::{
 pub use wrap::{
     PropertyBool, PropertyColor, PropertyDimension, PropertyEnum, PropertyFloat32, PropertyRect,
     PropertyBool, PropertyColor, PropertyDimension, PropertyEnum, PropertyFloat32, PropertyRect,

+ 13 - 28
bin/app/src/ui/button.rs

@@ -27,14 +27,14 @@ use std::sync::{
 use tracing::instrument;
 use tracing::instrument;
 
 
 use crate::{
 use crate::{
-    gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, Renderer, RendererSync},
+    gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, Renderer},
     mesh::MeshBuilder,
     mesh::MeshBuilder,
-    prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyRect, PropertyUint32, Role},
+    prop::{PropertyAtomicGuard, PropertyBool, PropertyRect, PropertyUint32, Role},
     scene::{Pimpl, SceneNodeWeak},
     scene::{Pimpl, SceneNodeWeak},
     ExecutorPtr,
     ExecutorPtr,
 };
 };
 
 
-use super::{DrawUpdate, OnModify, UIObject};
+use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
 
 
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::button", $($arg)*); } }
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::button", $($arg)*); } }
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::button", $($arg)*); } }
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::button", $($arg)*); } }
@@ -46,6 +46,7 @@ pub struct Button {
     node: SceneNodeWeak,
     node: SceneNodeWeak,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     renderer: Renderer,
     renderer: Renderer,
+    redraw: RedrawTrigger,
 
 
     is_active: PropertyBool,
     is_active: PropertyBool,
     rect: PropertyRect,
     rect: PropertyRect,
@@ -55,11 +56,10 @@ pub struct Button {
     dc_key: u64,
     dc_key: u64,
 
 
     mouse_btn_held: AtomicBool,
     mouse_btn_held: AtomicBool,
-    parent_rect: SyncMutex<Option<Rectangle>>,
 }
 }
 
 
 impl Button {
 impl Button {
-    pub async fn new(node: SceneNodeWeak, renderer: Renderer) -> Pimpl {
+    pub async fn new(node: SceneNodeWeak, renderer: Renderer, redraw: RedrawTrigger) -> Pimpl {
         let node_ref = &node.upgrade().unwrap();
         let node_ref = &node.upgrade().unwrap();
         let is_active = PropertyBool::wrap(node_ref, Role::Internal, "is_active", 0).unwrap();
         let is_active = PropertyBool::wrap(node_ref, Role::Internal, "is_active", 0).unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
@@ -71,6 +71,7 @@ impl Button {
             node,
             node,
             tasks: SyncMutex::new(vec![]),
             tasks: SyncMutex::new(vec![]),
             renderer,
             renderer,
+            redraw,
             is_active,
             is_active,
             rect,
             rect,
             priority,
             priority,
@@ -78,22 +79,10 @@ impl Button {
             debug,
             debug,
             dc_key: OsRng.gen(),
             dc_key: OsRng.gen(),
             mouse_btn_held: AtomicBool::new(false),
             mouse_btn_held: AtomicBool::new(false),
-            parent_rect: SyncMutex::new(None),
         });
         });
 
 
         Pimpl::Button(self_)
         Pimpl::Button(self_)
     }
     }
-
-    #[instrument(target = "ui::button")]
-    async fn redraw(self: Arc<Self>, batch: BatchGuardPtr) {
-        let Some(parent_rect) = self.parent_rect.lock().clone() else { return };
-
-        let atom = &mut batch.spawn();
-
-        if let Err(e) = self.rect.eval(atom, &parent_rect) {
-            w!("Rect eval failure in redraw: {e}");
-        }
-    }
 }
 }
 
 
 #[async_trait]
 #[async_trait]
@@ -106,14 +95,18 @@ impl UIObject for Button {
         let me = Arc::downgrade(&self);
         let me = Arc::downgrade(&self);
 
 
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
-        on_modify.when_change(self.rect.prop(), Self::redraw);
+        // Buttons produce no draw output (except the debug outline). The
+        // pass evals the rect for hit-testing; external changes only need
+        // to request a pass. Internal-role eval echoes are skipped.
+        on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
+            self_.redraw.trigger();
+        });
 
 
         *self.tasks.lock() = on_modify.tasks;
         *self.tasks.lock() = on_modify.tasks;
     }
     }
 
 
     fn stop(&self) {
     fn stop(&self) {
         self.tasks.lock().clear();
         self.tasks.lock().clear();
-        *self.parent_rect.lock() = None;
     }
     }
 
 
     #[instrument(target = "ui::button")]
     #[instrument(target = "ui::button")]
@@ -122,8 +115,6 @@ impl UIObject for Button {
         parent_rect: Rectangle,
         parent_rect: Rectangle,
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
-        *self.parent_rect.lock() = Some(parent_rect);
-
         if let Err(e) = self.rect.eval(atom, &parent_rect) {
         if let Err(e) = self.rect.eval(atom, &parent_rect) {
             w!("Rect eval failure: {e}");
             w!("Rect eval failure: {e}");
         }
         }
@@ -223,13 +214,7 @@ impl UIObject for Button {
         }
         }
     }
     }
 
 
-    fn handle_touch_sync(
-        &self,
-        _renderer: &RendererSync,
-        phase: TouchPhase,
-        id: u64,
-        touch_pos: Point,
-    ) -> bool {
+    fn handle_touch_sync(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
         if !self.is_active.get() {
         if !self.is_active.get() {
             return false
             return false
         }
         }

+ 44 - 103
bin/app/src/ui/chatview/mod.rs

@@ -46,14 +46,14 @@ use crate::{
     gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi, Renderer},
     gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi, Renderer},
     mesh::{Color, MeshBuilder},
     mesh::{Color, MeshBuilder},
     prop::{
     prop::{
-        BatchGuardId, BatchGuardPtr, PropertyAtomicGuard, PropertyColor, PropertyFloat32,
-        PropertyRect, PropertyStr, PropertyUint32, Role,
+        PropertyAtomicGuard, PropertyColor, PropertyFloat32, PropertyRect, PropertyStr,
+        PropertyUint32, Role,
     },
     },
     scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
     scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
     text, ExecutorPtr,
     text, ExecutorPtr,
 };
 };
 
 
-use super::{DrawUpdate, OnModify, UIObject};
+use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
 
 
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::chatview", $($arg)*); } }
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::chatview", $($arg)*); } }
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::chatview", $($arg)*); } }
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::chatview", $($arg)*); } }
@@ -218,6 +218,7 @@ pub struct ChatView {
     node: SceneNodeWeak,
     node: SceneNodeWeak,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     renderer: Renderer,
     renderer: Renderer,
+    redraw: RedrawTrigger,
     sg_root: SceneNodePtr,
     sg_root: SceneNodePtr,
 
 
     tree: sled::Tree,
     tree: sled::Tree,
@@ -291,6 +292,7 @@ impl ChatView {
         tree: sled::Tree,
         tree: sled::Tree,
         window_scale: PropertyFloat32,
         window_scale: PropertyFloat32,
         renderer: Renderer,
         renderer: Renderer,
+        redraw: RedrawTrigger,
         sg_root: SceneNodePtr,
         sg_root: SceneNodePtr,
         ex: ExecutorPtr,
         ex: ExecutorPtr,
     ) -> Pimpl {
     ) -> Pimpl {
@@ -354,6 +356,7 @@ impl ChatView {
             node: node.clone(),
             node: node.clone(),
             tasks: SyncMutex::new(vec![]),
             tasks: SyncMutex::new(vec![]),
             renderer: renderer.clone(),
             renderer: renderer.clone(),
+            redraw,
             sg_root,
             sg_root,
 
 
             tree,
             tree,
@@ -512,8 +515,8 @@ impl ChatView {
         let mut msgbuf = self_.msgbuf.lock().await;
         let mut msgbuf = self_.msgbuf.lock().await;
         msgbuf.update_file_status(&url, &file_status);
         msgbuf.update_file_status(&url, &file_status);
         msgbuf.adjust_params();
         msgbuf.adjust_params();
-        let atom = self_.renderer.make_guard(gfxtag!("ChatView::set_file_status"));
-        self_.redraw_cached(atom.batch_id, &mut msgbuf).await;
+        drop(msgbuf);
+        self_.redraw.trigger();
 
 
         true
         true
     }
     }
@@ -570,7 +573,7 @@ impl ChatView {
 
 
     /// Mark line as selected
     /// Mark line as selected
     #[instrument(target = "ui::chatview")]
     #[instrument(target = "ui::chatview")]
-    async fn select_line(&self, batch_id: BatchGuardId, mut y: f32) {
+    async fn select_line(&self, mut y: f32) {
         // The cursor is inside the rect. We just have to find which line it clicked.
         // The cursor is inside the rect. We just have to find which line it clicked.
         let rect = self.rect.get();
         let rect = self.rect.get();
 
 
@@ -584,17 +587,16 @@ impl ChatView {
         let mut msgbuf = self.msgbuf.lock().await;
         let mut msgbuf = self.msgbuf.lock().await;
         let had = msgbuf.has_selection();
         let had = msgbuf.has_selection();
         msgbuf.select_line(y).await;
         msgbuf.select_line(y).await;
-
-        self.redraw_cached(batch_id, &mut msgbuf).await;
         let has = msgbuf.has_selection();
         let has = msgbuf.has_selection();
         drop(msgbuf);
         drop(msgbuf);
 
 
+        self.redraw.trigger();
         self.notify_select_changed(has, had).await;
         self.notify_select_changed(has, had).await;
     }
     }
 
 
     /// Mark line as deselected
     /// Mark line as deselected
     #[instrument(target = "ui::chatview")]
     #[instrument(target = "ui::chatview")]
-    async fn deselect_line(&self, batch_id: BatchGuardId, mut y: f32) {
+    async fn deselect_line(&self, mut y: f32) {
         let rect = self.rect.get();
         let rect = self.rect.get();
         y -= rect.y;
         y -= rect.y;
         let scroll = self.scroll.get();
         let scroll = self.scroll.get();
@@ -603,11 +605,10 @@ impl ChatView {
         let mut msgbuf = self.msgbuf.lock().await;
         let mut msgbuf = self.msgbuf.lock().await;
         let had = msgbuf.has_selection();
         let had = msgbuf.has_selection();
         msgbuf.deselect_line(y).await;
         msgbuf.deselect_line(y).await;
-
-        self.redraw_cached(batch_id, &mut msgbuf).await;
         let has = msgbuf.has_selection();
         let has = msgbuf.has_selection();
         drop(msgbuf);
         drop(msgbuf);
 
 
+        self.redraw.trigger();
         self.notify_select_changed(has, had).await;
         self.notify_select_changed(has, had).await;
     }
     }
 
 
@@ -646,14 +647,13 @@ impl ChatView {
 
 
     /// Deselect every selected message and redraw.
     /// Deselect every selected message and redraw.
     async fn handle_unselect(&self) {
     async fn handle_unselect(&self) {
-        let atom = self.renderer.make_guard(gfxtag!("ChatView::unselect"));
         let mut msgbuf = self.msgbuf.lock().await;
         let mut msgbuf = self.msgbuf.lock().await;
         let had = msgbuf.has_selection();
         let had = msgbuf.has_selection();
         msgbuf.unselect_all();
         msgbuf.unselect_all();
-        self.redraw_cached(atom.batch_id, &mut msgbuf).await;
         let has = msgbuf.has_selection();
         let has = msgbuf.has_selection();
         drop(msgbuf);
         drop(msgbuf);
 
 
+        self.redraw.trigger();
         self.notify_select_changed(has, had).await;
         self.notify_select_changed(has, had).await;
     }
     }
 
 
@@ -770,8 +770,8 @@ impl ChatView {
             }
             }
         }
         }
 
 
-        let atom = self.renderer.make_guard(gfxtag!("ChatView::handle_insert_line"));
-        self.redraw_cached(atom.batch_id, &mut msgbuf).await;
+        drop(msgbuf);
+        self.redraw.trigger();
         self.bgload_cv.notify();
         self.bgload_cv.notify();
     }
     }
     #[instrument(target = "ui::chatview")]
     #[instrument(target = "ui::chatview")]
@@ -792,8 +792,8 @@ impl ChatView {
             return
             return
         };
         };
         privmsg.confirmed = false;
         privmsg.confirmed = false;
-        let atom = self.renderer.make_guard(gfxtag!("ChatView::handle_insert_unconf_line"));
-        self.redraw_cached(atom.batch_id, &mut msgbuf).await;
+        drop(msgbuf);
+        self.redraw.trigger();
         self.bgload_cv.notify();
         self.bgload_cv.notify();
     }
     }
 
 
@@ -932,8 +932,7 @@ impl ChatView {
             remaining_visible -= msg_height;
             remaining_visible -= msg_height;
         }
         }
         if do_redraw {
         if do_redraw {
-            let atom = self.renderer.make_guard(gfxtag!("ChatView::handle_bgload"));
-            self.redraw_cached(atom.batch_id, &mut msgbuf).await;
+            self.redraw.trigger();
         }
         }
     }
     }
 
 
@@ -948,12 +947,11 @@ impl ChatView {
         if let Some(new_scroll) = self.adjust_scroll(&mut msgbuf, scroll, rect.h).await {
         if let Some(new_scroll) = self.adjust_scroll(&mut msgbuf, scroll, rect.h).await {
             scroll = new_scroll;
             scroll = new_scroll;
         }
         }
-
-        // 2/3 of time spent here  ~3.3ms
-        self.redraw_cached(atom.batch_id, &mut msgbuf).await;
+        drop(msgbuf);
 
 
         self.scroll.set(atom, scroll);
         self.scroll.set(atom, scroll);
         self.bgload_cv.notify();
         self.bgload_cv.notify();
+        self.redraw.trigger();
 
 
         scroll - old_scroll
         scroll - old_scroll
     }
     }
@@ -1031,8 +1029,8 @@ impl ChatView {
 
 
     /// Copy `url` to the clipboard and show the "Copied link" overlay above `anchor`
     /// Copy `url` to the clipboard and show the "Copied link" overlay above `anchor`
     /// (chatview-local coords) for `url_copy_duration` seconds. Re-arms on repeat.
     /// (chatview-local coords) for `url_copy_duration` seconds. Re-arms on repeat.
-    /// The overlay is emitted inline from `redraw_cached` (so it inherits the
-    /// chatview's position), hence we trigger a full redraw on show/hide.
+    /// The overlay is emitted inline by the draw pass (so it inherits the
+    /// chatview's position), hence we trigger a redraw on show/hide.
     async fn show_toast(&self, url: &str, anchor: Point) {
     async fn show_toast(&self, url: &str, anchor: Point) {
         clipboard::set(url);
         clipboard::set(url);
 
 
@@ -1055,9 +1053,7 @@ impl ChatView {
             padding: pad,
             padding: pad,
         };
         };
         *self.link_toast.lock() = Some(toast);
         *self.link_toast.lock() = Some(toast);
-
-        let atom = &mut self.renderer.make_guard(gfxtag!("ChatView::urltoast_show"));
-        self.redraw_all(atom).await;
+        self.redraw.trigger();
 
 
         // (Re)arm the dismiss task. A stale task won't clear a newer toast (version check).
         // (Re)arm the dismiss task. A stale task won't clear a newer toast (version check).
         let version = self.toast_version.fetch_add(1, Ordering::SeqCst) + 1;
         let version = self.toast_version.fetch_add(1, Ordering::SeqCst) + 1;
@@ -1069,43 +1065,12 @@ impl ChatView {
             let Some(self_) = me.upgrade() else { return };
             let Some(self_) = me.upgrade() else { return };
             if self_.toast_version.load(Ordering::SeqCst) == version {
             if self_.toast_version.load(Ordering::SeqCst) == version {
                 *self_.link_toast.lock() = None;
                 *self_.link_toast.lock() = None;
-                let atom = &mut self_.renderer.make_guard(gfxtag!("ChatView::urltoast_hide"));
-                self_.redraw_all(atom).await;
+                self_.redraw.trigger();
             }
             }
         })
         })
         .detach();
         .detach();
     }
     }
 
 
-    #[instrument(skip(msgbuf), target = "ui::chatview")]
-    async fn redraw_cached(&self, batch_id: BatchGuardId, msgbuf: &mut MessageBuffer) {
-        let rect = self.rect.get();
-
-        let mut mesh_instrs = self.get_meshes(msgbuf, &rect).await;
-
-        let mut instrs = vec![DrawInstruction::ApplyView(rect)];
-        instrs.append(&mut mesh_instrs);
-        if let Some(t) = self.toast_instr() {
-            instrs.push(t);
-        }
-
-        let draw_calls =
-            vec![(self.dc_key, DrawCall::new(instrs, vec![], self.z_index.get(), "chatview"))];
-
-        self.renderer.replace_draw_calls(Some(batch_id), draw_calls);
-    }
-
-    /// Invalidates cache and redraws everything
-    #[instrument(target = "ui::chatview")]
-    async fn redraw_all(&self, atom: &mut PropertyAtomicGuard) {
-        let parent_rect = self.parent_rect.lock().unwrap().clone();
-        self.rect.eval(atom, &parent_rect).expect("unable to eval rect");
-
-        let mut msgbuf = self.msgbuf.lock().await;
-        msgbuf.adjust_params();
-        msgbuf.clear_meshes();
-        self.redraw_cached(atom.batch_id, &mut msgbuf).await;
-    }
-
     /// Called by the long-press timer after `select_hold_time` elapses.
     /// Called by the long-press timer after `select_hold_time` elapses.
     /// If the finger is still down and within the stationary threshold, starts
     /// If the finger is still down and within the stationary threshold, starts
     /// text selection (or copies the URL if the finger is on one).
     /// text selection (or copies the URL if the finger is on one).
@@ -1150,8 +1115,7 @@ impl ChatView {
         if let Some(ti) = &mut *self.touch_info.lock() {
         if let Some(ti) = &mut *self.touch_info.lock() {
             ti.is_select_mode = Some(true);
             ti.is_select_mode = Some(true);
         }
         }
-        let atom = &mut self.renderer.make_guard(gfxtag!("ChatView::long_hold_fire"));
-        self.select_line(atom.batch_id, start_pos.y).await;
+        self.select_line(start_pos.y).await;
     }
     }
 }
 }
 
 
@@ -1227,32 +1191,9 @@ impl UIObject for ChatView {
 
 
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
 
 
-        async fn reload_view(self_: Arc<ChatView>, batch: BatchGuardPtr) {
-            let atom = &mut batch.spawn();
-            self_.scrollview(self_.scroll.get(), atom).await;
-        }
-        on_modify.when_change(self.scroll.prop(), reload_view);
-
-        async fn redraw(self_: Arc<ChatView>, batch: BatchGuardPtr) {
-            if !self_.rect.has_cached() {
-                return
-            }
-            let atom = &mut batch.spawn();
-            self_.redraw_all(atom).await;
-        }
-
-        //on_modify.when_change(self.baseline.prop(), redraw);
-        //on_modify.when_change(self.font_size.prop(), redraw);
-        //on_modify.when_change(self.timestamp_font_size.prop(), redraw);
-        //on_modify.when_change(self.timestamp_color.prop(), redraw);
-        //on_modify.when_change(self.timestamp_width.prop(), redraw);
-        //on_modify.when_change(self.line_height.prop(), redraw);
-        //on_modify.when_change(self.message_spacing.prop(), redraw);
-        //on_modify.when_change(self.text_color.prop(), redraw);
-        //on_modify.when_change(self.nick_colors.clone(), redraw);
-        //on_modify.when_change(self.hi_bg_color.prop(), redraw);
-        on_modify.when_change(self.rect.prop(), redraw);
-        //on_modify.when_change(self.debug.prop(), redraw);
+        on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
+            self_.redraw.trigger();
+        });
 
 
         let mut tasks = vec![
         let mut tasks = vec![
             insert_line_method_task,
             insert_line_method_task,
@@ -1282,12 +1223,18 @@ impl UIObject for ChatView {
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
         *self.parent_rect.lock() = Some(parent_rect.clone());
         *self.parent_rect.lock() = Some(parent_rect.clone());
+
+        // Rect property is its own memo: compare before/after eval.
+        let prev_rect = self.rect.get();
         self.rect.eval(atom, &parent_rect).ok()?;
         self.rect.eval(atom, &parent_rect).ok()?;
         let rect = self.rect.get();
         let rect = self.rect.get();
+        let rect_changed = rect != prev_rect;
 
 
         let mut msgbuf = self.msgbuf.lock().await;
         let mut msgbuf = self.msgbuf.lock().await;
-        msgbuf.adjust_window_scale();
-        msgbuf.clear_meshes();
+        let scale_changed = msgbuf.adjust_window_scale();
+        if rect_changed || scale_changed {
+            msgbuf.clear_meshes();
+        }
 
 
         let scroll = self.scroll.get();
         let scroll = self.scroll.get();
         if let Some(scroll) = self.adjust_scroll(&mut msgbuf, scroll, rect.h).await {
         if let Some(scroll) = self.adjust_scroll(&mut msgbuf, scroll, rect.h).await {
@@ -1381,15 +1328,13 @@ impl UIObject for ChatView {
             return false
             return false
         }
         }
 
 
-        let atom = self.renderer.make_guard(gfxtag!("ChatView::handle_mouse_btn_down"));
-
         // Query whether the clicked line is already selected. We select
         // Query whether the clicked line is already selected. We select
         // immediately only if it wasn't (for instant feedback). If it was
         // immediately only if it wasn't (for instant feedback). If it was
         // already selected we leave it and let handle_mouse_btn_up decide:
         // already selected we leave it and let handle_mouse_btn_up decide:
         // a stationary click toggles it off, but a drag keeps it selected.
         // a stationary click toggles it off, but a drag keeps it selected.
         let was_selected = self.is_line_selected(mouse_pos.y).await;
         let was_selected = self.is_line_selected(mouse_pos.y).await;
         if !was_selected {
         if !was_selected {
-            self.select_line(atom.batch_id, mouse_pos.y).await;
+            self.select_line(mouse_pos.y).await;
         }
         }
         *self.select_drag.lock() =
         *self.select_drag.lock() =
             Some(SelectDrag { down_y: mouse_pos.y, was_selected, dragged: false });
             Some(SelectDrag { down_y: mouse_pos.y, was_selected, dragged: false });
@@ -1425,8 +1370,7 @@ impl UIObject for ChatView {
         let drag = self.select_drag.lock().take();
         let drag = self.select_drag.lock().take();
         if let Some(d) = drag {
         if let Some(d) = drag {
             if !d.dragged && d.was_selected {
             if !d.dragged && d.was_selected {
-                let atom = self.renderer.make_guard(gfxtag!("ChatView::handle_mouse_btn_up"));
-                self.deselect_line(atom.batch_id, mouse_pos.y).await;
+                self.deselect_line(mouse_pos.y).await;
             }
             }
         }
         }
 
 
@@ -1463,8 +1407,7 @@ impl UIObject for ChatView {
             }
             }
         };
         };
         if dragged {
         if dragged {
-            let atom = &mut self.renderer.make_guard(gfxtag!("ChatView::handle_mouse_move"));
-            self.select_line(atom.batch_id, mouse_pos.y).await;
+            self.select_line(mouse_pos.y).await;
         }
         }
 
 
         let mut msgbuf = self.msgbuf.lock().await;
         let mut msgbuf = self.msgbuf.lock().await;
@@ -1498,7 +1441,6 @@ impl UIObject for ChatView {
 
 
         let rect = self.rect.get();
         let rect = self.rect.get();
         //t!("handle_touch({phase:?}, {id},{id},  {touch_pos:?})");
         //t!("handle_touch({phase:?}, {id},{id},  {touch_pos:?})");
-        let atom = &mut self.renderer.make_guard(gfxtag!("ChatView::handle_touch"));
 
 
         let touch_y = touch_pos.y;
         let touch_y = touch_pos.y;
 
 
@@ -1574,7 +1516,7 @@ impl UIObject for ChatView {
 
 
                 // We are in selection mode so don't scroll the screen until touch phase ends.
                 // We are in selection mode so don't scroll the screen until touch phase ends.
                 if is_select_mode == Some(true) {
                 if is_select_mode == Some(true) {
-                    self.select_line(atom.batch_id, touch_y).await;
+                    self.select_line(touch_y).await;
                     return true
                     return true
                 }
                 }
 
 
@@ -1584,7 +1526,7 @@ impl UIObject for ChatView {
                     return true
                     return true
                 }
                 }
                 let scroll = start_scroll + dist;
                 let scroll = start_scroll + dist;
-                // Redraws the screen from the cache
+                let atom = &mut self.renderer.make_guard(gfxtag!("ChatView::handle_touch_scroll"));
                 self.scrollview(scroll, atom).await;
                 self.scrollview(scroll, atom).await;
             }
             }
             TouchPhase::Ended | TouchPhase::Cancelled => {
             TouchPhase::Ended | TouchPhase::Cancelled => {
@@ -1614,9 +1556,9 @@ impl UIObject for ChatView {
                     // Not a URL/file tap and selection mode is active: toggle the line.
                     // Not a URL/file tap and selection mode is active: toggle the line.
                     if !is_handled && self.select_active.load(Ordering::Relaxed) {
                     if !is_handled && self.select_active.load(Ordering::Relaxed) {
                         if self.is_line_selected(touch_y).await {
                         if self.is_line_selected(touch_y).await {
-                            self.deselect_line(atom.batch_id, touch_y).await;
+                            self.deselect_line(touch_y).await;
                         } else {
                         } else {
-                            self.select_line(atom.batch_id, touch_y).await;
+                            self.select_line(touch_y).await;
                         }
                         }
                     }
                     }
                 }
                 }
@@ -1631,8 +1573,7 @@ impl UIObject for ChatView {
 impl Drop for ChatView {
 impl Drop for ChatView {
     fn drop(&mut self) {
     fn drop(&mut self) {
         let atom = self.renderer.make_guard(gfxtag!("ChatView::drop"));
         let atom = self.renderer.make_guard(gfxtag!("ChatView::drop"));
-        self.renderer
-            .replace_draw_calls(Some(atom.batch_id), vec![(self.dc_key, Default::default())]);
+        self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
     }
 }
 }
 
 

+ 4 - 2
bin/app/src/ui/chatview/page.rs

@@ -1136,13 +1136,15 @@ impl MessageBuffer {
         self.date_msgs.clear();
         self.date_msgs.clear();
     }
     }
 
 
-    pub fn adjust_window_scale(&mut self) {
+    /// Returns whether the scale changed (and params were re-adjusted).
+    pub fn adjust_window_scale(&mut self) -> bool {
         let window_scale = self.window_scale.get();
         let window_scale = self.window_scale.get();
         if self.old_window_scale == window_scale {
         if self.old_window_scale == window_scale {
-            return
+            return false
         }
         }
 
 
         self.adjust_params();
         self.adjust_params();
+        true
     }
     }
 
 
     /// This will force a reload of everything
     /// This will force a reload of everything

+ 7 - 9
bin/app/src/ui/edit/action.rs

@@ -20,9 +20,8 @@ use parking_lot::Mutex as SyncMutex;
 use rand::{rngs::OsRng, Rng};
 use rand::{rngs::OsRng, Rng};
 
 
 use crate::{
 use crate::{
-    gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi, Renderer},
+    gfx::{gfxtag, DrawInstruction, Point, Rectangle, Renderer},
     mesh::{Color, MeshBuilder},
     mesh::{Color, MeshBuilder},
-    prop::BatchGuardId,
     text,
     text,
 };
 };
 
 
@@ -115,6 +114,12 @@ impl ActionMode {
         *self.menu.lock() = Some(menu);
         *self.menu.lock() = Some(menu);
     }
     }
 
 
+    /// Dismiss the overlay. Call whenever the text/selection it refers to
+    /// is invalidated (text edit, cursor tap) so a stale menu can't linger.
+    pub fn clear(&self) {
+        *self.menu.lock() = None;
+    }
+
     /// Returns `Some(n)` if item n is selected.
     /// Returns `Some(n)` if item n is selected.
     pub fn interact(&self, pos: Point) -> Option<u32> {
     pub fn interact(&self, pos: Point) -> Option<u32> {
         let menu = std::mem::take(&mut *self.menu.lock())?;
         let menu = std::mem::take(&mut *self.menu.lock())?;
@@ -170,11 +175,4 @@ impl ActionMode {
 
 
         vec![DrawInstruction::Overlay(instrs)]
         vec![DrawInstruction::Overlay(instrs)]
     }
     }
-
-    /// When theres a state change, call this to update the draw cmds.
-    pub fn redraw(&self, batch_id: BatchGuardId) {
-        let dcs =
-            vec![(self.dc_key, DrawCall::new(self.get_instrs(), vec![], 1, "chatedit_action"))];
-        self.renderer.replace_draw_calls(Some(batch_id), dcs);
-    }
 }
 }

+ 153 - 217
bin/app/src/ui/edit/mod.rs

@@ -40,12 +40,12 @@ use crate::{
     clipboard,
     clipboard,
     gfx::{
     gfx::{
         anim::Frame as AnimFrame, gfxtag, DrawCall, DrawInstruction, DrawMesh, ManagedSeqAnimPtr,
         anim::Frame as AnimFrame, gfxtag, DrawCall, DrawInstruction, DrawMesh, ManagedSeqAnimPtr,
-        Point, Rectangle, RenderApi, Renderer, RendererSync, Vertex,
+        Point, Rectangle, RenderApi, Renderer, Vertex,
     },
     },
     mesh::MeshBuilder,
     mesh::MeshBuilder,
     prop::{
     prop::{
-        BatchGuardId, BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor,
-        PropertyFloat32, PropertyPtr, PropertyRect, PropertyStr, PropertyUint32, Role,
+        BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyFloat32,
+        PropertyPtr, PropertyRect, PropertyStr, PropertyUint32, Role,
     },
     },
     scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
     scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
     text::{self, Editor},
     text::{self, Editor},
@@ -56,7 +56,7 @@ const ACTION_COPY: u32 = 0;
 const ACTION_PASTE: u32 = 1;
 const ACTION_PASTE: u32 = 1;
 const ACTION_SELALL: u32 = 2;
 const ACTION_SELALL: u32 = 2;
 
 
-use super::{DrawUpdate, OnModify, UIObject};
+use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
 
 
 mod action;
 mod action;
 mod filter;
 mod filter;
@@ -168,6 +168,7 @@ pub struct BaseEdit {
     ex: ExecutorPtr,
     ex: ExecutorPtr,
     me: Weak<Self>,
     me: Weak<Self>,
     renderer: Renderer,
     renderer: Renderer,
+    redraw: RedrawTrigger,
     key_repeat: SyncMutex<PressedKeysSmoothRepeat>,
     key_repeat: SyncMutex<PressedKeysSmoothRepeat>,
 
 
     // Moves the draw cursor and applies scroll
     // Moves the draw cursor and applies scroll
@@ -178,7 +179,6 @@ pub struct BaseEdit {
     select_dc_key: u64,
     select_dc_key: u64,
     text_dc_key: u64,
     text_dc_key: u64,
     cursor_dc_key: u64,
     cursor_dc_key: u64,
-    cursor_mesh: SyncMutex<Option<DrawMesh>>,
     cursor_anim: SyncMutex<Option<ManagedSeqAnimPtr>>,
     cursor_anim: SyncMutex<Option<ManagedSeqAnimPtr>>,
 
 
     is_active: PropertyBool,
     is_active: PropertyBool,
@@ -219,8 +219,6 @@ pub struct BaseEdit {
     action_spacing: PropertyFloat32,
     action_spacing: PropertyFloat32,
 
 
     mouse_btn_held: AtomicBool,
     mouse_btn_held: AtomicBool,
-    is_cursor_visible: AtomicBool,
-    is_blink_paused: AtomicBool,
     /// Used to explicitly hide the cursor. Must be manually re-enabled.
     /// Used to explicitly hide the cursor. Must be manually re-enabled.
     hide_cursor: AtomicBool,
     hide_cursor: AtomicBool,
     /// Used to start select and scroll when mouse moves outside widget rect.
     /// Used to start select and scroll when mouse moves outside widget rect.
@@ -244,6 +242,7 @@ impl BaseEdit {
         node: SceneNodeWeak,
         node: SceneNodeWeak,
         window_scale: PropertyFloat32,
         window_scale: PropertyFloat32,
         renderer: Renderer,
         renderer: Renderer,
+        redraw: RedrawTrigger,
         edit_type: BaseEditType,
         edit_type: BaseEditType,
         ex: ExecutorPtr,
         ex: ExecutorPtr,
     ) -> Pimpl {
     ) -> Pimpl {
@@ -352,6 +351,7 @@ impl BaseEdit {
             ex,
             ex,
             me: me.clone(),
             me: me.clone(),
             renderer,
             renderer,
+            redraw,
             key_repeat: SyncMutex::new(PressedKeysSmoothRepeat::new(400, 50)),
             key_repeat: SyncMutex::new(PressedKeysSmoothRepeat::new(400, 50)),
 
 
             root_dc_key: OsRng.gen(),
             root_dc_key: OsRng.gen(),
@@ -360,7 +360,6 @@ impl BaseEdit {
             select_dc_key: OsRng.gen(),
             select_dc_key: OsRng.gen(),
             text_dc_key: OsRng.gen(),
             text_dc_key: OsRng.gen(),
             cursor_dc_key: OsRng.gen(),
             cursor_dc_key: OsRng.gen(),
-            cursor_mesh: SyncMutex::new(None),
             cursor_anim: SyncMutex::new(None),
             cursor_anim: SyncMutex::new(None),
 
 
             is_active,
             is_active,
@@ -401,8 +400,6 @@ impl BaseEdit {
             action_spacing,
             action_spacing,
 
 
             mouse_btn_held: AtomicBool::new(false),
             mouse_btn_held: AtomicBool::new(false),
-            is_cursor_visible: AtomicBool::new(true),
-            is_blink_paused: AtomicBool::new(false),
             hide_cursor: AtomicBool::new(false),
             hide_cursor: AtomicBool::new(false),
             sel_sender: SyncMutex::new(None),
             sel_sender: SyncMutex::new(None),
             scroll: scroll.clone(),
             scroll: scroll.clone(),
@@ -547,7 +544,7 @@ impl BaseEdit {
         mesh.append(verts, indices);
         mesh.append(verts, indices);
     }
     }
 
 
-    async fn change_focus(self: Arc<Self>, batch: BatchGuardPtr) {
+    async fn change_focus(self: Arc<Self>, _batch: BatchGuardPtr) {
         if !self.is_active.get() {
         if !self.is_active.get() {
             ed!("change_focus: not active, skipping");
             ed!("change_focus: not active, skipping");
             return
             return
@@ -555,9 +552,8 @@ impl BaseEdit {
         ed!("change_focus: focused={} -> [{}]", self.is_focused.get(), self.dbg_state());
         ed!("change_focus: focused={} -> [{}]", self.is_focused.get(), self.dbg_state());
         t!("Focus changed");
         t!("Focus changed");
 
 
-        let atom = &mut batch.spawn();
         // Cursor visibility will change so just redraw everything lol
         // Cursor visibility will change so just redraw everything lol
-        self.redraw(atom);
+        self.redraw.trigger();
     }
     }
 
 
     fn handle_shortcut(&self, key: char, mods: &KeyMods, atom: &mut PropertyAtomicGuard) -> bool {
     fn handle_shortcut(&self, key: char, mods: &KeyMods, atom: &mut PropertyAtomicGuard) -> bool {
@@ -600,8 +596,9 @@ impl BaseEdit {
             _ => return false,
             _ => return false,
         }
         }
 
 
-        self.eval_rect();
-        self.redraw(atom);
+        // Any edit invalidates the action menu's selection
+        self.action_mode.clear();
+        self.redraw.trigger();
         ed!("handle_shortcut: handled key={key:?} after=[{}]", self.dbg_state());
         ed!("handle_shortcut: handled key={key:?} after=[{}]", self.dbg_state());
         true
         true
     }
     }
@@ -720,10 +717,11 @@ impl BaseEdit {
             }
             }
         }
         }
 
 
-        self.eval_rect();
         self.behave.apply_cursor_scroll();
         self.behave.apply_cursor_scroll();
         self.pause_blinking();
         self.pause_blinking();
-        self.redraw(atom);
+        // Any edit invalidates the action menu's selection
+        self.action_mode.clear();
+        self.redraw.trigger();
 
 
         ed!("handle_key: handled {key:?} after=[{}]", self.dbg_state());
         ed!("handle_key: handled {key:?} after=[{}]", self.dbg_state());
         true
         true
@@ -751,6 +749,14 @@ impl BaseEdit {
         let rect = self.rect.get();
         let rect = self.rect.get();
         let local_pos = touch_pos - rect.pos();
         let local_pos = touch_pos - rect.pos();
 
 
+        // Grabbing a select handle must keep the action menu visible: the
+        // user adjusts the selection, then taps Copy/Paste. Check the drag
+        // BEFORE interact(), which consumes the menu.
+        if self.try_handle_drag(touch_pos) {
+            ed!("handle_touch_start: handled by drag handle");
+            return true
+        }
+
         let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_touch_start_action"));
         let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_touch_start_action"));
         if let Some(action_id) = self.action_mode.interact(local_pos) {
         if let Some(action_id) = self.action_mode.interact(local_pos) {
             match action_id {
             match action_id {
@@ -776,11 +782,12 @@ impl BaseEdit {
                 _ => {}
                 _ => {}
             }
             }
 
 
-            self.eval_rect();
-            self.redraw(atom);
+            self.redraw.trigger();
             return true;
             return true;
         } else {
         } else {
-            self.action_mode.redraw(atom.batch_id);
+            // interact() already consumed the menu; trigger so the pass
+            // re-emits the (now empty) action overlay.
+            self.redraw.trigger();
         }
         }
 
 
         if !rect.contains(touch_pos) {
         if !rect.contains(touch_pos) {
@@ -789,11 +796,6 @@ impl BaseEdit {
             return false
             return false
         }
         }
 
 
-        if self.try_handle_drag(touch_pos) {
-            ed!("handle_touch_start: handled by drag handle");
-            return true
-        }
-
         let mut touch_info = self.touch_info.lock();
         let mut touch_info = self.touch_info.lock();
         touch_info.start(touch_pos);
         touch_info.start(touch_pos);
         ed!("handle_touch_start: touch started, waiting for move/end");
         ed!("handle_touch_start: touch started, waiting for move/end");
@@ -866,7 +868,7 @@ impl BaseEdit {
         false
         false
     }
     }
 
 
-    fn handle_touch_move(&self, renderer: &RendererSync, mut touch_pos: Point) -> bool {
+    fn handle_touch_move(&self, mut touch_pos: Point) -> bool {
         if !self.is_active.get() {
         if !self.is_active.get() {
             return false
             return false
         }
         }
@@ -911,7 +913,6 @@ impl BaseEdit {
 
 
                     self.abs_to_local(&mut touch_pos);
                     self.abs_to_local(&mut touch_pos);
                     self.start_touch_select(touch_pos, atom);
                     self.start_touch_select(touch_pos, atom);
-                    self.redraw_select(renderer, atom.batch_id);
 
 
                     {
                     {
                         let editor = self.editor.lock();
                         let editor = self.editor.lock();
@@ -930,10 +931,19 @@ impl BaseEdit {
                     self.touch_info.lock().state = TouchStateAction::Select;
                     self.touch_info.lock().state = TouchStateAction::Select;
                 }
                 }
                 self.action_mode.set(menu);
                 self.action_mode.set(menu);
-                self.action_mode.redraw(atom.batch_id);
+                self.redraw.trigger();
                 ed!("handle_touch_move: StartSelect handled");
                 ed!("handle_touch_move: StartSelect handled");
             }
             }
             TouchStateAction::DragSelectHandle { side } => {
             TouchStateAction::DragSelectHandle { side } => {
+                // The IME can collapse the selection (finishing phone-select
+                // mode) while a handle drag is in progress. Abort the drag
+                // instead of asserting on the stale state.
+                if !self.is_phone_select.load(Ordering::Relaxed) {
+                    ed!("handle_touch_move: phone select finished mid-drag, aborting");
+                    self.touch_info.lock().state = TouchStateAction::Inactive;
+                    return true
+                }
+
                 let rect = self.rect.get();
                 let rect = self.rect.get();
                 let is_touch_hover = rect.contains(touch_pos);
                 let is_touch_hover = rect.contains(touch_pos);
 
 
@@ -947,7 +957,7 @@ impl BaseEdit {
                     // Stop any existing select/scroll process
                     // Stop any existing select/scroll process
                     sel_sender.try_send(None).unwrap();
                     sel_sender.try_send(None).unwrap();
                     // Mouse is inside so just select the text once and be done.
                     // Mouse is inside so just select the text once and be done.
-                    self.handle_select(renderer, touch_pos, Some(*side));
+                    self.handle_select(touch_pos, Some(*side));
                 }
                 }
             }
             }
             TouchStateAction::ScrollVert { start_pos, scroll_start } => {
             TouchStateAction::ScrollVert { start_pos, scroll_start } => {
@@ -958,10 +968,7 @@ impl BaseEdit {
                     return true
                     return true
                 }
                 }
                 self.scroll.store(scroll, Ordering::Release);
                 self.scroll.store(scroll, Ordering::Release);
-                let atom = &mut self
-                    .renderer
-                    .make_guard(gfxtag!("BaseEdit::TouchStateAction::ScrollVert"));
-                self.redraw_scroll(renderer, atom.batch_id);
+                self.redraw.trigger();
             }
             }
             TouchStateAction::SetCursorPos => {
             TouchStateAction::SetCursorPos => {
                 // TBH I can't even see the cursor under my thumb so I'll just
                 // TBH I can't even see the cursor under my thumb so I'll just
@@ -982,7 +989,7 @@ impl BaseEdit {
             TouchStateAction::Started { pos: _, instant: _ } | TouchStateAction::SetCursorPos => {
             TouchStateAction::Started { pos: _, instant: _ } | TouchStateAction::SetCursorPos => {
                 let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_touch_end"));
                 let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_touch_end"));
                 self.touch_set_cursor_pos(atom, touch_pos);
                 self.touch_set_cursor_pos(atom, touch_pos);
-                self.redraw(atom);
+                self.redraw.trigger();
             }
             }
             _ => {}
             _ => {}
         }
         }
@@ -1026,9 +1033,11 @@ impl BaseEdit {
         self.is_phone_select.store(false, Ordering::Release);
         self.is_phone_select.store(false, Ordering::Release);
         self.hide_cursor.store(false, Ordering::Release);
         self.hide_cursor.store(false, Ordering::Release);
         self.select_text.clone().set_null(atom, Role::Internal, 0).unwrap();
         self.select_text.clone().set_null(atom, Role::Internal, 0).unwrap();
+        // The action menu belongs to the selection; dismiss it too.
+        self.action_mode.clear();
     }
     }
 
 
-    fn handle_select<R: RenderApi>(&self, renderer: &R, mouse_pos: Point, side: Option<isize>) {
+    fn handle_select(&self, mouse_pos: Point, side: Option<isize>) {
         //t!("handle_select({mouse_pos:?}, {side:?})");
         //t!("handle_select({mouse_pos:?}, {side:?})");
         let rect = self.rect.get();
         let rect = self.rect.get();
         let is_mouse_hover = rect.contains(mouse_pos);
         let is_mouse_hover = rect.contains(mouse_pos);
@@ -1049,7 +1058,7 @@ impl BaseEdit {
             let scroll = (self.scroll.load(Ordering::Relaxed) + delta).clamp(0., max_scroll);
             let scroll = (self.scroll.load(Ordering::Relaxed) + delta).clamp(0., max_scroll);
             self.scroll.store(scroll, Ordering::Release);
             self.scroll.store(scroll, Ordering::Release);
 
 
-            self.redraw_scroll(renderer, atom.batch_id);
+            self.redraw.trigger();
         }
         }
 
 
         // Move mouse pos within this widget
         // Move mouse pos within this widget
@@ -1079,7 +1088,13 @@ impl BaseEdit {
             // Mouse select does not have this limitation.
             // Mouse select does not have this limitation.
             if let Some(side) = side {
             if let Some(side) = side {
                 assert!(side.abs() == 1);
                 assert!(side.abs() == 1);
-                assert!(self.is_phone_select.load(Ordering::Relaxed));
+                // Phone-select mode can be finished concurrently (e.g. the
+                // IME collapsed the selection) while a queued drag update
+                // was still in flight. Drop the stale update.
+                if !self.is_phone_select.load(Ordering::Relaxed) {
+                    ed!("handle_select: phone select finished mid-drag, dropping update");
+                    return
+                }
 
 
                 // Prevent selection crossing itself
                 // Prevent selection crossing itself
                 if side == -1 {
                 if side == -1 {
@@ -1128,114 +1143,28 @@ impl BaseEdit {
 
 
         self.pause_blinking();
         self.pause_blinking();
         //self.behave.apply_cursor_scroll();
         //self.behave.apply_cursor_scroll();
-        self.redraw_cursor(renderer);
-        self.redraw_select(renderer, atom.batch_id);
+        self.redraw.trigger();
     }
     }
 
 
-    /// Cursor must be redrawn after calling this either via `redraw_cursor()` or `redraw()`.
+    /// Holds the cursor solid while the user interacts. The renderer-managed
+    /// anim resumes blinking by itself once the idle time passes; there is no
+    /// app-side timed commit and the committed cursor draw call (which
+    /// references the anim) never needs to change for pausing.
     fn pause_blinking(&self) {
     fn pause_blinking(&self) {
-        // First, redraw cursor with static cursor
-        self.is_blink_paused.store(true, Ordering::Relaxed);
-        //self.redraw_cursor(&self.renderer);
-
-        let cursor_idle_time = self.cursor_idle_time.get();
-        let me = self.me.clone();
-
-        // Spawn task to sleep and then restore animation
-        self.ex
-            .spawn(async move {
-                // Sleep for idle time
-                msleep(cursor_idle_time as u64).await;
-
-                let Some(self_) = me.upgrade() else { return };
-
-                // Restore animation draw calls
-                let pos = self_.get_cursor_pos();
-                let anim = self_.cursor_anim.lock().clone().unwrap();
-                let instrs = vec![DrawInstruction::Move(pos), DrawInstruction::Animation(anim)];
-                self_.renderer.replace_draw_calls(
-                    None,
-                    vec![(
-                        self_.cursor_dc_key,
-                        DrawCall::new(instrs, vec![], 2, "chatedit_anim_curs"),
-                    )],
-                );
-
-                self_.is_blink_paused.store(false, Ordering::Relaxed);
-            })
-            .detach();
+        let anim = self.cursor_anim.lock().clone();
+        let Some(anim) = anim else { return };
+        anim.pause(0, self.cursor_idle_time.get() as u64);
     }
     }
 
 
     #[instrument(target = "ui::edit")]
     #[instrument(target = "ui::edit")]
-    fn redraw(&self, atom: &mut PropertyAtomicGuard) {
-        let draw_update = self.make_draw_calls();
-        self.renderer.replace_draw_calls(Some(atom.batch_id), draw_update.draw_calls);
-    }
-
-    /// Called when scroll changes. Moves content up or down. Nothing more.
-    fn redraw_scroll<R: RenderApi>(&self, renderer: &R, batch_id: BatchGuardId) {
-        let rect = self.rect.get();
-
-        let mut content_instrs = vec![DrawInstruction::ApplyView(rect.with_zero_pos())];
-        let mut bg_instrs = self.regen_bg_mesh(renderer);
-        content_instrs.append(&mut bg_instrs);
-        content_instrs.push(DrawInstruction::Move(self.behave.scroll()));
-
-        let draw_main = vec![(
-            self.content_dc_key,
-            DrawCall::new(
-                content_instrs,
-                vec![
-                    self.text_dc_key,
-                    self.phone_select_handle_dc_key,
-                    self.cursor_dc_key,
-                    self.select_dc_key,
-                ],
-                0,
-                "chatedit_content",
-            ),
-        )];
-        renderer.replace_draw_calls(Some(batch_id), draw_main);
-    }
-
-    fn redraw_cursor<R: RenderApi>(&self, renderer: &R) {
-        let instrs = self.get_cursor_instrs(renderer);
-        let draw_calls = vec![(self.cursor_dc_key, DrawCall::new(instrs, vec![], 2, "curs_redr"))];
-        renderer.replace_draw_calls(None, draw_calls);
-    }
-
-    fn redraw_select<R: RenderApi>(&self, renderer: &R, batch_id: BatchGuardId) {
-        //t!("redraw_select");
-        let sel_instrs = self.regen_select_mesh(renderer);
-        let phone_sel_instrs = self.regen_phone_select_handle_mesh(renderer);
-        let draw_calls = vec![
-            (self.select_dc_key, DrawCall::new(sel_instrs, vec![], 0, "chatedit_sel")),
-            (
-                self.phone_select_handle_dc_key,
-                DrawCall::new(phone_sel_instrs, vec![], 1, "chatedit_phone_sel_redraw_sel"),
-            ),
-        ];
-        renderer.replace_draw_calls(Some(batch_id), draw_calls);
-    }
-
-    fn get_cursor_instrs<R: RenderApi>(&self, _renderer: &R) -> Vec<DrawInstruction> {
+    fn get_cursor_instrs(&self) -> Vec<DrawInstruction> {
         if !self.is_focused.get() || self.hide_cursor.load(Ordering::Relaxed) {
         if !self.is_focused.get() || self.hide_cursor.load(Ordering::Relaxed) {
             return vec![]
             return vec![]
         }
         }
 
 
         let pos = self.get_cursor_pos();
         let pos = self.get_cursor_pos();
-        let mut instrs = Vec::with_capacity(2);
-        instrs.push(DrawInstruction::Move(pos));
-
-        if self.is_blink_paused.load(Ordering::Relaxed) {
-            let mesh = self.cursor_mesh.lock().clone().unwrap();
-            instrs.push(DrawInstruction::Draw(mesh));
-        } else {
-            // Use the animation for cursor blinking
-            let anim = self.cursor_anim.lock().clone().unwrap();
-            instrs.push(DrawInstruction::Animation(anim));
-        }
-        instrs
+        let anim = self.cursor_anim.lock().clone().unwrap();
+        vec![DrawInstruction::Move(pos), DrawInstruction::Animation(anim)]
     }
     }
 
 
     fn regen_bg_mesh<R: RenderApi>(&self, renderer: &R) -> Vec<DrawInstruction> {
     fn regen_bg_mesh<R: RenderApi>(&self, renderer: &R) -> Vec<DrawInstruction> {
@@ -1355,7 +1284,7 @@ impl BaseEdit {
     fn make_draw_calls(&self) -> DrawUpdate {
     fn make_draw_calls(&self) -> DrawUpdate {
         let rect = self.rect.get();
         let rect = self.rect.get();
 
 
-        let cursor_instrs = self.get_cursor_instrs(&self.renderer);
+        let cursor_instrs = self.get_cursor_instrs();
         let txt_instrs = self.regen_txt_mesh();
         let txt_instrs = self.regen_txt_mesh();
         let sel_instrs = self.regen_select_mesh(&self.renderer);
         let sel_instrs = self.regen_select_mesh(&self.renderer);
         let phone_sel_instrs = self.regen_phone_select_handle_mesh(&self.renderer);
         let phone_sel_instrs = self.regen_phone_select_handle_mesh(&self.renderer);
@@ -1448,8 +1377,8 @@ impl BaseEdit {
 
 
         let atom = &mut self_.renderer.make_guard(gfxtag!("BaseEdit::process_insert_text_method"));
         let atom = &mut self_.renderer.make_guard(gfxtag!("BaseEdit::process_insert_text_method"));
         self_.editor.lock().insert(&text, atom);
         self_.editor.lock().insert(&text, atom);
-        self_.eval_rect();
-        self_.redraw(atom);
+        self_.action_mode.clear();
+        self_.redraw.trigger();
         ed!("insert_text method: inserted {text:?} after=[{}]", self_.dbg_state());
         ed!("insert_text method: inserted {text:?} after=[{}]", self_.dbg_state());
         true
         true
     }
     }
@@ -1480,7 +1409,7 @@ impl BaseEdit {
 
 
         let atom = &mut self_.renderer.make_guard(gfxtag!("BaseEdit::process_focus_method"));
         let atom = &mut self_.renderer.make_guard(gfxtag!("BaseEdit::process_focus_method"));
         self_.is_focused.set(atom, true);
         self_.is_focused.set(atom, true);
-        self_.redraw(atom);
+        self_.redraw.trigger();
         ed!("focus method: done after=[{}]", self_.dbg_state());
         ed!("focus method: done after=[{}]", self_.dbg_state());
         true
         true
     }
     }
@@ -1502,7 +1431,7 @@ impl BaseEdit {
         self_.editor.lock().unfocus();
         self_.editor.lock().unfocus();
         let atom = &mut self_.renderer.make_guard(gfxtag!("BaseEdit::process_unfocus_method"));
         let atom = &mut self_.renderer.make_guard(gfxtag!("BaseEdit::process_unfocus_method"));
         self_.is_focused.set(atom, false);
         self_.is_focused.set(atom, false);
-        self_.redraw(atom);
+        self_.redraw.trigger();
         ed!("unfocus method: done after=[{}]", self_.dbg_state());
         ed!("unfocus method: done after=[{}]", self_.dbg_state());
         true
         true
     }
     }
@@ -1514,6 +1443,22 @@ impl BaseEdit {
             return
             return
         }
         }
 
 
+        // A handle drag owns the selection. Our own `set_selection` calls
+        // push state to the IME and Android's input connection echoes it
+        // back, sometimes normalized to a collapsed (cursor-only)
+        // selection. Applying such an echo mid-drag would collapse our
+        // selection and finish phone-select mode, making the handles
+        // vanish. Drop it: the next drag update re-asserts the real
+        // selection.
+        let drag_in_progress = {
+            let touch_info = self.touch_info.lock();
+            matches!(touch_info.state, TouchStateAction::DragSelectHandle { .. })
+        };
+        if drag_in_progress && state.select.0 == state.select.1 {
+            ed!("handle_android_event: DROP collapsed IME echo mid-drag state={state:?}");
+            return
+        }
+
         ed!("handle_android_event: ENTER incoming={state:?} before=[{}]", self.dbg_state());
         ed!("handle_android_event: ENTER incoming={state:?} before=[{}]", self.dbg_state());
         let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_android_event"));
         let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_android_event"));
 
 
@@ -1565,13 +1510,13 @@ impl BaseEdit {
 
 
         // Text changed - finish any active selection
         // Text changed - finish any active selection
         if is_text_changed {
         if is_text_changed {
-            self.eval_rect();
             self.behave.apply_cursor_scroll();
             self.behave.apply_cursor_scroll();
 
 
             self.pause_blinking();
             self.pause_blinking();
             //assert!(state.text != self.text.get());
             //assert!(state.text != self.text.get());
             self.finish_select(atom);
             self.finish_select(atom);
-            self.redraw(atom);
+            self.action_mode.clear();
+            self.redraw.trigger();
             ed!("handle_android_event: handled text change, after=[{}]", self.dbg_state());
             ed!("handle_android_event: handled text change, after=[{}]", self.dbg_state());
         } else if is_select_changed {
         } else if is_select_changed {
             // The IME can collapse a phone-style word selection out from under
             // The IME can collapse a phone-style word selection out from under
@@ -1584,14 +1529,11 @@ impl BaseEdit {
                 d!("IME has collapsed selection!");
                 d!("IME has collapsed selection!");
                 self.finish_select(atom);
                 self.finish_select(atom);
             }
             }
-            // Redrawing the entire text just for select changes is expensive
-            self.redraw_cursor(&self.renderer);
-            //t!("handle_android_event calling redraw_select");
-            self.redraw_select(&self.renderer, atom.batch_id);
+            self.redraw.trigger();
             ed!("handle_android_event: handled select change, after=[{}]", self.dbg_state());
             ed!("handle_android_event: handled select change, after=[{}]", self.dbg_state());
         } else if is_compose_changed {
         } else if is_compose_changed {
             self.editor.lock().refresh();
             self.editor.lock().refresh();
-            self.redraw(atom);
+            self.redraw.trigger();
             ed!("handle_android_event: handled compose change, after=[{}]", self.dbg_state());
             ed!("handle_android_event: handled compose change, after=[{}]", self.dbg_state());
         }
         }
     }
     }
@@ -1600,8 +1542,7 @@ impl BaseEdit {
 impl Drop for BaseEdit {
 impl Drop for BaseEdit {
     fn drop(&mut self) {
     fn drop(&mut self) {
         let atom = self.renderer.make_guard(gfxtag!("BaseEdit::drop"));
         let atom = self.renderer.make_guard(gfxtag!("BaseEdit::drop"));
-        self.renderer
-            .replace_draw_calls(Some(atom.batch_id), vec![(self.text_dc_key, Default::default())]);
+        self.renderer.replace_draw_calls(vec![(self.text_dc_key, Default::default())]);
     }
     }
 }
 }
 
 
@@ -1633,60 +1574,60 @@ impl UIObject for BaseEdit {
             ex.spawn(async move { while Self::process_unfocus_method(&me2, &method_sub).await {} });
             ex.spawn(async move { while Self::process_unfocus_method(&me2, &method_sub).await {} });
 
 
         let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
         let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
-        on_modify.when_change(self.is_focused.prop(), Self::change_focus);
+        on_modify.when_change_external(self.is_focused.prop(), Self::change_focus);
 
 
         // When text has been changed.
         // When text has been changed.
         // Cursor and selection might be invalidated.
         // Cursor and selection might be invalidated.
-        async fn reset(self_: Arc<BaseEdit>, batch: BatchGuardPtr) {
-            let atom = &mut batch.spawn();
+        async fn reset(self_: Arc<BaseEdit>, _batch: BatchGuardPtr) {
             //self_.select_text.set_null(Role::Internal, 0).unwrap();
             //self_.select_text.set_null(Role::Internal, 0).unwrap();
             self_.scroll.store(0., Ordering::Release);
             self_.scroll.store(0., Ordering::Release);
-            self_.eval_rect();
-            self_.redraw(atom);
+            self_.redraw.trigger();
         }
         }
-        async fn redraw(self_: Arc<BaseEdit>, batch: BatchGuardPtr) {
-            let atom = &mut batch.spawn();
-            self_.eval_rect();
-            self_.redraw(atom);
+        async fn redraw(self_: Arc<BaseEdit>, _batch: BatchGuardPtr) {
+            self_.redraw.trigger();
         }
         }
-        async fn set_text(self_: Arc<BaseEdit>, batch: BatchGuardPtr) {
+        async fn set_text(self_: Arc<BaseEdit>, _batch: BatchGuardPtr) {
             self_.editor.lock().on_text_prop_changed();
             self_.editor.lock().on_text_prop_changed();
-            let atom = &mut batch.spawn();
-            self_.eval_rect();
-            self_.redraw(atom);
-        }
-
-        on_modify.when_change(self.rect.prop(), redraw);
-        on_modify.when_change(self.baseline.prop(), redraw);
-        on_modify.when_change(self.lineheight.prop(), redraw);
-        on_modify.when_change(self.select_ascent.prop(), redraw);
-        on_modify.when_change(self.select_descent.prop(), redraw);
-        on_modify.when_change(self.handle_descent.prop(), redraw);
-        on_modify.when_change(self.padding.clone(), redraw);
-        on_modify.when_change(self.text.prop(), set_text);
+            self_.redraw.trigger();
+        }
+
+        on_modify.when_change_external(self.rect.prop(), redraw);
+        on_modify.when_change_external(self.baseline.prop(), redraw);
+        on_modify.when_change_external(self.lineheight.prop(), redraw);
+        on_modify.when_change_external(self.select_ascent.prop(), redraw);
+        on_modify.when_change_external(self.select_descent.prop(), redraw);
+        on_modify.when_change_external(self.handle_descent.prop(), redraw);
+        on_modify.when_change_external(self.padding.clone(), redraw);
+        on_modify.when_change_external(self.text.prop(), set_text);
         // The commented properties are modified on input events
         // The commented properties are modified on input events
         // So then redraw() will get repeatedly triggered when these properties
         // So then redraw() will get repeatedly triggered when these properties
         // are changed. We should find a solution. For now the hooks are disabled.
         // are changed. We should find a solution. For now the hooks are disabled.
         //on_modify.when_change(scroll.prop(), redraw);
         //on_modify.when_change(scroll.prop(), redraw);
         //on_modify.when_change(cursor_pos.prop(), redraw);
         //on_modify.when_change(cursor_pos.prop(), redraw);
-        on_modify.when_change(self.font_size.prop(), redraw);
-        on_modify.when_change(self.text.prop(), reset);
-        on_modify.when_change(self.text_color.prop(), redraw);
-        on_modify.when_change(self.placeholder_text.prop(), redraw);
-        on_modify.when_change(self.placeholder_color.prop(), redraw);
-        on_modify.when_change(self.hi_bg_color.prop(), redraw);
+        on_modify.when_change_external(self.font_size.prop(), redraw);
+        on_modify.when_change_external(self.text.prop(), reset);
+        on_modify.when_change_external(self.text_color.prop(), redraw);
+        on_modify.when_change_external(self.placeholder_text.prop(), redraw);
+        on_modify.when_change_external(self.placeholder_color.prop(), redraw);
+        on_modify.when_change_external(self.hi_bg_color.prop(), redraw);
         //on_modify.when_change(selected.clone(), redraw);
         //on_modify.when_change(selected.clone(), redraw);
-        on_modify.when_change(self.z_index.prop(), redraw);
-        on_modify.when_change(self.debug.prop(), redraw);
+        on_modify.when_change_external(self.z_index.prop(), redraw);
+        on_modify.when_change_external(self.debug.prop(), redraw);
 
 
         async fn regen_cursor(self_: Arc<BaseEdit>, _batch: BatchGuardPtr) {
         async fn regen_cursor(self_: Arc<BaseEdit>, _batch: BatchGuardPtr) {
-            // Free the cache
-            *self_.cursor_mesh.lock() = None;
+            // Rebuild the visible frame so cursor style changes apply to
+            // the blink animation.
+            let mesh = self_.regen_cursor_mesh(&self_.renderer);
+            let dc_visible =
+                DrawCall::new(vec![DrawInstruction::Draw(mesh)], vec![], 2, "cursor_vis");
+            if let Some(anim) = self_.cursor_anim.lock().as_ref() {
+                anim.update(0, AnimFrame::new(self_.cursor_blink_time.get(), dc_visible));
+            }
         }
         }
-        on_modify.when_change(self.cursor_color.prop(), regen_cursor);
-        on_modify.when_change(self.cursor_ascent.prop(), regen_cursor);
-        on_modify.when_change(self.cursor_descent.prop(), regen_cursor);
-        on_modify.when_change(self.cursor_width.prop(), regen_cursor);
+        on_modify.when_change_external(self.cursor_color.prop(), regen_cursor);
+        on_modify.when_change_external(self.cursor_ascent.prop(), regen_cursor);
+        on_modify.when_change_external(self.cursor_descent.prop(), regen_cursor);
+        on_modify.when_change_external(self.cursor_width.prop(), regen_cursor);
 
 
         // Create the blinking cursor animation using SeqAnim
         // Create the blinking cursor animation using SeqAnim
         // Frame 0: visible cursor
         // Frame 0: visible cursor
@@ -1695,12 +1636,8 @@ impl UIObject for BaseEdit {
         let cursor_mesh = self.regen_cursor_mesh(&self.renderer);
         let cursor_mesh = self.regen_cursor_mesh(&self.renderer);
 
 
         // Frame 0: visible cursor (just the draw, no position)
         // Frame 0: visible cursor (just the draw, no position)
-        let dc_visible = DrawCall::new(
-            vec![DrawInstruction::Draw(cursor_mesh.clone())],
-            vec![],
-            2,
-            "cursor_vis",
-        );
+        let dc_visible =
+            DrawCall::new(vec![DrawInstruction::Draw(cursor_mesh)], vec![], 2, "cursor_vis");
         cursor_anim.update(0, AnimFrame::new(self.cursor_blink_time.get(), dc_visible));
         cursor_anim.update(0, AnimFrame::new(self.cursor_blink_time.get(), dc_visible));
 
 
         // Frame 1: invisible (empty draw call)
         // Frame 1: invisible (empty draw call)
@@ -1708,7 +1645,6 @@ impl UIObject for BaseEdit {
         cursor_anim.update(1, AnimFrame::new(self.cursor_blink_time.get(), dc_invisible));
         cursor_anim.update(1, AnimFrame::new(self.cursor_blink_time.get(), dc_invisible));
 
 
         *self.cursor_anim.lock() = Some(cursor_anim);
         *self.cursor_anim.lock() = Some(cursor_anim);
-        *self.cursor_mesh.lock() = Some(cursor_mesh);
 
 
         let (sel_sender, sel_recvr) = async_channel::unbounded();
         let (sel_sender, sel_recvr) = async_channel::unbounded();
         *self.sel_sender.lock() = Some(sel_sender);
         *self.sel_sender.lock() = Some(sel_sender);
@@ -1727,14 +1663,14 @@ impl UIObject for BaseEdit {
                             if let Some((mouse_pos, side)) = scroll_stat {
                             if let Some((mouse_pos, side)) = scroll_stat {
                                 let self_ = me2.upgrade().unwrap();
                                 let self_ = me2.upgrade().unwrap();
                                 //t!("select task interrupt: {mouse_pos:?} (side={side:?})");
                                 //t!("select task interrupt: {mouse_pos:?} (side={side:?})");
-                                self_.handle_select(&self_.renderer, mouse_pos, side);
+                                self_.handle_select(mouse_pos, side);
                             };
                             };
                         }
                         }
                         _ = msleep(SELECT_TASK_UPDATE_TIME).fuse() => {
                         _ = msleep(SELECT_TASK_UPDATE_TIME).fuse() => {
                             if let Some((mouse_pos, side)) = scroll_stat {
                             if let Some((mouse_pos, side)) = scroll_stat {
                                 let self_ = me2.upgrade().unwrap();
                                 let self_ = me2.upgrade().unwrap();
                                 //t!("select task update: {mouse_pos:?} (side={side:?})");
                                 //t!("select task update: {mouse_pos:?} (side={side:?})");
-                                self_.handle_select(&self_.renderer, mouse_pos, side);
+                                self_.handle_select(mouse_pos, side);
                             };
                             };
                         }
                         }
                     }
                     }
@@ -1744,7 +1680,7 @@ impl UIObject for BaseEdit {
                     if let Some((mouse_pos, side)) = scroll_stat {
                     if let Some((mouse_pos, side)) = scroll_stat {
                         let self_ = me2.upgrade().unwrap();
                         let self_ = me2.upgrade().unwrap();
                         //t!("select task wake up: {mouse_pos:?} (side={side:?})");
                         //t!("select task wake up: {mouse_pos:?} (side={side:?})");
-                        self_.handle_select(&self_.renderer, mouse_pos, side);
+                        self_.handle_select(mouse_pos, side);
                     };
                     };
                 }
                 }
             }
             }
@@ -1782,7 +1718,6 @@ impl UIObject for BaseEdit {
         self.tasks.lock().clear();
         self.tasks.lock().clear();
         *self.parent_rect.lock() = None;
         *self.parent_rect.lock() = None;
         self.key_repeat.lock().clear();
         self.key_repeat.lock().clear();
-        *self.cursor_mesh.lock() = None;
     }
     }
 
 
     #[instrument(target = "ui::edit")]
     #[instrument(target = "ui::edit")]
@@ -1793,6 +1728,9 @@ impl UIObject for BaseEdit {
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
         *self.parent_rect.lock() = Some(parent_rect);
         *self.parent_rect.lock() = Some(parent_rect);
         self.eval_rect();
         self.eval_rect();
+        // The fresh eval may have changed the content height, so re-clamp
+        // the scroll to keep the cursor in view before computing draw instrs.
+        self.behave.apply_cursor_scroll();
         Some(self.make_draw_calls())
         Some(self.make_draw_calls())
     }
     }
 
 
@@ -1835,10 +1773,11 @@ impl UIObject for BaseEdit {
         t!("Key {:?} has {} actions", key, actions);
         t!("Key {:?} has {} actions", key, actions);
         let key_str = key.to_string().repeat(actions as usize);
         let key_str = key.to_string().repeat(actions as usize);
         self.editor.lock().insert(&key_str, atom);
         self.editor.lock().insert(&key_str, atom);
-        self.eval_rect();
         self.behave.apply_cursor_scroll();
         self.behave.apply_cursor_scroll();
         self.pause_blinking();
         self.pause_blinking();
-        self.redraw(atom);
+        // Any edit invalidates the action menu's selection
+        self.action_mode.clear();
+        self.redraw.trigger();
         ed!("handle_char: inserted {key_str:?} after=[{}]", self.dbg_state());
         ed!("handle_char: inserted {key_str:?} after=[{}]", self.dbg_state());
         true
         true
     }
     }
@@ -1910,8 +1849,7 @@ impl UIObject for BaseEdit {
             menu.pos = mouse_pos - rect.pos();
             menu.pos = mouse_pos - rect.pos();
 
 
             self.action_mode.set(menu);
             self.action_mode.set(menu);
-            let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_mouse_btn_down"));
-            self.action_mode.redraw(atom.batch_id);
+            self.redraw.trigger();
 
 
             return true
             return true
         }
         }
@@ -1950,8 +1888,9 @@ impl UIObject for BaseEdit {
         self.mouse_btn_held.store(true, Ordering::Relaxed);
         self.mouse_btn_held.store(true, Ordering::Relaxed);
 
 
         self.pause_blinking();
         self.pause_blinking();
-        self.eval_rect();
-        self.redraw(atom);
+        // A click moves the cursor; any open action menu is stale now
+        self.action_mode.clear();
+        self.redraw.trigger();
         true
         true
     }
     }
 
 
@@ -1995,11 +1934,12 @@ impl UIObject for BaseEdit {
                 _ => {}
                 _ => {}
             }
             }
 
 
-            self.eval_rect();
-            self.redraw(atom);
+            self.redraw.trigger();
             return true;
             return true;
         } else {
         } else {
-            self.action_mode.redraw(atom.batch_id);
+            // interact() already consumed the menu; trigger so the pass
+            // re-emits the (now empty) action overlay.
+            self.redraw.trigger();
         }
         }
 
 
         // Stop any selection scrolling
         // Stop any selection scrolling
@@ -2032,7 +1972,7 @@ impl UIObject for BaseEdit {
             // Stop any existing select/scroll process
             // Stop any existing select/scroll process
             sel_sender.send(None).await.unwrap();
             sel_sender.send(None).await.unwrap();
             // Mouse is inside so just select the text once and be done.
             // Mouse is inside so just select the text once and be done.
-            self.handle_select(&self.renderer, mouse_pos, None);
+            self.handle_select(mouse_pos, None);
         }
         }
 
 
         true
         true
@@ -2043,25 +1983,21 @@ impl UIObject for BaseEdit {
             return false
             return false
         }
         }
 
 
-        let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_mouse_wheel"));
-
         let mut scroll =
         let mut scroll =
             self.scroll.load(Ordering::Relaxed) - wheel_pos.y * self.scroll_speed.get();
             self.scroll.load(Ordering::Relaxed) - wheel_pos.y * self.scroll_speed.get();
         scroll = scroll.clamp(0., self.behave.max_scroll());
         scroll = scroll.clamp(0., self.behave.max_scroll());
         t!("handle_mouse_wheel({wheel_pos:?}) [scroll={scroll}]");
         t!("handle_mouse_wheel({wheel_pos:?}) [scroll={scroll}]");
         self.scroll.store(scroll, Ordering::Release);
         self.scroll.store(scroll, Ordering::Release);
-        self.redraw_scroll(&self.renderer, atom.batch_id);
+        self.redraw.trigger();
 
 
         true
         true
     }
     }
 
 
-    fn handle_touch_sync(
-        &self,
-        renderer: &RendererSync,
-        phase: TouchPhase,
-        id: u64,
-        touch_pos: Point,
-    ) -> bool {
+    /// Runs on the Stage thread inside the miniquad event callback.
+    /// Converted to mutate + trigger: state mutations happen inline and the
+    /// redraw is enqueued for the serialized draw pass (one extra hop of
+    /// latency for drag-handle feedback; see design.md D6).
+    fn handle_touch_sync(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
         if !self.is_active.get() {
         if !self.is_active.get() {
             return false
             return false
         }
         }
@@ -2073,7 +2009,7 @@ impl UIObject for BaseEdit {
 
 
         match phase {
         match phase {
             TouchPhase::Started => self.handle_touch_start(touch_pos),
             TouchPhase::Started => self.handle_touch_start(touch_pos),
-            TouchPhase::Moved => self.handle_touch_move(renderer, touch_pos),
+            TouchPhase::Moved => self.handle_touch_move(touch_pos),
             TouchPhase::Ended | TouchPhase::Cancelled => false,
             TouchPhase::Ended | TouchPhase::Cancelled => false,
         }
         }
     }
     }

+ 64 - 53
bin/app/src/ui/emoji_picker/mod.rs

@@ -28,14 +28,12 @@ use std::sync::{
 
 
 use crate::{
 use crate::{
     gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi, Renderer},
     gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi, Renderer},
-    prop::{
-        BatchGuardPtr, PropertyAtomicGuard, PropertyFloat32, PropertyRect, PropertyUint32, Role,
-    },
+    prop::{PropertyAtomicGuard, PropertyFloat32, PropertyRect, PropertyUint32, Role},
     scene::{Pimpl, SceneNodeWeak},
     scene::{Pimpl, SceneNodeWeak},
     ExecutorPtr,
     ExecutorPtr,
 };
 };
 
 
-use super::{DrawUpdate, OnModify, UIObject};
+use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
 
 
 mod default;
 mod default;
 use default::DEFAULT_EMOJI_LIST;
 use default::DEFAULT_EMOJI_LIST;
@@ -69,7 +67,11 @@ pub struct EmojiPicker {
     emoji_size: PropertyFloat32,
     emoji_size: PropertyFloat32,
     mouse_scroll_speed: PropertyFloat32,
     mouse_scroll_speed: PropertyFloat32,
 
 
-    parent_rect: SyncMutex<Option<Rectangle>>,
+    redraw: RedrawTrigger,
+    /// Cached emoji grid instructions. `None` means stale (rect, scroll or
+    /// z_index changed). Scroll is set with an internal role, so scroll
+    /// mutation sites invalidate explicitly.
+    draw_cache: SyncMutex<Option<Vec<DrawInstruction>>>,
     is_mouse_hover: AtomicBool,
     is_mouse_hover: AtomicBool,
     touch_info: SyncMutex<Option<TouchInfo>>,
     touch_info: SyncMutex<Option<TouchInfo>>,
 }
 }
@@ -79,6 +81,7 @@ impl EmojiPicker {
         node: SceneNodeWeak,
         node: SceneNodeWeak,
         renderer: Renderer,
         renderer: Renderer,
         emoji_meshes: EmojiMeshesPtr,
         emoji_meshes: EmojiMeshesPtr,
+        redraw: RedrawTrigger,
     ) -> Pimpl {
     ) -> Pimpl {
         let node_ref = &node.upgrade().unwrap();
         let node_ref = &node.upgrade().unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
@@ -104,7 +107,8 @@ impl EmojiPicker {
             emoji_size,
             emoji_size,
             mouse_scroll_speed,
             mouse_scroll_speed,
 
 
-            parent_rect: SyncMutex::new(None),
+            redraw,
+            draw_cache: SyncMutex::new(None),
             is_mouse_hover: AtomicBool::new(false),
             is_mouse_hover: AtomicBool::new(false),
             touch_info: SyncMutex::new(None),
             touch_info: SyncMutex::new(None),
         });
         });
@@ -180,59 +184,62 @@ impl EmojiPicker {
         }
         }
     }
     }
 
 
-    #[instrument(target = "ui::emoji_picker")]
-    fn redraw(&self, atom: &mut PropertyAtomicGuard) {
-        let Some(parent_rect) = self.parent_rect.lock().clone() else {
-            warn!(target: "ui:emoji_picker", "Skip draw since parent rect is empty");
-            return
-        };
-        let Some(draw_update) = self.get_draw_calls(parent_rect, atom) else {
-            error!(target: "ui:emoji_picker", "Emoji picker failed to draw");
-            return
-        };
-        self.renderer.replace_draw_calls(Some(atom.batch_id), draw_update.draw_calls);
-    }
-
     fn get_draw_calls(
     fn get_draw_calls(
         &self,
         &self,
         parent_rect: Rectangle,
         parent_rect: Rectangle,
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
+        // Rect property is its own memo: compare before/after eval.
+        let prev_rect = self.rect.get();
         if let Err(e) = self.rect.eval(atom, &parent_rect) {
         if let Err(e) = self.rect.eval(atom, &parent_rect) {
-            warn!(target: "ui::emoji_picker", "Rect eval failed: {e}");
+            warn!(target: "ui:emoji_picker", "Rect eval failed: {e}");
             return None
             return None
         }
         }
+        let rect = self.rect.get();
+        let rect_changed = rect != prev_rect;
 
 
         // Clamp scroll if needed due to window size change
         // Clamp scroll if needed due to window size change
         let max_scroll = self.max_scroll();
         let max_scroll = self.max_scroll();
         if self.scroll.get() > max_scroll {
         if self.scroll.get() > max_scroll {
             self.scroll.set(atom, max_scroll);
             self.scroll.set(atom, max_scroll);
+            *self.draw_cache.lock() = None;
         }
         }
 
 
-        let rect = self.rect.get();
-        let mut instrs = vec![DrawInstruction::ApplyView(rect)];
-
-        let off_x = self.calc_off_x();
-        let emoji_size = self.emoji_size.get();
+        // The grid depends on rect and scroll. Compute under the lock so
+        // concurrent invalidations land before or after, never between.
+        let mut cache = self.draw_cache.lock();
+        if cache.is_none() || rect_changed {
+            let mut instrs = vec![DrawInstruction::ApplyView(rect)];
+
+            let off_x = self.calc_off_x();
+            let emoji_size = self.emoji_size.get();
+
+            let mut x = 0.;
+            let mut y = -self.scroll.get();
+            for i in 0..DEFAULT_EMOJI_LIST.len() {
+                let pos = Point::new(x, y);
+                let mesh = self.emoji_meshes.lock().get(i);
+                instrs.extend_from_slice(&[
+                    DrawInstruction::SetPos(pos),
+                    DrawInstruction::Draw(mesh),
+                ]);
+
+                x += off_x;
+                if x > rect.w {
+                    x = 0.;
+                    y += emoji_size;
+                    //d!("Line break after idx={i}");
+                }
 
 
-        let mut x = 0.;
-        let mut y = -self.scroll.get();
-        for i in 0..DEFAULT_EMOJI_LIST.len() {
-            let pos = Point::new(x, y);
-            let mesh = self.emoji_meshes.lock().get(i);
-            instrs.extend_from_slice(&[DrawInstruction::SetPos(pos), DrawInstruction::Draw(mesh)]);
-
-            x += off_x;
-            if x > rect.w {
-                x = 0.;
-                y += emoji_size;
-                //d!("Line break after idx={i}");
+                if y > rect.h + emoji_size {
+                    break
+                }
             }
             }
 
 
-            if y > rect.h + emoji_size {
-                break
-            }
+            *cache = Some(instrs);
         }
         }
+        let instrs = cache.clone().unwrap();
+        drop(cache);
 
 
         Some(DrawUpdate {
         Some(DrawUpdate {
             key: self.dc_key,
             key: self.dc_key,
@@ -253,20 +260,24 @@ impl UIObject for EmojiPicker {
     async fn start(self: Arc<Self>, ex: ExecutorPtr) {
     async fn start(self: Arc<Self>, ex: ExecutorPtr) {
         let me = Arc::downgrade(&self);
         let me = Arc::downgrade(&self);
 
 
-        async fn redraw(self_: Arc<EmojiPicker>, batch: BatchGuardPtr) {
-            let atom = &mut batch.spawn();
-            self_.redraw(atom);
-        }
-
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
-        on_modify.when_change(self.rect.prop(), redraw);
-        on_modify.when_change(self.z_index.prop(), redraw);
+        // Invalidate the cache, then request a pass. Internal-role echoes
+        // (the pass's own evals) are skipped.
+        on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.z_index.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
 
 
         *self.tasks.lock() = on_modify.tasks;
         *self.tasks.lock() = on_modify.tasks;
     }
     }
 
 
     fn stop(&self) {
     fn stop(&self) {
         self.tasks.lock().clear();
         self.tasks.lock().clear();
+        *self.draw_cache.lock() = None;
         self.emoji_meshes.lock().clear();
         self.emoji_meshes.lock().clear();
     }
     }
 
 
@@ -276,10 +287,8 @@ impl UIObject for EmojiPicker {
         parent_rect: Rectangle,
         parent_rect: Rectangle,
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
-        *self.parent_rect.lock() = Some(parent_rect);
         self.get_draw_calls(parent_rect, atom)
         self.get_draw_calls(parent_rect, atom)
     }
     }
-
     async fn handle_mouse_move(&self, mouse_pos: Point) -> bool {
     async fn handle_mouse_move(&self, mouse_pos: Point) -> bool {
         let rect = self.rect.get();
         let rect = self.rect.get();
         self.is_mouse_hover.store(rect.contains(mouse_pos), Ordering::Relaxed);
         self.is_mouse_hover.store(rect.contains(mouse_pos), Ordering::Relaxed);
@@ -298,7 +307,8 @@ impl UIObject for EmojiPicker {
         scroll = scroll.clamp(0., self.max_scroll());
         scroll = scroll.clamp(0., self.max_scroll());
         self.scroll.set(atom, scroll);
         self.scroll.set(atom, scroll);
 
 
-        self.redraw(atom);
+        *self.draw_cache.lock() = None;
+        self.redraw.trigger();
 
 
         true
         true
     }
     }
@@ -361,7 +371,9 @@ impl UIObject for EmojiPicker {
                         let mut scroll = touch_info.start_scroll + y_diff;
                         let mut scroll = touch_info.start_scroll + y_diff;
                         scroll = scroll.clamp(0., self.max_scroll());
                         scroll = scroll.clamp(0., self.max_scroll());
                         self.scroll.set(atom, scroll);
                         self.scroll.set(atom, scroll);
-                        self.redraw(atom);
+
+                        *self.draw_cache.lock() = None;
+                        self.redraw.trigger();
                     }
                     }
                 }
                 }
                 TouchPhase::Ended | TouchPhase::Cancelled => {
                 TouchPhase::Ended | TouchPhase::Cancelled => {
@@ -384,8 +396,7 @@ impl UIObject for EmojiPicker {
 impl Drop for EmojiPicker {
 impl Drop for EmojiPicker {
     fn drop(&mut self) {
     fn drop(&mut self) {
         let atom = self.renderer.make_guard(gfxtag!("EmojiPicker::drop"));
         let atom = self.renderer.make_guard(gfxtag!("EmojiPicker::drop"));
-        self.renderer
-            .replace_draw_calls(Some(atom.batch_id), vec![(self.dc_key, Default::default())]);
+        self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
     }
 }
 }
 
 

+ 48 - 45
bin/app/src/ui/image.rs

@@ -35,13 +35,14 @@ use crate::{
     ExecutorPtr,
     ExecutorPtr,
 };
 };
 
 
-use super::{DrawUpdate, OnModify, UIObject};
+use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
 
 
 pub type ImagePtr = Arc<Image>;
 pub type ImagePtr = Arc<Image>;
 
 
 pub struct Image {
 pub struct Image {
     node: SceneNodeWeak,
     node: SceneNodeWeak,
     renderer: Renderer,
     renderer: Renderer,
+    redraw: RedrawTrigger,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
 
 
     texture: SyncMutex<Option<ManagedTexturePtr>>,
     texture: SyncMutex<Option<ManagedTexturePtr>>,
@@ -53,11 +54,12 @@ pub struct Image {
     priority: PropertyUint32,
     priority: PropertyUint32,
     path: PropertyStr,
     path: PropertyStr,
 
 
-    parent_rect: SyncMutex<Option<Rectangle>>,
+    /// Cached draw instructions. `None` means stale.
+    draw_cache: SyncMutex<Option<Vec<DrawInstruction>>>,
 }
 }
 
 
 impl Image {
 impl Image {
-    pub async fn new(node: SceneNodeWeak, renderer: Renderer) -> Pimpl {
+    pub async fn new(node: SceneNodeWeak, renderer: Renderer, redraw: RedrawTrigger) -> Pimpl {
         let node_ref = &node.upgrade().unwrap();
         let node_ref = &node.upgrade().unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let uv = PropertyRect::wrap(node_ref, Role::Internal, "uv").unwrap();
         let uv = PropertyRect::wrap(node_ref, Role::Internal, "uv").unwrap();
@@ -68,6 +70,7 @@ impl Image {
         let self_ = Arc::new(Self {
         let self_ = Arc::new(Self {
             node,
             node,
             renderer,
             renderer,
+            redraw,
             tasks: SyncMutex::new(vec![]),
             tasks: SyncMutex::new(vec![]),
 
 
             texture: SyncMutex::new(None),
             texture: SyncMutex::new(None),
@@ -79,17 +82,18 @@ impl Image {
             priority,
             priority,
             path,
             path,
 
 
-            parent_rect: SyncMutex::new(None),
+            draw_cache: SyncMutex::new(None),
         });
         });
 
 
         Pimpl::Image(self_)
         Pimpl::Image(self_)
     }
     }
 
 
-    async fn reload(self: Arc<Self>, batch: BatchGuardPtr) {
-        let texture = self.load_texture();
-        *self.texture.lock() = Some(texture);
+    async fn reload(self_: Arc<Self>, _batch: BatchGuardPtr) {
+        let texture = self_.load_texture();
+        *self_.texture.lock() = Some(texture);
 
 
-        self.clone().redraw(batch).await;
+        *self_.draw_cache.lock() = None;
+        self_.redraw.trigger();
     }
     }
 
 
     fn load_texture(&self) -> ManagedTexturePtr {
     fn load_texture(&self) -> ManagedTexturePtr {
@@ -119,21 +123,6 @@ impl Image {
         self.renderer.new_texture(width, height, bmp, TextureFormat::RGBA8, gfxtag!("img"))
         self.renderer.new_texture(width, height, bmp, TextureFormat::RGBA8, gfxtag!("img"))
     }
     }
 
 
-    #[instrument(target = "ui::image")]
-    async fn redraw(self: Arc<Self>, batch: BatchGuardPtr) {
-        let Some(parent_rect) = self.parent_rect.lock().clone() else {
-            warn!(target: "ui:image", "Skip draw since parent rect is empty");
-            return
-        };
-
-        let atom = &mut batch.spawn();
-        let Some(draw_update) = self.get_draw_calls(atom, parent_rect) else {
-            error!(target: "ui::image", "Image failed to draw");
-            return
-        };
-        self.renderer.replace_draw_calls(Some(batch.id), draw_update.draw_calls);
-    }
-
     /// Called whenever any property changes.
     /// Called whenever any property changes.
     fn regen_mesh(&self) -> MeshInfo {
     fn regen_mesh(&self) -> MeshInfo {
         let rect = self.rect.get();
         let rect = self.rect.get();
@@ -149,30 +138,35 @@ impl Image {
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
         parent_rect: Rectangle,
         parent_rect: Rectangle,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
+        // Rect property is its own memo: compare before/after eval.
+        let prev_rect = self.rect.get();
         self.rect.eval(atom, &parent_rect).ok()?;
         self.rect.eval(atom, &parent_rect).ok()?;
         let rect = self.rect.get();
         let rect = self.rect.get();
+        let rect_changed = rect != prev_rect;
         self.uv.eval(atom, &rect).ok()?;
         self.uv.eval(atom, &rect).ok()?;
 
 
-        let mesh = self.regen_mesh();
-        let texture = self.texture.lock().clone().expect("Node missing texture_id!");
-
-        let mesh = DrawMesh {
-            vertex_buffer: mesh.vertex_buffer,
-            index_buffer: mesh.index_buffer,
-            textures: Some(vec![texture]),
-            num_elements: mesh.num_elements,
-        };
+        // Mesh geometry depends on the rect; compute under the lock so a
+        // concurrent invalidation lands before or after, never between.
+        let mut cache = self.draw_cache.lock();
+        if cache.is_none() || rect_changed {
+            let mesh = self.regen_mesh();
+            let texture = self.texture.lock().clone().expect("Node missing texture_id!");
+            let mesh = DrawMesh {
+                vertex_buffer: mesh.vertex_buffer,
+                index_buffer: mesh.index_buffer,
+                textures: Some(vec![texture]),
+                num_elements: mesh.num_elements,
+            };
+            *cache = Some(vec![DrawInstruction::Move(rect.pos()), DrawInstruction::Draw(mesh)]);
+        }
+        let instrs = cache.clone().unwrap();
+        drop(cache);
 
 
         Some(DrawUpdate {
         Some(DrawUpdate {
             key: self.dc_key,
             key: self.dc_key,
             draw_calls: vec![(
             draw_calls: vec![(
                 self.dc_key,
                 self.dc_key,
-                DrawCall::new(
-                    vec![DrawInstruction::Move(rect.pos()), DrawInstruction::Draw(mesh)],
-                    vec![],
-                    self.z_index.get(),
-                    "img",
-                ),
+                DrawCall::new(instrs, vec![], self.z_index.get(), "img"),
             )],
             )],
         })
         })
     }
     }
@@ -192,9 +186,20 @@ impl UIObject for Image {
         let me = Arc::downgrade(&self);
         let me = Arc::downgrade(&self);
 
 
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
-        on_modify.when_change(self.rect.prop(), Self::redraw);
-        on_modify.when_change(self.uv.prop(), Self::redraw);
-        on_modify.when_change(self.z_index.prop(), Self::redraw);
+        // Invalidate the cache, then request a pass. Internal-role echoes
+        // (the pass's own evals) are skipped.
+        on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.uv.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.z_index.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
         on_modify.when_change(self.path.prop(), Self::reload);
         on_modify.when_change(self.path.prop(), Self::reload);
 
 
         *self.tasks.lock() = on_modify.tasks;
         *self.tasks.lock() = on_modify.tasks;
@@ -202,7 +207,7 @@ impl UIObject for Image {
 
 
     fn stop(&self) {
     fn stop(&self) {
         self.tasks.lock().clear();
         self.tasks.lock().clear();
-        *self.parent_rect.lock() = None;
+        *self.draw_cache.lock() = None;
         *self.texture.lock() = None;
         *self.texture.lock() = None;
     }
     }
 
 
@@ -212,7 +217,6 @@ impl UIObject for Image {
         parent_rect: Rectangle,
         parent_rect: Rectangle,
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
-        *self.parent_rect.lock() = Some(parent_rect);
         self.get_draw_calls(atom, parent_rect)
         self.get_draw_calls(atom, parent_rect)
     }
     }
 }
 }
@@ -220,8 +224,7 @@ impl UIObject for Image {
 impl Drop for Image {
 impl Drop for Image {
     fn drop(&mut self) {
     fn drop(&mut self) {
         let atom = self.renderer.make_guard(gfxtag!("Image::drop"));
         let atom = self.renderer.make_guard(gfxtag!("Image::drop"));
-        self.renderer
-            .replace_draw_calls(Some(atom.batch_id), vec![(self.dc_key, Default::default())]);
+        self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
     }
 }
 }
 
 

+ 4 - 10
bin/app/src/ui/layer.rs

@@ -24,8 +24,8 @@ use std::sync::Arc;
 use tracing::instrument;
 use tracing::instrument;
 
 
 use crate::{
 use crate::{
-    gfx::{DrawCall, DrawInstruction, Point, Rectangle, RenderApi, Renderer, RendererSync},
-    prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyRect, PropertyUint32, Role},
+    gfx::{DrawCall, DrawInstruction, Point, Rectangle, Renderer},
+    prop::{PropertyAtomicGuard, PropertyBool, PropertyRect, PropertyUint32, Role},
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
     util::i18n::I18nBabelFish,
     util::i18n::I18nBabelFish,
     ExecutorPtr,
     ExecutorPtr,
@@ -287,20 +287,14 @@ impl UIObject for Layer {
         false
         false
     }
     }
 
 
-    fn handle_touch_sync(
-        &self,
-        renderer: &RendererSync,
-        phase: TouchPhase,
-        id: u64,
-        mut touch_pos: Point,
-    ) -> bool {
+    fn handle_touch_sync(&self, phase: TouchPhase, id: u64, mut touch_pos: Point) -> bool {
         if !self.is_visible.get() {
         if !self.is_visible.get() {
             return false
             return false
         }
         }
         touch_pos -= self.rect.get().pos();
         touch_pos -= self.rect.get().pos();
         for child in self.get_children() {
         for child in self.get_children() {
             let obj = get_ui_object3(&child);
             let obj = get_ui_object3(&child);
-            if obj.handle_touch_sync(renderer, phase, id, touch_pos) {
+            if obj.handle_touch_sync(phase, id, touch_pos) {
                 return true
                 return true
             }
             }
         }
         }

+ 130 - 102
bin/app/src/ui/menu/mod.rs

@@ -33,17 +33,17 @@ use std::{
 };
 };
 
 
 use crate::{
 use crate::{
-    gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi, Renderer, RendererSync},
+    gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, Renderer},
     mesh::MeshBuilder,
     mesh::MeshBuilder,
     prop::{
     prop::{
-        BatchGuardId, BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor,
-        PropertyFloat32, PropertyPtr, PropertyRect, PropertyUint32, Role,
+        PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyFloat32, PropertyPtr,
+        PropertyRect, PropertyUint32, Role,
     },
     },
     scene::{MethodCallSub, Pimpl, SceneNodeWeak},
     scene::{MethodCallSub, Pimpl, SceneNodeWeak},
     text, ExecutorPtr,
     text, ExecutorPtr,
 };
 };
 
 
-use super::{DrawUpdate, OnModify, UIObject};
+use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
 
 
 mod shape;
 mod shape;
 
 
@@ -118,6 +118,7 @@ pub struct Menu {
     me: Weak<Self>,
     me: Weak<Self>,
     ex: ExecutorPtr,
     ex: ExecutorPtr,
     renderer: Renderer,
     renderer: Renderer,
+    redraw: RedrawTrigger,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     root_dc_key: u64,
     root_dc_key: u64,
     content_dc_key: u64,
     content_dc_key: u64,
@@ -156,6 +157,9 @@ pub struct Menu {
 
 
     parent_rect: SyncMutex<Option<Rectangle>>,
     parent_rect: SyncMutex<Option<Rectangle>>,
     saved_items: SyncMutex<Option<Vec<String>>>,
     saved_items: SyncMutex<Option<Vec<String>>>,
+
+    /// Cached content draw instructions. `None` means stale.
+    draw_cache: SyncMutex<Option<Vec<DrawInstruction>>>,
 }
 }
 
 
 impl Menu {
 impl Menu {
@@ -163,6 +167,7 @@ impl Menu {
         node: SceneNodeWeak,
         node: SceneNodeWeak,
         window_scale: PropertyFloat32,
         window_scale: PropertyFloat32,
         renderer: Renderer,
         renderer: Renderer,
+        redraw: RedrawTrigger,
         ex: ExecutorPtr,
         ex: ExecutorPtr,
     ) -> Pimpl {
     ) -> Pimpl {
         let node_ref = &node.upgrade().unwrap();
         let node_ref = &node.upgrade().unwrap();
@@ -199,6 +204,7 @@ impl Menu {
             me: me.clone(),
             me: me.clone(),
             ex,
             ex,
             renderer: renderer.clone(),
             renderer: renderer.clone(),
+            redraw,
             tasks: SyncMutex::new(vec![]),
             tasks: SyncMutex::new(vec![]),
             root_dc_key: OsRng.gen(),
             root_dc_key: OsRng.gen(),
             content_dc_key: OsRng.gen(),
             content_dc_key: OsRng.gen(),
@@ -233,6 +239,7 @@ impl Menu {
             is_edit_mode: AtomicBool::new(false),
             is_edit_mode: AtomicBool::new(false),
             parent_rect: SyncMutex::new(None),
             parent_rect: SyncMutex::new(None),
             saved_items: SyncMutex::new(None),
             saved_items: SyncMutex::new(None),
+            draw_cache: SyncMutex::new(None),
         });
         });
 
 
         Pimpl::Menu(self_)
         Pimpl::Menu(self_)
@@ -291,8 +298,8 @@ impl Menu {
                 self.is_edit_mode.store(true, Ordering::Release);
                 self.is_edit_mode.store(true, Ordering::Release);
                 let node = self.node.upgrade().unwrap();
                 let node = self.node.upgrade().unwrap();
                 node.trigger("edit_active", vec![]).await.unwrap();
                 node.trigger("edit_active", vec![]).await.unwrap();
-                let atom = &mut self.renderer.make_guard(gfxtag!("Menu::long_press"));
-                self.redraw(atom);
+                *self.draw_cache.lock() = None;
+                self.redraw.trigger();
             }
             }
         } else if is_tap {
         } else if is_tap {
             let is_edit_mode = self.is_edit_mode.load(Ordering::Relaxed);
             let is_edit_mode = self.is_edit_mode.load(Ordering::Relaxed);
@@ -312,7 +319,6 @@ impl Menu {
                         info!(target: "app::menu", "X clicked for item: {item_name}");
                         info!(target: "app::menu", "X clicked for item: {item_name}");
                         let atom = &mut self.renderer.make_guard(gfxtag!("Menu::delete_item"));
                         let atom = &mut self.renderer.make_guard(gfxtag!("Menu::delete_item"));
                         self.items.remove_str(atom, Role::App, item_idx).unwrap();
                         self.items.remove_str(atom, Role::App, item_idx).unwrap();
-                        self.redraw(atom);
                     } else {
                     } else {
                         self.handle_selection(item_idx).await;
                         self.handle_selection(item_idx).await;
                     }
                     }
@@ -323,14 +329,10 @@ impl Menu {
         }
         }
     }
     }
 
 
-    fn get_draw_calls(
-        &self,
-        atom: &mut PropertyAtomicGuard,
-        parent_rect: Rectangle,
-    ) -> Option<DrawUpdate> {
-        self.rect.eval(atom, &parent_rect).ok()?;
-        let rect = self.rect.get();
-
+    /// Build the content draw instructions. Called only when the draw
+    /// cache is stale or the rect changed; the root shell is re-emitted
+    /// cheaply by the draw pass on every pass.
+    fn make_content_instrs(&self, rect: &Rectangle) -> Vec<DrawInstruction> {
         let mut instrs = vec![];
         let mut instrs = vec![];
 
 
         let scroll = self.scroll.load(Ordering::Relaxed);
         let scroll = self.scroll.load(Ordering::Relaxed);
@@ -462,57 +464,7 @@ impl Menu {
             }
             }
         }
         }
 
 
-        Some(DrawUpdate {
-            key: self.root_dc_key,
-            draw_calls: vec![
-                (
-                    self.root_dc_key,
-                    DrawCall {
-                        instrs: vec![
-                            DrawInstruction::ApplyView(rect),
-                            DrawInstruction::Move(Point::new(0., -scroll)),
-                        ],
-                        dcs: vec![self.content_dc_key],
-                        z_index: self.z_index.get(),
-                        debug_str: "menu_root",
-                    },
-                ),
-                (
-                    self.content_dc_key,
-                    DrawCall {
-                        instrs,
-                        dcs: vec![],
-                        z_index: self.z_index.get(),
-                        debug_str: "menu_content",
-                    },
-                ),
-            ],
-        })
-    }
-
-    fn redraw(&self, atom: &mut PropertyAtomicGuard) {
-        let Some(parent_rect) = self.parent_rect.lock().clone() else { return };
-        let Some(draw_update) = self.get_draw_calls(atom, parent_rect) else { return };
-        self.renderer.replace_draw_calls(Some(atom.batch_id), draw_update.draw_calls);
-    }
-
-    fn redraw_scroll<R: RenderApi>(&self, renderer: &R) {
-        let rect = self.rect.get();
-        let scroll = self.scroll.load(Ordering::Relaxed);
-
-        // Only recreate root with updated scroll position
-        let root_instrs =
-            vec![DrawInstruction::ApplyView(rect), DrawInstruction::Move(Point::new(0., -scroll))];
-
-        let root_dc = DrawCall {
-            instrs: root_instrs,
-            dcs: vec![self.content_dc_key],
-            z_index: self.z_index.get(),
-            debug_str: "menu_root",
-        };
-
-        let draw_calls = vec![(self.root_dc_key, root_dc)];
-        renderer.replace_draw_calls(None, draw_calls);
+        instrs
     }
     }
 
 
     fn scrollview(&self, scroll: f32) {
     fn scrollview(&self, scroll: f32) {
@@ -545,7 +497,7 @@ impl Menu {
                 speed = self.speed.load(Ordering::Relaxed);
                 speed = self.speed.load(Ordering::Relaxed);
                 let scroll = self.scroll.load(Ordering::Relaxed);
                 let scroll = self.scroll.load(Ordering::Relaxed);
                 self.scrollview(scroll + speed);
                 self.scrollview(scroll + speed);
-                self.redraw_scroll(&self.renderer);
+                self.redraw.trigger();
                 speed *= resist;
                 speed *= resist;
                 self.speed.store(speed, Ordering::Relaxed);
                 self.speed.store(speed, Ordering::Relaxed);
                 darkfi::system::msleep(16).await;
                 darkfi::system::msleep(16).await;
@@ -592,7 +544,8 @@ impl Menu {
 
 
         // Exit edit mode
         // Exit edit mode
         self_.is_edit_mode.store(false, Ordering::Release);
         self_.is_edit_mode.store(false, Ordering::Release);
-        self_.redraw(atom);
+        *self_.draw_cache.lock() = None;
+        self_.redraw.trigger();
 
 
         true
         true
     }
     }
@@ -625,8 +578,8 @@ impl Menu {
         node.trigger("edit_done", data).await.unwrap();
         node.trigger("edit_done", data).await.unwrap();
 
 
         self_.is_edit_mode.store(false, Ordering::Release);
         self_.is_edit_mode.store(false, Ordering::Release);
-        let atom = &mut self_.renderer.make_guard(gfxtag!("Menu::done_edit"));
-        self_.redraw(atom);
+        *self_.draw_cache.lock() = None;
+        self_.redraw.trigger();
 
 
         true
         true
     }
     }
@@ -668,23 +621,58 @@ impl UIObject for Menu {
 
 
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
 
 
-        async fn redraw(self_: Arc<Menu>, batch: BatchGuardPtr) {
-            let atom = &mut batch.spawn();
-            self_.redraw(atom);
-        }
-
-        on_modify.when_change(self.items.clone(), redraw);
-        on_modify.when_change(self.rect.prop(), redraw);
-        on_modify.when_change(self.font_size.prop(), redraw);
-        on_modify.when_change(self.padding.clone(), redraw);
-        on_modify.when_change(self.text_color.prop(), redraw);
-        on_modify.when_change(self.bg_color.prop(), redraw);
-        on_modify.when_change(self.sep_size.prop(), redraw);
-        on_modify.when_change(self.sep_color.prop(), redraw);
-        on_modify.when_change(self.role1_color.prop(), redraw);
-        on_modify.when_change(self.role1_group.clone(), redraw);
-        on_modify.when_change(self.role2_color.prop(), redraw);
-        on_modify.when_change(self.role2_group.clone(), redraw);
+        on_modify.when_change_external(self.items.clone(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.font_size.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.padding.clone(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.text_color.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.bg_color.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.sep_size.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.sep_color.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.role1_color.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.role1_group.clone(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.role2_color.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.role2_group.clone(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.window_scale.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
 
 
         let mut tasks = vec![motion_task, cancel_task, done_task];
         let mut tasks = vec![motion_task, cancel_task, done_task];
         tasks.append(&mut on_modify.tasks);
         tasks.append(&mut on_modify.tasks);
@@ -693,6 +681,7 @@ impl UIObject for Menu {
 
 
     fn stop(&self) {
     fn stop(&self) {
         *self.tasks.lock() = vec![];
         *self.tasks.lock() = vec![];
+        *self.draw_cache.lock() = None;
     }
     }
 
 
     async fn draw(
     async fn draw(
@@ -701,7 +690,52 @@ impl UIObject for Menu {
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
         *self.parent_rect.lock() = Some(parent_rect);
         *self.parent_rect.lock() = Some(parent_rect);
-        self.get_draw_calls(atom, parent_rect)
+
+        // Rect property is its own memo: compare before/after eval.
+        let prev_rect = self.rect.get();
+        self.rect.eval(atom, &parent_rect).ok()?;
+        let rect = self.rect.get();
+        let rect_changed = rect != prev_rect;
+
+        // Compute under the lock so a concurrent invalidation lands
+        // before or after, never between.
+        let mut cache = self.draw_cache.lock();
+        if cache.is_none() || rect_changed {
+            *cache = Some(self.make_content_instrs(&rect));
+        }
+        let instrs = cache.clone().unwrap();
+        drop(cache);
+
+        // The root shell is cheap: re-emit it every pass with the
+        // current scroll so scroll pokes don't invalidate content.
+        let scroll = self.scroll.load(Ordering::Relaxed);
+
+        Some(DrawUpdate {
+            key: self.root_dc_key,
+            draw_calls: vec![
+                (
+                    self.root_dc_key,
+                    DrawCall {
+                        instrs: vec![
+                            DrawInstruction::ApplyView(rect),
+                            DrawInstruction::Move(Point::new(0., -scroll)),
+                        ],
+                        dcs: vec![self.content_dc_key],
+                        z_index: self.z_index.get(),
+                        debug_str: "menu_root",
+                    },
+                ),
+                (
+                    self.content_dc_key,
+                    DrawCall {
+                        instrs,
+                        dcs: vec![],
+                        z_index: self.z_index.get(),
+                        debug_str: "menu_content",
+                    },
+                ),
+            ],
+        })
     }
     }
 
 
     async fn handle_mouse_btn_down(&self, btn: MouseButton, mouse_pos: Point) -> bool {
     async fn handle_mouse_btn_down(&self, btn: MouseButton, mouse_pos: Point) -> bool {
@@ -758,8 +792,8 @@ impl UIObject for Menu {
                     arc_self.is_edit_mode.store(true, Ordering::Release);
                     arc_self.is_edit_mode.store(true, Ordering::Release);
                     let node = arc_self.node.upgrade().unwrap();
                     let node = arc_self.node.upgrade().unwrap();
                     node.trigger("edit_active", vec![]).await.unwrap();
                     node.trigger("edit_active", vec![]).await.unwrap();
-                    let atom = &mut arc_self.renderer.make_guard(gfxtag!("Menu::long_press"));
-                    arc_self.redraw(atom);
+                    *arc_self.draw_cache.lock() = None;
+                    arc_self.redraw.trigger();
                 }
                 }
             }
             }
         });
         });
@@ -840,20 +874,14 @@ impl UIObject for Menu {
         }
         }
 
 
         if should_redraw {
         if should_redraw {
-            let atom = &mut self.renderer.make_guard(gfxtag!("Menu::drag_update"));
-            self.redraw(atom);
+            *self.draw_cache.lock() = None;
+            self.redraw.trigger();
         }
         }
 
 
         false
         false
     }
     }
 
 
-    fn handle_touch_sync(
-        &self,
-        renderer: &RendererSync,
-        phase: TouchPhase,
-        id: u64,
-        touch_pos: Point,
-    ) -> bool {
+    fn handle_touch_sync(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
         if id != 0 {
         if id != 0 {
             return false
             return false
         }
         }
@@ -906,8 +934,8 @@ impl UIObject for Menu {
                 }
                 }
 
 
                 if should_redraw {
                 if should_redraw {
-                    let atom = &mut self.renderer.make_guard(gfxtag!("Menu::drag_update"));
-                    self.redraw(atom);
+                    *self.draw_cache.lock() = None;
+                    self.redraw.trigger();
                 }
                 }
 
 
                 let scroll = {
                 let scroll = {
@@ -932,7 +960,7 @@ impl UIObject for Menu {
                 };
                 };
 
 
                 self.scrollview(scroll);
                 self.scrollview(scroll);
-                self.redraw_scroll(renderer);
+                self.redraw.trigger();
                 true
                 true
             }
             }
 
 

+ 2 - 8
bin/app/src/ui/mod.rs

@@ -22,7 +22,7 @@ use miniquad::{KeyCode, KeyMods, MouseButton, TouchPhase};
 use std::sync::{Arc, Weak};
 use std::sync::{Arc, Weak};
 
 
 use crate::{
 use crate::{
-    gfx::{DrawCall, Point, Rectangle, RendererSync},
+    gfx::{DrawCall, Point, Rectangle},
     prop::{BatchGuardPtr, ModifyAction, PropertyAtomicGuard, PropertyPtr, Role},
     prop::{BatchGuardPtr, ModifyAction, PropertyAtomicGuard, PropertyPtr, Role},
     scene::{Pimpl, SceneNode as SceneNode3, SceneNodePtr, SceneNodeWeak},
     scene::{Pimpl, SceneNode as SceneNode3, SceneNodePtr, SceneNodeWeak},
     util::i18n::I18nBabelFish,
     util::i18n::I18nBabelFish,
@@ -148,13 +148,7 @@ pub trait UIObject: Sync {
         false
         false
     }
     }
 
 
-    fn handle_touch_sync(
-        &self,
-        _renderer: &RendererSync,
-        _phase: TouchPhase,
-        _id: u64,
-        _touch_pos: Point,
-    ) -> bool {
+    fn handle_touch_sync(&self, _phase: TouchPhase, _id: u64, _touch_pos: Point) -> bool {
         false
         false
     }
     }
 
 

+ 4 - 10
bin/app/src/ui/scroll_layer.rs

@@ -21,8 +21,8 @@ use miniquad::{KeyCode, KeyMods, MouseButton, TouchPhase};
 use std::sync::Arc;
 use std::sync::Arc;
 
 
 use crate::{
 use crate::{
-    gfx::{DrawCall, Point, Rectangle, RendererSync},
-    prop::{BatchGuardPtr, PropertyAtomicGuard},
+    gfx::{Point, Rectangle},
+    prop::PropertyAtomicGuard,
     scene::{Pimpl, SceneNodeWeak},
     scene::{Pimpl, SceneNodeWeak},
     util::i18n::I18nBabelFish,
     util::i18n::I18nBabelFish,
     ExecutorPtr,
     ExecutorPtr,
@@ -110,14 +110,8 @@ impl UIObject for ScrollLayer {
         self.inner.handle_touch(phase, id, touch_pos).await
         self.inner.handle_touch(phase, id, touch_pos).await
     }
     }
 
 
-    fn handle_touch_sync(
-        &self,
-        renderer: &RendererSync,
-        phase: TouchPhase,
-        id: u64,
-        touch_pos: Point,
-    ) -> bool {
-        self.inner.handle_touch_sync(renderer, phase, id, touch_pos)
+    fn handle_touch_sync(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
+        self.inner.handle_touch_sync(phase, id, touch_pos)
     }
     }
 
 
     fn set_i18n(&self, i18n_fish: &I18nBabelFish) {
     fn set_i18n(&self, i18n_fish: &I18nBabelFish) {

+ 60 - 35
bin/app/src/ui/text.rs

@@ -26,8 +26,8 @@ use crate::{
     gfx::{gfxtag, DrawCall, DrawInstruction, Rectangle, RenderApi, Renderer},
     gfx::{gfxtag, DrawCall, DrawInstruction, Rectangle, RenderApi, Renderer},
     mesh::{Color, MeshBuilder},
     mesh::{Color, MeshBuilder},
     prop::{
     prop::{
-        BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyEnum,
-        PropertyFloat32, PropertyRect, PropertyStr, PropertyUint32, Role,
+        PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyEnum, PropertyFloat32,
+        PropertyRect, PropertyStr, PropertyUint32, Role,
     },
     },
     scene::{Pimpl, SceneNodeWeak},
     scene::{Pimpl, SceneNodeWeak},
     text,
     text,
@@ -35,7 +35,7 @@ use crate::{
     ExecutorPtr,
     ExecutorPtr,
 };
 };
 
 
-use super::{DrawUpdate, OnModify, UIObject};
+use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
 
 
 pub type TextPtr = Arc<Text>;
 pub type TextPtr = Arc<Text>;
 
 
@@ -43,6 +43,7 @@ pub struct Text {
     node: SceneNodeWeak,
     node: SceneNodeWeak,
     renderer: Renderer,
     renderer: Renderer,
     i18n_fish: I18nBabelFish,
     i18n_fish: I18nBabelFish,
+    redraw: RedrawTrigger,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
 
 
     dc_key: u64,
     dc_key: u64,
@@ -61,7 +62,9 @@ pub struct Text {
     debug: PropertyBool,
     debug: PropertyBool,
 
 
     window_scale: PropertyFloat32,
     window_scale: PropertyFloat32,
-    parent_rect: SyncMutex<Option<Rectangle>>,
+    /// Cached layout + rendered instrs. `None` means stale: recompute in
+    /// the draw pass. Layout is the expensive part (shaping, line breaks).
+    draw_cache: SyncMutex<Option<(parley::Layout<Color>, Vec<DrawInstruction>)>>,
 }
 }
 
 
 impl Text {
 impl Text {
@@ -70,6 +73,7 @@ impl Text {
         window_scale: PropertyFloat32,
         window_scale: PropertyFloat32,
         renderer: Renderer,
         renderer: Renderer,
         i18n_fish: I18nBabelFish,
         i18n_fish: I18nBabelFish,
+        redraw: RedrawTrigger,
     ) -> Pimpl {
     ) -> Pimpl {
         let node_ref = &node.upgrade().unwrap();
         let node_ref = &node.upgrade().unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
@@ -90,6 +94,7 @@ impl Text {
             node,
             node,
             renderer,
             renderer,
             i18n_fish,
             i18n_fish,
+            redraw,
             tasks: SyncMutex::new(vec![]),
             tasks: SyncMutex::new(vec![]),
             dc_key: OsRng.gen(),
             dc_key: OsRng.gen(),
 
 
@@ -107,7 +112,7 @@ impl Text {
             debug,
             debug,
 
 
             window_scale,
             window_scale,
-            parent_rect: SyncMutex::new(None),
+            draw_cache: SyncMutex::new(None),
         });
         });
 
 
         Pimpl::Text(self_)
         Pimpl::Text(self_)
@@ -157,35 +162,34 @@ impl Text {
         text::render_layout_with_opts(layout, debug_opts, &self.renderer, gfxtag!("text"))
         text::render_layout_with_opts(layout, debug_opts, &self.renderer, gfxtag!("text"))
     }
     }
 
 
-    #[instrument(target = "ui::text")]
-    async fn redraw(self: Arc<Self>, batch: BatchGuardPtr) {
-        let Some(parent_rect) = self.parent_rect.lock().clone() else {
-            warn!(target: "ui:text", "Skip draw since parent rect is empty");
-            return
-        };
-
-        let atom = &mut batch.spawn();
-        let Some(draw_update) = self.get_draw_calls(atom, parent_rect) else {
-            error!(target: "ui::text", "Text failed to draw");
-            return
-        };
-        self.renderer.replace_draw_calls(Some(batch.id), draw_update.draw_calls);
-    }
-
     fn get_draw_calls(
     fn get_draw_calls(
         &self,
         &self,
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
         parent_rect: Rectangle,
         parent_rect: Rectangle,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
+        // Rect property is its own memo: compare before/after eval.
+        let prev_rect = self.rect.get();
         self.rect.eval(atom, &parent_rect).ok()?;
         self.rect.eval(atom, &parent_rect).ok()?;
         let rect = self.rect.get();
         let rect = self.rect.get();
+        let rect_changed = rect != prev_rect;
+
+        // Layout depends on the width, so a rect change invalidates the
+        // layout even if the text itself did not change. Compute under the
+        // lock: the compute is synchronous, so concurrent invalidations
+        // either land before (seen as None) or after (clear our result).
+        let mut cache = self.draw_cache.lock();
+        if cache.is_none() || rect_changed {
+            let layout = self.make_layout();
+            let mut instrs = vec![DrawInstruction::Move(rect.pos())];
+            instrs.append(&mut self.regen_mesh(&layout));
+            *cache = Some((layout, instrs));
+        }
+        let (layout, mut instrs) = cache.clone().unwrap();
+        drop(cache);
 
 
-        let layout = self.make_layout();
+        // Height output for parents that depend on it.
         self.height.set(atom, layout.height());
         self.height.set(atom, layout.height());
 
 
-        let mut instrs = vec![DrawInstruction::Move(rect.pos())];
-        instrs.append(&mut self.regen_mesh(&layout));
-
         if self.debug.get() {
         if self.debug.get() {
             let rect = self.rect.get().with_zero_pos();
             let rect = self.rect.get().with_zero_pos();
             let mut mesh = MeshBuilder::new(gfxtag!("text_debug-rect"));
             let mut mesh = MeshBuilder::new(gfxtag!("text_debug-rect"));
@@ -214,20 +218,43 @@ impl UIObject for Text {
         let me = Arc::downgrade(&self);
         let me = Arc::downgrade(&self);
 
 
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
-        on_modify.when_change(self.rect.prop(), Self::redraw);
-        on_modify.when_change(self.z_index.prop(), Self::redraw);
-        on_modify.when_change(self.text.prop(), Self::redraw);
-        on_modify.when_change(self.text_align.prop(), Self::redraw);
-        on_modify.when_change(self.font_size.prop(), Self::redraw);
-        on_modify.when_change(self.text_color.prop(), Self::redraw);
-        on_modify.when_change(self.debug.prop(), Self::redraw);
+        // Invalidate the cache, then request a pass. Internal-role echoes
+        // (the pass's own evals) are skipped.
+        on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.z_index.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.text.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.text_align.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.font_size.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.text_color.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.debug.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
 
 
         *self.tasks.lock() = on_modify.tasks;
         *self.tasks.lock() = on_modify.tasks;
     }
     }
 
 
     fn stop(&self) {
     fn stop(&self) {
         self.tasks.lock().clear();
         self.tasks.lock().clear();
-        *self.parent_rect.lock() = None;
+        *self.draw_cache.lock() = None;
     }
     }
 
 
     #[instrument(target = "ui::text")]
     #[instrument(target = "ui::text")]
@@ -236,7 +263,6 @@ impl UIObject for Text {
         parent_rect: Rectangle,
         parent_rect: Rectangle,
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
-        *self.parent_rect.lock() = Some(parent_rect);
         self.get_draw_calls(atom, parent_rect)
         self.get_draw_calls(atom, parent_rect)
     }
     }
 
 
@@ -248,8 +274,7 @@ impl UIObject for Text {
 impl Drop for Text {
 impl Drop for Text {
     fn drop(&mut self) {
     fn drop(&mut self) {
         let atom = self.renderer.make_guard(gfxtag!("Text::drop"));
         let atom = self.renderer.make_guard(gfxtag!("Text::drop"));
-        self.renderer
-            .replace_draw_calls(Some(atom.batch_id), vec![(self.dc_key, Default::default())]);
+        self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
     }
 }
 }
 
 

+ 66 - 38
bin/app/src/ui/tokentable/mod.rs

@@ -28,8 +28,7 @@ use crate::{
     gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi, Renderer},
     gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi, Renderer},
     mesh::MeshBuilder,
     mesh::MeshBuilder,
     prop::{
     prop::{
-        BatchGuardId, PropertyAtomicGuard, PropertyColor, PropertyFloat32, PropertyRect,
-        PropertyUint32, Role,
+        PropertyAtomicGuard, PropertyColor, PropertyFloat32, PropertyRect, PropertyUint32, Role,
     },
     },
     scene::SceneNodeWeak,
     scene::SceneNodeWeak,
     text,
     text,
@@ -37,7 +36,7 @@ use crate::{
     ExecutorPtr,
     ExecutorPtr,
 };
 };
 
 
-use super::{DrawUpdate, UIObject};
+use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
 
 
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::tokentable", $($arg)*); } }
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::tokentable", $($arg)*); } }
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::tokentable", $($arg)*); } }
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::tokentable", $($arg)*); } }
@@ -63,6 +62,7 @@ pub type TokenTablePtr = Arc<TokenTable>;
 pub struct TokenTable {
 pub struct TokenTable {
     node: SceneNodeWeak,
     node: SceneNodeWeak,
     renderer: Renderer,
     renderer: Renderer,
+    redraw: RedrawTrigger,
     mouse_btn_token: SyncMutex<Option<TokenId>>,
     mouse_btn_token: SyncMutex<Option<TokenId>>,
 
 
     rows: SyncMutex<Vec<TokenRow>>,
     rows: SyncMutex<Vec<TokenRow>>,
@@ -78,12 +78,15 @@ pub struct TokenTable {
     padding_x: PropertyFloat32,
     padding_x: PropertyFloat32,
     padding_y: PropertyFloat32,
     padding_y: PropertyFloat32,
 
 
+    /// Cached draw instructions. `None` means stale.
+    draw_cache: SyncMutex<Option<Vec<DrawInstruction>>>,
+
     parent_rect: SyncMutex<Option<Rectangle>>,
     parent_rect: SyncMutex<Option<Rectangle>>,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
 }
 }
 
 
 impl TokenTable {
 impl TokenTable {
-    pub async fn new(node: SceneNodeWeak, renderer: Renderer) -> Pimpl {
+    pub async fn new(node: SceneNodeWeak, renderer: Renderer, redraw: RedrawTrigger) -> Pimpl {
         let node_ref = &node.upgrade().unwrap();
         let node_ref = &node.upgrade().unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap();
         let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap();
@@ -98,6 +101,7 @@ impl TokenTable {
         let self_ = Arc::new(Self {
         let self_ = Arc::new(Self {
             node: node.clone(),
             node: node.clone(),
             renderer: renderer.clone(),
             renderer: renderer.clone(),
+            redraw,
             mouse_btn_token: SyncMutex::new(None),
             mouse_btn_token: SyncMutex::new(None),
             rows: SyncMutex::new(vec![]),
             rows: SyncMutex::new(vec![]),
             dc_key: OsRng.gen(),
             dc_key: OsRng.gen(),
@@ -109,6 +113,7 @@ impl TokenTable {
             separator_color,
             separator_color,
             padding_x,
             padding_x,
             padding_y,
             padding_y,
+            draw_cache: SyncMutex::new(None),
             parent_rect: SyncMutex::new(None),
             parent_rect: SyncMutex::new(None),
             tasks: SyncMutex::new(vec![]),
             tasks: SyncMutex::new(vec![]),
         });
         });
@@ -136,12 +141,12 @@ impl TokenTable {
             return false
             return false
         };
         };
 
 
-        self_.set_tokens(rows).await;
+        self_.set_tokens(rows);
         true
         true
     }
     }
 
 
     /// Replace all rows in the token table
     /// Replace all rows in the token table
-    pub async fn set_tokens(&self, rows: Vec<TokenRow>) {
+    pub fn set_tokens(&self, rows: Vec<TokenRow>) {
         // Ensure DRK token is always shown first (balance is set to 0 if not present)
         // Ensure DRK token is always shown first (balance is set to 0 if not present)
         let rows = if rows.iter().any(|row| row.id == *DARK_TOKEN_ID) {
         let rows = if rows.iter().any(|row| row.id == *DARK_TOKEN_ID) {
             let mut drk_row = None;
             let mut drk_row = None;
@@ -177,8 +182,8 @@ impl TokenTable {
 
 
         *self.rows.lock() = rows;
         *self.rows.lock() = rows;
 
 
-        let atom = self.renderer.make_guard(gfxtag!("TokenTable::set_tokens"));
-        self.redraw_cached(atom.batch_id).await;
+        *self.draw_cache.lock() = None;
+        self.redraw.trigger();
     }
     }
 
 
     /// Get row at specific screen y position
     /// Get row at specific screen y position
@@ -203,28 +208,7 @@ impl TokenTable {
         }
         }
     }
     }
 
 
-    /// Invalidates cache and redraws everything
-    async fn redraw_all(&self, atom: &mut PropertyAtomicGuard) {
-        let parent_rect = self.parent_rect.lock().unwrap().clone();
-        self.rect.eval(atom, &parent_rect).expect("unable to eval rect");
-        self.redraw_cached(atom.batch_id).await;
-    }
-
-    async fn redraw_cached(&self, batch_id: BatchGuardId) {
-        let rect = self.rect.get();
-
-        let mut mesh_instrs = self.get_meshes(&rect).await;
-
-        let mut instrs = vec![DrawInstruction::ApplyView(rect)];
-        instrs.append(&mut mesh_instrs);
-
-        let draw_calls =
-            vec![(self.dc_key, DrawCall::new(instrs, vec![], self.z_index.get(), "tokentable"))];
-
-        self.renderer.replace_draw_calls(Some(batch_id), draw_calls);
-    }
-
-    async fn get_meshes(&self, rect: &Rectangle) -> Vec<DrawInstruction> {
+    fn get_meshes(&self, rect: &Rectangle) -> Vec<DrawInstruction> {
         let rows = self.rows.lock();
         let rows = self.rows.lock();
         let font_size = self.font_size.get();
         let font_size = self.font_size.get();
         let text_color = self.text_color.get();
         let text_color = self.text_color.get();
@@ -299,12 +283,46 @@ impl UIObject for TokenTable {
             }
             }
         });
         });
 
 
-        *self.tasks.lock() = vec![set_tokens_method_task];
+        let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
+
+        on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.font_size.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.text_color.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.separator_color.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.padding_x.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.padding_y.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.z_index.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+
+        let mut tasks = vec![set_tokens_method_task];
+        tasks.append(&mut on_modify.tasks);
+        *self.tasks.lock() = tasks;
     }
     }
 
 
     fn stop(&self) {
     fn stop(&self) {
         self.tasks.lock().clear();
         self.tasks.lock().clear();
         *self.parent_rect.lock() = None;
         *self.parent_rect.lock() = None;
+        *self.draw_cache.lock() = None;
     }
     }
 
 
     async fn draw(
     async fn draw(
@@ -313,13 +331,24 @@ impl UIObject for TokenTable {
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
         *self.parent_rect.lock() = Some(parent_rect);
         *self.parent_rect.lock() = Some(parent_rect);
+
+        // Rect property is its own memo: compare before/after eval.
+        let prev_rect = self.rect.get();
         self.rect.eval(atom, &parent_rect).ok()?;
         self.rect.eval(atom, &parent_rect).ok()?;
         let rect = self.rect.get();
         let rect = self.rect.get();
-
-        let mut mesh_instrs = self.get_meshes(&rect).await;
-
-        let mut instrs = vec![DrawInstruction::ApplyView(rect)];
-        instrs.append(&mut mesh_instrs);
+        let rect_changed = rect != prev_rect;
+
+        // Compute under the lock so a concurrent invalidation lands
+        // before or after, never between.
+        let mut cache = self.draw_cache.lock();
+        if cache.is_none() || rect_changed {
+            let mut mesh_instrs = self.get_meshes(&rect);
+            let mut instrs = vec![DrawInstruction::ApplyView(rect)];
+            instrs.append(&mut mesh_instrs);
+            *cache = Some(instrs);
+        }
+        let instrs = cache.clone().unwrap();
+        drop(cache);
 
 
         Some(DrawUpdate {
         Some(DrawUpdate {
             key: self.dc_key,
             key: self.dc_key,
@@ -409,8 +438,7 @@ impl UIObject for TokenTable {
 impl Drop for TokenTable {
 impl Drop for TokenTable {
     fn drop(&mut self) {
     fn drop(&mut self) {
         let atom = self.renderer.make_guard(gfxtag!("TokenTable::drop"));
         let atom = self.renderer.make_guard(gfxtag!("TokenTable::drop"));
-        self.renderer
-            .replace_draw_calls(Some(atom.batch_id), vec![(self.dc_key, Default::default())]);
+        self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
     }
 }
 }
 
 

+ 54 - 31
bin/app/src/ui/vector_art/mod.rs

@@ -23,16 +23,15 @@ use std::sync::Arc;
 use tracing::instrument;
 use tracing::instrument;
 
 
 use crate::{
 use crate::{
-    gfx::{gfxtag, DrawCall, DrawInstruction, DrawMesh, Point, Rectangle, RenderApi, Renderer},
+    gfx::{gfxtag, DrawCall, DrawInstruction, DrawMesh, Rectangle, RenderApi, Renderer},
     prop::{
     prop::{
-        BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyFloat32, PropertyRect,
-        PropertyUint32, Role,
+        PropertyAtomicGuard, PropertyBool, PropertyFloat32, PropertyRect, PropertyUint32, Role,
     },
     },
     scene::{Pimpl, SceneNodeWeak},
     scene::{Pimpl, SceneNodeWeak},
     ExecutorPtr,
     ExecutorPtr,
 };
 };
 
 
-use super::{DrawUpdate, OnModify, UIObject};
+use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
 
 
 pub mod shape;
 pub mod shape;
 use shape::VectorShape;
 use shape::VectorShape;
@@ -42,6 +41,7 @@ pub type VectorArtPtr = Arc<VectorArt>;
 pub struct VectorArt {
 pub struct VectorArt {
     node: SceneNodeWeak,
     node: SceneNodeWeak,
     renderer: Renderer,
     renderer: Renderer,
+    redraw: RedrawTrigger,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
 
 
     shape: VectorShape,
     shape: VectorShape,
@@ -53,11 +53,19 @@ pub struct VectorArt {
     z_index: PropertyUint32,
     z_index: PropertyUint32,
     priority: PropertyUint32,
     priority: PropertyUint32,
 
 
-    parent_rect: SyncMutex<Option<Rectangle>>,
+    /// Cached draw instructions. `None` means the output is stale and must
+    /// be recomputed by the draw pass. Shape is static, so only visibility,
+    /// rect, scale and z_index changes invalidate it.
+    draw_cache: SyncMutex<Option<Vec<DrawInstruction>>>,
 }
 }
 
 
 impl VectorArt {
 impl VectorArt {
-    pub async fn new(node: SceneNodeWeak, shape: VectorShape, renderer: Renderer) -> Pimpl {
+    pub async fn new(
+        node: SceneNodeWeak,
+        shape: VectorShape,
+        renderer: Renderer,
+        redraw: RedrawTrigger,
+    ) -> Pimpl {
         let node_ref = &node.upgrade().unwrap();
         let node_ref = &node.upgrade().unwrap();
         let is_visible = PropertyBool::wrap(node_ref, Role::Internal, "is_visible", 0).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 rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
@@ -68,6 +76,7 @@ impl VectorArt {
         let self_ = Arc::new(Self {
         let self_ = Arc::new(Self {
             node,
             node,
             renderer,
             renderer,
+            redraw,
             tasks: SyncMutex::new(vec![]),
             tasks: SyncMutex::new(vec![]),
 
 
             shape,
             shape,
@@ -79,27 +88,12 @@ impl VectorArt {
             z_index,
             z_index,
             priority,
             priority,
 
 
-            parent_rect: SyncMutex::new(None),
+            draw_cache: SyncMutex::new(None),
         });
         });
 
 
         Pimpl::VectorArt(self_)
         Pimpl::VectorArt(self_)
     }
     }
 
 
-    #[instrument(target = "ui::vector_art")]
-    async fn redraw(self: Arc<Self>, batch: BatchGuardPtr) {
-        let Some(parent_rect) = self.parent_rect.lock().clone() else {
-            warn!(target: "ui:vector_art", "Skip draw since parent rect is empty");
-            return
-        };
-
-        let atom = &mut batch.spawn();
-        let Some(draw_update) = self.get_draw_calls(atom, parent_rect) else {
-            error!(target: "ui:vector_art", "Mesh failed to draw");
-            return
-        };
-        self.renderer.replace_draw_calls(Some(batch.id), draw_update.draw_calls);
-    }
-
     fn get_draw_instrs(&self) -> Vec<DrawInstruction> {
     fn get_draw_instrs(&self) -> Vec<DrawInstruction> {
         if !self.is_visible.get() {
         if !self.is_visible.get() {
             //t!("Skipping draw for invisible node");
             //t!("Skipping draw for invisible node");
@@ -131,11 +125,27 @@ impl VectorArt {
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
         parent_rect: Rectangle,
         parent_rect: Rectangle,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
+        // The rect property is its own memo: its stored value is the result
+        // of the last eval. Compare before/after to detect geometry changes
+        // without a separate last_rect field.
+        let prev_rect = self.rect.get();
         if let Err(e) = self.rect.eval(atom, &parent_rect) {
         if let Err(e) = self.rect.eval(atom, &parent_rect) {
             warn!(target: "ui::vector_art", "Rect eval failure: {e}");
             warn!(target: "ui::vector_art", "Rect eval failure: {e}");
             return None
             return None
         }
         }
-        let instrs = self.get_draw_instrs();
+        let rect_changed = self.rect.get() != prev_rect;
+
+        // Compute while holding the lock: the compute is synchronous, so a
+        // concurrent invalidation either lands before us (we see None and
+        // recompute with the newer state) or after us (it clears our result
+        // and the trailing pass recomputes). No lost invalidation.
+        let mut cache = self.draw_cache.lock();
+        if cache.is_none() || rect_changed {
+            *cache = Some(self.get_draw_instrs());
+        }
+        let instrs = cache.clone().unwrap();
+        drop(cache);
+
         Some(DrawUpdate {
         Some(DrawUpdate {
             key: self.dc_key,
             key: self.dc_key,
             draw_calls: vec![(
             draw_calls: vec![(
@@ -156,17 +166,32 @@ impl UIObject for VectorArt {
         let me = Arc::downgrade(&self);
         let me = Arc::downgrade(&self);
 
 
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
         let mut on_modify = OnModify::new(ex, self.node.clone(), 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.scale.prop(), Self::redraw);
-        on_modify.when_change(self.z_index.prop(), Self::redraw);
+        // Invalidate the cache, then request a pass. Internal-role echoes
+        // (the pass's own evals) are skipped: reacting to them would queue
+        // a pass for every pass, forever.
+        on_modify.when_change_external(self.is_visible.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.scale.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.z_index.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
 
 
         *self.tasks.lock() = on_modify.tasks;
         *self.tasks.lock() = on_modify.tasks;
     }
     }
 
 
     fn stop(&self) {
     fn stop(&self) {
         self.tasks.lock().clear();
         self.tasks.lock().clear();
-        *self.parent_rect.lock() = None;
+        *self.draw_cache.lock() = None;
     }
     }
 
 
     #[instrument(target = "ui::vector_art")]
     #[instrument(target = "ui::vector_art")]
@@ -175,7 +200,6 @@ impl UIObject for VectorArt {
         parent_rect: Rectangle,
         parent_rect: Rectangle,
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
-        *self.parent_rect.lock() = Some(parent_rect);
         self.get_draw_calls(atom, parent_rect)
         self.get_draw_calls(atom, parent_rect)
     }
     }
 }
 }
@@ -183,8 +207,7 @@ impl UIObject for VectorArt {
 impl Drop for VectorArt {
 impl Drop for VectorArt {
     fn drop(&mut self) {
     fn drop(&mut self) {
         let atom = self.renderer.make_guard(gfxtag!("VectorArt::drop"));
         let atom = self.renderer.make_guard(gfxtag!("VectorArt::drop"));
-        self.renderer
-            .replace_draw_calls(Some(atom.batch_id), vec![(self.dc_key, Default::default())]);
+        self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
     }
 }
 }
 
 

+ 68 - 51
bin/app/src/ui/vid/mod.rs

@@ -33,7 +33,7 @@ use crate::{
     ExecutorPtr,
     ExecutorPtr,
 };
 };
 
 
-use super::{DrawUpdate, OnModify, UIObject};
+use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
 
 
 mod decode;
 mod decode;
 #[allow(dead_code)]
 #[allow(dead_code)]
@@ -71,6 +71,7 @@ impl Av1VideoData {
 pub struct Video {
 pub struct Video {
     node: SceneNodeWeak,
     node: SceneNodeWeak,
     renderer: Renderer,
     renderer: Renderer,
+    redraw: RedrawTrigger,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     load_tasks: SyncMutex<Vec<smol::Task<()>>>,
     load_tasks: SyncMutex<Vec<smol::Task<()>>>,
     ex: ExecutorPtr,
     ex: ExecutorPtr,
@@ -86,11 +87,19 @@ pub struct Video {
     priority: PropertyUint32,
     priority: PropertyUint32,
     path: PropertyStr,
     path: PropertyStr,
 
 
+    /// Cached draw instructions. `None` means stale.
+    draw_cache: SyncMutex<Option<Vec<DrawInstruction>>>,
+
     parent_rect: SyncMutex<Option<Rectangle>>,
     parent_rect: SyncMutex<Option<Rectangle>>,
 }
 }
 
 
 impl Video {
 impl Video {
-    pub async fn new(node: SceneNodeWeak, renderer: Renderer, ex: ExecutorPtr) -> Pimpl {
+    pub async fn new(
+        node: SceneNodeWeak,
+        renderer: Renderer,
+        redraw: RedrawTrigger,
+        ex: ExecutorPtr,
+    ) -> Pimpl {
         let node_ref = &node.upgrade().unwrap();
         let node_ref = &node.upgrade().unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let uv = PropertyRect::wrap(node_ref, Role::Internal, "uv").unwrap();
         let uv = PropertyRect::wrap(node_ref, Role::Internal, "uv").unwrap();
@@ -101,6 +110,7 @@ impl Video {
         let self_ = Arc::new(Self {
         let self_ = Arc::new(Self {
             node,
             node,
             renderer,
             renderer,
+            redraw,
             tasks: SyncMutex::new(vec![]),
             tasks: SyncMutex::new(vec![]),
             load_tasks: SyncMutex::new(vec![]),
             load_tasks: SyncMutex::new(vec![]),
             ex,
             ex,
@@ -116,15 +126,18 @@ impl Video {
             priority,
             priority,
             path,
             path,
 
 
+            draw_cache: SyncMutex::new(None),
+
             parent_rect: SyncMutex::new(None),
             parent_rect: SyncMutex::new(None),
         });
         });
 
 
         Pimpl::Video(self_)
         Pimpl::Video(self_)
     }
     }
 
 
-    async fn reload(self: Arc<Self>, batch: BatchGuardPtr) {
-        self.load_video();
-        self.redraw(batch).await;
+    async fn reload(self_: Arc<Self>, _batch: BatchGuardPtr) {
+        self_.load_video();
+        *self_.draw_cache.lock() = None;
+        self_.redraw.trigger();
     }
     }
 
 
     fn load_video(&self) {
     fn load_video(&self) {
@@ -138,21 +151,6 @@ impl Video {
         *self._decoder_handle.lock() = Some(decoder_handle);
         *self._decoder_handle.lock() = Some(decoder_handle);
     }
     }
 
 
-    #[instrument(target = "ui::video")]
-    async fn redraw(self: Arc<Self>, batch: BatchGuardPtr) {
-        let Some(parent_rect) = self.parent_rect.lock().clone() else {
-            warn!(target: "ui:video", "Skip draw since parent rect is empty");
-            return
-        };
-
-        let atom = &mut batch.spawn();
-        let Some(draw_update) = self.get_draw_calls(atom, parent_rect) else {
-            error!(target: "ui:video", "Video failed to draw");
-            return
-        };
-        self.renderer.replace_draw_calls(Some(batch.id), draw_update.draw_calls);
-    }
-
     fn regen_mesh(&self) -> MeshInfo {
     fn regen_mesh(&self) -> MeshInfo {
         let rect = self.rect.get();
         let rect = self.rect.get();
         let uv = self.uv.get();
         let uv = self.uv.get();
@@ -162,15 +160,10 @@ impl Video {
         mesh.alloc(&self.renderer)
         mesh.alloc(&self.renderer)
     }
     }
 
 
-    fn get_draw_calls(
-        &self,
-        atom: &mut PropertyAtomicGuard,
-        parent_rect: Rectangle,
-    ) -> Option<DrawUpdate> {
-        self.rect.eval(atom, &parent_rect).ok()?;
-        let rect = self.rect.get();
-        self.uv.eval(atom, &rect).ok()?;
-
+    /// Wire decoder output into the renderer anim and build the video
+    /// instructions. Called only when the draw cache is stale or the
+    /// rect changed; the anim then advances renderer-side.
+    fn make_instrs(&self, rect: &Rectangle) -> Option<Vec<DrawInstruction>> {
         let mesh = self.regen_mesh();
         let mesh = self.regen_mesh();
 
 
         let (vid_data, tsubs) = {
         let (vid_data, tsubs) = {
@@ -244,22 +237,11 @@ impl Video {
 
 
         debug!(target: "ui::video", "Loaded {loaded_n_frames} / {total_frames} frames");
         debug!(target: "ui::video", "Loaded {loaded_n_frames} / {total_frames} frames");
 
 
-        Some(DrawUpdate {
-            key: self.dc_key,
-            draw_calls: vec![(
-                self.dc_key,
-                DrawCall::new(
-                    vec![
-                        DrawInstruction::SetPipeline(GraphicPipeline::YUV),
-                        DrawInstruction::Move(rect.pos()),
-                        DrawInstruction::Animation(vid_data.anim.clone()),
-                    ],
-                    vec![],
-                    self.z_index.get(),
-                    "vid",
-                ),
-            )],
-        })
+        Some(vec![
+            DrawInstruction::SetPipeline(GraphicPipeline::YUV),
+            DrawInstruction::Move(rect.pos()),
+            DrawInstruction::Animation(vid_data.anim.clone()),
+        ])
     }
     }
 }
 }
 
 
@@ -277,9 +259,18 @@ impl UIObject for Video {
         let me = Arc::downgrade(&self);
         let me = Arc::downgrade(&self);
 
 
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
         let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
-        on_modify.when_change(self.rect.prop(), Self::redraw);
-        on_modify.when_change(self.uv.prop(), Self::redraw);
-        on_modify.when_change(self.z_index.prop(), Self::redraw);
+        on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.uv.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.z_index.prop(), |self_, _| async move {
+            *self_.draw_cache.lock() = None;
+            self_.redraw.trigger();
+        });
         on_modify.when_change(self.path.prop(), Self::reload);
         on_modify.when_change(self.path.prop(), Self::reload);
 
 
         *self.tasks.lock() = on_modify.tasks;
         *self.tasks.lock() = on_modify.tasks;
@@ -289,6 +280,7 @@ impl UIObject for Video {
         self.tasks.lock().clear();
         self.tasks.lock().clear();
         *self.parent_rect.lock() = None;
         *self.parent_rect.lock() = None;
         *self.vid_data.lock() = None;
         *self.vid_data.lock() = None;
+        *self.draw_cache.lock() = None;
         // Threads terminate naturally when channels close
         // Threads terminate naturally when channels close
     }
     }
 
 
@@ -299,15 +291,40 @@ impl UIObject for Video {
         atom: &mut PropertyAtomicGuard,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
     ) -> Option<DrawUpdate> {
         *self.parent_rect.lock() = Some(parent_rect);
         *self.parent_rect.lock() = Some(parent_rect);
-        self.get_draw_calls(atom, parent_rect)
+
+        // Rect property is its own memo: compare before/after eval.
+        let prev_rect = self.rect.get();
+        self.rect.eval(atom, &parent_rect).ok()?;
+        let rect = self.rect.get();
+        let rect_changed = rect != prev_rect;
+        self.uv.eval(atom, &rect).ok()?;
+
+        // Compute under the lock so a concurrent invalidation lands
+        // before or after, never between. A video that has not loaded
+        // yet stays uncached so the next pass retries.
+        let mut cache = self.draw_cache.lock();
+        if cache.is_none() || rect_changed {
+            if let Some(instrs) = self.make_instrs(&rect) {
+                *cache = Some(instrs);
+            }
+        }
+        let instrs = cache.clone()?;
+        drop(cache);
+
+        Some(DrawUpdate {
+            key: self.dc_key,
+            draw_calls: vec![(
+                self.dc_key,
+                DrawCall::new(instrs, vec![], self.z_index.get(), "vid"),
+            )],
+        })
     }
     }
 }
 }
 
 
 impl Drop for Video {
 impl Drop for Video {
     fn drop(&mut self) {
     fn drop(&mut self) {
         let atom = self.renderer.make_guard(gfxtag!("Video::drop"));
         let atom = self.renderer.make_guard(gfxtag!("Video::drop"));
-        self.renderer
-            .replace_draw_calls(Some(atom.batch_id), vec![(self.dc_key, Default::default())]);
+        self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
     }
 }
 }
 
 

+ 5 - 11
bin/app/src/ui/win/mod.rs

@@ -27,7 +27,7 @@ use crate::{
         gfxtag, DrawCall, DrawInstruction, GraphicsEventCharSub, GraphicsEventKeyDownSub,
         gfxtag, DrawCall, DrawInstruction, GraphicsEventCharSub, GraphicsEventKeyDownSub,
         GraphicsEventKeyUpSub, GraphicsEventMouseButtonDownSub, GraphicsEventMouseButtonUpSub,
         GraphicsEventKeyUpSub, GraphicsEventMouseButtonDownSub, GraphicsEventMouseButtonUpSub,
         GraphicsEventMouseMoveSub, GraphicsEventMouseWheelSub, GraphicsEventPublisherPtr,
         GraphicsEventMouseMoveSub, GraphicsEventMouseWheelSub, GraphicsEventPublisherPtr,
-        GraphicsEventTouchSub, Point, Rectangle, RenderApi, Renderer, RendererSync,
+        GraphicsEventTouchSub, Point, Rectangle, RenderApi, Renderer,
     },
     },
     prop::{
     prop::{
         BatchGuardPtr, PropertyAtomicGuard, PropertyDimension, PropertyFloat32, PropertyStr, Role,
         BatchGuardPtr, PropertyAtomicGuard, PropertyDimension, PropertyFloat32, PropertyStr, Role,
@@ -223,7 +223,7 @@ impl Window {
                     // Insets are set with an internal role, so draw-pass
                     // Insets are set with an internal role, so draw-pass
                     // widgets skip the echo notifications. Trigger the pass
                     // widgets skip the echo notifications. Trigger the pass
                     // explicitly so the new insets get laid out.
                     // explicitly so the new insets get laid out.
-                    self_.redraw.trigger();
+                    self_.redraw_tx.trigger();
                 }
                 }
             })
             })
         };
         };
@@ -515,17 +515,11 @@ impl Window {
         }
         }
     }
     }
 
 
-    pub fn handle_touch_sync(
-        &self,
-        renderer: &RendererSync,
-        phase: TouchPhase,
-        id: u64,
-        mut touch_pos: Point,
-    ) -> bool {
+    pub fn handle_touch_sync(&self, phase: TouchPhase, id: u64, mut touch_pos: Point) -> bool {
         self.local_scale(&mut touch_pos);
         self.local_scale(&mut touch_pos);
         for child in self.get_children() {
         for child in self.get_children() {
             let obj = get_ui_object3(&child);
             let obj = get_ui_object3(&child);
-            if obj.handle_touch_sync(renderer, phase, id, touch_pos) {
+            if obj.handle_touch_sync(phase, id, touch_pos) {
                 return true
                 return true
             }
             }
         }
         }
@@ -566,7 +560,7 @@ impl Window {
         draw_calls.push((0, dc));
         draw_calls.push((0, dc));
         //t!("  => {:?}", draw_calls);
         //t!("  => {:?}", draw_calls);
 
 
-        self.renderer.replace_draw_calls(Some(atom.batch_id), draw_calls);
+        self.renderer.replace_draw_calls(draw_calls);
     }
     }
 
 
     async fn reload_locale(&self, atom: &mut PropertyAtomicGuard) {
     async fn reload_locale(&self, atom: &mut PropertyAtomicGuard) {