Browse Source

app: replace Renderer::make_guard() with RedrawTrigger::make_guard() to prevent partial draws.

Since draw-pass serialization, Renderer::make_guard() was a noop stub and instead we call redraw.trigger().

But a chain of property changes can still result in partial draws, so luckily we still had the Renderer guards that could now be migrated to RedrawTrigger guards instead.

This means there is a single draw update once a series of connected/dependent properties updates sharing the same context has settled (and the guard expires).
x 1 day ago
parent
commit
3ba3930f05

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

@@ -194,7 +194,6 @@ pub async fn make(
     db: &sled::Db,
     i18n_fish: &I18nBabelFish,
     emoji_meshes: emoji_picker::EmojiMeshesPtr,
-    is_first_time: bool,
     redraw: RedrawTrigger,
 ) -> SceneNodePtr {
     let window_scale =
@@ -335,10 +334,10 @@ pub async fn make(
     let sg_root2 = sg_root.clone();
     let layer_node2 = layer_node.clone();
     let chatview_is_visible = PropertyBool::wrap(&layer_node, Role::App, "is_visible", 0).unwrap();
-    let renderer2 = renderer.clone();
+    let redraw2 = redraw.clone();
     let goback = async move || {
         info!(target: "app::chat", "clicked back");
-        let atom = &mut renderer2.make_guard(gfxtag!("goback action"));
+        let atom = &mut redraw2.make_guard(gfxtag!("goback action"));
 
         let editz_node = layer_node2.lookup_node("/content/editz").unwrap();
         editz_node.call_method("unfocus", vec![]).await.unwrap();
@@ -739,13 +738,13 @@ pub async fn make(
     let select_is_visible = PropertyBool::wrap(&select_layer, Role::App, "is_visible", 0).unwrap();
     let back_btn_bg_node2 = back_btn_bg_node.clone();
     let sg_root2 = sg_root.clone();
-    let renderer2 = renderer.clone();
+    let redraw2 = redraw.clone();
     let (slot, recvr) = Slot::new("select_changed_slot");
     chatview_node.register("select_changed", slot).unwrap();
     let listen_select = ex.spawn(async move {
         while let Ok(data) = recvr.recv().await {
             let Ok(selected) = bool::decode(&mut std::io::Cursor::new(&data)) else { continue };
-            let atom = &mut renderer2.make_guard(gfxtag!("select_changed"));
+            let atom = &mut redraw2.make_guard(gfxtag!("select_changed"));
             select_is_visible.set(atom, selected);
             back_btn_bg_node2.set_property_bool(atom, Role::App, "is_visible", !selected).unwrap();
             if let Some(netstatus_layer) = sg_root2.lookup_node("/window/content/netstatus_layer") {
@@ -1058,18 +1057,18 @@ pub async fn make(
     let editz_text2 = editz_text.clone();
     let channel2 = channel.to_string();
     let sg_root2 = sg_root.clone();
-    let renderer2 = renderer.clone();
+    let redraw2 = redraw.clone();
     let sendmsg = move || {
         let editz_text = editz_text2.clone();
         let channel = channel2.clone();
         let sg_root = sg_root2.clone();
         let chatview_node = chatview_node.clone();
-        let renderer = renderer2.clone();
+        let redraw = redraw2.clone();
         async move {
             let mut text = editz_text.get();
             info!(target: "app::chat", "Send '{text}' to channel: {channel}");
             {
-                let atom = &mut renderer.make_guard(gfxtag!("sendmsg clear edit"));
+                let atom = &mut redraw.make_guard(gfxtag!("sendmsg clear edit"));
                 editz_text.set(atom, "");
             }
 
@@ -1082,7 +1081,7 @@ pub async fn make(
                 let nick = text.split_whitespace().nth(1).unwrap_or("anon");
                 info!(target: "app::chat", "Setting nick to: {nick}");
                 {
-                    let atom = &mut renderer.make_guard(gfxtag!("sendmsg action"));
+                    let atom = &mut redraw.make_guard(gfxtag!("sendmsg action"));
                     darkirc.set_property_str(atom, Role::App, "nick", nick).unwrap();
                 }
 
@@ -1177,7 +1176,7 @@ pub async fn make(
     let (slot, recvr) = Slot::new("emoji_clicked");
     let chatedit_node2 = chatedit_node.clone();
     node.register("click", slot).unwrap();
-    let renderer2 = renderer.clone();
+    let redraw2 = redraw.clone();
     let listen_click = ex.spawn(async move {
         let mut panel_height = if cfg!(target_os = "android") {
             let keyb_height = android_keyboard_height();
@@ -1192,7 +1191,7 @@ pub async fn make(
 
         while let Ok(_) = recvr.recv().await {
             info!(target: "app::chat", "clicked emoji");
-            let atom = &mut renderer2.make_guard(gfxtag!("emoji click action"));
+            let atom = &mut redraw2.make_guard(gfxtag!("emoji click action"));
 
             if cfg!(target_os = "android") {
                 let keyb_height = android_keyboard_height();
@@ -1269,11 +1268,11 @@ pub async fn make(
     let (slot, recvr) = Slot::new("nickcmd_clicked");
     node.register("click", slot).unwrap();
     let editz_text2 = editz_text.clone();
-    let renderer2 = renderer.clone();
+    let redraw2 = redraw.clone();
     let listen_click = ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             info!(target: "app::chat", "clicked /nick");
-            let atom = &mut renderer2.make_guard(gfxtag!("nickcmd_clicked action"));
+            let atom = &mut redraw2.make_guard(gfxtag!("nickcmd_clicked action"));
             // This will autohide this popup due to ending in a space.
             // Setting the property will retrigger the logic whether to show popup.
             editz_text2.set(atom, "/nick ");
@@ -1378,10 +1377,10 @@ pub async fn make(
     cmd_layer_node.link(node);
 
     let editz_text_sub = editz_text.prop().subscribe_modify();
-    let renderer = renderer.clone();
+    let redraw = redraw.clone();
     let editz_text_task = ex.spawn(async move {
         while let Ok(_) = editz_text_sub.receive().await {
-            let atom = &mut renderer.make_guard(gfxtag!("chatedit txt changed"));
+            let atom = &mut redraw.make_guard(gfxtag!("chatedit txt changed"));
 
             let text = editz_text.get();
             debug!(target: "app::chat", "text changed: {text}");

+ 16 - 19
bin/app/src/app/schema/menu/channel.rs

@@ -50,7 +50,7 @@ use crate::{
     expr,
     gfx::gfxtag,
     mesh::{COLOR_CYAN, COLOR_INACTIVE, COLOR_MINT, COLOR_MINT_OP, MINT_BTN_GRADIENT},
-    prop::{PropertyBool, PropertyFloat32, Role},
+    prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, Role},
     scene::{Pimpl, SceneNodePtr, Slot},
     shape,
     ui::{
@@ -171,7 +171,6 @@ pub async fn make(
     channels_tree: sled::Tree,
     db: &sled::Db,
     emoji_meshes: EmojiMeshesPtr,
-    is_first_time: bool,
 ) -> SceneNodePtr {
     let mut cc = expr::Compiler::new();
     cc.add_const_f32("CHATEDIT_PAD", CHATEDIT_PAD);
@@ -189,8 +188,7 @@ pub async fn make(
     cc.add_const_f32("COPY_BTN_SIZE", COPY_BTN_SIZE);
     cc.add_const_f32("CHANNEL_ITEM_HEIGHT", CHANNEL_ITEM_HEIGHT);
 
-    let renderer = app.renderer.clone();
-    let atom = &mut renderer.make_guard(gfxtag!("write_click"));
+    let atom = &mut PropertyAtomicGuard::none();
 
     // Header
     let node = create_vector_art("header_bg");
@@ -281,13 +279,13 @@ pub async fn make(
     let sg_root = app.sg_root.clone();
     let contact_vis = contact_is_visible.clone();
     let channel_vis = channel_is_visible.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let menu_node = sg_root.lookup_node("/window/content/chat/menu_layer").unwrap();
     let netstatus_layer = sg_root.lookup_node("/window/content/netstatus_layer").unwrap();
     let content_go = content.clone();
     let goback = async move || {
         info!(target: "app::chat", "clicked back");
-        let atom = &mut renderer.make_guard(gfxtag!("go back action"));
+        let atom = &mut redraw.make_guard(gfxtag!("go back action"));
 
         unfocus_editors(&content_go).await;
 
@@ -603,11 +601,11 @@ pub async fn make(
     let sg_root = app.sg_root.clone();
     let contact_vis = contact_is_visible.clone();
     let channel_vis = channel_is_visible.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let content_tab = content.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("channels_click"));
+            let atom = &mut redraw.make_guard(gfxtag!("channels_click"));
 
             unfocus_editors(&content_tab).await;
 
@@ -698,11 +696,11 @@ pub async fn make(
     let sg_root = app.sg_root.clone();
     let contact_vis = contact_is_visible.clone();
     let channel_vis = channel_is_visible.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let content_tab = content.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("contacts_click"));
+            let atom = &mut redraw.make_guard(gfxtag!("contacts_click"));
 
             unfocus_editors(&content_tab).await;
 
@@ -1260,14 +1258,14 @@ pub async fn make(
     let (slot, recvr) = Slot::new("receive_copy_clicked");
     node.register("click", slot).unwrap();
     let secedit_node2 = secedit_node.clone();
-    let renderer2 = app.renderer.clone();
+    let redraw2 = app.redraw_trigger.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             debug!(target: "app::menu", "secret paste button clicked");
             match miniquad::window::clipboard_get() {
                 Some(clipboard_text) => {
                     let text_prop = secedit_node2.get_property("text").unwrap();
-                    let atom = &mut renderer2.make_guard(gfxtag!("secret paste"));
+                    let atom = &mut redraw2.make_guard(gfxtag!("secret paste"));
                     text_prop.set_str(atom, Role::App, 0, &clipboard_text).unwrap();
                     if let crate::scene::Pimpl::Edit(edit) = secedit_node2.pimpl() {
                         edit.on_text_prop_changed();
@@ -1422,14 +1420,14 @@ pub async fn make(
     let (slot, recvr) = Slot::new("gensecret_clicked");
     node.register("click", slot).unwrap();
     let secedit_node3 = secedit_node.clone();
-    let renderer_clone = app.renderer.clone();
+    let redraw_clone = app.redraw_trigger.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             debug!(target: "app::menu", "gen secret button clicked");
             let secret_bytes: [u8; 32] = OsRng.gen();
             let secret = bs58::encode(secret_bytes).into_string();
             let text_prop = secedit_node3.get_property("text").unwrap();
-            let atom = &mut renderer_clone.make_guard(gfxtag!("gen secret"));
+            let atom = &mut redraw_clone.make_guard(gfxtag!("gen secret"));
             text_prop.set_str(atom, Role::App, 0, &secret).unwrap();
             if let crate::scene::Pimpl::Edit(edit) = secedit_node3.pimpl() {
                 edit.on_text_prop_changed();
@@ -1547,7 +1545,7 @@ pub async fn make(
     let nickedit2 = nickedit_node.clone();
     let secedit2 = secedit_node.clone();
     let menu_prop2 = menu_node.get_property("items").unwrap();
-    let renderer2 = app.renderer.clone();
+    let redraw2 = app.redraw_trigger.clone();
     let sg_root2 = app.sg_root.clone();
 
     let save_channel = app.ex.spawn(async move {
@@ -1594,12 +1592,12 @@ pub async fn make(
             channels_tree2.insert(key, val).unwrap();
             let _ = channels_tree2.flush_async().await;
 
-            let atom = &mut renderer2.make_guard(gfxtag!("add_channel"));
+            let atom = &mut redraw2.make_guard(gfxtag!("add_channel"));
             menu_prop2.push_str(atom, Role::App, &channel_name).unwrap();
 
             i!("Successfully saved channel: {}", channel_name);
 
-            let atom = &mut renderer2.make_guard(gfxtag!("clear_channel_fields"));
+            let atom = &mut redraw2.make_guard(gfxtag!("clear_channel_fields"));
             name_prop.set_str(atom, Role::App, 0, "").unwrap();
             secret_prop.set_str(atom, Role::App, 0, "").unwrap();
         }
@@ -1639,7 +1637,7 @@ pub async fn make(
             i!("Selected channel: {channel}");
             let path = format!("/window/content/{}_chat_layer", &channel);
 
-            let atom = &mut renderer.make_guard(gfxtag!("channel_selected"));
+            let atom = &mut redraw2.make_guard(gfxtag!("channel_selected"));
 
             // Check if chat layer already exists
             if let Some(node) = sg_root.lookup_node(&path) {
@@ -1659,7 +1657,6 @@ pub async fn make(
                 &db2,
                 &i18n_fish2,
                 emoji_meshes2.clone(),
-                is_first_time,
                 redraw2.clone(),
             )
             .await;

+ 14 - 17
bin/app/src/app/schema/menu/contact.rs

@@ -50,7 +50,7 @@ use crate::{
     expr,
     gfx::gfxtag,
     mesh::{COLOR_CYAN, COLOR_INACTIVE, COLOR_MINT, COLOR_MINT_OP, MINT_BTN_GRADIENT},
-    prop::{PropertyBool, PropertyFloat32, Role},
+    prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, Role},
     scene::{Pimpl, SceneNodePtr, Slot},
     shape,
     ui::{
@@ -171,7 +171,6 @@ pub async fn make(
     contacts_tree: sled::Tree,
     db: &sled::Db,
     emoji_meshes: EmojiMeshesPtr,
-    is_first_time: bool,
 ) -> SceneNodePtr {
     let mut cc = expr::Compiler::new();
     cc.add_const_f32("CHATEDIT_PAD", CHATEDIT_PAD);
@@ -189,8 +188,7 @@ pub async fn make(
     cc.add_const_f32("COPY_BTN_SIZE", COPY_BTN_SIZE);
     cc.add_const_f32("CHANNEL_ITEM_HEIGHT", CHANNEL_ITEM_HEIGHT);
 
-    let renderer = app.renderer.clone();
-    let atom = &mut renderer.make_guard(gfxtag!("write_click"));
+    let atom = &mut PropertyAtomicGuard::none();
 
     // Header
     let node = create_vector_art("header_bg");
@@ -281,13 +279,13 @@ pub async fn make(
     let sg_root = app.sg_root.clone();
     let contact_vis = contact_is_visible.clone();
     let channel_vis = channel_is_visible.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let menu_node = sg_root.lookup_node("/window/content/chat/menu_layer").unwrap();
     let netstatus_layer = sg_root.lookup_node("/window/content/netstatus_layer").unwrap();
     let content_go = content.clone();
     let goback = async move || {
         info!(target: "app::chat", "clicked back");
-        let atom = &mut renderer.make_guard(gfxtag!("go back action"));
+        let atom = &mut redraw.make_guard(gfxtag!("go back action"));
 
         unfocus_editors(&content_go).await;
 
@@ -603,11 +601,11 @@ pub async fn make(
     let sg_root = app.sg_root.clone();
     let contact_vis = contact_is_visible.clone();
     let channel_vis = channel_is_visible.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let content_tab = content.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("contacts_click"));
+            let atom = &mut redraw.make_guard(gfxtag!("contacts_click"));
 
             unfocus_editors(&content_tab).await;
 
@@ -694,11 +692,11 @@ pub async fn make(
     let sg_root = app.sg_root.clone();
     let contact_vis = contact_is_visible.clone();
     let channel_vis = channel_is_visible.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let content_tab = content.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("channels_click"));
+            let atom = &mut redraw.make_guard(gfxtag!("channels_click"));
 
             unfocus_editors(&content_tab).await;
 
@@ -1263,14 +1261,14 @@ pub async fn make(
     let (slot, recvr) = Slot::new("receive_copy_clicked");
     node.register("click", slot).unwrap();
     let secedit_node2 = secedit_node.clone();
-    let renderer_clone = app.renderer.clone();
+    let redraw_clone = app.redraw_trigger.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             debug!(target: "app::menu", "secret paste button clicked");
             match crate::clipboard::get() {
                 Some(clipboard_text) => {
                     let text_prop = secedit_node2.get_property("text").unwrap();
-                    let atom = &mut renderer_clone.make_guard(gfxtag!("secret paste"));
+                    let atom = &mut redraw_clone.make_guard(gfxtag!("secret paste"));
                     text_prop.set_str(atom, Role::App, 0, &clipboard_text).unwrap();
                     if let crate::scene::Pimpl::Edit(edit) = secedit_node2.pimpl() {
                         edit.on_text_prop_changed();
@@ -1468,7 +1466,7 @@ pub async fn make(
     let nickedit2 = nickedit_node.clone();
     let secedit2 = secedit_node.clone();
     let menu_prop2 = menu_node.get_property("items").unwrap();
-    let renderer2 = app.renderer.clone();
+    let redraw2 = app.redraw_trigger.clone();
     let sg_root2 = app.sg_root.clone();
 
     let save_contact = app.ex.spawn(async move {
@@ -1506,11 +1504,11 @@ pub async fn make(
             let _ = contacts_tree2.flush_async().await;
 
             let contact_name = format!("@{}", name);
-            let atom = &mut renderer2.make_guard(gfxtag!("add_contact"));
+            let atom = &mut redraw2.make_guard(gfxtag!("add_contact"));
             menu_prop2.push_str(atom, Role::App, &contact_name).unwrap();
             i!("Successfully saved contact: {}", contact_name);
 
-            let atom = &mut renderer2.make_guard(gfxtag!("clear_contact_fields"));
+            let atom = &mut redraw2.make_guard(gfxtag!("clear_contact_fields"));
             name_prop.set_str(atom, Role::App, 0, "").unwrap();
             public_prop.set_str(atom, Role::App, 0, "").unwrap();
         }
@@ -1535,7 +1533,7 @@ pub async fn make(
             let contact: String = deserialize(&data).unwrap();
             i!("Selected contact: {contact}");
             let path = format!("/window/content/{}_chat_layer", &contact);
-            let atom = &mut renderer.make_guard(gfxtag!("contact_selected"));
+            let atom = &mut redraw2.make_guard(gfxtag!("contact_selected"));
 
             if let Some(node) = sg_root.lookup_node(&path) {
                 node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
@@ -1553,7 +1551,6 @@ pub async fn make(
                 &db2,
                 &i18n_fish2,
                 emoji_meshes2.clone(),
-                is_first_time,
                 redraw2.clone(),
             )
             .await;

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

@@ -266,13 +266,13 @@ impl EditButtons {
         // Subscribe to edit_active signal
         let (slot, recvr) = Slot::new("edit_activated");
         menu_node.register("edit_active", slot).unwrap();
-        let renderer = app.renderer.clone();
+        let redraw = app.redraw_trigger.clone();
         let editlayer = self.editlayer_is_visible.clone();
         let sibling_on = sibling.clone();
         let task = app.ex.spawn(async move {
             while let Ok(_) = recvr.recv().await {
                 debug!(target: "app::menu", "menu edit active");
-                let atom = &mut renderer.make_guard(gfxtag!("edit_active"));
+                let atom = &mut redraw.make_guard(gfxtag!("edit_active"));
                 if let Some(s) = &sibling_on {
                     s.set(atom, false);
                 }
@@ -313,12 +313,12 @@ impl EditButtons {
         let (slot, recvr) = Slot::new(slot_name);
         btn.register("click", slot).unwrap();
         let menu_node = menu_node.clone();
-        let renderer = app.renderer.clone();
+        let redraw = app.redraw_trigger.clone();
         let editlayer = self.editlayer_is_visible.clone();
         let task = app.ex.spawn(async move {
             while let Ok(_) = recvr.recv().await {
                 menu_node.call_method(method, vec![]).await.unwrap();
-                let atom = &mut renderer.make_guard(gfxtag!(slot_name));
+                let atom = &mut redraw.make_guard(gfxtag!(slot_name));
                 editlayer.set(atom, false);
                 if let Some(s) = &sibling {
                     s.set(atom, true);

+ 7 - 11
bin/app/src/app/schema/menu/mod.rs

@@ -117,7 +117,6 @@ pub async fn make(
     contacts_tree: sled::Tree,
     db: &sled::Db,
     emoji_meshes: EmojiMeshesPtr,
-    is_first_time: bool,
 ) {
     let window_scale = PropertyFloat32::wrap(
         &app.sg_root.lookup_node("/window").unwrap(),
@@ -127,8 +126,7 @@ pub async fn make(
     )
     .unwrap();
 
-    let renderer = app.renderer.clone();
-    let atom = &mut renderer.make_guard(gfxtag!("setup"));
+    let atom = &mut PropertyAtomicGuard::none();
 
     // Create chat container layer
     let chat_layer = create_layer("chat");
@@ -216,7 +214,6 @@ pub async fn make(
         contacts_tree.clone(),
         db,
         emoji_meshes.clone(),
-        is_first_time,
     )
     .await;
 
@@ -231,7 +228,6 @@ pub async fn make(
         channels_tree.clone(),
         db,
         emoji_meshes.clone(),
-        is_first_time,
     )
     .await;
 
@@ -372,13 +368,13 @@ pub async fn make(
 
     let (slot, recvr) = Slot::new("write_clicked");
     node.register("click", slot).unwrap();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let contact_is_visible =
         PropertyBool::wrap(&contact_layer, Role::App, "is_visible", 0).unwrap();
     let menulayer_is_visible1 = menulayer_is_visible.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("write_click"));
+            let atom = &mut redraw.make_guard(gfxtag!("write_click"));
             contact_is_visible.set(atom, true);
             menulayer_is_visible1.set(atom, false);
         }
@@ -454,7 +450,7 @@ pub async fn make(
     node.register("select", slot).unwrap();
     let sg_root = app.sg_root.clone();
     let menu_is_visible = PropertyBool::wrap(&layer_node, Role::App, "is_visible", 0).unwrap();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let role1_group = node.get_property("role1_group").unwrap();
     let role2_group = node.get_property("role2_group").unwrap();
     let listen_click = app.ex.spawn(async move {
@@ -462,7 +458,7 @@ pub async fn make(
             let channel: String = deserialize(&data).unwrap();
             let path = format!("/window/content/chat/{}_chat_layer", channel);
             if let Some(node) = sg_root.lookup_node(path) {
-                let atom = &mut renderer.make_guard(gfxtag!("channel_clicked"));
+                let atom = &mut redraw.make_guard(gfxtag!("channel_clicked"));
                 info!(target: "app::menu", "clicked: {channel}!");
                 role1_group.remove_str_item(atom, Role::App, &channel);
                 role2_group.remove_str_item(atom, Role::App, &channel);
@@ -589,11 +585,11 @@ pub async fn setup_wallet_button(app: &App, menu_layer: SceneNodePtr, i18n_fish:
         0,
     )
     .unwrap();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let menulayer_is_visible2 = menulayer_is_visible.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("wallet_click"));
+            let atom = &mut redraw.make_guard(gfxtag!("wallet_click"));
             wallet_is_visible.set(atom, true);
             menulayer_is_visible2.set(atom, false);
         }

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

@@ -20,7 +20,7 @@ use darkfi::system::msleep;
 use darkfi_serial::{deserialize, Encodable};
 use indoc::indoc;
 use sled_overlay::sled;
-use std::{fs::File, io::Write};
+use std::io::Write;
 
 use crate::{
     app::{
@@ -72,10 +72,6 @@ mod ui_consts {
         get_external_storage_path().join("chatdb")
     }
 
-    pub fn get_first_time_filename() -> PathBuf {
-        get_appdata_path().join("first_time")
-    }
-
     pub fn get_settingsdb_path() -> PathBuf {
         get_appdata_path().join("settings")
     }
@@ -100,10 +96,6 @@ mod desktop_paths {
         dirs::data_local_dir().unwrap().join("darkfi/app/chatdb")
     }
 
-    pub fn get_first_time_filename() -> PathBuf {
-        dirs::cache_dir().unwrap().join("darkfi/app/first_time")
-    }
-
     pub fn get_settingsdb_path() -> PathBuf {
         dirs::cache_dir().unwrap().join("darkfi/app/settings")
     }
@@ -217,7 +209,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     node.register("shortcut", slot).unwrap();
     let window_scale = app.sg_root.lookup_node("/setting/scale").unwrap();
     let window_scale2 = window_scale.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let listen_zoom = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             let scale = 0.9 * window_scale2.get_property_f32("value").unwrap();
@@ -230,7 +222,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
                 scale.encode(&mut file).unwrap();
             }
 
-            let atom = &mut renderer.make_guard(gfxtag!("zoom_out shortcut"));
+            let atom = &mut redraw.make_guard(gfxtag!("zoom_out shortcut"));
             window_scale2.set_property_f32(atom, Role::User, "value", scale).unwrap();
         }
     });
@@ -245,7 +237,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     let (slot, recvr) = Slot::new("zoom_in_pressed");
     node.register("shortcut", slot).unwrap();
     let window_scale2 = window_scale.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let listen_zoom = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             let scale = 1.1 * window_scale2.get_property_f32("value").unwrap();
@@ -258,7 +250,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
                 scale.encode(&mut file).unwrap();
             }
 
-            let atom = &mut renderer.make_guard(gfxtag!("zoom_in shortcut"));
+            let atom = &mut redraw.make_guard(gfxtag!("zoom_in shortcut"));
             window_scale2.set_property_f32(atom, Role::User, "value", scale).unwrap();
         }
     });
@@ -523,7 +515,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     prop.set_f32(atom, Role::App, 3, NETSTATUS_ICON_SIZE).unwrap();
 
     let sg_root = app.sg_root.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let (slot, recvr) = Slot::new("reconnect_clicked");
     node.register("click", slot).unwrap();
     let reconnect_task = app.ex.spawn(async move {
@@ -535,7 +527,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
                 sg_root.lookup_node("/window/content/netstatus_layer/netstat_klik").unwrap();
 
             {
-                let atom = &mut renderer.make_guard(gfxtag!("netstat_klik_show"));
+                let atom = &mut redraw.make_guard(gfxtag!("netstat_klik_show"));
                 if let Err(e) = netstat_klik.set_property_bool(atom, Role::App, "is_visible", true)
                 {
                     e!("Failed to show netstat_klik: {e}");
@@ -558,7 +550,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
 
             // Hide netstat-klik icon
             {
-                let atom = &mut renderer.make_guard(gfxtag!("netstat_klik_hide"));
+                let atom = &mut redraw.make_guard(gfxtag!("netstat_klik_hide"));
                 if let Err(e) = netstat_klik.set_property_bool(atom, Role::App, "is_visible", false)
                 {
                     e!("Failed to hide netstat_klik: {e}");
@@ -674,15 +666,6 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
         }
     });
 
-    let is_first_time = !get_first_time_filename().exists();
-    if is_first_time {
-        let filename = get_first_time_filename();
-        if let Some(parent) = filename.parent() {
-            let _ = std::fs::create_dir_all(parent);
-        }
-        let _ = File::create(filename);
-    }
-
     let channels_tree = db.open_tree("channels").expect("cannot open channels tree");
     let contacts_tree = db.open_tree("contacts").expect("cannot open contacts tree");
 
@@ -708,7 +691,6 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
         contacts_tree.clone(),
         &db,
         emoji_meshes.clone(),
-        is_first_time,
     )
     .await;
     let chat_layer = app.sg_root.lookup_node("/window/content/chat").unwrap();
@@ -735,7 +717,6 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
             &db,
             i18n_fish,
             emoji_meshes.clone(),
-            is_first_time,
             app.redraw_trigger.clone(),
         )
         .await;

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

@@ -189,14 +189,14 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 2, code).unwrap();
     prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
 
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let main_is_visible2 = main_is_visible.clone();
     let sg_root = app.sg_root.clone();
     let (slot, recvr) = Slot::new("receive_clicked");
     node.register("click", slot).unwrap();
     let listen_click = app.ex.spawn(async move {
         while recvr.recv().await.is_ok() {
-            let atom = &mut renderer.make_guard(gfxtag!("receive button click"));
+            let atom = &mut redraw.make_guard(gfxtag!("receive button click"));
             main_is_visible2.set(atom, false);
 
             let receive_layer =
@@ -308,14 +308,14 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 2, code).unwrap();
     prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
 
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let sg_root = app.sg_root.clone();
     let main_is_visible3 = main_is_visible.clone();
     let (slot, recvr) = Slot::new("send_clicked");
     node.register("click", slot).unwrap();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("send button click"));
+            let atom = &mut redraw.make_guard(gfxtag!("send button click"));
             main_is_visible3.set(atom, false);
             let send_layer =
                 sg_root.lookup_node("/window/content/wallet/send_step1_layer").unwrap();
@@ -491,7 +491,7 @@ async fn create_chat_btn(
     prop.set_f32(atom, Role::App, 3, CHAT_BTN_SIZE).unwrap();
 
     let sg_root = app.sg_root.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let menu_is_visible = PropertyBool::wrap(
         &sg_root.lookup_node("/window/content/chat/menu_layer").unwrap(),
         Role::App,
@@ -511,7 +511,7 @@ async fn create_chat_btn(
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             info!(target: "app::wallet", "clicked back from wallet");
-            let atom = &mut renderer.make_guard(gfxtag!("wallet goback action"));
+            let atom = &mut redraw.make_guard(gfxtag!("wallet goback action"));
             wallet_is_visible.set(atom, false);
             menu_is_visible.set(atom, true);
         }

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

@@ -92,12 +92,12 @@ pub async fn make(
     let main_is_visible = PropertyBool::wrap(&main_layer, Role::App, "is_visible", 0).unwrap();
     let receive_is_visible =
         PropertyBool::wrap(&receive_layer, Role::App, "is_visible", 0).unwrap();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let (slot, recvr) = Slot::new("receive_back_clicked");
     node.register("click", slot).unwrap();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("receive back button"));
+            let atom = &mut redraw.make_guard(gfxtag!("receive back button"));
             receive_is_visible.set(atom, false);
             main_is_visible.set(atom, true);
         }

+ 2 - 2
bin/app/src/app/schema/wallet/send.rs

@@ -95,7 +95,7 @@ pub async fn make(
 
     // Add listener for tx built signal to update send button label and show fee
     let set_built_tx_sub = tx_status_layer.subscribe_method_call("set_built_tx").unwrap();
-    let renderer_for_built = app.renderer.clone();
+    let redraw_for_built = app.redraw_trigger.clone();
     let sg_root_for_built = app.sg_root.clone();
     app.tasks.lock().unwrap().push(app.ex.spawn(async move {
         while let Ok(mcall) = set_built_tx_sub.receive().await {
@@ -110,7 +110,7 @@ pub async fn make(
                 }
             }
 
-            let atom = &mut renderer_for_built.make_guard(gfxtag!("tx built - update send button"));
+            let atom = &mut redraw_for_built.make_guard(gfxtag!("tx built - update send button"));
 
             // Make send button active
             if let Some(send_label_node) = sg_root_for_built

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

@@ -106,14 +106,14 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
 
     let main_is_visible = PropertyBool::wrap(&main_layer, Role::App, "is_visible", 0).unwrap();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let main_is_visible1 = main_is_visible.clone();
     let step1_is_visible1 = step1_is_visible.clone();
     let (slot, recvr) = Slot::new("send_back_clicked");
     node.register("click", slot).unwrap();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("send back button"));
+            let atom = &mut redraw.make_guard(gfxtag!("send back button"));
             main_is_visible1.set(atom, true);
             step1_is_visible1.set(atom, false);
         }
@@ -179,7 +179,7 @@ pub async fn make(
     let (slot, recvr) = Slot::new("token_row_clicked");
     send_tokens_table.register("row_click", slot).unwrap();
     let sg_root = app.sg_root.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let send_tx_data2 = send_tx_data.clone();
     let step1_is_visible3 = step1_is_visible.clone();
     let listen_click = app.ex.spawn(async move {
@@ -191,7 +191,7 @@ pub async fn make(
                 data.token_id = Some(row.id);
                 drop(data);
 
-                let atom = &mut renderer.make_guard(gfxtag!("token selection"));
+                let atom = &mut redraw.make_guard(gfxtag!("token selection"));
                 if let Some(selected_token_symbol) = sg_root.lookup_node(
                     "/window/content/wallet/send_step2_layer/send_selected_token_symbol",
                 ) {

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

@@ -136,12 +136,12 @@ pub async fn make(
 
     let step1_is_visible2 = step1_is_visible.clone();
     let step2_is_visible1 = step2_is_visible.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let (slot, recvr) = Slot::new("send_back_clicked2");
     node.register("click", slot).unwrap();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("send step2 back button"));
+            let atom = &mut redraw.make_guard(gfxtag!("send step2 back button"));
             step2_is_visible1.set(atom, false);
             step1_is_visible2.set(atom, true);
         }
@@ -376,12 +376,12 @@ pub async fn make(
     let (slot, recvr) = Slot::new("send_paste_clicked");
     node.register("click", slot).unwrap();
     let recipient_input2 = recipient_input.clone();
-    let renderer_clone = app.renderer.clone();
+    let redraw_clone = app.redraw_trigger.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             if let Some(clipboard_text) = clipboard::get() {
                 let text_prop = recipient_input2.get_property("text").unwrap();
-                let atom = &mut renderer_clone.make_guard(gfxtag!("step2 recipient paste"));
+                let atom = &mut redraw_clone.make_guard(gfxtag!("step2 recipient paste"));
                 text_prop.set_str(atom, Role::App, 0, &clipboard_text).unwrap();
                 if let crate::scene::Pimpl::Edit(edit) = recipient_input2.pimpl() {
                     edit.on_text_prop_changed();
@@ -412,12 +412,12 @@ pub async fn make(
     let recipient_input2 = recipient_input.clone();
     let recipient_text = recipient_input.get_property("text").unwrap();
     let recipient_text_sub = recipient_text.subscribe_modify();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let btn_bg_valid_clone = btn_bg_valid.clone();
     let btn_bg_invalid_clone = btn_bg_invalid.clone();
     let listen_recipient_text = app.ex.spawn(async move {
         while let Ok(_) = recipient_text_sub.receive().await {
-            let atom = &mut renderer.make_guard(gfxtag!("wallet recipient input recv"));
+            let atom = &mut redraw.make_guard(gfxtag!("wallet recipient input recv"));
             let label_text_color = add_recipient_label_node.get_property("text_color").unwrap();
             let btn_bg_valid_visible = btn_bg_valid_clone.get_property("is_visible").unwrap();
             let btn_bg_invalid_visible = btn_bg_invalid_clone.get_property("is_visible").unwrap();
@@ -454,7 +454,7 @@ pub async fn make(
     app.tasks.lock().unwrap().push(listen_recipient_text);
 
     let step2_is_visible2 = step2_is_visible.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let recipient_input2 = recipient_input.clone();
     let send_tx_data3 = send_tx_data.clone();
     let sg_root = app.sg_root.clone();
@@ -468,7 +468,7 @@ pub async fn make(
                 continue;
             };
 
-            let atom = &mut renderer.make_guard(gfxtag!("add recipient button"));
+            let atom = &mut redraw.make_guard(gfxtag!("add recipient button"));
 
             let data = {
                 let mut tx_data = send_tx_data3.lock().unwrap();

+ 8 - 8
bin/app/src/app/schema/wallet/send_step3.rs

@@ -111,13 +111,13 @@ pub async fn make(
 
     let step2_is_visible3 = step2_is_visible.clone();
     let step3_is_visible1 = step3_is_visible.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let sg_root2 = app.sg_root.clone();
     let (slot, recvr) = Slot::new("send_back_clicked3");
     node.register("click", slot).unwrap();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("send step3 back button"));
+            let atom = &mut redraw.make_guard(gfxtag!("send step3 back button"));
             // Reset error message on back button click
             if let Some(error_node) =
                 sg_root2.lookup_node("/window/content/wallet/send_step3_layer/error")
@@ -585,14 +585,14 @@ pub async fn make(
     let send_tx_data5 = send_tx_data.clone();
     let amount_text = amount_input2.get_property("text").unwrap();
     let amount_text_sub = amount_text.subscribe_modify();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let sg_root = app.sg_root.clone();
     let btn_bg_valid_clone = btn_bg_valid.clone();
     let btn_bg_invalid_clone = btn_bg_invalid.clone();
     let add_amount_label_node_for_validation = add_amount_label_node.clone();
     let listen_amount_text = app.ex.spawn(async move {
         while let Ok(_) = amount_text_sub.receive().await {
-            let atom = &mut renderer.make_guard(gfxtag!("wallet amount input recv"));
+            let atom = &mut redraw.make_guard(gfxtag!("wallet amount input recv"));
             // Reset error message on amount change
             if let Some(error_node) =
                 sg_root.lookup_node("/window/content/wallet/send_step3_layer/error")
@@ -658,7 +658,7 @@ pub async fn make(
     });
     app.tasks.lock().unwrap().push(listen_amount_text);
 
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let amount_input2 = input_node.clone();
     let send_tx_data4 = send_tx_data.clone();
     let step3_is_visible2 = step3_is_visible.clone();
@@ -667,7 +667,7 @@ pub async fn make(
     node.register("click", slot).unwrap();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("add amount button"));
+            let atom = &mut redraw.make_guard(gfxtag!("add amount button"));
             // Reset error message on button click
             if let Some(error_node) = sg_root.lookup_node("/window/content/wallet/send_step3_layer/error") {
                 error_node.set_property_str(atom, Role::App, "text", "").unwrap();
@@ -751,7 +751,7 @@ pub async fn make(
 
     // Add listener for step3 visibility to focus/unfocus amount input
     let step3_is_visible_clone = step3_is_visible.clone();
-    let renderer_clone = app.renderer.clone();
+    let redraw_clone = app.redraw_trigger.clone();
     let sg_root = app.sg_root.clone();
     let amount_wrapper_clone = amount_wrapper.clone();
     let input_node_clone = input_node.clone();
@@ -784,7 +784,7 @@ pub async fn make(
                 };
                 if let Some(token_id) = token_id {
                     if !token_symbol.is_empty() {
-                        let atom = &mut renderer_clone
+                        let atom = &mut redraw_clone
                             .make_guard(gfxtag!("update amount positions on visible"));
                         update_amount_screen(
                             atom,

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

@@ -106,12 +106,12 @@ pub async fn make(
 
     let step3_is_visible2 = step3_is_visible.clone();
     let step4_is_visible1 = step4_is_visible.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let (slot, recvr) = Slot::new("send_back_clicked4");
     node.register("click", slot).unwrap();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("send step4 back button"));
+            let atom = &mut redraw.make_guard(gfxtag!("send step4 back button"));
             step4_is_visible1.set(atom, false);
             step3_is_visible2.set(atom, true);
         }
@@ -467,7 +467,7 @@ pub async fn make(
     )
     .await;
 
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let sg_root = app.sg_root.clone();
     let step4_is_visible1 = step4_is_visible.clone();
     let send_tx_data_for_send = send_tx_data.clone();
@@ -483,7 +483,7 @@ pub async fn make(
                     continue;
                 }
             }
-            let atom = &mut renderer.make_guard(gfxtag!("send button"));
+            let atom = &mut redraw.make_guard(gfxtag!("send button"));
 
             step4_is_visible1.set(atom, false);
             if let Some(tx_status) = sg_root.lookup_node("/window/content/wallet/tx_status_layer") {
@@ -526,7 +526,7 @@ pub async fn make(
 
     // Add listener for step4 visibility to update amount positions
     let step4_is_visible_clone = step4_is_visible.clone();
-    let renderer_clone = app.renderer.clone();
+    let redraw_clone = app.redraw_trigger.clone();
     let amount_wrapper_clone = amount_wrapper.clone();
     let amount_text_node_clone = amount_text_node.clone();
     let token_symbol_node_clone = token_symbol_node.clone();
@@ -546,7 +546,7 @@ pub async fn make(
                     }
                 }
 
-                let atom = &mut renderer_clone.make_guard(gfxtag!("update step4 amount positions"));
+                let atom = &mut redraw_clone.make_guard(gfxtag!("update step4 amount positions"));
                 let data = send_tx_data_clone2.lock().unwrap().clone();
 
                 let token_symbol = data.token_symbol.unwrap_or_else(|| "".to_string());

+ 4 - 4
bin/app/src/app/schema/wallet/tx_status.rs

@@ -281,14 +281,14 @@ pub async fn make(
     .await;
 
     let main_is_visible = PropertyBool::wrap(&main_layer, Role::App, "is_visible", 0).unwrap();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     let tx_status_is_visible1 = tx_status_is_visible.clone();
     let send_tx_data2 = send_tx_data.clone();
     let (slot, recvr) = Slot::new("tx_status_close_clicked");
     node.register("click", slot).unwrap();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            let atom = &mut renderer.make_guard(gfxtag!("tx status close button"));
+            let atom = &mut redraw.make_guard(gfxtag!("tx status close button"));
             tx_status_is_visible1.set(atom, false);
             main_is_visible.set(atom, true);
 
@@ -304,10 +304,10 @@ pub async fn make(
     let set_tx_status_sub = tx_status_layer.subscribe_method_call("set_tx_status").unwrap();
     let tx_status_layer_clone = tx_status_layer.clone();
     let sg_root = app.sg_root.clone();
-    let renderer = app.renderer.clone();
+    let redraw = app.redraw_trigger.clone();
     app.tasks.lock().unwrap().push(app.ex.spawn(async move {
         while let Ok(mcall) = set_tx_status_sub.receive().await {
-            let atom = &mut renderer.make_guard(gfxtag!("set_tx_status"));
+            let atom = &mut redraw.make_guard(gfxtag!("set_tx_status"));
 
             let mut cur = std::io::Cursor::new(mcall.data);
             let tx_id = Option::<String>::decode(&mut cur).unwrap();

+ 4 - 4
bin/app/src/app/schema/wallet/util.rs

@@ -709,7 +709,7 @@ pub async fn create_tooltip(
     // Subscribe to show method for auto-hide behavior
     let show_method_sub = tooltip_layer.subscribe_method_call("show").unwrap();
     let tooltip_clone = tooltip_layer.clone();
-    let renderer2 = app.renderer.clone();
+    let redraw2 = app.redraw_trigger.clone();
     let (reset_sender, reset_receiver) = unbounded::<()>();
 
     app.tasks.lock().unwrap().push(app.ex.spawn(async move {
@@ -718,7 +718,7 @@ pub async fn create_tooltip(
             let _ = show_method_sub.receive().await;
 
             // Show the tooltip
-            let atom = &mut renderer2.make_guard(gfxtag!("tooltip show"));
+            let atom = &mut redraw2.make_guard(gfxtag!("tooltip show"));
             tooltip_clone.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
 
             // Send reset signal to hide timer
@@ -728,7 +728,7 @@ pub async fn create_tooltip(
 
     // Hide timer task
     let tooltip2 = tooltip_layer.clone();
-    let renderer2 = app.renderer.clone();
+    let redraw2 = app.redraw_trigger.clone();
     app.tasks.lock().unwrap().push(app.ex.spawn(async move {
         loop {
             // Wait for show signal
@@ -749,7 +749,7 @@ pub async fn create_tooltip(
             }
 
             // Hide the tooltip
-            let atom = &mut renderer2.make_guard(gfxtag!("tooltip hide"));
+            let atom = &mut redraw2.make_guard(gfxtag!("tooltip hide"));
             tooltip2.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
         }
     }));

+ 0 - 8
bin/app/src/gfx/api.rs

@@ -274,14 +274,6 @@ impl Renderer {
         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()
-    }
 }
 
 impl RenderApi for Renderer {

+ 17 - 17
bin/app/src/main.rs

@@ -188,10 +188,10 @@ impl God {
         {
             let sg_root = sg_root.clone();
             let ex = bg_ex.clone();
-            let renderer = renderer.clone();
+            let redraw = app.redraw_trigger.clone();
             let zmq_task = bg_ex.spawn(async {
                 i!("Enabled net debugging backend in this build");
-                let zmq_rpc = ZeroMQAdapter::new(sg_root, renderer, ex).await;
+                let zmq_rpc = ZeroMQAdapter::new(sg_root, redraw, ex).await;
                 zmq_rpc.run().await;
             });
             bg_runtime.push_task(zmq_task);
@@ -200,9 +200,9 @@ impl God {
         {
             let ex = bg_ex.clone();
             let cv = cv_app_is_setup.clone();
-            let renderer = renderer.clone();
+            let redraw = app.redraw_trigger.clone();
             let plug_task = bg_ex.spawn(async move {
-                load_plugins(ex, sg_root, renderer, cv, db).await;
+                load_plugins(ex, sg_root, redraw, cv, db).await;
             });
             bg_runtime.push_task(plug_task);
         }
@@ -277,7 +277,7 @@ static GOD: OnceLock<God> = OnceLock::new();
 async fn load_plugins(
     ex: ExecutorPtr,
     sg_root: SceneNodePtr,
-    renderer: Renderer,
+    redraw: crate::ui::RedrawTrigger,
     cv: Arc<CondVar>,
     db: sled::Db,
 ) {
@@ -305,10 +305,10 @@ async fn load_plugins(
         darkirc.register("recv", slot).unwrap();
         let sg_root2 = sg_root.clone();
         let darkirc_nick = PropertyStr::wrap(&darkirc, Role::App, "nick", 0).unwrap();
-        let renderer2 = renderer.clone();
+        let redraw2 = redraw.clone();
         let listen_recv = ex.spawn(async move {
         while let Ok(data) = recvr.recv().await {
-            let atom = &mut renderer2.make_guard(gfxtag!("darkirc msg recv"));
+            let atom = &mut redraw2.make_guard(gfxtag!("darkirc msg recv"));
 
             let mut cur = Cursor::new(&data);
             let channel = String::decode(&mut cur).unwrap();
@@ -357,7 +357,7 @@ async fn load_plugins(
         let (slot, recvr) = Slot::new("connect");
         darkirc.register("connect", slot).unwrap();
         let sg_root2 = sg_root.clone();
-        let renderer2 = renderer.clone();
+        let redraw2 = redraw.clone();
         let listen_connect = ex.spawn(async move {
             let net0 = sg_root2.lookup_node("/window/content/netstatus_layer/net0").unwrap();
             let net1 = sg_root2.lookup_node("/window/content/netstatus_layer/net1").unwrap();
@@ -372,7 +372,7 @@ async fn load_plugins(
             while let Ok(data) = recvr.recv().await {
                 let (peers_count, is_dag_synced): (u32, bool) = deserialize(&data).unwrap();
 
-                let atom = &mut renderer2.make_guard(gfxtag!("netstatus change"));
+                let atom = &mut redraw2.make_guard(gfxtag!("netstatus change"));
 
                 if peers_count == 0 {
                     net0_is_visible.set(atom, true);
@@ -462,7 +462,7 @@ async fn load_plugins(
         let (slot, recvr) = Slot::new("connect");
         drk.register("connect", slot).unwrap();
         let sg_root2 = sg_root.clone();
-        let renderer2 = renderer.clone();
+        let redraw2 = redraw.clone();
         let listen_connect = ex.spawn(async move {
             let net0 = sg_root2.lookup_node("/window/content/wallet/netstatus_layer/net0").unwrap();
             let net1 = sg_root2.lookup_node("/window/content/wallet/netstatus_layer/net1").unwrap();
@@ -476,7 +476,7 @@ async fn load_plugins(
 
             while let Ok(data) = recvr.recv().await {
                 let status: u8 = deserialize(&data).unwrap();
-                let atom = &mut renderer2.make_guard(gfxtag!("blockchain netstatus change"));
+                let atom = &mut redraw2.make_guard(gfxtag!("blockchain netstatus change"));
 
                 match status {
                     1 => {
@@ -510,7 +510,7 @@ async fn load_plugins(
         let (slot, recv) = Slot::new("balances_update");
         let _ = drk.register("balances_updated", slot);
         let sg_root2 = sg_root.clone();
-        let renderer2 = renderer.clone();
+        let redraw2 = redraw.clone();
         let drk_node2 = drk.clone();
         let listen_balances = ex.spawn(async move {
             use crate::ui::TokenRow;
@@ -522,7 +522,7 @@ async fn load_plugins(
 
                 let mut cur = std::io::Cursor::new(data);
                 if let Ok(balances) = Vec::<(String, TokenId, u64)>::decode(&mut cur) {
-                    let atom = &mut renderer2.make_guard(gfxtag!("wallet - refresh tokens"));
+                    let atom = &mut redraw2.make_guard(gfxtag!("wallet - refresh tokens"));
 
                     let token_rows: Vec<TokenRow> = balances
                         .iter()
@@ -608,7 +608,7 @@ async fn load_plugins(
         let (slot, recv) = Slot::new("tx_built");
         let _ = drk.register("tx_built", slot);
         let sg_root2 = sg_root.clone();
-        let renderer2 = renderer.clone();
+        let redraw2 = redraw.clone();
         let listen_tx_built = ex.spawn(async move {
             while let Ok(data) = recv.recv().await {
                 let mut cur = std::io::Cursor::new(data);
@@ -620,7 +620,7 @@ async fn load_plugins(
                 let tx = Transaction::decode(&mut cur).unwrap();
 
                 // Update tx_status_layer with built transaction
-                let atom = &mut renderer2.make_guard(gfxtag!("tx built"));
+                let atom = &mut redraw2.make_guard(gfxtag!("tx built"));
                 if let Some(tx_status) =
                     sg_root2.lookup_node("/window/content/wallet/tx_status_layer")
                 {
@@ -660,12 +660,12 @@ async fn load_plugins(
         let (slot, recv) = Slot::new("tx_built_error");
         let _ = drk.register("tx_built_error", slot);
         let sg_root2 = sg_root.clone();
-        let renderer2 = renderer.clone();
+        let redraw2 = redraw.clone();
         let listen_tx_built_error = ex.spawn(async move {
             while let Ok(data) = recv.recv().await {
                 let mut cur = std::io::Cursor::new(data);
                 let error_message = String::decode(&mut cur).unwrap();
-                let atom = &mut renderer2.make_guard(gfxtag!("tx built error"));
+                let atom = &mut redraw2.make_guard(gfxtag!("tx built error"));
 
                 // Display error message in step3
                 if let Some(error_node) =

+ 6 - 6
bin/app/src/net.rs

@@ -24,9 +24,10 @@ use zeromq::{Socket, SocketRecv, SocketSend};
 use crate::{
     error::{Error, Result},
     expr::SExprCode,
-    gfx::{gfxtag, Renderer},
+    gfx::gfxtag,
     prop::{PropertyType, Role},
     scene::{SceneNodeId, SceneNodePtr, ScenePath, Slot},
+    ui::RedrawTrigger,
     ExecutorPtr,
 };
 
@@ -78,7 +79,7 @@ pub struct ZeroMQAdapter {
     slot_recvr: Option<mpsc::Receiver<(Vec<u8>, Vec<u8>)>>,
     */
     sg_root: SceneNodePtr,
-    renderer: Renderer,
+    redraw: RedrawTrigger,
     ex: ExecutorPtr,
 
     zmq_rep: Mutex<zeromq::RepSocket>,
@@ -86,7 +87,7 @@ pub struct ZeroMQAdapter {
 }
 
 impl ZeroMQAdapter {
-    pub async fn new(sg_root: SceneNodePtr, renderer: Renderer, ex: ExecutorPtr) -> Arc<Self> {
+    pub async fn new(sg_root: SceneNodePtr, redraw: RedrawTrigger, ex: ExecutorPtr) -> Arc<Self> {
         let mut zmq_rep = zeromq::RepSocket::new();
         if USE_IPV6 {
             zmq_rep.bind("tcp://[::]:9484").await.unwrap();
@@ -103,7 +104,7 @@ impl ZeroMQAdapter {
 
         Arc::new(Self {
             sg_root,
-            renderer,
+            redraw,
             ex,
             zmq_rep: Mutex::new(zmq_rep),
             zmq_pub: Mutex::new(zmq_pub),
@@ -252,8 +253,7 @@ impl ZeroMQAdapter {
                 let node = self.sg_root.lookup_node(node_path).ok_or(Error::NodeNotFound)?;
                 let prop = node.get_property(&prop_name).ok_or(Error::PropertyNotFound)?;
 
-                let atom =
-                    &mut self.renderer.make_guard(gfxtag!("ZeroMQAdapter::SetPropertyValue"));
+                let atom = &mut self.redraw.make_guard(gfxtag!("ZeroMQAdapter::SetPropertyValue"));
 
                 match prop_type {
                     PropertyType::Null => {

+ 15 - 26
bin/app/src/plugin/darkirc.rs

@@ -97,9 +97,6 @@ mod paths {
     use crate::android::{get_appdata_path, get_external_storage_path};
     use std::path::PathBuf;
 
-    pub fn get_evgrdb_path() -> PathBuf {
-        get_external_storage_path().join("evgr2")
-    }
     pub fn get_chatdb_path() -> PathBuf {
         get_external_storage_path().join("chatdb")
     }
@@ -107,10 +104,6 @@ mod paths {
         get_external_storage_path().join("use_tor.txt")
     }
 
-    pub fn nick_filename() -> PathBuf {
-        get_appdata_path().join("/nick2.txt")
-    }
-
     pub fn p2p_datastore_path() -> PathBuf {
         get_appdata_path().join("darkirc2_p2p")
     }
@@ -123,9 +116,6 @@ mod paths {
 mod paths {
     use std::path::PathBuf;
 
-    pub fn get_evgrdb_path() -> PathBuf {
-        dirs::data_local_dir().unwrap().join("darkfi/app/evgr2")
-    }
     pub fn get_chatdb_path() -> PathBuf {
         dirs::data_local_dir().unwrap().join("darkfi/app/chatdb")
     }
@@ -133,10 +123,6 @@ mod paths {
         dirs::data_local_dir().unwrap().join("darkfi/app/use_tor.txt")
     }
 
-    pub fn nick_filename() -> PathBuf {
-        dirs::cache_dir().unwrap().join("darkfi/app/nick2.txt")
-    }
-
     pub fn p2p_datastore_path() -> PathBuf {
         dirs::cache_dir().unwrap().join("darkfi/app/darkirc2_p2p")
     }
@@ -190,6 +176,7 @@ pub struct DarkIrc {
     pub contacts: RwLock<HashMap<String, IrcContact>>,
     channels_tree: sled::Tree,
     contacts_tree: sled::Tree,
+    nick_tree: sled::Tree,
     dm_secret: SecretKey,
     settings: PluginSettings,
     ex: ExecutorPtr,
@@ -209,16 +196,9 @@ impl DarkIrc {
         node_ref.link(setting_root.clone());
 
         i!("Starting DarkIRC backend");
-        let evgr_path = get_evgrdb_path();
-        let evgr_db = match sled::open(&evgr_path) {
-            Ok(db) => db,
-            Err(err) => {
-                e!("Sled database '{}' failed to open: {err}!", evgr_path.display());
-                return Err(Error::SledDbErr)
-            }
-        };
 
-        let setting_tree = evgr_db.open_tree("settings")?;
+        let setting_tree = db.open_tree("darkirc_settings")?;
+        i!("Opened darkirc_settings tree from unified db");
 
         // Use the unified db for reading channels (UI stores channels there)
         let channels_tree = db.open_tree("channels")?;
@@ -227,6 +207,9 @@ impl DarkIrc {
         let contacts_tree = db.open_tree("contacts")?;
         i!("Opened contacts tree from unified db");
 
+        let nick_tree = db.open_tree("nick")?;
+        i!("Opened nick tree from unified db");
+
         let dm_secret = Self::load_or_create_dm_identity(&db);
         let dm_public_b58 = bs58::encode(dm_secret.public_key().to_bytes()).into_string();
         // Expose our DM public key on the plugin node so it can be displayed/shared.
@@ -325,8 +308,10 @@ impl DarkIrc {
             }
         };
 
-        if let Ok(prev_nick) = std::fs::read_to_string(nick_filename()) {
-            nick.set(&mut PropertyAtomicGuard::none(), prev_nick);
+        if let Ok(Some(nick_bytes)) = nick_tree.get(b"value") {
+            if let Ok(prev_nick) = String::from_utf8(nick_bytes.to_vec()) {
+                nick.set(&mut PropertyAtomicGuard::none(), prev_nick);
+            }
         }
 
         let self_ = Arc::new(Self {
@@ -343,6 +328,7 @@ impl DarkIrc {
             contacts: RwLock::new(HashMap::new()),
             channels_tree,
             contacts_tree,
+            nick_tree,
             dm_secret,
 
             settings,
@@ -974,7 +960,10 @@ impl DarkIrc {
 
         let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
         async fn save_nick(self_: Arc<DarkIrc>, _batch: BatchGuardPtr) {
-            let _ = std::fs::write(nick_filename(), self_.nick.get());
+            if let Err(err) = self_.nick_tree.insert(b"value", self_.nick.get().as_bytes()) {
+                e!("Failed persisting nick to sled: {err}");
+            }
+            let _ = self_.nick_tree.flush();
         }
         on_modify.when_change(self.nick.prop(), save_nick);
 

+ 3 - 4
bin/app/src/prop/guard.rs

@@ -53,18 +53,17 @@ pub struct PropertyAtomicGuard {
 }
 
 impl PropertyAtomicGuard {
-    pub fn new(start_batch: BatchGuardCb, end_batch: BatchGuardCb) -> Self {
+    pub fn new(end_batch: BatchGuardCb) -> Self {
         let batch_id = BATCH_ID.fetch_add(1, Ordering::Relaxed);
-        start_batch(batch_id);
         Self { batch_id, updates: vec![], end_batch: Some(end_batch), parent: None }
     }
 
     /// Should only be used when there's an explicit end_batch() called manually at the end
     /// of the context.
     /// You probably mostly want to either `batch.spawn()` from an existing batch
-    /// or use `renderer.make_guard()`.
+    /// or use `RedrawTrigger::make_guard()`.
     pub fn none() -> Self {
-        Self::new(Box::new(|_| {}), Box::new(|_| {}))
+        Self::new(Box::new(|_| {}))
     }
 
     pub(super) fn add(&mut self, prop: PropertyPtr, role: Role, action: ModifyAction) {

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

@@ -830,7 +830,7 @@ impl ChatView {
             }
 
             let scroll = self.scroll.get() + speed;
-            let atom = &mut self.renderer.make_guard(gfxtag!("ChatView::motion_task"));
+            let atom = &mut self.redraw.make_guard(gfxtag!("ChatView::motion_task"));
             let dist = self.scrollview(scroll, atom).await;
 
             // We reached the end so just stop
@@ -950,7 +950,6 @@ impl ChatView {
 
         self.scroll.set(atom, scroll);
         self.bgload_cv.notify();
-        self.redraw.trigger();
 
         scroll - old_scroll
     }
@@ -1524,7 +1523,7 @@ impl UIObject for ChatView {
                     return true
                 }
                 let scroll = start_scroll + dist;
-                let atom = &mut self.renderer.make_guard(gfxtag!("ChatView::handle_touch_scroll"));
+                let atom = &mut self.redraw.make_guard(gfxtag!("ChatView::handle_touch_scroll"));
                 self.scrollview(scroll, atom).await;
             }
             TouchPhase::Ended | TouchPhase::Cancelled => {
@@ -1572,7 +1571,6 @@ impl UIObject for ChatView {
 
 impl Drop for ChatView {
     fn drop(&mut self) {
-        let atom = self.renderer.make_guard(gfxtag!("ChatView::drop"));
         self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
 }

+ 24 - 44
bin/app/src/ui/edit/mod.rs

@@ -597,7 +597,6 @@ impl BaseEdit {
 
         // 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());
         true
     }
@@ -720,7 +719,6 @@ impl BaseEdit {
         self.pause_blinking();
         // Any edit invalidates the action menu's selection
         self.action_mode.clear();
-        self.redraw.trigger();
 
         ed!("handle_key: handled {key:?} after=[{}]", self.dbg_state());
         true
@@ -756,7 +754,7 @@ impl BaseEdit {
             return true
         }
 
-        let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_touch_start_action"));
+        let atom = &mut self.redraw.make_guard(gfxtag!("BaseEdit::handle_touch_start_action"));
         if let Some(action_id) = self.action_mode.interact(local_pos) {
             match action_id {
                 ACTION_COPY => {
@@ -781,12 +779,10 @@ impl BaseEdit {
                 _ => {}
             }
 
-            self.redraw.trigger();
             return true;
         } else {
-            // interact() already consumed the menu; trigger so the pass
-            // re-emits the (now empty) action overlay.
-            self.redraw.trigger();
+            // interact() already consumed the menu; the pass re-emits the
+            // (now empty) action overlay when the batch settles.
         }
 
         if !rect.contains(touch_pos) {
@@ -895,9 +891,8 @@ impl BaseEdit {
                     self.window_scale.get(),
                 );
 
-                let atom = &mut self
-                    .renderer
-                    .make_guard(gfxtag!("BaseEdit::TouchStateAction::StartSelect"));
+                let atom =
+                    &mut self.redraw.make_guard(gfxtag!("BaseEdit::TouchStateAction::StartSelect"));
 
                 if self.text.get().is_empty() {
                     menu.add("Paste", ACTION_PASTE);
@@ -930,7 +925,6 @@ impl BaseEdit {
                     self.touch_info.lock().state = TouchStateAction::Select;
                 }
                 self.action_mode.set(menu);
-                self.redraw.trigger();
                 ed!("handle_touch_move: StartSelect handled");
             }
             TouchStateAction::DragSelectHandle { side } => {
@@ -986,9 +980,8 @@ impl BaseEdit {
         match state {
             TouchStateAction::Inactive => return false,
             TouchStateAction::Started { pos: _, instant: _ } | TouchStateAction::SetCursorPos => {
-                let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_touch_end"));
+                let atom = &mut self.redraw.make_guard(gfxtag!("BaseEdit::handle_touch_end"));
                 self.touch_set_cursor_pos(atom, touch_pos);
-                self.redraw.trigger();
             }
             _ => {}
         }
@@ -1043,7 +1036,7 @@ impl BaseEdit {
 
         let mut clip_mouse_pos = rect.clip_point(mouse_pos);
 
-        let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_mouse_move"));
+        let atom = &mut self.redraw.make_guard(gfxtag!("BaseEdit::handle_mouse_move"));
 
         // Handle scrolling
         if !is_mouse_hover {
@@ -1056,8 +1049,6 @@ impl BaseEdit {
             let delta = travel * SELECT_SCROLL_TRAVEL_SPEED;
             let scroll = (self.scroll.load(Ordering::Relaxed) + delta).clamp(0., max_scroll);
             self.scroll.store(scroll, Ordering::Release);
-
-            self.redraw.trigger();
         }
 
         // Move mouse pos within this widget
@@ -1142,7 +1133,6 @@ impl BaseEdit {
 
         self.pause_blinking();
         //self.behave.apply_cursor_scroll();
-        self.redraw.trigger();
     }
 
     /// Holds the cursor solid while the user interacts. The renderer-managed
@@ -1272,11 +1262,12 @@ impl BaseEdit {
         vec![DrawInstruction::Draw(mesh.alloc(renderer).draw_untextured())]
     }
 
-    /// This does not make use of an atom since we want to de-atomize updating this widget from
-    /// its dependencies so theres zero latency when typing.
+    /// This does not make use of its own batch since we want to de-atomize updating this
+    /// widget from its dependencies so theres zero latency when typing.
+    /// Runs inside the draw pass, so it borrows the pass's atom: property
+    /// echoes are deferred there and no extra pass is triggered.
     /// Should be called when text contents changes.
-    fn eval_rect(&self) {
-        let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::make_draw_calls"));
+    fn eval_rect(&self, atom: &mut PropertyAtomicGuard) {
         self.behave.eval_rect(atom);
     }
 
@@ -1374,10 +1365,9 @@ impl BaseEdit {
             panic!("self destroyed before insert_text_method_task was stopped!");
         };
 
-        let atom = &mut self_.renderer.make_guard(gfxtag!("BaseEdit::process_insert_text_method"));
+        let atom = &mut self_.redraw.make_guard(gfxtag!("BaseEdit::process_insert_text_method"));
         self_.editor.lock().insert(&text, atom);
         self_.action_mode.clear();
-        self_.redraw.trigger();
         ed!("insert_text method: inserted {text:?} after=[{}]", self_.dbg_state());
         true
     }
@@ -1406,9 +1396,8 @@ impl BaseEdit {
         }
         self_.editor.lock().focus();
 
-        let atom = &mut self_.renderer.make_guard(gfxtag!("BaseEdit::process_focus_method"));
+        let atom = &mut self_.redraw.make_guard(gfxtag!("BaseEdit::process_focus_method"));
         self_.is_focused.set(atom, true);
-        self_.redraw.trigger();
         ed!("focus method: done after=[{}]", self_.dbg_state());
         true
     }
@@ -1428,9 +1417,8 @@ impl BaseEdit {
         };
 
         self_.editor.lock().unfocus();
-        let atom = &mut self_.renderer.make_guard(gfxtag!("BaseEdit::process_unfocus_method"));
+        let atom = &mut self_.redraw.make_guard(gfxtag!("BaseEdit::process_unfocus_method"));
         self_.is_focused.set(atom, false);
-        self_.redraw.trigger();
         ed!("unfocus method: done after=[{}]", self_.dbg_state());
         true
     }
@@ -1459,7 +1447,7 @@ impl BaseEdit {
         }
 
         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.redraw.make_guard(gfxtag!("BaseEdit::handle_android_event"));
 
         let is_new_select_collapsed = state.select.0 == state.select.1;
         let (is_text_changed, is_select_changed, is_compose_changed) = {
@@ -1515,7 +1503,6 @@ impl BaseEdit {
             //assert!(state.text != self.text.get());
             self.finish_select(atom);
             self.action_mode.clear();
-            self.redraw.trigger();
             ed!("handle_android_event: handled text change, after=[{}]", self.dbg_state());
         } else if is_select_changed {
             // The IME can collapse a phone-style word selection out from under
@@ -1528,11 +1515,9 @@ impl BaseEdit {
                 d!("IME has collapsed selection!");
                 self.finish_select(atom);
             }
-            self.redraw.trigger();
             ed!("handle_android_event: handled select change, after=[{}]", self.dbg_state());
         } else if is_compose_changed {
             self.editor.lock().refresh();
-            self.redraw.trigger();
             ed!("handle_android_event: handled compose change, after=[{}]", self.dbg_state());
         }
     }
@@ -1540,7 +1525,6 @@ impl BaseEdit {
 
 impl Drop for BaseEdit {
     fn drop(&mut self) {
-        let atom = self.renderer.make_guard(gfxtag!("BaseEdit::drop"));
         self.renderer.replace_draw_calls(vec![(self.text_dc_key, Default::default())]);
     }
 }
@@ -1723,10 +1707,10 @@ impl UIObject for BaseEdit {
     async fn draw(
         &self,
         parent_rect: Rectangle,
-        _atom: &mut PropertyAtomicGuard,
+        atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
         *self.parent_rect.lock() = Some(parent_rect);
-        self.eval_rect();
+        self.eval_rect(atom);
         // 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();
@@ -1755,7 +1739,7 @@ impl UIObject for BaseEdit {
             repeater.key_down(PressedKey::Char(key), repeat)
         };
 
-        let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_char"));
+        let atom = &mut self.redraw.make_guard(gfxtag!("BaseEdit::handle_char"));
 
         if mods.ctrl || mods.alt || mods.logo {
             if repeat {
@@ -1776,7 +1760,6 @@ impl UIObject for BaseEdit {
         self.pause_blinking();
         // 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());
         true
     }
@@ -1806,7 +1789,7 @@ impl UIObject for BaseEdit {
             t!("Key {:?} has {} actions", key, actions);
         }
 
-        let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_key_down"));
+        let atom = &mut self.redraw.make_guard(gfxtag!("BaseEdit::handle_key_down"));
 
         let mut is_handled = false;
         for _ in 0..actions {
@@ -1858,7 +1841,7 @@ impl UIObject for BaseEdit {
             return false
         }
 
-        let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_mouse_btn_down"));
+        let atom = &mut self.redraw.make_guard(gfxtag!("BaseEdit::handle_mouse_btn_down"));
 
         // clicking inside box will:
         // 1. make it active
@@ -1889,7 +1872,6 @@ impl UIObject for BaseEdit {
         self.pause_blinking();
         // A click moves the cursor; any open action menu is stale now
         self.action_mode.clear();
-        self.redraw.trigger();
         true
     }
 
@@ -1908,7 +1890,7 @@ impl UIObject for BaseEdit {
         // releasing mouse button will end selection
         self.mouse_btn_held.store(false, Ordering::Relaxed);
 
-        let atom = &mut self.renderer.make_guard(gfxtag!("BaseEdit::handle_mouse_btn_up"));
+        let atom = &mut self.redraw.make_guard(gfxtag!("BaseEdit::handle_mouse_btn_up"));
         if let Some(action_id) = self.action_mode.interact(mouse_pos) {
             match action_id {
                 ACTION_COPY => {
@@ -1933,12 +1915,10 @@ impl UIObject for BaseEdit {
                 _ => {}
             }
 
-            self.redraw.trigger();
             return true;
         } else {
-            // interact() already consumed the menu; trigger so the pass
-            // re-emits the (now empty) action overlay.
-            self.redraw.trigger();
+            // interact() already consumed the menu; the pass re-emits the
+            // (now empty) action overlay when the batch settles.
         }
 
         // Stop any selection scrolling

+ 2 - 5
bin/app/src/ui/emoji_picker/mod.rs

@@ -300,7 +300,7 @@ impl UIObject for EmojiPicker {
             return false
         }
         t!("handle_mouse_wheel()");
-        let atom = &mut self.renderer.make_guard(gfxtag!("EmojiPicker::handle_mouse_wheel"));
+        let atom = &mut self.redraw.make_guard(gfxtag!("EmojiPicker::handle_mouse_wheel"));
 
         let mut scroll = self.scroll.get();
         scroll -= self.mouse_scroll_speed.get() * wheel_pos.y;
@@ -308,7 +308,6 @@ impl UIObject for EmojiPicker {
         self.scroll.set(atom, scroll);
 
         *self.draw_cache.lock() = None;
-        self.redraw.trigger();
 
         true
     }
@@ -331,7 +330,7 @@ impl UIObject for EmojiPicker {
             return false
         }
 
-        let atom = &mut self.renderer.make_guard(gfxtag!("EmojiPicker::handle_touch"));
+        let atom = &mut self.redraw.make_guard(gfxtag!("EmojiPicker::handle_touch"));
 
         let rect = self.rect.get();
         let pos = touch_pos - Point::new(rect.x, rect.y);
@@ -373,7 +372,6 @@ impl UIObject for EmojiPicker {
                         self.scroll.set(atom, scroll);
 
                         *self.draw_cache.lock() = None;
-                        self.redraw.trigger();
                     }
                 }
                 TouchPhase::Ended | TouchPhase::Cancelled => {
@@ -395,7 +393,6 @@ impl UIObject for EmojiPicker {
 
 impl Drop for EmojiPicker {
     fn drop(&mut self) {
-        let atom = self.renderer.make_guard(gfxtag!("EmojiPicker::drop"));
         self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
 }

+ 0 - 1
bin/app/src/ui/image.rs

@@ -223,7 +223,6 @@ impl UIObject for Image {
 
 impl Drop for Image {
     fn drop(&mut self) {
-        let atom = self.renderer.make_guard(gfxtag!("Image::drop"));
         self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
 }

+ 4 - 5
bin/app/src/ui/menu/mod.rs

@@ -336,7 +336,7 @@ impl Menu {
 
                     if pos.x >= x_min && pos.x <= x_max {
                         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.redraw.make_guard(gfxtag!("Menu::delete_item"));
                         self.items.remove_str(atom, Role::App, item_idx).unwrap();
                     } else {
                         self.handle_selection(item_idx).await;
@@ -640,7 +640,7 @@ impl Menu {
             return true
         };
 
-        let atom = &mut self_.renderer.make_guard(gfxtag!("Menu::cancel_edit"));
+        let atom = &mut self_.redraw.make_guard(gfxtag!("Menu::cancel_edit"));
 
         // Restore the saved items
         // It must exist otherwise theres a logic err
@@ -650,7 +650,6 @@ impl Menu {
         // Exit edit mode
         self_.is_edit_mode.store(false, Ordering::Release);
         self_.invalidate_draw();
-        self_.redraw.trigger();
 
         true
     }
@@ -947,7 +946,7 @@ impl UIObject for Menu {
         if let Some(drag_info) = drag {
             if drag_info.item_idx != drag_info.insert_idx {
                 let item = self.items.get_str(drag_info.item_idx).unwrap();
-                let atom = &mut self.renderer.make_guard(gfxtag!("Menu::reorder_item"));
+                let atom = &mut self.redraw.make_guard(gfxtag!("Menu::reorder_item"));
                 self.items.remove_str(atom, Role::App, drag_info.item_idx).unwrap();
                 let insert_idx = drag_info.insert_idx;
                 self.items.insert_str(atom, Role::App, insert_idx, &item).unwrap();
@@ -1155,7 +1154,7 @@ impl UIObject for Menu {
                 if let Some(drag_info) = drag {
                     if drag_info.item_idx != drag_info.insert_idx {
                         let item = self.items.get_str(drag_info.item_idx).unwrap();
-                        let atom = &mut self.renderer.make_guard(gfxtag!("Menu::reorder_item"));
+                        let atom = &mut self.redraw.make_guard(gfxtag!("Menu::reorder_item"));
                         self.items.remove_str(atom, Role::App, drag_info.item_idx).unwrap();
                         let insert_idx = drag_info.insert_idx;
                         self.items.insert_str(atom, Role::App, insert_idx, &item).unwrap();

+ 31 - 6
bin/app/src/ui/mod.rs

@@ -89,8 +89,10 @@ macro_rules! t { ($($arg:tt)*) => { trace!(target: "scene::on_modify", $($arg)*)
 /// Handle for requesting a redraw pass from the root window's draw loop.
 /// Cheap to clone. The underlying queue is bounded(1), so triggers sent
 /// while a pass is running or pending are coalesced into a single
-/// additional pass. State mutations must happen before calling `trigger()`
-/// so the resulting pass observes them.
+/// additional pass. Property mutations SHOULD be made through
+/// `make_guard()` so the trigger fires once, after the whole update
+/// chain has settled. State mutations must happen before calling
+/// `trigger()` so the resulting pass observes them.
 #[derive(Clone)]
 pub struct RedrawTrigger(async_channel::Sender<()>);
 
@@ -101,10 +103,16 @@ impl RedrawTrigger {
         (Self(tx), rx)
     }
 
-    /// Request a draw pass. Never blocks. A trigger is only dropped when
-    /// another is already queued, which is equivalent: the queued token
-    /// guarantees a pass that starts after this call, and since callers
-    /// mutate state before triggering, that pass observes the mutation.
+    /// Request a draw pass without a batch scope. Only for mutations that
+    /// need no `PropertyAtomicGuard` (plain fields, caches): the trigger is
+    /// enqueued immediately, so all state must already be settled. For
+    /// property updates use `make_guard()` instead, which defers the
+    /// trigger to end-of-batch.
+    ///
+    /// Never blocks. A trigger is only dropped when another is already
+    /// queued, which is equivalent: the queued token guarantees a pass
+    /// that starts after this call, and since callers mutate state before
+    /// triggering, that pass observes the mutation.
     ///
     /// Correctness relies on the draw loop draining exactly one token per
     /// iteration *before* drawing. Do not change the loop to recv after
@@ -115,6 +123,23 @@ impl RedrawTrigger {
     pub fn trigger(&self) {
         let _ = self.0.try_send(());
     }
+
+    /// Open a property-update batch bound to this trigger. Property
+    /// notifications are deferred until the batch — including any batches
+    /// spawned from it by property-change reactions holding the batch
+    /// guard — completes, and then exactly one redraw trigger is enqueued.
+    /// Use this instead of manual `trigger()` calls around property
+    /// mutations so a pass can never observe the intermediate state of a
+    /// multi-step update.
+    pub fn make_guard(&self, debug_str: Option<&'static str>) -> PropertyAtomicGuard {
+        let redraw = self.0.clone();
+        PropertyAtomicGuard::new(Box::new(move |_| {
+            if let Some(tag) = debug_str {
+                t!("Redraw batch ({tag}) ended, triggering redraw");
+            }
+            let _ = redraw.try_send(());
+        }))
+    }
 }
 
 #[async_trait]

+ 0 - 1
bin/app/src/ui/text.rs

@@ -273,7 +273,6 @@ impl UIObject for Text {
 
 impl Drop for Text {
     fn drop(&mut self) {
-        let atom = self.renderer.make_guard(gfxtag!("Text::drop"));
         self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
 }

+ 0 - 1
bin/app/src/ui/tokentable/mod.rs

@@ -437,7 +437,6 @@ impl UIObject for TokenTable {
 
 impl Drop for TokenTable {
     fn drop(&mut self) {
-        let atom = self.renderer.make_guard(gfxtag!("TokenTable::drop"));
         self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
 }

+ 0 - 1
bin/app/src/ui/vector_art/mod.rs

@@ -206,7 +206,6 @@ impl UIObject for VectorArt {
 
 impl Drop for VectorArt {
     fn drop(&mut self) {
-        let atom = self.renderer.make_guard(gfxtag!("VectorArt::drop"));
         self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
 }

+ 0 - 1
bin/app/src/ui/vid/mod.rs

@@ -323,7 +323,6 @@ impl UIObject for Video {
 
 impl Drop for Video {
     fn drop(&mut self) {
-        let atom = self.renderer.make_guard(gfxtag!("Video::drop"));
         self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
     }
 }

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

@@ -138,12 +138,10 @@ impl Window {
                     panic!("self destroyed before modify_task was stopped!");
                 };
 
-                let atom = &mut self_.renderer.make_guard(gfxtag!("Window::resize_task"));
-                // Now update the properties
+                // Now update the properties. The guard triggers a redraw
+                // pass once the update batch has settled.
+                let atom = &mut self_.redraw_tx.make_guard(gfxtag!("Window::resize_task"));
                 screen_size2.set(atom, size);
-                drop(atom);
-
-                self_.redraw_tx.trigger();
             }
         });
 
@@ -160,7 +158,9 @@ impl Window {
                 }
 
                 let Some(self_) = me2.upgrade() else { break };
-                let atom = &mut self_.renderer.make_guard(gfxtag!("Window::draw_pass"));
+                // A none() guard: the pass must not trigger a follow-on
+                // pass of itself when its own batch settles.
+                let atom = &mut PropertyAtomicGuard::none();
                 self_.draw(atom).await;
             }
         });
@@ -213,17 +213,16 @@ impl Window {
             ex.spawn(async move {
                 while let Ok(insets_val) = insets_rx.recv().await {
                     let Some(self_) = me.upgrade() else { break };
-                    let atom = &mut self_.renderer.make_guard(gfxtag!("Window::insets_task"));
                     let scale = self_.scale.get();
                     let insets_val = Rectangle::from(insets_val) / scale;
                     t!("Insets changed: {insets_val:?}");
-                    insets.set(atom, &insets_val);
-                    drop(atom);
 
                     // Insets are set with an internal role, so draw-pass
-                    // widgets skip the echo notifications. Trigger the pass
-                    // explicitly so the new insets get laid out.
-                    self_.redraw_tx.trigger();
+                    // widgets skip the echo notifications. The guard
+                    // triggers a pass once the batch settles so the new
+                    // insets get laid out.
+                    let atom = &mut self_.redraw_tx.make_guard(gfxtag!("Window::insets_task"));
+                    insets.set(atom, &insets_val);
                 }
             })
         };