Przeglądaj źródła

app/layer: migrate Layer to new trigger redraw paradigm.

darkfi 1 tydzień temu
rodzic
commit
17da9178bf

+ 1 - 1
bin/app/Makefile

@@ -29,7 +29,7 @@ DEBUG_FEATURES = --features=enable-filelog,enable-plugins
 
 #DEV_FEATURES = --features=enable-filelog,enable-netdebug,emulate-android
 #DEV_FEATURES = --features=enable-filelog,enable-netdebug,enable-plugins
-DEV_FEATURES = --features=schema-app,enable-netdebug,enable-plugin-darkirc
+DEV_FEATURES = --features=schema-app,enable-netdebug
 
 default: build-release
 	./darkfi-app

+ 8 - 5
bin/app/src/app/schema/chat.rs

@@ -42,7 +42,7 @@ use crate::{
     shape,
     ui::{
         chatview, emoji_picker, BaseEdit, BaseEditType, Button, ChatView, EmojiPicker, Layer,
-        Shortcut, Text, VectorArt, VectorShape,
+        RedrawTrigger, Shortcut, Text, VectorArt, VectorShape,
     },
     util::{i18n::I18nBabelFish, unixtime},
     ExecutorPtr,
@@ -191,6 +191,7 @@ pub async fn make(
     i18n_fish: &I18nBabelFish,
     emoji_meshes: emoji_picker::EmojiMeshesPtr,
     is_first_time: bool,
+    redraw: RedrawTrigger,
 ) -> SceneNodePtr {
     let window_scale =
         PropertyFloat32::wrap(&sg_root.lookup_node("/window").unwrap(), Role::Internal, "scale", 0)
@@ -222,7 +223,7 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     layer_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     layer_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
-    let layer_node = layer_node.setup(|me| Layer::new(me, renderer.clone())).await;
+    let layer_node = layer_node.setup(|me| Layer::new(me, renderer.clone(), redraw.clone())).await;
     content.link(layer_node.clone());
 
     // Create a bg mesh on top to fade the bg image
@@ -475,7 +476,7 @@ pub async fn make(
     layer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     layer_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
     layer_node.set_property_u32(atom, Role::App, "priority", 1).unwrap();
-    let layer_node = layer_node.setup(|me| Layer::new(me, renderer.clone())).await;
+    let layer_node = layer_node.setup(|me| Layer::new(me, renderer.clone(), redraw.clone())).await;
     chat_layer_node.link(layer_node.clone());
 
     // ChatView
@@ -643,7 +644,8 @@ pub async fn make(
     select_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     select_layer.set_property_u32(atom, Role::App, "z_index", 100).unwrap();
     select_layer.set_property_u32(atom, Role::App, "priority", 100).unwrap();
-    let select_layer = select_layer.setup(|me| Layer::new(me, renderer.clone())).await;
+    let select_layer =
+        select_layer.setup(|me| Layer::new(me, renderer.clone(), redraw.clone())).await;
     content.link(select_layer.clone());
 
     // Single background box covering both buttons (the whole top strip).
@@ -1231,7 +1233,8 @@ pub async fn make(
     prop.add_depend(&editbox_bg_rect_prop, 1, "editz_bg_top_y");
     cmd_layer_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     cmd_layer_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
-    let cmd_layer_node = cmd_layer_node.setup(|me| Layer::new(me, renderer.clone())).await;
+    let cmd_layer_node =
+        cmd_layer_node.setup(|me| Layer::new(me, renderer.clone(), redraw.clone())).await;
     layer_node.link(cmd_layer_node.clone());
 
     let cmd_hint_is_visible =

+ 11 - 10
bin/app/src/app/schema/menu/channel.rs

@@ -324,7 +324,9 @@ pub async fn make(
     .unwrap();
     content_area_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     content_area_node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
-    let content_area = content_area_node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let content_area = content_area_node
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(content_area.clone());
 
     // Red bottom glow below outline
@@ -1206,7 +1208,8 @@ pub async fn make(
     node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "priority", 1).unwrap();
-    let editlayer_node = node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let editlayer_node =
+        node.setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(editlayer_node.clone());
 
     let node = create_vector_art("btns_bg");
@@ -1420,6 +1423,7 @@ pub async fn make(
     let db2 = db.clone();
     let i18n_fish2 = i18n_fish.clone();
     let emoji_meshes2 = emoji_meshes.clone();
+    let redraw2 = app.redraw_trigger.clone();
 
     let listen_select = app.ex.spawn(async move {
         while let Ok(data) = recvr.recv().await {
@@ -1448,6 +1452,7 @@ pub async fn make(
                 &i18n_fish2,
                 emoji_meshes2.clone(),
                 is_first_time,
+                redraw2.clone(),
             )
             .await;
             match node.pimpl() {
@@ -1472,14 +1477,10 @@ pub async fn make(
             // Hide channel screen
             channel_vis.set(atom, false);
 
-            // Force redraw so newly added node parent_rect gets set.
-            // There are other ways to do this but this is easiest for now.
-            // We can think later about doing this better.
-            let win = sg_root.lookup_node("/window").unwrap();
-            match win.pimpl() {
-                Pimpl::Window(win) => win.draw(atom).await,
-                _ => panic!("wrong pimpl"),
-            }
+            // Trigger a draw pass so the newly added node's parent_rect
+            // gets set. The pass walks the whole tree so the new layer is
+            // drawn with correct geometry.
+            redraw2.trigger();
 
             // Trigger rescan for this channel
             if let Some(darkirc) = sg_root.lookup_node("/plugin/darkirc") {

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

@@ -325,7 +325,9 @@ pub async fn make(
     .unwrap();
     content_area_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     content_area_node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
-    let content_area = content_area_node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let content_area = content_area_node
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(content_area.clone());
 
     // Red bottom glow below outline
@@ -1228,7 +1230,8 @@ pub async fn make(
     node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "priority", 1).unwrap();
-    let editlayer_node = node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let editlayer_node =
+        node.setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content_area.link(editlayer_node.clone());
 
     let node = create_vector_art("btns_bg");
@@ -1418,6 +1421,7 @@ pub async fn make(
     let db2 = db.clone();
     let i18n_fish2 = i18n_fish.clone();
     let emoji_meshes2 = emoji_meshes.clone();
+    let redraw2 = app.redraw_trigger.clone();
     let contact_vis = contact_is_visible.clone();
 
     let listen_select = app.ex.spawn(async move {
@@ -1444,6 +1448,7 @@ pub async fn make(
                 &i18n_fish2,
                 emoji_meshes2.clone(),
                 is_first_time,
+                redraw2.clone(),
             )
             .await;
             match node.pimpl() {
@@ -1462,11 +1467,10 @@ pub async fn make(
 
             contact_vis.set(atom, false);
 
-            let win = sg_root.lookup_node("/window").unwrap();
-            match win.pimpl() {
-                Pimpl::Window(win) => win.draw(atom).await,
-                _ => panic!("wrong pimpl"),
-            }
+            // Trigger a draw pass so the newly added node's parent_rect
+            // gets set. The pass walks the whole tree so the new layer is
+            // drawn with correct geometry.
+            redraw2.trigger();
 
             // Trigger rescan to fetch this contact's DM history from the DAG.
             // The rescan reloads contacts first, so a freshly added contact's

+ 2 - 1
bin/app/src/app/schema/menu/edit_buttons.rs

@@ -107,7 +107,8 @@ pub async fn create_edit_buttons(
     node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "priority", 1).unwrap();
-    let editlayer_node = node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let editlayer_node =
+        node.setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     parent.link(editlayer_node.clone());
 
     let editlayer_is_visible =

+ 17 - 4
bin/app/src/app/schema/menu/mod.rs

@@ -135,7 +135,9 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     contact_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     contact_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let contact_layer = contact_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let contact_layer = contact_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(contact_layer.clone());
     let contact_is_visible =
         PropertyBool::wrap(&contact_layer, Role::App, "is_visible", 0).unwrap();
@@ -149,7 +151,9 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     channel_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     channel_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let channel_layer = channel_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let channel_layer = channel_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(channel_layer.clone());
     let channel_is_visible =
         PropertyBool::wrap(&channel_layer, Role::App, "is_visible", 0).unwrap();
@@ -174,7 +178,9 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     layer_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     layer_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
-    let layer_node = layer_node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let layer_node = layer_node
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(layer_node.clone());
 
     let menulayer_is_visible = PropertyBool::wrap(&layer_node, Role::App, "is_visible", 0).unwrap();
@@ -291,7 +297,8 @@ pub async fn make(
     node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_u32(atom, Role::App, "priority", 1).unwrap();
-    let mainlayer_node = node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let mainlayer_node =
+        node.setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     let mainlayer_is_visible =
         PropertyBool::wrap(&mainlayer_node, Role::App, "is_visible", 0).unwrap();
     layer_node.link(mainlayer_node.clone());
@@ -474,6 +481,7 @@ pub async fn make(
     menu_node.register("edit_done", edit_done_slot).unwrap();
     let sg_root = app.sg_root.clone();
     let menu_node2 = menu_node.clone();
+    let redraw = app.redraw_trigger.clone();
     let edit_done_listen = app.ex.spawn(async move {
         while let Ok(data) = edit_done_recvr.recv().await {
             let deleted_items: Vec<String> = deserialize(&data).unwrap();
@@ -492,6 +500,11 @@ pub async fn make(
                 }
             }
 
+            // Unlinking changes no property, so request a pass explicitly:
+            // the parent layer re-collects child references next pass and
+            // the removed layer's subtree is dropped from the output.
+            redraw.trigger();
+
             // TODO: reload plugin so it drops un-joined channels/contacts from its maps.
             // if let Some(darkirc) = sg_root.lookup_node("/plugin/darkirc") {
             //     for item in &deleted_items {

+ 7 - 3
bin/app/src/app/schema/mod.rs

@@ -312,7 +312,8 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     prop.add_depend(&window_insets, 3, "insets_bottom");
     content.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     content.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
-    let content = content.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let content =
+        content.setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     window.link(content.clone());
 
     if COLOR_SCHEME == ColorScheme::DarkMode {
@@ -409,7 +410,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     prop.set_f32(atom, Role::App, 3, 1000.).unwrap();
     netlayer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     netlayer_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let netlayer_node = netlayer_node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let netlayer_node = netlayer_node
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(netlayer_node.clone());
 
     let node = create_vector_art("net0");
@@ -566,7 +569,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
     settingslayer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     settingslayer_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let settingslayer_node =
-        settingslayer_node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+        settingslayer_node.setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
     content.link(settingslayer_node.clone());
 
     // Background
@@ -703,6 +706,7 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish, db
             i18n_fish,
             emoji_meshes.clone(),
             is_first_time,
+            app.redraw_trigger.clone(),
         )
         .await;
     }

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

@@ -83,7 +83,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     layer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
-    let layer_node = layer_node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let layer_node = layer_node
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     window.link(layer_node.clone());
 
     // Create a bg mesh

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

@@ -58,7 +58,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     layer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
-    let layer_node = layer_node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let layer_node = layer_node
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     window.link(layer_node.clone());
 
     // Create a bg mesh

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

@@ -43,7 +43,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     layer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
-    let layer_node = layer_node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let layer_node = layer_node
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     window.link(layer_node.clone());
 
     // Create a bg mesh

+ 3 - 1
bin/app/src/app/schema/wallet/main.rs

@@ -65,7 +65,9 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     main_layer.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     main_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let main_layer = main_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let main_layer = main_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     wallet_layer.link(main_layer.clone());
 
     let main_is_visible = PropertyBool::wrap(&main_layer, Role::App, "is_visible", 0).unwrap();

+ 3 - 1
bin/app/src/app/schema/wallet/mod.rs

@@ -58,7 +58,9 @@ pub async fn make(app: &App, content: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     wallet_layer.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     wallet_layer.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
-    let wallet_layer = wallet_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let wallet_layer = wallet_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     content.link(wallet_layer.clone());
 
     // Create main wallet layer

+ 3 - 1
bin/app/src/app/schema/wallet/netstatus.rs

@@ -52,7 +52,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, 1000.).unwrap();
     netlayer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     netlayer_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
-    let netlayer_node = netlayer_node.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let netlayer_node = netlayer_node
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     wallet_layer.link(netlayer_node.clone());
 
     let node = create_vector_art("net0");

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

@@ -57,7 +57,9 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     receive_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     receive_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let receive_layer = receive_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let receive_layer = receive_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     wallet_layer.link(receive_layer.clone());
 
     create_bg_mesh(app, atom, &receive_layer, "receive_bg").await;

+ 3 - 1
bin/app/src/app/schema/wallet/send_step1.rs

@@ -71,7 +71,9 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     send_step1_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     send_step1_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let send_step1_layer = send_step1_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let send_step1_layer = send_step1_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     wallet_layer.link(send_step1_layer.clone());
     let step1_is_visible =
         PropertyBool::wrap(&send_step1_layer, Role::App, "is_visible", 0).unwrap();

+ 3 - 1
bin/app/src/app/schema/wallet/send_step2.rs

@@ -100,7 +100,9 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     send_step2_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     send_step2_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let send_step2_layer = send_step2_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let send_step2_layer = send_step2_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     wallet_layer.link(send_step2_layer.clone());
     let step2_is_visible =
         PropertyBool::wrap(&send_step2_layer, Role::App, "is_visible", 0).unwrap();

+ 6 - 2
bin/app/src/app/schema/wallet/send_step3.rs

@@ -75,7 +75,9 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     send_step3_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     send_step3_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let send_step3_layer = send_step3_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let send_step3_layer = send_step3_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     wallet_layer.link(send_step3_layer.clone());
     let step3_is_visible =
         PropertyBool::wrap(&send_step3_layer, Role::App, "is_visible", 0).unwrap();
@@ -323,7 +325,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, AMOUNT_FONTSIZE).unwrap();
     prop.add_depend(&addr_h_prop, 0, "addr_height");
     amount_wrapper.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let amount_wrapper = amount_wrapper.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let amount_wrapper = amount_wrapper
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     send_step3_layer.link(amount_wrapper.clone());
 
     // Error message text

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

@@ -70,7 +70,9 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     send_step4_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     send_step4_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let send_step4_layer = send_step4_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let send_step4_layer = send_step4_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     wallet_layer.link(send_step4_layer.clone());
     let step4_is_visible =
         PropertyBool::wrap(&send_step4_layer, Role::App, "is_visible", 0).unwrap();
@@ -265,7 +267,9 @@ pub async fn make(
     prop.set_f32(atom, Role::App, 3, AMOUNT_FONTSIZE).unwrap();
     prop.add_depend(&addr_h_prop, 0, "addr_height");
     amount_wrapper.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    let amount_wrapper = amount_wrapper.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let amount_wrapper = amount_wrapper
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     send_step4_layer.link(amount_wrapper.clone());
 
     // Amount text

+ 3 - 1
bin/app/src/app/schema/wallet/tx_status.rs

@@ -103,7 +103,9 @@ pub async fn make(
     prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
     tx_status_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
     tx_status_layer.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
-    let tx_status_layer = tx_status_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let tx_status_layer = tx_status_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     wallet_layer.link(tx_status_layer.clone());
     let tx_status_is_visible =
         PropertyBool::wrap(&tx_status_layer, Role::App, "is_visible", 0).unwrap();

+ 3 - 1
bin/app/src/app/schema/wallet/util.rs

@@ -559,7 +559,9 @@ pub async fn create_tooltip(
 
     tooltip_layer.add_method("show", vec![], None).unwrap();
 
-    let tooltip_layer = tooltip_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    let tooltip_layer = tooltip_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
     parent_layer.link(tooltip_layer.clone());
 
     // Create box

+ 22 - 42
bin/app/src/ui/layer.rs

@@ -32,7 +32,8 @@ use crate::{
 };
 
 use super::{
-    get_children_ordered, get_ui_object3, get_ui_object_ptr, DrawUpdate, OnModify, UIObject,
+    get_children_ordered, get_ui_object3, get_ui_object_ptr, DrawUpdate, OnModify, RedrawTrigger,
+    UIObject,
 };
 
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui:layer", $($arg)*); } }
@@ -41,7 +42,7 @@ pub type LayerPtr = Arc<Layer>;
 
 pub struct Layer {
     node: SceneNodeWeak,
-    renderer: Renderer,
+    redraw: RedrawTrigger,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
     dc_key: u64,
 
@@ -49,21 +50,21 @@ pub struct Layer {
     rect: PropertyRect,
     z_index: PropertyUint32,
     priority: PropertyUint32,
-
-    parent_rect: SyncMutex<Option<Rectangle>>,
 }
 
 impl Layer {
-    pub async fn new(node: SceneNodeWeak, renderer: Renderer) -> Pimpl {
-        let node_ref = &node.upgrade().unwrap();
+    pub async fn new(_node: SceneNodeWeak, renderer: Renderer, redraw: RedrawTrigger) -> Pimpl {
+        let node_ref = &_node.upgrade().unwrap();
         let is_visible = PropertyBool::wrap(node_ref, Role::Internal, "is_visible", 0).unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
         let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
         let priority = PropertyUint32::wrap(node_ref, Role::Internal, "priority", 0).unwrap();
 
+        let _ = renderer;
+
         let self_ = Arc::new(Self {
-            node,
-            renderer,
+            node: _node,
+            redraw,
             tasks: SyncMutex::new(vec![]),
             dc_key: OsRng.gen(),
 
@@ -71,8 +72,6 @@ impl Layer {
             rect,
             z_index,
             priority,
-
-            parent_rect: SyncMutex::new(None),
         });
 
         Pimpl::Layer(self_)
@@ -83,21 +82,6 @@ impl Layer {
         get_children_ordered(&node)
     }
 
-    #[instrument(target = "ui::layer")]
-    async fn redraw(self: Arc<Self>, batch: BatchGuardPtr) {
-        let Some(parent_rect) = self.parent_rect.lock().clone() else {
-            warn!(target: "ui:layer", "Skip draw since parent rect is empty");
-            return
-        };
-
-        let atom = &mut batch.spawn();
-        let Some(draw_update) = self.get_draw_calls(parent_rect, atom).await else {
-            error!(target: "ui:layer", "Layer failed to draw");
-            return
-        };
-        self.renderer.replace_draw_calls(Some(batch.id), draw_update.draw_calls);
-    }
-
     async fn get_draw_calls(
         &self,
         parent_rect: Rectangle,
@@ -154,9 +138,19 @@ impl UIObject for Layer {
         let me = Arc::downgrade(&self);
 
         let mut on_modify = OnModify::new(ex.clone(), 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.z_index.prop(), Self::redraw);
+        // Stateless in the pass: property changes only request a draw pass.
+        // All layer output is recomputed by the pass itself. Internal-role
+        // sets are eval echoes of the pass, so only external (App) changes
+        // trigger — otherwise every pass would queue another, forever.
+        on_modify.when_change_external(self.is_visible.prop(), |self_, _| async move {
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
+            self_.redraw.trigger();
+        });
+        on_modify.when_change_external(self.z_index.prop(), |self_, _| async move {
+            self_.redraw.trigger();
+        });
 
         *self.tasks.lock() = on_modify.tasks;
 
@@ -168,7 +162,6 @@ impl UIObject for Layer {
 
     fn stop(&self) {
         self.tasks.lock().clear();
-        *self.parent_rect.lock() = None;
         for child in self.get_children() {
             let obj = get_ui_object3(&child);
             obj.stop();
@@ -181,19 +174,6 @@ impl UIObject for Layer {
         parent_rect: Rectangle,
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
-        *self.parent_rect.lock() = Some(parent_rect);
-
-        /*
-        if !parent_rect.dim().contains(&offset_rect) {
-            error!(
-                target: "ui::layer",
-                "layer rect {:?} is not inside parent {:?}",
-                offset_rect, parent_rect
-            );
-            return None
-        }
-        */
-
         self.get_draw_calls(parent_rect, atom).await
     }
 

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

@@ -185,6 +185,32 @@ impl<T: Send + Sync + 'static> OnModify<T> {
         f: impl Fn(Arc<T>, BatchGuardPtr) -> F + Send + 'static,
     ) where
         F: std::future::Future<Output = ()> + Send + 'static,
+    {
+        self.when_change_impl(prop, false, f)
+    }
+
+    /// Like `when_change`, but also skips `Role::Internal` modifications of
+    /// dependencies. Draw-pass-migrated widgets want this: internal sets are
+    /// eval echoes (typically produced by the draw pass itself), so reacting
+    /// to them would queue a pass for every pass, forever. External mutation
+    /// sites (handlers, resize/insets tasks) trigger passes explicitly.
+    pub fn when_change_external<F>(
+        &mut self,
+        prop: PropertyPtr,
+        f: impl Fn(Arc<T>, BatchGuardPtr) -> F + Send + 'static,
+    ) where
+        F: std::future::Future<Output = ()> + Send + 'static,
+    {
+        self.when_change_impl(prop, true, f)
+    }
+
+    fn when_change_impl<F>(
+        &mut self,
+        prop: PropertyPtr,
+        skip_internal: bool,
+        f: impl Fn(Arc<T>, BatchGuardPtr) -> F + Send + 'static,
+    ) where
+        F: std::future::Future<Output = ()> + Send + 'static,
     {
         let mut on_modify_subs = vec![(Arc::downgrade(&prop), None, prop.subscribe_modify())];
         for dep in prop.get_depends() {
@@ -209,8 +235,12 @@ impl<T: Send + Sync + 'static> OnModify<T> {
                     return
                 };
 
-                // Skip internal messages from ourselves or explicitly marked ignored
-                if (idx == 0 && role == Role::Internal) || role == Role::Ignored {
+                // Skip internal messages from ourselves or explicitly marked ignored.
+                // Draw-pass widgets also skip internal dependency echoes.
+                if (idx == 0 && role == Role::Internal) ||
+                    (skip_internal && role == Role::Internal) ||
+                    role == Role::Ignored
+                {
                     continue
                 }
                 if let Some(prop_i) = prop_i {

+ 7 - 3
bin/app/src/ui/scroll_layer.rs

@@ -28,7 +28,7 @@ use crate::{
     ExecutorPtr,
 };
 
-use super::{DrawUpdate, Layer, LayerPtr, UIObject};
+use super::{DrawUpdate, Layer, LayerPtr, RedrawTrigger, UIObject};
 
 pub type ScrollLayerPtr = Arc<ScrollLayer>;
 
@@ -37,8 +37,12 @@ pub struct ScrollLayer {
 }
 
 impl ScrollLayer {
-    pub async fn new(node: SceneNodeWeak, renderer: crate::gfx::Renderer) -> Pimpl {
-        let layer = Layer::new(node.clone(), renderer).await;
+    pub async fn new(
+        node: SceneNodeWeak,
+        renderer: crate::gfx::Renderer,
+        redraw: RedrawTrigger,
+    ) -> Pimpl {
+        let layer = Layer::new(node.clone(), renderer, redraw).await;
         let inner = match layer {
             Pimpl::Layer(l) => l,
             _ => unreachable!(),

+ 6 - 0
bin/app/src/ui/win/mod.rs

@@ -218,6 +218,12 @@ impl Window {
                     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.trigger();
                 }
             })
         };