Ver código fonte

app/wallet: update create_bottom_button

x 2 semanas atrás
pai
commit
fcb83e893a

+ 8 - 17
bin/app/src/app/schema/wallet/send.rs

@@ -22,7 +22,6 @@ use darkfi_serial::Decodable;
 use crate::{
     app::App,
     gfx::gfxtag,
-    mesh::COLOR_CYAN,
     prop::{PropertyBool, PropertyFloat32, Role},
     scene::SceneNodePtr,
     util::i18n::I18nBabelFish,
@@ -104,25 +103,17 @@ pub async fn make(
             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
-                .lookup_node("/window/content/wallet/send_step4_layer/send_btn_label")
+            if let Some(send_layer_node) = sg_root_for_built
+                .lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer")
             {
-                send_label_node.set_property_str(atom, Role::App, "text", "send").unwrap();
-                let prop = send_label_node.get_property("text_color").unwrap();
-                prop.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
-                prop.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
-                prop.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
-                prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
+                send_layer_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
             }
-            if let Some(send_bg_grey_node) = sg_root_for_built
-                .lookup_node("/window/content/wallet/send_step4_layer/send_btn_bg_grey")
+            if let Some(send_layer_grey_node) = sg_root_for_built
+                .lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer_grey")
             {
-                send_bg_grey_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
-            }
-            if let Some(send_bg_node) =
-                sg_root_for_built.lookup_node("/window/content/wallet/send_step4_layer/send_btn_bg")
-            {
-                send_bg_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+                send_layer_grey_node
+                    .set_property_bool(atom, Role::App, "is_visible", false)
+                    .unwrap();
             }
 
             // Show transaction fee

+ 19 - 45
bin/app/src/app/schema/wallet/send_step2.rs

@@ -315,59 +315,33 @@ pub async fn make(
     send_step2_layer.link(node);
 
     // Add recipient button
-    let (node, btn_bg_valid, btn_bg_invalid, add_recipient_label_node) =
-        create_bottom_button_with_states(
-            app,
-            atom,
-            &send_step2_layer,
-            "send_add_recipient_btn",
-            &mut cc,
-            "add recipient",
-            &window_scale,
-            i18n_fish,
-            false,
-        )
-        .await;
+    let (node, btn_layer_valid, btn_layer_grey) = create_bottom_button_with_states(
+        app,
+        atom,
+        &send_step2_layer,
+        "send_add_recipient_btn",
+        &mut cc,
+        "add recipient",
+        "add recipient",
+        &window_scale,
+        i18n_fish,
+        false,
+    )
+    .await;
     let recipient_input2 = recipient_input.clone();
     let recipient_text = recipient_input.get_property("text").unwrap();
     let recipient_text_sub = recipient_text.subscribe_modify();
     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 btn_layer_valid_clone = btn_layer_valid.clone();
+    let btn_layer_grey_clone = btn_layer_grey.clone();
     let listen_recipient_text = app.ex.spawn(async move {
         while let Ok(_) = recipient_text_sub.receive().await {
             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();
             let addr = recipient_input2.get_property_str("text").unwrap();
-            // Grey color for invalid
-            if addr.is_empty() {
-                label_text_color.set_f32(atom, Role::App, 0, 0.5).unwrap();
-                label_text_color.set_f32(atom, Role::App, 1, 0.5).unwrap();
-                label_text_color.set_f32(atom, Role::App, 2, 0.5).unwrap();
-                label_text_color.set_f32(atom, Role::App, 3, 1.).unwrap();
-                btn_bg_valid_visible.set_bool(atom, Role::App, 0, false).unwrap();
-                btn_bg_invalid_visible.set_bool(atom, Role::App, 0, true).unwrap();
-            } else {
-                // Cyan color for valid
-                if addr.clone().parse::<Address>().is_ok() {
-                    label_text_color.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
-                    label_text_color.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
-                    label_text_color.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
-                    label_text_color.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
-                    btn_bg_valid_visible.set_bool(atom, Role::App, 0, true).unwrap();
-                    btn_bg_invalid_visible.set_bool(atom, Role::App, 0, false).unwrap();
-                } else {
-                    // Grey color for invalid
-                    label_text_color.set_f32(atom, Role::App, 0, 0.5).unwrap();
-                    label_text_color.set_f32(atom, Role::App, 1, 0.5).unwrap();
-                    label_text_color.set_f32(atom, Role::App, 2, 0.5).unwrap();
-                    label_text_color.set_f32(atom, Role::App, 3, 1.).unwrap();
-                    btn_bg_valid_visible.set_bool(atom, Role::App, 0, false).unwrap();
-                    btn_bg_invalid_visible.set_bool(atom, Role::App, 0, true).unwrap();
-                }
-            }
+            // Cyan for valid, grey for invalid: switch button state layers
+            let valid = !addr.is_empty() && addr.parse::<Address>().is_ok();
+            btn_layer_valid_clone.set_property_bool(atom, Role::App, "is_visible", valid).unwrap();
+            btn_layer_grey_clone.set_property_bool(atom, Role::App, "is_visible", !valid).unwrap();
         }
     });
     app.tasks.lock().push(listen_recipient_text);

+ 33 - 48
bin/app/src/app/schema/wallet/send_step3.rs

@@ -433,19 +433,19 @@ pub async fn make(
     amount_wrapper.link(token_symbol_node.clone());
 
     // Add amount button with states
-    let (node, btn_bg_valid, btn_bg_invalid, add_amount_label_node) =
-        create_bottom_button_with_states(
-            app,
-            atom,
-            &send_step3_layer,
-            "send_add_amount_btn",
-            &mut cc,
-            "add amount",
-            &window_scale,
-            i18n_fish,
-            false, // Start with invalid state (grey)
-        )
-        .await;
+    let (node, btn_layer_valid, btn_layer_grey) = create_bottom_button_with_states(
+        app,
+        atom,
+        &send_step3_layer,
+        "send_add_amount_btn",
+        &mut cc,
+        "add amount",
+        "add amount",
+        &window_scale,
+        i18n_fish,
+        false, // Start with invalid state (grey)
+    )
+    .await;
 
     // Amount input validation listener
     let amount_input2 = input_node.clone();
@@ -457,9 +457,8 @@ pub async fn make(
     let amount_text_sub = amount_text.subscribe_modify();
     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 btn_layer_valid_clone = btn_layer_valid.clone();
+    let btn_layer_grey_clone = btn_layer_grey.clone();
     let listen_amount_text = app.ex.spawn(async move {
         while let Ok(_) = amount_text_sub.receive().await {
             let atom = &mut redraw.make_guard(gfxtag!("wallet amount input recv"));
@@ -470,10 +469,6 @@ pub async fn make(
                 error_node.set_property_str(atom, Role::App, "text", "").unwrap();
             }
 
-            let label_text_color =
-                add_amount_label_node_for_validation.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();
             let amount = amount_input2.get_property_str("text").unwrap();
 
             let sanitized_amount = if amount.is_empty() {
@@ -509,21 +504,13 @@ pub async fn make(
                     }
                 };
 
-                if is_valid {
-                    label_text_color.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
-                    label_text_color.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
-                    label_text_color.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
-                    label_text_color.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
-                    btn_bg_valid_visible.set_bool(atom, Role::App, 0, true).unwrap();
-                    btn_bg_invalid_visible.set_bool(atom, Role::App, 0, false).unwrap();
-                } else {
-                    label_text_color.set_f32(atom, Role::App, 0, 0.5).unwrap();
-                    label_text_color.set_f32(atom, Role::App, 1, 0.5).unwrap();
-                    label_text_color.set_f32(atom, Role::App, 2, 0.5).unwrap();
-                    label_text_color.set_f32(atom, Role::App, 3, 1.).unwrap();
-                    btn_bg_valid_visible.set_bool(atom, Role::App, 0, false).unwrap();
-                    btn_bg_invalid_visible.set_bool(atom, Role::App, 0, true).unwrap();
-                }
+                // Cyan for valid, grey for invalid: switch button state layers
+                btn_layer_valid_clone
+                    .set_property_bool(atom, Role::App, "is_visible", is_valid)
+                    .unwrap();
+                btn_layer_grey_clone
+                    .set_property_bool(atom, Role::App, "is_visible", !is_valid)
+                    .unwrap();
             }
         }
     });
@@ -582,24 +569,22 @@ pub async fn make(
                     amount_node.set_property_str(atom, Role::App, "text", &amount).unwrap();
                 }
 
-                // Switch to step4 and set send button to "building tx..."
+                // Switch to step4 with the send button in "building tx..." state
                 step3_is_visible2.set(atom, false);
                 if let Some(step4) = sg_root.lookup_node("/window/content/wallet/send_step4_layer") {
                     step4.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
                 }
-                if let Some(send_label_node) = sg_root.lookup_node("/window/content/wallet/send_step4_layer/send_btn_label") {
-                    send_label_node.set_property_str(atom, Role::App, "text", "building tx...").unwrap();
-                    let prop = send_label_node.get_property("text_color").unwrap();
-                    prop.set_f32(atom, Role::App, 0, 0.5).unwrap();
-                    prop.set_f32(atom, Role::App, 1, 0.5).unwrap();
-                    prop.set_f32(atom, Role::App, 2, 0.5).unwrap();
-                    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
-                }
-                if let Some(send_bg_grey_node) = sg_root.lookup_node("/window/content/wallet/send_step4_layer/send_btn_bg_grey") {
-                    send_bg_grey_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+                if let Some(send_layer_node) =
+                    sg_root.lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer")
+                {
+                    send_layer_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
                 }
-                if let Some(send_bg_node) = sg_root.lookup_node("/window/content/wallet/send_step4_layer/send_btn_bg") {
-                    send_bg_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+                if let Some(send_layer_grey_node) =
+                    sg_root.lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer_grey")
+                {
+                    send_layer_grey_node
+                        .set_property_bool(atom, Role::App, "is_visible", true)
+                        .unwrap();
                 }
 
                 // Build the transaction via DrkPlugin

+ 26 - 16
bin/app/src/app/schema/wallet/send_step4.rs

@@ -395,20 +395,44 @@ pub async fn make(
     .await;
 
     // Send button (bottom button)
-    let (node, _bg_valid, _bg_invalid, _label) = create_bottom_button_with_states(
+    let (node, send_btn_layer, _send_btn_layer_grey) = create_bottom_button_with_states(
         app,
         atom,
         &send_step4_layer,
         "send_btn",
         &mut cc,
         "send",
+        "building tx...",
         &window_scale,
         i18n_fish,
-        true,
+        false,
     )
     .await;
 
+    // Inset box shadow in the send button
     let redraw = app.redraw_trigger.clone();
+    let shadow_node = create_vector_art("send_btn_shadow");
+    let prop = shadow_node.get_property("rect").unwrap();
+    prop.set_default_f32(0, 0.).unwrap();
+    prop.set_default_f32(1, 0.).unwrap();
+    prop.set_default_expr(2, expr::load_var("w")).unwrap();
+    prop.set_default_expr(3, expr::load_var("h")).unwrap();
+    shadow_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+    let shadow_node = shadow_node
+        .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
+    let mut shape = VectorShape::new();
+    shape.add_box_shadow_inset(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        50.,
+        [COLOR_CYAN[0], COLOR_CYAN[1], COLOR_CYAN[2], 0.25],
+    );
+    shadow_node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    send_btn_layer.link(shadow_node);
+
     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();
@@ -416,14 +440,6 @@ pub async fn make(
     node.register("click", slot).unwrap();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
-            // Skip if the button is disabled
-            if let Some(btn_node) =
-                sg_root.lookup_node("/window/content/wallet/send_step4_layer/send_btn_bg")
-            {
-                if !btn_node.get_property_bool("is_visible").unwrap() {
-                    continue;
-                }
-            }
             let atom = &mut redraw.make_guard(gfxtag!("send button"));
 
             step4_is_visible1.set(atom, false);
@@ -508,12 +524,6 @@ pub async fn make(
                 .await;
 
                 if data.tx_built {
-                    // Set send button label
-                    if let Some(send_label_node) = sg_root
-                        .lookup_node("/window/content/wallet/send_step4_layer/send_btn_label")
-                    {
-                        send_label_node.set_property_str(atom, Role::App, "text", "send").unwrap();
-                    }
                     // Show transaction fee
                     if let Some(tx_fee_label) = sg_root
                         .lookup_node("/window/content/wallet/send_step4_layer/send_fee_label")

+ 127 - 86
bin/app/src/app/schema/wallet/util.rs

@@ -398,14 +398,29 @@ pub async fn create_bottom_button(
     window_scale: &PropertyFloat32,
     i18n_fish: &I18nBabelFish,
 ) -> SceneNodePtr {
-    let node = create_vector_art(&format!("{}_bg", name));
-    let prop = node.get_property("rect").unwrap();
+    // Button container: parent-frame rect, button-local children.
+    let btn_layer = create_layer(&format!("{name}_layer"));
+    btn_layer.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+    let prop = btn_layer.get_property("rect").unwrap();
     prop.set_default_f32(0, PADDING_X).unwrap();
     let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
     prop.set_default_expr(1, code).unwrap();
     let code = cc.compile("w - PADDING_X * 2.").unwrap();
     prop.set_default_expr(2, code).unwrap();
     prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
+    btn_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let btn_layer = btn_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
+    layer.link(btn_layer.clone());
+
+    // Bg outline
+    let node = create_vector_art(&format!("{}_bg", name));
+    let prop = node.get_property("rect").unwrap();
+    prop.set_default_f32(0, 0.).unwrap();
+    prop.set_default_f32(1, 0.).unwrap();
+    prop.set_default_expr(2, expr::load_var("w")).unwrap();
+    prop.set_default_expr(3, expr::load_var("h")).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
     let mut shape = VectorShape::new();
@@ -420,19 +435,16 @@ pub async fn create_bottom_button(
     node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
     let node =
         node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
-    layer.link(node.clone());
+    btn_layer.link(node);
 
     // Button label text (if provided)
     if let Some(text) = label_text {
         let label_node = create_text(&format!("{}_label", name));
         let prop = label_node.get_property("rect").unwrap();
-        prop.set_default_f32(0, PADDING_X).unwrap();
-        let code = cc
-            .compile("h - PADDING_X - BUTTON_HEIGHT + BUTTON_HEIGHT / 2 - BUTTON_FONTSIZE / 1.8")
-            .unwrap();
+        prop.set_default_f32(0, 0.).unwrap();
+        let code = cc.compile("BUTTON_HEIGHT / 2 - BUTTON_FONTSIZE / 1.8").unwrap();
         prop.set_default_expr(1, code).unwrap();
-        let code = cc.compile("w - PADDING_X * 2.").unwrap();
-        prop.set_default_expr(2, code).unwrap();
+        prop.set_default_expr(2, expr::load_var("w")).unwrap();
         prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
         label_node.get_property("font_size").unwrap().set_default_f32(0, BUTTON_FONTSIZE).unwrap();
         label_node.set_property_str(atom, Role::App, "text", text).unwrap();
@@ -453,76 +465,122 @@ pub async fn create_bottom_button(
                 )
             })
             .await;
-        layer.link(label_node);
+        btn_layer.link(label_node);
     }
 
     // Button
     let node = create_button(name);
     node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
     let prop = node.get_property("rect").unwrap();
-    prop.set_default_f32(0, PADDING_X).unwrap();
-    let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
-    prop.set_default_expr(1, code).unwrap();
-    let code = cc.compile("w - PADDING_X * 2.").unwrap();
-    prop.set_default_expr(2, code).unwrap();
-    prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
+    prop.set_default_f32(0, 0.).unwrap();
+    prop.set_default_f32(1, 0.).unwrap();
+    prop.set_default_expr(2, expr::load_var("w")).unwrap();
+    prop.set_default_expr(3, expr::load_var("h")).unwrap();
 
     let node =
         node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
-    layer.link(node.clone());
+    btn_layer.link(node.clone());
     node
 }
 
-/// Creates a bottom button with two states: valid (teal) and invalid (grey).
-/// Returns a tuple of (button_node, bg_valid_node, bg_invalid_node, label_node) for visibility control.
+/// Creates a bottom button with two states: valid (teal, active) and
+/// invalid (grey, inactive). Each state is a self-contained layer, so
+/// switching state is just an `is_visible` toggle on the two layers.
+/// Returns (button_node, valid_layer, grey_layer).
 pub async fn create_bottom_button_with_states(
     app: &App,
     atom: &mut PropertyAtomicGuard,
     layer: &SceneNodePtr,
     name: &str,
     cc: &mut Compiler,
-    label_text: &str,
+    label_valid: &str,
+    label_grey: &str,
     window_scale: &PropertyFloat32,
     i18n_fish: &I18nBabelFish,
     initial_valid: bool,
-) -> (SceneNodePtr, SceneNodePtr, SceneNodePtr, SceneNodePtr) {
-    // Button bg (teal outline - valid state)
-    let bg_valid = create_vector_art(&format!("{}_bg", name));
-    let prop = bg_valid.get_property("rect").unwrap();
-    prop.set_default_f32(0, PADDING_X).unwrap();
-    let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
-    prop.set_default_expr(1, code).unwrap();
-    let code = cc.compile("w - PADDING_X * 2.").unwrap();
-    prop.set_default_expr(2, code).unwrap();
-    prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
-    bg_valid.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    bg_valid.set_property_bool(atom, Role::App, "is_visible", initial_valid).unwrap();
-    let mut shape = VectorShape::new();
-    shape.add_outline(
-        expr::const_f32(0.),
-        expr::const_f32(0.),
-        expr::load_var("w"),
-        expr::load_var("h"),
-        1.,
+) -> (SceneNodePtr, SceneNodePtr, SceneNodePtr) {
+    let btn_layer = create_bottom_button_state(
+        app,
+        atom,
+        layer,
+        name,
+        cc,
+        label_valid,
+        window_scale,
+        i18n_fish,
         COLOR_TEAL,
-    );
-    bg_valid.set_property_shape(atom, Role::App, "shape", shape).unwrap();
-    let bg_valid = bg_valid
-        .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
-    layer.link(bg_valid.clone());
+        COLOR_CYAN,
+        true,
+        initial_valid,
+        "",
+    )
+    .await;
+    let grey_layer = create_bottom_button_state(
+        app,
+        atom,
+        layer,
+        name,
+        cc,
+        label_grey,
+        window_scale,
+        i18n_fish,
+        [0.5, 0.5, 0.5, 1.],
+        [0.5, 0.5, 0.5, 1.],
+        false,
+        !initial_valid,
+        "_grey",
+    )
+    .await;
+
+    let btn = btn_layer
+        .lookup_node(&format!("/{name}"))
+        .unwrap_or_else(|| panic!("bottom button node missing: {name}"));
+    (btn, btn_layer, grey_layer)
+}
 
-    // Button bg (grey outline - invalid state)
-    let bg_invalid = create_vector_art(&format!("{}_bg_grey", name));
-    let prop = bg_invalid.get_property("rect").unwrap();
+/// One state of a bottom button, wrapped in its own layer so callers can
+/// switch states by toggling layer visibility. Node names get `suffix`
+/// appended so both states can live under the same parent.
+#[allow(clippy::too_many_arguments)]
+async fn create_bottom_button_state(
+    app: &App,
+    atom: &mut PropertyAtomicGuard,
+    layer: &SceneNodePtr,
+    name: &str,
+    cc: &mut Compiler,
+    label_text: &str,
+    window_scale: &PropertyFloat32,
+    i18n_fish: &I18nBabelFish,
+    outline_color: Color,
+    label_color: Color,
+    is_active: bool,
+    is_visible: bool,
+    suffix: &str,
+) -> SceneNodePtr {
+    // Button container
+    let btn_layer = create_layer(&format!("{name}_layer{suffix}"));
+    btn_layer.set_property_bool(atom, Role::App, "is_visible", is_visible).unwrap();
+    let prop = btn_layer.get_property("rect").unwrap();
     prop.set_default_f32(0, PADDING_X).unwrap();
     let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
     prop.set_default_expr(1, code).unwrap();
     let code = cc.compile("w - PADDING_X * 2.").unwrap();
     prop.set_default_expr(2, code).unwrap();
     prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
-    bg_invalid.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
-    bg_invalid.set_property_bool(atom, Role::App, "is_visible", !initial_valid).unwrap();
+    btn_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let btn_layer = btn_layer
+        .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
+        .await;
+    layer.link(btn_layer.clone());
+
+    // Bg outline
+    let node = create_vector_art(&format!("{name}_bg{suffix}"));
+    let prop = node.get_property("rect").unwrap();
+    prop.set_default_f32(0, 0.).unwrap();
+    prop.set_default_f32(1, 0.).unwrap();
+    prop.set_default_expr(2, expr::load_var("w")).unwrap();
+    prop.set_default_expr(3, expr::load_var("h")).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
     let mut shape = VectorShape::new();
     shape.add_outline(
         expr::const_f32(0.),
@@ -530,41 +588,26 @@ pub async fn create_bottom_button_with_states(
         expr::load_var("w"),
         expr::load_var("h"),
         1.,
-        [0.5, 0.5, 0.5, 1.],
+        outline_color,
     );
-    bg_invalid.set_property_shape(atom, Role::App, "shape", shape).unwrap();
-    let bg_invalid = bg_invalid
-        .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
-        .await;
-    layer.link(bg_invalid.clone());
+    node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
+    let node =
+        node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
+    btn_layer.link(node);
 
     // Button label text
-    let label_node = create_text(&format!("{}_label", name));
+    let label_node = create_text(&format!("{name}_label{suffix}"));
     let prop = label_node.get_property("rect").unwrap();
-    prop.set_default_f32(0, PADDING_X).unwrap();
-    let code = cc
-        .compile("h - PADDING_X - BUTTON_HEIGHT + BUTTON_HEIGHT / 2 - BUTTON_FONTSIZE / 1.8")
-        .unwrap();
-    prop.set_default_expr(1, code).unwrap();
-    let code = cc.compile("w - PADDING_X * 2.").unwrap();
-    prop.set_default_expr(2, code).unwrap();
+    prop.set_default_f32(0, 0.).unwrap();
+    prop.set_default_f32(1, BUTTON_HEIGHT / 2. - BUTTON_FONTSIZE / 1.8).unwrap();
+    prop.set_default_expr(2, expr::load_var("w")).unwrap();
     prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
     label_node.get_property("font_size").unwrap().set_default_f32(0, BUTTON_FONTSIZE).unwrap();
     label_node.set_property_str(atom, Role::App, "text", label_text).unwrap();
     label_node.set_property_enum(atom, Role::App, "text_align", "center").unwrap();
 
     let prop = label_node.get_property("text_color").unwrap();
-    if initial_valid {
-        prop.set_default_f32(0, COLOR_CYAN[0]).unwrap();
-        prop.set_default_f32(1, COLOR_CYAN[1]).unwrap();
-        prop.set_default_f32(2, COLOR_CYAN[2]).unwrap();
-        prop.set_default_f32(3, COLOR_CYAN[3]).unwrap();
-    } else {
-        prop.set_default_f32(0, 0.5).unwrap();
-        prop.set_default_f32(1, 0.5).unwrap();
-        prop.set_default_f32(2, 0.5).unwrap();
-        prop.set_default_f32(3, 1.).unwrap();
-    }
+    prop.set_default_f32_multi(&label_color).unwrap();
     label_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
     let label_node = label_node
         .setup(|me| {
@@ -577,24 +620,22 @@ pub async fn create_bottom_button_with_states(
             )
         })
         .await;
-    layer.link(label_node.clone());
+    btn_layer.link(label_node);
 
     // Button
-    let btn = create_button(name);
-    btn.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let btn = create_button(&format!("{name}{suffix}"));
+    btn.set_property_bool(atom, Role::App, "is_active", is_active).unwrap();
     let prop = btn.get_property("rect").unwrap();
-    prop.set_default_f32(0, PADDING_X).unwrap();
-    let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
-    prop.set_default_expr(1, code).unwrap();
-    let code = cc.compile("w - PADDING_X * 2.").unwrap();
-    prop.set_default_expr(2, code).unwrap();
-    prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
+    prop.set_default_f32(0, 0.).unwrap();
+    prop.set_default_f32(1, 0.).unwrap();
+    prop.set_default_expr(2, expr::load_var("w")).unwrap();
+    prop.set_default_expr(3, expr::load_var("h")).unwrap();
 
     let btn =
         btn.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
-    layer.link(btn.clone());
+    btn_layer.link(btn);
 
-    (btn, bg_valid, bg_invalid, label_node)
+    btn_layer
 }
 
 /// Create tooltip component (with auto-hide)

+ 8 - 16
bin/app/src/main.rs

@@ -685,28 +685,20 @@ async fn load_plugins(
                 }
 
                 // Reset step4 send button to disabled state
-                if let Some(send_label_node) =
-                    sg_root2.lookup_node("/window/content/wallet/send_step4_layer/send_btn_label")
+                if let Some(send_layer_node) =
+                    sg_root2.lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer")
                 {
-                    send_label_node.set_property_str(atom, Role::App, "text", "send").unwrap();
-                    let prop = send_label_node.get_property("text_color").unwrap();
-                    prop.set_f32(atom, Role::App, 0, 0.5).unwrap();
-                    prop.set_f32(atom, Role::App, 1, 0.5).unwrap();
-                    prop.set_f32(atom, Role::App, 2, 0.5).unwrap();
-                    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+                    send_layer_node
+                        .set_property_bool(atom, Role::App, "is_visible", false)
+                        .unwrap();
                 }
-                if let Some(send_bg_grey_node) =
-                    sg_root2.lookup_node("/window/content/wallet/send_step4_layer/send_btn_bg_grey")
+                if let Some(send_layer_grey_node) = sg_root2
+                    .lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer_grey")
                 {
-                    send_bg_grey_node
+                    send_layer_grey_node
                         .set_property_bool(atom, Role::App, "is_visible", true)
                         .unwrap();
                 }
-                if let Some(send_bg_node) =
-                    sg_root2.lookup_node("/window/content/wallet/send_step4_layer/send_btn_bg")
-                {
-                    send_bg_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
-                }
 
                 // Go back to step3
                 if let Some(step4) = sg_root2.lookup_node("/window/content/wallet/send_step4_layer")